diff --git a/autotests/weathertest.cpp b/autotests/weathertest.cpp index 987999c..21cb786 100644 --- a/autotests/weathertest.cpp +++ b/autotests/weathertest.cpp @@ -1,71 +1,72 @@ /* 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 "itinerary_version.h" #include #include #include #include #include #include #include #include class WeatherTest : public QObject { Q_OBJECT private slots: void initTestCase() { QCoreApplication::setApplicationName(QStringLiteral("kde-itinerary-weatherunittest")); QCoreApplication::setApplicationVersion(QStringLiteral(ITINERARY_VERSION_STRING)); QStandardPaths::setTestModeEnabled(true); QDir d(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1String("/weather")); d.removeRecursively(); QVERIFY(!d.exists()); } void testForecastRetrieval() { WeatherForecastManager mgr; + mgr.setAllowNetworkAccess(true); const auto now = QDateTime::currentDateTimeUtc().addSecs(1800); auto fc = mgr.forecast(46.1, 7.78, now).value(); QVERIFY(!fc.isValid()); QSignalSpy updateSpy(&mgr, &WeatherForecastManager::forecastUpdated); QVERIFY(updateSpy.isValid()); mgr.monitorLocation(46.1, 7.78); QVERIFY(updateSpy.wait()); fc = mgr.forecast(46.1, 7.78, now).value(); qDebug() << fc.dateTime() << fc.temperature() << fc.symbolType() << fc.symbolIconName(); QVERIFY(fc.isValid()); QVERIFY(fc.dateTime().isValid()); QVERIFY(fc.dateTime() <= now); QVERIFY(fc.symbolType() != WeatherForecast::Unknown); QVERIFY(fc.temperature() > -50); QVERIFY(fc.temperature() < 50); } }; QTEST_GUILESS_MAIN(WeatherTest) #include "weathertest.moc" diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 4d140c5..92abf53 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -1,86 +1,87 @@ set(itinerary_srcs countryinformation.cpp pkpassmanager.cpp reservationmanager.cpp timelinemodel.cpp ) ecm_qt_declare_logging_category(itinerary_srcs HEADER logging.h IDENTIFIER Log CATEGORY_NAME org.kde.itinerary ) add_library(itinerary STATIC ${itinerary_srcs}) target_link_libraries(itinerary PUBLIC KPim::Itinerary KPim::PkPass KF5::I18n Qt5::Network ) add_executable(itinerary-app main.cpp applicationcontroller.cpp localizer.cpp pkpassimageprovider.cpp + settings.cpp qml.qrc ) target_include_directories(itinerary-app PRIVATE ${CMAKE_BINARY_DIR}) target_link_libraries(itinerary-app PRIVATE itinerary itinerary-weather Qt5::Quick KF5::Contacts ) if (ANDROID) # explicitly add runtime dependencies and transitive link dependencies, # so androiddeployqt picks them up target_link_libraries(itinerary-app PRIVATE KF5::Archive KF5::Kirigami2 Qt5::AndroidExtras Qt5::Svg KF5::Prison ) kirigami_package_breeze_icons(ICONS document-open edit-delete go-next-symbolic map-symbolic settings-configure view-calendar-day view-refresh ) else () target_link_libraries(itinerary-app PRIVATE Qt5::Positioning) set_target_properties(itinerary-app PROPERTIES OUTPUT_NAME "itinerary") endif() qml_lint( main.qml BoardingPass.qml BusDelegate.qml BusPage.qml CountryInfoDelegate.qml DetailsPage.qml FlightDelegate.qml FlightPage.qml HotelDelegate.qml HotelPage.qml PkPassPage.qml PlaceDelegate.qml RestaurantDelegate.qml RestaurantPage.qml SettingsPage.qml TicketTokenDelegate.qml TimelineDelegate.qml TimelinePage.qml TouristAttractionDelegate.qml TrainDelegate.qml TrainPage.qml ) install(TARGETS itinerary-app ${INSTALL_TARGETS_DEFAULT_ARGS}) if (NOT ANDROID) install(PROGRAMS org.kde.itinerary.desktop DESTINATION ${KDE_INSTALL_APPDIR}) endif() diff --git a/src/app/SettingsPage.qml b/src/app/SettingsPage.qml index e51bc8d..a5fb597 100644 --- a/src/app/SettingsPage.qml +++ b/src/app/SettingsPage.qml @@ -1,64 +1,66 @@ /* 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 . */ import QtQuick 2.5 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.1 as QQC2 import org.kde.kirigami 2.4 as Kirigami import org.kde.itinerary 1.0 import "." as App Kirigami.ScrollablePage { id: root title: qsTr("Settings") GridLayout { columns: 2 width: root.width QQC2.Label { text: qsTr("Home Country") } QQC2.ComboBox { model: [ "TODO", "TODO" ] } QQC2.Label { text: qsTr("Weather Forecast") } QQC2.Switch { id: weatherSwitch + checked: _settings.weatherForecastEnabled + onToggled: _settings.weatherForecastEnabled = checked } QQC2.Label { Layout.columnSpan: 2 Layout.fillWidth: true text: qsTr("Showing weather forecasts will query online services.") visible: !weatherSwitch.checked } // ATTENTION do not remove this note, see https://api.met.no/license_data.html QQC2.Label { Layout.columnSpan: 2 Layout.fillWidth: true text: qsTr("Using data from The Norwegian Meteorological Institute under Creative Commons 4.0 BY International license.") visible: weatherSwitch.checked wrapMode: Text.WordWrap onLinkActivated: Qt.openUrlExternally(link) } } onBackRequested: pageStack.pop() } diff --git a/src/app/main.cpp b/src/app/main.cpp index f424152..9960c7f 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -1,141 +1,150 @@ /* 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 "itinerary_version.h" #include "logging.h" #include "applicationcontroller.h" #include "countryinformation.h" #include "localizer.h" #include "pkpassmanager.h" #include "timelinemodel.h" #include "pkpassimageprovider.h" #include "reservationmanager.h" +#include "settings.h" + +#include #include #include #include #include #include #include #ifdef Q_OS_ANDROID #include #include #endif #include #include #include #include void handleViewIntent(PkPassManager *passMgr) { #ifdef Q_OS_ANDROID // handle opened files const auto activity = QtAndroid::androidActivity(); if (!activity.isValid()) return; const auto intent = activity.callObjectMethod("getIntent", "()Landroid/content/Intent;"); if (!intent.isValid()) return; const auto uri = intent.callObjectMethod("getData", "()Landroid/net/Uri;"); if (!uri.isValid()) return; const auto scheme = uri.callObjectMethod("getScheme", "()Ljava/lang/String;"); if (scheme.toString() == QLatin1String("content")) { const auto tmpFile = activity.callObjectMethod("receiveContent", "(Landroid/net/Uri;)Ljava/lang/String;", uri.object()); passMgr->importPassFromTempFile(tmpFile.toString()); } else if (scheme.toString() == QLatin1String("file")) { const auto uriStr = uri.callObjectMethod("toString", "()Ljava/lang/String;"); passMgr->importPass(QUrl(uriStr.toString())); } else { const auto uriStr = uri.callObjectMethod("toString", "()Ljava/lang/String;"); qCWarning(Log) << "Unknown intent URI:" << uriStr.toString(); } #else Q_UNUSED(passMgr); #endif } #ifdef Q_OS_ANDROID Q_DECL_EXPORT #endif int main(int argc, char **argv) { QCoreApplication::setApplicationName(QStringLiteral("kde-itinerary")); QCoreApplication::setOrganizationName(QStringLiteral("KDE")); QCoreApplication::setOrganizationDomain(QStringLiteral("kde.org")); QCoreApplication::setApplicationVersion(QStringLiteral(ITINERARY_VERSION_STRING)); QGuiApplication::setApplicationDisplayName(QStringLiteral("KDE Itinerary")); // TODO i18n QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); QGuiApplication app(argc, argv); QGuiApplication::setWindowIcon(QIcon::fromTheme(QStringLiteral("map-globe"))); QCommandLineParser parser; parser.addHelpOption(); parser.addVersionOption(); parser.addPositionalArgument(QStringLiteral("pass"), QStringLiteral("PkPass file to import.")); parser.process(app); PkPassManager passMgr; ReservationManager resMgr; resMgr.setPkPassManager(&passMgr); TimelineModel timelineModel; timelineModel.setReservationManager(&resMgr); ApplicationController appController; + Settings settings; + + WeatherForecastManager weatherForecastMgr; + weatherForecastMgr.setAllowNetworkAccess(settings.weatherForecastEnabled()); + QObject::connect(&settings, &Settings::weatherForecastEnabledChanged, &weatherForecastMgr, &WeatherForecastManager::setAllowNetworkAccess); qmlRegisterUncreatableType("org.kde.pkpass", 1, 0, "Barcode", {}); qmlRegisterUncreatableType("org.kde.pkpass", 1, 0, "Field", {}); qRegisterMetaType(); qmlRegisterUncreatableType("org.kde.kitinerary", 1, 0, "Ticket", {}); qmlRegisterUncreatableMetaObject(KItinerary::KnowledgeDb::staticMetaObject, "org.kde.kitinerary", 1, 0, "KnowledgeDb", {}); qmlRegisterUncreatableType("org.kde.itinerary", 1, 0, "CountryInformation", {}); qmlRegisterUncreatableType("org.kde.itinerary", 1, 0, "TimelineModel", {}); qmlRegisterSingletonType("org.kde.itinerary", 1, 0, "Localizer", [](QQmlEngine*, QJSEngine*) -> QObject*{ return new Localizer; }); QQmlApplicationEngine engine; engine.addImageProvider(QStringLiteral("org.kde.pkpass"), new PkPassImageProvider(&passMgr)); engine.rootContext()->setContextProperty(QStringLiteral("_pkpassManager"), &passMgr); engine.rootContext()->setContextProperty(QStringLiteral("_reservationManager"), &resMgr); engine.rootContext()->setContextProperty(QStringLiteral("_timelineModel"), &timelineModel); engine.rootContext()->setContextProperty(QStringLiteral("_appController"), &appController); + engine.rootContext()->setContextProperty(QStringLiteral("_settings"), &settings); engine.load(QStringLiteral(":/main.qml")); for (const auto &file : parser.positionalArguments()) { if (file.endsWith(QLatin1String(".pkpass"))) passMgr.importPass(QUrl::fromLocalFile(file)); else resMgr.importReservation(QUrl::fromLocalFile(file)); } handleViewIntent(&passMgr); return app.exec(); } diff --git a/src/app/settings.cpp b/src/app/settings.cpp new file mode 100644 index 0000000..c2dc9d7 --- /dev/null +++ b/src/app/settings.cpp @@ -0,0 +1,49 @@ +/* + 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 "settings.h" + +#include + +Settings::Settings(QObject *parent) + : QObject(parent) +{ + QSettings s; + s.beginGroup(QLatin1String("Settings")); + m_weatherEnabled = s.value(QLatin1String("WeatherForecastEnabled"), false).toBool(); +} + +Settings::~Settings() = default; + +bool Settings::weatherForecastEnabled() const +{ + return m_weatherEnabled; +} + +void Settings::setWeatherForecastEnabled(bool enabled) +{ + if (m_weatherEnabled == enabled) { + return; + } + + m_weatherEnabled = enabled; + QSettings s; + s.beginGroup(QLatin1String("Settings")); + s.setValue(QLatin1String("WeatherForecastEnabled"), enabled); + + emit weatherForecastEnabledChanged(enabled); +} diff --git a/src/app/settings.h b/src/app/settings.h new file mode 100644 index 0000000..11c6d2a --- /dev/null +++ b/src/app/settings.h @@ -0,0 +1,42 @@ +/* + 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 SETTINGS_H +#define SETTINGS_H + +#include + +/** Application settings accessible by QML. */ +class Settings : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool weatherForecastEnabled READ weatherForecastEnabled WRITE setWeatherForecastEnabled NOTIFY weatherForecastEnabledChanged) +public: + explicit Settings(QObject *parent = nullptr); + ~Settings(); + + bool weatherForecastEnabled() const; + void setWeatherForecastEnabled(bool enabled); + +signals: + void weatherForecastEnabledChanged(bool enabled); + +private: + bool m_weatherEnabled = false; +}; + +#endif // SETTINGS_H diff --git a/src/weather/weatherforecastmanager.cpp b/src/weather/weatherforecastmanager.cpp index a62c6bc..8ec0f7c 100644 --- a/src/weather/weatherforecastmanager.cpp +++ b/src/weather/weatherforecastmanager.cpp @@ -1,359 +1,365 @@ /* 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 /* * 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.x << t.y; 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); } QVariant WeatherForecastManager::forecast(float latitude, float longitude, const QDateTime& dt) const { WeatherTile t{latitude, longitude}; QFile f(cachePath(t) + QLatin1String("forecast.xml")); if (f.exists() && f.open(QFile::ReadOnly)) { QXmlStreamReader reader(&f); auto forecasts = parseForecast(reader); mergeForecasts(forecasts); auto it = std::lower_bound(forecasts.begin(), forecasts.end(), dt, [](const WeatherForecast &lhs, const QDateTime &rhs) { return lhs.dateTime() < rhs; }); if (it != forecasts.begin()) { --it; } if (it != forecasts.end()) { return QVariant::fromValue(*it); } } return {}; } void WeatherForecastManager::fetchTile(WeatherTile tile) { m_pendingTiles.push_back(tile); fetchNext(); } void WeatherForecastManager::fetchNext() { - if (m_pendingReply || m_pendingTiles.empty()) { + 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.x / WeatherTile::Size)); query.addQueryItem(QStringLiteral("lon"), QString::number(tile.y / 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.x) + QLatin1Char('/') + QString::number(tile.y) + QLatin1Char('/')); QDir().mkpath(path); return path; } void WeatherForecastManager::writeToCacheFile(QNetworkReply* reply) const { const auto tile = reply->request().attribute(QNetworkRequest::User).value(); qDebug() << tile.x << tile.y; 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()); } } 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()); } static void alignToHour(QDateTime &dt) { dt.setTime(QTime(dt.time().hour(), 0, 0, 0)); } 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 static const WeatherForecast::SymbolType symbol_map[] = { WeatherForecast::Unknown, // 0 WeatherForecast::Clear, // 1 Sun WeatherForecast::LightClouds, // 2 LightCloud WeatherForecast::PartlyCloudy, // 3 PartlyCloud WeatherForecast::Clouds, // 4 Cloud WeatherForecast::LightRainShowers, // 5 LightRainSun WeatherForecast::LightRainShowers, // 6 LightRainThunderSun WeatherForecast::Hail, // 7 SleetSun WeatherForecast::LightSnowShowers, // 8 SnowSun WeatherForecast::LightRain, // 9 LightRain WeatherForecast::Rain, // 10 Rain WeatherForecast::ThunderStorm, // 11 RainThunder WeatherForecast::Hail, // 12 Sleet WeatherForecast::Snow, // 13 Snow WeatherForecast::Snow, // 14 SnowThunder WeatherForecast::Fog, // 15 Fog WeatherForecast::Hail, // 20 SleetSunThunder WeatherForecast::Unknown, // 21 SnowSunThunder WeatherForecast::LightRain, // 22 LightRainThunder WeatherForecast::Hail, // 23 SleetThunder WeatherForecast::ThunderStormShowers, // 24 DrizzleThunderSun WeatherForecast::ThunderStormShowers, // 25 RainThunderSun WeatherForecast::ThunderStormShowers, // 26 LightSleetThunderSun WeatherForecast::Hail, // 27 HeavySleetThunderSun WeatherForecast::LightSnowShowers, // 28 LightSnowThunderSun WeatherForecast::Snow, // 29 HeavySnowThunderSun WeatherForecast::ThunderStorm, // 30 DrizzleThunder WeatherForecast::Hail, // 31 LightSleetThunder WeatherForecast::Hail, // 32 HeavySleetThunder WeatherForecast::Snow, // 33 LightSnowThunder WeatherForecast::Snow, // 34 HeavySnowThunder WeatherForecast::LightRainShowers, // 40 DrizzleSun WeatherForecast::RainShowers, // 41 RainSun WeatherForecast::Hail, // 42 LightSleetSun WeatherForecast::Hail, // 43 HeavySleetSun WeatherForecast::LightSnowShowers, // 44 LightSnowSun WeatherForecast::Snow, // 45 HeavysnowSun WeatherForecast::LightRain, // 46 Drizzle WeatherForecast::Hail, // 47 LightSleet WeatherForecast::Hail, // 48 HeavySleet WeatherForecast::LightSnow, // 49 LightSnow 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")) { fc.setTemperature(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 } if (symId <= 50) { fc.setSymbolType(symbol_map[symId]); } } break; case QXmlStreamReader::EndElement: if (reader.name() == QLatin1String("time")) { return fc; } break; default: break; } reader.readNext(); } return fc; } #include "moc_weatherforecastmanager.cpp" diff --git a/src/weather/weatherforecastmanager.h b/src/weather/weatherforecastmanager.h index cbdf7f4..57f210b 100644 --- a/src/weather/weatherforecastmanager.h +++ b/src/weather/weatherforecastmanager.h @@ -1,70 +1,74 @@ /* 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 #include #include class WeatherForecast; struct WeatherTile; 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); // TODO unmonitor location(s)? /** Get the forecast for the given time and location. */ Q_INVOKABLE QVariant forecast(float latitude, float longitude, const QDateTime &dt) const; 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; 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; QNetworkAccessManager *m_nam = nullptr; QNetworkReply *m_pendingReply = nullptr; + bool m_allowNetwork = false; }; #endif // WEATHERFORECASTMANAGER_H