KPublicTransport

location.cpp
1/*
2 SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "location.h"
8
9#include "datatypes_p.h"
10#include "equipment.h"
11#include "equipmentutil.h"
12#include "identifier_p.h"
13#include "json_p.h"
14#include "mergeutil_p.h"
15#include "rentalvehicle.h"
16#include "rentalvehicleutil_p.h"
17#include "ifopt/ifoptutil.h"
18
19#include <KTimeZone>
20#include <KCountry>
21#include <KCountrySubdivision>
22
23#include <QDebug>
24#include <QHash>
25#include <QJsonArray>
26#include <QJsonObject>
27#include <QRegularExpression>
28#include <QTimeZone>
29
30#include <cmath>
31
32using namespace KPublicTransport;
33using namespace Qt::Literals;
34
35namespace KPublicTransport {
36
37class LocationPrivate : public QSharedData
38{
39public:
41 QString name;
42 double latitude = NAN;
43 double longitude = NAN;
44 QTimeZone timeZone;
45 IdentifierSet ids;
46
47 QString streetAddress;
48 QString postalCode;
49 QString locality;
50 QString region;
51 QString country;
52
53 int floorLevel = std::numeric_limits<int>::lowest();
54
55 QVariant data;
56};
57
58}
59
60KPUBLICTRANSPORT_MAKE_GADGET(Location)
61KPUBLICTRANSPORT_MAKE_PROPERTY(Location, Location::Type, type, setType)
62KPUBLICTRANSPORT_MAKE_PROPERTY(Location, QString, name, setName)
63KPUBLICTRANSPORT_MAKE_PROPERTY(Location, double, latitude, setLatitude)
64KPUBLICTRANSPORT_MAKE_PROPERTY(Location, double, longitude, setLongitude)
65KPUBLICTRANSPORT_MAKE_PROPERTY(Location, QString, streetAddress, setStreetAddress)
66KPUBLICTRANSPORT_MAKE_PROPERTY(Location, QString, postalCode, setPostalCode)
67KPUBLICTRANSPORT_MAKE_PROPERTY(Location, QString, locality, setLocality)
68KPUBLICTRANSPORT_MAKE_PROPERTY(Location, int, floorLevel, setFloorLevel)
69
70void Location::setRegion(const QString &regionCode)
71{
72 d.detach();
73 d->region = regionCode;
74}
75
76QString Location::region() const
77{
78 if (d->region.isEmpty() && hasCoordinate()) {
79 auto subdivision = KCountrySubdivision::fromLocation((float)latitude(), (float)longitude());
80 const_cast<Location *>(this)->setRegion(subdivision.code());
81 }
82
83 return d->region;
84}
85
86void Location::setCountry(const QString &countryCode)
87{
88 d.detach();
89 d->country = countryCode;
90}
91
92QString Location::country() const
93{
94 if (d->country.isEmpty() && hasCoordinate()) {
95 auto country = KCountry::fromLocation((float)latitude(), (float)longitude());
96 const_cast<Location *>(this)->setCountry(country.alpha2());
97 }
98
99 return d->country;
100}
101
102KPUBLICTRANSPORT_MAKE_PROPERTY(Location, QVariant, data, setData)
103
104void Location::setCoordinate(double latitude, double longitude)
105{
106 d.detach();
107 d->latitude = latitude;
108 d->longitude = longitude;
109}
110
111bool Location::hasCoordinate() const
112{
113 return !std::isnan(d->latitude) && !std::isnan(d->longitude) && std::abs(d->latitude) <= 90.0 && std::abs(d->longitude) <= 180.0;
114}
115
116bool Location::hasFloorLevel() const
117{
118 return d->floorLevel > std::numeric_limits<int>::lowest() && d->floorLevel < std::numeric_limits<int>::max();
119}
120
122{
123 return !hasCoordinate() && d->name.isEmpty() && d->ids.isEmpty() && d->streetAddress.isEmpty();
124}
125
127{
128 if (d->timeZone.isValid()) {
129 return d->timeZone;
130 }
131 if (hasCoordinate()) {
132 if (const auto tzId = KTimeZone::fromLocation((float)latitude(), (float)longitude()); tzId) {
133 return QTimeZone(tzId);
134 }
135 }
136 return {};
137}
138
139void Location::setTimeZone(const QTimeZone &tz)
140{
141 d.detach();
142 d->timeZone = tz;
143}
144
146{
147 return d->ids.identifier(identifierType);
148}
149
150void Location::setIdentifier(const QString &identifierType, const QString &id)
151{
152 d.detach();
153 d->ids.setIdentifier(identifierType, id);
154}
155
156bool Location::hasIdentifier(QAnyStringView identifierType) const
157{
158 return d->ids.hasIdentifier(identifierType);
159}
160
161QStringList Location::identifierTypes() const
162{
163 return d->ids.identifierTypes();
164}
165
167{
168 return d->data.value<RentalVehicleStation>();
169}
170
172{
173 return d->data.value<RentalVehicle>();
174}
175
176KPublicTransport::Equipment Location::equipment() const
177{
178 return d->data.value<KPublicTransport::Equipment>();
179}
180
181// keep this sorted by key
182struct {
183 const char *key;
184 const char *value;
185} static const name_normalization_map[] = {
186 { "bahnhof", nullptr },
187 { "bhf", nullptr },
188 { "centraal", "central" },
189 { "cs", "central" },
190 { "de", nullptr },
191 { "flughafen", "airport" },
192 { "gare", nullptr },
193 { "hbf", "hauptbahnhof" },
194 { "rer", nullptr },
195 { "st", "saint" },
196 { "str", "strasse" },
197};
198
199static QStringList splitAndNormalizeName(const QString &name)
200{
201 static const QRegularExpression splitRegExp(uR"([, \‍(\)-/\.\[\]])"_s);
202 auto l = name.split(splitRegExp, Qt::SkipEmptyParts);
203
204 for (auto it = l.begin(); it != l.end();) {
205 // ignore single-letter fragments, with the exception of the 'H' used in Denmark
206 // this seem to be mostly transport mode abbreviations (such as 'S' and 'U' in Germany)
207 if ((*it).size() == 1) {
208 it = l.erase(it);
209 continue;
210 }
211
212 *it = (*it).toCaseFolded();
213 const auto b = (*it).toUtf8();
214 const auto entry = std::lower_bound(std::begin(name_normalization_map), std::end(name_normalization_map), b.constData(), [](const auto &lhs, const auto rhs) {
215 return strcmp(lhs.key, rhs) < 0;
216 });
217 if (entry != std::end(name_normalization_map) && strcmp((*entry).key, b.constData()) == 0) {
218 if (!(*entry).value) {
219 it = l.erase(it);
220 continue;
221 }
222 *it = QString::fromUtf8((*entry).value);
223 }
224 ++it;
225 }
226
227 l.removeDuplicates();
228 std::sort(l.begin(), l.end());
229 return l;
230}
231
232static QString stripDiacritics(const QString &s)
233{
234 QString res;
235 res.reserve(s.size());
236
237 // if the character has a canonical decomposition use that and skip the combining diacritic markers following it
238 // see https://en.wikipedia.org/wiki/Unicode_equivalence
239 // see https://en.wikipedia.org/wiki/Combining_character
240 for (const auto &c : s) {
241 if (c.decompositionTag() == QChar::Canonical) {
242 res.push_back(c.decomposition().at(0));
243 } else {
244 res.push_back(c);
245 }
246 }
247
248 return res;
249}
250
251// keep this ordered (see https://en.wikipedia.org/wiki/List_of_Unicode_characters)
252struct {
253 ushort key;
254 const char* replacement;
255} static const transliteration_map[] = {
256 { 0x00DF, "ss" }, // ß
257 { 0x00E4, "ae" }, // ä
258 { 0x00F6, "oe" }, // ö
259 { 0x00F8, "oe" }, // ø
260 { 0x00FC, "ue" }, // ü
261};
262
263static QString applyTransliterations(const QString &s)
264{
265 QString res;
266 res.reserve(s.size());
267
268 for (const auto c : s) {
269 const auto it = std::lower_bound(std::begin(transliteration_map), std::end(transliteration_map), c, [](const auto &lhs, const auto rhs) {
270 return QChar(lhs.key) < rhs;
271 });
272 if (it != std::end(transliteration_map) && QChar((*it).key) == c) {
273 res += QString::fromUtf8((*it).replacement);
274 continue;
275 }
276
277 if (c.decompositionTag() == QChar::Canonical) { // see above
278 res += c.decomposition().at(0);
279 } else {
280 res += c;
281 }
282 }
283
284 return res;
285}
286
287static bool isCompatibleLocationType(Location::Type lhs, Location::Type rhs)
288{
289 return lhs == rhs
290 || (lhs == Location::Place && rhs == Location::Stop)
291 || (rhs == Location::Place && lhs == Location::Stop);
292}
293
294static int isSameDistanceThreshold(Location::Type type)
295{
296 switch (type) {
297 case Location::Place:
298 case Location::Stop:
300 return 25; // meter
302 return 10;
304 return 5;
307 return 3;
308 }
309 Q_UNREACHABLE();
310}
311
312bool Location::isSame(const Location &lhs, const Location &rhs)
313{
314 const auto dist = Location::distance(lhs.latitude(), lhs.longitude(), rhs.latitude(), rhs.longitude());
315 // further than 1km apart is certainly not the same
316 if (lhs.hasCoordinate() && rhs.hasCoordinate() && dist > 1000) {
317 return false;
318 }
319 // incompatible types are also unmergable
320 if (!isCompatibleLocationType(lhs.type(), rhs.type())) {
321 return false;
322 }
323
324 // ids - IFOPT takes priority here due to its special hierarchical handling, but only for stations
325 const auto lhsIfopt = lhs.identifier(IfoptUtil::identifierType());
326 const auto rhsIfopt = rhs.identifier(IfoptUtil::identifierType());
327 if (!lhsIfopt.isEmpty() && !rhsIfopt.isEmpty() && (lhs.type() == Location::Stop || rhs.type() == Location::Stop)) {
328 return IfoptUtil::isSameStopPlace(lhsIfopt, rhsIfopt);
329 }
330
331 switch (lhs.d->ids.compare(rhs.d->ids)) {
332 case IdentifierSet::NotEqual: return false;
333 case IdentifierSet::Equal: return true;
334 case IdentifierSet::NoIntersection: break;
335 }
336
339 return false;
340 }
341 if (lhs.type() == Location::Equipment && lhs.equipment().type() != rhs.equipment().type()) {
342 return false;
343 }
344
345 // name
346 if (isSameName(lhs.name(), rhs.name())) {
347 return true;
348 }
349
350 // TODO consider the address properties here?
351
352 // anything sufficiently close together is assumed to be the same
353 if (lhs.hasCoordinate() && rhs.hasCoordinate() && dist < std::min(isSameDistanceThreshold(lhs.type()), isSameDistanceThreshold(rhs.type()))) {
354 return true;
355 }
356
357 return false;
358}
359
360bool Location::isSameName(const QString &lhs, const QString &rhs)
361{
362 // simple prefix test, before we do the expensive fragment-based comparison below
363 if (lhs.startsWith(rhs, Qt::CaseInsensitive) || rhs.startsWith(lhs, Qt::CaseSensitive)) {
364 return true;
365 }
366
367 const auto lhsNameFragments = splitAndNormalizeName(lhs);
368 const auto rhsNameFragments = splitAndNormalizeName(rhs);
369
370 // first try with stripping diacritics
371 std::vector<QString> lhsNormalized;
372 lhsNormalized.reserve(lhsNameFragments.size());
373 std::transform(lhsNameFragments.begin(), lhsNameFragments.end(), std::back_inserter(lhsNormalized), stripDiacritics);
374 std::sort(lhsNormalized.begin(), lhsNormalized.end());
375 lhsNormalized.erase(std::unique(lhsNormalized.begin(), lhsNormalized.end()), lhsNormalized.end());
376
377 std::vector<QString> rhsNormalized;
378 rhsNormalized.reserve(rhsNameFragments.size());
379 std::transform(rhsNameFragments.begin(), rhsNameFragments.end(), std::back_inserter(rhsNormalized), stripDiacritics);
380 std::sort(rhsNormalized.begin(), rhsNormalized.end());
381 rhsNormalized.erase(std::unique(rhsNormalized.begin(), rhsNormalized.end()), rhsNormalized.end());
382
383 if (lhsNormalized == rhsNormalized) {
384 return true;
385 }
386
387 // if that didn't help, try to apply alternative transliterations of diacritics
388 lhsNormalized.clear();
389 std::transform(lhsNameFragments.begin(), lhsNameFragments.end(), std::back_inserter(lhsNormalized), applyTransliterations);
390 rhsNormalized.clear();
391 std::transform(rhsNameFragments.begin(), rhsNameFragments.end(), std::back_inserter(rhsNormalized), applyTransliterations);
392 return lhsNormalized == rhsNormalized;
393}
394
395static double mergeCoordinate(double lhs, double rhs)
396{
397 if (std::isnan(lhs)) {
398 return rhs;
399 }
400 if (std::isnan(rhs)) {
401 return lhs;
402 }
403
404 return (lhs + rhs) / 2.0;
405}
406
408{
409 Location l(lhs);
410 l.setType(std::max(lhs.type(), rhs.type()));
411
412 // merge identifiers
413 l.d->ids.merge(rhs.d->ids);
414 if (const auto ifoptId = IfoptUtil::merge(lhs.identifier(IfoptUtil::identifierType()), rhs.identifier(IfoptUtil::identifierType())); !ifoptId.isEmpty()) {
415 l.setIdentifier(IfoptUtil::identifierType(), ifoptId.toString());
416 }
417
418 if (!lhs.hasCoordinate()) {
419 l.setCoordinate(rhs.latitude(), rhs.longitude());
420 }
421
422 l.setName(MergeUtil::mergeString(lhs.name(), rhs.name()));
423
424 if (!lhs.d->timeZone.isValid()) {
425 l.setTimeZone(rhs.d->timeZone);
426 }
427
428 l.setLatitude(mergeCoordinate(lhs.latitude(), rhs.latitude()));
429 l.setLongitude(mergeCoordinate(lhs.longitude(), rhs.longitude()));
430
431 l.setStreetAddress(MergeUtil::mergeString(lhs.streetAddress(), rhs.streetAddress()));
432 l.setPostalCode(MergeUtil::mergeString(lhs.postalCode(), rhs.postalCode()));
433 l.setLocality(MergeUtil::mergeString(lhs.locality(), rhs.locality()));
434 l.setRegion(MergeUtil::mergeString(lhs.region(), rhs.region()));
435 l.setCountry(MergeUtil::mergeString(lhs.country(), rhs.country()));
436
437 switch (l.type()) {
438 case Place:
440 case Stop:
441 case Address:
442 break;
444 l.setData(RentalVehicleUtil::merge(lhs.rentalVehicleStation(), rhs.rentalVehicleStation()));
445 break;
446 case RentedVehicle:
447 l.setData(RentalVehicleUtil::merge(lhs.rentalVehicle(), rhs.rentalVehicle()));
448 break;
449 case Equipment:
450 l.setData(EquipmentUtil::merge(lhs.equipment(), rhs.equipment()));
451 break;
452 }
453
454 return l;
455}
456
457// see https://en.wikipedia.org/wiki/Haversine_formula
458double Location::distance(double lat1, double lon1, double lat2, double lon2)
459{
460 const auto degToRad = M_PI / 180.0;
461 const auto earthRadius = 6371000.0; // in meters
462
463 const auto d_lat = (lat1 - lat2) * degToRad;
464 const auto d_lon = (lon1 - lon2) * degToRad;
465
466 const auto a = pow(sin(d_lat / 2.0), 2) + cos(lat1 * degToRad) * cos(lat2 * degToRad) * pow(sin(d_lon / 2.0), 2);
467 return 2.0 * earthRadius * atan2(sqrt(a), sqrt(1.0 - a));
468}
469
470double Location::distance(const Location &lhs, const Location &rhs)
471{
472 if (!lhs.hasCoordinate() || !rhs.hasCoordinate()) {
473 return NAN;
474 }
475 return Location::distance(lhs.latitude(), lhs.longitude(), rhs.latitude(), rhs.longitude());
476}
477
479{
480 auto obj = Json::toJson(loc);
481 if (loc.d->timeZone.isValid()) {
482 obj.insert("timezone"_L1, QString::fromUtf8(loc.d->timeZone.id()));
483 }
484 if (!loc.hasFloorLevel()) {
485 obj.remove("floorLevel"_L1);
486 }
487
488 if (!loc.d->ids.isEmpty()) {
489 obj.insert("identifier"_L1, loc.d->ids.toJson());
490 }
491
492 switch (loc.type()) {
493 case Place:
494 obj.remove("type"_L1);
495 [[fallthrough]];
496 case Address:
497 case Stop:
499 break;
501 obj.insert("rentalVehicleStation"_L1, RentalVehicleStation::toJson(loc.rentalVehicleStation()));
502 break;
503 case RentedVehicle:
504 obj.insert("rentalVehicle"_L1, RentalVehicle::toJson(loc.rentalVehicle()));
505 break;
506 case Equipment:
507 obj.insert("equipment"_L1, Equipment::toJson(loc.equipment()));
508 break;
509 }
510
511 return obj;
512}
513
514QJsonArray Location::toJson(const std::vector<Location> &locs)
515{
516 return Json::toJson(locs);
517}
518
520{
521 switch (d->type) {
522 case Location::Stop:
523 return u"qrc:///org.kde.kpublictransport/assets/images/transport-stop.svg"_s;
529 return equipment().iconName();
531 return u"qrc:///org.kde.kpublictransport/assets/images/transport-mode-car.svg"_s;
533 case Location::Place:
534 break;
535 }
536 return u"map-symbolic"_s;
537}
538
540{
541 auto loc = Json::fromJson<Location>(obj);
542 const auto tz = obj.value("timezone"_L1).toString();
543 if (!tz.isEmpty()) {
544 loc.setTimeZone(QTimeZone(tz.toUtf8()));
545 }
546
547 loc.d->ids.fromJson(obj.value("identifier"_L1).toObject());
548
549 switch (loc.type()) {
550 case Place:
551 case Address:
552 case Stop:
554 break;
556 loc.setData(RentalVehicleStation::fromJson(obj.value("rentalVehicleStation"_L1).toObject()));
557 break;
558 case RentedVehicle:
559 loc.setData(RentalVehicle::fromJson(obj.value("rentalVehicle"_L1).toObject()));
560 break;
561 case Equipment:
562 loc.setData(Equipment::fromJson(obj.value("equipment"_L1).toObject()));
563 break;
564 }
565
566 return loc;
567}
568
569std::vector<Location> Location::fromJson(const QJsonArray &a)
570{
571 return Json::fromJson<Location>(a);
572}
573
574#include "moc_location.cpp"
static KCountrySubdivision fromLocation(float latitude, float longitude)
static KCountry fromLocation(float latitude, float longitude)
QString iconName
An icon representing the equipment type.
Definition equipment.h:45
static QJsonObject toJson(const Equipment &equipment)
Serializes one object to JSON.
Definition equipment.cpp:58
static Equipment fromJson(const QJsonObject &obj)
Deserialize an object from JSON.
Definition equipment.cpp:63
bool hasFloorLevel
Indicates whether the floor level is set.
Definition location.h:76
QStringList identifierTypes
Identifier types set on this location.
Definition location.h:96
KPublicTransport::Equipment equipment
Equipment information, if applicable for this location.
Definition location.h:88
KPublicTransport::RentalVehicle rentalVehicle
Rental vehicle information, if applicable for this location.
Definition location.h:86
double longitude
Longitude of the location, in degree, NaN if unknown.
Definition location.h:56
static Location fromJson(const QJsonObject &obj)
Deserialize a Location object from JSON.
Definition location.cpp:539
QTimeZone timeZone() const
The timezone this location is in, if known.
Definition location.cpp:126
KPublicTransport::RentalVehicleStation rentalVehicleStation
Rental vehicle dock information, if applicable for this location.
Definition location.h:84
QString region
Region (as in ISO 3166-2) of the location, if known.
Definition location.h:65
static bool isSameName(const QString &lhs, const QString &rhs)
Checks if two location names refer to the same location.
Definition location.cpp:360
Type
Type of location.
Definition location.h:35
@ RentedVehicleStation
a pick-up/drop-off point for dock-based rental bike/scooter systems
Definition location.h:38
@ Place
a location that isn't of any specific type
Definition location.h:36
@ RentedVehicle
a free-floating rental bike/scooter
Definition location.h:39
@ Equipment
elevator/escalator
Definition location.h:40
@ Address
postal addresses
Definition location.h:42
@ Stop
a public transport stop (train station, bus stop, etc)
Definition location.h:37
@ CarpoolPickupDropoff
a pickup or dropoff location for a carpool trip
Definition location.h:41
static QJsonObject toJson(const Location &loc)
Serializes one Location object to JSON.
Definition location.cpp:478
QString iconName
Icon representing the location type.
Definition location.h:93
QString identifier(QAnyStringView identifierType) const
Location identifiers.
Definition location.cpp:145
static double distance(double lat1, double lon1, double lat2, double lon2)
Compute the distance between two geo coordinates, in meters.
Definition location.cpp:458
static Location merge(const Location &lhs, const Location &rhs)
Merge two departure instances.
Definition location.cpp:407
double latitude
Latitude of the location, in degree, NaN if unknown.
Definition location.h:54
QString streetAddress
Street address of the location, if known.
Definition location.h:59
QString country
Country of the location as ISO 3166-1 alpha 2 code, if known.
Definition location.h:67
QString locality
Locality/city of the location, if known.
Definition location.h:63
QString name
Human-readable name of the location.
Definition location.h:52
QString postalCode
Postal code of the location, if known.
Definition location.h:61
bool isEmpty() const
Returns true if this is an default-constructed location object not specifying any location.
Definition location.cpp:121
Type type
Location type.
Definition location.h:49
static bool isSame(const Location &lhs, const Location &rhs)
Checks if to instances refer to the same location (which does not necessarily mean they are exactly e...
Definition location.cpp:312
Additional information for a vehicle renting station, attached to Location objects.
static RentalVehicleStation fromJson(const QJsonObject &obj)
Deserialize an object from JSON.
QString iconName
Icon representing this rental vehicle station.
static bool isSame(const RentalVehicleStation &lhs, const RentalVehicleStation &rhs)
Checks if two instances refer to the same station.
bool isValid
Not an empty/default constructed object.
static QJsonObject toJson(const RentalVehicleStation &station)
Serializes one object to JSON.
An individual rental vehicle used on a JourneySection, ie.
static RentalVehicle fromJson(const QJsonObject &obj)
Deserialize an object from JSON.
static QJsonObject toJson(const RentalVehicle &vehicle)
Serializes one object to JSON.
QString vehicleTypeIconName
Icon representing the vehicle type.
bool isSameStopPlace(QStringView lhs, QStringView rhs)
Checks whether two valid IFOPT ids refer to the same stop place.
Definition ifoptutil.cpp:58
QString identifierType()
The identifier type for use in Location::identifer for IFOPT ids.
Definition ifoptutil.cpp:83
QStringView merge(QStringView lhs, QStringView rhs)
Merge two IFOPT ids that refer to the same stop place while retaining the maximum level of detail.
Definition ifoptutil.cpp:63
QStringView countryCode(QStringView coachNumber)
Returns the UIC country code from coachNumber.
Query operations and data types for accessing realtime public transport information from online servi...
KI18NLOCALEDATA_EXPORT const char * fromLocation(float latitude, float longitude)
QJsonValue value(QLatin1StringView key) const const
QJsonObject toObject() const const
QString toString() const const
const QChar at(qsizetype position) const const
QString fromUtf8(QByteArrayView str)
void push_back(QChar ch)
void reserve(qsizetype size)
qsizetype size() const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
CaseInsensitive
SkipEmptyParts
QByteArray id() const const
bool isValid() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 21 2025 11:47:40 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.