KPublicTransport

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

KDE's Doxygen guidelines are available online.