KItinerary

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

KDE's Doxygen guidelines are available online.