From c1760bbc371160f2c740aeb3a4300a581317d470 Mon Sep 17 00:00:00 2001 From: map-per Date: Sat, 13 Dec 2025 16:14:22 +0100 Subject: [PATCH] [editor] Support complex POI types in the editor (#2855) Signed-off-by: map-per --- .../sdk/src/main/assets/mapcss-mapping.csv | 1 + data/editor.config | 116 ++++----- data/mapcss-mapping.csv | 4 +- libs/editor/CMakeLists.txt | 2 + libs/editor/editor_tests/CMakeLists.txt | 1 + .../editor_tests/feature_type_to_osm_test.cpp | 224 ++++++++++++++++++ libs/editor/feature_type_to_osm.cpp | 166 +++++++++++++ libs/editor/feature_type_to_osm.hpp | 34 +++ libs/editor/xml_feature.cpp | 34 +-- qt/CMakeLists.txt | 1 + 10 files changed, 485 insertions(+), 98 deletions(-) create mode 120000 android/sdk/src/main/assets/mapcss-mapping.csv create mode 100644 libs/editor/editor_tests/feature_type_to_osm_test.cpp create mode 100644 libs/editor/feature_type_to_osm.cpp create mode 100644 libs/editor/feature_type_to_osm.hpp diff --git a/android/sdk/src/main/assets/mapcss-mapping.csv b/android/sdk/src/main/assets/mapcss-mapping.csv new file mode 120000 index 000000000..5c30a5f51 --- /dev/null +++ b/android/sdk/src/main/assets/mapcss-mapping.csv @@ -0,0 +1 @@ +../../../../../data/mapcss-mapping.csv \ No newline at end of file diff --git a/data/editor.config b/data/editor.config index ca4c1e745..85068ae20 100644 --- a/data/editor.config +++ b/data/editor.config @@ -394,32 +394,25 @@ - - + - - + - - + - - + - - + - - + - - + @@ -705,6 +698,9 @@ + + + @@ -1115,6 +1111,7 @@ + @@ -1144,26 +1141,22 @@ - - + - - + - - + - - + - - + - + + @@ -1509,67 +1502,50 @@ - diff --git a/data/mapcss-mapping.csv b/data/mapcss-mapping.csv index 177f9bf8f..70b7dbeae 100644 --- a/data/mapcss-mapping.csv +++ b/data/mapcss-mapping.csv @@ -5,7 +5,7 @@ # highway|bus_stop;[highway=bus_stop];;name;int_name;22; # It contains: # - type name: "highway|bus_stop" ("|" is converted to "-" internally) -# - mapcss selectors for tags: "[highway=bus_stop]", multiple selectors are separated with commas +# - mapcss selectors for tags: "[highway=bus_stop]", multiple selectors are separated with commas, best practice tagging for OSM editor is listed first # - "x" for a deprecated type or an empty cell otherwise # - primary title tag (usually "name") # - secondary title tag (usually "int_name") @@ -626,7 +626,7 @@ highway|trunk_link|tunnel;[highway=trunk_link][tunnel?];;name;int_name;503; drinking_water|yes;[drinking_water=yes],[drinking_water=treated],[drinking_water:refill=yes];;;;504; drinking_water|no;505; amenity|sailing_school;[amenity=sailing_school],[education=sailing_school];;name;int_name;506; -amenity|flight_school;[amenity=sailing_school],[education=flight_school];;name;int_name;507; +amenity|flight_school;[amenity=flight_school],[education=flight_school];;name;int_name;507; amenity|prep_school;[amenity=prep_school],[education=prep_school];;name;int_name;508; amenity|car_pooling;509; social_facility|soup_kitchen;510; diff --git a/libs/editor/CMakeLists.txt b/libs/editor/CMakeLists.txt index 33f94d1e0..8c5c84a5a 100644 --- a/libs/editor/CMakeLists.txt +++ b/libs/editor/CMakeLists.txt @@ -18,6 +18,8 @@ set(SRC edits_migration.hpp feature_matcher.cpp feature_matcher.hpp + feature_type_to_osm.cpp + feature_type_to_osm.hpp new_feature_categories.cpp new_feature_categories.hpp opening_hours_ui.cpp diff --git a/libs/editor/editor_tests/CMakeLists.txt b/libs/editor/editor_tests/CMakeLists.txt index e2909c900..0675f944a 100644 --- a/libs/editor/editor_tests/CMakeLists.txt +++ b/libs/editor/editor_tests/CMakeLists.txt @@ -5,6 +5,7 @@ set(SRC editor_config_test.cpp editor_notes_test.cpp feature_matcher_test.cpp + feature_type_to_osm_test.cpp match_by_geometry_test.cpp new_feature_categories_test.cpp opening_hours_ui_test.cpp diff --git a/libs/editor/editor_tests/feature_type_to_osm_test.cpp b/libs/editor/editor_tests/feature_type_to_osm_test.cpp new file mode 100644 index 000000000..544006900 --- /dev/null +++ b/libs/editor/editor_tests/feature_type_to_osm_test.cpp @@ -0,0 +1,224 @@ +#include "testing/testing.hpp" + +#include "editor/feature_type_to_osm.hpp" + +#include "indexer/classificator.hpp" +#include "indexer/classificator_loader.hpp" + +using namespace editor; + +UNIT_TEST(simpleType) +{ + std::string data = + "amenity|restaurant;61;\n" + "amenity|bicycle_parking;1071;\n"; + + classificator::Load(); + + TypeToOSMTranslator translator(false); + std::stringstream s(data); + translator.LoadFromStream(s); + + uint32_t type = classif().GetTypeByReadableObjectName("amenity-restaurant"); + std::vector result = translator.OsmTagsFromType(type); + TEST_EQUAL(result.size(), 1, ()); + TEST_EQUAL(result[0].key, "amenity", ()); + TEST_EQUAL(result[0].value, "restaurant", ()); +} + +UNIT_TEST(simpleTypeWithTags) +{ + std::string data = + "building;[building];;addr:housenumber;name;1;\n" + "amenity|school;[amenity=school],[education=school];;name;int_name;36;\n" + "amenity|doctors;[amenity=doctors][healthcare=doctor],[amenity=doctors],[healthcare=doctor];;name;int_name;207;\n"; + + classificator::Load(); + + TypeToOSMTranslator translator(false); + std::stringstream s(data); + translator.LoadFromStream(s); + + uint32_t buildingType = classif().GetTypeByReadableObjectName("building"); + std::vector buildingResult = translator.OsmTagsFromType(buildingType); + TEST_EQUAL(buildingResult.size(), 1, ()); + TEST_EQUAL(buildingResult[0].key, "building", ()); + TEST_EQUAL(buildingResult[0].value, "yes", ()); + + uint32_t schoolType = classif().GetTypeByReadableObjectName("amenity-school"); + std::vector schoolResult = translator.OsmTagsFromType(schoolType); + TEST_EQUAL(schoolResult.size(), 1, ()); + TEST_EQUAL(schoolResult[0].key, "amenity", ()); + TEST_EQUAL(schoolResult[0].value, "school", ()); + + uint32_t doctorType = classif().GetTypeByReadableObjectName("amenity-doctors"); + std::vector doctorResult = translator.OsmTagsFromType(doctorType); + TEST_EQUAL(doctorResult.size(), 2, ()); + TEST_EQUAL(doctorResult[0].key, "amenity", ()); + TEST_EQUAL(doctorResult[0].value, "doctors", ()); + TEST_EQUAL(doctorResult[1].key, "healthcare", ()); + TEST_EQUAL(doctorResult[1].value, "doctor", ()); +} + +UNIT_TEST(complexType) +{ + std::string data = + "building;[building];;addr:housenumber;name;1;\n" + " # comment that should be ignored\n" + "\n" + "amenity|restaurant;61;\n" + "tourism|information|office;[tourism=information][information=office];;name;int_name;313;\n" + "historic|castle|fortress;[historic=castle][castle_type=fortress],[historic=fortress];;name;int_name;1144;\n" + "#comment\n" + "amenity|place_of_worship|christian|mormon;[amenity=place_of_worship][religion=christian][denomination=mormon];;name;int_name;1572;\n"; + + classificator::Load(); + + TypeToOSMTranslator translator(false); + std::stringstream s(data); + translator.LoadFromStream(s); + + uint32_t officeType = classif().GetTypeByReadableObjectName("tourism-information-office"); + std::vector officeResult = translator.OsmTagsFromType(officeType); + TEST_EQUAL(officeResult.size(), 2, ()); + TEST_EQUAL(officeResult[0].key, "tourism", ()); + TEST_EQUAL(officeResult[0].value, "information", ()); + TEST_EQUAL(officeResult[1].key, "information", ()); + TEST_EQUAL(officeResult[1].value, "office", ()); + + uint32_t fortressType = classif().GetTypeByReadableObjectName("historic-castle-fortress"); + std::vector fortressResult = translator.OsmTagsFromType(fortressType); + TEST_EQUAL(fortressResult.size(), 2, ()); + TEST_EQUAL(fortressResult[0].key, "historic", ()); + TEST_EQUAL(fortressResult[0].value, "castle", ()); + TEST_EQUAL(fortressResult[1].key, "castle_type", ()); + TEST_EQUAL(fortressResult[1].value, "fortress", ()); + + uint32_t mormonType = classif().GetTypeByReadableObjectName("amenity-place_of_worship-christian-mormon"); + std::vector mormonResult = translator.OsmTagsFromType(mormonType); + TEST_EQUAL(mormonResult.size(), 3, ()); + TEST_EQUAL(mormonResult[0].key, "amenity", ()); + TEST_EQUAL(mormonResult[0].value, "place_of_worship", ()); + TEST_EQUAL(mormonResult[1].key, "religion", ()); + TEST_EQUAL(mormonResult[1].value, "christian", ()); + TEST_EQUAL(mormonResult[2].key, "denomination", ()); + TEST_EQUAL(mormonResult[2].value, "mormon", ()); +} + +UNIT_TEST(mandatorySelector) +{ + std::string data = + "amenity|parking|fee;[amenity=parking][fee];;name;int_name;125;\n" + "highway|track|bridge;[highway=track][bridge?];;name;int_name;193;\n" + "shop;[shop?];;name;int_name;943;\n" + "disusedbusiness;[disused:shop?],[disused:amenity=restaurant],[disused:amenity=fast_food],[disused:amenity=cafe],[disused:amenity=pub],[disused:amenity=bar];;;;1237;\n"; + + classificator::Load(); + + TypeToOSMTranslator translator(false); + std::stringstream s(data); + translator.LoadFromStream(s); + + uint32_t parkingType = classif().GetTypeByReadableObjectName("amenity-parking-fee"); + std::vector parkingResult = translator.OsmTagsFromType(parkingType); + TEST_EQUAL(parkingResult.size(), 2, ()); + TEST_EQUAL(parkingResult[0].key, "amenity", ()); + TEST_EQUAL(parkingResult[0].value, "parking", ()); + TEST_EQUAL(parkingResult[1].key, "fee", ()); + TEST_EQUAL(parkingResult[1].value, "yes", ()); + + uint32_t trackType = classif().GetTypeByReadableObjectName("highway-track-bridge"); + std::vector trackResult = translator.OsmTagsFromType(trackType); + TEST_EQUAL(trackResult.size(), 2, ()); + TEST_EQUAL(trackResult[0].key, "highway", ()); + TEST_EQUAL(trackResult[0].value, "track", ()); + TEST_EQUAL(trackResult[1].key, "bridge", ()); + TEST_EQUAL(trackResult[1].value, "yes", ()); + + uint32_t shopType = classif().GetTypeByReadableObjectName("shop"); + std::vector shopResult = translator.OsmTagsFromType(shopType); + TEST_EQUAL(shopResult.size(), 1, ()); + TEST_EQUAL(shopResult[0].key, "shop", ()); + TEST_EQUAL(shopResult[0].value, "yes", ()); + + uint32_t disusedType = classif().GetTypeByReadableObjectName("disusedbusiness"); + std::vector disusedResult = translator.OsmTagsFromType(disusedType); + TEST_EQUAL(disusedResult.size(), 1, ()); + TEST_EQUAL(disusedResult[0].key, "disused:shop", ()); + TEST_EQUAL(disusedResult[0].value, "yes", ()); +} + +UNIT_TEST(forbiddenSelector) +{ + std::string data = + "amenity|lounger;[amenity=lounger][!seasonal];;name;int_name;153;\n" + "amenity|charging_station|motorcar|small;[amenity=charging_station][motorcar?][!capacity],[amenity=charging_station][motorcar?][capacity=1],[amenity=charging_station][motorcar?][capacity=2];;name;int_name;201;\n"; + + classificator::Load(); + + TypeToOSMTranslator translator(false); + std::stringstream s(data); + translator.LoadFromStream(s); + + uint32_t loungerType = classif().GetTypeByReadableObjectName("amenity-lounger"); + std::vector loungerResult = translator.OsmTagsFromType(loungerType); + TEST_EQUAL(loungerResult.size(), 1, ()); + TEST_EQUAL(loungerResult[0].key, "amenity", ()); + TEST_EQUAL(loungerResult[0].value, "lounger", ()); + + uint32_t chargingType = classif().GetTypeByReadableObjectName("amenity-charging_station-motorcar-small"); + std::vector chargingResult = translator.OsmTagsFromType(chargingType); + TEST_EQUAL(chargingResult.size(), 2, ()); + TEST_EQUAL(chargingResult[0].key, "amenity", ()); + TEST_EQUAL(chargingResult[0].value, "charging_station", ()); + TEST_EQUAL(chargingResult[1].key, "motorcar", ()); + TEST_EQUAL(chargingResult[1].value, "yes", ()); +} + +UNIT_TEST(ignoreComments) +{ + std::string data = + "building;[building];;addr:housenumber;name;1;\n" + " # comment that should be ignored\n" + "\n" + "deprecated:waterway|riverbank:05.2024;52;x\n" + "amenity|restaurant;61;\n" + "moved:amenity|telephone:05.2024;122;amenity|telephone\n" + "natural|lake;564;natural|water|lake\n"; // moved type, should be ignored + + classificator::Load(); + + TypeToOSMTranslator translator(false); + std::stringstream s(data); + translator.LoadFromStream(s); +} + +UNIT_TEST(loadConfigFile) +{ + TypeToOSMTranslator translator(false); + translator.LoadConfigFile(); + + size_t size = translator.GetStorage().size(); + LOG(LINFO, ("Size of feature type storage:", size)); + ASSERT(size > 1300, ()); + ASSERT(size < 1700, ()); +} + +UNIT_TEST(testWithRealFile) +{ + classificator::Load(); + + uint32_t restaurantType = classif().GetTypeByReadableObjectName("amenity-restaurant"); + std::vector restaurantResult = GetOSMTranslator().OsmTagsFromType(restaurantType); + TEST_EQUAL(restaurantResult.size(), 1, ()); + TEST_EQUAL(restaurantResult[0].key, "amenity", ()); + TEST_EQUAL(restaurantResult[0].value, "restaurant", ()); + + uint32_t officeType = classif().GetTypeByReadableObjectName("tourism-information-office"); + std::vector officeResult = GetOSMTranslator().OsmTagsFromType(officeType); + TEST_EQUAL(officeResult.size(), 2, ()); + TEST_EQUAL(officeResult[0].key, "tourism", ()); + TEST_EQUAL(officeResult[0].value, "information", ()); + TEST_EQUAL(officeResult[1].key, "information", ()); + TEST_EQUAL(officeResult[1].value, "office", ()); +} diff --git a/libs/editor/feature_type_to_osm.cpp b/libs/editor/feature_type_to_osm.cpp new file mode 100644 index 000000000..9b8ebdcea --- /dev/null +++ b/libs/editor/feature_type_to_osm.cpp @@ -0,0 +1,166 @@ +#include "editor/feature_type_to_osm.hpp" + +#include "base/assert.hpp" +#include "coding/reader_streambuf.hpp" +#include "indexer/classificator.hpp" +#include "platform/platform.hpp" + +#include + +namespace editor +{ +TypeToOSMTranslator::TypeToOSMTranslator(bool initialize) +{ + if (initialize) + LoadConfigFile(); +} + +void TypeToOSMTranslator::LoadConfigFile() +{ + Platform & p = GetPlatform(); + std::unique_ptr reader = p.GetReader("mapcss-mapping.csv"); + ReaderStreamBuf buffer(std::move(reader)); + std::istream s(&buffer); + + LoadFromStream(s); +} + +void TypeToOSMTranslator::LoadFromStream(std::istream & s) +{ + m_storage.clear(); + + std::string line; + while (s.good()) + { + getline(s, line); + strings::Trim(line); + + // skip empty lines, comments, deprecated and moved types + if (line.empty() || line.front() == '#' || line.starts_with("deprecated") || line.starts_with("moved") || + line.back() != ';') + continue; + + std::vector rowTokens = strings::Tokenize(line, ";"); + if (rowTokens.size() < 2) + { + ASSERT(false, ("Invalid feature type definition:", line)); + continue; + } + + // Get internal feature type + std::vector featureTypeTokens = strings::Tokenize(rowTokens[0], "|"); + uint32_t type = classif().GetTypeByPathSafe(featureTypeTokens); + ASSERT(type != IndexAndTypeMapping::INVALID_TYPE, ("Feature with invalid type:", line)); + + if (rowTokens.size() == 2) + { + // Derive OSM tags from type name + ASSERT(featureTypeTokens.size() <= 2, ("OSM tags can not be inferred from name:", line)); + + OSMTag osmTag; + + // e.g. "amenity-restaurant" + if (featureTypeTokens.size() >= 2) + { + osmTag.key = featureTypeTokens[0]; + osmTag.value = featureTypeTokens[1]; + } + // e.g. "building" + else if (featureTypeTokens.size() == 1) + { + osmTag.key = featureTypeTokens[0]; + osmTag.value = "yes"; + } + + m_storage.insert({type, {osmTag}}); + } + else + { + // OSM tags are listed in the feature type entry + std::vector osmTagTokens = strings::Tokenize(rowTokens[1], ","); + + // First entry is the best practice way to tag a feature + std::string_view osmTagList = osmTagTokens[0]; + + // Process OSM tag list (e.g. "[tourism=information][information=office]") + std::vector osmTags; + size_t pos = 0; + + while ((pos = osmTagList.find('[', pos)) != std::string::npos) + { + size_t end = osmTagList.find(']', pos); + + if (end == std::string::npos) + { + ASSERT(false, ("Bracket not closed in OSM tag:", line)); + break; + } + + std::string_view keyValuePair = osmTagList.substr(pos + 1, end - pos - 1); + + if (keyValuePair.empty()) + { + ASSERT(false, ("Key value pair is empty:", line)); + break; + } + + size_t equalSign = keyValuePair.find('='); + if (equalSign != std::string::npos) + { + // Tags in key=value format + OSMTag osmTag; + osmTag.key = keyValuePair.substr(0, equalSign); + osmTag.value = keyValuePair.substr(equalSign + 1); + + // mapcss-mapping.csv uses 'not' instead of 'no' as a workaround for the rendering engine + if (osmTag.value == "not") + osmTag.value = "no"; + + osmTags.push_back(osmTag); + } + else if (keyValuePair.front() == '!') + { + // Tags with "forbidden" selector '!' are skipped + } + else + { + // Tags with optional "mandatory" selector '?' + if (keyValuePair.back() == '?') + keyValuePair.remove_suffix(1); + + OSMTag osmTag; + osmTag.key = keyValuePair; + osmTag.value = "yes"; + + osmTags.push_back(osmTag); + } + + pos = end + 1; + } + + ASSERT(!osmTags.empty(), ("No OSM tags found for feature:", line)); + + m_storage.insert({type, osmTags}); + } + } +} + +std::vector const & TypeToOSMTranslator::OsmTagsFromType(uint32_t type) const +{ + auto it = m_storage.find(type); + + if (it == m_storage.end()) + { + ASSERT(false, ("OSM tags for type", type, "could not be found")); + return {}; + } + + return it->second; +} + +TypeToOSMTranslator const & GetOSMTranslator() +{ + static TypeToOSMTranslator translator; + return translator; +} +} // namespace editor diff --git a/libs/editor/feature_type_to_osm.hpp b/libs/editor/feature_type_to_osm.hpp new file mode 100644 index 000000000..7500300e2 --- /dev/null +++ b/libs/editor/feature_type_to_osm.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include + +namespace editor +{ + +struct OSMTag +{ + std::string key; + std::string value; +}; + +class TypeToOSMTranslator +{ +public: + TypeToOSMTranslator() : TypeToOSMTranslator(true) {} + explicit TypeToOSMTranslator(bool initialize); + + void LoadConfigFile(); + void LoadFromStream(std::istream & s); + std::vector const & OsmTagsFromType(uint32_t type) const; + + std::unordered_map> const & GetStorage() const { return m_storage; } + +private: + std::unordered_map> m_storage; +}; + +TypeToOSMTranslator const & GetOSMTranslator(); + +} // namespace editor diff --git a/libs/editor/xml_feature.cpp b/libs/editor/xml_feature.cpp index 37b856e88..ad11d5549 100644 --- a/libs/editor/xml_feature.cpp +++ b/libs/editor/xml_feature.cpp @@ -1,4 +1,6 @@ #include "editor/xml_feature.hpp" + +#include "editor/feature_type_to_osm.hpp" #include "editor/keys_to_remove.hpp" #include "indexer/classificator.hpp" @@ -640,36 +642,16 @@ void XMLFeature::RemoveTag(string_view key) void XMLFeature::SetOSMTagsForType(uint32_t type) { - if (ftypes::IsRecyclingCentreChecker::Instance()(type)) - { - SetTagValue("amenity", "recycling"); - SetTagValue("recycling_type", "centre"); - } - else if (ftypes::IsRecyclingContainerChecker::Instance()(type)) - { - SetTagValue("amenity", "recycling"); - SetTagValue("recycling_type", "container"); - } - else if (ftypes::IsAddressChecker::Instance()(type)) + if (ftypes::IsAddressChecker::Instance()(type)) { // Addresses don't have a category tag + return; } - else - { - string const strType = classif().GetReadableObjectName(type); - strings::SimpleTokenizer iter(strType, "-"); - string_view const k = *iter; - if (++iter) - { - // Main type is stored as "k=amenity v=restaurant" - SetTagValue(k, *iter); - } - else { - // Main type is stored as "k=building v=yes" - SetTagValue(k, kYes); - } - } + std::vector const & osmTags = GetOSMTranslator().OsmTagsFromType(type); + + for(auto const & osmTag : osmTags) + SetTagValue(osmTag.key, osmTag.value); } void XMLFeature::UpdateOSMTag(std::string_view key, std::string_view value) diff --git a/qt/CMakeLists.txt b/qt/CMakeLists.txt index ea25618b3..dee36ed81 100644 --- a/qt/CMakeLists.txt +++ b/qt/CMakeLists.txt @@ -126,6 +126,7 @@ copy_resources( patterns.txt transit_colors.txt types.txt + mapcss-mapping.csv World.mwm WorldCoasts.mwm )