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"