KOSMIndoorMap

osmelementinformationmodel.cpp
1/*
2 SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "osmelementinformationmodel.h"
8#include "osmelementinformationmodel_data.cpp"
9
10#include "localization.h"
11#include "osmaddress.h"
12
13#include <KLocalizedString>
14
15#include <cctype>
16
17using namespace KOSMIndoorMap;
18
19static QString formatDistance(int meter)
20{
21 if (meter < 1000) {
22 return i18n("%1m", meter);
23 }
24 if (meter < 10000) {
25 return i18n("%1km", ((int)meter/100)/10.0);
26 }
27 return i18n("%1km", (int)qRound(meter/1000.0));
28}
29
30bool OSMElementInformationModel::Info::operator<(OSMElementInformationModel::Info other) const
31{
32 if (category == other.category) {
33 return key < other.key;
34 }
35 return category < other.category;
36}
37
38bool OSMElementInformationModel::Info::operator==(OSMElementInformationModel::Info other) const
39{
40 return category == other.category && key == other.key;
41}
42
43
44OSMElementInformationModel::OSMElementInformationModel(QObject *parent)
46 , m_langs(OSM::Languages::fromQLocale(QLocale()))
47{
48}
49
50OSMElementInformationModel::~OSMElementInformationModel() = default;
51
52OSMElement OSMElementInformationModel::element() const
53{
54 return OSMElement(m_element);
55}
56
57void OSMElementInformationModel::setElement(const OSMElement &element)
58{
59 if (m_element == element.element()) {
60 return;
61 }
62
64 m_element = element.element();
65 m_infos.clear();
66 if (m_element.type() != OSM::Type::Null) {
67 reload();
68 }
70 Q_EMIT elementChanged();
71}
72
73void OSMElementInformationModel::clear()
74{
75 if (m_element.type() == OSM::Type::Null) {
76 return;
77 }
79 m_infos.clear();
80 m_element = {};
82 Q_EMIT elementChanged();
83}
84
85QString OSMElementInformationModel::name() const
86{
87 return valueForKey(Info{m_nameKey, Header}).toString();
88}
89
90QString OSMElementInformationModel::category() const
91{
92 return valueForKey(Info{m_categoryKey, Header}).toString();
93}
94
95int OSMElementInformationModel::rowCount(const QModelIndex &parent) const
96{
97 if (parent.isValid() || m_element.type() == OSM::Type::Null) {
98 return 0;
99 }
100 return m_infos.size();
101}
102
103QVariant OSMElementInformationModel::data(const QModelIndex &index, int role) const
104{
105 if (!index.isValid()) {
106 return {};
107 }
108
109 const auto info = m_infos[index.row()];
110 switch (role) {
111 case TypeRole:
112 switch (info.key) {
113 case Wikipedia:
114 case Phone:
115 case Email:
116 case Website:
117 case OperatorWikipedia:
118 case DebugLink:
119 return Link;
120 case Address:
121 return PostalAddress;
122 case OpeningHours:
123 return OpeningHoursType;
124 default:
125 return String;
126 }
127 case KeyRole:
128 return info.key;
129 case KeyLabelRole:
130 if (info.key == DebugKey) {
131 return debugTagKey(index.row());
132 }
133 return keyName(info.key);
134 case ValueRole:
135 switch (info.key) {
136 case DebugKey: return debugTagValue(index.row());
137 case Wikipedia: return i18n("Wikipedia");
138 default: return valueForKey(info);
139 }
140 case ValueUrlRole:
141 return urlify(valueForKey(info), info.key);
142 case CategoryRole:
143 return info.category;
144 case CategoryLabelRole:
145 return categoryLabel(info.category);
146 }
147
148 return {};
149}
150
151QHash<int, QByteArray> OSMElementInformationModel::roleNames() const
152{
154 r.insert(KeyRole, "key");
155 r.insert(KeyLabelRole, "keyLabel");
156 r.insert(ValueRole, "value");
157 r.insert(ValueUrlRole, "url");
158 r.insert(CategoryRole, "category");
159 r.insert(CategoryLabelRole, "categoryLabel");
160 r.insert(TypeRole, "type");
161 return r;
162}
163
164#define M(name, key, category) { name, OSMElementInformationModel::key, OSMElementInformationModel::category }
165struct KeyCategoryMapEntry {
166 const char *keyName;
167 OSMElementInformationModel::Key m_key;
168 OSMElementInformationModel::KeyCategory m_category;
169
170 constexpr inline OSMElementInformationModel::Key key() const { return m_key; }
171 constexpr inline OSMElementInformationModel::KeyCategory category() const { return m_category; }
172};
173
174static constexpr const KeyCategoryMapEntry simple_key_map[] = {
175 M("addr:city", Address, Contact),
176 M("addr:street", Address, Contact),
177 M("amenity", Category, Header),
178 M("bicycle_parking", BicycleParking, Parking),
179 M("brand", Name, Header),
180 M("brand:wikipedia", Wikipedia, UnresolvedCategory),
181 M("building", Category, Header),
182 M("bus_lines", Routes, Main),
183 M("bus_routes", Routes, Main),
184 M("buses", Routes, Main),
185 M("capacity", Capacity, UnresolvedCategory),
186 M("capacity:charging", CapacityCharing, Parking),
187 M("capacity:disabled", CapacityDisabled, Parking),
188 M("capacity:parent", CapacityParent, Parking),
189 M("capacity:women", CapacityWomen, Parking),
190 M("centralkey", CentralKey, Accessibility),
191 M("changing_table", DiaperChangingTable, UnresolvedCategory),
192 M("charge", Fee, UnresolvedCategory),
193 M("contact:city", Address, Contact),
194 M("contact:email", Email, Contact),
195 M("contact:phone", Phone, Contact),
196 M("contact:street", Address, Contact),
197 M("contact:website", Website, Contact),
198 M("cuisine", Cuisine, Main),
199 M("description", Description, Main),
200 M("diaper", DiaperChangingTable, UnresolvedCategory),
201 M("diplomatic", Category, Header),
202 M("email", Email, Contact),
203 M("fee", Fee, UnresolvedCategory),
204 M("genus", Name, Header),
205 M("historic", Category, Header),
206 M("int_name", Name, Header),
207 M("leisure", Category, Header),
208 M("maxstay", MaxStay, Parking),
209 M("mx:realtime_available", AvailableVehicles, Main),
210 M("mx:remaining_range", RemainingRange, Main),
211 M("mx:vehicle", Category, Header),
212 M("network", Network, Operator),
213 M("network:wikipedia", OperatorWikipedia, Operator),
214 M("office", Category, Header),
215 M("old_name", OldName, UnresolvedCategory),
216 M("opening_hours", OpeningHours, OpeningHoursCategory),
217 M("operator", OperatorName, Operator),
218 M("operator:email", Email, Contact),
219 M("operator:phone", Phone, Contact),
220 M("operator:website", Website, Contact),
221 M("operator:wikipedia", OperatorWikipedia, Operator),
222 M("parking:fee", Fee, Parking),
223 M("payment:cash", PaymentCash, Payment),
224 M("payment:coins", PaymentCash, Payment),
225 M("payment:notes", PaymentCash, Payment),
226 M("phone", Phone, Contact),
227 M("room", Category, Header),
228 M("route_ref", Routes, Main),
229 M("shop", Category, Header),
230 M("tactile_writing", TactileWriting, Accessibility), // occurs also unqualified
231 M("takeaway", Takeaway, Main),
232 M("toilets:fee", Fee, Toilets),
233 M("toilets:wheelchair", Wheelchair, Toilets),
234 M("tourism", Category, Header),
235 M("url", Website, Contact),
236 M("website", Website, Contact),
237 M("wheelchair", Wheelchair, Accessibility),
238 M("wheelchair:lift", WheelchairLift, Accessibility),
239};
240static_assert(isSortedLookupTable(simple_key_map), "key map is not sorted!");
241
242static constexpr const KeyCategoryMapEntry localized_key_map[] = {
243 M("name", Name, Header),
244 M("loc_name", Name, Header),
245 M("species", Name, Header),
246 M("species:wikipedia", Wikipedia, UnresolvedCategory),
247 M("speech_output", SpeechOutput, Accessibility),
248 M("wikipedia", Wikipedia, UnresolvedCategory),
249};
250#undef M
251
252template <typename KeyMapEntry, std::size_t N>
253void OSMElementInformationModel::addEntryForKey(const char *keyName, const KeyMapEntry(&map)[N])
254{
255 const auto it = std::lower_bound(std::begin(map), std::end(map), keyName, [](const auto &lhs, auto rhs) {
256 return std::strcmp(lhs.keyName, rhs) < 0;
257 });
258 if (it != std::end(map) && std::strcmp((*it).keyName, keyName) == 0) {
259 m_infos.push_back(Info{(*it).key(), (*it).category()});
260 }
261}
262
263template <typename KeyMapEntry, std::size_t N>
264void OSMElementInformationModel::addEntryForLocalizedKey(const char *keyName, const KeyMapEntry(&map)[N])
265{
266 for (const auto &entry : map) {
267 const auto mapKeyLen = std::strlen(entry.keyName);
268 if (std::strncmp(keyName, entry.keyName, mapKeyLen) != 0) {
269 continue;
270 }
271 const auto keyNameLen = std::strlen(keyName);
272 if (keyNameLen == mapKeyLen || (keyNameLen == mapKeyLen + 3 && keyName[mapKeyLen] == ':')) {
273 m_infos.push_back(Info{entry.key(), entry.category()});
274 return;
275 }
276 }
277}
278
279void OSMElementInformationModel::reload()
280{
281 m_nameKey = NoKey;
282 m_categoryKey = NoKey;
283
284 const bool isRoom = m_element.tagValue("indoor") == "room";
285 for (auto it = m_element.tagsBegin(); it != m_element.tagsEnd(); ++it) {
286 addEntryForLocalizedKey((*it).key.name(), localized_key_map);
287 addEntryForKey((*it).key.name(), simple_key_map);
288 addEntryForKey((*it).key.name(), payment_generic_type_map);
289 addEntryForKey((*it).key.name(), payment_type_map);
290 addEntryForKey((*it).key.name(), diet_type_map);
291 addEntryForKey((*it).key.name(), socket_type_map);
292 addEntryForKey((*it).key.name(), authentication_type_map);
293 addEntryForKey((*it).key.name(), gender_type_map);
294 addEntryForLocalizedKey((*it).key.name(), tactile_writing_map);
295
296 if (isRoom && std::strcmp((*it).key.name(), "ref") == 0) {
297 m_infos.push_back(Info{Name, Header});
298 }
299 }
300
301 std::sort(m_infos.begin(), m_infos.end());
302 m_infos.erase(std::unique(m_infos.begin(), m_infos.end()), m_infos.end());
303 resolveCategories();
304 resolveHeaders();
305
306 // if we don't have a primary group, promote a suitable secondary one
307 for (auto cat : {Parking, Toilets}) {
308 if (promoteMainCategory(cat)) {
309 break;
310 }
311 }
312
313 // resolve all remaining unresolved elements to the primary category
314 for (auto &info : m_infos) {
315 if (info.category == UnresolvedCategory) {
316 info.category = Main;
317 }
318 }
319 std::sort(m_infos.begin(), m_infos.end());
320 m_infos.erase(std::unique(m_infos.begin(), m_infos.end()), m_infos.end());
321
322 if (m_debug) {
323 m_infos.push_back(Info{ DebugLink, DebugCategory });
324 const auto count = std::distance(m_element.tagsBegin(), m_element.tagsEnd());
325 std::fill_n(std::back_inserter(m_infos), count, Info{ DebugKey, DebugCategory });
326 }
327}
328
329void OSMElementInformationModel::resolveCategories()
330{
331 if (m_infos.empty() || m_infos[0].category != UnresolvedCategory) {
332 return;
333 }
334 for (auto &info : m_infos) {
335 if (info.category != UnresolvedCategory) {
336 break;
337 }
338 switch (info.key) {
339 case Fee:
340 if (m_element.tagValue("parking:fee").isEmpty() && (!m_element.tagValue("parking").isEmpty()
341 || m_element.tagValue("amenity") == "parking" || m_element.tagValue("amenity") == "bicycle_parking"))
342 {
343 info.category = Parking;
344 } else if (m_element.tagValue("toilets:fee").isEmpty() && (m_element.tagValue("toilets") == "yes" || m_element.tagValue("amenity") == "toilets")) {
345 info.category = Toilets;
346 } else {
347 info.category = Main;
348 }
349 break;
350 case Capacity:
351 if (m_element.tagValue("amenity").endsWith("rental")) {
352 info.category = Main;
353 } else {
354 info.category = Parking;
355 }
356 break;
357 default:
358 {
359 // for anything else: if it's not clearly something we have a secondary group for, resolve it to Main
360 const auto amenity = m_element.tagValue("amenity");
361 if ((amenity != "parking" && amenity != "toilets")
362 || !m_element.tagValue("office").isEmpty()
363 || (!m_element.tagValue("room").isEmpty() && m_element.tagValue("room") != "toilets")
364 || !m_element.tagValue("shop").isEmpty()
365 || !m_element.tagValue("tourism").isEmpty()) {
366 info.category = Main;
367 }
368 break;
369 }
370 }
371 }
372 std::sort(m_infos.begin(), m_infos.end());
373}
374
375void OSMElementInformationModel::resolveHeaders()
376{
377 for (auto key : { Name, Network, OperatorName, Category }) {
378 if (m_nameKey != NoKey) {
379 break;
380 }
381
382 const auto it = std::find_if(m_infos.begin(), m_infos.end(), [key](Info info) {
383 return info.key == key;
384 });
385 if (it == m_infos.end()) {
386 continue;
387 }
388
389 m_nameKey = (*it).key;
390 m_infos.erase(it);
391 break;
392 }
393
394 // we use the categories as header if there is no name, so don't duplicate that
395 const auto it = std::find_if(m_infos.begin(), m_infos.end(), [](Info info) {
396 return info.key == Category;
397 });
398 if (it == m_infos.end() || m_nameKey == Category) {
399 return;
400 }
401
402 m_infos.erase(it);
403 m_categoryKey = Category;
404}
405
406bool OSMElementInformationModel::promoteMainCategory(OSMElementInformationModel::KeyCategory cat)
407{
408 const auto hasMain = std::any_of(m_infos.begin(), m_infos.end(), [](const auto &info) {
409 return info.category == Main;
410 });
411
412 if (hasMain) {
413 return true;
414 }
415
416 bool didPromote = false;
417 for (auto &info : m_infos) {
418 if (info.category == cat) {
419 info.category = (info.key == Wheelchair ? Accessibility : Main);
420 didPromote = true;
421 }
422 }
423
424 if (didPromote) {
425 std::sort(m_infos.begin(), m_infos.end());
426 }
427 return didPromote;
428}
429
430QString OSMElementInformationModel::categoryLabel(OSMElementInformationModel::KeyCategory cat) const
431{
432 switch (cat) {
433 case UnresolvedCategory:
434 case Header:
435 case Main: return {};
436 case OpeningHoursCategory: return i18n("Opening Hours");
437 case Contact: return i18n("Contact");
438 case Payment: return i18n("Payment");
439 case Toilets: return i18n("Toilets");
440 case Accessibility: return i18n("Accessibility");
441 case Parking: return i18n("Parking");
442 case Operator: return i18n("Operator");
443 case DebugCategory: return QStringLiteral("Debug");
444 }
445 return {};
446}
447
448QString OSMElementInformationModel::debugTagKey(int row) const
449{
450 const auto tagCount = std::distance(m_element.tagsBegin(), m_element.tagsEnd());
451 const auto tagIdx = row - (rowCount() - tagCount);
452 return QString::fromUtf8((*(m_element.tagsBegin() + tagIdx)).key.name());
453}
454
455QString OSMElementInformationModel::debugTagValue(int row) const
456{
457 const auto tagCount = std::distance(m_element.tagsBegin(), m_element.tagsEnd());
458 const auto tagIdx = row - (rowCount() - tagCount);
459 return QString::fromUtf8((*(m_element.tagsBegin() + tagIdx)).value);
460}
461
462QString OSMElementInformationModel::keyName(OSMElementInformationModel::Key key) const
463{
464 switch (key) {
465 case NoKey:
466 case Name:
467 case Category: return {};
468 case OldName: return i18n("Formerly");
469 case Description: return i18n("Description");
470 case Routes: return i18n("Routes");
471 case Cuisine: return i18n("Cuisine");
472 case Diet: return i18n("Diet");
473 case Takeaway: return i18n("Takeaway");
474 case Socket: return i18nc("electrical power socket", "Socket");
475 case OpeningHours: return {};
476 case AvailableVehicles: return i18n("Available vehicles");
477 case Fee: return i18n("Fee");
478 case Authentication: return i18n("Authentication");
479 case BicycleParking: return i18n("Bicycle parking");
480 case Capacity: return i18n("Capacity");
481 case CapacityDisabled: return i18n("Disabled parking spaces");
482 case CapacityWomen: return i18n("Women parking spaces");
483 case CapacityParent: return i18n("Parent parking spaces");
484 case CapacityCharing: return i18n("Parking spaces for charging");
485 case MaxStay: return i18n("Maximum stay");
486 case DiaperChangingTable: return i18n("Diaper changing table");
487 case Gender: return i18n("Gender");
488 case Wikipedia: return {};
489 case Address: return i18n("Address");
490 case Phone: return i18n("Phone");
491 case Email: return i18n("Email");
492 case Website: return i18n("Website");
493 case PaymentCash: return i18n("Cash");
494 case PaymentDigital: return i18n("Digital");
495 case PaymentDebitCard: return i18n("Debit cards");
496 case PaymentCreditCard: return i18n("Credit cards");
497 case PaymentStoredValueCard: return i18n("Stored value cards");
498 case Wheelchair: return i18n("Wheelchair access");
499 case WheelchairLift: return i18n("Wheelchair lift");
500 case CentralKey: return i18n("Central key");
501 case SpeechOutput: return i18n("Speech output");
502 case TactileWriting: return i18n("Tactile writing");
503 case OperatorName: return {};
504 case Network: return i18nc("transport network", "Network");
505 case OperatorWikipedia: return {};
506 case RemainingRange: return i18nc("remaining travel range of a battery powered vehicle", "Remaining range");
507 case DebugLink: return QStringLiteral("OSM");
508 case DebugKey: return {};
509 }
510 return {};
511}
512
513static void appendNonEmpty(const QByteArray &tagValue, QList<QByteArray> &l)
514{
515 if (tagValue.isEmpty()) {
516 return;
517 }
518 auto split = tagValue.split(';');
519 for (const auto &s : split) {
520 if (!s.isEmpty()) {
521 l.push_back(s.trimmed());
522 }
523 }
524}
525
526static QChar::Script scriptForString(QStringView s)
527{
528 return std::accumulate(s.begin(), s.end(), QChar::Script_Unknown, [](QChar::Script script, QChar c) { return std::max(script, c.script());});
529}
530
531// why do we have two different script enums???
532// ### far from complete, this only handles the cases where int_name is in widespread use so far
533struct {
534 QLocale::Script localeScript;
535 QChar::Script charScript;
536} static constexpr const script_map[] = {
539};
540
541static bool isSameScript(QLocale::Script ls, QChar::Script cs)
542{
543 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);
544}
545
546QVariant OSMElementInformationModel::valueForKey(Info info) const
547{
548 switch (info.key) {
549 case NoKey: return {};
550 case Name: {
551 const auto n = QString::fromUtf8(m_element.tagValue(m_langs, "name", "loc_name", "int_name", "brand", "ref", "species", "genus"));
552 const auto script = scriptForString(n);
553 if (!isSameScript(QLocale().script(), script) && script > QChar::Script_Latin) {
554 const auto transliterated = QString::fromUtf8(m_element.tagValue(m_langs, "int_name"));
555 if (transliterated.isEmpty() || transliterated == n) {
556 return n;
557 }
558 return i18nc("local name (transliterated name)", "%1 (%2)", n, transliterated);
559 }
560 return n;
561 }
562 case Category:
563 {
565 appendNonEmpty(m_element.tagValue("amenity"), l);
566 appendNonEmpty(m_element.tagValue("shop"), l);
567 appendNonEmpty(m_element.tagValue("tourism"), l);
568 appendNonEmpty(m_element.tagValue("vending"), l);
569 const auto diplomatic = m_element.tagValue("diplomatic");
570 appendNonEmpty(diplomatic, l);
571 if (diplomatic.isEmpty()) {
572 appendNonEmpty(m_element.tagValue("office"), l);
573 }
574 appendNonEmpty(m_element.tagValue("leisure"), l);
575 appendNonEmpty(m_element.tagValue("historic"), l);
576 appendNonEmpty(m_element.tagValue("mx:vehicle"), l);
577 if (l.isEmpty()) {
578 appendNonEmpty(m_element.tagValue("room"), l);
579 }
580
581 QStringList out;
582 out.reserve(l.size());
583
584 // TODO drop general categories if specific ones are available (e.g. restaurant vs fast_food)
585
586 for (auto it = l.begin(); it != l.end();++it) {
587 if ((*it).isEmpty() || (*it) == "yes" || (*it) == "no" || (*it) == "vending_machine" || (*it) == "building") {
588 continue;
589 }
590 out.push_back(Localization::amenityType((*it).constData()));
591 }
592
593 if (out.isEmpty()) { // fall back to building, but only take terms we have translated
594 appendNonEmpty(m_element.tagValue("building"), l);
595 for (const auto &key : l) {
596 auto s = Localization::amenityType(key.constData(), Localization::ReturnEmptyOnUnknownKey);
597 if (!s.isEmpty()) {
598 out.push_back(std::move(s));
599 }
600 }
601 }
602
603 std::sort(out.begin(), out.end());
604 out.erase(std::unique(out.begin(), out.end()), out.end());
605 return QLocale().createSeparatedList(out);
606 }
607 case OldName:
608 {
609 const auto l = QString::fromUtf8(m_element.tagValue("old_name")).split(QLatin1Char(';'));
610 return l.join(QLatin1String(", "));
611 }
612 case Description:
613 return m_element.tagValue(m_langs, "description");
614 case Routes:
615 {
616 auto l = QString::fromUtf8(m_element.tagValue("route_ref", "bus_routes", "bus_lines", "buses")).split(QLatin1Char(';'), Qt::SkipEmptyParts);
617 for (auto &s : l) {
618 s = s.trimmed();
619 }
620 return QLocale().createSeparatedList(l);
621 }
622 case Cuisine: return Localization::cuisineTypes(m_element.tagValue("cuisine"));
623 case Diet:
624 {
625 QStringList l;
626 for (const auto &d : diet_type_map) {
627 const auto v = m_element.tagValue(d.keyName);
628 const auto label = d.label.toString();
629 if (v == "yes") {
630 l.push_back(label);
631 } else if (v == "only") {
632 l.push_back(i18n("only %1", label));
633 } else if (v == "no") {
634 l.push_back(i18n("no %1", label));
635 }
636 }
637 return l.join(QLatin1String(", "));
638 }
639 case Takeaway: return translatedBoolValue(m_element.tagValue("takeaway")); // TODO decode (yes/only/no) and translate
640 case Socket:
641 {
642 QStringList l;
643 for (const auto &socket : socket_type_map) {
644 const auto value = m_element.tagValue(socket.keyName);
645 if (value.isEmpty() || value == "no") {
646 continue;
647 }
648
649 auto s = socket.label.toString();
650
651 QStringList details;
652 if (value != "yes") {
653 details.push_back(QString::fromUtf8(value));
654 }
655
656 const auto current = m_element.tagValue(QByteArray(socket.keyName + QByteArray(":current")).constData());
657 if (!current.isEmpty()) {
658 if (std::all_of(current.begin(), current.end(), [](unsigned char c) { return std::isdigit(c); })) {
659 details.push_back(i18nc("electrical current/Ampere value", "%1 A", QString::fromUtf8(current)));
660 } else {
661 details.push_back(QString::fromUtf8(current));
662 }
663 }
664 const auto output = m_element.tagValue(QByteArray(socket.keyName + QByteArray(":output")).constData());
665 if (!output.isEmpty()) {
666 if (std::all_of(output.begin(), output.end(), [](unsigned char c) { return std::isdigit(c); })) {
667 details.push_back(i18nc("electrical power/kilowatt value", "%1 kW", QString::fromUtf8(output)));
668 } else {
669 details.push_back(QString::fromUtf8(output));
670 }
671 }
672
673 if (!details.empty()) {
674 s += QLatin1String(" (") + details.join(QLatin1String(", ")) + QLatin1Char(')');
675 }
676 l.push_back(s);
677 }
678 return QLocale().createSeparatedList(l);
679 }
680 case OpeningHours: return QString::fromUtf8(m_element.tagValue("opening_hours"));
681 case AvailableVehicles:
682 {
683 const auto total = m_element.tagValue("mx:realtime_available").toInt();
684 QStringList types;
685 for (const auto &v : available_vehicles_map) {
686 const auto b = m_element.tagValue(v.keyName);
687 if (b.isEmpty()) {
688 continue;
689 }
690 types.push_back(v.label.subs(b.toInt()).toString());
691 }
692
693 if (types.isEmpty()) {
694 return QLocale().toString(total);
695 } else if (types.size() == 1) {
696 return types.at(0);
697 } else {
698 return i18n("%1 (%2)", total, QLocale().createSeparatedList(types));
699 }
700 }
701 case Fee:
702 {
703 QByteArray fee;
704 switch (info.category) {
705 case Parking: fee = m_element.tagValue("parking:fee", "fee"); break;
706 case Toilets: fee = m_element.tagValue("toilets:fee", "fee"); break;
707 default: fee = m_element.tagValue("fee");
708 }
709 auto s = QString::fromUtf8(fee);
710 const auto charge = QString::fromUtf8(m_element.tagValue("charge"));
711 if (s.isEmpty()) {
712 return charge;
713 }
714 if (!charge.isEmpty()) {
715 s += QLatin1String(" (") + charge + QLatin1Char(')');
716 }
717 return s;
718 }
719 case Authentication:
720 {
721 QStringList l;
722 for (const auto &auth : authentication_type_map) {
723 const auto v = m_element.tagValue(auth.keyName);
724 if (v.isEmpty() || v == "no") {
725 continue;
726 }
727 l.push_back(auth.label.toString());
728 }
729 return QLocale().createSeparatedList(l);
730 }
731 case BicycleParking: return translateValues(m_element.tagValue("bicycle_parking"), bicycle_parking_map);
732 case Capacity: return QString::fromUtf8(m_element.tagValue("capacity"));
733 case CapacityDisabled: return capacitryValue("capacity:disabled");
734 case CapacityWomen: return capacitryValue("capacity:women");
735 case CapacityParent: return capacitryValue("capacity:parent");
736 case CapacityCharing: return capacitryValue("capacity:charging");
737 case MaxStay: return QString::fromUtf8(m_element.tagValue("maxstay"));
738 case DiaperChangingTable:
739 // TODO look for changing_table:location too
740 return translatedBoolValue(m_element.tagValue("changing_table", "diaper"));
741 case Gender:
742 {
743 QStringList l;
744 for (const auto &gender : gender_type_map) {
745 const auto v = m_element.tagValue(gender.keyName);
746 if (v.isEmpty() || v == "no") {
747 continue;
748 }
749 l.push_back(gender.label.toString());
750 }
751 return QLocale().createSeparatedList(l);
752 }
753 case Wikipedia: return wikipediaUrl(m_element.tagValue(m_langs, "wikipedia", "brand:wikipedia", "species:wikipedia"));
754 case Address: return QVariant::fromValue(OSMAddress(m_element));
755 case Phone: return QString::fromUtf8(m_element.tagValue("contact:phone", "phone", "telephone", "operator:phone"));
756 case Email: return QString::fromUtf8(m_element.tagValue("contact:email", "email", "operator:email"));
757 case Website: return QString::fromUtf8(m_element.tagValue("website", "contact:website", "url", "operator:website"));
758 case PaymentCash:
759 {
760 const auto coins = m_element.tagValue("payment:coins");
761 const auto notes = m_element.tagValue("payment:notes");
762 if (coins.isEmpty() && notes.isEmpty()) {
763 return translatedBoolValue(m_element.tagValue("payment:cash"));
764 }
765 if (coins == "yes" && notes == "yes") {
766 return i18n("yes");
767 }
768 if (coins == "yes") {
769 return i18nc("payment option", "coins only");
770 }
771 if (notes == "yes") {
772 return i18nc("payment option", "notes only");
773 }
774 return i18n("no");
775 }
776 case PaymentDigital:
777 case PaymentDebitCard:
778 case PaymentCreditCard:
779 case PaymentStoredValueCard:
780 return paymentMethodValue(info.key);
781 case Wheelchair:
782 {
783 QByteArray wheelchair;
784 if (info.category == Toilets) {
785 wheelchair = m_element.tagValue("toilets:wheelchair", "wheelchair");
786 } else {
787 wheelchair = m_element.tagValue("wheelchair");
788 }
789 const auto a = translateValue(wheelchair.constData(), wheelchair_map);
790 const auto d = QString::fromUtf8(m_element.tagValue(m_langs, "wheelchair:description"));
791 if (!d.isEmpty()) {
792 return QString(a + QLatin1String(" (") + d + QLatin1Char(')'));
793 }
794 return a;
795 }
796 case WheelchairLift:
797 return translatedBoolValue(m_element.tagValue("wheelchair:lift"));
798 case CentralKey:
799 // translate enum values
800 return QString::fromUtf8(m_element.tagValue("centralkey"));
801 case SpeechOutput:
802 // TODO: rather than as a boolean value, list the available languages here when we have that information
803 return translatedBoolValue(m_element.tagValue(m_langs, "speech_output"));
804 case TactileWriting:
805 {
806 // TODO: rather than as a boolean value, list the available languages here when we have that information
807 QStringList l;
808 bool explicitNo = false;
809 for (const auto &writing : tactile_writing_map) {
810 const auto v = m_element.tagValue(m_langs, writing.keyName);
811 if (v.isEmpty()) {
812 continue;
813 }
814 if (v == "no") {
815 explicitNo = true;
816 continue;
817 }
818 l.push_back(writing.label.toString());
819 }
820 if (!l.isEmpty()) {
821 return QLocale().createSeparatedList(l);
822 }
823 const auto v = m_element.tagValue(m_langs, "tactile_writing");
824 if (explicitNo && v.isEmpty()) {
825 return i18n("no");
826 }
827 return translatedBoolValue(v);
828 }
829 case OperatorName: return QString::fromUtf8(m_element.tagValue("operator"));
830 case Network: return QString::fromUtf8(m_element.tagValue("network"));
831 case OperatorWikipedia: return wikipediaUrl(m_element.tagValue(m_langs, "operator:wikipedia", "network:wikipedia"));
832 case RemainingRange:
833 {
834 const auto range = m_element.tagValue("mx:remaining_range").toInt();
835 return formatDistance(range);
836 }
837 case DebugLink: return m_element.url();
838 case DebugKey: return {};
839 }
840 return {};
841}
842
843QVariant OSMElementInformationModel::urlify(const QVariant& v, OSMElementInformationModel::Key key) const
844{
845 if (v.userType() != QMetaType::QString) {
846 return v;
847 }
848 const auto s = v.toString();
849
850 switch (key) {
851 case Email:
852 if (!s.startsWith(QLatin1String("mailto:"))) {
853 return QString(QLatin1String("mailto:") + s);
854 }
855 return s;
856 case Phone:
857 {
858 if (s.startsWith(QLatin1String("tel:"))) {
859 return s;
860 }
861 QString e = QLatin1String("tel:") + s;
862 e.remove(QLatin1Char(' '));
863 return e;
864 }
865 case Website:
866 case DebugLink:
867 if (s.startsWith(QLatin1String("http"))) {
868 return s;
869 }
870 return QString(QLatin1String("https://") + s);
871 default:
872 return {};
873 }
874
875 return {};
876}
877
878QString OSMElementInformationModel::paymentMethodList(OSMElementInformationModel::Key key) const
879{
880 QStringList l;
881 for (const auto &payment : payment_type_map) {
882 if (payment.key() != key) {
883 continue;
884 }
885 if (m_element.tagValue(payment.keyName) == "yes") {
886 l.push_back(payment.label.toString());
887 }
888 }
889 std::sort(l.begin(), l.end());
890 return QLocale().createSeparatedList(l);
891}
892
893QString OSMElementInformationModel::paymentMethodValue(OSMElementInformationModel::Key key) const
894{
895 const auto s = paymentMethodList(key);
896 if (!s.isEmpty()) {
897 return s;
898 }
899
900 for (const auto &payment : payment_generic_type_map) {
901 if (payment.key() != key) {
902 continue;
903 }
904 const auto s = m_element.tagValue(payment.keyName);
905 if (!s.isEmpty()) {
906 return QString::fromUtf8(s);
907 }
908 }
909 return {};
910}
911
912QUrl OSMElementInformationModel::wikipediaUrl(const QByteArray &wp) const
913{
914 if (wp.isEmpty()) {
915 return {};
916 }
917
918 const auto s = QString::fromUtf8(wp);
919 const auto idx = s.indexOf(QLatin1Char(':'));
920 if (idx < 0) {
921 return {};
922 }
923
924 QUrl url;
925 url.setScheme(QStringLiteral("https"));
926 url.setHost(QStringView(s).left(idx) + QLatin1String(".wikipedia.org"));
927 url.setPath(QLatin1String("/wiki/") + QStringView(s).mid(idx + 1));
928 return url;
929}
930
931QString OSMElementInformationModel::capacitryValue(const char *prop) const
932{
933 const auto v = m_element.tagValue(prop);
934 return translatedBoolValue(v);
935}
936
937QString OSMElementInformationModel::translatedBoolValue(const QByteArray &value) const
938{
939 if (value == "yes") {
940 return i18n("yes");
941 }
942 if (value == "no") {
943 return i18n("no");
944 }
945 return QString::fromUtf8(value);
946}
947
948#include "moc_osmelementinformationmodel.cpp"
QString toString() const
Postal address from OSM data.
Definition osmaddress.h:16
QML wrapper around an OSM element.
Definition osmelement.h:19
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
char * toString(const EngineQuery &query)
QString cuisineTypes(const QByteArray &value, Localization::TranslationOption opt=Localization::ReturnUnknownKey)
Translated values of the cuisine tag (does list splitting).
QString amenityType(const char *value, Localization::TranslationOption opt=Localization::ReturnUnknownKey)
Translated name for an amenity tag value (after list splitting).
OSM-based multi-floor indoor maps for buildings.
Category category(StandardShortcut id)
Low-level types and functions to work with raw OSM data as efficiently as possible.
QByteArray tagValue(const Elem &elem, TagKey key)
Returns the tag value for key of elem.
Definition datatypes.h:410
virtual QHash< int, QByteArray > roleNames() const const
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const override
const char * constData() const const
bool endsWith(QByteArrayView bv) const const
bool isEmpty() const const
QList< QByteArray > split(char sep) const const
int toInt(bool *ok, int base) const const
const_reference at(qsizetype i) const const
iterator begin()
bool empty() const const
iterator end()
iterator erase(const_iterator begin, const_iterator end)
bool isEmpty() const const
void push_back(parameter_type value)
void reserve(qsizetype size)
qsizetype size() const const
QString createSeparatedList(const QStringList &list) const const
QString toString(QDate date, FormatType format) const const
bool isValid() const const
int row() const const
Q_EMITQ_EMIT
QObject * parent() const const
QString fromUtf8(QByteArrayView str)
QString & remove(QChar ch, Qt::CaseSensitivity cs)
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
QString join(QChar separator) const const
const_iterator begin() const const
const_iterator end() const const
SkipEmptyParts
QTextStream & left(QTextStream &stream)
QFuture< void > map(Iterator begin, Iterator end, MapFunctor &&function)
void setHost(const QString &host, ParsingMode mode)
void setPath(const QString &path, ParsingMode mode)
void setScheme(const QString &scheme)
QVariant fromValue(T &&value)
QString toString() const const
int userType() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:20:03 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.