KItinerary

locationutil.cpp
1 /*
2  SPDX-FileCopyrightText: 2018 Volker Krause <[email protected]>
3 
4  SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "locationutil.h"
8 #include "locationutil_p.h"
9 
10 #include <KItinerary/BoatTrip>
11 #include <KItinerary/BusTrip>
12 #include <KItinerary/Event>
13 #include <KItinerary/Flight>
14 #include <KItinerary/Place>
15 #include <KItinerary/Reservation>
16 #include <KItinerary/TrainTrip>
17 #include <KItinerary/Visit>
18 
19 #include <KContacts/Address>
20 
21 #include <QDebug>
22 #include <QUrl>
23 #include <QUrlQuery>
24 
25 #include <cmath>
26 
27 using namespace KItinerary;
28 
29 KContacts::Address LocationUtil::toAddress(const PostalAddress &addr)
30 {
32  a.setStreet(addr.streetAddress());
33  a.setPostalCode(addr.postalCode());
34  a.setLocality(addr.addressLocality());
35  a.setRegion(addr.addressRegion());
36  a.setCountry(addr.addressCountry());
37  return a;
38 }
39 
41 {
42  if (JsonLd::isA<RentalCarReservation>(res)) {
43  const auto pickup = departureLocation(res);
44  const auto dropoff = arrivalLocation(res);
45  if (dropoff.value<Place>().name().isEmpty()) {
46  return false;
47  }
48  return !isSameLocation(pickup, dropoff);
49  }
50  return JsonLd::isA<FlightReservation>(res) || JsonLd::isA<TrainReservation>(res) || JsonLd::isA<BusReservation>(res) || JsonLd::isA<TaxiReservation>(res) || JsonLd::isA<BoatReservation>(res);
51 }
52 
54 {
55  if (JsonLd::isA<FlightReservation>(res)) {
56  return res.value<FlightReservation>().reservationFor().value<Flight>().arrivalAirport();
57  }
58  if (JsonLd::isA<TrainReservation>(res)) {
59  return res.value<TrainReservation>().reservationFor().value<TrainTrip>().arrivalStation();
60  }
61  if (JsonLd::isA<BusReservation>(res)) {
62  return res.value<BusReservation>().reservationFor().value<BusTrip>().arrivalBusStop();
63  }
64  if (JsonLd::isA<RentalCarReservation>(res)) {
65  return res.value<RentalCarReservation>().dropoffLocation();
66  }
67  if (JsonLd::isA<BoatReservation>(res)) {
68  return res.value<BoatReservation>().reservationFor().value<BoatTrip>().arrivalBoatTerminal();
69  }
70  return {};
71 }
72 
74 {
75  if (JsonLd::isA<FlightReservation>(res)) {
76  return res.value<FlightReservation>().reservationFor().value<Flight>().departureAirport();
77  }
78  if (JsonLd::isA<TrainReservation>(res)) {
79  return res.value<TrainReservation>().reservationFor().value<TrainTrip>().departureStation();
80  }
81  if (JsonLd::isA<BusReservation>(res)) {
82  return res.value<BusReservation>().reservationFor().value<BusTrip>().departureBusStop();
83  }
84  if (JsonLd::isA<RentalCarReservation>(res)) {
85  return res.value<RentalCarReservation>().pickupLocation();
86  }
87  if (JsonLd::isA<TaxiReservation>(res)) {
88  return res.value<TaxiReservation>().pickupLocation();
89  }
90  if (JsonLd::isA<BoatReservation>(res)) {
91  return res.value<BoatReservation>().reservationFor().value<BoatTrip>().departureBoatTerminal();
92  }
93  return {};
94 }
95 
97 {
98  if (JsonLd::isA<LodgingReservation>(res)) {
99  return res.value<LodgingReservation>().reservationFor();
100  }
101  if (JsonLd::isA<FoodEstablishmentReservation>(res)) {
102  return res.value<FoodEstablishmentReservation>().reservationFor();
103  }
104  if (JsonLd::isA<TouristAttractionVisit>(res)) {
105  return res.value<TouristAttractionVisit>().touristAttraction();
106  }
107  if (JsonLd::isA<EventReservation>(res)) {
108  return res.value<EventReservation>().reservationFor().value<Event>().location();
109  }
110  if (JsonLd::isA<RentalCarReservation>(res)) {
111  return res.value<RentalCarReservation>().pickupLocation();
112  }
113 
114  return {};
115 }
116 
118 {
119  if (JsonLd::canConvert<Place>(location)) {
120  return JsonLd::convert<Place>(location).geo();
121  }
122  if (JsonLd::canConvert<Organization>(location)) {
123  return JsonLd::convert<Organization>(location).geo();
124  }
125 
126  return {};
127 }
128 
130 {
131  if (JsonLd::canConvert<Place>(location)) {
132  return JsonLd::convert<Place>(location).address();
133  }
134  if (JsonLd::canConvert<Organization>(location)) {
135  return JsonLd::convert<Organization>(location).address();
136  }
137 
138  return {};
139 }
140 
141 QString LocationUtil::name(const QVariant &location)
142 {
143  if (JsonLd::isA<Airport>(location)) {
144  const auto airport = location.value<Airport>();
145  return airport.name().isEmpty() ? airport.iataCode() : airport.name();
146  }
147  if (JsonLd::canConvert<Place>(location)) {
148  return JsonLd::convert<Place>(location).name();
149  }
150  if (JsonLd::canConvert<Organization>(location)) {
151  return JsonLd::convert<Organization>(location).name();
152  }
153 
154  return {};
155 }
156 
157 int LocationUtil::distance(const GeoCoordinates &coord1, const GeoCoordinates &coord2)
158 {
159  return distance(coord1.latitude(), coord1.longitude(), coord2.latitude(), coord2.longitude());
160 }
161 
162 // see https://en.wikipedia.org/wiki/Haversine_formula
163 int LocationUtil::distance(float lat1, float lon1, float lat2, float lon2)
164 {
165  const auto degToRad = M_PI / 180.0;
166  const auto earthRadius = 6371000.0; // in meters
167 
168  const auto d_lat = (lat1 - lat2) * degToRad;
169  const auto d_lon = (lon1 - lon2) * degToRad;
170 
171  const auto a = pow(sin(d_lat / 2.0), 2) + cos(lat1 * degToRad) * cos(lat2 * degToRad) * pow(sin(d_lon / 2.0), 2);
172  return 2.0 * earthRadius * atan2(sqrt(a), sqrt(1.0 - a));
173 }
174 
175 // if the character has a canonical decomposition use that and skip the combining diacritic markers following it
176 // see https://en.wikipedia.org/wiki/Unicode_equivalence
177 // see https://en.wikipedia.org/wiki/Combining_character
178 static QString stripDiacritics(const QString &s)
179 {
180  QString res;
181  res.reserve(s.size());
182  for (const auto &c : s) {
183  if (c.decompositionTag() == QChar::Canonical) {
184  res.push_back(c.decomposition().at(0));
185  } else {
186  res.push_back(c);
187  }
188  }
189  return res;
190 }
191 
192 // keep this ordered (see https://en.wikipedia.org/wiki/List_of_Unicode_characters)
193 struct {
194  ushort key;
195  const char* replacement;
196 } static const transliteration_map[] = {
197  { u'ä', "ae" },
198  { u'ö', "oe" },
199  { u'ø', "oe" },
200  { u'ü', "ue" }
201 };
202 
203 static QString applyTransliterations(const QString &s)
204 {
205  QString res;
206  res.reserve(s.size());
207 
208  for (const auto c : s) {
209  const auto it = std::lower_bound(std::begin(transliteration_map), std::end(transliteration_map), c, [](const auto &lhs, const auto rhs) {
210  return QChar(lhs.key) < rhs;
211  });
212  if (it != std::end(transliteration_map) && QChar((*it).key) == c) {
213  res += QString::fromUtf8((*it).replacement);
214  continue;
215  }
216 
217  if (c.decompositionTag() == QChar::Canonical) { // see above
218  res += c.decomposition().at(0);
219  } else {
220  res += c;
221  }
222  }
223 
224  return res;
225 }
226 
227 static bool compareSpaceCaseInsenstive(const QString &lhs, const QString &rhs)
228 {
229  auto lit = lhs.begin();
230  auto rit = rhs.begin();
231  while (true) {
232  while ((*lit).isSpace() && lit != lhs.end()) {
233  ++lit;
234  }
235  while ((*rit).isSpace() && rit != rhs.end()) {
236  ++rit;
237  }
238  if (lit == lhs.end() || rit == rhs.end()) {
239  break;
240  }
241  if ((*lit).toCaseFolded() != (*rit).toCaseFolded()) {
242  return false;
243  }
244  ++lit;
245  ++rit;
246  }
247 
248  return lit == lhs.end() && rit == rhs.end();
249 }
250 
251 static bool isSameLocationName(const QString &lhs, const QString &rhs, LocationUtil::Accuracy accuracy)
252 {
253  if (lhs.isEmpty() || rhs.isEmpty()) {
254  return false;
255  }
256 
257  // actually equal
258  if (lhs.compare(rhs, Qt::CaseInsensitive) == 0) {
259  return true;
260  }
261 
262  // check if any of the Unicode normalization approaches helps
263  const auto lhsNormalized = stripDiacritics(lhs);
264  const auto rhsNormalized = stripDiacritics(rhs);
265  const auto lhsTransliterated = applyTransliterations(lhs);
266  const auto rhsTransliterated = applyTransliterations(rhs);
267  if (compareSpaceCaseInsenstive(lhsNormalized, rhsNormalized) || compareSpaceCaseInsenstive(lhsNormalized, rhsTransliterated)
268  || compareSpaceCaseInsenstive(lhsTransliterated, rhsNormalized) || compareSpaceCaseInsenstive(lhsTransliterated, rhsTransliterated)) {
269  return true;
270  }
271 
272  if (accuracy == LocationUtil::CityLevel) {
273  // check for a common prefix
274  bool foundSeparator = false;
275  for (auto i = 0; i < std::min(lhsNormalized.size(), rhsNormalized.size()); ++i) {
276  if (lhsNormalized[i] != rhsNormalized[i]) {
277  return foundSeparator;
278  }
279  foundSeparator |= !lhsNormalized[i].isLetter();
280  }
281 
282  return lhsNormalized.startsWith(rhsNormalized) || rhsNormalized.startsWith(lhsNormalized);
283  }
284 
285  return false;
286 }
287 
289 {
290  const auto lhsGeo = geo(lhs);
291  const auto rhsGeo = geo(rhs);
292  if (lhsGeo.isValid() && rhsGeo.isValid()) {
293  const auto d = distance(lhsGeo, rhsGeo);
294  switch (accuracy) {
295  case Exact:
296  return d < 100;
297  case WalkingDistance:
298  {
299  // airports are large but we have no local transport there, so the distance threshold needs to be higher there
300  const auto isAirport = JsonLd::isA<Airport>(lhs) || JsonLd::isA<Airport>(rhs);
301  return d < (isAirport ? 2000 : 1000);
302  }
303  case CityLevel:
304  if (d >= 50000) {
305  return false;
306  }
307  if (d < 2000) {
308  return true;
309  }
310  if (d < 50000 && address(lhs).addressLocality().isEmpty() && name(lhs).isEmpty()) {
311  return true;
312  }
313  break;
314  }
315  }
316 
317  const auto lhsAddr = address(lhs);
318  const auto rhsAddr = address(rhs);
319  switch (accuracy) {
320  case Exact:
321  case WalkingDistance:
322  if (!lhsAddr.streetAddress().isEmpty() && !lhsAddr.addressLocality().isEmpty()) {
323  return lhsAddr.streetAddress() == rhsAddr.streetAddress() && lhsAddr.addressLocality() == rhsAddr.addressLocality();
324  }
325  break;
326  case CityLevel:
327  if (!lhsAddr.addressLocality().isEmpty()) {
328  return lhsAddr.addressLocality() == rhsAddr.addressLocality();
329  }
330  break;
331  }
332 
333  return isSameLocationName(name(lhs), name(rhs), accuracy);
334 }
335 
337 {
338  QUrl url;
339  url.setScheme(QStringLiteral("geo"));
340 
341  const auto geo = LocationUtil::geo(location);
342  if (geo.isValid()) {
343  url.setPath(QString::number(geo.latitude()) + QLatin1Char(',') + QString::number(geo.longitude()));
344  return url;
345  }
346 
347  const auto addr = LocationUtil::address(location);
348  if (!addr.isEmpty()) {
349  url.setPath(QStringLiteral("0,0"));
350  QUrlQuery query;
351  query.addQueryItem(QStringLiteral("q"), toAddress(addr).formatted(KContacts::AddressFormatStyle::GeoUriQuery));
352  url.setQuery(query);
353  return url;
354  }
355 
356  return {};
357 }
QString::iterator begin()
A train reservation.
Definition: reservation.h:99
void setLocality(const QString &locality)
QString number(int n, int base)
QVariant location(const QVariant &res)
Returns the location of a non-transport reservation.
void setStreet(const QString &street)
A Taxi reservation.
Definition: reservation.h:148
QString fromUtf8(const char *str, int size)
int size() const const
CaseInsensitive
T value() const const
A boat or ferry reservation.
Definition: reservation.h:158
@ CityLevel
Locations are in the same city.
Definition: locationutil.h:62
An event reservation.
Definition: reservation.h:128
QUrl geoUri(const QVariant &location)
Returns a geo: URI for the given location.
void reserve(int size)
QString::iterator end()
void setScheme(const QString &scheme)
A bus trip.
Definition: bustrip.h:21
A hotel reservation.
Definition: reservation.h:70
Base class for places.
Definition: place.h:68
void setRegion(const QString &region)
Accuracy
Location comparison accuracy.
Definition: locationutil.h:60
A bus reservation.
Definition: reservation.h:107
bool isEmpty() const const
A boat or ferry trip.
Definition: boattrip.h:22
QVariant arrivalLocation(const QVariant &res)
Returns the arrival location of the given reservation.
void push_back(QChar ch)
QVariant departureLocation(const QVariant &res)
Returns the departure location of the given reservation.
An event.
Definition: event.h:20
PostalAddress address(const QVariant &location)
Returns the address of the given location.
GeoCoordinates geo(const QVariant &location)
Returns the geo coordinates of a given location.
Postal address.
Definition: place.h:45
bool isSameLocation(const QVariant &lhs, const QVariant &rhs, Accuracy accuracy=Exact)
Returns true if the given locations are the same.
Geographic coordinates.
Definition: place.h:22
int distance(const GeoCoordinates &coord1, const GeoCoordinates &coord2)
Computes the distance between to geo coordinates in meters.
void setQuery(const QString &query, QUrl::ParsingMode mode)
void setPostalCode(const QString &code)
Airport.
Definition: place.h:102
A flight.
Definition: flight.h:24
void setPath(const QString &path, QUrl::ParsingMode mode)
void setCountry(const QString &country)
const QChar at(int position) const const
A Rental Car reservation.
Definition: reservation.h:136
A train trip.
Definition: traintrip.h:23
int compare(const QString &other, Qt::CaseSensitivity cs) const const
@ Exact
Locations match exactly.
Definition: locationutil.h:61
bool isEmpty
The country this address is in, as ISO 3166-1 alpha 2 code.
Definition: place.h:55
@ WalkingDistance
Locations are close enough together to not need transportation.
Definition: locationutil.h:63
A flight reservation.
Definition: reservation.h:81
bool isLocationChange(const QVariant &res)
Returns true if the given reservation is a location change.
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Wed Aug 17 2022 03:58:04 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.