File indexing completed on 2025-02-02 05:02:38
0001 /* 0002 SPDX-FileCopyrightText: 2019 Volker Krause <vkrause@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "transfermanager.h" 0008 0009 #include "constants.h" 0010 #include "jsonio.h" 0011 #include "logging.h" 0012 #include "favoritelocationmodel.h" 0013 #include "livedatamanager.h" 0014 #include "publictransport.h" 0015 #include "reservationhelper.h" 0016 #include "reservationmanager.h" 0017 #include "tripgroup.h" 0018 #include "tripgroupmanager.h" 0019 0020 #include <KItinerary/BoatTrip> 0021 #include <KItinerary/BusTrip> 0022 #include <KItinerary/Event> 0023 #include <KItinerary/Flight> 0024 #include <KItinerary/LocationUtil> 0025 #include <KItinerary/Reservation> 0026 #include <KItinerary/SortUtil> 0027 #include <KItinerary/TrainTrip> 0028 0029 #include <KPublicTransport/Journey> 0030 #include <KPublicTransport/JourneyReply> 0031 #include <KPublicTransport/Manager> 0032 0033 #include <KLocalizedString> 0034 0035 #include <QDir> 0036 #include <QFile> 0037 #include <QJsonObject> 0038 #include <QSettings> 0039 #include <QStandardPaths> 0040 0041 using namespace KItinerary; 0042 0043 // bump this to trigger a full rescan for transfers 0044 enum { CurrentFullScanVersion = 1 }; 0045 0046 TransferManager::TransferManager(QObject *parent) 0047 : QObject(parent) 0048 { 0049 } 0050 0051 TransferManager::~TransferManager() = default; 0052 0053 void TransferManager::setReservationManager(ReservationManager *resMgr) 0054 { 0055 m_resMgr = resMgr; 0056 connect(m_resMgr, &ReservationManager::batchAdded, this, qOverload<const QString&>(&TransferManager::checkReservation)); 0057 connect(m_resMgr, &ReservationManager::batchChanged, this, qOverload<const QString&>(&TransferManager::checkReservation)); 0058 connect(m_resMgr, &ReservationManager::batchRemoved, this, &TransferManager::reservationRemoved); 0059 rescan(); 0060 } 0061 0062 void TransferManager::setTripGroupManager(TripGroupManager* tgMgr) 0063 { 0064 m_tgMgr = tgMgr; 0065 connect(m_tgMgr, &TripGroupManager::tripGroupAdded, this, &TransferManager::tripGroupChanged); 0066 connect(m_tgMgr, &TripGroupManager::tripGroupChanged, this, &TransferManager::tripGroupChanged); 0067 rescan(); 0068 } 0069 0070 void TransferManager::setFavoriteLocationModel(FavoriteLocationModel *favLocModel) 0071 { 0072 m_favLocModel = favLocModel; 0073 connect(m_favLocModel, &FavoriteLocationModel::rowsInserted, this, [this]() { rescan(true); }); 0074 rescan(); 0075 } 0076 0077 void TransferManager::setLiveDataManager(LiveDataManager *liveDataMgr) 0078 { 0079 m_liveDataMgr = liveDataMgr; 0080 connect(m_liveDataMgr, &LiveDataManager::arrivalUpdated,this, [this](const QString &resId) { 0081 // update anchor time if we have a transfer for this 0082 auto t = transfer(resId, Transfer::After); 0083 if (t.state() == Transfer::Discarded || t.state() == Transfer::UndefinedState) { 0084 return; 0085 } 0086 0087 t.setAnchorTime(anchorTimeAfter(resId, m_resMgr->reservation(resId))); 0088 addOrUpdateTransfer(t); 0089 0090 // TODO if there's existing transfer, check if we miss this now 0091 // if so: warn and search for a new one if auto transfers are enabled 0092 }); 0093 rescan(); 0094 } 0095 0096 void TransferManager::setAutoAddTransfers(bool enable) 0097 { 0098 m_autoAddTransfers = enable; 0099 rescan(); 0100 } 0101 0102 void TransferManager::setAutoFillTransfers(bool enable) 0103 { 0104 m_autoFillTransfers = enable; 0105 } 0106 0107 Transfer TransferManager::transfer(const QString &resId, Transfer::Alignment alignment) const 0108 { 0109 const auto it = m_transfers[alignment].constFind(resId); 0110 if (it != m_transfers[alignment].constEnd()) { 0111 return it.value(); 0112 } 0113 0114 const auto t = readFromFile(resId, alignment); 0115 m_transfers[alignment].insert(resId, t); 0116 return t; 0117 } 0118 0119 void TransferManager::setJourneyForTransfer(Transfer transfer, const KPublicTransport::Journey &journey) 0120 { 0121 transfer.setState(Transfer::Selected); 0122 transfer.setJourney(journey); 0123 m_transfers[transfer.alignment()].insert(transfer.reservationId(), transfer); 0124 writeToFile(transfer); 0125 Q_EMIT transferChanged(transfer); 0126 } 0127 0128 Transfer TransferManager::setFavoriteLocationForTransfer(Transfer transfer, const FavoriteLocation& favoriteLocation) 0129 { 0130 if (transfer.floatingLocationType() != Transfer::FavoriteLocation) { 0131 qCWarning(Log) << "Attempting to changing transfer floating location of wrong type"; 0132 return transfer; 0133 } 0134 0135 KPublicTransport::Location loc; 0136 loc.setLatitude(favoriteLocation.latitude()); 0137 loc.setLongitude(favoriteLocation.longitude()); 0138 0139 if (transfer.alignment() == Transfer::Before) { 0140 transfer.setFrom(loc); 0141 transfer.setFromName(favoriteLocation.name()); 0142 } else { 0143 transfer.setTo(loc); 0144 transfer.setToName(favoriteLocation.name()); 0145 } 0146 transfer.setJourney({}); 0147 m_transfers[transfer.alignment()].insert(transfer.reservationId(), transfer); 0148 writeToFile(transfer); 0149 Q_EMIT transferChanged(transfer); 0150 0151 return transfer; 0152 } 0153 0154 void TransferManager::discardTransfer(Transfer transfer) 0155 { 0156 transfer.setState(Transfer::Discarded); 0157 transfer.setJourney({}); 0158 addOrUpdateTransfer(transfer); 0159 } 0160 0161 bool TransferManager::canAddTransfer(const QString& resId, Transfer::Alignment alignment) const 0162 { 0163 auto t = transfer(resId, alignment); 0164 if (t.state() == Transfer::Selected || t.state() == Transfer::Pending) { 0165 return false; // already exists 0166 } 0167 0168 const auto res = m_resMgr->reservation(resId); 0169 // in case it's new 0170 t.setReservationId(resId); 0171 t.setAlignment(alignment); 0172 0173 const bool canAdd = (alignment == Transfer::Before ? checkTransferBefore(resId, res, t) : checkTransferAfter(resId, res, t)) != ShouldRemove; 0174 return canAdd && t.hasLocations() && t.anchorTime().isValid(); 0175 } 0176 0177 Transfer TransferManager::addTransfer(const QString& resId, Transfer::Alignment alignment) 0178 { 0179 const auto res = m_resMgr->reservation(resId); 0180 0181 auto t = transfer(resId, alignment); 0182 // in case this is new 0183 t.setReservationId(resId); 0184 t.setAlignment(alignment); 0185 // in case this was previously discarded 0186 t.setState(Transfer::UndefinedState); 0187 determineAnchorDeltaDefault(t, res); 0188 0189 if ((alignment == Transfer::Before ? checkTransferBefore(resId, res, t) : checkTransferAfter(resId, res, t)) != ShouldRemove) { 0190 addOrUpdateTransfer(t); 0191 return t; 0192 } else { 0193 return {}; 0194 } 0195 } 0196 0197 void TransferManager::rescan(bool force) 0198 { 0199 if (!m_resMgr || !m_tgMgr || !m_favLocModel || !m_autoAddTransfers || !m_liveDataMgr) { 0200 return; 0201 } 0202 0203 QSettings settings; 0204 settings.beginGroup(QStringLiteral("TransferManager")); 0205 const auto previousFullScanVersion = settings.value(QLatin1StringView("FullScan"), 0).toInt(); 0206 if (!force && previousFullScanVersion >= CurrentFullScanVersion) { 0207 return; 0208 } 0209 0210 qCInfo(Log) << "Performing a full transfer search..." << previousFullScanVersion; 0211 for (const auto &batchId : m_resMgr->batches()) { 0212 checkReservation(batchId); 0213 } 0214 settings.setValue(QStringLiteral("FullScan"), CurrentFullScanVersion); 0215 } 0216 0217 void TransferManager::checkReservation(const QString &resId) 0218 { 0219 if (!m_autoAddTransfers) { 0220 return; 0221 } 0222 0223 const auto res = m_resMgr->reservation(resId); 0224 0225 const auto now = currentDateTime(); 0226 if (anchorTimeAfter(resId, res) < now) { 0227 return; 0228 } 0229 checkReservation(resId, res, Transfer::After); 0230 if (anchorTimeBefore(resId, res) < now) { 0231 return; 0232 } 0233 checkReservation(resId, res, Transfer::Before); 0234 } 0235 0236 void TransferManager::checkReservation(const QString &resId, const QVariant &res, Transfer::Alignment alignment) 0237 { 0238 auto t = transfer(resId, alignment); 0239 if (t.state() == Transfer::Discarded) { // user already discarded this 0240 return; 0241 } 0242 0243 // in case this is new 0244 t.setReservationId(resId); 0245 t.setAlignment(alignment); 0246 determineAnchorDeltaDefault(t, res); 0247 0248 const auto action = alignment == Transfer::Before ? checkTransferBefore(resId, res, t) : checkTransferAfter(resId, res, t); 0249 switch (action) { 0250 case ShouldAutoAdd: 0251 addOrUpdateTransfer(t); 0252 break; 0253 case CanAddManually: 0254 break; 0255 case ShouldRemove: 0256 removeTransfer(t); 0257 break; 0258 } 0259 } 0260 0261 /** Checks whether @p loc1 and @p loc2 are far enough apart to need a tranfer, 0262 * with sufficient certainty. 0263 */ 0264 static bool isLikelyNotSameLocation(const QVariant &loc1, const QVariant &loc2) 0265 { 0266 // if we are sure they are the same location we are done here 0267 if (LocationUtil::isSameLocation(loc1, loc2, LocationUtil::WalkingDistance)) { 0268 return false; 0269 } 0270 0271 // if both have a geo coordinate we are also sure about both being different from the above check 0272 if (LocationUtil::geo(loc1).isValid() && LocationUtil::geo(loc2).isValid()) { 0273 return true; 0274 } 0275 0276 // if we have to rely on the name, only do that if we are really sure they are different 0277 return !LocationUtil::isSameLocation(loc1, loc2, LocationUtil::CityLevel); 0278 } 0279 0280 /** Check whether @p loc1 and @p loc2 have a realistic distance for a transfer, 0281 * assuming we know their geo coodinates. 0282 * This helps filtering out non-sense transfers if we end up with entries in the wrong order. 0283 */ 0284 static bool isPlausibleDistance(const QVariant &loc1, const QVariant &loc2) 0285 { 0286 const auto geo1 = LocationUtil::geo(loc1); 0287 const auto geo2 = LocationUtil::geo(loc2); 0288 if (!geo1.isValid() || !geo2.isValid()) { 0289 return true; 0290 } 0291 return LocationUtil::distance(geo1, geo2) < 100'000; 0292 } 0293 0294 TransferManager::CheckTransferResult TransferManager::checkTransferBefore(const QString &resId, const QVariant &res, Transfer &transfer) const 0295 { 0296 if (ReservationHelper::isCancelled(res) || !SortUtil::hasStartTime(res)) { 0297 return ShouldRemove; 0298 } 0299 0300 transfer.setAnchorTime(anchorTimeBefore(resId, res)); 0301 const auto isLocationChange = LocationUtil::isLocationChange(res); 0302 QVariant toLoc; 0303 if (isLocationChange) { 0304 toLoc = LocationUtil::departureLocation(res); 0305 } else { 0306 toLoc = LocationUtil::location(res); 0307 } 0308 transfer.setTo(PublicTransport::locationFromPlace(toLoc, res)); 0309 transfer.setToName(LocationUtil::name(toLoc)); 0310 0311 // TODO pre-transfers should happen in the following cases: 0312 // - res is a location change and we are currently at home (== first element in a trip group) 0313 // - res is a location change and we are not at the departure location yet 0314 // - res is an event and we are not at its location already 0315 // ... and can happen in the following cases: 0316 // - res is not in a trip group at all (that assumes we are at home) 0317 // - res is a location change, and the previous element is also a location change but not a connection 0318 // (ie. transfer from favorite location at the destination of a roundtrip trip group) 0319 0320 0321 const auto notInGroup = isNotInTripGroup(resId); 0322 if ((isLocationChange && isFirstInTripGroup(resId)) || notInGroup) { 0323 const auto f = pickFavorite(toLoc, resId, Transfer::Before); 0324 transfer.setFrom(locationFromFavorite(f)); 0325 transfer.setFromName(f.name()); 0326 transfer.setFloatingLocationType(Transfer::FavoriteLocation); 0327 return notInGroup ? CanAddManually : ShouldAutoAdd; 0328 } 0329 0330 // find the first preceeding non-cancelled reservation 0331 QString prevResId = resId; 0332 QVariant prevRes; 0333 while (true) { 0334 prevResId = m_resMgr->previousBatch(prevResId); // TODO this fails for multiple nested range elements! 0335 if (prevResId.isEmpty()) { 0336 return ShouldRemove; 0337 } 0338 prevRes = m_resMgr->reservation(prevResId); 0339 if (!ReservationHelper::isCancelled(prevRes)) { 0340 break; 0341 } 0342 } 0343 0344 // check if there is a transfer after prevRes already 0345 const auto prevTransfer = this->transfer(prevResId, Transfer::After); 0346 if (prevTransfer.state() != Transfer::UndefinedState && prevTransfer.state() != Transfer::Discarded) { 0347 if (prevTransfer.floatingLocationType() == Transfer::FavoriteLocation) { 0348 transfer.setFrom(prevTransfer.to()); 0349 transfer.setFromName(prevTransfer.toName()); 0350 transfer.setFloatingLocationType(Transfer::FavoriteLocation); 0351 return CanAddManually; 0352 } 0353 return ShouldRemove; 0354 } 0355 0356 QVariant prevLoc; 0357 if (LocationUtil::isLocationChange(prevRes)) { 0358 prevLoc = LocationUtil::arrivalLocation(prevRes); 0359 } else { 0360 prevLoc = LocationUtil::location(prevRes); 0361 } 0362 if (!toLoc.isNull() && !prevLoc.isNull() && isLikelyNotSameLocation(toLoc, prevLoc) && isPlausibleDistance(toLoc, prevLoc)) { 0363 qDebug() << res << prevRes << LocationUtil::name(toLoc) << LocationUtil::name(prevLoc) << transfer.anchorTime(); 0364 transfer.setFrom(PublicTransport::locationFromPlace(prevLoc, prevRes)); 0365 transfer.setFromName(LocationUtil::name(prevLoc)); 0366 transfer.setFloatingLocationType(Transfer::Reservation); 0367 return isLocationChange ? ShouldAutoAdd : CanAddManually; 0368 } 0369 0370 // transfer to favorite at destination of a roundtrip trip group 0371 if (LocationUtil::isLocationChange(res) && LocationUtil::isLocationChange(prevRes) && LocationUtil::isSameLocation(toLoc, prevLoc)) { 0372 const auto arrivalTime = SortUtil::endDateTime(prevRes); 0373 const auto departureTime = SortUtil::startDateTime(res); 0374 transfer.setFloatingLocationType(Transfer::FavoriteLocation); 0375 const auto f = pickFavorite(toLoc, resId, Transfer::Before); 0376 transfer.setFrom(locationFromFavorite(f)); 0377 transfer.setFromName(f.name()); 0378 return std::chrono::seconds(arrivalTime.secsTo(departureTime)) < Constants::MaximumLayoverTime ? ShouldRemove : CanAddManually; 0379 } 0380 0381 return ShouldRemove; 0382 } 0383 0384 TransferManager::CheckTransferResult TransferManager::checkTransferAfter(const QString &resId, const QVariant &res, Transfer &transfer) const 0385 { 0386 if (ReservationHelper::isCancelled(res) || !SortUtil::hasEndTime(res)) { 0387 return ShouldRemove; 0388 } 0389 0390 transfer.setAnchorTime(anchorTimeAfter(resId, res)); 0391 const auto isLocationChange = LocationUtil::isLocationChange(res); 0392 QVariant fromLoc; 0393 if (isLocationChange) { 0394 fromLoc = LocationUtil::arrivalLocation(res); 0395 } else { 0396 fromLoc = LocationUtil::location(res); 0397 } 0398 transfer.setFrom(PublicTransport::locationFromPlace(fromLoc, res)); 0399 transfer.setFromName(LocationUtil::name(fromLoc)); 0400 0401 // TODO post-transfer should happen in the following cases: 0402 // - res is a location change and we are the last element in a trip group (ie. going home) 0403 // - res is a location change and the following element is in a different location, or has a different departure location 0404 // - res is an event and the following or enclosing element is a lodging element 0405 // ... and can happen in the following cases 0406 // - res is not in a trip group at all (that assumes we are at home) 0407 // - res is a location change, and the subsequent element is also a location change but not a connection 0408 // (ie. transfer to favorite location at the destination of a roundtrip trip group) 0409 0410 const auto notInGroup = isNotInTripGroup(resId); 0411 if ((isLocationChange && isLastInTripGroup(resId)) || notInGroup) { 0412 const auto f = pickFavorite(fromLoc, resId, Transfer::After); 0413 transfer.setTo(locationFromFavorite(f)); 0414 transfer.setToName(f.name()); 0415 transfer.setFloatingLocationType(Transfer::FavoriteLocation); 0416 return notInGroup ? CanAddManually : ShouldAutoAdd; 0417 } 0418 0419 // find next non-cancelled reservation 0420 QString nextResId = resId; 0421 QVariant nextRes; 0422 while (true) { 0423 nextResId = m_resMgr->nextBatch(nextResId); 0424 if (nextResId.isEmpty()) { 0425 return ShouldRemove; 0426 } 0427 nextRes = m_resMgr->reservation(nextResId); 0428 if (!ReservationHelper::isCancelled(nextRes)) { 0429 break; 0430 } 0431 } 0432 0433 // check if there is a transfer before nextRes already 0434 const auto nextTransfer = this->transfer(nextResId, Transfer::Before); 0435 if (nextTransfer.state() != Transfer::UndefinedState && nextTransfer.state() != Transfer::Discarded) { 0436 if (nextTransfer.floatingLocationType() == Transfer::FavoriteLocation) { 0437 transfer.setTo(nextTransfer.from()); 0438 transfer.setToName(nextTransfer.fromName()); 0439 transfer.setFloatingLocationType(Transfer::FavoriteLocation); 0440 return CanAddManually; 0441 } 0442 return ShouldRemove; 0443 } 0444 0445 QVariant nextLoc; 0446 if (LocationUtil::isLocationChange(nextRes)) { 0447 nextLoc = LocationUtil::departureLocation(nextRes); 0448 } else { 0449 nextLoc = LocationUtil::location(nextRes); 0450 } 0451 if (!fromLoc.isNull() && !nextLoc.isNull() && isLikelyNotSameLocation(fromLoc, nextLoc) && isPlausibleDistance(fromLoc, nextLoc)) { 0452 qDebug() << res << nextRes << LocationUtil::name(fromLoc) << LocationUtil::name(nextLoc) << transfer.anchorTime(); 0453 transfer.setTo(PublicTransport::locationFromPlace(nextLoc, nextRes)); 0454 transfer.setToName(LocationUtil::name(nextLoc)); 0455 transfer.setFloatingLocationType(Transfer::Reservation); 0456 return isLocationChange ? ShouldAutoAdd : CanAddManually; 0457 } 0458 0459 // transfer to favorite at destination of a roundtrip trip group 0460 if (LocationUtil::isLocationChange(res) && LocationUtil::isLocationChange(nextRes) && LocationUtil::isSameLocation(fromLoc, nextLoc)) { 0461 const auto arrivalTime = SortUtil::endDateTime(res); 0462 const auto departureTime = SortUtil::startDateTime(nextRes); 0463 transfer.setFloatingLocationType(Transfer::FavoriteLocation); 0464 const auto f = pickFavorite(fromLoc, resId, Transfer::After); 0465 transfer.setTo(locationFromFavorite(f)); 0466 transfer.setToName(f.name()); 0467 return std::chrono::seconds(arrivalTime.secsTo(departureTime)) < Constants::MaximumLayoverTime ? ShouldRemove : CanAddManually; 0468 } 0469 0470 return ShouldRemove; 0471 } 0472 0473 void TransferManager::reservationRemoved(const QString &resId) 0474 { 0475 m_transfers[Transfer::Before].remove(resId); 0476 m_transfers[Transfer::After].remove(resId); 0477 removeFile(resId, Transfer::Before); 0478 removeFile(resId, Transfer::After); 0479 // TODO updates to adjacent transfers? 0480 Q_EMIT transferRemoved(resId, Transfer::Before); 0481 Q_EMIT transferRemoved(resId, Transfer::After); 0482 } 0483 0484 void TransferManager::tripGroupChanged(const QString &tgId) 0485 { 0486 const auto tg = m_tgMgr->tripGroup(tgId); 0487 for (const auto &resId : tg.elements()) { 0488 checkReservation(resId); 0489 } 0490 } 0491 0492 bool TransferManager::isFirstInTripGroup(const QString &resId) const 0493 { 0494 const auto tgId = m_tgMgr->tripGroupForReservation(resId); 0495 return tgId.elements().empty() ? false : tgId.elements().at(0) == resId; 0496 } 0497 0498 bool TransferManager::isLastInTripGroup(const QString &resId) const 0499 { 0500 const auto tgId = m_tgMgr->tripGroupForReservation(resId); 0501 return tgId.elements().empty() ? false : tgId.elements().constLast() == resId; 0502 } 0503 0504 bool TransferManager::isNotInTripGroup(const QString &resId) const 0505 { 0506 return m_tgMgr->tripGroupIdForReservation(resId).isEmpty(); 0507 } 0508 0509 // default transfer anchor deltas (in minutes) 0510 enum { FlightDelta, TrainDelta, BusDelta, BoatDelta, RestaurantDelta, FallbackDelta }; 0511 static constexpr const int default_deltas[][2] = { 0512 { 90, 30 }, // Flight 0513 { 20, 10 }, // Train 0514 { 15, 10 }, // Bus 0515 { 60, 30 }, // Boat/Ferry 0516 { 5, 5 }, // Restaurant 0517 { 30, 15 }, // anything else 0518 }; 0519 0520 void TransferManager::determineAnchorDeltaDefault(Transfer &transfer, const QVariant &res) const 0521 { 0522 if (transfer.state() != Transfer::UndefinedState) { 0523 return; 0524 } 0525 0526 int delta; 0527 if (JsonLd::isA<FlightReservation>(res)) { 0528 delta = default_deltas[FlightDelta][transfer.alignment()]; 0529 } else if (JsonLd::isA<TrainReservation>(res)) { 0530 delta = default_deltas[TrainDelta][transfer.alignment()]; 0531 } else if (JsonLd::isA<BusReservation>(res)) { 0532 delta = default_deltas[BusDelta][transfer.alignment()]; 0533 } else if (JsonLd::isA<BoatReservation>(res)) { 0534 delta = default_deltas[BoatDelta][transfer.alignment()]; 0535 } else if (JsonLd::isA<FoodEstablishmentReservation>(res)) { 0536 delta = default_deltas[RestaurantDelta][transfer.alignment()]; 0537 } else { 0538 delta = default_deltas[FallbackDelta][transfer.alignment()]; 0539 } 0540 transfer.setAnchorTimeDelta(delta * 60); 0541 } 0542 0543 QDateTime TransferManager::anchorTimeBefore(const QString &resId, const QVariant &res) const 0544 { 0545 if (JsonLd::isA<TrainReservation>(res)) { 0546 const auto departure = m_liveDataMgr->departure(resId); 0547 if (departure.hasExpectedDepartureTime()) { 0548 return departure.expectedDepartureTime(); 0549 } 0550 } 0551 if (JsonLd::isA<FlightReservation>(res)) { 0552 const auto flight = res.value<FlightReservation>().reservationFor().value<Flight>(); 0553 if (flight.boardingTime().isValid()) { 0554 return flight.boardingTime(); 0555 } 0556 } 0557 if (LocationUtil::isLocationChange(res)) { 0558 return SortUtil::startDateTime(res); 0559 } 0560 0561 if (JsonLd::isA<EventReservation>(res)) { 0562 const auto event = res.value<EventReservation>().reservationFor().value<Event>(); 0563 if (event.doorTime().isValid()) { 0564 return event.doorTime(); 0565 } 0566 return event.startDate(); 0567 } 0568 if (JsonLd::isA<FoodEstablishmentReservation>(res)) { 0569 return res.value<FoodEstablishmentReservation>().startTime(); 0570 } 0571 0572 return {}; 0573 } 0574 0575 QDateTime TransferManager::anchorTimeAfter(const QString &resId, const QVariant &res) const 0576 { 0577 if (JsonLd::isA<TrainReservation>(res)) { 0578 const auto arrival = m_liveDataMgr->arrival(resId); 0579 if (arrival.hasExpectedArrivalTime()) { 0580 return arrival.expectedArrivalTime(); 0581 } 0582 } 0583 if (LocationUtil::isLocationChange(res)) { 0584 return SortUtil::endDateTime(res); 0585 } 0586 0587 if (JsonLd::isA<EventReservation>(res)) { 0588 return res.value<EventReservation>().reservationFor().value<Event>().endDate(); 0589 } 0590 if (JsonLd::isA<FoodEstablishmentReservation>(res)) { 0591 return res.value<FoodEstablishmentReservation>().endTime(); 0592 } 0593 0594 return {}; 0595 } 0596 0597 KPublicTransport::Location TransferManager::locationFromFavorite(const FavoriteLocation &favLoc) 0598 { 0599 KPublicTransport::Location loc; 0600 loc.setLatitude(favLoc.latitude()); 0601 loc.setLongitude(favLoc.longitude()); 0602 return loc; 0603 } 0604 0605 FavoriteLocation TransferManager::pickFavorite(const QVariant &anchoredLoc, const QString &resId, Transfer::Alignment alignment) const 0606 { 0607 const auto &favLocs = m_favLocModel->favoriteLocations(); 0608 if (favLocs.empty()) { 0609 return {}; 0610 } 0611 0612 // TODO selection strategy: 0613 // (1) pick the same favorite as was used before/after resId 0614 // (2) pick the favorite closest to anchoredLoc - this can work very well if the favorites aren't close to each other 0615 // (3) pick the first one 0616 0617 Q_UNUSED(resId) 0618 Q_UNUSED(alignment) 0619 0620 // pick the first location within a 50km distance 0621 const auto anchordCoord = LocationUtil::geo(anchoredLoc); 0622 if (!anchordCoord.isValid()) { 0623 return {}; 0624 } 0625 const auto it = std::find_if(favLocs.begin(), favLocs.end(), [&anchordCoord](const auto &fav) { 0626 const auto d = LocationUtil::distance(anchordCoord.latitude(), anchordCoord.longitude(), fav.latitude(), fav.longitude()); 0627 return d < 50'000; 0628 }); 0629 if (it != favLocs.end()) { 0630 return (*it); 0631 } 0632 return {}; 0633 } 0634 0635 void TransferManager::addOrUpdateTransfer(Transfer &t) 0636 { 0637 if (t.state() == Transfer::UndefinedState) { // newly added 0638 if (!t.hasLocations()) { // undefined home location 0639 return; 0640 } 0641 t.setState(Transfer::Pending); 0642 autoFillTransfer(t); 0643 m_transfers[t.alignment()].insert(t.reservationId(), t); 0644 writeToFile(t); 0645 Q_EMIT transferAdded(t); 0646 } else if (t.state() == Transfer::Discarded) { 0647 m_transfers[t.alignment()].insert(t.reservationId(), t); 0648 writeToFile(t); 0649 Q_EMIT transferRemoved(t.reservationId(), t.alignment()); 0650 } else { // update existing data 0651 m_transfers[t.alignment()].insert(t.reservationId(), t); 0652 writeToFile(t); 0653 Q_EMIT transferChanged(t); 0654 } 0655 } 0656 0657 void TransferManager::removeTransfer(const Transfer &t) 0658 { 0659 if (t.state() == Transfer::UndefinedState) { // this was never added 0660 return; 0661 } 0662 m_transfers[t.alignment()].remove(t.reservationId()); 0663 removeFile(t.reservationId(), t.alignment()); 0664 Q_EMIT transferRemoved(t.reservationId(), t.alignment()); 0665 } 0666 0667 static QString transferBasePath() 0668 { 0669 return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1StringView("/transfers/"); 0670 } 0671 0672 Transfer TransferManager::readFromFile(const QString& resId, Transfer::Alignment alignment) const 0673 { 0674 const QString fileName = transferBasePath() + Transfer::identifier(resId, alignment) + QLatin1StringView(".json"); 0675 QFile f(fileName); 0676 if (!f.open(QFile::ReadOnly)) { 0677 return {}; 0678 } 0679 return Transfer::fromJson(JsonIO::read(f.readAll()).toObject()); 0680 } 0681 0682 void TransferManager::writeToFile(const Transfer &transfer) const 0683 { 0684 QDir().mkpath(transferBasePath()); 0685 const QString fileName = transferBasePath() + transfer.identifier() + QLatin1StringView(".json"); 0686 QFile f(fileName); 0687 if (!f.open(QFile::WriteOnly)) { 0688 qCWarning(Log) << "Failed to store transfer data" << f.fileName() << f.errorString(); 0689 return; 0690 } 0691 f.write(JsonIO::write(Transfer::toJson(transfer))); 0692 } 0693 0694 void TransferManager::removeFile(const QString &resId, Transfer::Alignment alignment) const 0695 { 0696 const QString fileName = transferBasePath() + Transfer::identifier(resId, alignment) + QLatin1StringView(".json"); 0697 QFile::remove(fileName); 0698 } 0699 0700 void TransferManager::importTransfer(const Transfer &transfer) 0701 { 0702 if (transfer.state() == Transfer::UndefinedState) { 0703 return; 0704 } 0705 0706 const bool update = m_transfers[transfer.alignment()].contains(transfer.reservationId()); 0707 m_transfers[transfer.alignment()].insert(transfer.reservationId(), transfer); 0708 writeToFile(transfer); 0709 0710 update ? Q_EMIT transferChanged(transfer) : Q_EMIT transferAdded(transfer); 0711 } 0712 0713 KPublicTransport::JourneyRequest TransferManager::journeyRequestForTransfer(const Transfer &transfer) const 0714 { 0715 using namespace KPublicTransport; 0716 JourneyRequest req; 0717 req.setFrom(transfer.from()); 0718 req.setTo(transfer.to()); 0719 req.setDateTime(transfer.journeyTime()); 0720 req.setDateTimeMode(transfer.alignment() == Transfer::Before ? JourneyRequest::Arrival : JourneyRequest::Departure); 0721 req.setDownloadAssets(true); 0722 req.setIncludeIntermediateStops(true); 0723 req.setIncludePaths(true); 0724 req.setMaximumResults(6); 0725 return req; 0726 } 0727 0728 static KPublicTransport::Journey pickJourney(const Transfer &t, const std::vector<KPublicTransport::Journey> &journeys) 0729 { 0730 if (journeys.empty()) { 0731 return {}; 0732 } 0733 return t.alignment() == Transfer::Before ? journeys.back() : journeys.front(); 0734 } 0735 0736 void TransferManager::autoFillTransfer(Transfer &t) 0737 { 0738 if (!m_autoFillTransfers || t.state() != Transfer::Pending || !t.hasLocations()) { 0739 return; 0740 } 0741 0742 t.setState(Transfer::Searching); 0743 0744 auto reply = m_liveDataMgr->publicTransportManager()->queryJourney(journeyRequestForTransfer(t)); 0745 const auto batchId = t.reservationId(); 0746 const auto alignment = t.alignment(); 0747 connect(reply, &KPublicTransport::JourneyReply::finished, this, [this, reply, batchId, alignment]() { 0748 reply->deleteLater(); 0749 auto t = transfer(batchId, alignment); 0750 if (t.state() != Transfer::Searching) { // user override happened meanwhile 0751 qDebug() << "ignoring journey reply, transfer state changed"; 0752 return; 0753 } 0754 0755 if (reply->error() != KPublicTransport::JourneyReply::NoError) { 0756 qDebug() << reply->errorString(); 0757 t.setState(reply->error() == KPublicTransport::JourneyReply::NotFoundError ? Transfer::Discarded : Transfer::Pending); 0758 } 0759 0760 const auto journeys = std::move(reply->takeResult()); 0761 if (journeys.empty() && t.state() == Transfer::Searching) { 0762 qDebug() << "no journeys found for transfer, discarding"; 0763 t.setState(Transfer::Discarded); 0764 } 0765 0766 const auto journey = pickJourney(t, journeys); 0767 if (journey.scheduledArrivalTime().isValid()) { 0768 t.setJourney(journey); 0769 t.setState(Transfer::Selected); 0770 } else if (t.state() == Transfer::Searching) { 0771 t.setState(Transfer::Pending); 0772 } 0773 addOrUpdateTransfer(t); 0774 }); 0775 } 0776 0777 QDateTime TransferManager::currentDateTime() const 0778 { 0779 if (Q_UNLIKELY(m_nowOverride.isValid())) { 0780 return m_nowOverride; 0781 } 0782 return QDateTime::currentDateTime(); 0783 } 0784 0785 void TransferManager::overrideCurrentDateTime(const QDateTime &dt) 0786 { 0787 m_nowOverride = dt; 0788 } 0789 0790 void TransferManager::clear() 0791 { 0792 QDir d(transferBasePath()); 0793 qCInfo(Log) << "deleting" << transferBasePath(); 0794 d.removeRecursively(); 0795 0796 QSettings settings; 0797 settings.beginGroup(QStringLiteral("TransferManager")); 0798 settings.remove(QStringLiteral("FullScan")); 0799 } 0800 0801 #include "moc_transfermanager.cpp"