diff --git a/autotests/timelinemodeltest.cpp b/autotests/timelinemodeltest.cpp index 0a5c48b..dc5cd39 100644 --- a/autotests/timelinemodeltest.cpp +++ b/autotests/timelinemodeltest.cpp @@ -1,239 +1,340 @@ /* 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))); + 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(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 b7ff8d5..6ec8250 100644 --- a/src/app/timelinemodel.cpp +++ b/src/app/timelinemodel.cpp @@ -1,492 +1,504 @@ /* 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); + 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; - insertWeatherElements(); + 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; } - insertWeatherElements(); + 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::insertWeatherElements() +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 = QDate::currentDate(); for (; it != m_elements.end() && date < QDate::currentDate().addDays(9);) { + if ((*it).dt.date() < 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; } - if (date == (*it).dt.date() && (*it).elementType == WeatherForecast) { // weather element already present - date = date.addDays(1); - ++it; - continue; - } - - const auto res = m_resMgr->reservation((*it).id); - const auto newGeo = geoCoordinate(res); - if (isLocationChange(res) || newGeo.isValid()) { - geo = newGeo; - } + ::WeatherForecast fc; if (geo.isValid()) { m_weatherMgr->monitorLocation(geo.latitude(), geo.longitude()); - const auto fc = m_weatherMgr->forecast(geo.latitude(), geo.longitude(), QDateTime(date, QTime(0, 0)), QDateTime(date, QTime(23, 59))); - if (fc.isValid()) { - const auto row = std::distance(m_elements.begin(), it); - beginInsertRows({}, row, row); - it = m_elements.insert(it, Element{{}, QVariant::fromValue(fc), QDateTime(date, QTime()), WeatherForecast, SelfContained}); - endInsertRows(); - } + fc = m_weatherMgr->forecast(geo.latitude(), geo.longitude(), QDateTime(date, QTime(0, 0)), QDateTime(date, QTime(23, 59))); } + + // case 1: we have forecast data, and a matching weather element: update + if (fc.isValid() && (*it).dt.date() == 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), QDateTime(date, QTime()), WeatherForecast, SelfContained}); + endInsertRows(); + } + // case 3: we have no forecast data, but a matching weather element: remove + else if ((*it).elementType == WeatherForecast && (*it).dt.date() == date) { + const auto row = std::distance(m_elements.begin(), it); + beginRemoveRows({}, row, row); + it = m_elements.erase(it); + endRemoveRows(); + } + date = date.addDays(1); ++it; } // append weather elements beyond the end of the list if necessary while (date < QDate::currentDate().addDays(9) && geo.isValid()) { m_weatherMgr->monitorLocation(geo.latitude(), geo.longitude()); const auto fc = m_weatherMgr->forecast(geo.latitude(), geo.longitude(), QDateTime(date, QTime(0, 0)), QDateTime(date, QTime(23, 59))); if (fc.isValid()) { const auto row = std::distance(m_elements.begin(), it); beginInsertRows({}, row, row); it = m_elements.insert(it, Element{{}, QVariant::fromValue(fc), QDateTime(date, QTime()), WeatherForecast, SelfContained}); ++it; endInsertRows(); } date = date.addDays(1); } qDebug() << "weather recomputation done"; } - -void TimelineModel::updateWeatherElements() -{ - for (auto it = m_elements.begin(); it != m_elements.end(); ++it) { - if ((*it).elementType == WeatherForecast) { - // TODO see above - (*it).content = QVariant::fromValue(m_weatherMgr->forecast(52, 13.5, QDateTime((*it).dt.date(), QTime(0, 0)), QDateTime((*it).dt.date(), QTime(23, 59)))); - const auto idx = index(std::distance(m_elements.begin(), it), 0); - emit dataChanged(idx, idx); - } - } - - insertWeatherElements(); -} diff --git a/src/app/timelinemodel.h b/src/app/timelinemodel.h index 213a10a..addc0a8 100644 --- a/src/app/timelinemodel.h +++ b/src/app/timelinemodel.h @@ -1,114 +1,113 @@ /* 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 . */ #ifndef TIMELINEMODEL_H #define TIMELINEMODEL_H #include #include class ReservationManager; class WeatherForecastManager; namespace KItinerary { class GeoCoordinates; } class TimelineModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(int todayRow READ todayRow NOTIFY todayRowChanged) public: enum Role { PassIdRole = Qt::UserRole + 1, SectionHeader, ReservationRole, ReservationIdRole, ElementTypeRole, TodayEmptyRole, IsTodayRole, ElementRangeRole, CountryInformationRole, WeatherForecastRole }; enum ElementType { Undefined, Flight, TrainTrip, BusTrip, Hotel, Restaurant, TouristAttraction, TodayMarker, CountryInfo, WeatherForecast }; Q_ENUM(ElementType) // indicates whether an element is self-contained or the beginning/end of a longer timespan/range enum RangeType { SelfContained, RangeBegin, RangeEnd }; Q_ENUM(RangeType) explicit TimelineModel(QObject *parent = nullptr); ~TimelineModel(); void setReservationManager(ReservationManager *mgr); void setWeatherForecastManager(WeatherForecastManager *mgr); void setHomeCountryIsoCode(const QString &isoCode); QVariant data(const QModelIndex& index, int role) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override; QHash roleNames() const override; int todayRow() const; signals: void todayRowChanged(); private: struct Element { QString id; // reservation id QVariant content; // non-reservation content QDateTime dt; // relevant date/time ElementType elementType; RangeType rangeType; }; void reservationAdded(const QString &resId); void insertElement(Element &&elem); void reservationUpdated(const QString &resId); void updateElement(const QString &resId, const QVariant &res, RangeType rangeType); void reservationRemoved(const QString &resId); void updateInformationElements(); std::vector::iterator erasePreviousCountyInfo(std::vector::iterator it); - void insertWeatherElements(); void updateWeatherElements(); ReservationManager *m_resMgr = nullptr; WeatherForecastManager *m_weatherMgr = nullptr; std::vector m_elements; QString m_homeCountry; }; #endif // TIMELINEMODEL_H diff --git a/src/weather/weatherforecastmanager.cpp b/src/weather/weatherforecastmanager.cpp index 59ac328..5f69aed 100644 --- a/src/weather/weatherforecastmanager.cpp +++ b/src/weather/weatherforecastmanager.cpp @@ -1,441 +1,441 @@ /* 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 "weatherforecastmanager.h" #include "weatherforecast.h" #include "weathertile.h" #include #include #include #include #include #include #include #include #include #include static void alignToHour(QDateTime &dt) { dt.setTime(QTime(dt.time().hour(), 0, 0, 0)); } /* * ATTENTION! * Before touching anything in here, especially regarding the network operations * make sure to read and understand https://api.met.no/conditions_service.html! */ WeatherForecastManager::WeatherForecastManager(QObject *parent) : QObject(parent) { } WeatherForecastManager::~WeatherForecastManager() = default; void WeatherForecastManager::setAllowNetworkAccess(bool enabled) { m_allowNetwork = enabled; fetchNext(); } void WeatherForecastManager::monitorLocation(float latitude, float longitude) { WeatherTile t{latitude, longitude}; qDebug() << latitude << longitude << t.lat << t.lon; auto it = std::lower_bound(m_monitoredTiles.begin(), m_monitoredTiles.end(), t); if (it != m_monitoredTiles.end() && (*it) == t) { return; } m_monitoredTiles.insert(it, t); fetchTile(t); } WeatherForecast WeatherForecastManager::forecast(float latitude, float longitude, const QDateTime &dt) const { return forecast(latitude, longitude, dt, dt.addSecs(3600)); } WeatherForecast WeatherForecastManager::forecast(float latitude, float longitude, const QDateTime &begin, const QDateTime &end) const { auto beginDt = std::max(begin, QDateTime::currentDateTimeUtc()); alignToHour(beginDt); auto endDt = std::max(end, QDateTime::currentDateTimeUtc()); alignToHour(endDt); if (!beginDt.isValid() || !endDt.isValid() || beginDt > endDt) { return {}; } if (Q_UNLIKELY(m_testMode)) { WeatherForecast fc; fc.setDateTime(beginDt); - fc.setMinimumTemperature(23.0f); - fc.setMaximumTemperature(23.0f); - fc.setPrecipitation(0.0f); + fc.setMinimumTemperature(std::min(latitude, longitude)); + fc.setMaximumTemperature(std::max(latitude, longitude)); + fc.setPrecipitation(23.0f); fc.setSymbolType(WeatherForecast::LightClouds); return fc; } WeatherTile tile{latitude, longitude}; if (!loadForecastData(tile)) { return {}; } const auto &forecasts = m_forecastData[tile]; const auto beginIt = std::lower_bound(forecasts.begin(), forecasts.end(), beginDt, [](const WeatherForecast &lhs, const QDateTime &rhs) { return lhs.dateTime() < rhs; }); if (beginIt == forecasts.end()) { return {}; } const auto endIt = std::lower_bound(forecasts.begin(), forecasts.end(), endDt, [](const WeatherForecast &lhs, const QDateTime &rhs) { return lhs.dateTime() < rhs; }); WeatherForecast fc(*beginIt); for (auto it = beginIt; it != endIt; ++it) { fc.merge(*it); } return fc; } void WeatherForecastManager::fetchTile(WeatherTile tile) { QFileInfo fi(cachePath(tile) + QLatin1String("forecast.xml")); if (fi.exists() && fi.lastModified().toUTC().addSecs(3600 * 2) >= QDateTime::currentDateTimeUtc()) { // cache is already new enough return; } m_pendingTiles.push_back(tile); fetchNext(); } void WeatherForecastManager::fetchNext() { if (!m_allowNetwork || m_pendingReply || m_pendingTiles.empty()) { return; } const auto tile = m_pendingTiles.front(); m_pendingTiles.pop_front(); if (!m_nam) { m_nam = new QNetworkAccessManager(this); } QUrl url; url.setScheme(QStringLiteral("https")); url.setHost(QStringLiteral("api.met.no")); url.setPath(QStringLiteral("/weatherapi/locationforecast/1.9/")); QUrlQuery query; query.addQueryItem(QStringLiteral("lat"), QString::number(tile.lat / WeatherTile::Size)); query.addQueryItem(QStringLiteral("lon"), QString::number(tile.lon / WeatherTile::Size)); url.setQuery(query); qDebug() << url; QNetworkRequest req(url); req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); req.setAttribute(QNetworkRequest::User, QVariant::fromValue(tile)); // see §Identification on https://api.met.no/conditions_service.html req.setHeader(QNetworkRequest::UserAgentHeader, QString(QCoreApplication::applicationName() + QLatin1Char(' ') + QCoreApplication::applicationVersion() + QLatin1String(" (kde-pim@kde.org)"))); // TODO see §Cache on https://api.met.no/conditions_service.html // see §Compression on https://api.met.no/conditions_service.html req.setRawHeader("Accept-Encoding", "gzip"); m_pendingReply = m_nam->get(req); connect(m_pendingReply, &QNetworkReply::finished, this, &WeatherForecastManager::tileDownloaded); } void WeatherForecastManager::tileDownloaded() { // TODO handle 304 Not Modified // TODO handle 429 Too Many Requests if (m_pendingReply->error() != QNetworkReply::NoError) { qWarning() << m_pendingReply->errorString(); } else { writeToCacheFile(m_pendingReply); } m_pendingReply->deleteLater(); m_pendingReply = nullptr; if (m_pendingTiles.empty()) { emit forecastUpdated(); } fetchNext(); } QString WeatherForecastManager::cachePath(WeatherTile tile) const { const auto path = QString(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1String("/weather/") + QString::number(tile.lat) + QLatin1Char('/') + QString::number(tile.lon) + QLatin1Char('/')); QDir().mkpath(path); return path; } void WeatherForecastManager::writeToCacheFile(QNetworkReply* reply) const { const auto tile = reply->request().attribute(QNetworkRequest::User).value(); qDebug() << tile.lat << tile.lon; qDebug() << reply->rawHeaderPairs(); QFile f(cachePath(tile) + QLatin1String("forecast.xml")); if (!f.open(QFile::WriteOnly)) { qWarning() << "Failed to open weather cache location:" << f.errorString(); return; } const auto contentEncoding = reply->rawHeader("Content-Encoding"); if (contentEncoding == "gzip") { const auto data = reply->readAll(); if (data.size() < 4 || data.at(0) != 0x1f || data.at(1) != char(0x8b)) { qWarning() << "Invalid gzip format"; return; } z_stream stream; unsigned char buffer[1024]; stream.zalloc = nullptr; stream.zfree = nullptr; stream.opaque = nullptr; stream.avail_in = data.size(); stream.next_in = reinterpret_cast(const_cast(data.data())); auto ret = inflateInit2(&stream, 15 + 32); // see docs, the magic numbers enable gzip decoding if (ret != Z_OK) { qWarning() << "Failed to initialize zlib stream."; return; } do { stream.avail_out = sizeof(buffer); stream.next_out = buffer; ret = inflate(&stream, Z_NO_FLUSH); if (ret != Z_OK && ret != Z_STREAM_END) { qWarning() << "Zlib decoding failed!" << ret; break; } f.write(reinterpret_cast(buffer), sizeof(buffer) - stream.avail_out); } while (stream.avail_out == 0); inflateEnd(&stream); } else { f.write(reply->readAll()); } m_forecastData.erase(tile); } bool WeatherForecastManager::loadForecastData(WeatherTile tile) const { const auto it = m_forecastData.find(tile); if (it != m_forecastData.end()) { return true; } QFile f(cachePath(tile) + QLatin1String("forecast.xml")); if (!f.exists() || !f.open(QFile::ReadOnly)) { return false; } QXmlStreamReader reader(&f); auto forecasts = parseForecast(reader); mergeForecasts(forecasts); if (forecasts.empty()) { return false; } m_forecastData.insert(it, {tile, std::move(forecasts)}); return true; } void WeatherForecastManager::mergeForecasts(std::vector& forecasts) const { std::stable_sort(forecasts.begin(), forecasts.end(), [](const WeatherForecast &lhs, const WeatherForecast &rhs) { return lhs.dateTime() < rhs.dateTime(); }); // merge duplicated time slices auto storeIt = forecasts.begin(); for (auto it = forecasts.begin(); it != forecasts.end();) { (*storeIt) = (*it); auto mergeIt = it; for (; mergeIt != forecasts.end(); ++mergeIt) { if ((*it).dateTime() == (*mergeIt).dateTime()) { (*storeIt).merge(*mergeIt); } else { break; } } ++storeIt; it = mergeIt; } forecasts.erase(storeIt, forecasts.end()); } std::vector WeatherForecastManager::parseForecast(QXmlStreamReader &reader) const { std::vector result; auto beginDt = QDateTime::currentDateTimeUtc(); alignToHour(beginDt); while (!reader.atEnd()) { if (reader.tokenType() == QXmlStreamReader::StartElement) { if (reader.name() == QLatin1String("weatherdata") || reader.name() == QLatin1String("product")) { reader.readNext(); // enter these elements continue; } if (reader.name() == QLatin1String("time") && reader.attributes().value(QLatin1String("datatype")) == QLatin1String("forecast")) { // normalize time ranges to 1 hour auto from = QDateTime::fromString(reader.attributes().value(QLatin1String("from")).toString(), Qt::ISODate); from = std::max(from, beginDt); alignToHour(from); auto to = QDateTime::fromString(reader.attributes().value(QLatin1String("to")).toString(), Qt::ISODate); alignToHour(to); if (to == from) { to = to.addSecs(3600); } if (to < beginDt || to <= from || !to.isValid() || !from.isValid()) { reader.skipCurrentElement(); continue; } auto fc = parseForecastElement(reader); for (int i = 0; i < from.secsTo(to); i += 3600) { fc.setDateTime(from.addSecs(i * 3600)); result.push_back(fc); } continue; } // unknown element reader.skipCurrentElement(); } else { reader.readNext(); } } return result; } // Icon mapping: https://api.met.no/weatherapi/weathericon/1.1/documentation struct symbol_map_t { uint8_t id; WeatherForecast::SymbolType type; }; static const symbol_map_t symbol_map[] = { { 1, WeatherForecast::Clear }, // 1 Sun { 2, WeatherForecast::LightClouds }, // 2 LightCloud { 3, WeatherForecast::PartlyCloudy }, // 3 PartlyCloud { 4, WeatherForecast::Clouds }, // 4 Cloud { 5, WeatherForecast::LightRainShowers }, // 5 LightRainSun { 6, WeatherForecast::LightRainShowers }, // 6 LightRainThunderSun { 7, WeatherForecast::Hail }, // 7 SleetSun { 8, WeatherForecast::LightSnowShowers }, // 8 SnowSun { 9, WeatherForecast::LightRain }, // 9 LightRain { 10, WeatherForecast::Rain }, // 10 Rain { 11, WeatherForecast::ThunderStorm }, // 11 RainThunder { 12, WeatherForecast::Hail }, // 12 Sleet { 13, WeatherForecast::Snow }, // 13 Snow { 14, WeatherForecast::Snow }, // 14 SnowThunder { 15, WeatherForecast::Fog }, // 15 Fog { 20, WeatherForecast::Hail }, // 20 SleetSunThunder { 21, WeatherForecast::Unknown }, // 21 SnowSunThunder { 22, WeatherForecast::LightRain }, // 22 LightRainThunder { 23, WeatherForecast::Hail }, // 23 SleetThunder { 24, WeatherForecast::ThunderStormShowers }, // 24 DrizzleThunderSun { 25, WeatherForecast::ThunderStormShowers }, // 25 RainThunderSun { 26, WeatherForecast::ThunderStormShowers }, // 26 LightSleetThunderSun { 27, WeatherForecast::Hail }, // 27 HeavySleetThunderSun { 28, WeatherForecast::LightSnowShowers }, // 28 LightSnowThunderSun { 29, WeatherForecast::Snow }, // 29 HeavySnowThunderSun { 30, WeatherForecast::ThunderStorm }, // 30 DrizzleThunder { 31, WeatherForecast::Hail }, // 31 LightSleetThunder { 32, WeatherForecast::Hail }, // 32 HeavySleetThunder { 33, WeatherForecast::Snow }, // 33 LightSnowThunder { 34, WeatherForecast::Snow }, // 34 HeavySnowThunder { 40, WeatherForecast::LightRainShowers }, // 40 DrizzleSun { 41, WeatherForecast::RainShowers }, // 41 RainSun { 42, WeatherForecast::Hail }, // 42 LightSleetSun { 43, WeatherForecast::Hail }, // 43 HeavySleetSun { 44, WeatherForecast::LightSnowShowers }, // 44 LightSnowSun { 45, WeatherForecast::Snow }, // 45 HeavysnowSun { 46, WeatherForecast::LightRain }, // 46 Drizzle { 47, WeatherForecast::Hail }, // 47 LightSleet { 48, WeatherForecast::Hail }, // 48 HeavySleet { 49, WeatherForecast::LightSnow }, // 49 LightSnow { 50, WeatherForecast::Snow } // 50 HeavySnow }; WeatherForecast WeatherForecastManager::parseForecastElement(QXmlStreamReader &reader) const { WeatherForecast fc; while (!reader.atEnd()) { switch (reader.tokenType()) { case QXmlStreamReader::StartElement: if (reader.name() == QLatin1String("temperature")) { const auto t = reader.attributes().value(QLatin1String("value")).toFloat(); fc.setMinimumTemperature(t); fc.setMaximumTemperature(t); } else if (reader.name() == QLatin1String("minTemperature")) { fc.setMinimumTemperature(reader.attributes().value(QLatin1String("value")).toFloat()); } else if (reader.name() == QLatin1String("maxTemperature")) { fc.setMaximumTemperature(reader.attributes().value(QLatin1String("value")).toFloat()); } else if (reader.name() == QLatin1String("symbol")) { auto symId = reader.attributes().value(QLatin1String("number")).toInt(); if (symId > 100) { symId -= 100; // map polar night symbols } const auto it = std::lower_bound(std::begin(symbol_map), std::end(symbol_map), symId, [](symbol_map_t lhs, uint8_t rhs) { return lhs.id < rhs; }); if (it != std::end(symbol_map) && (*it).id == symId) { fc.setSymbolType((*it).type); } } else if (reader.name() == QLatin1String("precipitation")) { fc.setPrecipitation(reader.attributes().value(QLatin1String("value")).toFloat()); } break; case QXmlStreamReader::EndElement: if (reader.name() == QLatin1String("time")) { return fc; } break; default: break; } reader.readNext(); } return fc; } void WeatherForecastManager::setTestModeEnabled(bool testMode) { m_testMode = testMode; } #include "moc_weatherforecastmanager.cpp"