diff --git a/autotests/timelinemodeltest.cpp b/autotests/timelinemodeltest.cpp index dc5cd39..892975a 100644 --- a/autotests/timelinemodeltest.cpp +++ b/autotests/timelinemodeltest.cpp @@ -1,340 +1,332 @@ /* Copyright (C) 2018 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 #include #include #include #include #include class TimelineModelTest : public QObject { Q_OBJECT private: void clearPasses(PkPassManager *mgr) { for (const auto id : mgr->passes()) mgr->removePass(id); } void clearReservations(ReservationManager *mgr) { for (const auto id : mgr->reservations()) { mgr->removeReservation(id); } } private slots: void initTestCase() { QStandardPaths::setTestModeEnabled(true); } void testModel() { PkPassManager mgr; clearPasses(&mgr); ReservationManager resMgr; clearReservations(&resMgr); resMgr.setPkPassManager(&mgr); TimelineModel model; model.setReservationManager(&resMgr); QSignalSpy insertSpy(&model, &TimelineModel::rowsInserted); QVERIFY(insertSpy.isValid()); QSignalSpy updateSpy(&model, &TimelineModel::dataChanged); QVERIFY(updateSpy.isValid()); QSignalSpy rmSpy(&model, &TimelineModel::rowsRemoved); QVERIFY(rmSpy.isValid()); QCOMPARE(model.rowCount(), 1); QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker); mgr.importPass(QUrl::fromLocalFile(QLatin1String(SOURCE_DIR "/data/boardingpass-v1.pkpass"))); QCOMPARE(insertSpy.size(), 1); QCOMPARE(insertSpy.at(0).at(1).toInt(), 0); QCOMPARE(insertSpy.at(0).at(2).toInt(), 0); QVERIFY(updateSpy.isEmpty()); QCOMPARE(model.rowCount(), 2); QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight); mgr.importPass(QUrl::fromLocalFile(QLatin1String(SOURCE_DIR "/data/boardingpass-v2.pkpass"))); QCOMPARE(insertSpy.size(), 1); QCOMPARE(updateSpy.size(), 1); QCOMPARE(updateSpy.at(0).at(0).toModelIndex().row(), 0); QCOMPARE(model.rowCount(), 2); clearReservations(&resMgr); QCOMPARE(insertSpy.size(), 1); QCOMPARE(updateSpy.size(), 1); QCOMPARE(rmSpy.size(), 1); QCOMPARE(model.rowCount(), 1); } void testNestedElements() { ReservationManager resMgr; clearReservations(&resMgr); TimelineModel model; model.setHomeCountryIsoCode(QStringLiteral("DE")); model.setReservationManager(&resMgr); QSignalSpy insertSpy(&model, &TimelineModel::rowsInserted); QVERIFY(insertSpy.isValid()); QSignalSpy updateSpy(&model, &TimelineModel::dataChanged); QVERIFY(updateSpy.isValid()); QSignalSpy rmSpy(&model, &TimelineModel::rowsRemoved); QVERIFY(rmSpy.isValid()); QCOMPARE(model.rowCount(), 1); QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker); resMgr.importReservation(QUrl::fromLocalFile(QLatin1String(SOURCE_DIR "/data/haus-randa-v1.json"))); QCOMPARE(insertSpy.size(), 3); QCOMPARE(insertSpy.at(0).at(1).toInt(), 0); QCOMPARE(insertSpy.at(0).at(2).toInt(), 0); QCOMPARE(insertSpy.at(1).at(1).toInt(), 1); QCOMPARE(insertSpy.at(1).at(2).toInt(), 1); QVERIFY(updateSpy.isEmpty()); QCOMPARE(model.rowCount(), 4); QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::CountryInfo); QCOMPARE(model.index(1, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Hotel); QCOMPARE(model.index(1, 0).data(TimelineModel::ElementRangeRole), TimelineModel::RangeBegin); QCOMPARE(model.index(2, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Hotel); QCOMPARE(model.index(2, 0).data(TimelineModel::ElementRangeRole), TimelineModel::RangeEnd); // move end date of a hotel booking: dataChanged on RangeBegin, move (or del/ins) on RangeEnd resMgr.importReservation(QUrl::fromLocalFile(QLatin1String(SOURCE_DIR "/data/haus-randa-v2.json"))); QCOMPARE(insertSpy.size(), 4); QCOMPARE(updateSpy.size(), 1); QCOMPARE(rmSpy.size(), 1); QCOMPARE(updateSpy.at(0).at(0).toModelIndex().row(), 1); QCOMPARE(insertSpy.at(2).at(1).toInt(), 0); QCOMPARE(insertSpy.at(2).at(2).toInt(), 0); QCOMPARE(rmSpy.at(0).at(1), 2); QCOMPARE(model.rowCount(), 4); // delete a split element const auto resId = model.data(model.index(1, 0), TimelineModel::ReservationIdRole).toString(); QVERIFY(!resId.isEmpty()); resMgr.removeReservation(resId); QCOMPARE(rmSpy.size(), 4); QCOMPARE(model.rowCount(), 1); QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker); } void testCountryInfos() { ReservationManager resMgr; clearReservations(&resMgr); TimelineModel model; model.setHomeCountryIsoCode(QStringLiteral("DE")); model.setReservationManager(&resMgr); QCOMPARE(model.rowCount(), 1); QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker); resMgr.importReservation(QUrl::fromLocalFile(QLatin1String(SOURCE_DIR "/data/flight-txl-lhr-sfo.json"))); QCOMPARE(model.rowCount(), 5); // 2x country info, 2x flights, today marker QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::CountryInfo); auto countryInfo = model.index(0, 0).data(TimelineModel::CountryInformationRole).value(); QCOMPARE(countryInfo.drivingSide(), KItinerary::KnowledgeDb::DrivingSide::Left); QCOMPARE(countryInfo.drivingSideDiffers(), true); QCOMPARE(countryInfo.powerPlugCompatibility(), CountryInformation::Incompatible); QCOMPARE(model.index(1, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight); QCOMPARE(model.index(2, 0).data(TimelineModel::ElementTypeRole), TimelineModel::CountryInfo); countryInfo = model.index(2, 0).data(TimelineModel::CountryInformationRole).value(); QCOMPARE(countryInfo.drivingSide(), KItinerary::KnowledgeDb::DrivingSide::Right); QCOMPARE(countryInfo.drivingSideDiffers(), false); QCOMPARE(countryInfo.powerPlugCompatibility(), CountryInformation::Incompatible); QCOMPARE(model.index(3, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight); QCOMPARE(model.index(4, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker); // remove the GB flight should also remove the GB country info auto resId = model.index(1, 0).data(TimelineModel::ReservationIdRole).toString(); resMgr.removeReservation(resId); QCOMPARE(model.rowCount(), 3); QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::CountryInfo); QCOMPARE(model.index(1, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight); QCOMPARE(model.index(2, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker); // remove the US flight should also remove the US country info resId = model.index(1, 0).data(TimelineModel::ReservationIdRole).toString(); resMgr.removeReservation(resId); QCOMPARE(model.rowCount(), 1); QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker); } void testWeatherElements() { using namespace KItinerary; ReservationManager resMgr; clearReservations(&resMgr); WeatherForecastManager weatherMgr; weatherMgr.setTestModeEnabled(true); TimelineModel model; model.setReservationManager(&resMgr); model.setWeatherForecastManager(&weatherMgr); QCOMPARE(model.rowCount(), 1); // no weather data, as we don't know where we are // Add an element that will result in a defined location GeoCoordinates geo; geo.setLatitude(52.0f); geo.setLongitude(13.0f); Airport a; a.setGeo(geo); Flight f; f.setArrivalAirport(a); f.setDepartureTime(QDateTime(QDate(2018, 1, 1), QTime(0, 0))); FlightReservation res; res.setReservationFor(f); resMgr.addReservation(res); QCOMPARE(model.rowCount(), 11); // 1x flight, 1x today, 9x weather QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight); QCOMPARE(model.index(1, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker); QCOMPARE(model.index(2, 0).data(TimelineModel::ElementTypeRole), TimelineModel::WeatherForecast); auto fc = model.index(2, 0).data(TimelineModel::WeatherForecastRole).value(); QVERIFY(fc.isValid()); QCOMPARE(fc.dateTime().date(), QDate::currentDate()); QCOMPARE(fc.minimumTemperature(), 13.0f); QCOMPARE(fc.maximumTemperature(), 52.0f); QCOMPARE(model.index(10, 0).data(TimelineModel::ElementTypeRole), TimelineModel::WeatherForecast); fc = model.index(10, 0).data(TimelineModel::WeatherForecastRole).value(); QVERIFY(fc.isValid()); QCOMPARE(fc.dateTime().date(), QDate::currentDate().addDays(8)); // Add a flight one day from now changing location mid-day geo.setLatitude(46.0f); geo.setLongitude(8.0f); a.setGeo(geo); f.setArrivalAirport(a); f.setDepartureTime(QDateTime(QDate::currentDate().addDays(1), QTime(12, 0))); + f.setArrivalTime(QDateTime(QDate::currentDate().addDays(1), QTime(14, 0))); res.setReservationFor(f); resMgr.addReservation(res); - QEXPECT_FAIL("", "weather for location changes not implemented yet", Continue); QCOMPARE(model.rowCount(), 13); // 2x flight, 1x today, 10x weather QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight); QCOMPARE(model.index(1, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker); QCOMPARE(model.index(2, 0).data(TimelineModel::ElementTypeRole), TimelineModel::WeatherForecast); fc = model.index(2, 0).data(TimelineModel::WeatherForecastRole).value(); QVERIFY(fc.isValid()); QCOMPARE(fc.dateTime().date(), QDate::currentDate()); QCOMPARE(model.index(3, 0).data(TimelineModel::ElementTypeRole), TimelineModel::WeatherForecast); fc = model.index(3, 0).data(TimelineModel::WeatherForecastRole).value(); QVERIFY(fc.isValid()); QCOMPARE(fc.minimumTemperature(), 13.0f); QCOMPARE(fc.maximumTemperature(), 52.0f); QCOMPARE(fc.dateTime(), QDateTime(QDate::currentDate().addDays(1), QTime(0, 0))); QCOMPARE(model.index(4, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight); QCOMPARE(model.index(5, 0).data(TimelineModel::ElementTypeRole), TimelineModel::WeatherForecast); fc = model.index(5, 0).data(TimelineModel::WeatherForecastRole).value(); QVERIFY(fc.isValid()); QCOMPARE(fc.minimumTemperature(), 8.0f); QCOMPARE(fc.maximumTemperature(), 46.0f); - QEXPECT_FAIL("", "weather for location changes not implemented yet", Continue); - QCOMPARE(fc.dateTime(), QDateTime(QDate::currentDate().addDays(1), QTime(12, 0))); + QCOMPARE(fc.dateTime(), QDateTime(QDate::currentDate().addDays(1), QTime(14, 0))); QCOMPARE(model.index(6, 0).data(TimelineModel::ElementTypeRole), TimelineModel::WeatherForecast); fc = model.index(6, 0).data(TimelineModel::WeatherForecastRole).value(); QCOMPARE(fc.minimumTemperature(), 8.0f); QCOMPARE(fc.maximumTemperature(), 46.0f); QVERIFY(fc.isValid()); - QEXPECT_FAIL("", "weather for location changes not implemented yet", Continue); QCOMPARE(fc.dateTime(), QDateTime(QDate::currentDate().addDays(2), QTime(0, 0))); // check we get update signals for all weather elements QSignalSpy spy(&model, &TimelineModel::dataChanged); QVERIFY(spy.isValid()); emit weatherMgr.forecastUpdated(); - QEXPECT_FAIL("", "weather for location changes not implemented yet", Continue); QCOMPARE(spy.size(), 10); fc = model.index(3, 0).data(TimelineModel::WeatherForecastRole).value(); QVERIFY(fc.isValid()); QCOMPARE(fc.minimumTemperature(), 13.0f); QCOMPARE(fc.maximumTemperature(), 52.0f); QCOMPARE(fc.dateTime(), QDateTime(QDate::currentDate().addDays(1), QTime(0, 0))); fc = model.index(9, 0).data(TimelineModel::WeatherForecastRole).value(); QVERIFY(fc.isValid()); QCOMPARE(fc.minimumTemperature(), 8.0f); QCOMPARE(fc.maximumTemperature(), 46.0f); - QEXPECT_FAIL("", "weather for location changes not implemented yet", Continue); QCOMPARE(fc.dateTime(), QDateTime(QDate::currentDate().addDays(5), QTime(0, 0))); // add a location change far in the future, this must not change anything geo.setLatitude(60.0f); geo.setLongitude(11.0f); a.setGeo(geo); f.setArrivalAirport(a); f.setDepartureTime(QDateTime(QDate::currentDate().addYears(1), QTime(6, 0))); res.setReservationFor(f); resMgr.addReservation(res); - QEXPECT_FAIL("", "weather for location changes not implemented yet", Continue); QCOMPARE(model.rowCount(), 14); fc = model.index(3, 0).data(TimelineModel::WeatherForecastRole).value(); QVERIFY(fc.isValid()); QCOMPARE(fc.minimumTemperature(), 13.0f); QCOMPARE(fc.maximumTemperature(), 52.0f); QCOMPARE(fc.dateTime(), QDateTime(QDate::currentDate().addDays(1), QTime(0, 0))); fc = model.index(9, 0).data(TimelineModel::WeatherForecastRole).value(); QVERIFY(fc.isValid()); QCOMPARE(fc.minimumTemperature(), 8.0f); QCOMPARE(fc.maximumTemperature(), 46.0f); - QEXPECT_FAIL("", "weather for location changes not implemented yet", Continue); QCOMPARE(fc.dateTime(), QDateTime(QDate::currentDate().addDays(5), QTime(0, 0))); // result is the same when data hasn't been added incrementally model.setReservationManager(nullptr); model.setReservationManager(&resMgr); - QEXPECT_FAIL("", "weather for location changes not implemented yet", Continue); QCOMPARE(model.rowCount(), 14); fc = model.index(3, 0).data(TimelineModel::WeatherForecastRole).value(); QVERIFY(fc.isValid()); QCOMPARE(fc.minimumTemperature(), 13.0f); QCOMPARE(fc.maximumTemperature(), 52.0f); QCOMPARE(fc.dateTime(), QDateTime(QDate::currentDate().addDays(1), QTime(0, 0))); fc = model.index(9, 0).data(TimelineModel::WeatherForecastRole).value(); QVERIFY(fc.isValid()); QCOMPARE(fc.minimumTemperature(), 8.0f); QCOMPARE(fc.maximumTemperature(), 46.0f); - QEXPECT_FAIL("", "weather for location changes not implemented yet", Continue); QCOMPARE(fc.dateTime(), QDateTime(QDate::currentDate().addDays(5), QTime(0, 0))); } }; QTEST_GUILESS_MAIN(TimelineModelTest) #include "timelinemodeltest.moc" diff --git a/src/app/timelinemodel.cpp b/src/app/timelinemodel.cpp index 3e4c306..5667220 100644 --- a/src/app/timelinemodel.cpp +++ b/src/app/timelinemodel.cpp @@ -1,510 +1,523 @@ /* Copyright (C) 2018 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 "timelinemodel.h" #include "countryinformation.h" #include "pkpassmanager.h" #include "reservationmanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KItinerary; static bool needsSplitting(const QVariant &res) { return res.userType() == qMetaTypeId(); } static QDateTime relevantDateTime(const QVariant &res, TimelineModel::RangeType range) { if (range == TimelineModel::RangeBegin || range == TimelineModel::SelfContained) { return SortUtil::startDateTime(res); } if (range == TimelineModel::RangeEnd) { return SortUtil::endtDateTime(res); } return {}; } static QString passId(const QVariant &res) { const auto passTypeId = JsonLdDocument::readProperty(res, "pkpassPassTypeIdentifier").toString(); const auto serialNum = JsonLdDocument::readProperty(res, "pkpassSerialNumber").toString(); if (passTypeId.isEmpty() || serialNum.isEmpty()) { return {}; } return passTypeId + QLatin1Char('/') + QString::fromUtf8(serialNum.toUtf8().toBase64(QByteArray::Base64UrlEncoding)); } static TimelineModel::ElementType elementType(const QVariant &res) { if (JsonLd::isA(res)) { return TimelineModel::Flight; } if (JsonLd::isA(res)) { return TimelineModel::Hotel; } if (JsonLd::isA(res)) { return TimelineModel::TrainTrip; } if (JsonLd::isA(res)) { return TimelineModel::BusTrip; } if (JsonLd::isA(res)) { return TimelineModel::Restaurant; } if (JsonLd::isA(res)) { return TimelineModel::TouristAttraction; } return {}; } static QString destinationCountry(const QVariant &res) { if (JsonLd::isA(res)) { return res.value().reservationFor().value().arrivalAirport().address().addressCountry(); } if (JsonLd::isA(res)) { return res.value().reservationFor().value().arrivalStation().address().addressCountry(); } if (JsonLd::isA(res)) { return res.value().reservationFor().value().address().addressCountry(); } if (JsonLd::isA(res)) { return res.value().reservationFor().value().arrivalStation().address().addressCountry(); } if (JsonLd::isA(res)) { return res.value().reservationFor().value().address().addressCountry(); } if (JsonLd::isA(res)) { return res.value().touristAttraction().address().addressCountry(); } return {}; } static GeoCoordinates geoCoordinate(const QVariant &res) { if (JsonLd::isA(res)) { return res.value().reservationFor().value().arrivalAirport().geo(); } if (JsonLd::isA(res)) { return res.value().reservationFor().value().arrivalStation().geo(); } if (JsonLd::isA(res)) { return res.value().reservationFor().value().arrivalStation().geo(); } if (JsonLd::isA(res)) { return res.value().reservationFor().value().geo(); } if (JsonLd::isA(res)) { return res.value().reservationFor().value().geo(); } if (JsonLd::isA(res)) { return res.value().touristAttraction().geo(); } return {}; } static bool isLocationChange(const QVariant &res) { return JsonLd::isA(res) || JsonLd::isA(res) || JsonLd::isA(res); } TimelineModel::TimelineModel(QObject *parent) : QAbstractListModel(parent) { } TimelineModel::~TimelineModel() = default; void TimelineModel::setReservationManager(ReservationManager* mgr) { // for auto tests only if (Q_UNLIKELY(!mgr)) { beginResetModel(); - disconnect(mgr, &ReservationManager::reservationAdded, this, &TimelineModel::reservationAdded); - disconnect(mgr, &ReservationManager::reservationUpdated, this, &TimelineModel::reservationUpdated); - disconnect(mgr, &ReservationManager::reservationRemoved, this, &TimelineModel::reservationRemoved); + disconnect(m_resMgr, &ReservationManager::reservationAdded, this, &TimelineModel::reservationAdded); + disconnect(m_resMgr, &ReservationManager::reservationUpdated, this, &TimelineModel::reservationUpdated); + disconnect(m_resMgr, &ReservationManager::reservationRemoved, this, &TimelineModel::reservationRemoved); m_resMgr = mgr; m_elements.clear(); endResetModel(); return; } beginResetModel(); m_resMgr = mgr; for (const auto &resId : mgr->reservations()) { const auto res = m_resMgr->reservation(resId); if (needsSplitting(res)) { m_elements.push_back(Element{resId, {}, relevantDateTime(res, RangeBegin), elementType(res), RangeBegin}); m_elements.push_back(Element{resId, {}, relevantDateTime(res, RangeEnd), elementType(res), RangeEnd}); } else { m_elements.push_back(Element{resId, {}, relevantDateTime(res, SelfContained), elementType(res), SelfContained}); } } m_elements.push_back(Element{{}, {}, QDateTime(QDate::currentDate(), QTime(0, 0)), TodayMarker, SelfContained}); std::sort(m_elements.begin(), m_elements.end(), [](const Element &lhs, const Element &rhs) { return lhs.dt < rhs.dt; }); connect(mgr, &ReservationManager::reservationAdded, this, &TimelineModel::reservationAdded); connect(mgr, &ReservationManager::reservationUpdated, this, &TimelineModel::reservationUpdated); connect(mgr, &ReservationManager::reservationRemoved, this, &TimelineModel::reservationRemoved); endResetModel(); updateInformationElements(); emit todayRowChanged(); } void TimelineModel::setWeatherForecastManager(WeatherForecastManager* mgr) { m_weatherMgr = mgr; updateWeatherElements(); connect(m_weatherMgr, &WeatherForecastManager::forecastUpdated, this, &TimelineModel::updateWeatherElements); } void TimelineModel::setHomeCountryIsoCode(const QString &isoCode) { m_homeCountry = isoCode; updateInformationElements(); } int TimelineModel::rowCount(const QModelIndex& parent) const { if (parent.isValid() || !m_resMgr) { return 0; } return m_elements.size(); } QVariant TimelineModel::data(const QModelIndex& index, int role) const { if (!index.isValid() || !m_resMgr) { return {}; } const auto &elem = m_elements.at(index.row()); const auto res = m_resMgr->reservation(elem.id); switch (role) { case PassIdRole: return passId(res); case SectionHeader: { if (elem.dt.isNull()) { return {}; } if (elem.dt.date() == QDate::currentDate()) { return i18n("Today"); } return i18nc("weekday, date", "%1, %2", QLocale().dayName(elem.dt.date().dayOfWeek(), QLocale::LongFormat), QLocale().toString(elem.dt.date(), QLocale::ShortFormat)); } case ReservationRole: return res; case ReservationIdRole: return elem.id; case ElementTypeRole: return elem.elementType; case TodayEmptyRole: if (elem.elementType == TodayMarker) { return index.row() == (int)(m_elements.size() - 1) || m_elements.at(index.row() + 1).dt.date() > QDate::currentDate(); } return {}; case IsTodayRole: return elem.dt.date() == QDate::currentDate(); case ElementRangeRole: return elem.rangeType; case CountryInformationRole: case WeatherForecastRole: return elem.content; } return {}; } QHash TimelineModel::roleNames() const { auto names = QAbstractListModel::roleNames(); names.insert(PassIdRole, "passId"); names.insert(SectionHeader, "sectionHeader"); names.insert(ReservationRole, "reservation"); names.insert(ReservationIdRole, "reservationId"); names.insert(ElementTypeRole, "type"); names.insert(TodayEmptyRole, "isTodayEmpty"); names.insert(IsTodayRole, "isToday"); names.insert(ElementRangeRole, "rangeType"); names.insert(CountryInformationRole, "countryInformation"); names.insert(WeatherForecastRole, "weatherForecast"); return names; } int TimelineModel::todayRow() const { const auto it = std::find_if(m_elements.begin(), m_elements.end(), [](const Element &e) { return e.elementType == TodayMarker; }); return std::distance(m_elements.begin(), it); } void TimelineModel::reservationAdded(const QString &resId) { const auto res = m_resMgr->reservation(resId); if (needsSplitting(res)) { insertElement(Element{resId, {}, relevantDateTime(res, RangeBegin), elementType(res), RangeBegin}); insertElement(Element{resId, {}, relevantDateTime(res, RangeEnd), elementType(res), RangeEnd}); } else { insertElement(Element{resId, {}, relevantDateTime(res, SelfContained), elementType(res), SelfContained}); } updateInformationElements(); emit todayRowChanged(); } void TimelineModel::insertElement(Element &&elem) { auto it = std::lower_bound(m_elements.begin(), m_elements.end(), elem.dt, [](const Element &lhs, const QDateTime &rhs) { return lhs.dt < rhs; }); auto index = std::distance(m_elements.begin(), it); beginInsertRows({}, index, index); m_elements.insert(it, std::move(elem)); endInsertRows(); } void TimelineModel::reservationUpdated(const QString &resId) { const auto res = m_resMgr->reservation(resId); if (needsSplitting(res)) { updateElement(resId, res, RangeBegin); updateElement(resId, res, RangeEnd); } else { updateElement(resId, res, SelfContained); } updateInformationElements(); } void TimelineModel::updateElement(const QString &resId, const QVariant &res, TimelineModel::RangeType rangeType) { const auto it = std::find_if(m_elements.begin(), m_elements.end(), [resId, rangeType](const Element &e) { return e.id == resId && e.rangeType == rangeType; }); if (it == m_elements.end()) { return; } const auto row = std::distance(m_elements.begin(), it); const auto newDt = relevantDateTime(res, rangeType); if ((*it).dt != newDt) { // element moved beginRemoveRows({}, row, row); m_elements.erase(it); endRemoveRows(); insertElement(Element{resId, {}, newDt, elementType(res), rangeType}); } else { emit dataChanged(index(row, 0), index(row, 0)); } } void TimelineModel::reservationRemoved(const QString &resId) { const auto it = std::find_if(m_elements.begin(), m_elements.end(), [resId](const Element &e) { return e.id == resId; }); if (it == m_elements.end()) { return; } const auto isSplit = (*it).rangeType == RangeBegin; const auto row = std::distance(m_elements.begin(), it); beginRemoveRows({}, row, row); m_elements.erase(it); endRemoveRows(); emit todayRowChanged(); if (isSplit) { reservationRemoved(resId); } updateInformationElements(); } void TimelineModel::updateInformationElements() { // the country information is shown before transitioning into a country that // differs in one or more properties from the home country and we where that // differences is introduced by the transition CountryInformation homeCountry; homeCountry.setIsoCode(m_homeCountry); auto previousCountry = homeCountry; for (auto it = m_elements.begin(); it != m_elements.end(); ++it) { switch ((*it).elementType) { case TodayMarker: case WeatherForecast: it = erasePreviousCountyInfo(it); continue; case CountryInfo: previousCountry = (*it).content.value(); it = erasePreviousCountyInfo(it); // purge multiple consecutive country info elements continue; default: break; } auto newCountry = homeCountry; newCountry.setIsoCode(destinationCountry(m_resMgr->reservation((*it).id))); if (newCountry == previousCountry) { continue; } if (newCountry == homeCountry) { assert(it != m_elements.begin()); // previousCountry == homeCountry in this case // purge outdated country info element it = erasePreviousCountyInfo(it); previousCountry = newCountry; continue; } // add new country info element auto row = std::distance(m_elements.begin(), it); beginInsertRows({}, row, row); it = m_elements.insert(it, Element{{}, QVariant::fromValue(newCountry), (*it).dt, CountryInfo, SelfContained}); endInsertRows(); previousCountry = newCountry; } updateWeatherElements(); } std::vector::iterator TimelineModel::erasePreviousCountyInfo(std::vector::iterator it) { if (it == m_elements.begin()) { return it; } auto it2 = it; --it2; if ((*it2).elementType == CountryInfo) { const auto row = std::distance(m_elements.begin(), it2); beginRemoveRows({}, row, row); it = m_elements.erase(it2); endRemoveRows(); } return it; } void TimelineModel::updateWeatherElements() { if (!m_weatherMgr || m_elements.empty()) { return; } qDebug() << "recomputing weather elements"; GeoCoordinates geo; // look through the past, clean up weather elements there and figure out where we are auto it = m_elements.begin(); for (; it != m_elements.end() && (*it).dt < QDateTime::currentDateTimeUtc();) { if ((*it).elementType == WeatherForecast) { const auto row = std::distance(m_elements.begin(), it); beginRemoveRows({}, row, row); it = m_elements.erase(it); endRemoveRows(); continue; } const auto res = m_resMgr->reservation((*it).id); const auto newGeo = geoCoordinate(res); if (isLocationChange(res) || newGeo.isValid()) { geo = newGeo; } ++it; } auto date = QDateTime::currentDateTime(); + date.setTime(QTime(date.time().hour() + 1, 0)); while(it != m_elements.end() && date < m_weatherMgr->maximumForecastTime()) { if ((*it).dt < date || (*it).elementType == TodayMarker) { // track where we are const auto res = m_resMgr->reservation((*it).id); const auto newGeo = geoCoordinate(res); if (isLocationChange(res) || newGeo.isValid()) { geo = newGeo; } ++it; continue; } + // determine the length of the forecast range (at most until the end of the day) auto endTime = date; endTime.setTime(QTime(23, 59, 59)); + for (auto it2 = it; it2 != m_elements.end(); ++it2) { + if ((*it2).dt >= endTime) { + break; + } + const auto res = m_resMgr->reservation((*it2).id); + const auto newGeo = geoCoordinate(res); + if (isLocationChange(res)) { + endTime = std::min(endTime, relevantDateTime(res, RangeEnd)); + break; + } + } ::WeatherForecast fc; if (geo.isValid()) { m_weatherMgr->monitorLocation(geo.latitude(), geo.longitude()); fc = m_weatherMgr->forecast(geo.latitude(), geo.longitude(), date, endTime); } // case 1: we have forecast data, and a matching weather element: update if (fc.isValid() && (*it).dt == date && (*it).elementType == WeatherForecast) { (*it).content = QVariant::fromValue(fc); const auto idx = index(std::distance(m_elements.begin(), it), 0); emit dataChanged(idx, idx); } // case 2: we have forecast data, but no matching weather element: insert else if (fc.isValid()) { const auto row = std::distance(m_elements.begin(), it); beginInsertRows({}, row, row); it = m_elements.insert(it, Element{{}, QVariant::fromValue(fc), date, WeatherForecast, SelfContained}); endInsertRows(); } - // case 3: we have no forecast data, but a matching weather element: remove + // case 3: we have no forecast data, but a matching weather element: remove else if ((*it).elementType == WeatherForecast && (*it).dt == date) { const auto row = std::distance(m_elements.begin(), it); beginRemoveRows({}, row, row); it = m_elements.erase(it); endRemoveRows(); } date = endTime.addSecs(1); ++it; } // append weather elements beyond the end of the list if necessary while (date < m_weatherMgr->maximumForecastTime() && geo.isValid()) { auto endTime = date; endTime.setTime(QTime(23, 59, 59)); m_weatherMgr->monitorLocation(geo.latitude(), geo.longitude()); const auto fc = m_weatherMgr->forecast(geo.latitude(), geo.longitude(), date, endTime); if (fc.isValid()) { const auto row = std::distance(m_elements.begin(), it); beginInsertRows({}, row, row); it = m_elements.insert(it, Element{{}, QVariant::fromValue(fc), date, WeatherForecast, SelfContained}); ++it; endInsertRows(); } date = endTime.addSecs(1); } qDebug() << "weather recomputation done"; }