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 <QRegularExpression>
24#include <QUrl>
25#include <QUrlQuery>
26
27#include <cmath>
28
29using namespace KItinerary;
30
31KContacts::Address LocationUtil::toAddress(const PostalAddress &addr)
32{
34 a.setStreet(addr.streetAddress());
35 a.setPostalCode(addr.postalCode());
36 a.setLocality(addr.addressLocality());
37 a.setRegion(addr.addressRegion());
38 a.setCountry(addr.addressCountry());
39 return a;
40}
41
43{
45 const auto pickup = departureLocation(res);
46 const auto dropoff = arrivalLocation(res);
47 if (dropoff.value<Place>().name().isEmpty()) {
48 return false;
49 }
50 return !isSameLocation(pickup, dropoff);
51 }
53}
54
56{
58 return res.value<FlightReservation>().reservationFor().value<Flight>().arrivalAirport();
59 }
61 return res.value<TrainReservation>().reservationFor().value<TrainTrip>().arrivalStation();
62 }
64 return res.value<BusReservation>().reservationFor().value<BusTrip>().arrivalBusStop();
65 }
67 return res.value<RentalCarReservation>().dropoffLocation();
68 }
70 return res.value<BoatReservation>().reservationFor().value<BoatTrip>().arrivalBoatTerminal();
71 }
72 return {};
73}
74
76{
78 return res.value<FlightReservation>().reservationFor().value<Flight>().departureAirport();
79 }
81 return res.value<TrainReservation>().reservationFor().value<TrainTrip>().departureStation();
82 }
84 return res.value<BusReservation>().reservationFor().value<BusTrip>().departureBusStop();
85 }
87 return res.value<RentalCarReservation>().pickupLocation();
88 }
90 return res.value<TaxiReservation>().pickupLocation();
91 }
93 return res.value<BoatReservation>().reservationFor().value<BoatTrip>().departureBoatTerminal();
94 }
95 return {};
96}
97
99{
101 return res.value<LodgingReservation>().reservationFor();
102 }
104 return res.value<FoodEstablishmentReservation>().reservationFor();
105 }
107 return res.value<TouristAttractionVisit>().touristAttraction();
108 }
110 return res.value<EventReservation>().reservationFor().value<Event>().location();
111 }
113 return res.value<RentalCarReservation>().pickupLocation();
114 }
115
116 return {};
117}
118
120{
122 return JsonLd::convert<Place>(location).geo();
123 }
126 }
127
128 return {};
129}
130
132{
133 if (url.host().contains(QLatin1StringView("google"))) {
134 QRegularExpression regExp(
135 QStringLiteral("[/=@](-?\\d+\\.\\d+),(-?\\d+\\.\\d+)"));
136 auto match = regExp.match(url.path());
137 if (!match.hasMatch()) {
138 match = regExp.match(url.query());
139 }
140
141 if (match.hasMatch()) {
142 const auto latitude = match.capturedView(1).toDouble();
143 const auto longitude = match.capturedView(2).toDouble();
144
145 return GeoCoordinates{latitude, longitude};
146 }
147 }
148
149 return {};
150}
151
153{
155 return JsonLd::convert<Place>(location).address();
156 }
158 return JsonLd::convert<Organization>(location).address();
159 }
160
161 return {};
162}
163
165{
167 const auto airport = location.value<Airport>();
168 return airport.name().isEmpty() ? airport.iataCode() : airport.name();
169 }
171 return JsonLd::convert<Place>(location).name();
172 }
175 }
176
177 return {};
178}
179
180int LocationUtil::distance(const GeoCoordinates &coord1, const GeoCoordinates &coord2)
181{
182 return distance(coord1.latitude(), coord1.longitude(), coord2.latitude(), coord2.longitude());
183}
184
185// see https://en.wikipedia.org/wiki/Haversine_formula
186int LocationUtil::distance(double lat1, double lon1, double lat2, double lon2)
187{
188 const auto degToRad = M_PI / 180.0;
189 const auto earthRadius = 6371000.0; // in meters
190
191 const auto d_lat = (lat1 - lat2) * degToRad;
192 const auto d_lon = (lon1 - lon2) * degToRad;
193
194 const auto a = pow(sin(d_lat / 2.0), 2) + cos(lat1 * degToRad) * cos(lat2 * degToRad) * pow(sin(d_lon / 2.0), 2);
195 return 2.0 * earthRadius * atan2(sqrt(a), sqrt(1.0 - a));
196}
197
198// if the character has a canonical decomposition use that and skip the combining diacritic markers following it
199// see https://en.wikipedia.org/wiki/Unicode_equivalence
200// see https://en.wikipedia.org/wiki/Combining_character
201static QString stripDiacritics(const QString &s)
202{
203 QString res;
204 res.reserve(s.size());
205 for (const auto &c : s) {
206 if (c.decompositionTag() == QChar::Canonical) {
207 res.push_back(c.decomposition().at(0));
208 } else {
209 res.push_back(c);
210 }
211 }
212 return res;
213}
214
215static bool compareSpaceCaseInsenstive(const QString &lhs, const QString &rhs)
216{
217 auto lit = lhs.begin();
218 auto rit = rhs.begin();
219 while (true) {
220 while ((*lit).isSpace() && lit != lhs.end()) {
221 ++lit;
222 }
223 while ((*rit).isSpace() && rit != rhs.end()) {
224 ++rit;
225 }
226 if (lit == lhs.end() || rit == rhs.end()) {
227 break;
228 }
229 if ((*lit).toCaseFolded() != (*rit).toCaseFolded()) {
230 return false;
231 }
232 ++lit;
233 ++rit;
234 }
235
236 return lit == lhs.end() && rit == rhs.end();
237}
238
239static bool hasCommonPrefix(QStringView lhs, QStringView rhs)
240{
241 // check for a common prefix
242 bool foundSeparator = false;
243 for (auto i = 0; i < std::min(lhs.size(), rhs.size()); ++i) {
244 if (lhs[i].toCaseFolded() != rhs[i].toCaseFolded()) {
245 return foundSeparator;
246 }
247 foundSeparator |= !lhs[i].isLetter();
248 }
249
250 return lhs.startsWith(rhs, Qt::CaseInsensitive) || rhs.startsWith(lhs, Qt::CaseInsensitive);
251}
252
253[[nodiscard]] static bool isStrictLongPrefix(QStringView lhs, QStringView rhs)
254{
255 // 17 is the maximum field length in RCT2
256 if (lhs.size() < 17 || rhs.size() < 17) {
257 return false;
258 }
259 if (lhs.startsWith(rhs, Qt::CaseInsensitive)) {
260 return lhs.at(rhs.size()).isLetter();
261 }
262 if (rhs.startsWith(lhs, Qt::CaseInsensitive)) {
263 return rhs.at(lhs.size()).isLetter();
264 }
265 return false;
266}
267
268static bool isSameLocationName(const QString &lhs, const QString &rhs, LocationUtil::Accuracy accuracy)
269{
270 if (lhs.isEmpty() || rhs.isEmpty()) {
271 return false;
272 }
273
274 // actually equal
275 if (lhs.compare(rhs, Qt::CaseInsensitive) == 0) {
276 return true;
277 }
278
279 // check if any of the Unicode normalization approaches helps
280 const auto lhsNormalized = stripDiacritics(lhs);
281 const auto rhsNormalized = stripDiacritics(rhs);
282 const auto lhsTransliterated = StringUtil::transliterate(lhs);
283 const auto rhsTransliterated = StringUtil::transliterate(rhs);
284 if (compareSpaceCaseInsenstive(lhsNormalized, rhsNormalized) || compareSpaceCaseInsenstive(lhsNormalized, rhsTransliterated)
285 || compareSpaceCaseInsenstive(lhsTransliterated, rhsNormalized) || compareSpaceCaseInsenstive(lhsTransliterated, rhsTransliterated)) {
286 return true;
287 }
288
289 // sufficiently long prefix that we can assume RCT2 field overflow
290 if (isStrictLongPrefix(lhs, rhs)) {
291 return true;
292 }
293
294 if (accuracy == LocationUtil::CityLevel) {
295 // check for a common prefix
296 return hasCommonPrefix(lhsNormalized, rhsNormalized) || hasCommonPrefix(lhsTransliterated, rhsTransliterated);
297 }
298
299 return false;
300}
301
303{
304 const auto lhsGeo = geo(lhs);
305 const auto rhsGeo = geo(rhs);
306 const auto lhsAddr = address(lhs);
307 const auto rhsAddr = address(rhs);
308
309 const auto lhsIsTransportStop = JsonLd::isA<Airport>(lhs) || JsonLd::isA<TrainStation>(lhs) || JsonLd::isA<BusStation>(lhs);
310 const auto rhsIsTransportStop = JsonLd::isA<Airport>(rhs) || JsonLd::isA<TrainStation>(rhs) || JsonLd::isA<BusStation>(rhs);
311 const auto isNameComparable = lhsIsTransportStop && rhsIsTransportStop;
312
313 if (lhsGeo.isValid() && rhsGeo.isValid()) {
314 const auto d = distance(lhsGeo, rhsGeo);
315 switch (accuracy) {
316 case Exact:
317 return d < 100;
318 case WalkingDistance:
319 {
320 // airports are large but we have no local transport there, so the distance threshold needs to be higher there
321 const auto isAirport = JsonLd::isA<Airport>(lhs) || JsonLd::isA<Airport>(rhs);
322 return d < (isAirport ? 2000 : 1000);
323 }
324 case CityLevel:
325 if (d >= 50000) {
326 return false;
327 }
328 if (d < 2000) {
329 return true;
330 }
331 if (d < 50000 && (lhsAddr.addressLocality().isEmpty() || rhsAddr.addressLocality().isEmpty()) && (!isNameComparable || name(lhs).isEmpty() || name(rhs).isEmpty())) {
332 return true;
333 }
334 break;
335 }
336 }
337
338 switch (accuracy) {
339 case Exact:
340 case WalkingDistance:
341 if (!lhsAddr.streetAddress().isEmpty() && !rhsAddr.addressLocality().isEmpty()) {
342 return lhsAddr.streetAddress() == rhsAddr.streetAddress() && lhsAddr.addressLocality() == rhsAddr.addressLocality();
343 }
344 break;
345 case CityLevel:
346 if (!lhsAddr.addressLocality().isEmpty() && !rhsAddr.addressLocality().isEmpty()) {
347 return isSameLocationName(lhsAddr.addressLocality(), rhsAddr.addressLocality(), LocationUtil::Exact);
348 }
349 break;
350 }
351
352 return isSameLocationName(name(lhs), name(rhs), accuracy);
353}
354
356{
357 QUrl url;
358 url.setScheme(QStringLiteral("geo"));
359
360 const auto geo = LocationUtil::geo(location);
361 if (geo.isValid()) {
362 url.setPath(QString::number(geo.latitude()) + QLatin1Char(',') + QString::number(geo.longitude()));
363 return url;
364 }
365
366 const auto addr = LocationUtil::address(location);
367 if (!addr.isEmpty()) {
368 url.setPath(QStringLiteral("0,0"));
369 QUrlQuery query;
370 query.addQueryItem(QStringLiteral("q"), toAddress(addr).formatted(KContacts::AddressFormatStyle::GeoUriQuery));
371 url.setQuery(query);
372 return url;
373 }
374
375 return {};
376}
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.
GeoCoordinates geoFromUrl(const QUrl &url)
Parses geo coordinates from a given mapping service URLs, such as Google Maps links.
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)
QRegularExpressionMatch match(QStringView subjectView, qsizetype offset, MatchType matchType, MatchOptions matchOptions) const const
iterator begin()
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
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
QString host(ComponentFormattingOptions options) const const
QString path(ComponentFormattingOptions options) const const
QString query(ComponentFormattingOptions options) const const
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 Sat Dec 21 2024 16:56:37 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.