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 <wikidata/wikidataquery.h>
14
15#include <KLocalizedString>
16
17#include <QUrlQuery>
18
19#include <cctype>
20
21using namespace Qt::Literals::StringLiterals;
22using namespace KOSMIndoorMap;
23
24[[nodiscard]] static QString formatDistance(int meter)
25{
26 if (meter < 1000) {
27 return i18n("%1m", meter);
28 }
29 if (meter < 10000) {
30 return i18n("%1km", ((int)meter/100)/10.0);
31 }
32 return i18n("%1km", (int)qRound(meter/1000.0));
33}
34
35bool OSMElementInformationModel::Info::operator<(OSMElementInformationModel::Info other) const
36{
37 if (category == other.category) {
38 return key < other.key;
39 }
40 return category < other.category;
41}
42
43bool OSMElementInformationModel::Info::operator==(OSMElementInformationModel::Info other) const
44{
45 return category == other.category && key == other.key;
46}
47
48
49OSMElementInformationModel::OSMElementInformationModel(QObject *parent)
51 , m_langs(OSM::Languages::fromQLocale(QLocale()))
52{
53 m_wikidataMgr.setUserAgentEmailAddress(u"kde-pim@kde.org"_s);
54}
55
56OSMElementInformationModel::~OSMElementInformationModel() = default;
57
58OSMElement OSMElementInformationModel::element() const
59{
60 return OSMElement(m_element);
61}
62
63void OSMElementInformationModel::setElement(const OSMElement &element)
64{
65 if (m_element == element.element()) {
66 return;
67 }
68
70 m_element = element.element();
71 m_infos.clear();
72 if (m_element.type() != OSM::Type::Null) {
73 reload();
74 }
76 Q_EMIT elementChanged();
77}
78
79void OSMElementInformationModel::clear()
80{
81 if (m_element.type() == OSM::Type::Null) {
82 return;
83 }
85 m_infos.clear();
86 m_element = {};
88 Q_EMIT elementChanged();
89}
90
91QString OSMElementInformationModel::name() const
92{
93 return valueForKey(Info{m_nameKey, Header}).toString();
94}
95
96QString OSMElementInformationModel::category() const
97{
98 return valueForKey(Info{m_categoryKey, Header}).toString();
99}
100
101int OSMElementInformationModel::rowCount(const QModelIndex &parent) const
102{
103 if (parent.isValid() || m_element.type() == OSM::Type::Null) {
104 return 0;
105 }
106 return (int)m_infos.size();
107}
108
109QVariant OSMElementInformationModel::data(const QModelIndex &index, int role) const
110{
111 if (!index.isValid()) {
112 return {};
113 }
114
115 const auto info = m_infos[index.row()];
116 switch (role) {
117 case TypeRole:
118 switch (info.key) {
119 case Wikipedia:
120 case Phone:
121 case Email:
122 case Website:
123 case OperatorWikipedia:
124 case DebugLink:
125 return Link;
126 case Address:
127 return PostalAddress;
128 case OpeningHours:
129 return OpeningHoursType;
130 case Image:
131 case Logo:
132 return ImageType;
133 case DebugKey:
134 return debugTagUrl(index.row()).isValid() ? Link : String;
135 default:
136 return String;
137 }
138 case KeyRole:
139 return info.key;
140 case KeyLabelRole:
141 if (info.key == DebugKey) {
142 return debugTagKey(index.row());
143 }
144 return keyName(info.key);
145 case ValueRole:
146 switch (info.key) {
147 case DebugKey: return debugTagValue(index.row());
148 case Wikipedia: return i18n("Wikipedia");
149 default: return valueForKey(info);
150 }
151 case ValueUrlRole:
152 if (info.key == DebugKey) {
153 return debugTagUrl(index.row());
154 }
155 return urlify(valueForKey(info), info.key);
156 case CategoryRole:
157 return info.category;
158 case CategoryLabelRole:
159 return categoryLabel(info.category);
160 }
161
162 return {};
163}
164
165QHash<int, QByteArray> OSMElementInformationModel::roleNames() const
166{
168 r.insert(KeyRole, "key");
169 r.insert(KeyLabelRole, "keyLabel");
170 r.insert(ValueRole, "value");
171 r.insert(ValueUrlRole, "url");
172 r.insert(CategoryRole, "category");
173 r.insert(CategoryLabelRole, "categoryLabel");
174 r.insert(TypeRole, "type");
175 return r;
176}
177
178#define M(name, key, category) { name, OSMElementInformationModel::key, OSMElementInformationModel::category }
179struct KeyCategoryMapEntry {
180 const char *keyName;
181 OSMElementInformationModel::Key m_key;
182 OSMElementInformationModel::KeyCategory m_category;
183
184 [[nodiscard]] constexpr inline OSMElementInformationModel::Key key() const { return m_key; }
185 [[nodiscard]] constexpr inline OSMElementInformationModel::KeyCategory category() const { return m_category; }
186};
187
188static constexpr const KeyCategoryMapEntry simple_key_map[] = {
189 M("addr:city", Address, Contact),
190 M("addr:street", Address, Contact),
191 M("amenity", Category, Header),
192 M("bicycle_parking", BicycleParking, Parking),
193 M("brand", Name, Header),
194 M("brand:wikidata", Image, Main),
195 M("brand:wikipedia", Wikipedia, UnresolvedCategory),
196 M("building", Category, Header),
197 M("bus_lines", Routes, Main),
198 M("bus_routes", Routes, Main),
199 M("buses", Routes, Main),
200 M("capacity", Capacity, UnresolvedCategory),
201 M("capacity:charging", CapacityCharing, Parking),
202 M("capacity:disabled", CapacityDisabled, Parking),
203 M("capacity:parent", CapacityParent, Parking),
204 M("capacity:women", CapacityWomen, Parking),
205 M("centralkey", CentralKey, Accessibility),
206 M("changing_table", DiaperChangingTable, UnresolvedCategory),
207 M("charge", Fee, UnresolvedCategory),
208 M("contact:city", Address, Contact),
209 M("contact:email", Email, Contact),
210 M("contact:phone", Phone, Contact),
211 M("contact:street", Address, Contact),
212 M("contact:website", Website, Contact),
213 M("cuisine", Cuisine, Main),
214 M("description", Description, Main),
215 M("diaper", DiaperChangingTable, UnresolvedCategory),
216 M("diplomatic", Category, Header),
217 M("email", Email, Contact),
218 M("fee", Fee, UnresolvedCategory),
219 M("genus", Name, Header),
220 M("genus:wikidata", Image, Main),
221 M("historic", Category, Header),
222 M("image", Image, Main),
223 M("int_name", Name, Header),
224 M("leisure", Category, Header),
225 M("maxstay", MaxStay, Parking),
226 M("memorial:text", Description, Main),
227 M("mx:realtime_available", AvailableVehicles, Main),
228 M("mx:remaining_range", RemainingRange, Main),
229 M("mx:vehicle", Category, Header),
230 M("network", Network, Operator),
231 M("network:wikidata", Image, Main),
232 M("network:wikipedia", OperatorWikipedia, Operator),
233 M("office", Category, Header),
234 M("old_name", OldName, UnresolvedCategory),
235 M("opening_hours", OpeningHours, OpeningHoursCategory),
236 M("operator", OperatorName, Operator),
237 M("operator:email", Email, Contact),
238 M("operator:phone", Phone, Contact),
239 M("operator:website", Website, Contact),
240 M("operator:wikidata", Image, Main),
241 M("operator:wikipedia", OperatorWikipedia, Operator),
242 M("parking:fee", Fee, Parking),
243 M("payment:cash", PaymentCash, Payment),
244 M("payment:coins", PaymentCash, Payment),
245 M("payment:notes", PaymentCash, Payment),
246 M("phone", Phone, Contact),
247 M("room", Category, Header),
248 M("route_ref", Routes, Main),
249 M("shop", Category, Header),
250 M("species:wikidata", Image, Main),
251 M("tactile_writing", TactileWriting, Accessibility), // occurs also unqualified
252 M("takeaway", Takeaway, Main),
253 M("toilets:fee", Fee, Toilets),
254 M("toilets:wheelchair", Wheelchair, Toilets),
255 M("tourism", Category, Header),
256 M("url", Website, Contact),
257 M("vending", VendingMachineOffer, Header),
258 M("website", Website, Contact),
259 M("wheelchair", Wheelchair, Accessibility),
260 M("wheelchair:lift", WheelchairLift, Accessibility),
261 M("wikidata", Image, Main),
262 M("wikimedia_commons", Image, Main),
263};
264static_assert(isSortedLookupTable(simple_key_map), "key map is not sorted!");
265
266static constexpr const KeyCategoryMapEntry localized_key_map[] = {
267 M("name", Name, Header),
268 M("loc_name", Name, Header),
269 M("species", Name, Header),
270 M("species:wikipedia", Wikipedia, UnresolvedCategory),
271 M("speech_output", SpeechOutput, Accessibility),
272 M("wikipedia", Wikipedia, UnresolvedCategory),
273};
274#undef M
275
276template <typename KeyMapEntry, std::size_t N>
277void OSMElementInformationModel::addEntryForKey(const char *keyName, const KeyMapEntry(&map)[N])
278{
279 const auto it = std::lower_bound(std::begin(map), std::end(map), keyName, [](const auto &lhs, auto rhs) {
280 return std::strcmp(lhs.keyName, rhs) < 0;
281 });
282 if (it != std::end(map) && std::strcmp((*it).keyName, keyName) == 0) {
283 m_infos.push_back(Info{(*it).key(), (*it).category()});
284 }
285}
286
287template <typename KeyMapEntry, std::size_t N>
288void OSMElementInformationModel::addEntryForLocalizedKey(const char *keyName, const KeyMapEntry(&map)[N])
289{
290 for (const auto &entry : map) {
291 const auto mapKeyLen = std::strlen(entry.keyName);
292 if (std::strncmp(keyName, entry.keyName, mapKeyLen) != 0) {
293 continue;
294 }
295 const auto keyNameLen = std::strlen(keyName);
296 if (keyNameLen == mapKeyLen || (keyNameLen == mapKeyLen + 3 && keyName[mapKeyLen] == ':')) {
297 m_infos.push_back(Info{entry.key(), entry.category()});
298 return;
299 }
300 }
301}
302
303void OSMElementInformationModel::reload()
304{
305 m_nameKey = NoKey;
306 m_categoryKey = NoKey;
307
308 const bool isRoom = m_element.tagValue("indoor") == "room";
309 for (auto it = m_element.tagsBegin(); it != m_element.tagsEnd(); ++it) {
310 addEntryForLocalizedKey((*it).key.name(), localized_key_map);
311 addEntryForKey((*it).key.name(), simple_key_map);
312 addEntryForKey((*it).key.name(), payment_generic_type_map);
313 addEntryForKey((*it).key.name(), payment_type_map);
314 addEntryForKey((*it).key.name(), diet_type_map);
315 addEntryForKey((*it).key.name(), socket_type_map);
316 addEntryForKey((*it).key.name(), authentication_type_map);
317 addEntryForLocalizedKey((*it).key.name(), tactile_writing_map);
318
319 if (isRoom && std::strcmp((*it).key.name(), "ref") == 0) {
320 m_infos.push_back(Info{Name, Header});
321 }
322 }
324 m_infos.emplace_back(Gender, UnresolvedCategory);
325 }
326
327 std::sort(m_infos.begin(), m_infos.end());
328 m_infos.erase(std::unique(m_infos.begin(), m_infos.end()), m_infos.end());
329 resolveOnlineContent();
330 resolveCategories();
331 resolveHeaders();
332
333 // if we don't have a primary group, promote a suitable secondary one
334 for (auto cat : {Parking, Toilets}) {
335 if (promoteMainCategory(cat)) {
336 break;
337 }
338 }
339
340 // resolve all remaining unresolved elements to the primary category
341 for (auto &info : m_infos) {
342 if (info.category == UnresolvedCategory) {
343 info.category = Main;
344 }
345 }
346 std::sort(m_infos.begin(), m_infos.end());
347 m_infos.erase(std::unique(m_infos.begin(), m_infos.end()), m_infos.end());
348
349 if (m_debug) {
350 m_infos.push_back(Info{ DebugLink, DebugCategory });
351 const auto count = std::distance(m_element.tagsBegin(), m_element.tagsEnd());
352 std::fill_n(std::back_inserter(m_infos), count, Info{ DebugKey, DebugCategory });
353 }
354}
355
356void OSMElementInformationModel::resolveOnlineContent()
357{
358 if (!m_allowOnlineContent) {
359 m_infos.erase(std::remove_if(m_infos.begin(), m_infos.end(), [](const auto &info) {
360 return info.key == Image || info.key == Logo;
361 }), m_infos.end());
362 return;
363 }
364
365 const auto commons = m_element.tagValue("wikimedia_commons");
366 const auto hasValidCommons = commons.startsWith("File:");
367 const auto image = m_element.tagValue("image");
368 const auto hasValidImage = image.contains("://commons.wikimedia.org/");
369 const auto wdId = m_element.tagValue("wikidata", "species:wikidata", "genus:wikidata", "subject:wikidata", "operator:wikidata", "network:wikidata", "brand:wikidata");
370
371 // query Wikidata content
372 if (!hasValidCommons && !hasValidImage && !wdId.isEmpty()) {
373 auto query = new Wikidata::EntitiesQuery(this);
374 query->setItems({Wikidata::Q{wdId}});
375 connect(query, &Wikidata::EntitiesQuery::finished, this, [query, this]() {
376 query->deleteLater();
377 auto res = query->takeResult();
378 for (const auto &item : res) {
379 std::vector<Wikidata::P> props({Wikidata::P::image, Wikidata::P::imageOfInterior, Wikidata::P::aerialView, Wikidata::P::view, Wikidata::P::modelImage});
380 // prefer night time view images at night
381 if (const auto now = QTime::currentTime(); now.hour() >= 22 || now.hour() <= 6) {
382 props.insert(props.begin(), Wikidata::P::nighttimeView);
383 } else {
384 props.emplace_back(Wikidata::P::nighttimeView);
385 }
386 // same for winter view
387 if (const auto today = QDate::currentDate(); today.month() > 10 || today.month() < 3) {
388 props.insert(props.begin(), Wikidata::P::winterView);
389 } else {
390 props.emplace_back(Wikidata::P::nighttimeView);
391 }
392 // prefer the logo for brand/operator/network
393 bool isLogo = false;
394 if (item.id() == Wikidata::Q(m_element.tagValue("operator:wikidata", "network:wikidata", "brand:wikidata"))) {
395 props = {Wikidata::P::logoImage};
396 isLogo = true;
397 } else {
398 props.emplace_back(Wikidata::P::logoImage);
399 }
400
401 for (const auto p : props) {
402 const auto img = item.value<QString>(p);
403 if (img.isEmpty()) {
404 continue;
405 }
406 m_wikidataImageMap.insert(item.id(), img);
407 const auto it = std::find_if(m_infos.begin(), m_infos.end(), [](const auto &info) { return info.key == Image; });
408 if (isLogo) {
409 (*it).key = Logo;
410 }
411 const auto idx = index((int)std::distance(m_infos.begin(), it), 0);
412 Q_EMIT dataChanged(idx, idx);
413 break;
414 }
415 }
416 });
417 m_wikidataMgr.execute(query);
418 }
419
420 if (!hasValidCommons && !hasValidImage && wdId.isEmpty()) {
421 m_infos.erase(std::remove_if(m_infos.begin(), m_infos.end(), [](const auto &info) { return info.key == Image; }), m_infos.end());
422 }
423}
424
425void OSMElementInformationModel::resolveCategories()
426{
427 if (m_infos.empty() || m_infos[0].category != UnresolvedCategory) {
428 return;
429 }
430 for (auto &info : m_infos) {
431 if (info.category != UnresolvedCategory) {
432 break;
433 }
434 switch (info.key) {
435 case Fee:
436 if (m_element.tagValue("parking:fee").isEmpty() && (!m_element.tagValue("parking").isEmpty()
437 || m_element.tagValue("amenity") == "parking" || m_element.tagValue("amenity") == "bicycle_parking"))
438 {
439 info.category = Parking;
440 } else if (m_element.tagValue("toilets:fee").isEmpty() && (m_element.tagValue("toilets") == "yes" || m_element.tagValue("amenity") == "toilets")) {
441 info.category = Toilets;
442 } else {
443 info.category = Main;
444 }
445 break;
446 case Capacity:
447 if (m_element.tagValue("amenity").endsWith("rental")) {
448 info.category = Main;
449 } else {
450 info.category = Parking;
451 }
452 break;
453 default:
454 {
455 // for anything else: if it's not clearly something we have a secondary group for, resolve it to Main
456 const auto amenity = m_element.tagValue("amenity");
457 if ((amenity != "parking" && amenity != "toilets")
458 || !m_element.tagValue("office").isEmpty()
459 || (!m_element.tagValue("room").isEmpty() && m_element.tagValue("room") != "toilets")
460 || !m_element.tagValue("shop").isEmpty()
461 || !m_element.tagValue("tourism").isEmpty()) {
462 info.category = Main;
463 }
464 break;
465 }
466 }
467 }
468 std::sort(m_infos.begin(), m_infos.end());
469}
470
471void OSMElementInformationModel::resolveHeaders()
472{
473 // find the best name candidate
474 for (auto key : { Name, VendingMachineOffer, Network, OperatorName, Category }) {
475 if (m_nameKey != NoKey) {
476 break;
477 }
478
479 const auto it = std::find_if(m_infos.begin(), m_infos.end(), [key](Info info) {
480 return info.key == key;
481 });
482 if (it == m_infos.end()) {
483 continue;
484 }
485
486 m_nameKey = (*it).key;
487 m_infos.erase(it);
488 break;
489 }
490
491 // same again for the category header
492 for (auto key : { VendingMachineOffer, Category }) {
493 const auto it = std::find_if(m_infos.begin(), m_infos.end(), [key](Info info) {
494 return info.key == key;
495 });
496 if (it == m_infos.end()) {
497 continue;
498 }
499
500 if (m_categoryKey == NoKey && m_nameKey != key) {
501 m_categoryKey = (*it).key;
502 }
503
504 m_infos.erase(it);
505 }
506}
507
508bool OSMElementInformationModel::promoteMainCategory(OSMElementInformationModel::KeyCategory cat)
509{
510 const auto hasMain = std::any_of(m_infos.begin(), m_infos.end(), [](const auto &info) {
511 return info.category == Main;
512 });
513
514 if (hasMain) {
515 return true;
516 }
517
518 bool didPromote = false;
519 for (auto &info : m_infos) {
520 if (info.category == cat) {
521 info.category = (info.key == Wheelchair ? Accessibility : Main);
522 didPromote = true;
523 }
524 }
525
526 if (didPromote) {
527 std::sort(m_infos.begin(), m_infos.end());
528 }
529 return didPromote;
530}
531
532QString OSMElementInformationModel::categoryLabel(OSMElementInformationModel::KeyCategory cat) const
533{
534 switch (cat) {
535 case UnresolvedCategory:
536 case Header:
537 case Main: return {};
538 case OpeningHoursCategory: return i18n("Opening Hours");
539 case Contact: return i18n("Contact");
540 case Payment: return i18n("Payment");
541 case Toilets: return i18n("Toilets");
542 case Accessibility: return i18n("Accessibility");
543 case Parking: return i18n("Parking");
544 case Operator: return i18n("Operator");
545 case DebugCategory: return QStringLiteral("Debug");
546 }
547 return {};
548}
549
550QString OSMElementInformationModel::debugTagKey(int row) const
551{
552 const auto tagCount = std::distance(m_element.tagsBegin(), m_element.tagsEnd());
553 const auto tagIdx = row - (rowCount() - tagCount);
554 return QString::fromUtf8((*(m_element.tagsBegin() + tagIdx)).key.name());
555}
556
557QString OSMElementInformationModel::debugTagValue(int row) const
558{
559 const auto tagCount = std::distance(m_element.tagsBegin(), m_element.tagsEnd());
560 const auto tagIdx = row - (rowCount() - tagCount);
561 return QString::fromUtf8((*(m_element.tagsBegin() + tagIdx)).value);
562}
563
564QUrl OSMElementInformationModel::debugTagUrl(int row) const
565{
566 const auto tagCount = std::distance(m_element.tagsBegin(), m_element.tagsEnd());
567 const auto tagIdx = row - (rowCount() - tagCount);
568 const auto key = QByteArrayView((*(m_element.tagsBegin() + tagIdx)).key.name());
569 const auto value = (*(m_element.tagsBegin() + tagIdx)).value;
570 if (key.endsWith(":wikipedia") || key == "wikipedia") {
571 return wikipediaUrl(value);
572 }
573 if (key.endsWith(":wikidata") || key == "wikidata") {
574 return QUrl(u"https://wikidata.org/wiki/" + QString::fromUtf8(value));
575 }
576 if (value.startsWith("http"_L1)) {
577 return QUrl(QString::fromUtf8(value));
578 }
579 return {};
580}
581
582QString OSMElementInformationModel::keyName(OSMElementInformationModel::Key key) const
583{
584 switch (key) {
585 case NoKey:
586 case Name:
587 case Category:
588 case VendingMachineOffer:
589 case Image:
590 case Logo:
591 return {};
592 case OldName: return i18n("Formerly");
593 case Description: return i18n("Description");
594 case Routes: return i18n("Routes");
595 case Cuisine: return i18n("Cuisine");
596 case Diet: return i18n("Diet");
597 case Takeaway: return i18n("Takeaway");
598 case Socket: return i18nc("electrical power socket", "Socket");
599 case OpeningHours: return {};
600 case AvailableVehicles: return i18n("Available vehicles");
601 case Fee: return i18n("Fee");
602 case Authentication: return i18n("Authentication");
603 case BicycleParking: return i18n("Bicycle parking");
604 case Capacity: return i18n("Capacity");
605 case CapacityDisabled: return i18n("Disabled parking spaces");
606 case CapacityWomen: return i18n("Women parking spaces");
607 case CapacityParent: return i18n("Parent parking spaces");
608 case CapacityCharing: return i18n("Parking spaces for charging");
609 case MaxStay: return i18n("Maximum stay");
610 case DiaperChangingTable: return i18n("Diaper changing table");
611 case Gender: return i18n("Gender");
612 case Wikipedia: return {};
613 case Address: return i18n("Address");
614 case Phone: return i18n("Phone");
615 case Email: return i18n("Email");
616 case Website: return i18n("Website");
617 case PaymentCash: return i18n("Cash");
618 case PaymentDigital: return i18n("Digital");
619 case PaymentDebitCard: return i18n("Debit cards");
620 case PaymentCreditCard: return i18n("Credit cards");
621 case PaymentStoredValueCard: return i18n("Stored value cards");
622 case Wheelchair: return i18n("Wheelchair access");
623 case WheelchairLift: return i18n("Wheelchair lift");
624 case CentralKey: return i18n("Central key");
625 case SpeechOutput: return i18n("Speech output");
626 case TactileWriting: return i18n("Tactile writing");
627 case OperatorName: return {};
628 case Network: return i18nc("transport network", "Network");
629 case OperatorWikipedia: return {};
630 case RemainingRange: return i18nc("remaining travel range of a battery powered vehicle", "Remaining range");
631 case DebugLink: return QStringLiteral("OSM");
632 case DebugKey: return {};
633 }
634 return {};
635}
636
637static void appendNonEmpty(const QByteArray &tagValue, QList<QByteArray> &l)
638{
639 if (tagValue.isEmpty()) {
640 return;
641 }
642 auto split = tagValue.split(';');
643 for (const auto &s : split) {
644 if (!s.isEmpty()) {
645 l.push_back(s.trimmed());
646 }
647 }
648}
649
650[[nodiscard]] static QChar::Script scriptForString(QStringView s)
651{
652 return std::accumulate(s.begin(), s.end(), QChar::Script_Unknown, [](QChar::Script script, QChar c) { return std::max(script, c.script());});
653}
654
655// why do we have two different script enums???
656// ### far from complete, this only handles the cases where int_name is in widespread use so far
657struct {
658 QLocale::Script localeScript;
659 QChar::Script charScript;
660} static constexpr const script_map[] = {
663};
664
665[[nodiscard]] static bool isSameScript(QLocale::Script ls, QChar::Script cs)
666{
667 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);
668}
669
670[[nodiscard]] static QUrl wikimediaCommondRedirect(const QString &fileName)
671{
672 if (fileName.isEmpty()) {
673 return {};
674 }
675
676 QUrl redirectUrl;
677 redirectUrl.setScheme(u"https"_s);
678 redirectUrl.setHost(u"commons.wikimedia.org"_s);
679 redirectUrl.setPath(u"/wiki/Special:Redirect/file"_s);
681 query.addQueryItem(u"wptype"_s, u"file"_s);
682 query.addQueryItem(u"wpvalue"_s, fileName);
683 query.addQueryItem(u"width"_s, u"512"_s);
684 redirectUrl.setQuery(query);
685 return redirectUrl;
686}
687
688QVariant OSMElementInformationModel::valueForKey(Info info) const
689{
690 switch (info.key) {
691 case NoKey: return {};
692 case Name: {
693 const auto n = QString::fromUtf8(m_element.tagValue(m_langs, "name", "loc_name", "int_name", "brand", "ref", "species", "genus"));
694 const auto script = scriptForString(n);
695 if (!isSameScript(QLocale().script(), script) && script > QChar::Script_Latin) {
696 const auto transliterated = QString::fromUtf8(m_element.tagValue(m_langs, "int_name"));
697 if (transliterated.isEmpty() || transliterated == n) {
698 return n;
699 }
700 return i18nc("local name (transliterated name)", "%1 (%2)", n, transliterated);
701 }
702 return n;
703 }
704 case Category:
705 {
707 appendNonEmpty(m_element.tagValue("amenity"), l);
708 appendNonEmpty(m_element.tagValue("shop"), l);
709 appendNonEmpty(m_element.tagValue("tourism"), l);
710 // appendNonEmpty(m_element.tagValue("vending"), l);
711 const auto diplomatic = m_element.tagValue("diplomatic");
712 appendNonEmpty(diplomatic, l);
713 if (diplomatic.isEmpty()) {
714 appendNonEmpty(m_element.tagValue("office"), l);
715 }
716 appendNonEmpty(m_element.tagValue("leisure"), l);
717 appendNonEmpty(m_element.tagValue("historic"), l);
718 appendNonEmpty(m_element.tagValue("mx:vehicle"), l);
719 if (l.isEmpty()) {
720 appendNonEmpty(m_element.tagValue("room"), l);
721 }
722
723 QStringList out;
724 out.reserve(l.size());
725
726 // TODO drop general categories if specific ones are available (e.g. restaurant vs fast_food)
727
728 for (auto it = l.begin(); it != l.end();++it) {
729 if ((*it).isEmpty() || (*it) == "yes" || (*it) == "no" || (*it) == "building") {
730 continue;
731 }
732 out.push_back(Localization::amenityType((*it).constData()));
733 }
734
735 if (out.isEmpty()) { // fall back to building, but only take terms we have translated
736 appendNonEmpty(m_element.tagValue("building"), l);
737 for (const auto &key : l) {
738 auto s = Localization::amenityType(key.constData(), Localization::ReturnEmptyOnUnknownKey);
739 if (!s.isEmpty()) {
740 out.push_back(std::move(s));
741 }
742 }
743 }
744
745 std::sort(out.begin(), out.end());
746 out.erase(std::unique(out.begin(), out.end()), out.end());
747 return QLocale().createSeparatedList(out);
748 }
749 case Image:
750 case Logo:
751 {
752 const auto commons = m_element.tagValue("wikimedia_commons");
753 if (commons.startsWith("File:")) {
754 return wikimediaCommondRedirect(QString::fromUtf8(QByteArrayView(commons).mid(5)));
755 }
756 const QUrl url(QString::fromUtf8(m_element.tagValue("image")));
757 if (url.host() == "commons.wikimedia.org"_L1) {
758 return wikimediaCommondRedirect(url.fileName());
759 }
760 const auto wdId = m_element.tagValue("wikidata", "species:wikidata", "genus:wikidata", "subject:wikidata", "operator:wikidata", "network:wikidata", "brand:wikidata");
761 return wikimediaCommondRedirect(m_wikidataImageMap.value(Wikidata::Q{wdId}));
762 }
763 case OldName:
764 {
765 const auto l = QString::fromUtf8(m_element.tagValue("old_name")).split(QLatin1Char(';'));
766 return l.join(QLatin1String(", "));
767 }
768 case Description:
769 return m_element.tagValue(m_langs, "description", "memorial:text");
770 case Routes:
771 {
772 auto l = QString::fromUtf8(m_element.tagValue("route_ref", "bus_routes", "bus_lines", "buses")).split(QLatin1Char(';'), Qt::SkipEmptyParts);
773 for (auto &s : l) {
774 s = s.trimmed();
775 }
776 return QLocale().createSeparatedList(l);
777 }
778 case Cuisine: return Localization::cuisineTypes(m_element.tagValue("cuisine"));
779 case VendingMachineOffer:
780 return Localization::amenityTypes(m_element.tagValue("vending"), Localization::ReturnEmptyOnUnknownKey);
781 case Diet:
782 {
783 QStringList l;
784 for (const auto &d : diet_type_map) {
785 const auto v = m_element.tagValue(d.keyName);
786 const auto label = d.label.toString();
787 if (v == "yes") {
788 l.push_back(label);
789 } else if (v == "only") {
790 l.push_back(i18n("only %1", label));
791 } else if (v == "no") {
792 l.push_back(i18n("no %1", label));
793 }
794 }
795 return l.join(QLatin1String(", "));
796 }
797 case Takeaway: return translatedBoolValue(m_element.tagValue("takeaway")); // TODO decode (yes/only/no) and translate
798 case Socket:
799 {
800 QStringList l;
801 for (const auto &socket : socket_type_map) {
802 const auto value = m_element.tagValue(socket.keyName);
803 if (value.isEmpty() || value == "no") {
804 continue;
805 }
806
807 auto s = socket.label.toString();
808
809 QStringList details;
810 if (value != "yes") {
811 details.push_back(QString::fromUtf8(value));
812 }
813
814 const auto current = m_element.tagValue(QByteArray(socket.keyName + QByteArray(":current")).constData());
815 if (!current.isEmpty()) {
816 if (std::all_of(current.begin(), current.end(), [](unsigned char c) { return std::isdigit(c); })) {
817 details.push_back(i18nc("electrical current/Ampere value", "%1 A", QString::fromUtf8(current)));
818 } else {
819 details.push_back(QString::fromUtf8(current));
820 }
821 }
822 const auto output = m_element.tagValue(QByteArray(socket.keyName + QByteArray(":output")).constData());
823 if (!output.isEmpty()) {
824 if (std::all_of(output.begin(), output.end(), [](unsigned char c) { return std::isdigit(c); })) {
825 details.push_back(i18nc("electrical power/kilowatt value", "%1 kW", QString::fromUtf8(output)));
826 } else {
827 details.push_back(QString::fromUtf8(output));
828 }
829 }
830
831 if (!details.empty()) {
832 s += QLatin1String(" (") + details.join(QLatin1String(", ")) + QLatin1Char(')');
833 }
834 l.push_back(s);
835 }
836 return QLocale().createSeparatedList(l);
837 }
838 case OpeningHours: return QString::fromUtf8(m_element.tagValue("opening_hours"));
839 case AvailableVehicles:
840 {
841 const auto total = m_element.tagValue("mx:realtime_available").toInt();
842 QStringList types;
843 for (const auto &v : available_vehicles_map) {
844 const auto b = m_element.tagValue(v.keyName);
845 if (b.isEmpty()) {
846 continue;
847 }
848 types.push_back(v.label.subs(b.toInt()).toString());
849 }
850
851 if (types.isEmpty()) {
852 return QLocale().toString(total);
853 } else if (types.size() == 1) {
854 return types.at(0);
855 } else {
856 return i18n("%1 (%2)", total, QLocale().createSeparatedList(types));
857 }
858 }
859 case Fee:
860 {
861 QByteArray fee;
862 switch (info.category) {
863 case Parking: fee = m_element.tagValue("parking:fee", "fee"); break;
864 case Toilets: fee = m_element.tagValue("toilets:fee", "fee"); break;
865 default: fee = m_element.tagValue("fee");
866 }
867 auto s = QString::fromUtf8(fee);
868 const auto charge = QString::fromUtf8(m_element.tagValue("charge"));
869 if (s.isEmpty()) {
870 return charge;
871 }
872 if (!charge.isEmpty()) {
873 s += QLatin1String(" (") + charge + QLatin1Char(')');
874 }
875 return s;
876 }
877 case Authentication:
878 {
879 QStringList l;
880 for (const auto &auth : authentication_type_map) {
881 const auto v = m_element.tagValue(auth.keyName);
882 if (v.isEmpty() || v == "no") {
883 continue;
884 }
885 l.push_back(auth.label.toString());
886 }
887 return QLocale().createSeparatedList(l);
888 }
889 case BicycleParking: return translateValues(m_element.tagValue("bicycle_parking"), bicycle_parking_map);
890 case Capacity: return QString::fromUtf8(m_element.tagValue("capacity"));
891 case CapacityDisabled: return capacitryValue("capacity:disabled");
892 case CapacityWomen: return capacitryValue("capacity:women");
893 case CapacityParent: return capacitryValue("capacity:parent");
894 case CapacityCharing: return capacitryValue("capacity:charging");
895 case MaxStay: return QString::fromUtf8(m_element.tagValue("maxstay"));
896 case DiaperChangingTable:
897 // TODO look for changing_table:location too
898 return translatedBoolValue(m_element.tagValue("changing_table", "diaper"));
899 case Gender:
900 return Localization::genderSegregation(m_element);
901 case Wikipedia: return wikipediaUrl(m_element.tagValue(m_langs, "wikipedia", "brand:wikipedia", "species:wikipedia"));
902 case Address: return QVariant::fromValue(OSMAddress(m_element));
903 case Phone: return QString::fromUtf8(m_element.tagValue("contact:phone", "phone", "telephone", "operator:phone"));
904 case Email: return QString::fromUtf8(m_element.tagValue("contact:email", "email", "operator:email"));
905 case Website: return QString::fromUtf8(m_element.tagValue("website", "contact:website", "url", "operator:website"));
906 case PaymentCash:
907 {
908 // TODO deal with the case that the tag value is a list of supported coins/notes (e.g. on vending machines)
909 const auto coins = m_element.tagValue("payment:coins");
910 const auto notes = m_element.tagValue("payment:notes");
911 if (coins.isEmpty() && notes.isEmpty()) {
912 return translatedBoolValue(m_element.tagValue("payment:cash"));
913 }
914 if (!coins.isEmpty() && !notes.isEmpty() && coins != "no" && notes != "no") {
915 return i18n("yes");
916 }
917 if (!coins.isEmpty() && coins != "no" && (notes == "no" || notes.isEmpty())) {
918 return i18nc("payment option", "coins only");
919 }
920 if (!notes.isEmpty() && notes != "no" && (coins == "no" || coins.isEmpty())) {
921 return i18nc("payment option", "notes only");
922 }
923 return i18n("no");
924 }
925 case PaymentDigital:
926 case PaymentDebitCard:
927 case PaymentCreditCard:
928 case PaymentStoredValueCard:
929 return paymentMethodValue(info.key);
930 case Wheelchair:
931 {
932 QByteArray wheelchair;
933 if (info.category == Toilets) {
934 wheelchair = m_element.tagValue("toilets:wheelchair", "wheelchair");
935 } else {
936 wheelchair = m_element.tagValue("wheelchair");
937 }
938 const auto a = translateValue(wheelchair.constData(), wheelchair_map);
939 const auto d = QString::fromUtf8(m_element.tagValue(m_langs, "wheelchair:description"));
940 if (!d.isEmpty()) {
941 return QString(a + QLatin1String(" (") + d + QLatin1Char(')'));
942 }
943 return a;
944 }
945 case WheelchairLift:
946 return translatedBoolValue(m_element.tagValue("wheelchair:lift"));
947 case CentralKey:
948 // translate enum values
949 return QString::fromUtf8(m_element.tagValue("centralkey"));
950 case SpeechOutput:
951 // TODO: rather than as a boolean value, list the available languages here when we have that information
952 return translatedBoolValue(m_element.tagValue(m_langs, "speech_output"));
953 case TactileWriting:
954 {
955 // TODO: rather than as a boolean value, list the available languages here when we have that information
956 QStringList l;
957 bool explicitNo = false;
958 for (const auto &writing : tactile_writing_map) {
959 const auto v = m_element.tagValue(m_langs, writing.keyName);
960 if (v.isEmpty()) {
961 continue;
962 }
963 if (v == "no") {
964 explicitNo = true;
965 continue;
966 }
967 l.push_back(writing.label.toString());
968 }
969 if (!l.isEmpty()) {
970 return QLocale().createSeparatedList(l);
971 }
972 const auto v = m_element.tagValue(m_langs, "tactile_writing");
973 if (explicitNo && v.isEmpty()) {
974 return i18n("no");
975 }
976 return translatedBoolValue(v);
977 }
978 case OperatorName: return QString::fromUtf8(m_element.tagValue("operator"));
979 case Network: return QString::fromUtf8(m_element.tagValue("network"));
980 case OperatorWikipedia: return wikipediaUrl(m_element.tagValue(m_langs, "operator:wikipedia", "network:wikipedia"));
981 case RemainingRange:
982 {
983 const auto range = m_element.tagValue("mx:remaining_range").toInt();
984 return formatDistance(range);
985 }
986 case DebugLink: return m_element.url();
987 case DebugKey: return {};
988 }
989 return {};
990}
991
992QVariant OSMElementInformationModel::urlify(const QVariant& v, OSMElementInformationModel::Key key) const
993{
994 if (v.userType() != QMetaType::QString && key != Image && key != Logo) {
995 return v;
996 }
997 const auto s = v.toString();
998
999 switch (key) {
1000 case Image:
1001 case Logo:
1002 {
1003 if (const auto commons = m_element.tagValue("wikimedia_commons"); commons.startsWith("File:")) {
1004 return QUrl(u"https://commons.wikimedia.org/wiki/"_s + QString::fromUtf8(commons));
1005 }
1006 if (const QUrl url(QString::fromUtf8(m_element.tagValue("image"))); url.host() == "commons.wikimedia.org"_L1) {
1007 return wikimediaCommondRedirect(url.fileName());
1008 }
1009 if (const auto wdId = m_element.tagValue("wikidata", "species:wikidata", "genus:wikidata", "subject:wikidata", "operator:wikidata", "network:wikidata", "brand:wikidata"); !wdId.isEmpty()) {
1010 return QUrl(u"https://wikidata.org/wiki/" + QString::fromUtf8(wdId));
1011 }
1012 return {};
1013 }
1014 case Email:
1015 if (!s.startsWith(QLatin1String("mailto:"))) {
1016 return QString(QLatin1String("mailto:") + s);
1017 }
1018 return s;
1019 case Phone:
1020 {
1021 if (s.startsWith(QLatin1String("tel:"))) {
1022 return s;
1023 }
1024 QString e = QLatin1String("tel:") + s;
1025 e.remove(QLatin1Char(' '));
1026 return e;
1027 }
1028 case Website:
1029 case DebugLink:
1030 if (s.startsWith(QLatin1String("http"))) {
1031 return s;
1032 }
1033 return QString(QLatin1String("https://") + s);
1034 default:
1035 return {};
1036 }
1037
1038 return {};
1039}
1040
1041QString OSMElementInformationModel::paymentMethodList(OSMElementInformationModel::Key key) const
1042{
1043 QStringList l;
1044 for (const auto &payment : payment_type_map) {
1045 if (payment.key() != key) {
1046 continue;
1047 }
1048 if (m_element.tagValue(payment.keyName) == "yes") {
1049 l.push_back(payment.label.toString());
1050 }
1051 }
1052 std::sort(l.begin(), l.end());
1053 return QLocale().createSeparatedList(l);
1054}
1055
1056QString OSMElementInformationModel::paymentMethodValue(OSMElementInformationModel::Key key) const
1057{
1058 const auto s = paymentMethodList(key);
1059 if (!s.isEmpty()) {
1060 return s;
1061 }
1062
1063 for (const auto &payment : payment_generic_type_map) {
1064 if (payment.key() != key) {
1065 continue;
1066 }
1067 const auto s = m_element.tagValue(payment.keyName);
1068 if (!s.isEmpty()) {
1069 return QString::fromUtf8(s);
1070 }
1071 }
1072 return {};
1073}
1074
1075QUrl OSMElementInformationModel::wikipediaUrl(const QByteArray &wp) const
1076{
1077 if (wp.isEmpty()) {
1078 return {};
1079 }
1080
1081 const auto s = QString::fromUtf8(wp);
1082 const auto idx = s.indexOf(QLatin1Char(':'));
1083 if (idx < 0) {
1084 return {};
1085 }
1086
1087 QUrl url;
1088 url.setScheme(QStringLiteral("https"));
1089 url.setHost(QStringView(s).left(idx) + QLatin1String(".wikipedia.org"));
1090 url.setPath(QLatin1String("/wiki/") + QStringView(s).mid(idx + 1));
1091 return url;
1092}
1093
1094QString OSMElementInformationModel::capacitryValue(const char *prop) const
1095{
1096 const auto v = m_element.tagValue(prop);
1097 return translatedBoolValue(v);
1098}
1099
1100QString OSMElementInformationModel::translatedBoolValue(const QByteArray &value) const
1101{
1102 if (value == "yes") {
1103 return i18n("yes");
1104 }
1105 if (value == "no") {
1106 return i18n("no");
1107 }
1108 return QString::fromUtf8(value);
1109}
1110
1111#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:21
Wikidata multi-entity retrieval query.
Wikidata item identifier.
Definition entities.h:75
void execute(Query *query)
Execute query.
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
char * toString(const EngineQuery &query)
KSERVICE_EXPORT KService::List query(FilterFunc filterFunc)
QString cuisineTypes(const QByteArray &value, Localization::TranslationOption opt=Localization::ReturnUnknownKey)
Translated values of the cuisine tag (does list splitting).
QString genderSegregation(OSM::Element element)
Translated gender segregation information e.g.
bool hasGenderSegregrationKey(OSM::Element element)
Checks whether element contains a known key for gender segregation information.
QString amenityTypes(const QByteArray &value, Localization::TranslationOption opt=Localization::ReturnUnknownKey)
Translated list of amenity tag values (including 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:417
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList< int > &roles)
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 contains(QByteArrayView bv) const const
bool endsWith(QByteArrayView bv) const const
bool isEmpty() const const
QList< QByteArray > split(char sep) const const
bool startsWith(QByteArrayView bv) const const
int toInt(bool *ok, int base) const const
QDate currentDate()
int month() const const
iterator insert(const Key &key, const T &value)
T value(const Key &key) 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)
iterator insert(const_iterator before, parameter_type value)
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
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QObject * parent() const const
QString fromUtf8(QByteArrayView str)
bool isEmpty() const const
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)
QTime currentTime()
int hour() const const
bool isValid() const const
void setHost(const QString &host, ParsingMode mode)
void setPath(const QString &path, ParsingMode mode)
void setQuery(const QString &query, 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 Fri Jul 26 2024 11:57:46 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.