summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorIvan Solovev <ivan.solovev@qt.io>2023-01-25 18:04:26 +0100
committerQt Cherry-pick Bot <cherrypick_bot@qt-project.org>2023-02-02 15:43:54 +0000
commitd58edeb06059d4c3bb76ab422687b86a4fca2e08 (patch)
tree85712eee42ce366ea506a07941435deed549293f
parent107f258db576af488a933ec88aa1ea3ffa39e4f2 (diff)
WeatherInfo example: Implement openmeteo backend
Another backend to provide weather data. Just in case the previous two exceed their limits. Fixes: QTBUG-109071 Change-Id: I37d1c971a3ffb04a558333ceaa31a08b0871187c Reviewed-by: Juha Vuolle <juha.vuolle@qt.io> (cherry picked from commit 51f5d2239743ab386ea0a8c1400bd4a937216842) Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
-rw-r--r--examples/positioning/weatherinfo/CMakeLists.txt1
-rw-r--r--examples/positioning/weatherinfo/appmodel.cpp2
-rw-r--r--examples/positioning/weatherinfo/doc/src/weatherinfo.qdoc7
-rw-r--r--examples/positioning/weatherinfo/openmeteobackend.cpp293
-rw-r--r--examples/positioning/weatherinfo/openmeteobackend.h32
-rw-r--r--examples/positioning/weatherinfo/weatherinfo.pro2
6 files changed, 334 insertions, 3 deletions
diff --git a/examples/positioning/weatherinfo/CMakeLists.txt b/examples/positioning/weatherinfo/CMakeLists.txt
index f826c26c..f44446a9 100644
--- a/examples/positioning/weatherinfo/CMakeLists.txt
+++ b/examples/positioning/weatherinfo/CMakeLists.txt
@@ -46,6 +46,7 @@ qt_add_qml_module(weatherinfo
AUTO_RESOURCE_PREFIX
SOURCES
appmodel.cpp appmodel.h
+ openmeteobackend.cpp openmeteobackend.h
openweathermapbackend.cpp openweathermapbackend.h
providerbackend.cpp providerbackend.h
weatherapibackend.cpp weatherapibackend.h
diff --git a/examples/positioning/weatherinfo/appmodel.cpp b/examples/positioning/weatherinfo/appmodel.cpp
index c496bfbe..36b3bcf6 100644
--- a/examples/positioning/weatherinfo/appmodel.cpp
+++ b/examples/positioning/weatherinfo/appmodel.cpp
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include "appmodel.h"
+#include "openmeteobackend.h"
#include "openweathermapbackend.h"
#include "weatherapibackend.h"
@@ -219,6 +220,7 @@ AppModel::AppModel(QObject *parent) :
d->m_supportedBackends.push_back(new OpenWeatherMapBackend(this));
d->m_supportedBackends.push_back(new WeatherApiBackend(this));
+ d->m_supportedBackends.push_back(new OpenMeteoBackend(this));
registerBackend(0);
//! [1]
diff --git a/examples/positioning/weatherinfo/doc/src/weatherinfo.qdoc b/examples/positioning/weatherinfo/doc/src/weatherinfo.qdoc
index 814d3831..d332329f 100644
--- a/examples/positioning/weatherinfo/doc/src/weatherinfo.qdoc
+++ b/examples/positioning/weatherinfo/doc/src/weatherinfo.qdoc
@@ -24,20 +24,21 @@
\section1 Weather Data Providers
- The example uses two unrelated weather data providers:
+ The example uses several unrelated weather data providers:
\list
\li \l {http://www.openweathermap.org}{OpenWeather}
\li \l {https://www.weatherapi.com/}{WeatherAPI.com}
+ \li \l {https://open-meteo.com/}{Open-Meteo}
\endlist
The provider to be used is selected automatically at runtime and can be
changed if the selected provider is not available. However, it can't be
specified manually.
- \note Free plans are used for both providers, which implies certain
+ \note Free plans are used for all providers, which implies certain
limitations on the amount of weather requests. If the limits are exceeded,
- the providers become temporary unavailable. When both providers are
+ the providers become temporary unavailable. When all providers are
unavailable, the application would not be able to show any weather
information. In this case it is required to wait until at least one of the
providers becomes available again.
diff --git a/examples/positioning/weatherinfo/openmeteobackend.cpp b/examples/positioning/weatherinfo/openmeteobackend.cpp
new file mode 100644
index 00000000..889dcde6
--- /dev/null
+++ b/examples/positioning/weatherinfo/openmeteobackend.cpp
@@ -0,0 +1,293 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#include "openmeteobackend.h"
+
+#include <QtCore/qjsonarray.h>
+#include <QtCore/qjsondocument.h>
+#include <QtCore/qjsonobject.h>
+#include <QtCore/qjsonvalue.h>
+#include <QtCore/qloggingcategory.h>
+#include <QtCore/qstring.h>
+#include <QtCore/qtimezone.h>
+#include <QtCore/qurl.h>
+#include <QtCore/qurlquery.h>
+#include <QtNetwork/qnetworkaccessmanager.h>
+#include <QtNetwork/qnetworkreply.h>
+
+Q_DECLARE_LOGGING_CATEGORY(requestsLog)
+
+using namespace Qt::StringLiterals;
+
+static constexpr qsizetype kMaxEntries = 4;
+
+static QString niceTemperatureString(double t)
+{
+ return QString::number(qRound(t)) + QChar(0xB0);
+}
+
+/*
+ Weather codes are taken from the bottom of this page:
+ https://open-meteo.com/en/docs
+
+ The possible strings are based on the icon names. The icon name is built up
+ as follows:
+ weather-[mystring].png
+*/
+static QString weatherCodeToIcon(int code)
+{
+ switch (code) {
+ case 0:
+ return "sunny"_L1;
+ case 1:
+ return "sunny-very-few-clouds"_L1;
+ case 2:
+ return "few-clouds"_L1;
+ case 3:
+ return "overcast"_L1;
+ case 45:
+ case 48:
+ return "fog"_L1;
+ case 51:
+ case 53:
+ case 55:
+ case 56:
+ case 57:
+ return "showers-scattered"_L1;
+ case 61:
+ case 63:
+ case 65:
+ case 80:
+ case 81:
+ case 82:
+ return "showers"_L1;
+ case 66:
+ case 67:
+ return "sleet"_L1;
+ case 71:
+ case 73:
+ case 75:
+ case 77:
+ case 85:
+ case 86:
+ return "snow"_L1;
+ case 95:
+ case 96:
+ case 99:
+ return "thundershower"_L1;
+ }
+ return "sunny"_L1; // default
+}
+
+/*
+ Weather codes and descriptions are taken from the bottom of this page:
+ https://open-meteo.com/en/docs
+*/
+static QString weatherCodeToDescription(int code)
+{
+ switch (code) {
+ case 0:
+ return "clear sky"_L1;
+ case 1:
+ return "mainly clear"_L1;
+ case 2:
+ return "partly cloudy"_L1;
+ case 3:
+ return "overcast"_L1;
+ case 45:
+ case 48:
+ return "fog"_L1;
+ case 51:
+ case 53:
+ case 55:
+ case 56:
+ case 57:
+ return "drizzle"_L1;
+ case 61:
+ case 63:
+ case 65:
+ return "rain"_L1;
+ case 66:
+ case 67:
+ return "freezing rain"_L1;
+ case 71:
+ case 73:
+ case 75:
+ case 77:
+ case 85:
+ case 86:
+ return "snow"_L1;
+ case 80:
+ case 81:
+ case 82:
+ return "pouring rain"_L1;
+ case 95:
+ case 96:
+ case 99:
+ return "thunderstorm"_L1;
+ }
+ return QString();
+}
+
+// The supported cities are taken from WeatherInfo.qml.
+// Need to keep in sync with it.
+static QGeoCoordinate coordinateForCity(const QString &city)
+{
+ if (city == "Brisbane"_L1)
+ return QGeoCoordinate(-27.3818, 152.8531);
+ else if (city == "Oslo"_L1)
+ return QGeoCoordinate(59.8939, 10.7151);
+ else if (city == "Helsinki"_L1)
+ return QGeoCoordinate(60.1103, 24.8786);
+ else if (city == "New York"_L1)
+ return QGeoCoordinate(40.6977, -74.1198);
+
+ return QGeoCoordinate();
+}
+
+OpenMeteoBackend::OpenMeteoBackend(QObject *parent)
+ : ProviderBackend{parent},
+ m_networkManager{new QNetworkAccessManager(this)}
+{
+}
+
+void OpenMeteoBackend::requestWeatherInfo(const QString &city)
+{
+ const auto coordinate = coordinateForCity(city);
+ if (!coordinate.isValid()) {
+ qCDebug(requestsLog) << "Weather request for unknown city:" << city;
+ emit errorOccurred();
+ return;
+ }
+ generateWeatherRequest(city, coordinate);
+}
+
+void OpenMeteoBackend::requestWeatherInfo(const QGeoCoordinate &coordinate)
+{
+ generateWeatherRequest(QString(), coordinate);
+}
+
+void OpenMeteoBackend::handleWeatherForecastReply(QNetworkReply *reply,
+ const LocationInfo &location)
+{
+ Q_UNUSED(reply);
+ Q_UNUSED(location);
+ if (!reply) {
+ emit errorOccurred();
+ return;
+ }
+
+ bool parsed = false;
+ // first item is current weather, then forecast data
+ QList<WeatherInfo> weatherData;
+ if (!reply->error()) {
+ const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
+ const QJsonObject documentObject = document.object();
+
+ // utc offset
+ const QJsonValue offsetVal = documentObject.value("utc_offset_seconds"_L1);
+ const qint64 utcOffset = !offsetVal.isUndefined() ? offsetVal.toInteger() : -1;
+
+ // current weather
+ WeatherInfo currentWeather;
+ const QJsonObject currWeatherObj = documentObject.value("current_weather"_L1).toObject();
+ const QJsonValue currTemp = currWeatherObj.value("temperature"_L1);
+ if (currTemp.isDouble())
+ currentWeather.m_temperature = niceTemperatureString(currTemp.toDouble());
+ const QJsonValue weatherCode = currWeatherObj.value("weathercode"_L1);
+ if (weatherCode.isDouble()) {
+ const int code = weatherCode.toInt(-1);
+ if (code >= 0) {
+ currentWeather.m_weatherIconId = weatherCodeToIcon(code);
+ currentWeather.m_weatherDescription = weatherCodeToDescription(code);
+ }
+ }
+ if (!currentWeather.m_temperature.isEmpty()
+ && !currentWeather.m_weatherIconId.isEmpty()
+ && !currentWeather.m_weatherDescription.isEmpty()) {
+ weatherData.append(currentWeather);
+ } else {
+ qCDebug(requestsLog) << "Failed to extract current weather";
+ }
+
+ // daily weather
+ const QJsonObject dailyData = documentObject.value("daily"_L1).toObject();
+ const QJsonArray days = dailyData.value("time"_L1).toArray();
+ const QJsonArray weatherCodes = dailyData.value("weathercode"_L1).toArray();
+ const QJsonArray maxTemperatures = dailyData.value("temperature_2m_max"_L1).toArray();
+ const QJsonArray minTemperatures = dailyData.value("temperature_2m_min"_L1).toArray();
+ if (days.size() >= kMaxEntries && weatherCodes.size() >= kMaxEntries
+ && maxTemperatures.size() >= kMaxEntries
+ && minTemperatures.size() >= kMaxEntries) {
+ for (qsizetype i = 0; i < kMaxEntries; ++i) {
+ WeatherInfo info;
+ const qint64 unixTime = days.at(i).toInteger(-1);
+ if (unixTime > 0) {
+ const QDateTime date = QDateTime::fromSecsSinceEpoch(unixTime + utcOffset);
+ info.m_dayOfWeek = date.toString("ddd"_L1);
+ }
+ const int code = weatherCodes.at(i).toInt(-1);
+ if (code >= 0)
+ info.m_weatherIconId = weatherCodeToIcon(code);
+ const double minTemp = minTemperatures.at(i).toDouble();
+ const double maxTemp = maxTemperatures.at(i).toDouble();
+ info.m_temperature = niceTemperatureString(minTemp) + u'/'
+ + niceTemperatureString(maxTemp);
+
+ if (!info.m_dayOfWeek.isEmpty() && !info.m_temperature.isEmpty()
+ && !info.m_weatherIconId.isEmpty()) {
+ weatherData.append(info);
+ } else {
+ qCDebug(requestsLog) << "Failed to extract weather forecast";
+ break;
+ }
+ }
+ }
+
+ parsed = (utcOffset != -1) && (weatherData.size() == (kMaxEntries + 1));
+ }
+
+ if (parsed) {
+ emit weatherInformation(location, weatherData);
+ } else {
+ emit errorOccurred();
+ if (reply->error())
+ qCDebug(requestsLog) << reply->errorString();
+ else
+ qCDebug(requestsLog, "Failed to parse weather JSON.");
+ }
+
+ reply->deleteLater();
+}
+
+void OpenMeteoBackend::generateWeatherRequest(const QString &city,
+ const QGeoCoordinate &coordinate)
+{
+ QUrl url("https://api.open-meteo.com/v1/forecast"_L1);
+
+ QUrlQuery query;
+ query.addQueryItem("latitude"_L1, QString::number(coordinate.latitude()));
+ query.addQueryItem("longitude"_L1, QString::number(coordinate.longitude()));
+ query.addQueryItem("daily"_L1, "weathercode,temperature_2m_max,temperature_2m_min"_L1);
+ query.addQueryItem("temperature_unit"_L1, "celsius"_L1);
+ query.addQueryItem("current_weather"_L1, "true"_L1);
+ query.addQueryItem("timezone"_L1, QString::fromLatin1(QTimeZone::systemTimeZoneId()));
+ query.addQueryItem("timeformat"_L1, "unixtime"_L1);
+
+ url.setQuery(query);
+
+ // We always need to have a non-empty "city" name to display to the user.
+ // Other backends provide this information, but here we just use the string
+ // representation of QGeoCoordinate if the city is unknown.
+ // If the city is known, we pass an empty QGeoCoordinate object, so that
+ // the WeatherDataCache is populated correctly.
+ const LocationInfo info = city.isEmpty()
+ ? LocationInfo{coordinate.toString(QGeoCoordinate::DegreesMinutesWithHemisphere),
+ coordinate}
+ : LocationInfo{city, QGeoCoordinate()};
+
+ QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url));
+ connect(reply, &QNetworkReply::finished, this, [this, reply, info]() {
+ handleWeatherForecastReply(reply, info);
+ });
+}
diff --git a/examples/positioning/weatherinfo/openmeteobackend.h b/examples/positioning/weatherinfo/openmeteobackend.h
new file mode 100644
index 00000000..def72e3a
--- /dev/null
+++ b/examples/positioning/weatherinfo/openmeteobackend.h
@@ -0,0 +1,32 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#ifndef OPENMETEOBACKEND_H
+#define OPENMETEOBACKEND_H
+
+#include "providerbackend.h"
+
+QT_BEGIN_NAMESPACE
+class QNetworkAccessManager;
+class QNetworkReply;
+QT_END_NAMESPACE
+
+class OpenMeteoBackend : public ProviderBackend
+{
+ Q_OBJECT
+public:
+ explicit OpenMeteoBackend(QObject *parent = nullptr);
+
+ void requestWeatherInfo(const QString &city) override;
+ void requestWeatherInfo(const QGeoCoordinate &coordinate) override;
+
+private slots:
+ void handleWeatherForecastReply(QNetworkReply *reply, const LocationInfo &location);
+
+private:
+ void generateWeatherRequest(const QString &city, const QGeoCoordinate &coordinate);
+
+ QNetworkAccessManager *m_networkManager;
+};
+
+#endif // OPENMETEOBACKEND_H
diff --git a/examples/positioning/weatherinfo/weatherinfo.pro b/examples/positioning/weatherinfo/weatherinfo.pro
index c5729bc2..573274fb 100644
--- a/examples/positioning/weatherinfo/weatherinfo.pro
+++ b/examples/positioning/weatherinfo/weatherinfo.pro
@@ -12,11 +12,13 @@ QML_IMPORT_MAJOR_VERSION = 1
SOURCES += main.cpp \
appmodel.cpp \
+ openmeteobackend.cpp \
openweathermapbackend.cpp \
providerbackend.cpp \
weatherapibackend.cpp
HEADERS += appmodel.h \
+ openmeteobackend.h \
openweathermapbackend.h \
providerbackend.h \
weatherapibackend.h