diff --git a/autotests/timelinedelegatecontrollertest.cpp b/autotests/timelinedelegatecontrollertest.cpp index 1d446d2..0f6cada 100644 --- a/autotests/timelinedelegatecontrollertest.cpp +++ b/autotests/timelinedelegatecontrollertest.cpp @@ -1,103 +1,146 @@ /* Copyright (C) 2019 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include #include #include #include #include #include #include using namespace KItinerary; class TimelineDelegateControllerTest : public QObject { Q_OBJECT private: void clearReservations(ReservationManager *mgr) { const auto batches = mgr->batches(); // copy, as this is getting modified in the process for (const auto &id : batches) { mgr->removeBatch(id); } QCOMPARE(mgr->batches().size(), 0); } private Q_SLOTS: void initTestCase() { qputenv("TZ", "UTC"); QStandardPaths::setTestModeEnabled(true); } void testEmptyController() { TimelineDelegateController controller; QCOMPARE(controller.isCurrent(), false); QCOMPARE(controller.progress(), 0.0f); controller.setBatchId(QStringLiteral("foo")); QCOMPARE(controller.isCurrent(), false); QCOMPARE(controller.progress(), 0.0f); ReservationManager mgr; controller.setReservationManager(&mgr); QCOMPARE(controller.isCurrent(), false); QCOMPARE(controller.progress(), 0.0f); } - void testController() + void testProgress() { ReservationManager mgr; clearReservations(&mgr); TrainTrip trip; trip.setTrainNumber(QStringLiteral("TGV 1235")); trip.setDepartureTime(QDateTime::currentDateTime().addDays(-1)); TrainReservation res; res.setReservationNumber(QStringLiteral("XXX007")); res.setReservationFor(trip); TimelineDelegateController controller; QSignalSpy currentSpy(&controller, &TimelineDelegateController::currentChanged); controller.setReservationManager(&mgr); mgr.addReservation(res); QCOMPARE(mgr.batches().size(), 1); const auto batchId = mgr.batches().at(0); controller.setBatchId(batchId); QCOMPARE(controller.isCurrent(), false); QCOMPARE(controller.progress(), 0.0f); trip.setArrivalTime(QDateTime::currentDateTime().addDays(1)); res.setReservationFor(trip); mgr.updateReservation(batchId, res); QCOMPARE(controller.isCurrent(), true); QCOMPARE(controller.progress(), 0.5f); QCOMPARE(currentSpy.size(), 1); } + + void testPreviousLocation() + { + ReservationManager mgr; + clearReservations(&mgr); + + TimelineDelegateController controller; + controller.setReservationManager(&mgr); + + { + TrainTrip trip; + trip.setTrainNumber(QStringLiteral("TGV 1235")); + trip.setDepartureTime(QDateTime::currentDateTime().addDays(2)); + TrainReservation res; + res.setReservationNumber(QStringLiteral("XXX007")); + res.setReservationFor(trip); + mgr.addReservation(res); + } + + QCOMPARE(mgr.batches().size(), 1); + const auto batchId = mgr.batches().at(0); + + controller.setBatchId(batchId); + QCOMPARE(controller.previousLocation(), QVariant()); + + TrainStation arrStation; + arrStation.setName(QStringLiteral("My Station")); + TrainTrip prevTrip; + prevTrip.setTrainNumber(QStringLiteral("ICE 1234")); + prevTrip.setDepartureTime(QDateTime::currentDateTime().addDays(1)); + prevTrip.setArrivalTime(QDateTime::currentDateTime().addDays(1)); + prevTrip.setArrivalStation(arrStation); + TrainReservation prevRes; + prevRes.setReservationNumber(QStringLiteral("XXX007")); + prevRes.setReservationFor(prevTrip); + + QSignalSpy changeSpy(&controller, &TimelineDelegateController::previousLocationChanged); + mgr.addReservation(prevRes); + + QCOMPARE(changeSpy.size(), 1); + QVERIFY(!controller.previousLocation().isNull()); + QCOMPARE(controller.previousLocation().value().name(), QLatin1String("My Station")); + } }; QTEST_GUILESS_MAIN(TimelineDelegateControllerTest) #include "timelinedelegatecontrollertest.moc" diff --git a/src/app/timelinedelegatecontroller.cpp b/src/app/timelinedelegatecontroller.cpp index 67d7f4f..02498f5 100644 --- a/src/app/timelinedelegatecontroller.cpp +++ b/src/app/timelinedelegatecontroller.cpp @@ -1,303 +1,309 @@ /* Copyright (C) 2019 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "timelinedelegatecontroller.h" #include "livedatamanager.h" #include "reservationmanager.h" #include #include #include #include #include #include #include #include #include #include QTimer* TimelineDelegateController::s_currentTimer = nullptr; int TimelineDelegateController::s_progressRefCount = 0; QTimer* TimelineDelegateController::s_progressTimer = nullptr; using namespace KItinerary; TimelineDelegateController::TimelineDelegateController(QObject *parent) : QObject(parent) { if (!s_currentTimer) { s_currentTimer = new QTimer(QCoreApplication::instance()); s_currentTimer->setSingleShot(true); s_currentTimer->setTimerType(Qt::VeryCoarseTimer); } connect(s_currentTimer, &QTimer::timeout, this, [this]() { checkForUpdate(m_batchId); }); } TimelineDelegateController::~TimelineDelegateController() = default; QObject* TimelineDelegateController::reservationManager() const { return m_resMgr; } void TimelineDelegateController::setReservationManager(QObject *resMgr) { if (m_resMgr == resMgr) { return; } m_resMgr = qobject_cast(resMgr); emit setupChanged(); emit contentChanged(); emit departureChanged(); emit arrivalChanged(); + emit previousLocationChanged(); connect(m_resMgr, &ReservationManager::batchChanged, this, &TimelineDelegateController::batchChanged); connect(m_resMgr, &ReservationManager::batchContentChanged, this, &TimelineDelegateController::batchChanged); + // ### could be done more efficiently + connect(m_resMgr, &ReservationManager::batchAdded, this, &TimelineDelegateController::previousLocationChanged); + connect(m_resMgr, &ReservationManager::batchRemoved, this, &TimelineDelegateController::previousLocationChanged); checkForUpdate(m_batchId); } QObject* TimelineDelegateController::liveDataManager() const { return m_liveDataMgr; } void TimelineDelegateController::setLiveDataManager(QObject* liveDataMgr) { if (m_liveDataMgr == liveDataMgr) { return; } m_liveDataMgr = qobject_cast(liveDataMgr); emit setupChanged(); emit departureChanged(); emit arrivalChanged(); connect(m_liveDataMgr, &LiveDataManager::arrivalUpdated, this, &TimelineDelegateController::checkForUpdate); connect(m_liveDataMgr, &LiveDataManager::departureUpdated, this, &TimelineDelegateController::checkForUpdate); connect(m_liveDataMgr, &LiveDataManager::arrivalUpdated, this, [this](const auto &batchId) { if (batchId == m_batchId) { emit arrivalChanged(); } }); connect(m_liveDataMgr, &LiveDataManager::departureUpdated, this, [this](const auto &batchId) { if (batchId == m_batchId) { emit departureChanged(); } }); checkForUpdate(m_batchId); } QString TimelineDelegateController::batchId() const { return m_batchId; } void TimelineDelegateController::setBatchId(const QString &batchId) { if (m_batchId == batchId) return; m_batchId = batchId; emit contentChanged(); emit departureChanged(); emit arrivalChanged(); + emit previousLocationChanged(); checkForUpdate(batchId); } bool TimelineDelegateController::isCurrent() const { return m_isCurrent; } void TimelineDelegateController::setCurrent(bool current, const QVariant &res) { if (current == m_isCurrent) { return; } m_isCurrent = current; emit currentChanged(); if (!LocationUtil::isLocationChange(res)) { return; } if (!s_progressTimer && m_isCurrent) { s_progressTimer = new QTimer(QCoreApplication::instance()); s_progressTimer->setInterval(std::chrono::minutes(1)); s_progressTimer->setTimerType(Qt::VeryCoarseTimer); s_progressTimer->setSingleShot(false); } if (m_isCurrent) { connect(s_progressTimer, &QTimer::timeout, this, &TimelineDelegateController::progressChanged); if (s_progressRefCount++ == 0) { s_progressTimer->start(); } } else { disconnect(s_progressTimer, &QTimer::timeout, this, &TimelineDelegateController::progressChanged); if (--s_progressRefCount == 0) { s_progressTimer->stop(); } } } float TimelineDelegateController::progress() const { if (!m_resMgr || m_batchId.isEmpty() || !m_isCurrent) { return 0.0f; } const auto res = m_resMgr->reservation(m_batchId); const auto startTime = liveStartDateTime(res); const auto endTime = liveEndDateTime(res); const auto tripLength = startTime.secsTo(endTime); if (tripLength <= 0) { return 0.0f; } const auto progress = startTime.secsTo(QDateTime::currentDateTime()); return std::min(std::max(0.0f, (float)progress / (float)tripLength), 1.0f); } KPublicTransport::Departure TimelineDelegateController::arrival() const { if (!m_liveDataMgr || m_batchId.isEmpty()) { return {}; } return m_liveDataMgr->arrival(m_batchId); } KPublicTransport::Departure TimelineDelegateController::departure() const { if (!m_liveDataMgr || m_batchId.isEmpty()) { return {}; } return m_liveDataMgr->departure(m_batchId); } void TimelineDelegateController::checkForUpdate(const QString& batchId) { if (!m_resMgr || m_batchId.isEmpty()) { setCurrent(false); return; } if (batchId != m_batchId) { return; } const auto res = m_resMgr->reservation(batchId); const auto now = QDateTime::currentDateTime(); const auto startTime = relevantStartDateTime(res); const auto endTime = liveEndDateTime(res); setCurrent(startTime < now && now < endTime, res); if (now < startTime) { scheduleNextUpdate(std::chrono::seconds(now.secsTo(startTime) + 1)); } else if (now < endTime) { scheduleNextUpdate(std::chrono::seconds(now.secsTo(endTime) + 1)); } } QDateTime TimelineDelegateController::relevantStartDateTime(const QVariant &res) const { auto startTime = SortUtil::startDateTime(res); if (JsonLd::isA(res)) { const auto flight = res.value().reservationFor().value(); if (flight.boardingTime().isValid()) { startTime = flight.boardingTime(); } } else if (JsonLd::isA(res) || JsonLd::isA(res)) { startTime = startTime.addSecs(-5 * 60); } return startTime; } QDateTime TimelineDelegateController::liveStartDateTime(const QVariant& res) const { if (m_liveDataMgr) { const auto dep = m_liveDataMgr->departure(m_batchId); if (dep.expectedDepartureTime().isValid()) { return dep.expectedDepartureTime(); } } return SortUtil::startDateTime(res); } QDateTime TimelineDelegateController::liveEndDateTime(const QVariant& res) const { if (m_liveDataMgr) { const auto arr = m_liveDataMgr->arrival(m_batchId); if (arr.expectedArrivalTime().isValid()) { return arr.expectedArrivalTime(); } } return SortUtil::endtDateTime(res); } void TimelineDelegateController::scheduleNextUpdate(std::chrono::milliseconds ms) { if (s_currentTimer->isActive() && s_currentTimer->remainingTimeAsDuration() < ms) { return; } s_currentTimer->start(ms); } void TimelineDelegateController::batchChanged(const QString& batchId) { if (batchId != m_batchId || m_batchId.isEmpty()) { return; } checkForUpdate(batchId); emit contentChanged(); emit arrivalChanged(); emit departureChanged(); + emit previousLocationChanged(); } QVariant TimelineDelegateController::previousLocation() const { if (m_batchId.isEmpty() || !m_resMgr) { return {}; } const auto prevBatch = m_resMgr->previousBatch(m_batchId); if (prevBatch.isEmpty()) { return {}; } const auto res = m_resMgr->reservation(prevBatch); const auto endTime = SortUtil::endtDateTime(res); if (endTime < QDateTime::currentDateTime()) { // TODO take live data into account (also for notification!) // past event, we can use GPS rather than predict our location from the itinerary return {}; } if (LocationUtil::isLocationChange(res)) { return LocationUtil::arrivalLocation(res); } else { return LocationUtil::location(res); } } diff --git a/src/app/timelinedelegatecontroller.h b/src/app/timelinedelegatecontroller.h index ea94b57..642accb 100644 --- a/src/app/timelinedelegatecontroller.h +++ b/src/app/timelinedelegatecontroller.h @@ -1,103 +1,104 @@ /* Copyright (C) 2019 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TIMELINEDELEGATECONTROLLER_H #define TIMELINEDELEGATECONTROLLER_H #include #include #include #include class QDateTime; class QTimer; class LiveDataManager; class ReservationManager; /** C++ side logic for timeline delegates. */ class TimelineDelegateController : public QObject { Q_OBJECT Q_PROPERTY(QObject* reservationManager READ reservationManager WRITE setReservationManager NOTIFY setupChanged) Q_PROPERTY(QObject* liveDataManager READ liveDataManager WRITE setLiveDataManager NOTIFY setupChanged) Q_PROPERTY(QString batchId READ batchId WRITE setBatchId NOTIFY contentChanged) Q_PROPERTY(bool isCurrent READ isCurrent NOTIFY currentChanged) Q_PROPERTY(float progress READ progress NOTIFY progressChanged) /** The location we are in before this element begins. * This is only relevant for future elements, past elements, or elements without a non-current predecessor return nothing here. */ - Q_PROPERTY(QVariant previousLocation READ previousLocation NOTIFY contentChanged) // TODO technically we need to update this too when a new reservation was inserted before this + Q_PROPERTY(QVariant previousLocation READ previousLocation NOTIFY previousLocationChanged) Q_PROPERTY(KPublicTransport::Departure arrival READ arrival NOTIFY arrivalChanged) Q_PROPERTY(KPublicTransport::Departure departure READ departure NOTIFY departureChanged) public: TimelineDelegateController(QObject *parent = nullptr); ~TimelineDelegateController(); QObject* reservationManager() const; void setReservationManager(QObject *resMgr); QObject* liveDataManager() const; void setLiveDataManager(QObject *liveDataMgr); QString batchId() const; void setBatchId(const QString &batchId); bool isCurrent() const; float progress() const; QVariant previousLocation() const; KPublicTransport::Departure arrival() const; KPublicTransport::Departure departure() const; Q_SIGNALS: void setupChanged(); void contentChanged(); void currentChanged(); void progressChanged(); void arrivalChanged(); void departureChanged(); + void previousLocationChanged(); private: void setCurrent(bool current, const QVariant &res = {}); void checkForUpdate(const QString &batchId); /** Time at which we consider @p res "current". */ QDateTime relevantStartDateTime(const QVariant &res) const; /** Time at which the event starts/stops based on realtime data. */ QDateTime liveStartDateTime(const QVariant &res) const; QDateTime liveEndDateTime(const QVariant &res) const; void batchChanged(const QString &batchId); ReservationManager *m_resMgr = nullptr; // ### should this be static? LiveDataManager *m_liveDataMgr = nullptr; QString m_batchId; bool m_isCurrent = false; static void scheduleNextUpdate(std::chrono::milliseconds ms); static QTimer *s_currentTimer; static int s_progressRefCount; static QTimer *s_progressTimer; }; #endif // TIMELINEDELEGATECONTROLLER_H