Source code
Revision control
Copy as Markdown
// 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:
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:
*/
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)
{
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);
});
}