diff --git a/editor/editor.pro b/editor/editor.pro index 79be0528bc..40cc940436 100644 --- a/editor/editor.pro +++ b/editor/editor.pro @@ -19,6 +19,7 @@ SOURCES += \ ui2oh.cpp \ user_stats.cpp \ xml_feature.cpp \ + editor_storage.cpp HEADERS += \ changeset_wrapper.hpp \ @@ -32,3 +33,4 @@ HEADERS += \ user_stats.hpp \ xml_feature.hpp \ yes_no_unknown.hpp \ + editor_storage.hpp diff --git a/editor/editor_storage.cpp b/editor/editor_storage.cpp new file mode 100644 index 0000000000..96dc771b32 --- /dev/null +++ b/editor/editor_storage.cpp @@ -0,0 +1,71 @@ +#include "editor/editor_storage.hpp" + +#include "platform/platform.hpp" + +#include "coding/internal/file_data.hpp" + +#include "base/logging.hpp" + +#include "std/string.hpp" + +#include "3party/pugixml/src/pugixml.hpp" + +using namespace pugi; + +namespace +{ +constexpr char const * kEditorXMLFileName = "edits.xml"; +string GetEditorFilePath() { return GetPlatform().WritablePathForFile(kEditorXMLFileName); } +} // namespace + +namespace editor +{ +bool StorageLocal::Save(xml_document const & doc) +{ + auto const editorFilePath = GetEditorFilePath(); + return my::WriteToTempAndRenameToFile(editorFilePath, [&doc](string const & fileName) { + return doc.save_file(fileName.data(), " "); + }); +} + +bool StorageLocal::Load(xml_document & doc) +{ + auto const editorFilePath = GetEditorFilePath(); + auto const result = doc.load_file(editorFilePath.c_str()); + // Note: status_file_not_found is ok if user has never made any edits. + if (result != status_ok && result != status_file_not_found) + { + LOG(LERROR, ("Can't load map edits from disk:", editorFilePath)); + return false; + } + + return true; +} + +void StorageLocal::Reset() +{ + my::DeleteFileX(GetEditorFilePath()); +} + + +StorageMemory::StorageMemory() + : m_doc(make_unique ()) +{} + +bool StorageMemory::Save(xml_document const & doc) +{ + m_doc->reset(doc); + return true; +} + +bool StorageMemory::Load(xml_document & doc) +{ + doc.reset(*m_doc); + return true; +} + +void StorageMemory::Reset() +{ + m_doc->reset(); +} +} // namespace editor diff --git a/editor/editor_storage.hpp b/editor/editor_storage.hpp new file mode 100644 index 0000000000..d712b6b53c --- /dev/null +++ b/editor/editor_storage.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include "std/unique_ptr.hpp" + +namespace pugi +{ +class xml_document; +} + +namespace editor +{ +// Editor storage interface. +class StorageBase +{ +public: + virtual ~StorageBase() {} + virtual bool Save(pugi::xml_document const & doc) = 0; + virtual bool Load(pugi::xml_document & doc) = 0; + virtual void Reset() = 0; +}; + +// Class which save/load edits to/from local file. +class StorageLocal : public StorageBase +{ +public: + bool Save(pugi::xml_document const & doc) override; + bool Load(pugi::xml_document & doc) override; + void Reset() override; +}; + +// Class which save/load edits to/from xml_document class instance. +class StorageMemory : public StorageBase +{ +public: + StorageMemory(); + bool Save(pugi::xml_document const & doc) override; + bool Load(pugi::xml_document & doc) override; + void Reset() override; + +private: + unique_ptr m_doc; +}; +} diff --git a/indexer/indexer_tests/indexer_tests.pro b/indexer/indexer_tests/indexer_tests.pro index 4eaa571965..7aa071bdd9 100644 --- a/indexer/indexer_tests/indexer_tests.pro +++ b/indexer/indexer_tests/indexer_tests.pro @@ -4,8 +4,9 @@ CONFIG -= app_bundle TEMPLATE = app ROOT_DIR = ../.. -DEPENDENCIES = indexer platform editor geometry coding base protobuf tomcrypt \ - succinct opening_hours pugixml stats_client \ +DEPENDENCIES = base indexer platform editor geometry coding map search storage \ + routing generator tess2 protobuf tomcrypt succinct opening_hours \ + pugixml stats_client jansson generator_tests_support search_tests_support include($$ROOT_DIR/common.pri) @@ -14,6 +15,7 @@ QT *= core HEADERS += \ test_mwm_set.hpp \ test_polylines.hpp \ + osm_editor_test.hpp SOURCES += \ ../../testing/testingmain.cpp \ @@ -46,3 +48,4 @@ SOURCES += \ test_type.cpp \ trie_test.cpp \ visibility_test.cpp \ + osm_editor_test.cpp diff --git a/indexer/indexer_tests/osm_editor_test.cpp b/indexer/indexer_tests/osm_editor_test.cpp new file mode 100644 index 0000000000..b3ee4e5956 --- /dev/null +++ b/indexer/indexer_tests/osm_editor_test.cpp @@ -0,0 +1,617 @@ +#include "indexer/indexer_tests/osm_editor_test.hpp" +#include "testing/testing.hpp" + +#include "search/reverse_geocoder.hpp" + +#include "indexer/classificator.hpp" +#include "indexer/feature_algo.hpp" +#include "indexer/ftypes_matcher.hpp" +#include "indexer/osm_editor.hpp" +#include "indexer/scales.hpp" + +#include "editor/editor_storage.hpp" + +using namespace generator::tests_support; + +namespace +{ +class IsCafeChecker : public ftypes::BaseChecker +{ + IsCafeChecker() + { + Classificator const & c = classif(); + m_types.push_back(c.GetTypeByPath({"amenity", "cafe"})); + } + +public: + static IsCafeChecker const & Instance() + { + static const IsCafeChecker instance; + return instance; + } +}; + +class TestCafe : public TestPOI +{ +public: + TestCafe(m2::PointD const & center, string const & name, string const & lang) + : TestPOI(center, name, lang) + { + SetTypes({{"amenity", "cafe"}}); + } +}; + +template +void ForEachCafeAtPoint(model::FeaturesFetcher & model, m2::PointD const & mercator, func && fn) +{ + m2::RectD const rect = MercatorBounds::RectByCenterXYAndSizeInMeters(mercator, 0.2 /* rect width */); + model.ForEachFeature(rect, [&](FeatureType & ft) + { + if (IsCafeChecker::Instance()(ft)) + { + fn(ft); + } + }, scales::GetUpperScale()); +} + +using TFeatureTypeFn = function; +void ForEachFeatureAtPoint(model::FeaturesFetcher & model, TFeatureTypeFn && fn, m2::PointD const & mercator) +{ + constexpr double kSelectRectWidthInMeters = 1.1; + constexpr double kMetersToLinearFeature = 3; + constexpr int kScale = scales::GetUpperScale(); + m2::RectD const rect = MercatorBounds::RectByCenterXYAndSizeInMeters(mercator, kSelectRectWidthInMeters); + model.ForEachFeature(rect, [&](FeatureType & ft) + { + switch (ft.GetFeatureType()) + { + case feature::GEOM_POINT: + if (rect.IsPointInside(ft.GetCenter())) + fn(ft); + break; + case feature::GEOM_LINE: + if (feature::GetMinDistanceMeters(ft, mercator) < kMetersToLinearFeature) + fn(ft); + break; + case feature::GEOM_AREA: + { + auto limitRect = ft.GetLimitRect(kScale); + // Be a little more tolerant. When used by editor mercator is given + // with some error, so we must extend limit rect a bit. + limitRect.Inflate(MercatorBounds::GetCellID2PointAbsEpsilon(), + MercatorBounds::GetCellID2PointAbsEpsilon()); + // Due to floating points accuracy issues (geometry is saved in editor up to 7 digits + // after dicimal poin) some feature vertexes are threated as external to a given feature. + constexpr double kFeatureDistanceToleranceInMeters = 1e-2; + if (limitRect.IsPointInside(mercator) && + feature::GetMinDistanceMeters(ft, mercator) <= kFeatureDistanceToleranceInMeters) + { + fn(ft); + } + } + break; + case feature::GEOM_UNDEFINED: + ASSERT(false, ("case feature::GEOM_UNDEFINED")); + break; + } + }, kScale); +} + +void FillEditableMapObject(osm::Editor const & editor, FeatureType const & ft, osm::EditableMapObject & emo) +{ + emo.SetFromFeatureType(ft); + emo.SetHouseNumber(ft.GetHouseNumber()); + emo.SetEditableProperties(editor.GetEditableProperties(ft)); +} +} // namespace + +namespace tests +{ +EditorTest::EditorTest() +{ + m_model.InitClassificator(); + m_infoGetter = make_unique(); + InitEditorForTest(); +} + +EditorTest::~EditorTest() +{ + for (auto const & file : m_files) + Cleanup(file); +} + +void EditorTest::GetFeatureTypeInfoTest() +{ + auto & editor = osm::Editor::Instance(); + + { + MwmSet::MwmId mwmId; + TEST(!editor.GetFeatureTypeInfo(mwmId, 0), ()); + } + + TestCafe cafe(m2::PointD(1.0, 1.0), "London Cafe", "en"); + auto const mwmId = ConstructTestMwm([&](TestMwmBuilder & builder) + { + builder.Add(cafe); + }); + ASSERT(mwmId.GetInfo(), ()); + + ForEachCafeAtPoint(m_model, m2::PointD(1.0, 1.0), [&](FeatureType & ft) + { + TEST(!editor.GetFeatureTypeInfo(mwmId, ft.GetID().m_index), ()); + + osm::EditableMapObject emo; + FillEditableMapObject(editor, ft, emo); + emo.SetBuildingLevels("1"); + editor.SaveEditedFeature(emo); + + auto const fti = editor.GetFeatureTypeInfo(mwmId, ft.GetID().m_index); + TEST_EQUAL(fti->m_feature.GetID(), ft.GetID(), ()); + }); +} + +void EditorTest::GetEditedFeatureTest() +{ + auto & editor = osm::Editor::Instance(); + + { + FeatureID feature; + FeatureType ft; + TEST(!editor.GetEditedFeature(feature, ft), ()); + } + + TestCafe cafe(m2::PointD(1.0, 1.0), "London Cafe", "en"); + auto const mwmId = ConstructTestMwm([&](TestMwmBuilder & builder) + { + builder.Add(cafe); + }); + ASSERT(mwmId.GetInfo(), ()); + + ForEachCafeAtPoint(m_model, m2::PointD(1.0, 1.0), [&](FeatureType & ft) + { + FeatureType featureType; + TEST(!editor.GetEditedFeature(ft.GetID(), featureType), ()); + + osm::EditableMapObject emo; + FillEditableMapObject(editor, ft, emo); + emo.SetBuildingLevels("1"); + editor.SaveEditedFeature(emo); + + TEST(editor.GetEditedFeature(ft.GetID(), featureType), ()); + TEST_EQUAL(ft.GetID(), featureType.GetID(), ()); + }); +} + +void EditorTest::GetEditedFeatureStreetTest() +{ + auto & editor = osm::Editor::Instance(); + + { + FeatureID feature; + string street; + TEST(!editor.GetEditedFeatureStreet(feature, street), ()); + } + + TestCafe cafe(m2::PointD(1.0, 1.0), "London Cafe", "en"); + auto const mwmId = ConstructTestMwm([&](TestMwmBuilder & builder) + { + builder.Add(cafe); + }); + ASSERT(mwmId.GetInfo(), ()); + + ForEachCafeAtPoint(m_model, m2::PointD(1.0, 1.0), [&](FeatureType & ft) + { + string street; + TEST(!editor.GetEditedFeatureStreet(ft.GetID(), street), ()); + + osm::EditableMapObject emo; + FillEditableMapObject(editor, ft, emo); + osm::LocalizedStreet ls{"some street", ""}; + emo.SetStreet(ls); + editor.SaveEditedFeature(emo); + + TEST(editor.GetEditedFeatureStreet(ft.GetID(), street), ()); + TEST_EQUAL(street, ls.m_defaultName, ()); + }); +} + +void EditorTest::OriginalFeatureHasDefaultNameTest() +{ + auto & editor = osm::Editor::Instance(); + + TestCafe cafe(m2::PointD(1.0, 1.0), "London Cafe", "en"); + TestCafe unnamedCafe(m2::PointD(2.0, 2.0), "", "en"); + TestCafe secondUnnamedCafe(m2::PointD(3.0, 3.0), "", "en"); + + auto const mwmId = ConstructTestMwm([&](TestMwmBuilder & builder) + { + builder.Add(cafe); + builder.Add(unnamedCafe); + builder.Add(secondUnnamedCafe); + }); + ASSERT(mwmId.GetInfo(), ()); + + ForEachCafeAtPoint(m_model, m2::PointD(1.0, 1.0), [&](FeatureType & ft) + { + TEST(editor.OriginalFeatureHasDefaultName(ft.GetID()), ()); + }); + + ForEachCafeAtPoint(m_model, m2::PointD(2.0, 2.0), [&](FeatureType & ft) + { + TEST(!editor.OriginalFeatureHasDefaultName(ft.GetID()), ()); + }); + + ForEachCafeAtPoint(m_model, m2::PointD(3.0, 3.0), [&](FeatureType & ft) + { + osm::EditableMapObject emo; + FillEditableMapObject(editor, ft, emo); + + StringUtf8Multilang names; + names.AddString(StringUtf8Multilang::GetLangIndex("en"), "Eng name"); + names.AddString(StringUtf8Multilang::GetLangIndex("default"), "Default name"); + emo.SetName(names); + + editor.SaveEditedFeature(emo); + + TEST(!editor.OriginalFeatureHasDefaultName(ft.GetID()), ()); + }); +} + +void EditorTest::GetFeatureStatusTest() +{ + auto & editor = osm::Editor::Instance(); + + TestCafe cafe(m2::PointD(1.0, 1.0), "London Cafe", "en"); + TestCafe unnamedCafe(m2::PointD(2.0, 2.0), "", "en"); + + auto const mwmId = ConstructTestMwm([&](TestMwmBuilder & builder) + { + builder.Add(cafe); + builder.Add(unnamedCafe); + }); + + ASSERT(mwmId.GetInfo(), ()); + + ForEachCafeAtPoint(m_model, m2::PointD(1.0, 1.0), [&](FeatureType & ft) + { + TEST_EQUAL(editor.GetFeatureStatus(ft.GetID()), osm::Editor::FeatureStatus::Untouched, ()); + + osm::EditableMapObject emo; + FillEditableMapObject(editor, ft, emo); + emo.SetBuildingLevels("1"); + editor.SaveEditedFeature(emo); + + TEST_EQUAL(editor.GetFeatureStatus(ft.GetID()), osm::Editor::FeatureStatus::Modified, ()); + editor.MarkFeatureAsObsolete(emo.GetID()); + TEST_EQUAL(editor.GetFeatureStatus(emo.GetID()), osm::Editor::FeatureStatus::Obsolete, ()); + }); + + ForEachCafeAtPoint(m_model, m2::PointD(2.0, 2.0), [&](FeatureType & ft) + { + TEST_EQUAL(editor.GetFeatureStatus(ft.GetID()), osm::Editor::FeatureStatus::Untouched, ()); + editor.DeleteFeature(ft); + TEST_EQUAL(editor.GetFeatureStatus(ft.GetID()), osm::Editor::FeatureStatus::Deleted, ()); + }); + + osm::EditableMapObject emo; + editor.CreatePoint(classif().GetTypeByPath({"amenity", "cafe"}), {3.0, 3.0}, mwmId, emo); + emo.SetHouseNumber("12"); + editor.SaveEditedFeature(emo); + + TEST_EQUAL(editor.GetFeatureStatus(emo.GetID()), osm::Editor::FeatureStatus::Created, ()); +} + +void EditorTest::IsFeatureUploadedTest() +{ + auto & editor = osm::Editor::Instance(); + + TestCafe cafe(m2::PointD(1.0, 1.0), "London Cafe", "en"); + + auto const mwmId = ConstructTestMwm([&](TestMwmBuilder & builder) + { + builder.Add(cafe); + }); + + ASSERT(mwmId.GetInfo(), ()); + + ForEachCafeAtPoint(m_model, m2::PointD(1.0, 1.0), [&](FeatureType & ft) + { + TEST(!editor.IsFeatureUploaded(ft.GetID().m_mwmId, ft.GetID().m_index), ()); + }); + + osm::EditableMapObject emo; + editor.CreatePoint(classif().GetTypeByPath({"amenity", "cafe"}), {3.0, 3.0}, mwmId, emo); + emo.SetHouseNumber("12"); + editor.SaveEditedFeature(emo); + + TEST(!editor.IsFeatureUploaded(emo.GetID().m_mwmId, emo.GetID().m_index), ()); + + pugi::xml_document doc; + pugi::xml_node root = doc.append_child("mapsme"); + root.append_attribute("format_version") = 1; + + pugi::xml_node mwmNode = root.append_child("mwm"); + mwmNode.append_attribute("name") = mwmId.GetInfo()->GetCountryName().c_str(); + mwmNode.append_attribute("version") = static_cast(mwmId.GetInfo()->GetVersion()); + pugi::xml_node created = mwmNode.append_child("create"); + + FeatureType ft; + editor.GetEditedFeature(emo.GetID().m_mwmId, emo.GetID().m_index, ft); + + editor::XMLFeature xf = ft.ToXML(true); + xf.SetMWMFeatureIndex(ft.GetID().m_index); + xf.SetModificationTime(time(nullptr)); + xf.SetUploadStatus("Uploaded"); + xf.AttachToParentNode(created); + editor.m_storage->Save(doc); + editor.LoadMapEdits(); + + TEST(editor.IsFeatureUploaded(emo.GetID().m_mwmId, emo.GetID().m_index), ()); +} + +void EditorTest::DeleteFeatureTest() +{ + auto & editor = osm::Editor::Instance(); + TestCafe cafe(m2::PointD(1.0, 1.0), "London Cafe", "en"); + + auto const mwmId = ConstructTestMwm([&](TestMwmBuilder & builder) + { + builder.Add(cafe); + }); + + ASSERT(mwmId.GetInfo(), ()); + + osm::EditableMapObject emo; + editor.CreatePoint(classif().GetTypeByPath({"amenity", "cafe"}), {3.0, 3.0}, mwmId, emo); + emo.SetHouseNumber("12"); + editor.SaveEditedFeature(emo); + + FeatureType ft; + editor.GetEditedFeature(emo.GetID().m_mwmId, emo.GetID().m_index, ft); + editor.DeleteFeature(ft); + + TEST_EQUAL(editor.GetFeatureStatus(ft.GetID()), osm::Editor::FeatureStatus::Untouched, ()); + + ForEachCafeAtPoint(m_model, m2::PointD(1.0, 1.0), [&](FeatureType & ft) + { + editor.DeleteFeature(ft); + TEST_EQUAL(editor.GetFeatureStatus(ft.GetID()), osm::Editor::FeatureStatus::Deleted, ()); + }); +} + +void EditorTest::ClearAllLocalEditsTest() +{ + auto & editor = osm::Editor::Instance(); + + TestCafe cafe(m2::PointD(1.0, 1.0), "London Cafe", "en"); + + auto const mwmId = ConstructTestMwm([&](TestMwmBuilder & builder) + { + builder.Add(cafe); + }); + ASSERT(mwmId.GetInfo(), ()); + + osm::EditableMapObject emo; + editor.CreatePoint(classif().GetTypeByPath({"amenity", "cafe"}), {3.0, 3.0}, mwmId, emo); + + editor.SaveEditedFeature(emo); + + TEST(!editor.m_features.empty(), ()); + editor.ClearAllLocalEdits(); + TEST(editor.m_features.empty(), ()); +} + +void EditorTest::GetFeaturesByStatusTest() +{ + auto & editor = osm::Editor::Instance(); + + { + MwmSet::MwmId mwmId; + auto const features = editor.GetFeaturesByStatus(mwmId, osm::Editor::FeatureStatus::Untouched); + TEST(features.empty(), ()); + } + + TestCafe cafe(m2::PointD(1.0, 1.0), "London Cafe", "en"); + TestCafe unnamedCafe(m2::PointD(2.0, 2.0), "", "en"); + TestCafe someCafe(m2::PointD(3.0, 3.0), "Some cafe", "en"); + + auto const mwmId = ConstructTestMwm([&](TestMwmBuilder & builder) + { + builder.Add(cafe); + builder.Add(unnamedCafe); + builder.Add(someCafe); + }); + + ASSERT(mwmId.GetInfo(), ()); + + FeatureID modifiedId, deletedId, obsoleteId, createdId; + + ForEachCafeAtPoint(m_model, m2::PointD(1.0, 1.0), [&](FeatureType & ft) + { + osm::EditableMapObject emo; + FillEditableMapObject(editor, ft, emo); + emo.SetBuildingLevels("1"); + editor.SaveEditedFeature(emo); + + modifiedId = emo.GetID(); + }); + + ForEachCafeAtPoint(m_model, m2::PointD(2.0, 2.0), [&](FeatureType & ft) + { + editor.DeleteFeature(ft); + deletedId = ft.GetID(); + }); + + ForEachCafeAtPoint(m_model, m2::PointD(3.0, 3.0), [&](FeatureType & ft) + { + editor.MarkFeatureAsObsolete(ft.GetID()); + obsoleteId = ft.GetID(); + }); + + osm::EditableMapObject emo; + editor.CreatePoint(classif().GetTypeByPath({"amenity", "cafe"}), {4.0, 4.0}, mwmId, emo); + emo.SetHouseNumber("12"); + editor.SaveEditedFeature(emo); + createdId = emo.GetID(); + + auto const modified = editor.GetFeaturesByStatus(mwmId, osm::Editor::FeatureStatus::Modified); + auto const deleted = editor.GetFeaturesByStatus(mwmId, osm::Editor::FeatureStatus::Deleted); + auto const obsolete = editor.GetFeaturesByStatus(mwmId, osm::Editor::FeatureStatus::Obsolete); + auto const created = editor.GetFeaturesByStatus(mwmId, osm::Editor::FeatureStatus::Created); + + TEST_EQUAL(modified.size(), 1, ()); + TEST_EQUAL(deleted.size(), 1, ()); + TEST_EQUAL(obsolete.size(), 1, ()); + TEST_EQUAL(created.size(), 1, ()); + + TEST_EQUAL(modified.front(), modifiedId.m_index, ()); + TEST_EQUAL(deleted.front(), deletedId.m_index, ()); + TEST_EQUAL(obsolete.front(), obsoleteId.m_index, ()); + TEST_EQUAL(created.front(), createdId.m_index, ()); +} + +void EditorTest::OnMapDeregisteredTest() +{ + auto & editor = osm::Editor::Instance(); + + TestCity london(m2::PointD(1.0, 1.0), "London", "en", 100 /* rank */); + TestCity moscow(m2::PointD(2.0, 2.0), "Moscow", "ru", 100 /* rank */); + BuildMwm("TestWorld", feature::DataHeader::world,[&](TestMwmBuilder & builder) + { + builder.Add(london); + builder.Add(moscow); + }); + + TestCafe cafeLondon(m2::PointD(1.0, 1.0), "London Cafe", "en"); + auto const gbMwmId = BuildMwm("GB", feature::DataHeader::country, [&](TestMwmBuilder & builder) + { + builder.Add(cafeLondon); + }); + ASSERT(gbMwmId.GetInfo(), ()); + + TestCafe cafeMoscow(m2::PointD(2.0, 2.0), "Moscow Cafe", "ru"); + auto const rfMwmId = BuildMwm("RF", feature::DataHeader::country, [&](TestMwmBuilder & builder) + { + builder.Add(cafeMoscow); + }); + ASSERT(rfMwmId.GetInfo(), ()); + + ForEachCafeAtPoint(m_model, m2::PointD(1.0, 1.0), [&](FeatureType & ft) + { + osm::EditableMapObject emo; + FillEditableMapObject(editor, ft, emo); + emo.SetBuildingLevels("1"); + editor.SaveEditedFeature(emo); + }); + + ForEachCafeAtPoint(m_model, m2::PointD(2.0, 2.0), [&](FeatureType & ft) + { + osm::EditableMapObject emo; + FillEditableMapObject(editor, ft, emo); + emo.SetBuildingLevels("1"); + editor.SaveEditedFeature(emo); + }); + + TEST(!editor.m_features.empty(), ()); + TEST_EQUAL(editor.m_features.size(), 2, ()); + + editor.OnMapDeregistered(gbMwmId.GetInfo()->GetLocalFile()); + + TEST_EQUAL(editor.m_features.size(), 1, ()); + auto const editedMwmId = editor.m_features.find(rfMwmId); + bool result = (editedMwmId != editor.m_features.end()); + TEST(result, ()); +} + +void EditorTest::Cleanup(platform::LocalCountryFile const & map) +{ + platform::CountryIndexes::DeleteFromDisk(map); + map.DeleteFromDisk(MapOptions::Map); +} + +void EditorTest::InitEditorForTest() +{ + auto & editor = osm::Editor::Instance(); + + editor.m_storage = make_unique(); + editor.ClearAllLocalEdits(); + + editor.SetMwmIdByNameAndVersionFn([this](string const & name) -> MwmSet::MwmId + { + return m_model.GetIndex().GetMwmIdByCountryFile(platform::CountryFile(name)); + }); + + editor.SetFeatureLoaderFn([this](FeatureID const & fid) -> unique_ptr + { + unique_ptr feature(new FeatureType()); + Index::FeaturesLoaderGuard const guard(m_model.GetIndex(), fid.m_mwmId); + if (!guard.GetOriginalFeatureByIndex(fid.m_index, *feature)) + return nullptr; + feature->ParseEverything(); + return feature; + }); + + editor.SetFeatureOriginalStreetFn([this](FeatureType & ft) -> string + { + search::ReverseGeocoder const coder(m_model.GetIndex()); + auto const streets = coder.GetNearbyFeatureStreets(ft); + if (streets.second < streets.first.size()) + return streets.first[streets.second].m_name; + return {}; + }); + + editor.SetForEachFeatureAtPointFn(bind(ForEachFeatureAtPoint, std::ref(m_model), _1, _2)); +} +} // namespace tests + +using tests::EditorTest; + +UNIT_CLASS_TEST(EditorTest, GetFeatureTypeInfoTest) +{ + EditorTest::GetFeatureTypeInfoTest(); +} + +UNIT_CLASS_TEST(EditorTest, OriginalFeatureHasDefaultNameTest) +{ + EditorTest::OriginalFeatureHasDefaultNameTest(); +} + +UNIT_CLASS_TEST(EditorTest, GetFeatureStatusTest) +{ + EditorTest::GetFeatureStatusTest(); +} + +UNIT_CLASS_TEST(EditorTest, IsFeatureUploadedTest) +{ + EditorTest::IsFeatureUploadedTest(); +} + +UNIT_CLASS_TEST(EditorTest, DeleteFeatureTest) +{ + EditorTest::DeleteFeatureTest(); +} + +UNIT_CLASS_TEST(EditorTest, ClearAllLocalEditsTest) +{ + EditorTest::ClearAllLocalEditsTest(); +} + +UNIT_CLASS_TEST(EditorTest, GetEditedFeatureStreetTest) +{ + EditorTest::GetEditedFeatureStreetTest(); +} + +UNIT_CLASS_TEST(EditorTest, GetEditedFeatureTest) +{ + EditorTest::GetEditedFeatureTest(); +} + +UNIT_CLASS_TEST(EditorTest, GetFeaturesByStatusTest) +{ + EditorTest::GetFeaturesByStatusTest(); +} + +UNIT_CLASS_TEST(EditorTest, OnMapDeregisteredTest) +{ + EditorTest::OnMapDeregisteredTest(); +} diff --git a/indexer/indexer_tests/osm_editor_test.hpp b/indexer/indexer_tests/osm_editor_test.hpp new file mode 100644 index 0000000000..8dc2772ad9 --- /dev/null +++ b/indexer/indexer_tests/osm_editor_test.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include "generator/generator_tests_support/test_feature.hpp" +#include "generator/generator_tests_support/test_mwm_builder.hpp" + +#include "platform/local_country_file_utils.hpp" + +#include "storage/country_info_getter.hpp" + +#include "indexer/mwm_set.hpp" + +#include "map/feature_vec_model.hpp" + +namespace tests +{ +class EditorTest +{ +public: + EditorTest(); + ~EditorTest(); + + void GetFeatureTypeInfoTest(); + void GetEditedFeatureTest(); + void GetEditedFeatureStreetTest(); + void OriginalFeatureHasDefaultNameTest(); + void GetFeatureStatusTest(); + void IsFeatureUploadedTest(); + void DeleteFeatureTest(); + void ClearAllLocalEditsTest(); + void GetFeaturesByStatusTest(); + void OnMapDeregisteredTest(); + +private: + template + MwmSet::MwmId ConstructTestMwm(TBuildFn && fn) + { + generator::tests_support::TestCity london(m2::PointD(1, 1), "London", "en", 100 /* rank */); + BuildMwm("TestWorld", feature::DataHeader::world,[&](generator::tests_support::TestMwmBuilder & builder) + { + builder.Add(london); + }); + + return BuildMwm("SomeCountry", feature::DataHeader::country, forward(fn)); + } + + template + MwmSet::MwmId BuildMwm(string const & name, feature::DataHeader::MapType type, TBuildFn && fn) + { + m_files.emplace_back(GetPlatform().WritableDir(), platform::CountryFile(name), 0 /* version */); + auto & file = m_files.back(); + Cleanup(file); + + { + generator::tests_support::TestMwmBuilder builder(file, type); + fn(builder); + } + + auto result = m_model.RegisterMap(file); + CHECK_EQUAL(result.second, MwmSet::RegResult::Success, ()); + + auto const & id = result.first; + if (type == feature::DataHeader::country) + { + auto const & info = id.GetInfo(); + if (info) + { + auto & infoGetter = static_cast(*m_infoGetter); + infoGetter.AddCountry(storage::CountryDef(name, info->m_limitRect)); + } + } + return id; + } + + void Cleanup(platform::LocalCountryFile const & map); + void InitEditorForTest(); + + model::FeaturesFetcher m_model; + unique_ptr m_infoGetter; + vector m_files; +}; +} // namespace tests diff --git a/indexer/osm_editor.cpp b/indexer/osm_editor.cpp index 1b428c35f0..fd030434e5 100644 --- a/indexer/osm_editor.cpp +++ b/indexer/osm_editor.cpp @@ -14,6 +14,7 @@ #include "platform/preferred_languages.hpp" #include "editor/changeset_wrapper.hpp" +#include "editor/editor_storage.hpp" #include "editor/osm_auth.hpp" #include "editor/server_api.hpp" #include "editor/xml_feature.hpp" @@ -38,8 +39,8 @@ #include "std/unordered_set.hpp" #include "3party/Alohalytics/src/alohalytics.h" -#include "3party/pugixml/src/pugixml.hpp" #include "3party/opening_hours/opening_hours.hpp" +#include "3party/pugixml/src/pugixml.hpp" using namespace pugi; using feature::EGeomType; @@ -48,7 +49,6 @@ using editor::XMLFeature; namespace { -constexpr char const * kEditorXMLFileName = "edits.xml"; constexpr char const * kXmlRootNode = "mapsme"; constexpr char const * kXmlMwmNode = "mwm"; constexpr char const * kDeleteSection = "delete"; @@ -74,8 +74,6 @@ bool NeedsUpload(string const & uploadStatus) uploadStatus != kWrongMatch; } -string GetEditorFilePath() { return GetPlatform().WritablePathForFile(kEditorXMLFileName); } - /// Compares editable fields connected with feature ignoring street. bool AreFeaturesEqualButStreet(FeatureType const & a, FeatureType const & b) { @@ -136,7 +134,10 @@ namespace osm // TODO(AlexZ): Normalize osm multivalue strings for correct merging // (e.g. insert/remove spaces after ';' delimeter); -Editor::Editor() : m_notes(editor::Notes::MakeNotes()) {} +Editor::Editor() + : m_notes(editor::Notes::MakeNotes()) + , m_storage(make_unique ()) +{} Editor & Editor::Instance() { @@ -153,16 +154,8 @@ void Editor::LoadMapEdits() } xml_document doc; - { - string const fullFilePath = GetEditorFilePath(); - xml_parse_result const res = doc.load_file(fullFilePath.c_str()); - // Note: status_file_not_found is ok if user has never made any edits. - if (res != status_ok && res != status_file_not_found) - { - LOG(LERROR, ("Can't load map edits from disk:", fullFilePath)); - return; - } - } + if (!m_storage->Load(doc)) + return; array, 4> const sections = {{ @@ -271,12 +264,12 @@ void Editor::LoadMapEdits() // Save edits with new indexes and mwm version to avoid another migration on next startup. if (needRewriteEdits) - Save(GetEditorFilePath()); + Save(); LOG(LINFO, ("Loaded", modified, "modified,", created, "created,", deleted, "deleted and", obsolete, "obsolete features.")); } -bool Editor::Save(string const & fullFilePath) const +bool Editor::Save() const { // TODO(AlexZ): Improve synchronization in Editor code. static mutex saveMutex; @@ -284,7 +277,7 @@ bool Editor::Save(string const & fullFilePath) const if (m_features.empty()) { - my::DeleteFileX(GetEditorFilePath()); + m_storage->Reset(); return true; } @@ -330,18 +323,13 @@ bool Editor::Save(string const & fullFilePath) const } } - return my::WriteToTempAndRenameToFile( - fullFilePath, - [&doc](string const & fileName) - { - return doc.save_file(fileName.data(), " "); - }); + return m_storage->Save(doc); } void Editor::ClearAllLocalEdits() { m_features.clear(); - Save(GetEditorFilePath()); + Save(); Invalidate(); } @@ -360,7 +348,7 @@ void Editor::OnMapDeregistered(platform::LocalCountryFile const & localFile) if (m_features.end() != matchedMwm) { m_features.erase(matchedMwm); - Save(GetEditorFilePath()); + Save(); } } @@ -411,7 +399,7 @@ void Editor::DeleteFeature(FeatureType const & feature) fti.m_feature = feature; // TODO(AlexZ): Synchronize Save call/make it on a separate thread. - Save(GetEditorFilePath()); + Save(); Invalidate(); } @@ -440,7 +428,7 @@ bool Editor::OriginalFeatureHasDefaultName(FeatureID const & fid) const } auto const & names = originalFeaturePtr->GetNames(); - + return names.HasString(StringUtf8Multilang::kDefaultCode); } @@ -519,7 +507,7 @@ Editor::SaveResult Editor::SaveEditedFeature(EditableMapObject const & emo) { RemoveFeatureFromStorageIfExists(fid.m_mwmId, fid.m_index); // TODO(AlexZ): Synchronize Save call/make it on a separate thread. - Save(GetEditorFilePath()); + Save(); Invalidate(); return SavedSuccessfully; } @@ -546,7 +534,7 @@ Editor::SaveResult Editor::SaveEditedFeature(EditableMapObject const & emo) m_features[fid.m_mwmId][fid.m_index] = move(fti); // TODO(AlexZ): Synchronize Save call/make it on a separate thread. - bool const savedSuccessfully = Save(GetEditorFilePath()); + bool const savedSuccessfully = Save(); Invalidate(); return savedSuccessfully ? SavedSuccessfully : NoFreeSpaceError; } @@ -558,7 +546,7 @@ bool Editor::RollBackChanges(FeatureID const & fid) RemoveFeatureFromStorageIfExists(fid.m_mwmId, fid.m_index); Invalidate(); - return Save(GetEditorFilePath()); + return Save(); } void Editor::ForEachFeatureInMwmRectAndScale(MwmSet::MwmId const & id, @@ -943,7 +931,7 @@ void Editor::SaveUploadedInformation(FeatureTypeInfo const & fromUploader) fti.m_uploadAttemptTimestamp = fromUploader.m_uploadAttemptTimestamp; fti.m_uploadStatus = fromUploader.m_uploadStatus; fti.m_uploadError = fromUploader.m_uploadError; - Save(GetEditorFilePath()); + Save(); } // Macros is used to avoid code duplication. @@ -1025,7 +1013,7 @@ void Editor::MarkFeatureAsObsolete(FeatureID const & fid) fti.m_status = FeatureStatus::Obsolete; fti.m_modificationTimestamp = time(nullptr); - Save(GetEditorFilePath()); + Save(); Invalidate(); } diff --git a/indexer/osm_editor.hpp b/indexer/osm_editor.hpp index 0ccfe8f264..a96682c98d 100644 --- a/indexer/osm_editor.hpp +++ b/indexer/osm_editor.hpp @@ -21,10 +21,22 @@ #include "std/string.hpp" #include "std/vector.hpp" +namespace editor +{ +class StorageBase; +} + +namespace tests +{ +class EditorTest; +} + namespace osm { class Editor final : public MwmSet::Observer { + friend class tests::EditorTest; + Editor(); public: @@ -175,7 +187,7 @@ public: private: // TODO(AlexZ): Synchronize Save call/make it on a separate thread. /// @returns false if fails. - bool Save(string const & fullFilePath) const; + bool Save() const; void RemoveFeatureFromStorageIfExists(MwmSet::MwmId const & mwmId, uint32_t index); void RemoveFeatureFromStorageIfExists(FeatureID const & fid); /// Notify framework that something has changed and should be redisplayed. @@ -204,7 +216,7 @@ private: FeatureTypeInfo const * GetFeatureTypeInfo(MwmSet::MwmId const & mwmId, uint32_t index) const; FeatureTypeInfo * GetFeatureTypeInfo(MwmSet::MwmId const & mwmId, uint32_t index); void SaveUploadedInformation(FeatureTypeInfo const & fromUploader); - + // TODO(AlexZ): Synchronize multithread access. /// Deleted, edited and created features. map> m_features; @@ -227,6 +239,8 @@ private: shared_ptr m_notes; // Mutex which locks OnMapDeregistered method mutex m_mapDeregisteredMutex; + + unique_ptr m_storage; }; // class Editor string DebugPrint(Editor::FeatureStatus fs); diff --git a/xcode/editor/editor.xcodeproj/project.pbxproj b/xcode/editor/editor.xcodeproj/project.pbxproj index 4283f84199..74c669d0f8 100644 --- a/xcode/editor/editor.xcodeproj/project.pbxproj +++ b/xcode/editor/editor.xcodeproj/project.pbxproj @@ -26,6 +26,8 @@ 34EB09201C5F846900F47F1F /* osm_feature_matcher.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 34EB091E1C5F846900F47F1F /* osm_feature_matcher.hpp */; }; 34FFB34C1C316A7600BFF6C3 /* server_api.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 34FFB34A1C316A7600BFF6C3 /* server_api.cpp */; }; 34FFB34D1C316A7600BFF6C3 /* server_api.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 34FFB34B1C316A7600BFF6C3 /* server_api.hpp */; }; + 3D489BEF1D4F67E10052AA38 /* editor_storage.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3D489BED1D4F67E10052AA38 /* editor_storage.cpp */; }; + 3D489BF01D4F67E10052AA38 /* editor_storage.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 3D489BEE1D4F67E10052AA38 /* editor_storage.hpp */; }; F60F02EE1C92CBF1003A0AF6 /* editor_notes.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F60F02EC1C92CBF1003A0AF6 /* editor_notes.cpp */; }; F60F02EF1C92CBF1003A0AF6 /* editor_notes.hpp in Headers */ = {isa = PBXBuildFile; fileRef = F60F02ED1C92CBF1003A0AF6 /* editor_notes.hpp */; }; /* End PBXBuildFile section */ @@ -52,6 +54,8 @@ 34EB091E1C5F846900F47F1F /* osm_feature_matcher.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = osm_feature_matcher.hpp; sourceTree = ""; }; 34FFB34A1C316A7600BFF6C3 /* server_api.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = server_api.cpp; sourceTree = ""; }; 34FFB34B1C316A7600BFF6C3 /* server_api.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = server_api.hpp; sourceTree = ""; }; + 3D489BED1D4F67E10052AA38 /* editor_storage.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = editor_storage.cpp; sourceTree = ""; }; + 3D489BEE1D4F67E10052AA38 /* editor_storage.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = editor_storage.hpp; sourceTree = ""; }; F60F02EC1C92CBF1003A0AF6 /* editor_notes.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = editor_notes.cpp; sourceTree = ""; }; F60F02ED1C92CBF1003A0AF6 /* editor_notes.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = editor_notes.hpp; sourceTree = ""; }; /* End PBXFileReference section */ @@ -87,6 +91,8 @@ 341138731C15AE02002E3B3E /* Editor */ = { isa = PBXGroup; children = ( + 3D489BED1D4F67E10052AA38 /* editor_storage.cpp */, + 3D489BEE1D4F67E10052AA38 /* editor_storage.hpp */, 3441CE461CFC1D3C00CF30D4 /* user_stats.cpp */, 3441CE471CFC1D3C00CF30D4 /* user_stats.hpp */, F60F02EC1C92CBF1003A0AF6 /* editor_notes.cpp */, @@ -123,6 +129,7 @@ 34EB09201C5F846900F47F1F /* osm_feature_matcher.hpp in Headers */, 34FFB34D1C316A7600BFF6C3 /* server_api.hpp in Headers */, F60F02EF1C92CBF1003A0AF6 /* editor_notes.hpp in Headers */, + 3D489BF01D4F67E10052AA38 /* editor_storage.hpp in Headers */, 340DC82A1C4E71E500EAA2CC /* changeset_wrapper.hpp in Headers */, 347C71291C295B1100BE9208 /* xml_feature.hpp in Headers */, 34583BC01C8854C100F94664 /* yes_no_unknown.hpp in Headers */, @@ -193,6 +200,7 @@ 347C71281C295B1100BE9208 /* xml_feature.cpp in Sources */, 341138781C15AE42002E3B3E /* opening_hours_ui.cpp in Sources */, 340C20DE1C3E4DFD00111D22 /* osm_auth.cpp in Sources */, + 3D489BEF1D4F67E10052AA38 /* editor_storage.cpp in Sources */, 3411387A1C15AE42002E3B3E /* ui2oh.cpp in Sources */, 340DC8291C4E71E500EAA2CC /* changeset_wrapper.cpp in Sources */, 34EB091F1C5F846900F47F1F /* osm_feature_matcher.cpp in Sources */, diff --git a/xcode/indexer/indexer.xcodeproj/project.pbxproj b/xcode/indexer/indexer.xcodeproj/project.pbxproj index e70ffdc8c1..ac7924a271 100644 --- a/xcode/indexer/indexer.xcodeproj/project.pbxproj +++ b/xcode/indexer/indexer.xcodeproj/project.pbxproj @@ -29,6 +29,8 @@ 3D489BC61D3D220F0052AA38 /* editable_map_object_test.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3D489BA71D3D1F8A0052AA38 /* editable_map_object_test.cpp */; }; 3D489BC71D3D22150052AA38 /* features_vector_test.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3D489BA81D3D1F8A0052AA38 /* features_vector_test.cpp */; }; 3D489BC81D3D22190052AA38 /* string_slice_tests.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3D489BA91D3D1F8A0052AA38 /* string_slice_tests.cpp */; }; + 3D489BF31D4F87740052AA38 /* osm_editor_test.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3D489BF11D4F87740052AA38 /* osm_editor_test.cpp */; }; + 3D489BF41D4F87740052AA38 /* osm_editor_test.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 3D489BF21D4F87740052AA38 /* osm_editor_test.hpp */; }; 56C74C1C1C749E4700B71B9F /* categories_holder_loader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 56C74C121C749E4700B71B9F /* categories_holder_loader.cpp */; }; 56C74C1D1C749E4700B71B9F /* categories_holder.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 56C74C131C749E4700B71B9F /* categories_holder.cpp */; }; 56C74C1E1C749E4700B71B9F /* categories_holder.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 56C74C141C749E4700B71B9F /* categories_holder.hpp */; }; @@ -214,6 +216,8 @@ 3D489BA71D3D1F8A0052AA38 /* editable_map_object_test.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = editable_map_object_test.cpp; sourceTree = ""; }; 3D489BA81D3D1F8A0052AA38 /* features_vector_test.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = features_vector_test.cpp; sourceTree = ""; }; 3D489BA91D3D1F8A0052AA38 /* string_slice_tests.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = string_slice_tests.cpp; sourceTree = ""; }; + 3D489BF11D4F87740052AA38 /* osm_editor_test.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = osm_editor_test.cpp; sourceTree = ""; }; + 3D489BF21D4F87740052AA38 /* osm_editor_test.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = osm_editor_test.hpp; sourceTree = ""; }; 56C74C121C749E4700B71B9F /* categories_holder_loader.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = categories_holder_loader.cpp; sourceTree = ""; }; 56C74C131C749E4700B71B9F /* categories_holder.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = categories_holder.cpp; sourceTree = ""; }; 56C74C141C749E4700B71B9F /* categories_holder.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = categories_holder.hpp; sourceTree = ""; }; @@ -434,6 +438,8 @@ 670C60F81AB0657700C38A8C /* indexer_tests */ = { isa = PBXGroup; children = ( + 3D489BF11D4F87740052AA38 /* osm_editor_test.cpp */, + 3D489BF21D4F87740052AA38 /* osm_editor_test.hpp */, 3D489BA71D3D1F8A0052AA38 /* editable_map_object_test.cpp */, 3D489BA81D3D1F8A0052AA38 /* features_vector_test.cpp */, 3D489BA91D3D1F8A0052AA38 /* string_slice_tests.cpp */, @@ -671,6 +677,7 @@ 675341271A3F540F00A0A8C3 /* features_vector.hpp in Headers */, 6753413D1A3F540F00A0A8C3 /* scale_index_builder.hpp in Headers */, 675341021A3F540F00A0A8C3 /* classificator_loader.hpp in Headers */, + 3D489BF41D4F87740052AA38 /* osm_editor_test.hpp in Headers */, 6758AED21BB4413000C26E27 /* drules_selector_parser.hpp in Headers */, 670BAACA1D0B0BBB000302DA /* string_slice.hpp in Headers */, 6753413F1A3F540F00A0A8C3 /* scale_index.hpp in Headers */, @@ -874,6 +881,7 @@ 675341101A3F540F00A0A8C3 /* drules_struct.pb.cc in Sources */, 6758AED11BB4413000C26E27 /* drules_selector_parser.cpp in Sources */, E906DE3B1CF44934004C4F5E /* postcodes_matcher.cpp in Sources */, + 3D489BF31D4F87740052AA38 /* osm_editor_test.cpp in Sources */, 6753413B1A3F540F00A0A8C3 /* point_to_int64.cpp in Sources */, ); runOnlyForDeploymentPostprocessing = 0;