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 }