co-maps/libs/indexer/editable_map_object.cpp

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

969 lines
28 KiB
C++
Raw Normal View History

2025-11-22 13:58:55 +01:00
#include "indexer/editable_map_object.hpp"
#include "indexer/classificator.hpp"
#include "indexer/edit_journal.hpp"
#include "indexer/feature_charge_sockets.hpp"
#include "indexer/feature_meta.hpp"
#include "indexer/ftypes_matcher.hpp"
#include "indexer/postcodes_matcher.hpp"
#include "indexer/validate_and_format_contacts.hpp"
#include "platform/preferred_languages.hpp"
#include "base/control_flow.hpp"
#include "base/string_utils.hpp"
#include <algorithm>
#include <cmath>
#include <regex>
#include <sstream>
namespace osm
{
using namespace std;
namespace
{
bool ExtractName(StringUtf8Multilang const & names, int8_t const langCode, vector<osm::LocalizedName> & result)
{
if (StringUtf8Multilang::kUnsupportedLanguageCode == langCode)
return false;
// Exclude languages that are already present.
auto const it = base::FindIf(
result, [langCode](osm::LocalizedName const & localizedName) { return localizedName.m_code == langCode; });
if (result.end() != it)
return false;
string_view name;
names.GetString(langCode, name);
result.emplace_back(langCode, name);
return true;
}
} // namespace
// LocalizedName -----------------------------------------------------------------------------------
LocalizedName::LocalizedName(int8_t const code, string_view name)
: m_code(code)
, m_lang(StringUtf8Multilang::GetLangByCode(code))
, m_langName(StringUtf8Multilang::GetLangNameByCode(code))
, m_name(name)
{}
LocalizedName::LocalizedName(string const & langCode, string const & name)
: m_code(StringUtf8Multilang::GetLangIndex(langCode))
, m_lang(StringUtf8Multilang::GetLangByCode(m_code))
, m_langName(StringUtf8Multilang::GetLangNameByCode(m_code))
, m_name(name)
{}
// EditableMapObject -------------------------------------------------------------------------------
bool EditableMapObject::IsNameEditable() const
{
return m_editableProperties.m_name;
}
bool EditableMapObject::IsAddressEditable() const
{
return m_editableProperties.m_address;
}
vector<MapObject::MetadataID> EditableMapObject::GetEditableProperties() const
{
auto props = m_editableProperties.m_metadata;
if (m_editableProperties.m_cuisine)
{
// Props are already sorted by Metadata::EType value.
auto insertBefore = props.begin();
if (insertBefore != props.end() && *insertBefore == MetadataID::FMD_OPEN_HOURS)
++insertBefore;
props.insert(insertBefore, MetadataID::FMD_CUISINE);
}
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();
if (!mwmInfo)
return NamesDataSource();
vector<int8_t> mwmLanguages;
mwmInfo->GetRegionData().GetLanguages(mwmLanguages);
auto const userLangCode = StringUtf8Multilang::GetLangIndex(languages::GetCurrentMapLanguage());
return GetNamesDataSource(m_name, mwmLanguages, userLangCode);
}
// static
NamesDataSource EditableMapObject::GetNamesDataSource(StringUtf8Multilang const & source,
vector<int8_t> const & mwmLanguages, int8_t const userLangCode)
{
NamesDataSource result;
auto & names = result.names;
auto & mandatoryCount = result.mandatoryNamesCount;
// Push default/native for country language.
if (ExtractName(source, StringUtf8Multilang::kDefaultCode, names))
++mandatoryCount;
// Push other languages.
source.ForEach([&names, mandatoryCount](int8_t const code, string_view name)
{
auto const mandatoryNamesEnd = names.begin() + mandatoryCount;
// Exclude languages which are already in container (languages with top priority).
auto const it = find_if(names.begin(), mandatoryNamesEnd,
[code](LocalizedName const & localizedName) { return localizedName.m_code == code; });
if (mandatoryNamesEnd == it)
names.emplace_back(code, name);
});
return result;
}
vector<LocalizedStreet> const & EditableMapObject::GetNearbyStreets() const
{
return m_nearbyStreets;
}
void EditableMapObject::ForEachMetadataItem(function<void(string_view tag, string_view value)> const & fn) const
{
m_metadata.ForEach([&fn](MetadataID type, std::string_view value)
{
switch (type)
{
// Multilang description may produce several tags with different values.
case MetadataID::FMD_DESCRIPTION:
{
auto const mlDescr = StringUtf8Multilang::FromBuffer(std::string(value));
mlDescr.ForEach([&fn](int8_t code, string_view v)
{
if (code == StringUtf8Multilang::kDefaultCode)
fn("description", v);
else
fn(string("description:").append(StringUtf8Multilang::GetLangByCode(code)), v);
});
break;
}
// Skip non-string values (they are not related to OSM anyway).
case MetadataID::FMD_CUSTOM_IDS:
case MetadataID::FMD_PRICE_RATES:
case MetadataID::FMD_RATINGS:
case MetadataID::FMD_EXTERNAL_URI:
case MetadataID::FMD_WHEELCHAIR: // Value is runtime only, data is taken from the classificator types, should not
// be used to update the OSM database
case MetadataID::FMD_CHARGE_SOCKETS: // multiple keys; handled via the edit journal
break;
default: fn(ToString(type), value); break;
}
});
}
void EditableMapObject::SetTestId(uint64_t id)
{
m_metadata.Set(feature::Metadata::FMD_TEST_ID, std::to_string(id));
}
void EditableMapObject::SetEditableProperties(osm::EditableProperties const & props)
{
m_editableProperties = props;
}
void EditableMapObject::SetName(StringUtf8Multilang const & name)
{
m_name = name;
}
void EditableMapObject::SetName(string_view name, int8_t langCode)
{
strings::Trim(name);
m_name.AddString(langCode, name);
}
// static
bool EditableMapObject::CanUseAsDefaultName(int8_t const lang, vector<int8_t> const & mwmLanguages)
{
for (auto const & mwmLang : mwmLanguages)
{
if (StringUtf8Multilang::kUnsupportedLanguageCode == mwmLang)
continue;
if (lang == mwmLang)
return true;
}
return false;
}
void EditableMapObject::SetMercator(m2::PointD const & center)
{
m_mercator = center;
}
void EditableMapObject::SetType(uint32_t featureType)
{
if (m_types.GetGeomType() == feature::GeomType::Undefined)
{
// Support only point type for newly created features.
m_types = feature::TypesHolder(feature::GeomType::Point);
m_types.Assign(featureType);
}
else
{
// Correctly replace "main" type in cases when feature holds more types.
ASSERT(!m_types.Empty(), ());
feature::TypesHolder copy = m_types;
// TODO(mgsergio): Replace by correct sorting from editor's config.
copy.SortBySpec();
m_types.Remove(*copy.begin());
m_types.Add(featureType);
}
}
void EditableMapObject::SetTypes(feature::TypesHolder const & types)
{
m_types = types;
}
void EditableMapObject::SetID(FeatureID const & fid)
{
m_featureID = fid;
}
void EditableMapObject::SetStreet(LocalizedStreet const & st)
{
m_street = st;
}
void EditableMapObject::SetNearbyStreets(vector<LocalizedStreet> && streets)
{
m_nearbyStreets = std::move(streets);
}
void EditableMapObject::SetHouseNumber(string const & houseNumber)
{
m_houseNumber = houseNumber;
}
void EditableMapObject::SetPostcode(std::string const & postcode)
{
m_metadata.Set(MetadataID::FMD_POSTCODE, postcode);
}
bool EditableMapObject::IsValidMetadata(MetadataID type, std::string const & value)
{
switch (type)
{
case MetadataID::FMD_WEBSITE: return ValidateWebsite(value);
case MetadataID::FMD_WEBSITE_MENU: return ValidateWebsite(value);
case MetadataID::FMD_CONTACT_FACEBOOK: return ValidateFacebookPage(value);
case MetadataID::FMD_CONTACT_INSTAGRAM: return ValidateInstagramPage(value);
case MetadataID::FMD_CONTACT_TWITTER: return ValidateTwitterPage(value);
case MetadataID::FMD_CONTACT_VK: return ValidateVkPage(value);
case MetadataID::FMD_CONTACT_LINE: return ValidateLinePage(value);
case MetadataID::FMD_CONTACT_FEDIVERSE: return ValidateFediversePage(value);
case MetadataID::FMD_CONTACT_BLUESKY: return ValidateBlueskyPage(value);
case MetadataID::FMD_STARS:
{
uint32_t stars;
return strings::to_uint(value, stars) && stars > 0 && stars <= feature::kMaxStarsCount;
}
case MetadataID::FMD_ELE:
{
/// @todo Reuse existing validadors in generator (osm2meta).
double ele;
return strings::to_double(value, ele) && ele > -11000 && ele < 9000;
}
case MetadataID::FMD_BUILDING_LEVELS: return ValidateBuildingLevels(value);
case MetadataID::FMD_LEVEL: return ValidateLevel(value);
case MetadataID::FMD_FLATS: return ValidateFlats(value);
case MetadataID::FMD_POSTCODE: return ValidatePostCode(value);
case MetadataID::FMD_PHONE_NUMBER: return ValidatePhoneList(value);
case MetadataID::FMD_EMAIL: return ValidateEmail(value);
default: return true;
}
}
void EditableMapObject::SetMetadata(MetadataID type, std::string value)
{
switch (type)
{
case MetadataID::FMD_WEBSITE: value = ValidateAndFormat_website(value); break;
case MetadataID::FMD_WEBSITE_MENU: value = ValidateAndFormat_website(value); break;
case MetadataID::FMD_CONTACT_FACEBOOK: value = ValidateAndFormat_facebook(value); break;
case MetadataID::FMD_CONTACT_INSTAGRAM: value = ValidateAndFormat_instagram(value); break;
case MetadataID::FMD_CONTACT_TWITTER: value = ValidateAndFormat_twitter(value); break;
case MetadataID::FMD_CONTACT_VK: value = ValidateAndFormat_vk(value); break;
case MetadataID::FMD_CONTACT_LINE: value = ValidateAndFormat_contactLine(value); break;
case MetadataID::FMD_CONTACT_FEDIVERSE: value = ValidateAndFormat_fediverse(value); break;
case MetadataID::FMD_CONTACT_BLUESKY: value = ValidateAndFormat_bluesky(value); break;
default: break;
}
m_metadata.Set(type, std::move(value));
}
bool EditableMapObject::UpdateMetadataValue(string_view key, string value)
{
MetadataID type;
if (!feature::Metadata::TypeFromString(key, type))
return false;
SetMetadata(type, std::move(value));
return true;
}
void EditableMapObject::SetOpeningHours(std::string oh)
{
m_metadata.Set(MetadataID::FMD_OPEN_HOURS, std::move(oh));
}
void EditableMapObject::SetChargeSockets(std::string sockets)
{
// parse the list of sockets provided by the frontend, and re-generate the
// socket list, thus ensuring it is valid & sorted.
ChargeSocketsHelper helper(sockets);
m_metadata.Set(MetadataID::FMD_CHARGE_SOCKETS, helper.ToString());
}
void EditableMapObject::SetInternet(feature::Internet internet)
{
m_metadata.Set(MetadataID::FMD_INTERNET, DebugPrint(internet));
uint32_t const wifiType = ftypes::IsWifiChecker::Instance().GetType();
bool const hasWiFi = m_types.Has(wifiType);
if (hasWiFi && internet != feature::Internet::Wlan)
m_types.Remove(wifiType);
else if (!hasWiFi && internet == feature::Internet::Wlan)
m_types.SafeAdd(wifiType);
}
LocalizedStreet const & EditableMapObject::GetStreet() const
{
return m_street;
}
template <class T>
void EditableMapObject::SetCuisinesImpl(vector<T> const & cuisines)
{
FeatureParams params;
// Ignore cuisine types as these will be set from the cuisines param
auto const & isCuisine = ftypes::IsCuisineChecker::Instance();
for (uint32_t const type : m_types)
if (!isCuisine(type))
params.m_types.push_back(type);
Classificator const & cl = classif();
for (auto const & cuisine : cuisines)
params.m_types.push_back(cl.GetTypeByPath({string_view("cuisine"), cuisine}));
// Move useless types to the end and resize to fit TypesHolder.
params.FinishAddingTypes();
m_types.Assign(params.m_types.begin(), params.m_types.end());
}
void EditableMapObject::SetCuisines(std::vector<std::string_view> const & cuisines)
{
SetCuisinesImpl(cuisines);
}
void EditableMapObject::SetCuisines(std::vector<std::string> const & cuisines)
{
SetCuisinesImpl(cuisines);
}
void EditableMapObject::SetPointType()
{
m_geomType = feature::GeomType::Point;
}
void EditableMapObject::RemoveBlankNames()
{
StringUtf8Multilang editedName;
m_name.ForEach([&editedName](int8_t langCode, string_view name)
{
if (!name.empty())
editedName.AddString(langCode, name);
});
m_name = editedName;
}
// static
bool EditableMapObject::ValidateBuildingLevels(string const & buildingLevels)
{
if (buildingLevels.empty())
return true;
if (buildingLevels.size() > 18 /* max number of digits in uint_64 */)
return false;
if ('0' == buildingLevels.front())
return false;
uint64_t levels;
return strings::to_uint64(buildingLevels, levels) && levels > 0 && levels <= kMaximumLevelsEditableByUsers;
}
// static
bool EditableMapObject::ValidateHouseNumber(string const & houseNumber)
{
// TODO(mgsergio): Use LooksLikeHouseNumber!
if (houseNumber.empty())
return true;
strings::UniString us = strings::MakeUniString(houseNumber);
// TODO: Improve this basic limit - it was choosen by @Zverik.
auto constexpr kMaxHouseNumberLength = 15;
if (us.size() > kMaxHouseNumberLength)
return false;
// TODO: Should we allow arabic numbers like U+0661 ١ Arabic-Indic Digit One?
strings::NormalizeDigits(us);
for (auto const c : us)
{
// Valid house numbers contain at least one digit.
if (strings::IsASCIIDigit(c))
return true;
}
return false;
}
bool EditableMapObject::CheckHouseNumberWhenIsAddress() const
{
// House number is mandatory for the address type. For other types it's optional.
return !m_houseNumber.empty() || !m_types.Has(classif().GetTypeByReadableObjectName("building-address"));
}
// static
bool EditableMapObject::ValidateFlats(string const & flats)
{
for (auto it = strings::SimpleTokenizer(flats, ";"); it; ++it)
{
string_view token = *it;
strings::Trim(token);
auto range = strings::Tokenize(token, "-");
if (range.empty() || range.size() > 2)
return false;
for (auto const & rangeBorder : range)
if (!all_of(begin(rangeBorder), end(rangeBorder), ::isalnum))
return false;
}
return true;
}
// static
bool EditableMapObject::ValidatePostCode(string const & postCode)
{
if (postCode.empty())
return true;
return search::LooksLikePostcode(postCode, false /* IsPrefix */);
}
// static
bool EditableMapObject::ValidatePhoneList(string const & phone)
{
// BNF:
// <digit> ::= '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
// <available_char> ::= ' ' | '+' | '-' | '(' | ')'
// <delimeter> ::= ',' | ';'
// <phone> ::= (<digit> | <available_chars>)+
// <phone_list> ::= '' | <phone> | <phone> <delimeter> <phone_list>
if (phone.empty())
return true;
auto constexpr kMaxNumberLen = 15;
auto constexpr kMinNumberLen = 5;
if (phone.size() < kMinNumberLen)
return false;
auto curr = phone.begin();
auto last = phone.begin();
do
{
last = find_if(curr, phone.end(), [](string::value_type const & ch) { return ch == ',' || ch == ';'; });
auto digitsCount = 0;
string const symbols = "+-() ";
for (; curr != last; ++curr)
{
if (!isdigit(*curr) && find(symbols.begin(), symbols.end(), *curr) == symbols.end())
return false;
if (isdigit(*curr))
++digitsCount;
}
if (digitsCount < kMinNumberLen || digitsCount > kMaxNumberLen)
return false;
curr = last;
}
while (last != phone.end() && ++curr != phone.end());
return true;
}
// static
bool EditableMapObject::ValidateEmail(string const & email)
{
if (email.empty())
return true;
if (strings::IsASCIIString(email))
{
static auto const s_emailRegex = regex(R"([^@\s]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+$)");
return regex_match(email, s_emailRegex);
}
if ('@' == email.front() || '@' == email.back())
return false;
if ('.' == email.back())
return false;
auto const atPos = find(begin(email), end(email), '@');
if (atPos == end(email))
return false;
// There should be only one '@' sign.
if (find(next(atPos), end(email), '@') != end(email))
return false;
// There should be at least one '.' sign after '@'
if (find(next(atPos), end(email), '.') == end(email))
return false;
return true;
}
// static
bool EditableMapObject::ValidateLevel(string const & level)
{
if (level.empty())
return true;
if (level.front() == ';' || level.back() == ';' || level.find(";;") != std::string::npos)
return false;
// validate ";" separated values separately
vector<std::string_view> const tokenizedValues = strings::Tokenize(level, ";");
for (std::string_view const & value : tokenizedValues)
{
auto const isValidNumber = [](std::string_view const & s)
{
auto constexpr kMinBuildingLevel = -9;
double valueDouble;
return strings::to_double(s, valueDouble) && valueDouble > kMinBuildingLevel && valueDouble <= kMaximumLevelsEditableByUsers;
};
// Check for simple value (e.g. "42")
if (!isValidNumber(value))
{
// Check for range (e.g. "-3-12")
size_t rangeSymbol = value.find('-', 1); // skip first as it could be a negative sign
if (rangeSymbol == std::string::npos)
return false;
std::string_view from = value.substr(0, rangeSymbol);
std::string_view to = value.substr(rangeSymbol + 1, value.size());
if (!isValidNumber(from) || !isValidNumber(to))
return false;
}
// Forbid leading zero (e.g. "04")
if (value.front() == '0' && value.size() >= 2 && value[1] != '.')
return false;
}
return true;
}
// static
bool EditableMapObject::ValidateName(string const & name)
{
if (name.empty())
return true;
static std::u32string_view constexpr excludedSymbols = U"^§><*=_±√•÷×";
using Iter = utf8::unchecked::iterator<string::const_iterator>;
for (Iter it{name.cbegin()}; it != Iter{name.cend()}; ++it)
{
auto const ch = *it;
// Exclude ASCII control characters.
if (ch <= 0x1F)
return false;
// Exclude {|}~ DEL and C1 control characters.
if (ch >= 0x7B && ch <= 0x9F)
return false;
// Exclude arrows, mathematical symbols, borders, geometric shapes.
if (ch >= 0x2190 && ch <= 0x2BFF)
return false;
// Emoji modifiers https://en.wikipedia.org/wiki/Emoji#Emoji_versus_text_presentation
if (ch == 0xFE0E || ch == 0xFE0F)
return false;
// Exclude format controls, musical symbols, emoticons, ornamental and pictographs,
// ancient and exotic alphabets.
if (ch >= 0xFFF0 && ch <= 0x1F9FF)
return false;
if (excludedSymbols.find(ch) != std::u32string_view::npos)
return false;
}
return true;
}
EditJournal const & EditableMapObject::GetJournal() const
{
return m_journal;
}
void EditableMapObject::SetJournal(EditJournal && editJournal)
{
m_journal = std::move(editJournal);
}
EditingLifecycle EditableMapObject::GetEditingLifecycle() const
{
return m_journal.GetEditingLifecycle();
}
void EditableMapObject::MarkAsCreated(uint32_t type, feature::GeomType geomType, m2::PointD mercator)
{
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();
}
void EditableMapObject::ApplyEditsFromJournal(EditJournal const & editJournal)
{
for (JournalEntry const & entry : editJournal.GetJournalHistory())
ApplyJournalEntry(entry);
for (JournalEntry const & entry : editJournal.GetJournal())
ApplyJournalEntry(entry);
}
void EditableMapObject::ApplyJournalEntry(JournalEntry const & entry)
{
LOG(LDEBUG, ("Applying Journal Entry: ", osm::EditJournal::ToString(entry)));
switch (entry.journalEntryType)
{
case JournalEntryType::TagModification:
{
TagModData const & tagModData = std::get<TagModData>(entry.data);
// Metadata
MetadataID type;
if (feature::Metadata::TypeFromString(tagModData.key, type))
{
m_metadata.Set(type, tagModData.new_value);
if (type == MetadataID::FMD_INTERNET)
{
uint32_t const wifiType = ftypes::IsWifiChecker::Instance().GetType();
if (tagModData.new_value == "wifi")
m_types.SafeAdd(wifiType);
else
m_types.Remove(wifiType);
}
break;
}
// Names
int8_t langCode = StringUtf8Multilang::GetCodeByOSMTag(tagModData.key);
if (langCode != StringUtf8Multilang::kUnsupportedLanguageCode)
{
m_name.AddString(langCode, tagModData.new_value);
break;
}
if (tagModData.key == "addr:street")
m_street.m_defaultName = tagModData.new_value;
else if (tagModData.key == "addr:housenumber")
m_houseNumber = tagModData.new_value;
else if (tagModData.key == "cuisine")
{
Classificator const & cl = classif();
// Remove old cuisine values
vector<std::string_view> oldCuisines = strings::Tokenize(tagModData.old_value, ";");
for (std::string_view const & cuisine : oldCuisines)
m_types.Remove(cl.GetTypeByPath({string_view("cuisine"), cuisine}));
// Add new cuisine values
vector<std::string_view> newCuisines = strings::Tokenize(tagModData.new_value, ";");
for (std::string_view const & cuisine : newCuisines)
m_types.SafeAdd(cl.GetTypeByPath({string_view("cuisine"), cuisine}));
}
else if (tagModData.key == "diet:vegetarian")
{
Classificator const & cl = classif();
uint32_t const vegetarianType = cl.GetTypeByPath({string_view("cuisine"), "vegetarian"});
if (tagModData.new_value == "yes")
m_types.SafeAdd(vegetarianType);
else
m_types.Remove(vegetarianType);
}
else if (tagModData.key == "diet:vegan")
{
Classificator const & cl = classif();
uint32_t const veganType = cl.GetTypeByPath({string_view("cuisine"), "vegan"});
if (tagModData.new_value == "yes")
m_types.SafeAdd(veganType);
else
m_types.Remove(veganType);
}
else
LOG(LWARNING, ("OSM key \"", tagModData.key, "\" is unknown, skipped"));
break;
}
case JournalEntryType::ObjectCreated:
{
ObjCreateData const & objCreatedData = std::get<ObjCreateData>(entry.data);
ASSERT_EQUAL(feature::GeomType::Point, objCreatedData.geomType,
("At the moment only new nodes (points) can be created."));
SetPointType();
SetMercator(objCreatedData.mercator);
m_types.Add(objCreatedData.type);
break;
}
case JournalEntryType::LegacyObject:
{
ASSERT_FAIL(("Legacy Objects can not be loaded from Journal"));
break;
}
case JournalEntryType::BusinessReplacement:
{
BusinessReplacementData const & businessReplacementData = std::get<BusinessReplacementData>(entry.data);
ApplyBusinessReplacement(businessReplacementData.new_type);
break;
}
}
}
void EditableMapObject::LogDiffInJournal(EditableMapObject const & unedited_emo)
{
LOG(LDEBUG, ("Executing LogDiffInJournal"));
// Name
for (StringUtf8Multilang::Lang language : StringUtf8Multilang::GetSupportedLanguages())
{
int8_t langCode = StringUtf8Multilang::GetLangIndex(language.m_code);
std::string_view new_name;
std::string_view old_name;
m_name.GetString(langCode, new_name);
unedited_emo.GetNameMultilang().GetString(langCode, old_name);
if (new_name != old_name)
{
std::string osmLangName = StringUtf8Multilang::GetOSMTagByCode(langCode);
m_journal.AddTagChange(std::move(osmLangName), std::string(old_name), std::string(new_name));
}
}
// Address
if (m_street.m_defaultName != unedited_emo.GetStreet().m_defaultName)
m_journal.AddTagChange("addr:street", unedited_emo.GetStreet().m_defaultName, m_street.m_defaultName);
if (m_houseNumber != unedited_emo.GetHouseNumber())
m_journal.AddTagChange("addr:housenumber", unedited_emo.GetHouseNumber(), m_houseNumber);
// Metadata
for (uint8_t i = 0; i < static_cast<uint8_t>(feature::Metadata::FMD_COUNT); ++i)
{
auto const type = static_cast<feature::Metadata::EType>(i);
// CHARGE_SOCKETS have multiple keys/values; handled separately further down
if (type == feature::Metadata::FMD_CHARGE_SOCKETS)
continue;
std::string_view const & value = GetMetadata(type);
std::string_view const & old_value = unedited_emo.GetMetadata(type);
if (value != old_value)
m_journal.AddTagChange(ToString(type), std::string(old_value), std::string(value));
}
// cuisine and diet
std::vector<std::string> new_cuisines = GetCuisines();
std::vector<std::string> old_cuisines = unedited_emo.GetCuisines();
auto const findAndErase = [](std::vector<std::string> & cuisinesPtr, std::string_view s)
{
auto it = std::find(cuisinesPtr.begin(), cuisinesPtr.end(), s);
if (it != cuisinesPtr.end())
{
cuisinesPtr.erase(it);
return "yes";
}
return "";
};
std::string new_vegetarian = findAndErase(new_cuisines, "vegetarian");
std::string old_vegetarian = findAndErase(old_cuisines, "vegetarian");
if (new_vegetarian != old_vegetarian)
m_journal.AddTagChange("diet:vegetarian", old_vegetarian, new_vegetarian);
std::string new_vegan = findAndErase(new_cuisines, "vegan");
std::string old_vegan = findAndErase(old_cuisines, "vegan");
if (new_vegan != old_vegan)
m_journal.AddTagChange("diet:vegan", old_vegan, new_vegan);
bool cuisinesModified = false;
if (new_cuisines.size() != old_cuisines.size())
cuisinesModified = true;
else
{
for (auto const & new_cuisine : new_cuisines)
{
if (!base::IsExist(old_cuisines, new_cuisine))
{
cuisinesModified = true;
break;
}
}
}
if (cuisinesModified)
m_journal.AddTagChange("cuisine", strings::JoinStrings(old_cuisines, ";"), strings::JoinStrings(new_cuisines, ";"));
// charge sockets
auto chargeSocketsDiff = ChargeSocketsHelper(GetChargeSockets()).Diff(unedited_emo.GetChargeSockets());
for (auto const & kvdiff : chargeSocketsDiff)
{
std::string key, old_value, new_value;
std::tie(key, old_value, new_value) = kvdiff;
m_journal.AddTagChange(key, old_value, new_value);
}
}
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<uint32_t> 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();
feature::TypesHolder const & rhsTypes = rhs.GetTypes();
if (!lhsTypes.Equals(rhsTypes))
return false;
if (lhs.GetHouseNumber() != rhs.GetHouseNumber())
return false;
if (lhs.GetCuisines() != rhs.GetCuisines())
return false;
if (!lhs.m_metadata.Equals(rhs.m_metadata))
return false;
if (lhs.GetNameMultilang() != rhs.GetNameMultilang())
return false;
return true;
}
} // namespace osm