diff --git a/autotests/transfertest.cpp b/autotests/transfertest.cpp index 0fd2e0d..07ce8ec 100644 --- a/autotests/transfertest.cpp +++ b/autotests/transfertest.cpp @@ -1,139 +1,139 @@ /* 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 class TransferTest : 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); } QByteArray readFile(const QString &fn) { QFile f(fn); f.open(QFile::ReadOnly); return f.readAll(); } private Q_SLOTS: void initTestCase() { qputenv("TZ", "UTC"); QStandardPaths::setTestModeEnabled(true); qRegisterMetaType(); } void testTransferManager() { ReservationManager resMgr; clearReservations(&resMgr); TripGroupManager::clear(); TripGroupManager tgMgr; tgMgr.setReservationManager(&resMgr); TransferManager::clear(); TransferManager mgr; mgr.overrideCurrentDateTime(QDateTime({2017, 1, 1}, {})); mgr.setReservationManager(&resMgr); mgr.setTripGroupManager(&tgMgr); QSignalSpy addSpy(&mgr, &TransferManager::transferAdded); QSignalSpy changeSpy(&mgr, &TransferManager::transferChanged); QSignalSpy removeSpy(&mgr, &TransferManager::transferRemoved); resMgr.importReservation(readFile(QLatin1String(SOURCE_DIR "/../tests/randa2017.json"))); // resMgr.importReservation(readFile(QLatin1String(SOURCE_DIR "/../tests/akademy2017.json"))); // resMgr.importReservation(readFile(QLatin1String(SOURCE_DIR "/../tests/akademy2018-program.json"))); QCOMPARE(addSpy.size(), 3); // to/from home, and one inbetween auto batchId = resMgr.batches().at(0); auto transfer = mgr.transfer(batchId, Transfer::Before); QCOMPARE(transfer.state(), Transfer::Pending); QCOMPARE(transfer.anchorTime(), QDateTime({2017, 9, 10}, {6, 45}, QTimeZone("Europe/Berlin"))); QCOMPARE(transfer.alignment(), Transfer::Before); QCOMPARE(transfer.reservationId(), batchId); QVERIFY(!transfer.from().hasCoordinate()); QVERIFY(transfer.to().hasCoordinate()); QCOMPARE(transfer.to().name(), QLatin1String("Berlin Tegel")); transfer = mgr.transfer(batchId, Transfer::After); QCOMPARE(transfer.state(), Transfer::UndefinedState); // verify persistence TransferManager mgr2; transfer = mgr2.transfer(batchId, Transfer::Before); QCOMPARE(transfer.state(), Transfer::Pending); QCOMPARE(transfer.anchorTime(), QDateTime({2017, 9, 10}, {6, 45}, QTimeZone("Europe/Berlin"))); QCOMPARE(transfer.alignment(), Transfer::Before); QCOMPARE(transfer.reservationId(), batchId); QVERIFY(!transfer.from().hasCoordinate()); QVERIFY(transfer.to().hasCoordinate()); QCOMPARE(transfer.to().name(), QLatin1String("Berlin Tegel")); // operations addSpy.clear(); changeSpy.clear(); removeSpy.clear(); KPublicTransport::Journey jny; KPublicTransport::JourneySection section; section.setScheduledDepartureTime(QDateTime({2017, 9, 10}, {5, 30})); section.setScheduledArrivalTime(QDateTime({2017, 9, 10}, {6, 0})); jny.setSections({section}); mgr.setJourneyForTransfer(transfer, jny); QCOMPARE(addSpy.size(), 0); QCOMPARE(changeSpy.size(), 1); QCOMPARE(removeSpy.size(), 0); transfer = mgr.transfer(batchId, Transfer::Before); - QCOMPARE(transfer.state(), Transfer::Valid); + QCOMPARE(transfer.state(), Transfer::Selected); QCOMPARE(transfer.journey().sections().size(), 1); addSpy.clear(); changeSpy.clear(); removeSpy.clear(); mgr.discardTransfer(transfer); QCOMPARE(addSpy.size(), 0); QCOMPARE(changeSpy.size(), 0); QCOMPARE(removeSpy.size(), 1); transfer = mgr.transfer(batchId, Transfer::Before); QCOMPARE(transfer.state(), Transfer::Discarded); } }; QTEST_GUILESS_MAIN(TransferTest) #include "transfertest.moc" diff --git a/src/app/TransferDelegate.qml b/src/app/TransferDelegate.qml index 4c3082b..7424bad 100644 --- a/src/app/TransferDelegate.qml +++ b/src/app/TransferDelegate.qml @@ -1,95 +1,95 @@ /* 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 . */ import QtQuick 2.5 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.1 as QQC2 import org.kde.kirigami 2.4 as Kirigami import org.kde.kpublictransport 1.0 import org.kde.itinerary 1.0 import "." as App App.TimelineDelegate { id: root property var transfer property bool journeyDetailsExpanded: false headerIconSource: "qrc:///images/transfer.svg" headerItem: RowLayout { QQC2.Label { text: i18n("%1 to %2", transfer.from.name, transfer.to.name) color: Kirigami.Theme.textColor Layout.fillWidth: true } QQC2.Label { text: Localizer.formatTime(transfer.journey, "scheduledDepartureTime") - visible: transfer.state == Transfer.Valid + visible: transfer.state == Transfer.Selected color: Kirigami.Theme.textColor } QQC2.Label { text: (transfer.journey.departureDelay >= 0 ? "+" : "") + transfer.journey.departureDelay color: (transfer.journey.departureDelay > 1) ? Kirigami.Theme.negativeTextColor : Kirigami.Theme.positiveTextColor - visible: transfer.state == Transfer.Valid && transfer.journey.hasExpectedDepartureTime + visible: transfer.state == Transfer.Selected && transfer.journey.hasExpectedDepartureTime } } contentItem: ColumnLayout { ListView { delegate: App.JourneySectionDelegate{} - model: (transfer.state == Transfer.Valid && journeyDetailsExpanded) ? transfer.journey.sections : 0 + model: (transfer.state == Transfer.Selected && journeyDetailsExpanded) ? transfer.journey.sections : 0 implicitHeight: contentHeight Layout.fillWidth: true boundsBehavior: Flickable.StopAtBounds } App.JourneySummaryDelegate { journey: transfer.journey - visible: transfer.state == Transfer.Valid && !journeyDetailsExpanded + visible: transfer.state == Transfer.Selected && !journeyDetailsExpanded Layout.fillWidth: true } QQC2.Button { text: i18n("Select...") - visible: transfer.state == Transfer.Valid && journeyDetailsExpanded + visible: transfer.state == Transfer.Selected && journeyDetailsExpanded onClicked: applicationWindow().pageStack.push(detailsComponent); } RowLayout { visible: transfer.state == Transfer.Pending QQC2.Label { text: i18n("Select...") Layout.fillWidth: true } QQC2.ToolButton { icon.name: "edit-delete" onClicked: TransferManager.discardTransfer(transfer) } } } Component { id: detailsComponent App.TransferPage { transfer: root.transfer } } onClicked: { - if (transfer.state == Transfer.Valid) { + if (transfer.state == Transfer.Selected) { journeyDetailsExpanded = !journeyDetailsExpanded; } else { applicationWindow().pageStack.push(detailsComponent); } } } diff --git a/src/app/transfer.h b/src/app/transfer.h index 351c49e..c528df1 100644 --- a/src/app/transfer.h +++ b/src/app/transfer.h @@ -1,92 +1,92 @@ /* 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 TRANSFER_H #define TRANSFER_H #include #include #include #include class TransferPrivate; class QJsonObject; /** Describes an actual or potential transfer between two reservation elements. */ class Transfer { Q_GADGET Q_PROPERTY(Alignment alignment READ alignment WRITE setAlignment) Q_PROPERTY(State state READ state WRITE setState) Q_PROPERTY(KPublicTransport::Location from READ from WRITE setFrom) Q_PROPERTY(KPublicTransport::Location to READ to WRITE setTo) Q_PROPERTY(KPublicTransport::Journey journey READ journey WRITE setJourney) Q_PROPERTY(QString reservationId READ reservationId WRITE setReservationId) Q_PROPERTY(QDateTime anchorTime READ anchorTime WRITE setAnchorTime) public: Transfer(); Transfer(const Transfer&); ~Transfer(); Transfer& operator=(const Transfer&); /** Aligned to the begin or end of the corresponding reservation. */ enum Alignment { Before, After }; Q_ENUM(Alignment) Alignment alignment() const; void setAlignment(Alignment alignment); /** No journey selected, journey selected, or explicitly discarded. */ enum State { UndefinedState, Pending, - Valid, + Selected, Discarded }; Q_ENUM(State) State state() const; void setState(State state); KPublicTransport::Location from() const; void setFrom(const KPublicTransport::Location &from); KPublicTransport::Location to() const; void setTo(const KPublicTransport::Location &to); KPublicTransport::Journey journey() const; void setJourney(const KPublicTransport::Journey &journey); QString reservationId() const; void setReservationId(const QString &resId); /** The time-wise fixed side of this transfer, ie. the start for Alignment::After and end for Alignment::Before. */ QDateTime anchorTime() const; void setAnchorTime(const QDateTime &dt); static QJsonObject toJson(const Transfer &transfer); static Transfer fromJson(const QJsonObject &obj); private: QExplicitlySharedDataPointer d; }; Q_DECLARE_METATYPE(Transfer) #endif // TRANSFER_H diff --git a/src/app/transfermanager.cpp b/src/app/transfermanager.cpp index d75c23b..8b974e5 100644 --- a/src/app/transfermanager.cpp +++ b/src/app/transfermanager.cpp @@ -1,401 +1,401 @@ /* 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 "transfermanager.h" #include "logging.h" #include "publictransport.h" #include "reservationmanager.h" #include "tripgroup.h" #include "tripgroupmanager.h" #include #include #include #include #include #include #include #include #include using namespace KItinerary; // bump this to trigger a full rescan for transfers enum { CurrentFullScanVerion = 1 }; TransferManager* TransferManager::s_instance = nullptr; TransferManager::TransferManager(QObject *parent) : QObject(parent) { s_instance = this; QSettings settings; settings.beginGroup(QStringLiteral("HomeLocation")); m_homeLat = settings.value(QStringLiteral("Latitude"), NAN).toFloat(); m_homeLon = settings.value(QStringLiteral("Longitude"), NAN).toFloat(); } TransferManager::~TransferManager() = default; void TransferManager::setReservationManager(ReservationManager *resMgr) { m_resMgr = resMgr; connect(m_resMgr, &ReservationManager::batchAdded, this, qOverload(&TransferManager::checkReservation)); connect(m_resMgr, &ReservationManager::batchChanged, this, qOverload(&TransferManager::checkReservation)); connect(m_resMgr, &ReservationManager::batchRemoved, this, &TransferManager::reservationRemoved); rescan(); } void TransferManager::setTripGroupManager(TripGroupManager* tgMgr) { m_tgMgr = tgMgr; connect(m_tgMgr, &TripGroupManager::tripGroupAdded, this, &TransferManager::tripGroupChanged); connect(m_tgMgr, &TripGroupManager::tripGroupChanged, this, &TransferManager::tripGroupChanged); rescan(); } Transfer TransferManager::transfer(const QString &resId, Transfer::Alignment alignment) const { const auto it = m_transfers[alignment].constFind(resId); if (it != m_transfers[alignment].constEnd()) { return it.value(); } const auto t = readFromFile(resId, alignment); m_transfers[alignment].insert(resId, t); return t; } void TransferManager::setJourneyForTransfer(Transfer transfer, const KPublicTransport::Journey &journey) { - transfer.setState(Transfer::Valid); + transfer.setState(Transfer::Selected); transfer.setJourney(journey); m_transfers[transfer.alignment()].insert(transfer.reservationId(), transfer); writeToFile(transfer); emit transferChanged(transfer); } void TransferManager::discardTransfer(Transfer transfer) { transfer.setState(Transfer::Discarded); transfer.setJourney({}); m_transfers[transfer.alignment()].insert(transfer.reservationId(), transfer); writeToFile(transfer); emit transferRemoved(transfer.reservationId(), transfer.alignment()); } float TransferManager::homeLatitude() const { return m_homeLat; } void TransferManager::setHomeLatitude(float lat) { if (m_homeLat == lat) { return; } m_homeLat = lat; QSettings settings; settings.beginGroup(QStringLiteral("HomeLocation")); settings.setValue(QStringLiteral("Latitude"), m_homeLat); emit homeLocationChanged(); } float TransferManager::homeLongitude() const { return m_homeLon; } void TransferManager::setHomeLongitude(float lon) { if (m_homeLon == lon) { return; } m_homeLon = lon; QSettings settings; settings.beginGroup(QStringLiteral("HomeLocation")); settings.setValue(QStringLiteral("Longitude"), m_homeLon); emit homeLocationChanged(); } bool TransferManager::hasHomeLocation() const { return !std::isnan(m_homeLat) && !std::isnan(m_homeLon); } KPublicTransport::Location TransferManager::homeLocation() const { if (!hasHomeLocation()) { return {}; } KPublicTransport::Location l; l.setName(i18n("Home")); l.setCoordinate(m_homeLat, m_homeLon); return l; } void TransferManager::rescan() { if (!m_resMgr || !m_tgMgr) { return; } QSettings settings; settings.beginGroup(QStringLiteral("TransferManager")); const auto previousFullScanVersion = settings.value(QLatin1String("FullScan"), 0).toInt(); if (previousFullScanVersion >= CurrentFullScanVerion) { return; } qCInfo(Log) << "Performing a full transfer search..." << previousFullScanVersion; for (const auto &batchId : m_resMgr->batches()) { checkReservation(batchId); } settings.setValue(QStringLiteral("FullScan"), CurrentFullScanVerion); } void TransferManager::checkReservation(const QString &resId) { const auto res = m_resMgr->reservation(resId); const auto now = currentDateTime(); if (SortUtil::endDateTime(res) < now) { return; } checkReservation(resId, res, Transfer::After); if (SortUtil::startDateTime(res) < now) { return; } checkReservation(resId, res, Transfer::Before); } void TransferManager::checkReservation(const QString &resId, const QVariant &res, Transfer::Alignment alignment) { auto t = transfer(resId, alignment); if (t.state() == Transfer::Discarded) { // user already discarded this return; } // in case this is new t.setReservationId(resId); t.setAlignment(alignment); alignment == Transfer::Before ? checkTransferBefore(resId, res, t) : checkTransferAfter(resId, res, t); } void TransferManager::checkTransferBefore(const QString &resId, const QVariant &res, Transfer transfer) { qDebug() << resId << res << transfer.state(); transfer.setAnchorTime(SortUtil::startDateTime(res)); const auto isLocationChange = LocationUtil::isLocationChange(res); if (isLocationChange) { transfer.setTo(PublicTransport::locationFromPlace(LocationUtil::departureLocation(res))); } else { transfer.setTo(PublicTransport::locationFromPlace(LocationUtil::location(res))); } // TODO pre-transfers should happen in the following cases: // - res is a location change and we are currently at home (== first element in a trip group) // - res is a location change and we are not at the departure location yet // - res is an event and we are not at its location already if (isLocationChange && isFirstInTripGroup(resId)) { transfer.setFrom(homeLocation()); addOrUpdateTransfer(transfer); return; } const auto prevResId = m_resMgr->previousBatch(resId); if (prevResId.isEmpty()) { removeTransfer(transfer); return; } const auto prevRes = m_resMgr->reservation(prevResId); // TODO removeTransfer(transfer); } void TransferManager::checkTransferAfter(const QString &resId, const QVariant &res, Transfer transfer) { qDebug() << resId << res << transfer.state(); transfer.setAnchorTime(SortUtil::endDateTime(res)); const auto isLocationChange = LocationUtil::isLocationChange(res); if (isLocationChange) { transfer.setFrom(PublicTransport::locationFromPlace(LocationUtil::arrivalLocation(res))); } else { transfer.setFrom(PublicTransport::locationFromPlace(LocationUtil::location(res))); } // TODO post-transfer should happen in the following cases: // - res is a location change and we are the last element in a trip group (ie. going home) // - res is a location change and the following element is in a different location, or has a different departure location // - res is an event and the following or enclosing element is a lodging element if (isLocationChange && isLastInTripGroup(resId)) { transfer.setTo(homeLocation()); addOrUpdateTransfer(transfer); return; } if (isLocationChange) { const auto nextResId = m_resMgr->nextBatch(resId); if (nextResId.isEmpty()) { removeTransfer(transfer); return; } const auto nextRes = m_resMgr->reservation(nextResId); const auto curLoc = LocationUtil::arrivalLocation(res); QVariant nextLoc; if (LocationUtil::isLocationChange(nextRes)) { nextLoc = LocationUtil::departureLocation(nextRes); } else { nextLoc = LocationUtil::location(nextRes); } if (!curLoc.isNull() && !nextLoc.isNull() && !LocationUtil::isSameLocation(curLoc, nextLoc, LocationUtil::WalkingDistance)) { qDebug() << res << nextRes << LocationUtil::name(LocationUtil::arrivalLocation(res)) << LocationUtil::name(nextLoc); transfer.setTo(PublicTransport::locationFromPlace(nextLoc)); addOrUpdateTransfer(transfer); return; } } // TODO removeTransfer(transfer); } void TransferManager::reservationRemoved(const QString &resId) { m_transfers[Transfer::Before].remove(resId); m_transfers[Transfer::After].remove(resId); removeFile(resId, Transfer::Before); removeFile(resId, Transfer::After); // TODO updates to adjacent transfers? emit transferRemoved(resId, Transfer::Before); emit transferRemoved(resId, Transfer::After); } void TransferManager::tripGroupChanged(const QString &tgId) { const auto tg = m_tgMgr->tripGroup(tgId); for (const auto &resId : tg.elements()) { checkReservation(resId); } } bool TransferManager::isFirstInTripGroup(const QString &resId) const { const auto tgId = m_tgMgr->tripGroupForReservation(resId); return tgId.elements().empty() ? false : tgId.elements().at(0) == resId; } bool TransferManager::isLastInTripGroup(const QString &resId) const { const auto tgId = m_tgMgr->tripGroupForReservation(resId); return tgId.elements().empty() ? false : tgId.elements().constLast() == resId; } void TransferManager::addOrUpdateTransfer(Transfer t) { if (t.state() == Transfer::UndefinedState) { // newly added t.setState(Transfer::Pending); m_transfers[t.alignment()].insert(t.reservationId(), t); writeToFile(t); emit transferAdded(t); } else { // update existing data m_transfers[t.alignment()].insert(t.reservationId(), t); writeToFile(t); emit transferChanged(t); } } void TransferManager::removeTransfer(const Transfer &t) { if (t.state() == Transfer::UndefinedState) { // this was never added return; } m_transfers[t.alignment()].remove(t.reservationId()); removeFile(t.reservationId(), t.alignment()); emit transferRemoved(t.reservationId(), t.alignment()); } static QString transferBasePath() { return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1String("/transfers/"); } Transfer TransferManager::readFromFile(const QString& resId, Transfer::Alignment alignment) const { const QString fileName = transferBasePath() + resId + (alignment == Transfer::Before ? QLatin1String("-BEFORE.json") : QLatin1String("-AFTER.json")); QFile f(fileName); if (!f.open(QFile::ReadOnly)) { return {}; } return Transfer::fromJson(QJsonDocument::fromJson(f.readAll()).object()); } void TransferManager::writeToFile(const Transfer &transfer) const { QDir().mkpath(transferBasePath()); const QString fileName = transferBasePath() + transfer.reservationId() + (transfer.alignment() == Transfer::Before ? QLatin1String("-BEFORE.json") : QLatin1String("-AFTER.json")); QFile f(fileName); if (!f.open(QFile::WriteOnly)) { qCWarning(Log) << "Failed to store transfer data" << f.fileName() << f.errorString(); return; } f.write(QJsonDocument(Transfer::toJson(transfer)).toJson()); } void TransferManager::removeFile(const QString &resId, Transfer::Alignment alignment) const { const QString fileName = transferBasePath() + resId + (alignment == Transfer::Before ? QLatin1String("-BEFORE.json") : QLatin1String("-AFTER.json")); QFile::remove(fileName); } TransferManager* TransferManager::instance() { return s_instance; } QDateTime TransferManager::currentDateTime() const { if (Q_UNLIKELY(m_nowOverride.isValid())) { return m_nowOverride; } return QDateTime::currentDateTime(); } void TransferManager::overrideCurrentDateTime(const QDateTime &dt) { m_nowOverride = dt; } void TransferManager::clear() { QDir d(transferBasePath()); qCInfo(Log) << "deleting" << transferBasePath(); d.removeRecursively(); QSettings settings; settings.beginGroup(QStringLiteral("TransferManager")); settings.remove(QStringLiteral("FullScan")); }