diff --git a/editor/editor.pro b/editor/editor.pro index 26adbfe0b1..1d44a39150 100644 --- a/editor/editor.pro +++ b/editor/editor.pro @@ -11,6 +11,7 @@ include($$ROOT_DIR/common.pri) SOURCES += \ changeset_wrapper.cpp \ editor_config.cpp \ + editor_notes.cpp \ opening_hours_ui.cpp \ osm_auth.cpp \ osm_feature_matcher.cpp \ @@ -21,6 +22,7 @@ SOURCES += \ HEADERS += \ changeset_wrapper.hpp \ editor_config.hpp \ + editor_notes.hpp \ new_feature_categories.hpp \ opening_hours_ui.hpp \ osm_auth.hpp \ diff --git a/editor/editor_notes.cpp b/editor/editor_notes.cpp new file mode 100644 index 0000000000..96cc830d54 --- /dev/null +++ b/editor/editor_notes.cpp @@ -0,0 +1,139 @@ +#include "editor/editor_notes.hpp" + +#include "platform/platform.hpp" + +#include "coding/internal/file_data.hpp" + +#include "base/string_utils.hpp" +#include "base/assert.hpp" +#include "base/logging.hpp" +#include "base/timer.hpp" + +#include "3party/pugixml/src/pugixml.hpp" + +namespace +{ +bool LoadFromXml(pugi::xml_document const & xml, + vector & notes, + uint32_t & uploadedNotesCount) +{ + uint64_t notesCount; + auto const root = xml.child("notes"); + if (!strings::to_uint64(root.attribute("count").value(), notesCount)) + return false; + uploadedNotesCount = static_cast(notesCount); + for (auto const xNode : root.select_nodes("note")) + { + m2::PointD point; + + auto const node = xNode.node(); + auto const point_x = node.attribute("x"); + if (!point_x || !strings::to_double(point_x.value(), point.x)) + return false; + + auto const point_y = node.attribute("y"); + if (!point_y || !strings::to_double(point_y.value(), point.y)) + return false; + + auto const text = node.attribute("text"); + if (!text) + return false; + + notes.emplace_back(point, text.value()); + } + return true; +} + +void SaveToXml(vector const & notes, + pugi::xml_document & xml, + uint32_t const UploadedNotesCount) +{ + auto root = xml.append_child("notes"); + root.append_attribute("count") = UploadedNotesCount; + for (auto const & note : notes) + { + auto node = root.append_child("note"); + node.append_attribute("x") = DebugPrint(note.m_point.x).data(); + node.append_attribute("y") = DebugPrint(note.m_point.y).data(); + node.append_attribute("text") = note.m_note.data(); + } +} +} // namespace + +namespace editor +{ +Notes::Notes(string const & fileName) + : m_fileName(fileName) +{ + Load(); +} + +void Notes::CreateNote(m2::PointD const & point, string const & text) +{ + lock_guard g(m_mu); + m_notes.emplace_back(point, text); + Save(); +} + +void Notes::Upload() +{ + throw "NotImplemented"; +} + +bool Notes::Load() +{ + string content; + try + { + auto const reader = GetPlatform().GetReader(m_fileName); + reader->ReadAsString(content); + } + catch (FileAbsentException const &) + { + LOG(LINFO, ("No edits file.")); + return true; + } + catch (Reader::Exception const &) + { + LOG(LERROR, ("Can't process file.", m_fileName)); + return false; + } + + pugi::xml_document xml; + if (!xml.load_buffer(content.data(), content.size())) + { + LOG(LERROR, ("Can't load notes, xml is illformed.")); + return false; + } + + lock_guard g(m_mu); + m_notes.clear(); + if (!LoadFromXml(xml, m_notes, m_uploadedNotes)) + { + LOG(LERROR, ("Can't load notes, file is illformed.")); + return false; + } + + return true; +} + +/// Not thread-safe, use syncronization. +bool Notes::Save() +{ + pugi::xml_document xml; + SaveToXml(m_notes, xml, m_uploadedNotes); + + string const tmpFileName = m_fileName + ".tmp"; + if (!xml.save_file(tmpFileName.data(), " ")) + { + LOG(LERROR, ("Can't save map edits into", tmpFileName)); + return false; + } + else if (!my::RenameFileX(tmpFileName, m_fileName)) + { + LOG(LERROR, ("Can't rename file", tmpFileName, "to", m_fileName)); + return false; + } + return true; +} +} diff --git a/editor/editor_notes.hpp b/editor/editor_notes.hpp new file mode 100644 index 0000000000..43d12c3430 --- /dev/null +++ b/editor/editor_notes.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include "geometry/point2d.hpp" + +#include "std/mutex.hpp" +#include "std/string.hpp" +#include "std/vector.hpp" + +namespace editor +{ +struct Note +{ + Note(m2::PointD const & point, string const & text) + : m_point(point), + m_note(text) + { + } + + m2::PointD m_point; + string m_note; +}; + +inline bool operator==(Note const & a, Note const & b) +{ + return a.m_point == b.m_point && b.m_note == b.m_note; +} + +class Notes +{ +public: + Notes(string const & fileName); + + void CreateNote(m2::PointD const & point, string const & text); + void Upload(); + + vector GetNotes() const { return m_notes; } + + uint32_t UnuploadedNotesCount() const { return m_notes.size(); } + uint32_t UploadedNotesCount() const { return m_uploadedNotes; } + +private: + bool Load(); + bool Save(); + + string const m_fileName; + mutable mutex m_mu; + + vector m_notes; + + uint32_t m_uploadedNotes; +}; +} // namespace diff --git a/editor/editor_tests/editor_notes_test.cpp b/editor/editor_tests/editor_notes_test.cpp new file mode 100644 index 0000000000..ee493e6a7c --- /dev/null +++ b/editor/editor_tests/editor_notes_test.cpp @@ -0,0 +1,40 @@ +#include "testing/testing.hpp" + +#include "editor/editor_notes.hpp" + +#include "platform/platform_tests_support/scoped_file.hpp" + +using namespace editor; + +namespace editor +{ +string DebugPrint(Note const & note) +{ + return "Note(" + DebugPrint(note.m_point) + ", \"" + note.m_note + "\")"; +} +} // namespace editor + +UNIT_TEST(Notes) +{ + auto const fileName = "notes.xml"; + auto const fullFileName = Platform::PathJoin({GetPlatform().WritableDir(), + fileName}); + platform::tests_support::ScopedFile sf(fileName); + { + Notes notes(fullFileName); + notes.CreateNote({1, 2}, "Some note1"); + notes.CreateNote({2, 2}, "Some note2"); + notes.CreateNote({1, 1}, "Some note3"); + } + { + Notes notes(fullFileName); + auto const result = notes.GetNotes(); + TEST_EQUAL(result.size(), 3, ()); + TEST_EQUAL(result, + (vector{ + {{1, 2}, "Some note1"}, + {{2, 2}, "Some note2"}, + {{1, 1}, "Some note3"} + }), ()); + } +} diff --git a/editor/editor_tests/editor_tests.pro b/editor/editor_tests/editor_tests.pro index 93a6ee6426..cc7090c2d2 100644 --- a/editor/editor_tests/editor_tests.pro +++ b/editor/editor_tests/editor_tests.pro @@ -15,6 +15,7 @@ HEADERS += \ SOURCES += \ $$ROOT_DIR/testing/testingmain.cpp \ editor_config_test.cpp \ + editor_notes_test.cpp \ opening_hours_ui_test.cpp \ osm_auth_test.cpp \ osm_feature_matcher_test.cpp \ diff --git a/platform/platform.cpp b/platform/platform.cpp index 2bc3a102f7..d3993614de 100644 --- a/platform/platform.cpp +++ b/platform/platform.cpp @@ -8,6 +8,7 @@ #include "coding/writer.hpp" #include "base/logging.hpp" +#include "base/string_utils.hpp" #include "std/target_os.hpp" #include "std/thread.hpp" @@ -93,6 +94,16 @@ bool Platform::RmDirRecursively(string const & dirName) return res; } +string Platform::PathJoin(vector const & parts) +{ +#ifdef OMIM_OS_WINDOWS + auto const delimiter = "\\"; +#else + auto const delimiter = "/"; +#endif + return strings::JoinStrings(parts, delimiter); +} + string Platform::ReadPathForFile(string const & file, string searchScope) const { if (searchScope.empty()) diff --git a/platform/platform.hpp b/platform/platform.hpp index a7575035de..6daace189e 100644 --- a/platform/platform.hpp +++ b/platform/platform.hpp @@ -115,7 +115,7 @@ public: /// @note If function fails, directory can be partially removed. static bool RmDirRecursively(string const & dirName); - /// @TODO create join method for string concatenation + static string PathJoin(vector const & parts); /// @return path for directory with temporary files with slash at the end string TmpDir() const { return m_tmpDir; } diff --git a/platform/platform_tests/platform_test.cpp b/platform/platform_tests/platform_test.cpp index d791c14fb7..70f8eda357 100644 --- a/platform/platform_tests/platform_test.cpp +++ b/platform/platform_tests/platform_test.cpp @@ -258,3 +258,14 @@ UNIT_TEST(IsSingleMwm) TEST(!version::IsSingleMwm(version::FOR_TESTING_TWO_COMPONENT_MWM1), ()); TEST(!version::IsSingleMwm(version::FOR_TESTING_TWO_COMPONENT_MWM2), ()); } + +UNIT_TEST(PathJoin) +{ +#ifdef OMIM_OS_WINDOWS + TEST_EQUAL("foo\\bar", Platform::PathJoin({"foo", "bar"}), ()); + TEST_EQUAL("foo\\bar\\baz", Platform::PathJoin({"foo", "bar", "baz"}), ()); +#else + TEST_EQUAL("foo/bar", Platform::PathJoin({"foo", "bar"}), ()); + TEST_EQUAL("foo/bar/baz", Platform::PathJoin({"foo", "bar", "baz"}), ()); +#endif +} diff --git a/platform/platform_tests_support/scoped_file.cpp b/platform/platform_tests_support/scoped_file.cpp index 111ff7a24e..68e23ebfc1 100644 --- a/platform/platform_tests_support/scoped_file.cpp +++ b/platform/platform_tests_support/scoped_file.cpp @@ -18,8 +18,14 @@ namespace platform { namespace tests_support { +ScopedFile::ScopedFile(string const & relativePath) + : m_fullPath(my::JoinFoldersToPath(GetPlatform().WritableDir(), relativePath)), + m_reset(false) +{ +} + ScopedFile::ScopedFile(string const & relativePath, string const & contents) - : m_fullPath(my::JoinFoldersToPath(GetPlatform().WritableDir(), relativePath)), m_reset(false) + : ScopedFile(relativePath) { { FileWriter writer(GetFullPath()); diff --git a/platform/platform_tests_support/scoped_file.hpp b/platform/platform_tests_support/scoped_file.hpp index f55e0722c9..a326c595af 100644 --- a/platform/platform_tests_support/scoped_file.hpp +++ b/platform/platform_tests_support/scoped_file.hpp @@ -18,6 +18,7 @@ class ScopedDir; class ScopedFile { public: + ScopedFile(string const & relativePath); ScopedFile(string const & relativePath, string const & contents); ScopedFile(ScopedDir const & dir, CountryFile const & countryFile, MapOptions file,