From 9bfebc2046671043b23423b2fd88241ac6cc026e Mon Sep 17 00:00:00 2001 From: map-per Date: Fri, 14 Nov 2025 22:10:02 +0100 Subject: [PATCH] Add "Business is vacant"/'disused' option to editor (#526) Signed-off-by: map-per Co-authored-by: map-per Co-committed-by: map-per --- .../organicmaps/editor/EditorFragment.java | 21 +++ .../editor/EditorHostFragment.java | 2 +- .../src/main/res/layout/fragment_editor.xml | 14 +- android/app/src/main/res/values/strings.xml | 8 ++ .../cpp/app/organicmaps/sdk/editor/Editor.cpp | 11 ++ .../app/organicmaps/sdk/editor/Editor.java | 2 + libs/editor/CMakeLists.txt | 1 + libs/editor/changeset_wrapper.cpp | 3 + libs/editor/keys_to_remove.hpp | 135 ++++++++++++++++++ libs/editor/osm_editor.cpp | 14 +- libs/editor/osm_editor.hpp | 4 +- libs/editor/xml_feature.cpp | 79 ++++++++++ libs/editor/xml_feature.hpp | 2 + libs/indexer/edit_journal.cpp | 20 +++ libs/indexer/edit_journal.hpp | 12 +- libs/indexer/editable_map_object.cpp | 85 ++++++++++- libs/indexer/editable_map_object.hpp | 7 + libs/map/framework.cpp | 7 + libs/map/framework.hpp | 1 + xcode/editor/editor.xcodeproj/project.pbxproj | 4 + 20 files changed, 424 insertions(+), 8 deletions(-) create mode 100644 libs/editor/keys_to_remove.hpp diff --git a/android/app/src/main/java/app/organicmaps/editor/EditorFragment.java b/android/app/src/main/java/app/organicmaps/editor/EditorFragment.java index 336358569..0a728c985 100644 --- a/android/app/src/main/java/app/organicmaps/editor/EditorFragment.java +++ b/android/app/src/main/java/app/organicmaps/editor/EditorFragment.java @@ -153,6 +153,7 @@ public class EditorFragment extends BaseMwmFragment implements View.OnClickListe private final Map mDetailsBlocks = new HashMap<>(); private final Map mSocialMediaBlocks = new HashMap<>(); private MaterialButton mReset; + private MaterialButton mDisused; private EditorHostFragment mParent; @@ -827,6 +828,8 @@ public class EditorFragment extends BaseMwmFragment implements View.OnClickListe osmInfo.setMovementMethod(LinkMovementMethod.getInstance()); mReset = view.findViewById(R.id.reset); mReset.setOnClickListener(this); + mDisused = view.findViewById(R.id.disused); + mDisused.setOnClickListener(this); mDetailsBlocks.put(Metadata.MetadataType.FMD_OPEN_HOURS, blockOpeningHours); mDetailsBlocks.put(Metadata.MetadataType.FMD_PHONE_NUMBER, blockPhone); @@ -894,6 +897,8 @@ public class EditorFragment extends BaseMwmFragment implements View.OnClickListe mParent.addLanguage(); else if (id == R.id.reset) reset(); + else if (id == R.id.disused) + placeDisused(); else if (id == R.id.block_outdoor_seating) mOutdoorSeating.toggle(); } @@ -939,9 +944,12 @@ public class EditorFragment extends BaseMwmFragment implements View.OnClickListe if (mParent.addingNewObject()) { UiUtils.hide(mReset); + UiUtils.hide(mDisused); return; } + mDisused.setVisibility(Editor.nativeCanMarkPlaceAsDisused() ? View.VISIBLE : View.GONE); + if (Editor.nativeIsMapObjectUploaded()) { mReset.setText(R.string.editor_place_doesnt_exist); @@ -1014,6 +1022,19 @@ public class EditorFragment extends BaseMwmFragment implements View.OnClickListe dialogFragment.setTextSaveListener(this::commitPlaceDoesntExists); } + private void placeDisused() + { + new MaterialAlertDialogBuilder(requireActivity(), R.style.MwmTheme_AlertDialog) + .setTitle(R.string.editor_mark_business_vacant_title) + .setMessage(R.string.editor_mark_business_vacant_description) + .setPositiveButton(R.string.editor_submit, (dlg, which) -> { + Editor.nativeMarkPlaceAsDisused(); + mParent.processEditedFeatures(); + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + private void commitPlaceDoesntExists(@NonNull String text) { Editor.nativePlaceDoesNotExist(text); diff --git a/android/app/src/main/java/app/organicmaps/editor/EditorHostFragment.java b/android/app/src/main/java/app/organicmaps/editor/EditorHostFragment.java index 72ffc99b0..89b0cd961 100644 --- a/android/app/src/main/java/app/organicmaps/editor/EditorHostFragment.java +++ b/android/app/src/main/java/app/organicmaps/editor/EditorHostFragment.java @@ -358,7 +358,7 @@ public class EditorHostFragment .show(); } - private void processEditedFeatures() + public void processEditedFeatures() { if (OsmOAuth.isAuthorized()) { diff --git a/android/app/src/main/res/layout/fragment_editor.xml b/android/app/src/main/res/layout/fragment_editor.xml index 657d49aeb..3f14a84be 100644 --- a/android/app/src/main/res/layout/fragment_editor.xml +++ b/android/app/src/main/res/layout/fragment_editor.xml @@ -394,7 +394,8 @@ + style="@style/MwmWidget.Editor.CardView" + android:layout_marginBottom="@dimen/margin_base"> + Describe what the place looks like now to send an error note to the OpenStreetMap community Please indicate the reason for deleting the place + + Business is vacant + + Mark business as vacant + + Use this if the business has moved out and the space is empty and ready for a new tenant. + + Submit Enter a valid phone number Enter a valid web address diff --git a/android/sdk/src/main/cpp/app/organicmaps/sdk/editor/Editor.cpp b/android/sdk/src/main/cpp/app/organicmaps/sdk/editor/Editor.cpp index bd0bfea2a..cfa01c107 100644 --- a/android/sdk/src/main/cpp/app/organicmaps/sdk/editor/Editor.cpp +++ b/android/sdk/src/main/cpp/app/organicmaps/sdk/editor/Editor.cpp @@ -277,6 +277,12 @@ JNIEXPORT jboolean JNICALL Java_app_organicmaps_sdk_editor_Editor_nativeIsNameEd return g_editableMapObject.IsNameEditable(); } +JNIEXPORT jboolean JNICALL Java_app_organicmaps_sdk_editor_Editor_nativeCanMarkPlaceAsDisused(JNIEnv * env, + jclass clazz) +{ + return g_editableMapObject.CanMarkPlaceAsDisused(); +} + JNIEXPORT jboolean JNICALL Java_app_organicmaps_sdk_editor_Editor_nativeIsPointType(JNIEnv * env, jclass clazz) { return g_editableMapObject.IsPointType(); @@ -434,6 +440,11 @@ JNIEXPORT void JNICALL Java_app_organicmaps_sdk_editor_Editor_nativeRollbackMapO g_framework->NativeFramework()->RollBackChanges(g_editableMapObject.GetID()); } +JNIEXPORT void JNICALL Java_app_organicmaps_sdk_editor_Editor_nativeMarkPlaceAsDisused(JNIEnv * env, jclass clazz) +{ + g_framework->NativeFramework()->MarkPlaceAsDisused(g_editableMapObject); +} + JNIEXPORT jobjectArray JNICALL Java_app_organicmaps_sdk_editor_Editor_nativeGetAllCreatableFeatureTypes(JNIEnv * env, jclass clazz, jstring jLang) diff --git a/android/sdk/src/main/java/app/organicmaps/sdk/editor/Editor.java b/android/sdk/src/main/java/app/organicmaps/sdk/editor/Editor.java index b73198b55..9e99bfac8 100644 --- a/android/sdk/src/main/java/app/organicmaps/sdk/editor/Editor.java +++ b/android/sdk/src/main/java/app/organicmaps/sdk/editor/Editor.java @@ -99,6 +99,7 @@ public final class Editor public static native boolean nativeIsAddressEditable(); public static native boolean nativeIsNameEditable(); + public static native boolean nativeCanMarkPlaceAsDisused(); public static native boolean nativeIsPointType(); public static native boolean nativeIsBuilding(); @@ -164,6 +165,7 @@ public final class Editor public static native void nativeCreateNote(String text); public static native void nativePlaceDoesNotExist(@NonNull String comment); public static native void nativeRollbackMapObject(); + public static native void nativeMarkPlaceAsDisused(); public static native void nativeCreateStandaloneNote(double lat, double lon, String text); /** diff --git a/libs/editor/CMakeLists.txt b/libs/editor/CMakeLists.txt index c96cf4c63..33f94d1e0 100644 --- a/libs/editor/CMakeLists.txt +++ b/libs/editor/CMakeLists.txt @@ -33,6 +33,7 @@ set(SRC xml_feature.cpp xml_feature.hpp yes_no_unknown.hpp + keys_to_remove.hpp ) omim_add_library(${PROJECT_NAME} ${SRC}) diff --git a/libs/editor/changeset_wrapper.cpp b/libs/editor/changeset_wrapper.cpp index a26a6fcd2..44011d226 100644 --- a/libs/editor/changeset_wrapper.cpp +++ b/libs/editor/changeset_wrapper.cpp @@ -57,6 +57,9 @@ std::string GetTypeForFeature(editor::XMLFeature const & node) } } + if (node.HasTag("disused:shop") || node.HasTag("disused:amenity")) + return "vacant business"; + if (node.HasTag("addr:housenumber") || node.HasTag("addr:street") || node.HasTag("addr:postcode")) return "address"; diff --git a/libs/editor/keys_to_remove.hpp b/libs/editor/keys_to_remove.hpp new file mode 100644 index 000000000..f52c543aa --- /dev/null +++ b/libs/editor/keys_to_remove.hpp @@ -0,0 +1,135 @@ +#pragma once + +#include + +// Keys that should be removed when a place in OSM is replaced, copied from +// https://github.com/mnalis/StreetComplete-taginfo-categorize/blob/master/sc_to_remove.txt + +// Changes to the list: don't remove 'wheelchair' and addresses in the 'contact:' style + +inline constexpr std::string_view kKeysToRemove[] = { + "shop_?[1-9]?(:.*)?", "craft_?[1-9]?", "amenity_?[1-9]?", "club_?[1-9]?", "old_amenity", + "old_shop", "information", "leisure", "office_?[1-9]?", "tourism", + // popular shop=* / craft=* subkeys + "marketplace", "household", "swimming_pool", "laundry", "golf", "sports", "ice_cream", + "scooter", "music", "retail", "yes", "ticket", "newsagent", "lighting", "truck", "car_repair", + "car_parts", "video", "fuel", "farm", "car", "tractor", "hgv", "ski", "sculptor", + "hearing_aids", "surf", "photo", "boat", "gas", "kitchen", "anime", "builder", "hairdresser", + "security", "bakery", "bakehouse", "fishing", "doors", "kiosk", "market", "bathroom", "lamps", + "vacant", "insurance(:.*)?", "caravan", "gift", "bicycle", "bicycle_rental", "insulation", + "communication", "mall", "model", "empty", "wood", "hunting", "motorcycle", "trailer", + "camera", "water", "fireplace", "outdoor", "blacksmith", "electronics", "fan", "piercing", + "stationery", "sensory_friendly(:.*)?", "street_vendor", "sells(:.*)?", "safety_equipment", + // obsoleted information + "(demolished|abandoned|disused)(:(?!bui).+)?", "was:.*", "not:.*", "damage", "created_by", + "check_date", "opening_date", "last_checked", "checked_exists:date", "pharmacy_survey", + "old_ref", "update", "import_uuid", "review", "fixme:atp", + // classifications / links to external databases + "fhrs:.*", "old_fhrs:.*", "fvst:.*", "ncat", "nat_ref", "gnis:.*", "winkelnummer", + "type:FR:FINESS", "type:FR:APE", "kvl_hro:amenity", "ref:DK:cvr(:.*)?", "certifications?", + "transiscope", "opendata:type", "local_ref", "official_ref", + // names and identifications + "name_?[1-9]?(:.*)?", ".*_name_?[1-9]?(:.*)?", "noname", "branch(:.*)?", "brand(:.*)?", + "not:brand(:.*)?", "network(:.*)?", "operator(:.*)?", "operator_type", "ref", "ref:vatin", + "designation", "SEP:CLAVEESC", "identifier", "ref:FR:SIRET", "ref:FR:SIREN", "ref:FR:NAF", + "(old_)?ref:FR:prix-carburants", + // contacts + "contact_person", "phone(:.*)?", "phone_?[1-9]?", "emergency:phone", "emergency_telephone_code", + "contact:(?!housenumber$|street$|place$|postcode$|city$|country$|pobox$|unit$).*", + "mobile", "fax", "facebook", "instagram", "twitter", "youtube", "telegram", "tiktok", "email", + "website_?[1-9]?(:.*)?", "app:.*", "ownership", + "url", "url:official", "source_ref:url", "owner", + // payments + "payment(:.*)?", "payment_multi_fee", "currency(:.*)?", "cash_withdrawal(:.*)?", "fee", + "charge", "charge_fee", "money_transfer", "donation:compensation", "paypoint", + // generic shop/craft attributes + "seasonal", "time", "opening_hours(:.*)?", "check_(in|out)", "wifi", "internet", + "internet_access(:.*)?", "second_hand", "self_service", "automated", "license:.*", + "bulk_purchase", ".*:covid19", "language:.*", "baby_feeding", "description(:.*)?", + "description[0-9]", "min_age", "max_age", "supermarket(:.*)?", "social_facility(:.*)?", + "functional", "trade", "wholesale", "sale", "smoking(:outside)?", "zero_waste", "origin", + "attraction", "strapline", "dog", "showroom", "toilets?(:.*)?", "sanitary_dump_station", + "changing_table(:.*)?", "blind", "company(:.*)?", "stroller", "walk-in", + "webshop", "operational_status.*", "status", "drive_through", "surveillance(:.*)?", + "outdoor_seating", "indoor_seating", "colour", "access_simple", "floor", "product_category", + "guide", "source_url", "category", "kids_area", "kids_area:indoor", "resort", "since", "state", + "temporary", "self_checkout", "audio_loop", "related_law(:.*)?", "official_status(:.*)?", + // food and drink details + "bar", "cafe", "coffee", "microroasting", "microbrewery", "brewery", "real_ale", "taproom", + "training", "distillery", "drink(:.*)?", "cocktails", "alcohol", "wine([:_].*)?", + "happy_hours", "diet:.*", "cuisine", "ethnic", "tasting", "breakfast", "lunch", "organic", + "produced_on_site", "restaurant", "food", "pastry", "pastry_shop", "product", "produce", + "chocolate", "fair_trade", "butcher", "reservation(:.*)?", "takeaway(:.*)?", "delivery(:.*)?", + "caterer", "real_fire", "flour_fortified", "highchair", "fast_food", "pub", "snack", + "confectionery", "drinking_water:refill", + // related to repair shops/crafts + "service(:.*)?", "motorcycle:.*", "repair", ".*:repair", "electronics_repair(:.*)?", + "workshop", + // shop=hairdresser, shop=clothes + "unisex", "male", "female", "gender", "gender_simple", "lgbtq(:.*)?", "gay", "female:signed", + "male:signed", + // healthcare + "healthcare(:.*)?", "healthcare_.*", "health", "health_.*", "speciality", "medical_.*", + "emergency_ward", "facility(:.*)?", "activities", "healthcare_facility(:.*)?", + "laboratory(:.*)?", "blood(:.*)?", "blood_components", "infection(:.*)?", "disease(:.*)?", + "covid19(:.*)?", "COVID_.*", "CovidVaccineCenterId", "coronaquarantine", "hospital(:.*)?", + "hospital_type_id", "emergency_room", "sample_collection(:.*)?", "bed_count", "capacity:beds", + "part_time_beds", "personnel:count", "staff_count(:.*)?", "admin_staff", "doctors", + "doctors_num", "nurses_num", "counselling_type", "testing_centres", "toilets_number", + "urgent_care", "vaccination", "clinic", "hospital", "pharmacy", "alternative", "laboratory", + "sample_collection", "provided_for(:.*)?", "social_facility_for", "ambulance", "ward", + "HSE_(code|hgid|hgroup|region)", "collection_centre", "design", "AUTORIZATIE", "reg_id", + "post_addr", "scope", "ESTADO", "NIVSOCIO", "NO", "EMP_EST", "COD_HAB", "CLA_PERS", "CLA_PRES", + "snis_code:.*", "hfac_bed", "hfac_type", "nature", "moph_code", "IJSN:.*", "massgis:id", + "OGD-Stmk:.*", "paho:.*", "panchayath", "pbf_contract", "pcode", "pe:minsa:.*", "who:.*", + "pharmacy:category", "tactile_paving", "HF_(ID|TYPE|N_EN)", "RoadConn", "bin", "hiv(:.*)?", + // accommodation & layout + "rooms", "stars", "accommodation", "beds", "capacity(:persons)?", "laundry_service", + "guest_house", + // amenity=place_of_worship + "deanery", "subject:(wikidata|wikipedia|wikimedia_commons)", "church", "church:type", + // schools + "capacity:(pupils|teachers)", "grades", "population:pupils(:.*)?", + "school:(FR|gender|trust|type|type_idn|group:type)", "primary", + // clubs + "animal(_breeding|_training)?", "billiards(:.*)?", "board_game", "sport_1", "sport:boating", + "boat:type", "canoe(_rental|:service)?", "kayak(_rental|:service)?", + "sailboat(_rental|:service)?", "horse_riding", "rugby", "boules", "callsign", "card_games", + "car_service", "catastro:ref", "chess(:.*)?", "children", "climbing(:.*)?", "club(:.*)?", + "communication(:amateur_radio.*)", "community_centre:for", "dffr:network", "dormitory", + "education_for:ages", "electrified", "esperanto", "events_venue", "family", "federation", + "free_flying(:.*)?", "freemasonry(:.*)?", "free_refill", "gaelic_games(:.*)?", "membership", + "military_service", "model_aerodrome(:.*)?", "mode_of_organisation(:.*)?", "snowmobile", + "social_centre(:for)?", "source_dat", "tennis", "old_website", "organisation", "school_type", + "scout(:type)?", "fraternity", "live_music", "lockable", "playground(:theme)?", "nudism", + "music_genre", "length", "fire_station:type:FR", "cadet", "observatory:type", "tower:type", + "zoo", "shooting", "commons", "groomer", "group_only", "hazard", "identity", "interaction", + "logo", "maxheight", "provides", "regional", "scale", "site", "plots", "allotments", + "local_food", "monitoring:pedestrian", "recording:automated", "yacht", "background_music", + "url:spaceapi", "openfire", "fraternity(:.*)?", + // misc specific attributes + "clothes", "shoes", "tailor", "beauty", "tobacco", "carpenter", "furniture", "lottery", + "sport", "dispensing", "tailor:.*", "gambling", "material", "raw_material", "stonemason", + "studio", "scuba_diving(:.*)?", "polling_station", "collector", "books", "agrarian", + "musical_instrument", "massage", "parts", "post_office(:.*)?", "religion", "denomination", + "rental", ".*:rental", "tickets:.*", "public_transport", "goods_supply", "pet", "appliance", + "artwork_type", "charity", "company", "crop", "dry_cleaning", "factory", "feature", + "air_conditioning", "atm", "vending", "vending_machine", "recycling_type", "museum", + "license_classes", "dance:.*", "isced:level", "school", "preschool", "university", + "research_institution", "research", "member_of", "topic", "townhall:type", "parish", "police", + "government", "thw:(lv|rb|ltg)", "office", "administration", "administrative", "association", + "transport", "utility", "consulting", "Commercial", "commercial", "private", "taxi", + "admin_level", "official_status", "target", "liaison", "diplomatic(:.*)?", "embassy", + "consulate", "aeroway", "department", "faculty", "aerospace:product", "boundary", "population", + "diocese", "depot", "cargo", "function", "game", "party", "political_party.*", + "telecom(munication)?", "service_times", "kitchen:facilities", "it:(type|sales)", + "cannabis:cbd", "bath:type", "bath:(open_air|sand_bath)", "animal_boarding", "animal_shelter", + "mattress", "screen", "monitoring:weather", "public", "theatre", "culture", "library", + "cooperative(:.*)?", "winery", "curtain", "lawyer(:.*)?", "local_authority(:.*)?", "equipment", + "hackerspace", + "camp_site", "camping", "bbq", "static_caravans", "emergency(:.*)?", "evacuation_cent(er|re)", + "education", "engineering", "forestry", "foundation", "lawyer", "logistics", "military", + "community_centre", "bank", "operational", "users_(PLWD|boy|elderly|female|girl|men)", + "Comments?", "comments?", "entrance:(width|step_count|kerb:height)", "fenced", "motor_vehicle", + "shelter", +}; diff --git a/libs/editor/osm_editor.cpp b/libs/editor/osm_editor.cpp index 6ddcaca72..b74d11c18 100644 --- a/libs/editor/osm_editor.cpp +++ b/libs/editor/osm_editor.cpp @@ -668,7 +668,7 @@ void Editor::UploadChanges(string const & oauthToken, ChangesetTags tags, Finish {} // Add tags to XMLFeature - UpdateXMLFeatureTags(feature, journal); + UpdateXMLFeatureTags(feature, journal, changeset); // Upload XMLFeature to OSM LOG(LDEBUG, ("CREATE Feature (newEditor)", feature)); @@ -686,7 +686,7 @@ void Editor::UploadChanges(string const & oauthToken, ChangesetTags tags, Finish XMLFeature feature = GetMatchingFeatureFromOSM(changeset, fti.m_object); // Update tags of XMLFeature - UpdateXMLFeatureTags(feature, journal); + UpdateXMLFeatureTags(feature, journal, changeset); // Upload XMLFeature to OSM LOG(LDEBUG, ("MODIFIED Feature (newEditor)", feature)); @@ -1321,7 +1321,8 @@ bool Editor::IsFeatureUploadedImpl(FeaturesContainer const & features, MwmId con return info && info->m_uploadStatus == kUploaded; } -void Editor::UpdateXMLFeatureTags(editor::XMLFeature & feature, std::list const & journal) +void Editor::UpdateXMLFeatureTags(editor::XMLFeature & feature, std::list const & journal, + ChangesetWrapper & changeset) { for (JournalEntry const & entry : journal) { @@ -1335,6 +1336,13 @@ void Editor::UpdateXMLFeatureTags(editor::XMLFeature & feature, std::list(entry.data); + feature.OSMBusinessReplacement(businessReplacementData.old_type, businessReplacementData.new_type); + changeset.AddChangesetTag("info:place_marked_as_disused", "yes"); + break; + } } } } diff --git a/libs/editor/osm_editor.hpp b/libs/editor/osm_editor.hpp index f412579bf..a08aca5ca 100644 --- a/libs/editor/osm_editor.hpp +++ b/libs/editor/osm_editor.hpp @@ -1,5 +1,6 @@ #pragma once +#include "editor/changeset_wrapper.hpp" #include "editor/config_loader.hpp" #include "editor/editor_config.hpp" #include "editor/editor_notes.hpp" @@ -241,7 +242,8 @@ private: static bool IsFeatureUploadedImpl(FeaturesContainer const & features, MwmId const & mwmId, uint32_t index); - void UpdateXMLFeatureTags(editor::XMLFeature & feature, std::list const & journal); + static void UpdateXMLFeatureTags(editor::XMLFeature & feature, std::list const & journal, + ChangesetWrapper & changeset); /// Deleted, edited and created features. base::AtomicSharedPtr m_features; diff --git a/libs/editor/xml_feature.cpp b/libs/editor/xml_feature.cpp index 5978bfc63..df737bbf1 100644 --- a/libs/editor/xml_feature.cpp +++ b/libs/editor/xml_feature.cpp @@ -1,4 +1,5 @@ #include "editor/xml_feature.hpp" +#include "editor/keys_to_remove.hpp" #include "indexer/classificator.hpp" #include "indexer/editable_map_object.hpp" @@ -15,6 +16,7 @@ #include "base/timer.hpp" #include +#include #include #include @@ -502,6 +504,29 @@ osm::EditJournal XMLFeature::GetEditJournal() const entry.data = legacyObjData; break; } + case osm::JournalEntryType::BusinessReplacement: + { + osm::BusinessReplacementData businessReplacementData; + + // Old Feature Type + std::string old_strType = getAttribute(xmlData, "old_type"); + if (old_strType.empty()) + MYTHROW(editor::InvalidJournalEntry, ("Old Feature type is empty")); + businessReplacementData.old_type = classif().GetTypeByReadableObjectName(old_strType); + if (businessReplacementData.old_type == IndexAndTypeMapping::INVALID_TYPE) + MYTHROW(editor::InvalidJournalEntry, ("Invalid old Feature Type:", old_strType)); + + // New Feature Type + std::string new_strType = getAttribute(xmlData, "new_type"); + if (new_strType.empty()) + MYTHROW(editor::InvalidJournalEntry, ("New Feature type is empty")); + businessReplacementData.new_type = classif().GetTypeByReadableObjectName(new_strType); + if (businessReplacementData.new_type == IndexAndTypeMapping::INVALID_TYPE) + MYTHROW(editor::InvalidJournalEntry, ("Invalid new Feature Type:", new_strType)); + + entry.data = businessReplacementData; + break; + } } if (isHistory) journal.AddJournalHistoryEntry(entry); @@ -572,6 +597,14 @@ void XMLFeature::SetEditJournal(osm::EditJournal const & journal) xmlData.append_attribute("version") = legacyObjData.version.data(); break; } + case osm::JournalEntryType::BusinessReplacement: + { + osm::BusinessReplacementData const & businessReplacementData = + std::get(entry.data); + xmlData.append_attribute("old_type") = classif().GetReadableObjectName(businessReplacementData.old_type).data(); + xmlData.append_attribute("new_type") = classif().GetReadableObjectName(businessReplacementData.new_type).data(); + break; + } } } }; @@ -675,6 +708,52 @@ void XMLFeature::UpdateOSMTag(std::string_view key, std::string_view value) } } +void XMLFeature::OSMBusinessReplacement(uint32_t old_type, uint32_t new_type) +{ + std::string name = GetTagValue("name"); + + // Remove OSM tags using the list from keys_to_remove.hpp + static boost::regex const regex([] + { + std::string regexPattern; + + for (auto const & key : kKeysToRemove) + { + if (!regexPattern.empty()) + regexPattern.append("|"); + regexPattern.append(key); + } + return regexPattern; + }()); + + ForEachTag([this](std::string_view key, std::string_view /*value*/) + { + if (boost::regex_match(key.begin(), key.end(), regex)) + RemoveTag(key); + }); + + if (classif().GetReadableObjectName(new_type) == "disusedbusiness") + { + // Mark as 'disused' + string const strOldType = classif().GetReadableObjectName(old_type); + strings::SimpleTokenizer iter(strOldType, "-"); + string_view const key = *iter; + if (++iter) + SetTagValue("disused:" + std::string(key), *iter); + else + SetTagValue("disused:" + std::string(key), "yes"); + + if (!name.empty()) + SetTagValue("old_name", name); + } + else + { + // Add new category tag + ASSERT_FAIL("Only marking places as 'disused' is implemented yet. Wrong new_type: " + + classif().GetReadableObjectName(new_type)); + } +} + string XMLFeature::GetAttribute(string const & key) const { return GetRootNode().attribute(key.data()).value(); diff --git a/libs/editor/xml_feature.hpp b/libs/editor/xml_feature.hpp index 8e2917cc2..6025ce9ef 100644 --- a/libs/editor/xml_feature.hpp +++ b/libs/editor/xml_feature.hpp @@ -187,6 +187,8 @@ public: /// Wrapper for SetTagValue and RemoveTag, avoids duplication for similar alternative osm tags void UpdateOSMTag(std::string_view key, std::string_view value); + /// Replace an old business with a new business + void OSMBusinessReplacement(uint32_t old_type, uint32_t new_type); std::string GetAttribute(std::string const & key) const; void SetAttribute(std::string const & key, std::string const & value); diff --git a/libs/indexer/edit_journal.cpp b/libs/indexer/edit_journal.cpp index 7b654e51e..5b458da8e 100644 --- a/libs/indexer/edit_journal.cpp +++ b/libs/indexer/edit_journal.cpp @@ -41,6 +41,14 @@ void EditJournal::MarkAsCreated(uint32_t type, feature::GeomType geomType, m2::P AddJournalEntry({JournalEntryType::ObjectCreated, time(nullptr), osm::ObjCreateData{type, geomType, mercator}}); } +void EditJournal::AddBusinessReplacement(uint32_t old_type, uint32_t new_type) +{ + LOG(LDEBUG, ("Business of type ", classif().GetReadableObjectName(old_type), " was replaced by a ", + classif().GetReadableObjectName(new_type))); + AddJournalEntry( + {JournalEntryType::BusinessReplacement, time(nullptr), osm::BusinessReplacementData{old_type, new_type}}); +} + void EditJournal::AddJournalEntry(JournalEntry entry) { m_journal.push_back(std::move(entry)); @@ -103,6 +111,15 @@ std::string EditJournal::ToString(osm::JournalEntry const & journalEntry) LegacyObjData const & legacyObjData = std::get(journalEntry.data); return ToString(journalEntry.journalEntryType).append(": version=\"").append(legacyObjData.version).append("\""); } + case osm::JournalEntryType::BusinessReplacement: + { + BusinessReplacementData const & businessReplacementData = std::get(journalEntry.data); + return ToString(journalEntry.journalEntryType) + .append(": Category changed from ") + .append(classif().GetReadableObjectName(businessReplacementData.old_type)) + .append(" to ") + .append(classif().GetReadableObjectName(businessReplacementData.new_type)); + } default: UNREACHABLE(); } } @@ -114,6 +131,7 @@ std::string EditJournal::ToString(osm::JournalEntryType journalEntryType) case osm::JournalEntryType::TagModification: return "TagModification"; case osm::JournalEntryType::ObjectCreated: return "ObjectCreated"; case osm::JournalEntryType::LegacyObject: return "LegacyObject"; + case osm::JournalEntryType::BusinessReplacement: return "BusinessReplacement"; default: UNREACHABLE(); } } @@ -126,6 +144,8 @@ std::optional EditJournal::TypeFromString(std::string const & return JournalEntryType::ObjectCreated; else if (entryType == "LegacyObject") return JournalEntryType::LegacyObject; + else if (entryType == "BusinessReplacement") + return JournalEntryType::BusinessReplacement; else return {}; } diff --git a/libs/indexer/edit_journal.hpp b/libs/indexer/edit_journal.hpp index 2e1cf5b24..edb27bfc0 100644 --- a/libs/indexer/edit_journal.hpp +++ b/libs/indexer/edit_journal.hpp @@ -16,6 +16,7 @@ enum class JournalEntryType TagModification, ObjectCreated, LegacyObject, // object without full journal history, used for transition to new editor + BusinessReplacement, // Possible future values: ObjectDeleted, ObjectDisused, ObjectNotDisused, LocationChanged, FeatureTypeChanged }; @@ -38,11 +39,17 @@ struct LegacyObjData std::string version; }; +struct BusinessReplacementData +{ + uint32_t old_type; + uint32_t new_type; +}; + struct JournalEntry { JournalEntryType journalEntryType = JournalEntryType::TagModification; time_t timestamp; - std::variant data; + std::variant data; }; /// Used to determine whether existing OSM object should be updated or new one created @@ -69,6 +76,9 @@ public: /// Log object creation in the journal void MarkAsCreated(uint32_t type, feature::GeomType geomType, m2::PointD mercator); + /// Log business replacement in the journal + void AddBusinessReplacement(uint32_t old_type, uint32_t new_type); + void AddJournalEntry(JournalEntry entry); /// Clear Journal and move content to journalHistory, used after upload to OSM diff --git a/libs/indexer/editable_map_object.cpp b/libs/indexer/editable_map_object.cpp index 53856ef0d..68a26567e 100644 --- a/libs/indexer/editable_map_object.cpp +++ b/libs/indexer/editable_map_object.cpp @@ -87,6 +87,32 @@ vector EditableMapObject::GetEditableProperties() const return props; } +bool EditableMapObject::CanMarkPlaceAsDisused() const +{ + if (GetEditingLifecycle() == EditingLifecycle::CREATED) + return false; + + auto types = GetTypes(); + types.SortBySpec(); + uint32_t mainType = *types.begin(); + std::string mainTypeStr = classif().GetReadableObjectName(mainType); + + constexpr string_view typePrefixes[] = { + "shop", + "amenity-restaurant", + "amenity-fast_food", + "amenity-cafe", + "amenity-pub", + "amenity-bar", + }; + + for (auto const & typePrefix : typePrefixes) + if (mainTypeStr.starts_with(typePrefix)) + return true; + + return false; +} + NamesDataSource EditableMapObject::GetNamesDataSource() { auto const mwmInfo = GetID().m_mwmId.GetInfo(); @@ -656,6 +682,16 @@ void EditableMapObject::MarkAsCreated(uint32_t type, feature::GeomType geomType, m_journal.MarkAsCreated(type, geomType, std::move(mercator)); } +void EditableMapObject::MarkAsDisused() +{ + auto types = GetTypes(); + types.SortBySpec(); + uint32_t old_type = *types.begin(); + uint32_t new_type = classif().GetTypeByReadableObjectName("disusedbusiness"); + ApplyBusinessReplacement(new_type); + m_journal.AddBusinessReplacement(old_type, new_type); +} + void EditableMapObject::ClearJournal() { m_journal.Clear(); @@ -673,7 +709,7 @@ void EditableMapObject::ApplyEditsFromJournal(EditJournal const & editJournal) void EditableMapObject::ApplyJournalEntry(JournalEntry const & entry) { LOG(LDEBUG, ("Applying Journal Entry: ", osm::EditJournal::ToString(entry))); - // Todo + switch (entry.journalEntryType) { case JournalEntryType::TagModification: @@ -760,6 +796,12 @@ void EditableMapObject::ApplyJournalEntry(JournalEntry const & entry) ASSERT_FAIL(("Legacy Objects can not be loaded from Journal")); break; } + case JournalEntryType::BusinessReplacement: + { + BusinessReplacementData const & businessReplacementData = std::get(entry.data); + ApplyBusinessReplacement(businessReplacementData.new_type); + break; + } } } @@ -859,6 +901,47 @@ void EditableMapObject::LogDiffInJournal(EditableMapObject const & unedited_emo) } } +void EditableMapObject::ApplyBusinessReplacement(uint32_t new_type) +{ + // Types + feature::TypesHolder new_feature_types; + + new_feature_types.Add(new_type); // Update feature type + + std::string wheelchairType = feature::GetReadableWheelchairType(m_types); + if (!wheelchairType.empty()) + new_feature_types.SafeAdd(classif().GetTypeByReadableObjectName(wheelchairType)); + + std::vector const buildingTypes = ftypes::IsBuildingChecker::Instance().GetTypes(); + for (uint32_t const & type : buildingTypes) + if (m_types.Has(type)) + new_feature_types.SafeAdd(type); + + m_types = new_feature_types; + + // Names + m_name.Clear(); + + // Metadata + feature::Metadata new_metadata; + + constexpr MetadataID metadataToKeep[] = { + MetadataID::FMD_WHEELCHAIR, + MetadataID::FMD_POSTCODE, + MetadataID::FMD_LEVEL, + MetadataID::FMD_ELE, + MetadataID::FMD_HEIGHT, + MetadataID::FMD_MIN_HEIGHT, + MetadataID::FMD_BUILDING_LEVELS, + MetadataID::FMD_BUILDING_MIN_LEVEL + }; + + for (MetadataID const & metadataID : metadataToKeep) + new_metadata.Set(metadataID, std::string(m_metadata.Get(metadataID))); + + m_metadata = new_metadata; +} + bool AreObjectsEqualIgnoringStreet(EditableMapObject const & lhs, EditableMapObject const & rhs) { feature::TypesHolder const & lhsTypes = lhs.GetTypes(); diff --git a/libs/indexer/editable_map_object.hpp b/libs/indexer/editable_map_object.hpp index 90c8170a0..ffa2a8fe4 100644 --- a/libs/indexer/editable_map_object.hpp +++ b/libs/indexer/editable_map_object.hpp @@ -78,6 +78,8 @@ public: /// All store/load/valid operations will be via MetadataEntryIFace interface instead of switch-case. std::vector GetEditableProperties() const; + bool CanMarkPlaceAsDisused() const; + /// See comment for NamesDataSource class. NamesDataSource GetNamesDataSource(); LocalizedStreet const & GetStreet() const; @@ -141,11 +143,16 @@ public: void SetJournal(EditJournal && editJournal); EditingLifecycle GetEditingLifecycle() const; void MarkAsCreated(uint32_t type, feature::GeomType geomType, m2::PointD mercator); + void MarkAsDisused(); void ClearJournal(); void ApplyEditsFromJournal(EditJournal const & journal); void ApplyJournalEntry(JournalEntry const & entry); void LogDiffInJournal(EditableMapObject const & unedited_emo); +private: + void ApplyBusinessReplacement(uint32_t new_type); + +public: /// Check whether langCode can be used as default name. static bool CanUseAsDefaultName(int8_t const langCode, std::vector const & nativeMwmLanguages); diff --git a/libs/map/framework.cpp b/libs/map/framework.cpp index 7bc3e5136..ba683261f 100644 --- a/libs/map/framework.cpp +++ b/libs/map/framework.cpp @@ -3091,6 +3091,13 @@ void Framework::DeleteFeature(FeatureID const & fid) UpdatePlacePageInfoForCurrentSelection(); } +void Framework::MarkPlaceAsDisused(osm::EditableMapObject emo) +{ + emo.MarkAsDisused(); + osm::Editor::Instance().SaveEditedFeature(emo); + UpdatePlacePageInfoForCurrentSelection(); +} + osm::NewFeatureCategories Framework::GetEditorCategories() const { return osm::Editor::Instance().GetNewFeatureCategories(); diff --git a/libs/map/framework.hpp b/libs/map/framework.hpp index 5b8f5c61e..8abb43cbd 100644 --- a/libs/map/framework.hpp +++ b/libs/map/framework.hpp @@ -755,6 +755,7 @@ public: bool GetEditableMapObject(FeatureID const & fid, osm::EditableMapObject & emo) const; osm::Editor::SaveResult SaveEditedMapObject(osm::EditableMapObject emo); void DeleteFeature(FeatureID const & fid); + void MarkPlaceAsDisused(osm::EditableMapObject emo); osm::NewFeatureCategories GetEditorCategories() const; bool RollBackChanges(FeatureID const & fid); void CreateNote(osm::MapObject const & mapObject, osm::Editor::NoteProblemType const type, std::string const & note); diff --git a/xcode/editor/editor.xcodeproj/project.pbxproj b/xcode/editor/editor.xcodeproj/project.pbxproj index e43c070a2..f9fc2ecce 100644 --- a/xcode/editor/editor.xcodeproj/project.pbxproj +++ b/xcode/editor/editor.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 271DC2172EC60C0C00442D94 /* keys_to_remove.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 271DC2162EC60C0C00442D94 /* keys_to_remove.hpp */; }; 340C20DE1C3E4DFD00111D22 /* osm_auth.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 340C20DC1C3E4DFD00111D22 /* osm_auth.cpp */; }; 340C20DF1C3E4DFD00111D22 /* osm_auth.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 340C20DD1C3E4DFD00111D22 /* osm_auth.hpp */; }; 340DC8291C4E71E500EAA2CC /* changeset_wrapper.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 340DC8271C4E71E500EAA2CC /* changeset_wrapper.cpp */; }; @@ -75,6 +76,7 @@ /* Begin PBXFileReference section */ 270C9C212E16AABF00ABA688 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; name = module.modulemap; path = ../../libs/editor/module.modulemap; sourceTree = SOURCE_ROOT; }; + 271DC2162EC60C0C00442D94 /* keys_to_remove.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = keys_to_remove.hpp; sourceTree = ""; }; 340C20DC1C3E4DFD00111D22 /* osm_auth.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = osm_auth.cpp; sourceTree = ""; }; 340C20DD1C3E4DFD00111D22 /* osm_auth.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = osm_auth.hpp; sourceTree = ""; }; 340DC8271C4E71E500EAA2CC /* changeset_wrapper.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = changeset_wrapper.cpp; sourceTree = ""; }; @@ -240,6 +242,7 @@ 6715560420BEC331002BA3B4 /* edits_migration.hpp */, 3D052486200F62ED00F24998 /* feature_matcher.cpp */, 3D052485200F62ED00F24998 /* feature_matcher.hpp */, + 271DC2162EC60C0C00442D94 /* keys_to_remove.hpp */, 6715565220BF0F86002BA3B4 /* new_feature_categories.cpp */, 6715565320BF0F87002BA3B4 /* new_feature_categories.hpp */, 341138741C15AE42002E3B3E /* opening_hours_ui.cpp */, @@ -337,6 +340,7 @@ 6715565520BF0F87002BA3B4 /* new_feature_categories.hpp in Headers */, 6715560820BEC332002BA3B4 /* edits_migration.hpp in Headers */, 3D052487200F62EE00F24998 /* feature_matcher.hpp in Headers */, + 271DC2172EC60C0C00442D94 /* keys_to_remove.hpp in Headers */, 3411387B1C15AE42002E3B3E /* ui2oh.hpp in Headers */, 341138791C15AE42002E3B3E /* opening_hours_ui.hpp in Headers */, );