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"