File indexing completed on 2024-05-12 04:42:10

0001 /*
0002     SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "osmelementinformationmodel.h"
0008 #include "osmelementinformationmodel_data.cpp"
0009 
0010 #include "localization.h"
0011 #include "osmaddress.h"
0012 
0013 #include <KLocalizedString>
0014 
0015 #include <cctype>
0016 
0017 using namespace KOSMIndoorMap;
0018 
0019 static QString formatDistance(int meter)
0020 {
0021     if (meter < 1000) {
0022         return i18n("%1m", meter);
0023     }
0024     if (meter < 10000) {
0025         return i18n("%1km", ((int)meter/100)/10.0);
0026     }
0027     return i18n("%1km", (int)qRound(meter/1000.0));
0028 }
0029 
0030 bool OSMElementInformationModel::Info::operator<(OSMElementInformationModel::Info other) const
0031 {
0032     if (category == other.category) {
0033         return key < other.key;
0034     }
0035     return category < other.category;
0036 }
0037 
0038 bool OSMElementInformationModel::Info::operator==(OSMElementInformationModel::Info other) const
0039 {
0040     return category == other.category && key == other.key;
0041 }
0042 
0043 
0044 OSMElementInformationModel::OSMElementInformationModel(QObject *parent)
0045     : QAbstractListModel(parent)
0046     , m_langs(OSM::Languages::fromQLocale(QLocale()))
0047 {
0048 }
0049 
0050 OSMElementInformationModel::~OSMElementInformationModel() = default;
0051 
0052 OSMElement OSMElementInformationModel::element() const
0053 {
0054     return OSMElement(m_element);
0055 }
0056 
0057 void OSMElementInformationModel::setElement(const OSMElement &element)
0058 {
0059     if (m_element == element.element()) {
0060         return;
0061     }
0062 
0063     beginResetModel();
0064     m_element = element.element();
0065     m_infos.clear();
0066     if (m_element.type() != OSM::Type::Null) {
0067         reload();
0068     }
0069     endResetModel();
0070     Q_EMIT elementChanged();
0071 }
0072 
0073 void OSMElementInformationModel::clear()
0074 {
0075     if (m_element.type() == OSM::Type::Null) {
0076         return;
0077     }
0078     beginResetModel();
0079     m_infos.clear();
0080     m_element = {};
0081     endResetModel();
0082     Q_EMIT elementChanged();
0083 }
0084 
0085 QString OSMElementInformationModel::name() const
0086 {
0087     return valueForKey(Info{m_nameKey, Header}).toString();
0088 }
0089 
0090 QString OSMElementInformationModel::category() const
0091 {
0092     return valueForKey(Info{m_categoryKey, Header}).toString();
0093 }
0094 
0095 int OSMElementInformationModel::rowCount(const QModelIndex &parent) const
0096 {
0097     if (parent.isValid() || m_element.type() == OSM::Type::Null) {
0098         return 0;
0099     }
0100     return m_infos.size();
0101 }
0102 
0103 QVariant OSMElementInformationModel::data(const QModelIndex &index, int role) const
0104 {
0105     if (!index.isValid()) {
0106         return {};
0107     }
0108 
0109     const auto info = m_infos[index.row()];
0110     switch (role) {
0111         case TypeRole:
0112             switch (info.key) {
0113                 case Wikipedia:
0114                 case Phone:
0115                 case Email:
0116                 case Website:
0117                 case OperatorWikipedia:
0118                 case DebugLink:
0119                     return Link;
0120                 case Address:
0121                     return PostalAddress;
0122                 case OpeningHours:
0123                     return OpeningHoursType;
0124                 default:
0125                     return String;
0126             }
0127         case KeyRole:
0128             return info.key;
0129         case KeyLabelRole:
0130             if (info.key == DebugKey) {
0131                 return debugTagKey(index.row());
0132             }
0133             return keyName(info.key);
0134         case ValueRole:
0135             switch (info.key) {
0136                 case DebugKey: return debugTagValue(index.row());
0137                 case Wikipedia: return i18n("Wikipedia");
0138                 default: return valueForKey(info);
0139             }
0140         case ValueUrlRole:
0141             return urlify(valueForKey(info), info.key);
0142         case CategoryRole:
0143             return info.category;
0144         case CategoryLabelRole:
0145             return categoryLabel(info.category);
0146     }
0147 
0148     return {};
0149 }
0150 
0151 QHash<int, QByteArray> OSMElementInformationModel::roleNames() const
0152 {
0153     auto r = QAbstractListModel::roleNames();
0154     r.insert(KeyRole, "key");
0155     r.insert(KeyLabelRole, "keyLabel");
0156     r.insert(ValueRole, "value");
0157     r.insert(ValueUrlRole, "url");
0158     r.insert(CategoryRole, "category");
0159     r.insert(CategoryLabelRole, "categoryLabel");
0160     r.insert(TypeRole, "type");
0161     return r;
0162 }
0163 
0164 #define M(name, key, category) { name, OSMElementInformationModel::key, OSMElementInformationModel::category }
0165 struct KeyCategoryMapEntry {
0166     const char *keyName;
0167     OSMElementInformationModel::Key m_key;
0168     OSMElementInformationModel::KeyCategory m_category;
0169 
0170     constexpr inline OSMElementInformationModel::Key key() const { return m_key; }
0171     constexpr inline OSMElementInformationModel::KeyCategory category() const { return m_category; }
0172 };
0173 
0174 static constexpr const KeyCategoryMapEntry simple_key_map[] = {
0175     M("addr:city", Address, Contact),
0176     M("addr:street", Address, Contact),
0177     M("amenity", Category, Header),
0178     M("bicycle_parking", BicycleParking, Parking),
0179     M("brand", Name, Header),
0180     M("brand:wikipedia", Wikipedia, UnresolvedCategory),
0181     M("building", Category, Header),
0182     M("bus_lines", Routes, Main),
0183     M("bus_routes", Routes, Main),
0184     M("buses", Routes, Main),
0185     M("capacity", Capacity, UnresolvedCategory),
0186     M("capacity:charging", CapacityCharing, Parking),
0187     M("capacity:disabled", CapacityDisabled, Parking),
0188     M("capacity:parent", CapacityParent, Parking),
0189     M("capacity:women", CapacityWomen, Parking),
0190     M("centralkey", CentralKey, Accessibility),
0191     M("changing_table", DiaperChangingTable, UnresolvedCategory),
0192     M("charge", Fee, UnresolvedCategory),
0193     M("contact:city", Address, Contact),
0194     M("contact:email", Email, Contact),
0195     M("contact:phone", Phone, Contact),
0196     M("contact:street", Address, Contact),
0197     M("contact:website", Website, Contact),
0198     M("cuisine", Cuisine, Main),
0199     M("description", Description, Main),
0200     M("diaper", DiaperChangingTable, UnresolvedCategory),
0201     M("diplomatic", Category, Header),
0202     M("email", Email, Contact),
0203     M("fee", Fee, UnresolvedCategory),
0204     M("genus", Name, Header),
0205     M("historic", Category, Header),
0206     M("int_name", Name, Header),
0207     M("leisure", Category, Header),
0208     M("maxstay", MaxStay, Parking),
0209     M("mx:realtime_available", AvailableVehicles, Main),
0210     M("mx:remaining_range", RemainingRange, Main),
0211     M("mx:vehicle", Category, Header),
0212     M("network", Network, Operator),
0213     M("network:wikipedia", OperatorWikipedia, Operator),
0214     M("office", Category, Header),
0215     M("old_name", OldName, UnresolvedCategory),
0216     M("opening_hours", OpeningHours, OpeningHoursCategory),
0217     M("operator", OperatorName, Operator),
0218     M("operator:email", Email, Contact),
0219     M("operator:phone", Phone, Contact),
0220     M("operator:website", Website, Contact),
0221     M("operator:wikipedia", OperatorWikipedia, Operator),
0222     M("parking:fee", Fee, Parking),
0223     M("payment:cash", PaymentCash, Payment),
0224     M("payment:coins", PaymentCash, Payment),
0225     M("payment:notes", PaymentCash, Payment),
0226     M("phone", Phone, Contact),
0227     M("room", Category, Header),
0228     M("route_ref", Routes, Main),
0229     M("shop", Category, Header),
0230     M("tactile_writing", TactileWriting, Accessibility), // occurs also unqualified
0231     M("takeaway", Takeaway, Main),
0232     M("toilets:fee", Fee, Toilets),
0233     M("toilets:wheelchair", Wheelchair, Toilets),
0234     M("tourism", Category, Header),
0235     M("url", Website, Contact),
0236     M("website", Website, Contact),
0237     M("wheelchair", Wheelchair, Accessibility),
0238     M("wheelchair:lift", WheelchairLift, Accessibility),
0239 };
0240 static_assert(isSortedLookupTable(simple_key_map), "key map is not sorted!");
0241 
0242 static constexpr const KeyCategoryMapEntry localized_key_map[] = {
0243     M("name", Name, Header),
0244     M("loc_name", Name, Header),
0245     M("species", Name, Header),
0246     M("species:wikipedia", Wikipedia, UnresolvedCategory),
0247     M("speech_output", SpeechOutput, Accessibility),
0248     M("wikipedia", Wikipedia, UnresolvedCategory),
0249 };
0250 #undef M
0251 
0252 template <typename KeyMapEntry, std::size_t N>
0253 void OSMElementInformationModel::addEntryForKey(const char *keyName, const KeyMapEntry(&map)[N])
0254 {
0255     const auto it = std::lower_bound(std::begin(map), std::end(map), keyName, [](const auto &lhs, auto rhs) {
0256         return std::strcmp(lhs.keyName, rhs) < 0;
0257     });
0258     if (it != std::end(map) && std::strcmp((*it).keyName, keyName) == 0) {
0259         m_infos.push_back(Info{(*it).key(), (*it).category()});
0260     }
0261 }
0262 
0263 template <typename KeyMapEntry, std::size_t N>
0264 void OSMElementInformationModel::addEntryForLocalizedKey(const char *keyName, const KeyMapEntry(&map)[N])
0265 {
0266     for (const auto &entry : map) {
0267         const auto mapKeyLen = std::strlen(entry.keyName);
0268         if (std::strncmp(keyName, entry.keyName, mapKeyLen) != 0) {
0269             continue;
0270         }
0271         const auto keyNameLen = std::strlen(keyName);
0272         if (keyNameLen == mapKeyLen || (keyNameLen == mapKeyLen + 3 && keyName[mapKeyLen] == ':')) {
0273             m_infos.push_back(Info{entry.key(), entry.category()});
0274             return;
0275         }
0276     }
0277 }
0278 
0279 void OSMElementInformationModel::reload()
0280 {
0281     m_nameKey = NoKey;
0282     m_categoryKey = NoKey;
0283 
0284     const bool isRoom = m_element.tagValue("indoor") == "room";
0285     for (auto it = m_element.tagsBegin(); it != m_element.tagsEnd(); ++it) {
0286         addEntryForLocalizedKey((*it).key.name(), localized_key_map);
0287         addEntryForKey((*it).key.name(), simple_key_map);
0288         addEntryForKey((*it).key.name(), payment_generic_type_map);
0289         addEntryForKey((*it).key.name(), payment_type_map);
0290         addEntryForKey((*it).key.name(), diet_type_map);
0291         addEntryForKey((*it).key.name(), socket_type_map);
0292         addEntryForKey((*it).key.name(), authentication_type_map);
0293         addEntryForKey((*it).key.name(), gender_type_map);
0294         addEntryForLocalizedKey((*it).key.name(), tactile_writing_map);
0295 
0296         if (isRoom && std::strcmp((*it).key.name(), "ref") == 0) {
0297             m_infos.push_back(Info{Name, Header});
0298         }
0299     }
0300 
0301     std::sort(m_infos.begin(), m_infos.end());
0302     m_infos.erase(std::unique(m_infos.begin(), m_infos.end()), m_infos.end());
0303     resolveCategories();
0304     resolveHeaders();
0305 
0306     // if we don't have a primary group, promote a suitable secondary one
0307     for (auto cat : {Parking, Toilets}) {
0308         if (promoteMainCategory(cat)) {
0309             break;
0310         }
0311     }
0312 
0313     // resolve all remaining unresolved elements to the primary category
0314     for (auto &info : m_infos) {
0315         if (info.category == UnresolvedCategory) {
0316             info.category = Main;
0317         }
0318     }
0319     std::sort(m_infos.begin(), m_infos.end());
0320     m_infos.erase(std::unique(m_infos.begin(), m_infos.end()), m_infos.end());
0321 
0322     if (m_debug) {
0323         m_infos.push_back(Info{ DebugLink, DebugCategory });
0324         const auto count = std::distance(m_element.tagsBegin(), m_element.tagsEnd());
0325         std::fill_n(std::back_inserter(m_infos), count, Info{ DebugKey, DebugCategory });
0326     }
0327 }
0328 
0329 void OSMElementInformationModel::resolveCategories()
0330 {
0331     if (m_infos.empty() || m_infos[0].category != UnresolvedCategory) {
0332         return;
0333     }
0334     for (auto &info : m_infos) {
0335         if (info.category != UnresolvedCategory) {
0336             break;
0337         }
0338         switch (info.key) {
0339             case Fee:
0340                 if (m_element.tagValue("parking:fee").isEmpty() && (!m_element.tagValue("parking").isEmpty()
0341                     || m_element.tagValue("amenity") == "parking" || m_element.tagValue("amenity") == "bicycle_parking"))
0342                 {
0343                     info.category = Parking;
0344                 } else if (m_element.tagValue("toilets:fee").isEmpty() && (m_element.tagValue("toilets") == "yes" || m_element.tagValue("amenity") == "toilets")) {
0345                     info.category = Toilets;
0346                 } else {
0347                     info.category = Main;
0348                 }
0349                 break;
0350             case Capacity:
0351                 if (m_element.tagValue("amenity").endsWith("rental")) {
0352                     info.category = Main;
0353                 } else {
0354                     info.category = Parking;
0355                 }
0356                 break;
0357             default:
0358             {
0359                 // for anything else: if it's not clearly something we have a secondary group for, resolve it to Main
0360                 const auto amenity = m_element.tagValue("amenity");
0361                 if ((amenity != "parking" && amenity != "toilets")
0362                     || !m_element.tagValue("office").isEmpty()
0363                     || (!m_element.tagValue("room").isEmpty() && m_element.tagValue("room") != "toilets")
0364                     || !m_element.tagValue("shop").isEmpty()
0365                     || !m_element.tagValue("tourism").isEmpty()) {
0366                     info.category = Main;
0367                 }
0368                 break;
0369             }
0370         }
0371     }
0372     std::sort(m_infos.begin(), m_infos.end());
0373 }
0374 
0375 void OSMElementInformationModel::resolveHeaders()
0376 {
0377     for (auto key : { Name, Network, OperatorName, Category }) {
0378         if (m_nameKey != NoKey) {
0379             break;
0380         }
0381 
0382         const auto it = std::find_if(m_infos.begin(), m_infos.end(), [key](Info info) {
0383             return info.key == key;
0384         });
0385         if (it == m_infos.end()) {
0386             continue;
0387         }
0388 
0389         m_nameKey = (*it).key;
0390         m_infos.erase(it);
0391         break;
0392     }
0393 
0394     // we use the categories as header if there is no name, so don't duplicate that
0395     const auto it = std::find_if(m_infos.begin(), m_infos.end(), [](Info info) {
0396         return info.key == Category;
0397     });
0398     if (it == m_infos.end() || m_nameKey == Category) {
0399         return;
0400     }
0401 
0402     m_infos.erase(it);
0403     m_categoryKey = Category;
0404 }
0405 
0406 bool OSMElementInformationModel::promoteMainCategory(OSMElementInformationModel::KeyCategory cat)
0407 {
0408     const auto hasMain = std::any_of(m_infos.begin(), m_infos.end(), [](const auto &info) {
0409         return info.category == Main;
0410     });
0411 
0412     if (hasMain) {
0413         return true;
0414     }
0415 
0416     bool didPromote = false;
0417     for (auto &info : m_infos) {
0418         if (info.category == cat) {
0419             info.category = (info.key == Wheelchair ? Accessibility : Main);
0420             didPromote = true;
0421         }
0422     }
0423 
0424     if (didPromote) {
0425         std::sort(m_infos.begin(), m_infos.end());
0426     }
0427     return didPromote;
0428 }
0429 
0430 QString OSMElementInformationModel::categoryLabel(OSMElementInformationModel::KeyCategory cat) const
0431 {
0432     switch (cat) {
0433         case UnresolvedCategory:
0434         case Header:
0435         case Main:          return {};
0436         case OpeningHoursCategory: return i18n("Opening Hours");
0437         case Contact:       return i18n("Contact");
0438         case Payment:       return i18n("Payment");
0439         case Toilets:       return i18n("Toilets");
0440         case Accessibility: return i18n("Accessibility");
0441         case Parking:       return i18n("Parking");
0442         case Operator:      return i18n("Operator");
0443         case DebugCategory: return QStringLiteral("Debug");
0444     }
0445     return {};
0446 }
0447 
0448 QString OSMElementInformationModel::debugTagKey(int row) const
0449 {
0450     const auto tagCount = std::distance(m_element.tagsBegin(), m_element.tagsEnd());
0451     const auto tagIdx = row - (rowCount() - tagCount);
0452     return QString::fromUtf8((*(m_element.tagsBegin() + tagIdx)).key.name());
0453 }
0454 
0455 QString OSMElementInformationModel::debugTagValue(int row) const
0456 {
0457     const auto tagCount = std::distance(m_element.tagsBegin(), m_element.tagsEnd());
0458     const auto tagIdx = row - (rowCount() - tagCount);
0459     return QString::fromUtf8((*(m_element.tagsBegin() + tagIdx)).value);
0460 }
0461 
0462 QString OSMElementInformationModel::keyName(OSMElementInformationModel::Key key) const
0463 {
0464     switch (key) {
0465         case NoKey:
0466         case Name:
0467         case Category: return {};
0468         case OldName: return i18n("Formerly");
0469         case Description: return i18n("Description");
0470         case Routes: return i18n("Routes");
0471         case Cuisine: return i18n("Cuisine");
0472         case Diet: return i18n("Diet");
0473         case Takeaway: return i18n("Takeaway");
0474         case Socket: return i18nc("electrical power socket", "Socket");
0475         case OpeningHours: return {};
0476         case AvailableVehicles: return i18n("Available vehicles");
0477         case Fee: return i18n("Fee");
0478         case Authentication: return i18n("Authentication");
0479         case BicycleParking: return i18n("Bicycle parking");
0480         case Capacity: return i18n("Capacity");
0481         case CapacityDisabled: return i18n("Disabled parking spaces");
0482         case CapacityWomen: return i18n("Women parking spaces");
0483         case CapacityParent: return i18n("Parent parking spaces");
0484         case CapacityCharing: return i18n("Parking spaces for charging");
0485         case MaxStay: return i18n("Maximum stay");
0486         case DiaperChangingTable: return i18n("Diaper changing table");
0487         case Gender: return i18n("Gender");
0488         case Wikipedia: return {};
0489         case Address: return i18n("Address");
0490         case Phone: return i18n("Phone");
0491         case Email: return i18n("Email");
0492         case Website: return i18n("Website");
0493         case PaymentCash: return i18n("Cash");
0494         case PaymentDigital: return i18n("Digital");
0495         case PaymentDebitCard: return i18n("Debit cards");
0496         case PaymentCreditCard: return i18n("Credit cards");
0497         case PaymentStoredValueCard: return i18n("Stored value cards");
0498         case Wheelchair: return i18n("Wheelchair access");
0499         case WheelchairLift: return i18n("Wheelchair lift");
0500         case CentralKey: return i18n("Central key");
0501         case SpeechOutput: return i18n("Speech output");
0502         case TactileWriting: return i18n("Tactile writing");
0503         case OperatorName: return {};
0504         case Network: return i18nc("transport network", "Network");
0505         case OperatorWikipedia: return {};
0506         case RemainingRange: return i18nc("remaining travel range of a battery powered vehicle", "Remaining range");
0507         case DebugLink: return QStringLiteral("OSM");
0508         case DebugKey: return {};
0509     }
0510     return {};
0511 }
0512 
0513 static void appendNonEmpty(const QByteArray &tagValue, QList<QByteArray> &l)
0514 {
0515     if (tagValue.isEmpty()) {
0516         return;
0517     }
0518     auto split = tagValue.split(';');
0519     for (const auto &s : split) {
0520         if (!s.isEmpty()) {
0521             l.push_back(s.trimmed());
0522         }
0523     }
0524 }
0525 
0526 static QChar::Script scriptForString(QStringView s)
0527 {
0528     return std::accumulate(s.begin(), s.end(), QChar::Script_Unknown, [](QChar::Script script, QChar c) { return std::max(script, c.script());});
0529 }
0530 
0531 // why do we have two different script enums???
0532 // ### far from complete, this only handles the cases where int_name is in widespread use so far
0533 struct {
0534     QLocale::Script localeScript;
0535     QChar::Script charScript;
0536 } static constexpr const script_map[] = {
0537     { QLocale::GreekScript, QChar::Script_Greek },
0538     { QLocale::CyrillicScript, QChar::Script_Cyrillic },
0539 };
0540 
0541 static bool isSameScript(QLocale::Script ls, QChar::Script cs)
0542 {
0543     return std::find_if(std::begin(script_map), std::end(script_map), [ls, cs](const auto &m) { return m.localeScript == ls && m.charScript == cs; }) != std::end(script_map);
0544 }
0545 
0546 QVariant OSMElementInformationModel::valueForKey(Info info) const
0547 {
0548     switch (info.key) {
0549         case NoKey: return {};
0550         case Name: {
0551             const auto n = QString::fromUtf8(m_element.tagValue(m_langs, "name", "loc_name", "int_name", "brand", "ref", "species", "genus"));
0552             const auto script = scriptForString(n);
0553             if (!isSameScript(QLocale().script(), script) && script > QChar::Script_Latin) {
0554                 const auto transliterated = QString::fromUtf8(m_element.tagValue(m_langs, "int_name"));
0555                 if (transliterated.isEmpty() || transliterated == n) {
0556                     return n;
0557                 }
0558                 return i18nc("local name (transliterated name)", "%1 (%2)", n, transliterated);
0559             }
0560             return n;
0561         }
0562         case Category:
0563         {
0564             QList<QByteArray> l;
0565             appendNonEmpty(m_element.tagValue("amenity"), l);
0566             appendNonEmpty(m_element.tagValue("shop"), l);
0567             appendNonEmpty(m_element.tagValue("tourism"), l);
0568             appendNonEmpty(m_element.tagValue("vending"), l);
0569             const auto diplomatic = m_element.tagValue("diplomatic");
0570             appendNonEmpty(diplomatic, l);
0571             if (diplomatic.isEmpty()) {
0572                 appendNonEmpty(m_element.tagValue("office"), l);
0573             }
0574             appendNonEmpty(m_element.tagValue("leisure"), l);
0575             appendNonEmpty(m_element.tagValue("historic"), l);
0576             appendNonEmpty(m_element.tagValue("mx:vehicle"), l);
0577             if (l.isEmpty()) {
0578                 appendNonEmpty(m_element.tagValue("room"), l);
0579             }
0580 
0581             QStringList out;
0582             out.reserve(l.size());
0583 
0584             // TODO drop general categories if specific ones are available (e.g. restaurant vs fast_food)
0585 
0586             for (auto it = l.begin(); it != l.end();++it) {
0587                 if ((*it).isEmpty() || (*it) == "yes" || (*it) == "no" || (*it) == "vending_machine" || (*it) == "building") {
0588                     continue;
0589                 }
0590                 out.push_back(Localization::amenityType((*it).constData()));
0591             }
0592 
0593             if (out.isEmpty()) { // fall back to building, but only take terms we have translated
0594                 appendNonEmpty(m_element.tagValue("building"), l);
0595                 for (const auto &key : l) {
0596                     auto s = Localization::amenityType(key.constData(), Localization::ReturnEmptyOnUnknownKey);
0597                     if (!s.isEmpty()) {
0598                         out.push_back(std::move(s));
0599                     }
0600                 }
0601             }
0602 
0603             std::sort(out.begin(), out.end());
0604             out.erase(std::unique(out.begin(), out.end()), out.end());
0605             return QLocale().createSeparatedList(out);
0606         }
0607         case OldName:
0608         {
0609             const auto l = QString::fromUtf8(m_element.tagValue("old_name")).split(QLatin1Char(';'));
0610             return l.join(QLatin1String(", "));
0611         }
0612         case Description:
0613             return m_element.tagValue(m_langs, "description");
0614         case Routes:
0615         {
0616             auto l = QString::fromUtf8(m_element.tagValue("route_ref", "bus_routes", "bus_lines", "buses")).split(QLatin1Char(';'), Qt::SkipEmptyParts);
0617             for (auto &s : l) {
0618                 s = s.trimmed();
0619             }
0620             return QLocale().createSeparatedList(l);
0621         }
0622         case Cuisine: return Localization::cuisineTypes(m_element.tagValue("cuisine"));
0623         case Diet:
0624         {
0625             QStringList l;
0626             for (const auto &d : diet_type_map) {
0627                 const auto v = m_element.tagValue(d.keyName);
0628                 const auto label = d.label.toString();
0629                 if (v == "yes") {
0630                     l.push_back(label);
0631                 } else if (v == "only") {
0632                     l.push_back(i18n("only %1", label));
0633                 } else if (v == "no") {
0634                     l.push_back(i18n("no %1", label));
0635                 }
0636             }
0637             return l.join(QLatin1String(", "));
0638         }
0639         case Takeaway: return translatedBoolValue(m_element.tagValue("takeaway")); // TODO decode (yes/only/no) and translate
0640         case Socket:
0641         {
0642             QStringList l;
0643             for (const auto &socket : socket_type_map) {
0644                 const auto value = m_element.tagValue(socket.keyName);
0645                 if (value.isEmpty() || value == "no") {
0646                     continue;
0647                 }
0648 
0649                 auto s = socket.label.toString();
0650 
0651                 QStringList details;
0652                 if (value != "yes") {
0653                     details.push_back(QString::fromUtf8(value));
0654                 }
0655 
0656                 const auto current = m_element.tagValue(QByteArray(socket.keyName + QByteArray(":current")).constData());
0657                 if (!current.isEmpty()) {
0658                     if (std::all_of(current.begin(), current.end(), [](unsigned char c) { return std::isdigit(c); })) {
0659                         details.push_back(i18nc("electrical current/Ampere value", "%1 A", QString::fromUtf8(current)));
0660                     } else {
0661                         details.push_back(QString::fromUtf8(current));
0662                     }
0663                 }
0664                 const auto output = m_element.tagValue(QByteArray(socket.keyName + QByteArray(":output")).constData());
0665                 if (!output.isEmpty()) {
0666                     if (std::all_of(output.begin(), output.end(), [](unsigned char c) { return std::isdigit(c); })) {
0667                         details.push_back(i18nc("electrical power/kilowatt value", "%1 kW", QString::fromUtf8(output)));
0668                     } else {
0669                         details.push_back(QString::fromUtf8(output));
0670                     }
0671                 }
0672 
0673                 if (!details.empty()) {
0674                     s += QLatin1String(" (") + details.join(QLatin1String(", ")) + QLatin1Char(')');
0675                 }
0676                 l.push_back(s);
0677             }
0678             return QLocale().createSeparatedList(l);
0679         }
0680         case OpeningHours: return QString::fromUtf8(m_element.tagValue("opening_hours"));
0681         case AvailableVehicles:
0682         {
0683             const auto total = m_element.tagValue("mx:realtime_available").toInt();
0684             QStringList types;
0685             for (const auto &v : available_vehicles_map) {
0686                 const auto b = m_element.tagValue(v.keyName);
0687                 if (b.isEmpty()) {
0688                     continue;
0689                 }
0690                 types.push_back(v.label.subs(b.toInt()).toString());
0691             }
0692 
0693             if (types.isEmpty()) {
0694                 return QLocale().toString(total);
0695             } else if (types.size() == 1) {
0696                 return types.at(0);
0697             } else {
0698                 return i18n("%1 (%2)", total, QLocale().createSeparatedList(types));
0699             }
0700         }
0701         case Fee:
0702         {
0703             QByteArray fee;
0704             switch (info.category) {
0705                 case Parking: fee = m_element.tagValue("parking:fee", "fee"); break;
0706                 case Toilets: fee = m_element.tagValue("toilets:fee", "fee"); break;
0707                 default: fee = m_element.tagValue("fee");
0708             }
0709             auto s = QString::fromUtf8(fee);
0710             const auto charge = QString::fromUtf8(m_element.tagValue("charge"));
0711             if (s.isEmpty()) {
0712                 return charge;
0713             }
0714             if (!charge.isEmpty()) {
0715                 s += QLatin1String(" (") + charge + QLatin1Char(')');
0716             }
0717             return s;
0718         }
0719         case Authentication:
0720         {
0721             QStringList l;
0722             for (const auto &auth : authentication_type_map) {
0723                 const auto v = m_element.tagValue(auth.keyName);
0724                 if (v.isEmpty() || v == "no") {
0725                     continue;
0726                 }
0727                 l.push_back(auth.label.toString());
0728             }
0729             return QLocale().createSeparatedList(l);
0730         }
0731         case BicycleParking: return translateValues(m_element.tagValue("bicycle_parking"), bicycle_parking_map);
0732         case Capacity: return QString::fromUtf8(m_element.tagValue("capacity"));
0733         case CapacityDisabled: return capacitryValue("capacity:disabled");
0734         case CapacityWomen: return capacitryValue("capacity:women");
0735         case CapacityParent: return capacitryValue("capacity:parent");
0736         case CapacityCharing: return capacitryValue("capacity:charging");
0737         case MaxStay: return QString::fromUtf8(m_element.tagValue("maxstay"));
0738         case DiaperChangingTable:
0739             // TODO look for changing_table:location too
0740             return translatedBoolValue(m_element.tagValue("changing_table", "diaper"));
0741         case Gender:
0742         {
0743             QStringList l;
0744             for (const auto &gender : gender_type_map) {
0745                 const auto v = m_element.tagValue(gender.keyName);
0746                 if (v.isEmpty() || v == "no") {
0747                     continue;
0748                 }
0749                 l.push_back(gender.label.toString());
0750             }
0751             return QLocale().createSeparatedList(l);
0752         }
0753         case Wikipedia: return wikipediaUrl(m_element.tagValue(m_langs, "wikipedia", "brand:wikipedia", "species:wikipedia"));
0754         case Address: return QVariant::fromValue(OSMAddress(m_element));
0755         case Phone: return QString::fromUtf8(m_element.tagValue("contact:phone", "phone", "telephone", "operator:phone"));
0756         case Email: return QString::fromUtf8(m_element.tagValue("contact:email", "email", "operator:email"));
0757         case Website: return QString::fromUtf8(m_element.tagValue("website", "contact:website", "url", "operator:website"));
0758         case PaymentCash:
0759         {
0760             const auto coins = m_element.tagValue("payment:coins");
0761             const auto notes = m_element.tagValue("payment:notes");
0762             if (coins.isEmpty() && notes.isEmpty()) {
0763                 return translatedBoolValue(m_element.tagValue("payment:cash"));
0764             }
0765             if (coins == "yes" && notes == "yes") {
0766                 return i18n("yes");
0767             }
0768             if (coins == "yes") {
0769                 return i18nc("payment option", "coins only");
0770             }
0771             if (notes == "yes") {
0772                 return i18nc("payment option", "notes only");
0773             }
0774             return i18n("no");
0775         }
0776         case PaymentDigital:
0777         case PaymentDebitCard:
0778         case PaymentCreditCard:
0779         case PaymentStoredValueCard:
0780             return paymentMethodValue(info.key);
0781         case Wheelchair:
0782         {
0783             QByteArray wheelchair;
0784             if (info.category == Toilets) {
0785                 wheelchair = m_element.tagValue("toilets:wheelchair", "wheelchair");
0786             } else {
0787                 wheelchair = m_element.tagValue("wheelchair");
0788             }
0789             const auto a = translateValue(wheelchair.constData(), wheelchair_map);
0790             const auto d = QString::fromUtf8(m_element.tagValue(m_langs, "wheelchair:description"));
0791             if (!d.isEmpty()) {
0792                 return QString(a + QLatin1String(" (") + d + QLatin1Char(')'));
0793             }
0794             return a;
0795         }
0796         case WheelchairLift:
0797             return translatedBoolValue(m_element.tagValue("wheelchair:lift"));
0798         case CentralKey:
0799             // translate enum values
0800             return QString::fromUtf8(m_element.tagValue("centralkey"));
0801         case SpeechOutput:
0802             // TODO: rather than as a boolean value, list the available languages here when we have that information
0803             return translatedBoolValue(m_element.tagValue(m_langs, "speech_output"));
0804         case TactileWriting:
0805         {
0806             // TODO: rather than as a boolean value, list the available languages here when we have that information
0807             QStringList l;
0808             bool explicitNo = false;
0809             for (const auto &writing : tactile_writing_map) {
0810                 const auto v = m_element.tagValue(m_langs, writing.keyName);
0811                 if (v.isEmpty()) {
0812                     continue;
0813                 }
0814                 if (v == "no") {
0815                     explicitNo = true;
0816                     continue;
0817                 }
0818                 l.push_back(writing.label.toString());
0819             }
0820             if (!l.isEmpty()) {
0821                 return QLocale().createSeparatedList(l);
0822             }
0823             const auto v = m_element.tagValue(m_langs, "tactile_writing");
0824             if (explicitNo && v.isEmpty()) {
0825                 return i18n("no");
0826             }
0827             return translatedBoolValue(v);
0828         }
0829         case OperatorName: return QString::fromUtf8(m_element.tagValue("operator"));
0830         case Network: return QString::fromUtf8(m_element.tagValue("network"));
0831         case OperatorWikipedia: return wikipediaUrl(m_element.tagValue(m_langs, "operator:wikipedia", "network:wikipedia"));
0832         case RemainingRange:
0833         {
0834             const auto range = m_element.tagValue("mx:remaining_range").toInt();
0835             return formatDistance(range);
0836         }
0837         case DebugLink: return m_element.url();
0838         case DebugKey: return {};
0839     }
0840     return {};
0841 }
0842 
0843 QVariant OSMElementInformationModel::urlify(const QVariant& v, OSMElementInformationModel::Key key) const
0844 {
0845     if (v.userType() != QMetaType::QString) {
0846         return v;
0847     }
0848     const auto s = v.toString();
0849 
0850     switch (key) {
0851         case Email:
0852             if (!s.startsWith(QLatin1String("mailto:"))) {
0853                 return QString(QLatin1String("mailto:") + s);
0854             }
0855             return s;
0856         case Phone:
0857         {
0858             if (s.startsWith(QLatin1String("tel:"))) {
0859                 return s;
0860             }
0861             QString e = QLatin1String("tel:") + s;
0862             e.remove(QLatin1Char(' '));
0863             return e;
0864         }
0865         case Website:
0866         case DebugLink:
0867             if (s.startsWith(QLatin1String("http"))) {
0868                 return s;
0869             }
0870             return QString(QLatin1String("https://") + s);
0871         default:
0872             return {};
0873     }
0874 
0875     return {};
0876 }
0877 
0878 QString OSMElementInformationModel::paymentMethodList(OSMElementInformationModel::Key key) const
0879 {
0880     QStringList l;
0881     for (const auto &payment : payment_type_map) {
0882         if (payment.key() != key) {
0883             continue;
0884         }
0885         if (m_element.tagValue(payment.keyName) == "yes") {
0886             l.push_back(payment.label.toString());
0887         }
0888     }
0889     std::sort(l.begin(), l.end());
0890     return QLocale().createSeparatedList(l);
0891 }
0892 
0893 QString OSMElementInformationModel::paymentMethodValue(OSMElementInformationModel::Key key) const
0894 {
0895     const auto s = paymentMethodList(key);
0896     if (!s.isEmpty()) {
0897         return s;
0898     }
0899 
0900     for (const auto &payment : payment_generic_type_map) {
0901         if (payment.key() != key) {
0902             continue;
0903         }
0904         const auto s = m_element.tagValue(payment.keyName);
0905         if (!s.isEmpty()) {
0906             return QString::fromUtf8(s);
0907         }
0908     }
0909     return {};
0910 }
0911 
0912 QUrl OSMElementInformationModel::wikipediaUrl(const QByteArray &wp) const
0913 {
0914     if (wp.isEmpty()) {
0915         return {};
0916     }
0917 
0918     const auto s = QString::fromUtf8(wp);
0919     const auto idx = s.indexOf(QLatin1Char(':'));
0920     if (idx < 0) {
0921         return {};
0922     }
0923 
0924     QUrl url;
0925     url.setScheme(QStringLiteral("https"));
0926     url.setHost(QStringView(s).left(idx) + QLatin1String(".wikipedia.org"));
0927     url.setPath(QLatin1String("/wiki/") + QStringView(s).mid(idx + 1));
0928     return url;
0929 }
0930 
0931 QString OSMElementInformationModel::capacitryValue(const char *prop) const
0932 {
0933     const auto v = m_element.tagValue(prop);
0934     return translatedBoolValue(v);
0935 }
0936 
0937 QString OSMElementInformationModel::translatedBoolValue(const QByteArray &value) const
0938 {
0939     if (value == "yes") {
0940         return i18n("yes");
0941     }
0942     if (value == "no") {
0943         return i18n("no");
0944     }
0945     return QString::fromUtf8(value);
0946 }
0947 
0948 #include "moc_osmelementinformationmodel.cpp"