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"