File indexing completed on 2025-02-02 05:02:31

0001 /*
0002     SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "livedatamanager.h"
0008 #include "logging.h"
0009 #include "notificationhelper.h"
0010 #include "pkpassmanager.h"
0011 #include "reservationhelper.h"
0012 #include "reservationmanager.h"
0013 #include "publictransport.h"
0014 #include "publictransportmatcher.h"
0015 
0016 #include <KItinerary/BusTrip>
0017 #include <KItinerary/Flight>
0018 #include <KItinerary/LocationUtil>
0019 #include <KItinerary/Place>
0020 #include <KItinerary/Reservation>
0021 #include <KItinerary/SortUtil>
0022 #include <KItinerary/TrainTrip>
0023 
0024 #include <KPublicTransport/JourneyReply>
0025 #include <KPublicTransport/JourneyRequest>
0026 #include <KPublicTransport/Location>
0027 #include <KPublicTransport/Manager>
0028 #include <KPublicTransport/OnboardStatus>
0029 #include <KPublicTransport/StopoverReply>
0030 #include <KPublicTransport/StopoverRequest>
0031 
0032 #include <KPkPass/Pass>
0033 
0034 #include <KLocalizedString>
0035 #include <KNotification>
0036 
0037 #include <QDir>
0038 #include <QDirIterator>
0039 #include <QFile>
0040 #include <QJsonDocument>
0041 #include <QJsonObject>
0042 #include <QSettings>
0043 #include <QStandardPaths>
0044 #include <QVector>
0045 
0046 #include <cassert>
0047 
0048 using namespace KItinerary;
0049 
0050 static constexpr const int POLL_COOLDOWN_ON_ERROR = 30; // seconds
0051 
0052 LiveDataManager::LiveDataManager(QObject *parent)
0053     : QObject(parent)
0054     , m_ptMgr(new KPublicTransport::Manager(this))
0055     , m_onboardStatus(new KPublicTransport::OnboardStatus(this))
0056 {
0057     QSettings settings;
0058     settings.beginGroup(QLatin1StringView("KPublicTransport"));
0059     m_ptMgr->setAllowInsecureBackends(settings.value(QLatin1StringView("AllowInsecureBackends"), false).toBool());
0060     m_ptMgr->setDisabledBackends(settings.value(QLatin1StringView("DisabledBackends"), QStringList()).toStringList());
0061     m_ptMgr->setEnabledBackends(settings.value(QLatin1StringView("EnabledBackends"), QStringList()).toStringList());
0062     connect(m_ptMgr, &KPublicTransport::Manager::configurationChanged, this, [this]() {
0063         QSettings settings;
0064         settings.beginGroup(QLatin1StringView("KPublicTransport"));
0065         settings.setValue(QLatin1StringView("AllowInsecureBackends"), m_ptMgr->allowInsecureBackends());
0066         settings.setValue(QLatin1StringView("DisabledBackends"), m_ptMgr->disabledBackends());
0067         settings.setValue(QLatin1StringView("EnabledBackends"), m_ptMgr->enabledBackends());
0068     });
0069 
0070     m_pollTimer.setSingleShot(true);
0071     connect(&m_pollTimer, &QTimer::timeout, this, &LiveDataManager::poll);
0072 
0073     connect(m_onboardStatus, &KPublicTransport::OnboardStatus::journeyChanged, [this]() {
0074         if (!m_onboardStatus->hasJourney()) {
0075             return;
0076         }
0077 
0078         for (const auto &resId : m_reservations) {
0079             auto res = m_resMgr->reservation(resId);
0080             if (!hasDeparted(resId, res) || hasArrived(resId, res)) {
0081                 continue;
0082             }
0083             const auto journey = m_onboardStatus->journey();
0084             if (journey.sections().empty()) {
0085                 return;
0086             }
0087             auto subjny = PublicTransportMatcher::subJourneyForReservation(res, journey.sections()[0]);
0088             if (subjny.mode() == KPublicTransport::JourneySection::Invalid) {
0089                 return;
0090             }
0091 
0092             updateJourneyData(subjny, resId, res);
0093         }
0094     });
0095 }
0096 
0097 LiveDataManager::~LiveDataManager() = default;
0098 
0099 void LiveDataManager::setReservationManager(ReservationManager *resMgr)
0100 {
0101     assert(m_pkPassMgr);
0102     m_resMgr = resMgr;
0103     connect(resMgr, &ReservationManager::batchAdded, this, &LiveDataManager::batchAdded, Qt::QueuedConnection);
0104     connect(resMgr, &ReservationManager::batchChanged, this, &LiveDataManager::batchChanged);
0105     connect(resMgr, &ReservationManager::batchContentChanged, this, &LiveDataManager::batchChanged);
0106     connect(resMgr, &ReservationManager::batchRenamed, this, &LiveDataManager::batchRenamed);
0107     connect(resMgr, &ReservationManager::batchRemoved, this, &LiveDataManager::batchRemoved);
0108 
0109     const auto resIds = resMgr->batches();
0110     for (const auto &resId : resIds) {
0111         if (!isRelevant(resId)) {
0112             continue;
0113         }
0114         m_reservations.push_back(resId);
0115     }
0116 
0117     m_pollTimer.setInterval(nextPollTime());
0118 }
0119 
0120 void LiveDataManager::setPkPassManager(PkPassManager *pkPassMgr)
0121 {
0122     m_pkPassMgr = pkPassMgr;
0123     connect(m_pkPassMgr, &PkPassManager::passUpdated, this, &LiveDataManager::pkPassUpdated);
0124 }
0125 
0126 void LiveDataManager::setPollingEnabled(bool pollingEnabled)
0127 {
0128     if (pollingEnabled) {
0129         m_pollTimer.setInterval(nextPollTime());
0130         m_pollTimer.start();
0131     } else {
0132         m_pollTimer.stop();
0133     }
0134 }
0135 
0136 void LiveDataManager::setShowNotificationsOnLockScreen(bool enabled)
0137 {
0138     m_showNotificationsOnLockScreen = enabled;
0139 }
0140 
0141 KPublicTransport::Stopover LiveDataManager::arrival(const QString &resId) const
0142 {
0143     return data(resId).arrival;
0144 }
0145 
0146 KPublicTransport::Stopover LiveDataManager::departure(const QString &resId) const
0147 {
0148     return data(resId).departure;
0149 }
0150 
0151 KPublicTransport::JourneySection LiveDataManager::journey(const QString &resId) const
0152 {
0153     return data(resId).journey;
0154 }
0155 
0156 void LiveDataManager::setJourney(const QString &resId, const KPublicTransport::JourneySection &journey)
0157 {
0158     auto &ld = data(resId);
0159     ld.journey = journey;
0160     ld.journeyTimestamp = now();
0161     ld.departure = journey.departure();
0162     ld.departureTimestamp = now();
0163     ld.arrival = journey.arrival();
0164     ld.arrivalTimestamp = now();
0165     ld.store(resId, LiveData::AllTypes);
0166 
0167     Q_EMIT journeyUpdated(resId);
0168     Q_EMIT departureUpdated(resId);
0169     Q_EMIT arrivalUpdated(resId);
0170 }
0171 
0172 void LiveDataManager::applyJourney(const QString &resId, const KPublicTransport::JourneySection &journey)
0173 {
0174     updateJourneyData(journey, resId, m_resMgr->reservation(resId));
0175 }
0176 
0177 void LiveDataManager::checkForUpdates()
0178 {
0179     pollForUpdates(true);
0180 }
0181 
0182 void LiveDataManager::checkReservation(const QVariant &res, const QString& resId)
0183 {
0184     using namespace KPublicTransport;
0185     const auto arrived = hasArrived(resId, res);
0186 
0187     // load full journey if we don't have one yet
0188     if (!arrived && data(resId).journey.mode() == JourneySection::Invalid) {
0189         const auto from = PublicTransport::locationFromPlace(LocationUtil::departureLocation(res), res);
0190         const auto to = PublicTransport::locationFromPlace(LocationUtil::arrivalLocation(res), res);
0191         JourneyRequest req(from, to);
0192         // start searching slightly earlier, so leading walking section because our coordinates
0193         // aren't exactly at the right spot wont make the routing service consider the train we
0194         // are looking for as impossible to reach on time
0195         req.setDateTime(SortUtil::startDateTime(res).addSecs(-600));
0196         req.setDateTimeMode(JourneyRequest::Departure);
0197         req.setIncludeIntermediateStops(true);
0198         req.setIncludePaths(true);
0199         req.setModes(JourneySection::PublicTransport);
0200         PublicTransport::selectBackends(req, m_ptMgr, res);
0201         auto reply = m_ptMgr->queryJourney(req);
0202         connect(reply, &Reply::finished, this, [this, resId, reply]() { journeyQueryFinished(reply, resId); });
0203         m_lastPollAttempt.insert(resId, now());
0204         return;
0205     }
0206 
0207     if (!hasDeparted(resId, res)) {
0208         StopoverRequest req(PublicTransport::locationFromPlace(LocationUtil::departureLocation(res), res));
0209         req.setMode(StopoverRequest::QueryDeparture);
0210         req.setDateTime(SortUtil::startDateTime(res));
0211         PublicTransport::selectBackends(req, m_ptMgr, res);
0212         auto reply = m_ptMgr->queryStopover(req);
0213         connect(reply, &Reply::finished, this, [this, resId, reply]() { stopoverQueryFinished(reply, LiveData::Departure, resId); });
0214         m_lastPollAttempt.insert(resId, now());
0215     }
0216 
0217     if (!arrived) {
0218         StopoverRequest req(PublicTransport::locationFromPlace(LocationUtil::arrivalLocation(res), res));
0219         req.setMode(StopoverRequest::QueryArrival);
0220         req.setDateTime(SortUtil::endDateTime(res));
0221         PublicTransport::selectBackends(req, m_ptMgr, res);
0222         auto reply = m_ptMgr->queryStopover(req);
0223         connect(reply, &Reply::finished, this, [this, resId, reply]() { stopoverQueryFinished(reply, LiveData::Arrival, resId); });
0224         m_lastPollAttempt.insert(resId, now());
0225     }
0226 }
0227 
0228 void LiveDataManager::stopoverQueryFinished(KPublicTransport::StopoverReply* reply, LiveData::Type type, const QString& resId)
0229 {
0230     reply->deleteLater();
0231     if (reply->error() != KPublicTransport::Reply::NoError) {
0232         qCDebug(Log) << reply->error() << reply->errorString();
0233         return;
0234     }
0235     stopoverQueryFinished(reply->takeResult(), type, resId);
0236 }
0237 
0238 void LiveDataManager::stopoverQueryFinished(std::vector<KPublicTransport::Stopover> &&result, LiveData::Type type, const QString& resId)
0239 {
0240     const auto res = m_resMgr->reservation(resId);
0241     for (const auto &stop : result) {
0242         qCDebug(Log) << "Got stopover information:" << stop.route().line().name() << stop.scheduledDepartureTime();
0243         if (type == LiveData::Arrival ? PublicTransportMatcher::isArrivalForReservation(res, stop) : PublicTransportMatcher::isDepartureForReservation(res, stop)) {
0244             qCDebug(Log) << "Found stopover information:" << stop.route().line().name() << stop.expectedPlatform() << stop.expectedDepartureTime();
0245             updateStopoverData(stop, type, resId, res);
0246             return;
0247         }
0248     }
0249 
0250     // record this is a failed lookup so we don't try again
0251     data(resId).setTimestamp(type, now());
0252 }
0253 
0254 void LiveDataManager::journeyQueryFinished(KPublicTransport::JourneyReply *reply, const QString &resId)
0255 {
0256     reply->deleteLater();
0257     if (reply->error() != KPublicTransport::Reply::NoError) {
0258         qCDebug(Log) << reply->error() << reply->errorString();
0259         return;
0260     }
0261 
0262     using namespace KPublicTransport;
0263     const auto res = m_resMgr->reservation(resId);
0264     for (const auto &journey : reply->result()) {
0265         if (std::count_if(journey.sections().begin(), journey.sections().end(), [](const auto &sec) { return sec.mode() == JourneySection::PublicTransport; }) != 1) {
0266             continue;
0267         }
0268         const auto it = std::find_if(journey.sections().begin(), journey.sections().end(), [](const auto &sec) { return sec.mode() == JourneySection::PublicTransport; });
0269         assert(it != journey.sections().end());
0270         qCDebug(Log) << "Got journey information:" << (*it).route().line().name() << (*it).scheduledDepartureTime();
0271         if (PublicTransportMatcher::isJourneyForReservation(res, (*it))) {
0272             qCDebug(Log) << "Found journey information:" << (*it).route().line().name() << (*it).expectedDeparturePlatform() << (*it).expectedDepartureTime();
0273             updateJourneyData((*it), resId, res);
0274             return;
0275         }
0276     }
0277 
0278     // record this is a failed lookup so we don't try again
0279     data(resId).setTimestamp(LiveData::Arrival, now());
0280     data(resId).setTimestamp(LiveData::Departure, now());
0281 }
0282 
0283 static KPublicTransport::Stopover applyLayoutData(const KPublicTransport::Stopover &stop, const KPublicTransport::Stopover &layout)
0284 {
0285     auto res = stop;
0286     if (stop.vehicleLayout().isEmpty()) {
0287         res.setVehicleLayout(layout.vehicleLayout());
0288     }
0289     if (stop.platformLayout().isEmpty()) {
0290         res.setPlatformLayout(layout.platformLayout());
0291     }
0292     return res;
0293 }
0294 
0295 static void applyMissingStopoverData(KPublicTransport::Stopover &stop, const KPublicTransport::Stopover &oldStop)
0296 {
0297     if (stop.notes().empty()) {
0298         stop.setNotes(oldStop.notes());
0299     }
0300     if (stop.loadInformation().empty()) {
0301         stop.setLoadInformation(std::vector<KPublicTransport::LoadInfo>(oldStop.loadInformation()));
0302     }
0303 }
0304 
0305 void LiveDataManager::updateStopoverData(const KPublicTransport::Stopover &stop, LiveData::Type type, const QString &resId, const QVariant &res)
0306 {
0307     auto &ld = data(resId);
0308     const auto oldStop = ld.stopover(type);
0309     auto newStop = stop;
0310     newStop.applyMetaData(true); // download logo assets if needed
0311 
0312     // retain already existing vehicle/platform layout data if we are still departing/arriving in the same place
0313     if (PublicTransport::isSameStopoverForLayout(newStop, oldStop)) {
0314         newStop = applyLayoutData(newStop, oldStop);
0315     }
0316     applyMissingStopoverData(newStop, oldStop);
0317 
0318     ld.setStopover(type, newStop);
0319     ld.setTimestamp(type, now());
0320     if (type == LiveData::Departure) {
0321         ld.journey.setDeparture(newStop);
0322     } else {
0323         ld.journey.setArrival(newStop);
0324     }
0325     ld.journeyTimestamp = now();
0326     ld.store(resId);
0327 
0328     // update reservation with live data
0329     const auto newRes = type == LiveData::Arrival ? PublicTransport::mergeArrival(res, newStop) : PublicTransport::mergeDeparture(res, newStop);
0330     if (!ReservationHelper::equals(res, newRes)) {
0331         m_resMgr->updateReservation(resId, newRes);
0332     }
0333 
0334     // emit update signals
0335     Q_EMIT type == LiveData::Arrival ? arrivalUpdated(resId) : departureUpdated(resId);
0336     Q_EMIT journeyUpdated(resId);
0337 
0338     // check if we need to notify
0339     if (NotificationHelper::shouldNotify(oldStop, newStop, type)) {
0340         showNotification(resId, ld);
0341     }
0342 }
0343 
0344 static void applyMissingJourneyData(KPublicTransport::JourneySection &journey, const KPublicTransport::JourneySection &oldJny)
0345 {
0346     if (journey.intermediateStops().size() != oldJny.intermediateStops().size()) {
0347         return;
0348     }
0349 
0350     auto stops = journey.takeIntermediateStops();
0351     for (std::size_t i = 0; i < stops.size(); ++i) {
0352         if (!KPublicTransport::Stopover::isSame(stops[i], oldJny.intermediateStops()[i])) {
0353             journey.setIntermediateStops(std::move(stops));
0354             return;
0355         }
0356         applyMissingStopoverData(stops[i], oldJny.intermediateStops()[i]);
0357     }
0358     journey.setIntermediateStops(std::move(stops));
0359 
0360     if (!KPublicTransport::Stopover::isSame(journey.departure(), oldJny.departure())
0361      || !KPublicTransport::Stopover::isSame(journey.arrival(), oldJny.arrival())) {
0362         return;
0363     }
0364     auto s = journey.departure();
0365     applyMissingStopoverData(s, oldJny.departure());
0366     journey.setDeparture(s);
0367     s = journey.arrival();
0368     applyMissingStopoverData(s, oldJny.arrival());
0369     journey.setArrival(s);
0370 
0371     if (journey.path().isEmpty()) {
0372         journey.setPath(oldJny.path());
0373     }
0374     if (journey.notes().empty()) {
0375         journey.setNotes(oldJny.notes());
0376     }
0377 }
0378 
0379 void LiveDataManager::updateJourneyData(const KPublicTransport::JourneySection &journey, const QString &resId, const QVariant &res)
0380 {
0381     auto &ld = data(resId);
0382     const auto oldDep = ld.stopover(LiveData::Departure);
0383     const auto oldArr = ld.stopover(LiveData::Arrival);
0384     const auto oldJny = ld.journey;
0385     ld.journey = journey;
0386 
0387     // retain already existing vehicle/platform layout data if we are still departing/arriving in the same place
0388     if (PublicTransport::isSameStopoverForLayout(ld.departure, journey.departure())) {
0389         ld.journey.setDeparture(applyLayoutData(journey.departure(), ld.departure));
0390     }
0391     if (PublicTransport::isSameStopoverForLayout(ld.arrival, journey.arrival())) {
0392         ld.journey.setArrival(applyLayoutData(journey.arrival(), ld.arrival));
0393     }
0394     applyMissingJourneyData(ld.journey, oldJny);
0395 
0396     ld.journey.applyMetaData(true); // download logo assets if needed
0397     ld.journeyTimestamp = now();
0398     ld.departure = ld.journey.departure();
0399     ld.departure.addNotes(oldDep.notes());
0400     ld.departureTimestamp = now();
0401     ld.arrival = ld.journey.arrival();
0402     ld.arrival.setNotes(oldArr.notes());
0403     ld.arrivalTimestamp = now();
0404     ld.store(resId, LiveData::AllTypes);
0405 
0406     // update reservation with live data
0407     const auto newRes = PublicTransport::mergeJourney(res, ld.journey);
0408     if (!ReservationHelper::equals(res, newRes)) {
0409         m_resMgr->updateReservation(resId, newRes);
0410     }
0411 
0412     // emit update signals
0413     Q_EMIT journeyUpdated(resId);
0414     Q_EMIT departureUpdated(resId);
0415     Q_EMIT arrivalUpdated(resId);
0416 
0417     // check if we need to notify
0418     if (NotificationHelper::shouldNotify(oldDep, ld.journey.departure(), LiveData::Departure) ||
0419         NotificationHelper::shouldNotify(oldArr, ld.journey.arrival(), LiveData::Arrival)) {
0420         showNotification(resId, ld);
0421     }
0422 }
0423 
0424 void LiveDataManager::showNotification(const QString &resId, const LiveData &ld)
0425 {
0426     // check if we still have an active notification, if so, update that one
0427     const auto it = m_notifications.constFind(resId);
0428     if (it == m_notifications.cend() || !it.value()) {
0429         auto n = new KNotification(QStringLiteral("disruption"));
0430         fillNotification(n, ld);
0431         m_notifications.insert(resId, n);
0432         n->sendEvent();
0433     } else {
0434         fillNotification(it.value(), ld);
0435     }
0436 }
0437 
0438 void LiveDataManager::fillNotification(KNotification* n, const LiveData& ld) const
0439 {
0440     n->setTitle(NotificationHelper::title(ld));
0441     n->setText(NotificationHelper::message(ld));
0442     n->setIconName(QLatin1StringView("clock"));
0443     if (m_showNotificationsOnLockScreen) {
0444         n->setHint(QStringLiteral("x-kde-visibility"), QStringLiteral("public"));
0445     }
0446 }
0447 
0448 void LiveDataManager::showNotification(const QString &resId)
0449 {
0450     // this is only meant for testing!
0451     showNotification(resId, data(resId));
0452 }
0453 
0454 void LiveDataManager::cancelNotification(const QString &resId)
0455 {
0456     const auto nIt = m_notifications.find(resId);
0457     if (nIt != m_notifications.end()) {
0458         if (nIt.value()) {
0459             nIt.value()->close();
0460         }
0461         m_notifications.erase(nIt);
0462     }
0463 }
0464 
0465 QDateTime LiveDataManager::departureTime(const QString &resId, const QVariant &res) const
0466 {
0467     if (JsonLd::isA<TrainReservation>(res) || JsonLd::isA<BusReservation>(res)) {
0468         const auto &dep = departure(resId);
0469         if (dep.hasExpectedDepartureTime()) {
0470             return dep.expectedDepartureTime();
0471         }
0472     }
0473 
0474     return SortUtil::startDateTime(res);
0475 }
0476 
0477 QDateTime LiveDataManager::arrivalTime(const QString &resId, const QVariant &res) const
0478 {
0479     if (JsonLd::isA<TrainReservation>(res) || JsonLd::isA<BusReservation>(res)) {
0480         const auto &arr = arrival(resId);
0481         if (arr.hasExpectedArrivalTime()) {
0482             return arr.expectedArrivalTime();
0483         }
0484     }
0485 
0486     return SortUtil::endDateTime(res);
0487 }
0488 
0489 bool LiveDataManager::hasDeparted(const QString &resId, const QVariant &res) const
0490 {
0491     return departureTime(resId, res) < now();
0492 }
0493 
0494 bool LiveDataManager::hasArrived(const QString &resId, const QVariant &res) const
0495 {
0496     const auto n = now();
0497     // avoid loading live data for everything on startup
0498     if (SortUtil::endDateTime(res).addDays(1) < n) {
0499         return true;
0500     }
0501     return arrivalTime(resId, res) < now();
0502 }
0503 
0504 LiveData& LiveDataManager::data(const QString &resId) const
0505 {
0506     auto it = m_data.find(resId);
0507     if (it != m_data.end()) {
0508         return it.value();
0509     }
0510 
0511     it = m_data.insert(resId, LiveData::load(resId));
0512     return it.value();
0513 }
0514 
0515 void LiveDataManager::importData(const QString& resId, LiveData &&data)
0516 {
0517     // we don't need to store data, Importer already does that
0518     m_data[resId] = std::move(data);
0519     Q_EMIT journeyUpdated(resId);
0520     Q_EMIT departureUpdated(resId);
0521     Q_EMIT arrivalUpdated(resId);
0522 }
0523 
0524 bool LiveDataManager::isRelevant(const QString &resId) const
0525 {
0526     const auto res = m_resMgr->reservation(resId);
0527     // we only care about transit reservations
0528     if (!JsonLd::canConvert<Reservation>(res) || !LocationUtil::isLocationChange(res) || ReservationHelper::isCancelled(res)) {
0529         return false;
0530     }
0531     // we don't care about past events
0532     if (hasArrived(resId, res)) {
0533         return false;
0534     }
0535 
0536     // things handled by KPublicTransport
0537     if (JsonLd::isA<TrainReservation>(res) || JsonLd::isA<BusReservation>(res)) {
0538         return true;
0539     }
0540 
0541     // things with an updatable pkpass
0542     const auto passId = PkPassManager::passId(res);
0543     if (passId.isEmpty()) {
0544         return false;
0545     }
0546     const auto pass = m_pkPassMgr->pass(passId);
0547     return PkPassManager::canUpdate(pass);
0548 }
0549 
0550 void LiveDataManager::batchAdded(const QString &resId)
0551 {
0552     if (!isRelevant(resId)) {
0553         return;
0554     }
0555 
0556     m_reservations.push_back(resId);
0557     m_pollTimer.setInterval(nextPollTime());
0558 }
0559 
0560 void LiveDataManager::batchChanged(const QString &resId)
0561 {
0562     const auto it = std::find(m_reservations.begin(), m_reservations.end(), resId);
0563     const auto relevant = isRelevant(resId);
0564 
0565     if (it == m_reservations.end() && relevant) {
0566         m_reservations.push_back(resId);
0567     } else if (it != m_reservations.end() && !relevant) {
0568         m_reservations.erase(it);
0569     }
0570 
0571     // check if existing updates still apply, and remove them otherwise!
0572     const auto res = m_resMgr->reservation(resId);
0573     const auto dataIt = m_data.find(resId);
0574     if (dataIt != m_data.end()) {
0575         if ((*dataIt).departureTimestamp.isValid() && !PublicTransportMatcher::isDepartureForReservation(res, (*dataIt).departure)) {
0576             (*dataIt).departure = {};
0577             (*dataIt).departureTimestamp = {};
0578             (*dataIt).store(resId, LiveData::Departure);
0579             Q_EMIT departureUpdated(resId);
0580         }
0581         if ((*dataIt).arrivalTimestamp.isValid() && !PublicTransportMatcher::isArrivalForReservation(res, (*dataIt).arrival)) {
0582             (*dataIt).arrival = {};
0583             (*dataIt).arrivalTimestamp = {};
0584             (*dataIt).store(resId, LiveData::Arrival);
0585             Q_EMIT arrivalUpdated(resId);
0586         }
0587 
0588         if ((*dataIt).journeyTimestamp.isValid() && !PublicTransportMatcher::isJourneyForReservation(res, (*dataIt).journey)) {
0589             (*dataIt).journey = {};
0590             (*dataIt).journeyTimestamp = {};
0591             (*dataIt).store(resId, LiveData::Journey);
0592             Q_EMIT journeyUpdated(resId);
0593         }
0594     }
0595 
0596     m_pollTimer.setInterval(nextPollTime());
0597 }
0598 
0599 void LiveDataManager::batchRenamed(const QString &oldBatchId, const QString &newBatchId)
0600 {
0601     const auto it = std::find(m_reservations.begin(), m_reservations.end(), oldBatchId);
0602     if (it != m_reservations.end()) {
0603         *it = newBatchId;
0604     }
0605 }
0606 
0607 void LiveDataManager::batchRemoved(const QString &resId)
0608 {
0609     const auto it = std::find(m_reservations.begin(), m_reservations.end(), resId);
0610     if (it != m_reservations.end()) {
0611         m_reservations.erase(it);
0612     }
0613 
0614     cancelNotification(resId);
0615     LiveData::remove(resId);
0616     m_data.remove(resId);
0617     m_lastPollAttempt.remove(resId);
0618 }
0619 
0620 void LiveDataManager::poll()
0621 {
0622     qCDebug(Log);
0623     pollForUpdates(false);
0624 
0625     m_pollTimer.setInterval(std::max(nextPollTime(), 60 * 1000)); // we pool everything that happens within a minute here
0626     m_pollTimer.start();
0627 }
0628 
0629 void LiveDataManager::pollForUpdates(bool force)
0630 {
0631     for (auto it = m_reservations.begin(); it != m_reservations.end();) {
0632         const auto batchId = *it;
0633         const auto res = m_resMgr->reservation(*it);
0634 
0635         // clean up obsolete stuff
0636         if (hasArrived(*it, res)) {
0637             cancelNotification(*it);
0638             it = m_reservations.erase(it);
0639             m_lastPollAttempt.remove(batchId);
0640             continue;
0641         }
0642         ++it;
0643 
0644         if (!force && nextPollTimeForReservation(batchId) > 60 * 1000) {
0645             // data is still "fresh" according to the poll policy
0646             continue;
0647         }
0648 
0649         if (JsonLd::isA<TrainReservation>(res) || JsonLd::isA<BusReservation>(res)) {
0650             checkReservation(res, batchId);
0651         }
0652 
0653         // check for pkpass updates, for each element in this batch
0654         const auto resIds = m_resMgr->reservationsForBatch(batchId);
0655         for (const auto &resId : resIds) {
0656             const auto res = m_resMgr->reservation(resId);
0657             const auto passId = m_pkPassMgr->passId(res);
0658             if (!passId.isEmpty()) {
0659                 m_lastPollAttempt.insert(batchId, now());
0660                 m_pkPassMgr->updatePass(passId);
0661             }
0662         }
0663     }
0664 }
0665 
0666 int LiveDataManager::nextPollTime() const
0667 {
0668     int t = std::numeric_limits<int>::max();
0669     for (const auto &resId : m_reservations) {
0670         t = std::min(t, nextPollTimeForReservation(resId));
0671     }
0672     qCDebug(Log) << "next auto-update in" << (t/1000) << "secs";
0673     return t;
0674 }
0675 
0676 static constexpr const int MAX_POLL_INTERVAL = 7 * 24 * 3600;
0677 struct {
0678     int distance; // secs
0679     int pollInterval; // secs
0680 } static const pollIntervalTable[] = {
0681     { 3600, 5*60 }, // for <1h we poll every 5 minutes
0682     { 4 * 3600, 15 * 60 }, // for <4h we poll every 15 minutes
0683     { 24 * 3600, 3600 }, // for <1d we poll once per hour
0684     { 4 * 24 * 3600, 24 * 3600 }, // for <4d we poll once per day
0685     { 60 * 24 * 3600, MAX_POLL_INTERVAL }, // anything before we should at least do one poll to get full details right away
0686 };
0687 
0688 int LiveDataManager::nextPollTimeForReservation(const QString& resId) const
0689 {
0690     const auto res = m_resMgr->reservation(resId);
0691 
0692     const auto now = this->now();
0693     auto dist = now.secsTo(departureTime(resId, res));
0694     if (dist < 0) {
0695         dist = now.secsTo(arrivalTime(resId, res));
0696     }
0697     if (dist < 0) {
0698         return std::numeric_limits<int>::max();
0699     }
0700 
0701     const auto it = std::lower_bound(std::begin(pollIntervalTable), std::end(pollIntervalTable), dist, [](const auto &lhs, const auto rhs) {
0702         return lhs.distance < rhs;
0703     });
0704     if (it == std::end(pollIntervalTable)) {
0705         return std::numeric_limits<int>::max();
0706     }
0707 
0708     // check last poll time for this reservation
0709     const auto &ld = data(resId);
0710     const auto lastArrivalPoll = ld.arrivalTimestamp;
0711     const auto lastDeparturePoll = lastDeparturePollTime(resId, res);
0712     auto lastRelevantPoll = lastArrivalPoll;
0713     // ignore departure if we have already departed
0714     if (!hasDeparted(resId, res) && lastDeparturePoll.isValid()) {
0715         if (!lastArrivalPoll.isValid() || lastArrivalPoll > lastDeparturePoll) {
0716             lastRelevantPoll = lastDeparturePoll;
0717         }
0718     }
0719     const int lastPollDist = (!lastRelevantPoll.isValid() || lastRelevantPoll > now)
0720         ? MAX_POLL_INTERVAL // no poll yet == long time ago
0721         : lastRelevantPoll.secsTo(now);
0722     return std::max((it->pollInterval - lastPollDist) * 1000, pollCooldown(resId)); // we need msecs
0723 }
0724 
0725 QDateTime LiveDataManager::lastDeparturePollTime(const QString &batchId, const QVariant &res) const
0726 {
0727     auto dt = data(batchId).departureTimestamp;
0728     if (dt.isValid()) {
0729         return dt;
0730     }
0731 
0732     // check for pkpass updates
0733     const auto resIds = m_resMgr->reservationsForBatch(batchId);
0734     for (const auto &resId : resIds) {
0735         const auto res = m_resMgr->reservation(resId);
0736         const auto passId = m_pkPassMgr->passId(res);
0737         if (!passId.isEmpty()) {
0738             dt = m_pkPassMgr->updateTime(passId);
0739         }
0740         if (dt.isValid()) {
0741             return dt;
0742         }
0743     }
0744 
0745     return dt;
0746 }
0747 
0748 int LiveDataManager::pollCooldown(const QString &resId) const
0749 {
0750     const auto lastPollTime = m_lastPollAttempt.value(resId);
0751     if (!lastPollTime.isValid()) {
0752         return 0;
0753     }
0754     return std::clamp<int>(POLL_COOLDOWN_ON_ERROR - lastPollTime.secsTo(now()), 0, POLL_COOLDOWN_ON_ERROR) * 1000;
0755 }
0756 
0757 void LiveDataManager::pkPassUpdated(const QString &passId, const QStringList &changes)
0758 {
0759     if (changes.isEmpty()) {
0760         return;
0761     }
0762 
0763     QVariant passRes;
0764 
0765     // Find relevant reservation for the given passId.
0766     for (const QString &resId : std::as_const(m_reservations)) {
0767         const auto res = m_resMgr->reservation(resId);
0768         const auto resPassId = PkPassManager::passId(res);
0769         if (resPassId == passId) {
0770             passRes = res;
0771             break;
0772         }
0773     }
0774 
0775     QString text = changes.join(QLatin1Char('\n'));
0776 
0777     if (JsonLd::isA<FlightReservation>(passRes)) {
0778         const auto flight = passRes.value<FlightReservation>().reservationFor().value<Flight>();
0779 
0780         text.prepend(QLatin1Char('\n'));
0781         text.prepend(i18n("Flight %1 to %2:",
0782                           // TODO add formatter util for this.
0783                           flight.airline().iataCode() + QLatin1Char(' ') + flight.flightNumber(),
0784                           LocationUtil::name(LocationUtil::arrivalLocation(passRes))));
0785     }
0786 
0787     KNotification::event(KNotification::Notification, i18n("Itinerary change"), text, QLatin1StringView("clock"));
0788 }
0789 
0790 KPublicTransport::Manager* LiveDataManager::publicTransportManager() const
0791 {
0792     return m_ptMgr;
0793 }
0794 
0795 QDateTime LiveDataManager::now() const
0796 {
0797     if (Q_UNLIKELY(m_unitTestTime.isValid())) {
0798         return m_unitTestTime;
0799     }
0800     return QDateTime::currentDateTime();
0801 }
0802 
0803 #include "moc_livedatamanager.cpp"