diff --git a/src/app/timelinemodel.cpp b/src/app/timelinemodel.cpp index 6ec8250..3e4c306 100644 --- a/src/app/timelinemodel.cpp +++ b/src/app/timelinemodel.cpp @@ -1,504 +1,510 @@ /* 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; 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 = QDate::currentDate(); - for (; it != m_elements.end() && date < QDate::currentDate().addDays(9);) { + auto date = QDateTime::currentDateTime(); + while(it != m_elements.end() && date < m_weatherMgr->maximumForecastTime()) { - if ((*it).dt.date() < date || (*it).elementType == TodayMarker) { + 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; } + auto endTime = date; + endTime.setTime(QTime(23, 59, 59)); + ::WeatherForecast fc; if (geo.isValid()) { m_weatherMgr->monitorLocation(geo.latitude(), geo.longitude()); - fc = m_weatherMgr->forecast(geo.latitude(), geo.longitude(), QDateTime(date, QTime(0, 0)), QDateTime(date, QTime(23, 59))); + 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() == date && (*it).elementType == WeatherForecast) { + 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), QDateTime(date, QTime()), WeatherForecast, SelfContained}); + 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 - else if ((*it).elementType == WeatherForecast && (*it).dt.date() == date) { + 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 = date.addDays(1); + date = endTime.addSecs(1); ++it; } // append weather elements beyond the end of the list if necessary - while (date < QDate::currentDate().addDays(9) && geo.isValid()) { + 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(), QDateTime(date, QTime(0, 0)), QDateTime(date, QTime(23, 59))); + 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), QDateTime(date, QTime()), WeatherForecast, SelfContained}); + it = m_elements.insert(it, Element{{}, QVariant::fromValue(fc), date, WeatherForecast, SelfContained}); ++it; endInsertRows(); } - date = date.addDays(1); + date = endTime.addSecs(1); } qDebug() << "weather recomputation done"; } diff --git a/src/weather/weatherforecastmanager.cpp b/src/weather/weatherforecastmanager.cpp index 5f69aed..764d95e 100644 --- a/src/weather/weatherforecastmanager.cpp +++ b/src/weather/weatherforecastmanager.cpp @@ -1,441 +1,446 @@ /* 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(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; } +QDateTime WeatherForecastManager::maximumForecastTime() const +{ + return QDateTime(QDate::currentDate().addDays(9), QTime(0, 0)); +} + void WeatherForecastManager::setTestModeEnabled(bool testMode) { m_testMode = testMode; } #include "moc_weatherforecastmanager.cpp" diff --git a/src/weather/weatherforecastmanager.h b/src/weather/weatherforecastmanager.h index 7bf30f3..569fe58 100644 --- a/src/weather/weatherforecastmanager.h +++ b/src/weather/weatherforecastmanager.h @@ -1,85 +1,88 @@ /* 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 WEATHERFORECASTMANAGER_H #define WEATHERFORECASTMANAGER_H #include "weathertile.h" #include #include #include #include class WeatherForecast; class QNetworkAccessManager; class QNetworkReply; class QXmlStreamReader; /** Access to weather forecast data based on geo coorinates. */ class WeatherForecastManager : public QObject { Q_OBJECT public: explicit WeatherForecastManager(QObject *parent = nullptr); ~WeatherForecastManager(); /** Kill switch for network operations. */ void setAllowNetworkAccess(bool enabled); /** Monitor the specified location for weather forecasts. */ void monitorLocation(float latitude, float longitude); /** Get the forecast for the given time and location. */ WeatherForecast forecast(float latitude, float longitude, const QDateTime &dt) const; /** Get the forecast for the give time range and location. */ WeatherForecast forecast(float latitude, float longitude, const QDateTime &begin, const QDateTime &end) const; + /** Time until when we have forecast data. */ + QDateTime maximumForecastTime() const; + /** Enable unit test mode. * In this mode static forecast data is provided for all locations. */ void setTestModeEnabled(bool testMode); signals: /** Updated when new forecast data has been retrieved. */ void forecastUpdated(); private: void fetchTile(WeatherTile tile); void fetchNext(); void tileDownloaded(); QString cachePath(WeatherTile tile) const; void writeToCacheFile(QNetworkReply *reply) const; bool loadForecastData(WeatherTile tile) const; void mergeForecasts(std::vector &forecasts) const; std::vector parseForecast(QXmlStreamReader &reader) const; WeatherForecast parseForecastElement(QXmlStreamReader &reader) const; std::vector m_monitoredTiles; std::deque m_pendingTiles; mutable std::unordered_map> m_forecastData; QNetworkAccessManager *m_nam = nullptr; QNetworkReply *m_pendingReply = nullptr; bool m_allowNetwork = false; bool m_testMode = false; }; #endif // WEATHERFORECASTMANAGER_H