File indexing completed on 2024-12-22 04:59:46

0001 /*
0002     SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "locationutil.h"
0008 #include "locationutil_p.h"
0009 #include "stringutil.h"
0010 
0011 #include <KItinerary/BoatTrip>
0012 #include <KItinerary/BusTrip>
0013 #include <KItinerary/Event>
0014 #include <KItinerary/Flight>
0015 #include <KItinerary/Place>
0016 #include <KItinerary/Reservation>
0017 #include <KItinerary/TrainTrip>
0018 #include <KItinerary/Visit>
0019 
0020 #include <KContacts/Address>
0021 
0022 #include <QDebug>
0023 #include <QUrl>
0024 #include <QUrlQuery>
0025 
0026 #include <cmath>
0027 
0028 using namespace KItinerary;
0029 
0030 KContacts::Address LocationUtil::toAddress(const PostalAddress &addr)
0031 {
0032     KContacts::Address a;
0033     a.setStreet(addr.streetAddress());
0034     a.setPostalCode(addr.postalCode());
0035     a.setLocality(addr.addressLocality());
0036     a.setRegion(addr.addressRegion());
0037     a.setCountry(addr.addressCountry());
0038     return a;
0039 }
0040 
0041 bool LocationUtil::isLocationChange(const QVariant &res)
0042 {
0043     if (JsonLd::isA<RentalCarReservation>(res)) {
0044         const auto pickup = departureLocation(res);
0045         const auto dropoff = arrivalLocation(res);
0046         if (dropoff.value<Place>().name().isEmpty()) {
0047             return false;
0048         }
0049         return !isSameLocation(pickup, dropoff);
0050     }
0051     return JsonLd::isA<FlightReservation>(res) || JsonLd::isA<TrainReservation>(res) || JsonLd::isA<BusReservation>(res) || JsonLd::isA<TaxiReservation>(res) || JsonLd::isA<BoatReservation>(res);
0052 }
0053 
0054 QVariant LocationUtil::arrivalLocation(const QVariant &res)
0055 {
0056     if (JsonLd::isA<FlightReservation>(res)) {
0057         return res.value<FlightReservation>().reservationFor().value<Flight>().arrivalAirport();
0058     }
0059     if (JsonLd::isA<TrainReservation>(res)) {
0060         return res.value<TrainReservation>().reservationFor().value<TrainTrip>().arrivalStation();
0061     }
0062     if (JsonLd::isA<BusReservation>(res)) {
0063         return res.value<BusReservation>().reservationFor().value<BusTrip>().arrivalBusStop();
0064     }
0065     if (JsonLd::isA<RentalCarReservation>(res)) {
0066         return res.value<RentalCarReservation>().dropoffLocation();
0067     }
0068     if (JsonLd::isA<BoatReservation>(res)) {
0069         return  res.value<BoatReservation>().reservationFor().value<BoatTrip>().arrivalBoatTerminal();
0070     }
0071     return {};
0072 }
0073 
0074 QVariant LocationUtil::departureLocation(const QVariant &res)
0075 {
0076     if (JsonLd::isA<FlightReservation>(res)) {
0077         return res.value<FlightReservation>().reservationFor().value<Flight>().departureAirport();
0078     }
0079     if (JsonLd::isA<TrainReservation>(res)) {
0080         return res.value<TrainReservation>().reservationFor().value<TrainTrip>().departureStation();
0081     }
0082     if (JsonLd::isA<BusReservation>(res)) {
0083         return res.value<BusReservation>().reservationFor().value<BusTrip>().departureBusStop();
0084     }
0085     if (JsonLd::isA<RentalCarReservation>(res)) {
0086         return res.value<RentalCarReservation>().pickupLocation();
0087     }
0088     if (JsonLd::isA<TaxiReservation>(res)) {
0089         return res.value<TaxiReservation>().pickupLocation();
0090     }
0091     if (JsonLd::isA<BoatReservation>(res)) {
0092         return res.value<BoatReservation>().reservationFor().value<BoatTrip>().departureBoatTerminal();
0093     }
0094     return {};
0095 }
0096 
0097 QVariant LocationUtil::location(const QVariant &res)
0098 {
0099     if (JsonLd::isA<LodgingReservation>(res)) {
0100         return res.value<LodgingReservation>().reservationFor();
0101     }
0102     if (JsonLd::isA<FoodEstablishmentReservation>(res)) {
0103         return res.value<FoodEstablishmentReservation>().reservationFor();
0104     }
0105     if (JsonLd::isA<TouristAttractionVisit>(res)) {
0106         return res.value<TouristAttractionVisit>().touristAttraction();
0107     }
0108     if (JsonLd::isA<EventReservation>(res)) {
0109         return res.value<EventReservation>().reservationFor().value<Event>().location();
0110     }
0111     if (JsonLd::isA<RentalCarReservation>(res)) {
0112         return res.value<RentalCarReservation>().pickupLocation();
0113     }
0114 
0115     return {};
0116 }
0117 
0118 GeoCoordinates LocationUtil::geo(const QVariant &location)
0119 {
0120     if (JsonLd::canConvert<Place>(location)) {
0121         return JsonLd::convert<Place>(location).geo();
0122     }
0123     if (JsonLd::canConvert<Organization>(location)) {
0124         return JsonLd::convert<Organization>(location).geo();
0125     }
0126 
0127     return {};
0128 }
0129 
0130 PostalAddress LocationUtil::address(const QVariant &location)
0131 {
0132     if (JsonLd::canConvert<Place>(location)) {
0133         return JsonLd::convert<Place>(location).address();
0134     }
0135     if (JsonLd::canConvert<Organization>(location)) {
0136         return JsonLd::convert<Organization>(location).address();
0137     }
0138 
0139     return {};
0140 }
0141 
0142 QString LocationUtil::name(const QVariant &location)
0143 {
0144     if (JsonLd::isA<Airport>(location)) {
0145         const auto airport = location.value<Airport>();
0146         return airport.name().isEmpty() ? airport.iataCode() : airport.name();
0147     }
0148     if (JsonLd::canConvert<Place>(location)) {
0149         return JsonLd::convert<Place>(location).name();
0150     }
0151     if (JsonLd::canConvert<Organization>(location)) {
0152         return JsonLd::convert<Organization>(location).name();
0153     }
0154 
0155     return {};
0156 }
0157 
0158 int LocationUtil::distance(const GeoCoordinates &coord1, const GeoCoordinates &coord2)
0159 {
0160     return distance(coord1.latitude(), coord1.longitude(), coord2.latitude(), coord2.longitude());
0161 }
0162 
0163 // see https://en.wikipedia.org/wiki/Haversine_formula
0164 int LocationUtil::distance(float lat1, float lon1, float lat2, float lon2)
0165 {
0166     const auto degToRad = M_PI / 180.0;
0167     const auto earthRadius = 6371000.0; // in meters
0168 
0169     const auto d_lat = (lat1 - lat2) * degToRad;
0170     const auto d_lon = (lon1 - lon2) * degToRad;
0171 
0172     const auto a = pow(sin(d_lat / 2.0), 2) + cos(lat1 * degToRad) * cos(lat2 * degToRad) * pow(sin(d_lon / 2.0), 2);
0173     return 2.0 * earthRadius * atan2(sqrt(a), sqrt(1.0 - a));
0174 }
0175 
0176 // if the character has a canonical decomposition use that and skip the combining diacritic markers following it
0177 // see https://en.wikipedia.org/wiki/Unicode_equivalence
0178 // see https://en.wikipedia.org/wiki/Combining_character
0179 static QString stripDiacritics(const QString &s)
0180 {
0181     QString res;
0182     res.reserve(s.size());
0183     for (const auto &c : s) {
0184         if (c.decompositionTag() == QChar::Canonical) {
0185             res.push_back(c.decomposition().at(0));
0186         } else {
0187             res.push_back(c);
0188         }
0189     }
0190     return res;
0191 }
0192 
0193 static bool compareSpaceCaseInsenstive(const QString &lhs, const QString &rhs)
0194 {
0195     auto lit = lhs.begin();
0196     auto rit = rhs.begin();
0197     while (true) {
0198         while ((*lit).isSpace() && lit != lhs.end()) {
0199             ++lit;
0200         }
0201         while ((*rit).isSpace() && rit != rhs.end()) {
0202             ++rit;
0203         }
0204         if (lit == lhs.end() || rit == rhs.end()) {
0205             break;
0206         }
0207         if ((*lit).toCaseFolded() != (*rit).toCaseFolded()) {
0208             return false;
0209         }
0210         ++lit;
0211         ++rit;
0212     }
0213 
0214     return lit == lhs.end() && rit == rhs.end();
0215 }
0216 
0217 static bool hasCommonPrefix(QStringView lhs, QStringView rhs)
0218 {
0219     // check for a common prefix
0220     bool foundSeparator = false;
0221     for (auto i = 0; i < std::min(lhs.size(), rhs.size()); ++i) {
0222         if (lhs[i].toCaseFolded() != rhs[i].toCaseFolded()) {
0223             return foundSeparator;
0224         }
0225         foundSeparator |= !lhs[i].isLetter();
0226     }
0227 
0228     return lhs.startsWith(rhs, Qt::CaseInsensitive) || rhs.startsWith(lhs, Qt::CaseInsensitive);
0229 }
0230 
0231 static bool isSameLocationName(const QString &lhs, const QString &rhs, LocationUtil::Accuracy accuracy)
0232 {
0233     if (lhs.isEmpty() || rhs.isEmpty()) {
0234         return false;
0235     }
0236 
0237     // actually equal
0238     if (lhs.compare(rhs, Qt::CaseInsensitive) == 0) {
0239         return true;
0240     }
0241 
0242     // check if any of the Unicode normalization approaches helps
0243     const auto lhsNormalized = stripDiacritics(lhs);
0244     const auto rhsNormalized = stripDiacritics(rhs);
0245     const auto lhsTransliterated = StringUtil::transliterate(lhs);
0246     const auto rhsTransliterated = StringUtil::transliterate(rhs);
0247     if (compareSpaceCaseInsenstive(lhsNormalized, rhsNormalized) || compareSpaceCaseInsenstive(lhsNormalized, rhsTransliterated)
0248         || compareSpaceCaseInsenstive(lhsTransliterated, rhsNormalized) || compareSpaceCaseInsenstive(lhsTransliterated, rhsTransliterated)) {
0249         return true;
0250     }
0251 
0252     if (accuracy == LocationUtil::CityLevel) {
0253         // check for a common prefix
0254         return hasCommonPrefix(lhsNormalized, rhsNormalized) || hasCommonPrefix(lhsTransliterated, rhsTransliterated);
0255     }
0256 
0257     return false;
0258 }
0259 
0260 bool LocationUtil::isSameLocation(const QVariant &lhs, const QVariant &rhs, LocationUtil::Accuracy accuracy)
0261 {
0262     const auto lhsGeo = geo(lhs);
0263     const auto rhsGeo = geo(rhs);
0264     if (lhsGeo.isValid() && rhsGeo.isValid()) {
0265         const auto d = distance(lhsGeo, rhsGeo);
0266         switch (accuracy) {
0267             case Exact:
0268                 return d < 100;
0269             case WalkingDistance:
0270             {
0271                 // airports are large but we have no local transport there, so the distance threshold needs to be higher there
0272                 const auto isAirport = JsonLd::isA<Airport>(lhs) || JsonLd::isA<Airport>(rhs);
0273                 return d < (isAirport ? 2000 : 1000);
0274             }
0275             case CityLevel:
0276                 if (d >= 50000) {
0277                     return false;
0278                 }
0279                 if (d < 2000) {
0280                     return true;
0281                 }
0282                 if (d < 50000 && address(lhs).addressLocality().isEmpty() && name(lhs).isEmpty()) {
0283                     return true;
0284                 }
0285                 break;
0286         }
0287     }
0288 
0289     const auto lhsAddr = address(lhs);
0290     const auto rhsAddr = address(rhs);
0291     switch (accuracy) {
0292         case Exact:
0293         case WalkingDistance:
0294             if (!lhsAddr.streetAddress().isEmpty() && !lhsAddr.addressLocality().isEmpty()) {
0295                 return  lhsAddr.streetAddress() == rhsAddr.streetAddress() && lhsAddr.addressLocality() == rhsAddr.addressLocality();
0296             }
0297             break;
0298         case CityLevel:
0299             if (!lhsAddr.addressLocality().isEmpty()) {
0300                 return isSameLocationName(lhsAddr.addressLocality(), rhsAddr.addressLocality(), LocationUtil::Exact);
0301             }
0302             break;
0303     }
0304 
0305     return isSameLocationName(name(lhs), name(rhs), accuracy);
0306 }
0307 
0308 QUrl LocationUtil::geoUri(const QVariant &location)
0309 {
0310     QUrl url;
0311     url.setScheme(QStringLiteral("geo"));
0312 
0313     const auto geo = LocationUtil::geo(location);
0314     if (geo.isValid()) {
0315         url.setPath(QString::number(geo.latitude()) + QLatin1Char(',') + QString::number(geo.longitude()));
0316         return url;
0317     }
0318 
0319     const auto addr = LocationUtil::address(location);
0320     if (!addr.isEmpty()) {
0321         url.setPath(QStringLiteral("0,0"));
0322         QUrlQuery query;
0323         query.addQueryItem(QStringLiteral("q"), toAddress(addr).formatted(KContacts::AddressFormatStyle::GeoUriQuery));
0324         url.setQuery(query);
0325         return url;
0326     }
0327 
0328     return {};
0329 }