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
)