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

0001 /*
0002     SPDX-FileCopyrightText: 2019 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "timelinedelegatecontroller.h"
0008 
0009 #include "calendarhelper.h"
0010 #include "constants.h"
0011 #include "documentmanager.h"
0012 #include "livedatamanager.h"
0013 #include "locationhelper.h"
0014 #include "logging.h"
0015 #include "reservationhelper.h"
0016 #include "reservationmanager.h"
0017 #include "publictransport.h"
0018 #include "publictransportmatcher.h"
0019 #include "transfer.h"
0020 #include "transfermanager.h"
0021 
0022 #include <KItinerary/BusTrip>
0023 #include <KItinerary/CalendarHandler>
0024 #include <KItinerary/DocumentUtil>
0025 #include <KItinerary/Flight>
0026 #include <KItinerary/JsonLdDocument>
0027 #include <KItinerary/LocationUtil>
0028 #include <KItinerary/SortUtil>
0029 #include <KItinerary/Reservation>
0030 #include <KItinerary/Ticket>
0031 #include <KItinerary/TrainTrip>
0032 
0033 #include <KPublicTransport/Platform>
0034 #include <KPublicTransport/PlatformLayout>
0035 #include <KPublicTransport/Vehicle>
0036 
0037 #include <QJSEngine>
0038 #include <QJSValue>
0039 
0040 #include <QCoreApplication>
0041 #include <QDateTime>
0042 #include <QDebug>
0043 #include <QPointF>
0044 #include <QTimer>
0045 #include <QTimeZone>
0046 
0047 QTimer* TimelineDelegateController::s_currentTimer = nullptr;
0048 int TimelineDelegateController::s_progressRefCount = 0;
0049 QTimer* TimelineDelegateController::s_progressTimer = nullptr;
0050 
0051 using namespace KItinerary;
0052 
0053 TimelineDelegateController::TimelineDelegateController(QObject *parent)
0054     : QObject(parent)
0055 {
0056     if (!s_currentTimer) {
0057         s_currentTimer = new QTimer(QCoreApplication::instance());
0058         s_currentTimer->setSingleShot(true);
0059         s_currentTimer->setTimerType(Qt::VeryCoarseTimer);
0060     }
0061     connect(s_currentTimer, &QTimer::timeout, this, [this]() { checkForUpdate(m_batchId); });
0062 
0063     connect(this, &TimelineDelegateController::contentChanged, this, &TimelineDelegateController::connectionWarningChanged);
0064 }
0065 
0066 TimelineDelegateController::~TimelineDelegateController() = default;
0067 
0068 QObject* TimelineDelegateController::reservationManager() const
0069 {
0070     return m_resMgr;
0071 }
0072 
0073 void TimelineDelegateController::setReservationManager(QObject *resMgr)
0074 {
0075     if (m_resMgr == resMgr) {
0076         return;
0077     }
0078 
0079     m_resMgr = qobject_cast<ReservationManager*>(resMgr);
0080     Q_EMIT setupChanged();
0081     Q_EMIT contentChanged();
0082     Q_EMIT departureChanged();
0083     Q_EMIT arrivalChanged();
0084     Q_EMIT journeyChanged();
0085     Q_EMIT previousLocationChanged();
0086     Q_EMIT layoutChanged();
0087 
0088     connect(m_resMgr, &ReservationManager::batchChanged, this, &TimelineDelegateController::batchChanged);
0089     connect(m_resMgr, &ReservationManager::batchContentChanged, this, &TimelineDelegateController::batchChanged);
0090     // ### could be done more efficiently
0091     connect(m_resMgr, &ReservationManager::batchAdded, this, &TimelineDelegateController::previousLocationChanged);
0092     connect(m_resMgr, &ReservationManager::batchRemoved, this, &TimelineDelegateController::previousLocationChanged);
0093 
0094     checkForUpdate(m_batchId);
0095 }
0096 
0097 QObject* TimelineDelegateController::liveDataManager() const
0098 {
0099     return m_liveDataMgr;
0100 }
0101 
0102 void TimelineDelegateController::setLiveDataManager(QObject* liveDataMgr)
0103 {
0104     if (m_liveDataMgr == liveDataMgr) {
0105         return;
0106     }
0107 
0108     m_liveDataMgr = qobject_cast<LiveDataManager*>(liveDataMgr);
0109     Q_EMIT setupChanged();
0110     Q_EMIT departureChanged();
0111     Q_EMIT arrivalChanged();
0112     Q_EMIT journeyChanged();
0113     Q_EMIT layoutChanged();
0114 
0115     connect(m_liveDataMgr, &LiveDataManager::arrivalUpdated, this, &TimelineDelegateController::checkForUpdate);
0116     connect(m_liveDataMgr, &LiveDataManager::departureUpdated, this, &TimelineDelegateController::checkForUpdate);
0117     connect(m_liveDataMgr, &LiveDataManager::arrivalUpdated, this, [this](const auto &batchId) {
0118         if (batchId == m_batchId) {
0119             Q_EMIT arrivalChanged();
0120             Q_EMIT journeyChanged();
0121         }
0122     });
0123     connect(m_liveDataMgr, &LiveDataManager::departureUpdated, this, [this](const auto &batchId) {
0124         if (batchId == m_batchId) {
0125             Q_EMIT departureChanged();
0126             Q_EMIT journeyChanged();
0127         }
0128     });
0129     connect(m_liveDataMgr, &LiveDataManager::journeyUpdated, this, [this](const auto &batchId) {
0130         if (batchId == m_batchId) {
0131             Q_EMIT departureChanged();
0132             Q_EMIT arrivalChanged();
0133             Q_EMIT journeyChanged();
0134         }
0135     });
0136 
0137     checkForUpdate(m_batchId);
0138 }
0139 
0140 QObject* TimelineDelegateController::transferManager() const
0141 {
0142     return m_transferMgr;
0143 }
0144 
0145 void TimelineDelegateController::setTransferManager(QObject *transferMgr)
0146 {
0147     if (m_transferMgr == transferMgr) {
0148         return;
0149     }
0150 
0151     m_transferMgr = qobject_cast<TransferManager*>(transferMgr);
0152     Q_EMIT setupChanged();
0153 }
0154 
0155 QObject* TimelineDelegateController::documentManager() const
0156 {
0157     return m_documentMgr;
0158 }
0159 
0160 void TimelineDelegateController::setDocumentManager(QObject *documentMgr)
0161 {
0162     if (m_documentMgr == documentMgr) {
0163         return;
0164     }
0165 
0166     m_documentMgr = qobject_cast<DocumentManager*>(documentMgr);
0167     Q_EMIT setupChanged();
0168     Q_EMIT contentChanged();
0169 }
0170 
0171 QString TimelineDelegateController::batchId() const
0172 {
0173     return m_batchId;
0174 }
0175 
0176 void TimelineDelegateController::setBatchId(const QString &batchId)
0177 {
0178     if (m_batchId == batchId)
0179         return;
0180 
0181     m_batchId = batchId;
0182     Q_EMIT batchIdChanged();
0183     Q_EMIT contentChanged();
0184     Q_EMIT departureChanged();
0185     Q_EMIT arrivalChanged();
0186     Q_EMIT journeyChanged();
0187     Q_EMIT previousLocationChanged();
0188     checkForUpdate(batchId);
0189 }
0190 
0191 bool TimelineDelegateController::isCurrent() const
0192 {
0193     return m_isCurrent;
0194 }
0195 
0196 void TimelineDelegateController::setCurrent(bool current, const QVariant &res)
0197 {
0198     if (current == m_isCurrent) {
0199         return;
0200     }
0201 
0202     m_isCurrent = current;
0203     Q_EMIT currentChanged();
0204 
0205     if (!LocationUtil::isLocationChange(res)) {
0206         return;
0207     }
0208 
0209     if (!s_progressTimer && m_isCurrent) {
0210         s_progressTimer = new QTimer(QCoreApplication::instance());
0211         s_progressTimer->setInterval(std::chrono::minutes(1));
0212         s_progressTimer->setTimerType(Qt::VeryCoarseTimer);
0213         s_progressTimer->setSingleShot(false);
0214     }
0215 
0216     if (m_isCurrent) {
0217         connect(s_progressTimer, &QTimer::timeout, this, &TimelineDelegateController::progressChanged);
0218         if (s_progressRefCount++ == 0) {
0219             s_progressTimer->start();
0220         }
0221     } else {
0222         disconnect(s_progressTimer, &QTimer::timeout, this, &TimelineDelegateController::progressChanged);
0223         if (--s_progressRefCount == 0) {
0224             s_progressTimer->stop();
0225         }
0226     }
0227 }
0228 
0229 float TimelineDelegateController::progress() const
0230 {
0231     if (!m_resMgr || m_batchId.isEmpty() || !m_isCurrent) {
0232         return 0.0f;
0233     }
0234 
0235     const auto res = m_resMgr->reservation(m_batchId);
0236     const auto startTime = liveStartDateTime(res);
0237     const auto endTime = liveEndDateTime(res);
0238 
0239     const auto tripLength = startTime.secsTo(endTime);
0240     if (tripLength <= 0) {
0241         return 0.0f;
0242     }
0243     const auto progress = startTime.secsTo(QDateTime::currentDateTime());
0244 
0245     return std::min(std::max(0.0f, (float)progress / (float)tripLength), 1.0f);
0246 }
0247 
0248 KPublicTransport::Stopover TimelineDelegateController::arrival() const
0249 {
0250     if (!m_liveDataMgr || m_batchId.isEmpty()) {
0251         return {};
0252     }
0253     return m_liveDataMgr->arrival(m_batchId);
0254 }
0255 
0256 KPublicTransport::Stopover TimelineDelegateController::departure() const
0257 {
0258     if (!m_liveDataMgr || m_batchId.isEmpty()) {
0259         return {};
0260     }
0261     return m_liveDataMgr->departure(m_batchId);
0262 }
0263 
0264 KPublicTransport::JourneySection TimelineDelegateController::journey() const
0265 {
0266     if (!m_liveDataMgr || m_batchId.isEmpty()) {
0267         return {};
0268     }
0269     return m_liveDataMgr->journey(m_batchId);
0270 }
0271 
0272 void TimelineDelegateController::checkForUpdate(const QString& batchId)
0273 {
0274     if (!m_resMgr || m_batchId.isEmpty()) {
0275         setCurrent(false);
0276         return;
0277     }
0278     if (batchId != m_batchId) {
0279         return;
0280     }
0281 
0282     const auto res = m_resMgr->reservation(batchId);
0283     if (!LocationUtil::isLocationChange(res)) {
0284         return;
0285     }
0286     if (ReservationHelper::isCancelled(res)) {
0287         setCurrent(false);
0288         return;
0289     }
0290 
0291     const auto now = QDateTime::currentDateTime();
0292     const auto startTime = relevantStartDateTime(res);
0293     const auto endTime = liveEndDateTime(res);
0294 
0295     setCurrent(startTime < now && now < endTime, res);
0296 
0297     if (now < startTime) {
0298         scheduleNextUpdate(std::chrono::seconds(now.secsTo(startTime) + 1));
0299     } else if (now < endTime) {
0300         scheduleNextUpdate(std::chrono::seconds(now.secsTo(endTime) + 1));
0301     }
0302 }
0303 
0304 QDateTime TimelineDelegateController::relevantStartDateTime(const QVariant &res) const
0305 {
0306     auto startTime = SortUtil::startDateTime(res);
0307     if (JsonLd::isA<FlightReservation>(res)) {
0308         const auto flight = res.value<FlightReservation>().reservationFor().value<Flight>();
0309         if (flight.boardingTime().isValid()) {
0310             startTime = flight.boardingTime();
0311         }
0312     } else if (JsonLd::isA<TrainReservation>(res) || JsonLd::isA<BusReservation>(res)) {
0313         startTime = startTime.addSecs(-5 * 60);
0314     }
0315 
0316     return startTime;
0317 }
0318 
0319 QDateTime TimelineDelegateController::liveStartDateTime(const QVariant& res) const
0320 {
0321     if (m_liveDataMgr) {
0322         const auto dep = m_liveDataMgr->departure(m_batchId);
0323         if (dep.expectedDepartureTime().isValid()) {
0324             return dep.expectedDepartureTime();
0325         }
0326     }
0327     return SortUtil::startDateTime(res);
0328 }
0329 
0330 QDateTime TimelineDelegateController::liveEndDateTime(const QVariant& res) const
0331 {
0332     if (m_liveDataMgr) {
0333         const auto arr = m_liveDataMgr->arrival(m_batchId);
0334         if (arr.expectedArrivalTime().isValid()) {
0335             return arr.expectedArrivalTime();
0336         }
0337     }
0338     return SortUtil::endDateTime(res);
0339 }
0340 
0341 void TimelineDelegateController::scheduleNextUpdate(std::chrono::milliseconds ms)
0342 {
0343     if (s_currentTimer->isActive() && s_currentTimer->remainingTimeAsDuration() < ms) {
0344         return;
0345     }
0346     s_currentTimer->start(ms);
0347 }
0348 
0349 void TimelineDelegateController::batchChanged(const QString& batchId)
0350 {
0351     if (batchId != m_batchId || m_batchId.isEmpty()) {
0352         return;
0353     }
0354     checkForUpdate(batchId);
0355     Q_EMIT contentChanged();
0356     Q_EMIT arrivalChanged();
0357     Q_EMIT departureChanged();
0358     Q_EMIT journeyChanged();
0359     Q_EMIT previousLocationChanged();
0360 }
0361 
0362 QVariant TimelineDelegateController::previousLocation() const
0363 {
0364     if (m_batchId.isEmpty() || !m_resMgr) {
0365         return {};
0366     }
0367 
0368     const auto prevBatch = m_resMgr->previousBatch(m_batchId);
0369     if (prevBatch.isEmpty()) {
0370         return {};
0371     }
0372 
0373     const auto res = m_resMgr->reservation(prevBatch);
0374     auto endTime = SortUtil::endDateTime(res);
0375     if (m_liveDataMgr) {
0376         const auto arr = m_liveDataMgr->arrival(prevBatch);
0377         if (arr.hasExpectedArrivalTime()) {
0378             endTime = arr.expectedArrivalTime();
0379         }
0380     }
0381 
0382     if (endTime < QDateTime::currentDateTime()) {
0383         // past event, we can use GPS rather than predict our location from the itinerary
0384         return {};
0385     }
0386 
0387     if (LocationUtil::isLocationChange(res)) {
0388         return LocationUtil::arrivalLocation(res);
0389     } else {
0390         return LocationUtil::location(res);
0391     }
0392 }
0393 
0394 QDateTime TimelineDelegateController::effectiveEndTime() const
0395 {
0396     if (!m_resMgr || m_batchId.isEmpty()) {
0397         return {};
0398     }
0399 
0400     const auto arr = arrival();
0401     if (arr.hasExpectedArrivalTime()) {
0402         return arr.expectedArrivalTime();
0403     }
0404     return SortUtil::endDateTime(m_resMgr->reservation(m_batchId));
0405 }
0406 
0407 bool TimelineDelegateController::isLocationChange() const
0408 {
0409     if (!m_resMgr || m_batchId.isEmpty()) {
0410         return false;
0411     }
0412 
0413     const auto res = m_resMgr->reservation(m_batchId);
0414     return LocationUtil::isLocationChange(res);
0415 }
0416 
0417 bool TimelineDelegateController::isPublicTransport() const
0418 {
0419     if (!m_resMgr || m_batchId.isEmpty()) {
0420         return false;
0421     }
0422 
0423     const auto res = m_resMgr->reservation(m_batchId);
0424     return LocationUtil::isLocationChange(res) && !JsonLd::isA<RentalCarReservation>(res);
0425 }
0426 
0427 static bool isJourneyCandidate(const QVariant &res)
0428 {
0429     // TODO do we really need to constrain this to trains/buses? a long distance train can be a suitable alternative for a missed short distance flight for example
0430     return LocationUtil::isLocationChange(res) && (JsonLd::isA<TrainReservation>(res) || JsonLd::isA<BusReservation>(res));
0431 }
0432 
0433 static bool isLayover(const QVariant &res1, const QVariant &res2)
0434 {
0435     if (!LocationUtil::isLocationChange(res1) || !LocationUtil::isLocationChange(res2) || ReservationHelper::isUnbound(res1) || ReservationHelper::isUnbound(res2)) {
0436         return false;
0437     }
0438 
0439     const auto arrDt = SortUtil::endDateTime(res1);
0440     const auto depDt = SortUtil::startDateTime(res2);
0441     const auto layoverTime = arrDt.secsTo(depDt);
0442     if (layoverTime < 0 || layoverTime > Constants::MaximumLayoverTime.count()) {
0443         return false;
0444     }
0445 
0446     return LocationUtil::isSameLocation(LocationUtil::arrivalLocation(res1), LocationUtil::departureLocation(res2), LocationUtil::WalkingDistance);
0447 }
0448 
0449 KPublicTransport::JourneyRequest TimelineDelegateController::journeyRequestOne() const
0450 {
0451     if (!m_resMgr || m_batchId.isEmpty() || !m_liveDataMgr) {
0452         return {};
0453     }
0454 
0455     const auto res = m_resMgr->reservation(m_batchId);
0456     if (!isJourneyCandidate(res)) {
0457         return {};
0458     }
0459 
0460     KPublicTransport::JourneyRequest req;
0461     req.setFrom(PublicTransport::locationFromPlace(LocationUtil::departureLocation(res), res));
0462     req.setTo(PublicTransport::locationFromPlace(LocationUtil::arrivalLocation(res), res));
0463     req.setDateTime(std::max(QDateTime::currentDateTime(), SortUtil::startDateTime(res)));
0464     req.setDateTimeMode(KPublicTransport::JourneyRequest::Departure);
0465     req.setDownloadAssets(true);
0466     req.setIncludeIntermediateStops(true);
0467     req.setIncludePaths(true);
0468     PublicTransport::selectBackends(req, m_liveDataMgr->publicTransportManager(), res);
0469     return req;
0470 }
0471 
0472 KPublicTransport::JourneyRequest TimelineDelegateController::journeyRequestFull() const
0473 {
0474     auto req = journeyRequestOne();
0475     if (!req.isValid()) {
0476         return {};
0477     }
0478 
0479     // find full journey by looking at subsequent elements
0480     auto prevRes = m_resMgr->reservation(m_batchId);
0481     auto prevBatchId = m_batchId;
0482     while (true) {
0483         auto endBatchId = m_resMgr->nextBatch(prevBatchId);
0484         auto endRes = m_resMgr->reservation(endBatchId);
0485         if (!isJourneyCandidate(endRes) || !isLayover(prevRes, endRes)) {
0486             break;
0487         }
0488 
0489         req.setTo(PublicTransport::locationFromPlace(LocationUtil::arrivalLocation(endRes), endRes));
0490         prevRes = endRes;
0491         prevBatchId = endBatchId;
0492     }
0493 
0494     return req;
0495 }
0496 
0497 void TimelineDelegateController::applyJourney(const QVariant &journey, bool includeFollowing)
0498 {
0499     if (!m_resMgr || m_batchId.isEmpty()) {
0500         return;
0501     }
0502 
0503     const auto jny = journey.value<KPublicTransport::Journey>();
0504     std::vector<KPublicTransport::JourneySection> sections;
0505     std::copy_if(jny.sections().begin(), jny.sections().end(), std::back_inserter(sections), [](const auto &section) {
0506         return section.mode() == KPublicTransport::JourneySection::PublicTransport;
0507     });
0508     if (sections.empty()) {
0509         return;
0510     }
0511 
0512     // find all batches we are replying here (same logic as in journeyRequest)
0513     std::vector<QString> oldBatches({m_batchId});
0514     if (includeFollowing) {
0515         auto prevRes = m_resMgr->reservation(m_batchId);
0516         auto prevBatchId = m_batchId;
0517         while (true) {
0518             auto endBatchId = m_resMgr->nextBatch(prevBatchId);
0519             auto endRes = m_resMgr->reservation(endBatchId);
0520             qDebug() << endRes << isJourneyCandidate(endRes) << isLayover(prevRes, endRes);
0521             if (!isJourneyCandidate(endRes) || !isLayover(prevRes, endRes)) {
0522                 break;
0523             }
0524 
0525             oldBatches.push_back(endBatchId);
0526             prevRes = endRes;
0527             prevBatchId = endBatchId;
0528         }
0529     }
0530     qCDebug(Log) << "Affected batches:" << oldBatches;
0531 
0532     // align sections with affected batches, by type, and insert/update accordingly
0533     auto it = oldBatches.begin();
0534     QString lastResId;
0535     for (const auto &section : sections) {
0536         QVariant oldRes;
0537         if (it != oldBatches.end()) {
0538             lastResId = *it;
0539             oldRes = m_resMgr->reservation(*it);
0540         }
0541 
0542         // same type -> update the existing one
0543         if (PublicTransportMatcher::isSameMode(oldRes, section)) {
0544             const auto resIds = m_resMgr->reservationsForBatch(*it);
0545             for (const auto &resId : resIds) {
0546                 auto res = m_resMgr->reservation(resId);
0547                 res = PublicTransport::applyJourneySection(res, section);
0548                 m_resMgr->updateReservation(resId, res);
0549                 m_liveDataMgr->setJourney(resId, section);
0550             }
0551             ++it;
0552         } else {
0553             auto res = PublicTransport::reservationFromJourneySection(section);
0554 
0555             // copy ticket data from previous element
0556             // TODO this would need to be done for the entire batch!
0557             if (!lastResId.isEmpty()) {
0558                 auto lastRes = m_resMgr->reservation(lastResId);
0559                 JsonLdDocument::writeProperty(lastRes, "reservationFor", {});
0560                 res = JsonLdDocument::apply(lastRes, res);
0561             }
0562 
0563             const auto resId = m_resMgr->addReservation(res);
0564             m_liveDataMgr->setJourney(resId, section);
0565         }
0566     }
0567 
0568     // remove left over reservations
0569     for (; it != oldBatches.end(); ++it) {
0570         m_resMgr->removeBatch(*it);
0571     }
0572 }
0573 
0574 bool TimelineDelegateController::connectionWarning() const
0575 {
0576     if (!m_resMgr || m_batchId.isEmpty() || !m_liveDataMgr) {
0577         return false;
0578     }
0579 
0580     const auto curRes = m_resMgr->reservation(m_batchId);
0581     if (!LocationUtil::isLocationChange(curRes) || ReservationHelper::isCancelled(curRes)) {
0582         return false;
0583     }
0584 
0585     // if the current item has canceled departure/arrival, warn as well
0586     if (departure().disruptionEffect() == KPublicTransport::Disruption::NoService || arrival().disruptionEffect() == KPublicTransport::Disruption::NoService) {
0587         return true;
0588     }
0589 
0590     const auto prevResId = m_resMgr->previousBatch(m_batchId);
0591     const auto prevRes = m_resMgr->reservation(prevResId);
0592     if (!LocationUtil::isLocationChange(prevRes) || ReservationHelper::isCancelled(prevRes)) {
0593         return false;
0594     }
0595 
0596     const auto prevArr = m_liveDataMgr->arrival(prevResId);
0597     const auto prevArrDt = std::max(SortUtil::endDateTime(prevRes), prevArr.expectedArrivalTime());
0598 
0599     const auto curDepDt = std::max(SortUtil::startDateTime(curRes), departure().expectedDepartureTime());
0600     if (curDepDt.isValid() && prevArrDt.isValid()) {
0601         return curDepDt < prevArrDt;
0602     }
0603 
0604     return false;
0605 }
0606 
0607 bool TimelineDelegateController::isCanceled() const
0608 {
0609     if (!m_resMgr || m_batchId.isEmpty()) {
0610         return false;
0611     }
0612 
0613     const auto res = m_resMgr->reservation(m_batchId);
0614     return ReservationHelper::isCancelled(res);
0615 }
0616 
0617 static void mapArgumentsForLocation(QJSValue &args, const QVariant &location, QJSEngine *engine)
0618 {
0619     args.setProperty(QStringLiteral("placeName"), LocationUtil::name(location));
0620     args.setProperty(QStringLiteral("region"), LocationHelper::regionCode(location));
0621 
0622     const auto geo = LocationUtil::geo(location);
0623     args.setProperty(QStringLiteral("coordinate"), engine->toScriptValue(QPointF(geo.longitude(), geo.latitude())));
0624 }
0625 
0626 struct CoachData {
0627     QString coachName;
0628     KPublicTransport::VehicleSection::Class coachClass = KPublicTransport::VehicleSection::UnknownClass;
0629 };
0630 
0631 static CoachData coachDataForReservation(const QVariant &res)
0632 {
0633     if (!JsonLd::isA<TrainReservation>(res)) {
0634         return {};
0635     }
0636 
0637     const auto trainRes = res.value<TrainReservation>();
0638     const auto seat = trainRes.reservedTicket().value<Ticket>().ticketedSeat();
0639 
0640     CoachData data;
0641     data.coachName = seat.seatSection();
0642     if (seat.seatingType() == QLatin1StringView("1")) {
0643         data.coachClass = KPublicTransport::VehicleSection::FirstClass;
0644     } else if (seat.seatingType() == QLatin1StringView("2")) {
0645         data.coachClass = KPublicTransport::VehicleSection::SecondClass;
0646     }
0647     return data;
0648 }
0649 
0650 static QString platformSectionsForCoachData(const KPublicTransport::Stopover &stop, const CoachData &coach)
0651 {
0652     KPublicTransport::PlatformLayout layouter;
0653     if (!coach.coachName.isEmpty()) {
0654         return layouter.sectionsForVehicleSection(stop, coach.coachName);
0655     } else if (coach.coachClass != KPublicTransport::VehicleSection::UnknownClass) {
0656         return layouter.sectionsForClass(stop, coach.coachClass);
0657     } else {
0658         return layouter.sectionsForVehicle(stop);
0659     }
0660 
0661     return {};
0662 }
0663 
0664 static void mapArgumentsForPt(QJSValue &args, QLatin1StringView prefix, const KPublicTransport::Stopover &stop, const CoachData &coach)
0665 {
0666     const auto platformName = stop.hasExpectedPlatform() ? stop.expectedPlatform() : stop.scheduledPlatform();
0667     if (!platformName.isEmpty()) {
0668         const auto sections = platformSectionsForCoachData(stop, coach);
0669         args.setProperty(prefix + QLatin1StringView("PlatformName"), sections.isEmpty() ? platformName : (platformName + QLatin1Char(' ') + sections));
0670     }
0671 
0672     if (stop.route().line().mode() != KPublicTransport::Line::Unknown) {
0673         args.setProperty(prefix + QLatin1StringView("PlatformMode"), PublicTransport::lineModeToPlatformMode(stop.route().line().mode()));
0674     }
0675 
0676     const auto ifopt = stop.stopPoint().identifier(QStringLiteral("ifopt"));
0677     if (!ifopt.isEmpty()) {
0678         args.setProperty(prefix + QLatin1StringView("PlatformIfopt"), ifopt);
0679     }
0680 }
0681 
0682 static void mapArrivalArgumesForRes(QJSValue &args, const QVariant &res)
0683 {
0684     if (JsonLd::isA<TrainReservation>(res)) {
0685         const auto trip = res.value<TrainReservation>().reservationFor().value<TrainTrip>();
0686         args.setProperty(QStringLiteral("arrivalPlatformName"), trip.arrivalPlatform());
0687         args.setProperty(QStringLiteral("arrivalPlatformMode"), KOSMIndoorMap::Platform::Rail);
0688     } else if (JsonLd::isA<BusReservation>(res)) {
0689         const auto trip = res.value<BusReservation>().reservationFor().value<BusTrip>();
0690         args.setProperty(QStringLiteral("arrivalPlatformName"), trip.arrivalPlatform());
0691         args.setProperty(QStringLiteral("arrivalPlatformMode"), KOSMIndoorMap::Platform::Bus);
0692     }
0693     // TODO there is no arrival gate property (yet)
0694 }
0695 
0696 static void mapDepartureArgumentsForRes(QJSValue &args, const QVariant &res)
0697 {
0698     if (JsonLd::isA<TrainReservation>(res)) {
0699         const auto trip = res.value<TrainReservation>().reservationFor().value<TrainTrip>();
0700         args.setProperty(QStringLiteral("departurePlatformName"), trip.departurePlatform());
0701         args.setProperty(QStringLiteral("departurePlatformMode"), KOSMIndoorMap::Platform::Rail);
0702     } else if (JsonLd::isA<BusReservation>(res)) {
0703         const auto trip = res.value<BusReservation>().reservationFor().value<BusTrip>();
0704         args.setProperty(QStringLiteral("departurePlatformName"), trip.departurePlatform());
0705         args.setProperty(QStringLiteral("departurePlatformMode"), KOSMIndoorMap::Platform::Bus);
0706     } else if (JsonLd::isA<FlightReservation>(res)) {
0707         const auto flight = res.value<FlightReservation>().reservationFor().value<Flight>();
0708         args.setProperty(QStringLiteral("departureGateName"), flight.departureGate());
0709     }
0710 }
0711 
0712 QJSValue TimelineDelegateController::arrivalMapArguments() const
0713 {
0714     const auto engine = qjsEngine(this);
0715     if (!engine || !m_resMgr || m_batchId.isEmpty() || !m_transferMgr || !m_liveDataMgr) {
0716         return {};
0717     }
0718 
0719     const auto res = m_resMgr->reservation(m_batchId);
0720     if (!LocationUtil::isLocationChange(res)) {
0721         return {};
0722     }
0723 
0724     auto args = engine->newObject();
0725     mapArgumentsForLocation(args, LocationUtil::arrivalLocation(res), engine);
0726 
0727     // arrival location
0728     mapArrivalArgumesForRes(args, res);
0729     const auto arr = arrival();
0730     mapArgumentsForPt(args, QLatin1StringView("arrival"), arr, coachDataForReservation(res));
0731 
0732     // arrival time
0733     auto arrTime = arr.hasExpectedArrivalTime() ? arr.expectedArrivalTime() : arr.scheduledArrivalTime();
0734     if (!arrTime.isValid()) {
0735         arrTime = SortUtil::endDateTime(res);
0736     }
0737     args.setProperty(QStringLiteral("beginTime"), engine->toScriptValue(arrTime));
0738     if (arrTime.timeSpec() == Qt::TimeZone) {
0739         args.setProperty(QStringLiteral("timeZone"), QString::fromUtf8(arrTime.timeZone().id()));
0740     }
0741 
0742     // look for departure for a following transfer
0743     const auto transfer = m_transferMgr->transfer(m_batchId, Transfer::After);
0744     if (transfer.state() == Transfer::Selected) {
0745         const auto dep = PublicTransport::firstTransportSection(transfer.journey()).departure();
0746         mapArgumentsForPt(args, QLatin1StringView("departure"), dep, {});
0747         args.setProperty(QStringLiteral("endTime"), engine->toScriptValue(dep.hasExpectedDepartureTime() ? dep.expectedDepartureTime() : dep.scheduledDepartureTime()));
0748         return args;
0749     }
0750 
0751     // ... or layover
0752     const auto nextResId = m_resMgr->nextBatch(m_batchId);
0753     const auto nextRes = m_resMgr->reservation(nextResId);
0754     if (!isLayover(res, nextRes)) {
0755         return args;
0756     }
0757     mapDepartureArgumentsForRes(args, nextRes);
0758     const auto dep = m_liveDataMgr->departure(nextResId);
0759     mapArgumentsForPt(args, QLatin1StringView("departure"), dep, coachDataForReservation(nextRes));
0760 
0761     auto depTime = dep.hasExpectedDepartureTime() ? dep.expectedDepartureTime() : dep.scheduledDepartureTime();
0762     if (!depTime.isValid()) {
0763         depTime = SortUtil::startDateTime(nextRes);
0764     }
0765     args.setProperty(QStringLiteral("endTime"), engine->toScriptValue(depTime));
0766 
0767     return args;
0768 }
0769 
0770 QJSValue TimelineDelegateController::departureMapArguments() const
0771 {
0772     const auto engine = qjsEngine(this);
0773     if (!engine || !m_resMgr || m_batchId.isEmpty() || !m_transferMgr || !m_liveDataMgr) {
0774         return {};
0775     }
0776 
0777     const auto res = m_resMgr->reservation(m_batchId);
0778     if (!LocationUtil::isLocationChange(res)) {
0779         return {};
0780     }
0781 
0782     auto args = engine->newObject();
0783     mapArgumentsForLocation(args, LocationUtil::departureLocation(res), engine);
0784 
0785     // departure location
0786     mapDepartureArgumentsForRes(args, res);
0787     const auto dep = departure();
0788     mapArgumentsForPt(args, QLatin1StringView("departure"), dep, coachDataForReservation(res));
0789 
0790     // departure time
0791     auto depTime = dep.hasExpectedDepartureTime() ? dep.expectedDepartureTime() : dep.scheduledDepartureTime();
0792     if (!depTime.isValid()) {
0793         depTime = SortUtil::startDateTime(res);
0794     }
0795     args.setProperty(QStringLiteral("endTime"), engine->toScriptValue(depTime));
0796     if (depTime.timeSpec() == Qt::TimeZone) {
0797         args.setProperty(QStringLiteral("timeZone"), QString::fromUtf8(depTime.timeZone().id()));
0798     }
0799 
0800     // look for arrival for a preceding transfer
0801     const auto transfer = m_transferMgr->transfer(m_batchId, Transfer::Before);
0802     if (transfer.state() == Transfer::Selected) {
0803         const auto arr = PublicTransport::lastTransportSection(transfer.journey()).arrival();
0804         mapArgumentsForPt(args, QLatin1StringView("arrival"), arr, {});
0805         args.setProperty(QStringLiteral("beginTime"), engine->toScriptValue(arr.hasExpectedArrivalTime() ? arr.expectedArrivalTime() : arr.scheduledArrivalTime()));
0806         return args;
0807     }
0808 
0809     // ... or layover
0810     const auto prevResId = m_resMgr->previousBatch(m_batchId);
0811     const auto prevRes = m_resMgr->reservation(prevResId);
0812     if (!isLayover(prevRes, res)) {
0813         return args;
0814     }
0815     mapArrivalArgumesForRes(args, prevRes);
0816     const auto arr = m_liveDataMgr->arrival(prevResId);
0817     mapArgumentsForPt(args, QLatin1StringView("arrival"), arr, coachDataForReservation(prevRes));
0818 
0819     auto arrTime = arr.hasExpectedArrivalTime() ? arr.expectedArrivalTime() : arr.scheduledArrivalTime();
0820     if (!arrTime.isValid()) {
0821         arrTime = SortUtil::endDateTime(prevRes);
0822     }
0823     args.setProperty(QStringLiteral("beginTime"), engine->toScriptValue(arrTime));
0824 
0825     return args;
0826 }
0827 
0828 QJSValue TimelineDelegateController::mapArguments() const
0829 {
0830     const auto engine = qjsEngine(this);
0831     if (!engine || !m_resMgr || m_batchId.isEmpty() || !m_transferMgr || !m_liveDataMgr) {
0832         return {};
0833     }
0834 
0835     const auto res = m_resMgr->reservation(m_batchId);
0836     if (LocationUtil::isLocationChange(res)) {
0837         return {};
0838     }
0839 
0840     auto args = engine->newObject();
0841     mapArgumentsForLocation(args, LocationUtil::location(res), engine);
0842 
0843     // determine time on site, considering the following sources:
0844     // (1) the full days res is covering
0845     // (2) arrival time of a preceding location change, departure time of a following location change
0846     // (3) arrival time of a preceding transfer, departure time of a following transfer
0847 
0848     auto beginDt = SortUtil::startDateTime(res);
0849     beginDt.setTime({});
0850 
0851     for (auto prevResId = m_resMgr->previousBatch(m_batchId); !prevResId.isEmpty(); prevResId = m_resMgr->previousBatch(prevResId)) {
0852         const auto prevRes = m_resMgr->reservation(prevResId);
0853         if (LocationUtil::isLocationChange(prevRes)) {
0854             beginDt = std::max(SortUtil::endDateTime(prevRes), beginDt);
0855             break;
0856         }
0857     }
0858 
0859     auto transfer = m_transferMgr->transfer(m_batchId, Transfer::Before);
0860     if (transfer.state() == Transfer::Selected) {
0861         const auto arr = PublicTransport::lastTransportSection(transfer.journey()).arrival();
0862         mapArgumentsForPt(args, QLatin1StringView("arrival"), arr, {});
0863         beginDt = std::max(arr.hasExpectedArrivalTime() ? arr.expectedArrivalTime() : arr.scheduledArrivalTime(), beginDt);
0864     }
0865 
0866     args.setProperty(QStringLiteral("beginTime"), engine->toScriptValue(beginDt));
0867 
0868     auto endDt = SortUtil::endDateTime(res);
0869     if (endDt.isValid()) {
0870         endDt = endDt.addDays(1);
0871         endDt.setTime({});
0872     }
0873 
0874     transfer = m_transferMgr->transfer(m_batchId, Transfer::After);
0875     if (transfer.state() == Transfer::Selected) {
0876         const auto dep = PublicTransport::firstTransportSection(transfer.journey()).departure();
0877         mapArgumentsForPt(args, QLatin1StringView("departure"), dep, {});
0878         const auto depDt = dep.hasExpectedDepartureTime() ? dep.expectedDepartureTime() : dep.scheduledDepartureTime();
0879         endDt = endDt.isValid() ? std::min(endDt, depDt) : depDt;
0880     }
0881 
0882     // search the first location change after the end (which might not always be the next element)
0883     const auto dt = SortUtil::endDateTime(res);
0884     auto nextResId = m_resMgr->nextBatch(m_batchId);
0885     auto nextRes = m_resMgr->reservation(nextResId);
0886     while (dt.isValid() && !nextResId.isEmpty() && SortUtil::startDateTime(nextRes).date() <= dt.date()) {
0887         if (LocationUtil::isLocationChange(nextRes)) {
0888             const auto depDt = SortUtil::startDateTime(nextRes);
0889             if (depDt.isValid() && depDt >= SortUtil::endDateTime((res))) {
0890                 endDt = endDt.isValid() ? std::min(depDt, endDt) : depDt;
0891                 break;
0892             }
0893         }
0894 
0895         nextResId = m_resMgr->nextBatch(nextResId);
0896         nextRes = m_resMgr->reservation(nextResId);
0897     }
0898 
0899     if (endDt.isValid()) {
0900         args.setProperty(QStringLiteral("endTime"), engine->toScriptValue(endDt));
0901     }
0902 
0903     return args;
0904 }
0905 
0906 void TimelineDelegateController::addToCalendar(KCalendarCore::Calendar *cal)
0907 {
0908     const auto resIds = m_resMgr->reservationsForBatch(m_batchId);
0909     if (resIds.isEmpty() || !cal) {
0910         return;
0911     }
0912     QVector<QVariant> reservations;
0913     reservations.reserve(resIds.size());
0914     for (const auto &resId : resIds) {
0915         reservations.push_back(m_resMgr->reservation(resId));
0916     }
0917 
0918     const auto existingEvents = CalendarHandler::findEvents(cal, reservations.at(0));
0919     KCalendarCore::Event::Ptr event;
0920     if (existingEvents.size() == 1) {
0921         event = existingEvents.at(0);
0922         event->startUpdates();
0923     } else {
0924         event = KCalendarCore::Event::Ptr(new KCalendarCore::Event);
0925     }
0926 
0927     CalendarHandler::fillEvent(reservations, event);
0928     CalendarHelper::fillPreTransfer(event, m_transferMgr->transfer(m_batchId, Transfer::Before));
0929 
0930     if (existingEvents.size() == 1) {
0931         event->endUpdates();
0932     } else {
0933         cal->addEvent(event);
0934     }
0935 }
0936 
0937 static void applyVehicleLayout(KPublicTransport::Stopover &stop, const KPublicTransport::Stopover &layout)
0938 {
0939     if (PublicTransport::isSameStopoverForLayout(stop, layout)) {
0940         stop = KPublicTransport::Stopover::merge(stop, layout);
0941     }
0942 }
0943 
0944 void TimelineDelegateController::setVehicleLayout(const KPublicTransport::Stopover& stopover, bool arrival)
0945 {
0946     auto jny = journey();
0947     if (!arrival) {
0948         auto dep = jny.departure();
0949         applyVehicleLayout(dep, stopover);
0950         jny.setDeparture(dep);
0951         m_liveDataMgr->applyJourney(m_batchId, jny);
0952     } else {
0953         auto arr = jny.arrival();
0954         applyVehicleLayout(arr, stopover);
0955         jny.setArrival(arr);
0956         m_liveDataMgr->applyJourney(m_batchId, jny);
0957     }
0958 
0959     Q_EMIT layoutChanged();
0960 }
0961 
0962 QString TimelineDelegateController::departurePlatformSections() const
0963 {
0964     if (!m_resMgr) {
0965         return {};
0966     }
0967     const auto res = m_resMgr->reservation(m_batchId);
0968     return platformSectionsForCoachData(departure(), coachDataForReservation(res));
0969 }
0970 
0971 QString TimelineDelegateController::arrivalPlatformSections() const
0972 {
0973     if (!m_resMgr) {
0974         return {};
0975     }
0976     const auto res = m_resMgr->reservation(m_batchId);
0977     return platformSectionsForCoachData(arrival(), coachDataForReservation(res));
0978 }
0979 
0980 QStringList TimelineDelegateController::documentIds() const
0981 {
0982     if (!m_resMgr || !m_documentMgr || m_batchId.isEmpty()) {
0983         return {};
0984     }
0985 
0986     QStringList result;
0987     const auto resIds = m_resMgr->reservationsForBatch(m_batchId);
0988     for (const auto &resId : resIds) {
0989         const auto res = m_resMgr->reservation(resId);
0990         const auto docIds = DocumentUtil::documentIds(res);
0991         for (const auto &docId : docIds) {
0992             const auto id = docId.toString();
0993             if (!id.isEmpty() && m_documentMgr->hasDocument(id)) {
0994                 result.push_back(id);
0995             }
0996         }
0997     }
0998 
0999     std::sort(result.begin(), result.end());
1000     result.erase(std::unique(result.begin(), result.end()), result.end());
1001     return result;
1002 }
1003 
1004 #include "moc_timelinedelegatecontroller.cpp"