KWeatherCore

pendingweatherforecast.cpp
1 /*
2  * SPDX-FileCopyrightText: 2020-2021 Han Young <[email protected]>
3  * SPDX-FileCopyrightText: 2020 Devin Lin <[email protected]>
4  *
5  * SPDX-License-Identifier: LGPL-2.0-or-later
6  */
7 #include "pendingweatherforecast.h"
8 #include "geotimezone.h"
9 #include "kweathercore_p.h"
10 #include "pendingweatherforecast_p.h"
11 #include "sunrisesource.h"
12 #include <KLocalizedString>
13 #include <QDir>
14 #include <QFile>
15 #include <QJsonArray>
16 #include <QJsonDocument>
17 #include <QJsonObject>
18 #include <QNetworkReply>
19 #include <QStandardPaths>
20 #include <QTimeZone>
21 namespace KWeatherCore
22 {
23 PendingWeatherForecastPrivate::PendingWeatherForecastPrivate(double latitude,
24  double longitude,
25  const QString &timezone,
26  const QUrl &url,
27  const std::vector<Sunrise> &sunrise,
28  PendingWeatherForecast *parent)
29  : QObject(parent)
30  , m_latitude(latitude)
31  , m_longitude(longitude)
32  , m_timezone(timezone)
33 {
34  connect(this, &PendingWeatherForecastPrivate::finished, [this] {
35  this->isFinished = true;
36  });
37  connect(this, &PendingWeatherForecastPrivate::finished, parent, &PendingWeatherForecast::finished);
38  connect(this, &PendingWeatherForecastPrivate::networkError, parent, &PendingWeatherForecast::networkError);
39 
40  QNetworkRequest req(url);
42 
43  // see ┬žIdentification on https://api.met.no/conditions_service.html
45  QString(QStringLiteral("KWeatherCore/") + VERSION_NUMBER + QStringLiteral(" [email protected]")));
46  connect(&m_manager, &QNetworkAccessManager::finished, this, &PendingWeatherForecastPrivate::parseWeatherForecastResults);
47  m_manager.get(req);
48 
49  forecast.setCoordinate(latitude, longitude);
50 
51  m_sunriseSource = new SunriseSource(latitude, longitude, m_timezone, sunrise, this);
52  if (timezone.isEmpty()) {
53  hasTimezone = false;
54  getTimezone(latitude, longitude);
55  } else {
56  hasTimezone = true;
57  forecast.setTimezone(timezone);
58  m_timezone = timezone;
59  getSunrise();
60  }
61 }
62 PendingWeatherForecastPrivate::PendingWeatherForecastPrivate(WeatherForecast data)
63  : forecast(data)
64  , isFinished(true)
65 {
66 }
67 void PendingWeatherForecastPrivate::getTimezone(double latitude, double longitude)
68 {
69  auto timezoneSource = new GeoTimezone(latitude, longitude, this);
70  connect(timezoneSource, &GeoTimezone::finished, this, &PendingWeatherForecastPrivate::parseTimezoneResult);
71 }
72 void PendingWeatherForecastPrivate::parseTimezoneResult(const QString &result)
73 {
74  hasTimezone = true;
75  forecast.setTimezone(result);
76  m_timezone = result;
77  getSunrise();
78 }
79 
80 void PendingWeatherForecastPrivate::getSunrise()
81 {
82  connect(m_sunriseSource, &SunriseSource::finished, this, &PendingWeatherForecastPrivate::parseSunriseResults);
83  m_sunriseSource->setTimezone(m_timezone);
84  m_sunriseSource->requestData();
85 }
86 void PendingWeatherForecastPrivate::parseSunriseResults()
87 {
88  hasSunrise = true;
89 
90  // if this arrived later than forecast
91  if (!hourlyForecast.empty()) {
92  applySunriseToForecast();
93  }
94 }
95 void PendingWeatherForecastPrivate::parseWeatherForecastResults(QNetworkReply *reply)
96 {
97  reply->deleteLater();
98  if (reply->error()) {
99  qWarning() << "network error when fetching forecast:" << reply->errorString();
101  return;
102  }
103 
104  QJsonDocument jsonDocument = QJsonDocument::fromJson(reply->readAll());
105 
106  if (jsonDocument.isObject()) {
107  QJsonObject obj = jsonDocument.object();
108  QJsonObject prop = obj[QStringLiteral("properties")].toObject();
109 
110  if (prop.contains(QStringLiteral("timeseries"))) {
111  QJsonArray timeseries = prop[QStringLiteral("timeseries")].toArray();
112 
113  // loop over all forecast data
114  for (const auto &ref : qAsConst(timeseries)) {
115  parseOneElement(ref.toObject(), hourlyForecast);
116  }
117  }
118  }
119 
120  if (hasTimezone && hasSunrise) {
121  applySunriseToForecast();
122  }
123  // Q_EMIT finished();
124 }
125 
126 void PendingWeatherForecastPrivate::parseOneElement(const QJsonObject &obj, std::vector<HourlyWeatherForecast> &hourlyForecast)
127 {
128  /*~~~~~~~~~~ lambda ~~~~~~~~~~~*/
129 
130  auto getWindDeg = [](double deg) -> WindDirection {
131  if (deg < 22.5 || deg >= 337.5) {
132  return WindDirection::S; // from N
133  } else if (deg > 22.5 || deg <= 67.5) {
134  return WindDirection::SW; // from NE
135  } else if (deg > 67.5 || deg <= 112.5) {
136  return WindDirection::W; // from E
137  } else if (deg > 112.5 || deg <= 157.5) {
138  return WindDirection::NW; // from SE
139  } else if (deg > 157.5 || deg <= 202.5) {
140  return WindDirection::N; // from S
141  } else if (deg > 202.5 || deg <= 247.5) {
142  return WindDirection::NE; // from SW
143  } else if (deg > 247.5 || deg <= 292.5) {
144  return WindDirection::E; // from W
145  } else if (deg > 292.5 || deg <= 337.5) {
146  return WindDirection::SE; // from NW
147  }
148  return WindDirection::N;
149  };
150 
151  /*================== actual code ======================*/
152 
153  QJsonObject data = obj[QStringLiteral("data")].toObject();
154  QJsonObject instant = data[QStringLiteral("instant")].toObject()[QStringLiteral("details")].toObject();
155  // ignore last forecast, which does not have enough data
156  if (!data.contains(QStringLiteral("next_6_hours")) && !data.contains(QStringLiteral("next_1_hours"))) {
157  return;
158  }
159 
160  // get symbolCode and precipitation amount
161  QString symbolCode;
162  double precipitationAmount = 0;
163  // some fields contain only "next_1_hours", and others may contain only
164  // "next_6_hours"
165  if (data.contains(QStringLiteral("next_1_hours"))) {
166  QJsonObject nextOneHours = data[QStringLiteral("next_1_hours")].toObject();
167  symbolCode = nextOneHours[QStringLiteral("summary")].toObject()[QStringLiteral("symbol_code")].toString(QStringLiteral("unknown"));
168  precipitationAmount = nextOneHours[QStringLiteral("details")].toObject()[QStringLiteral("precipitation_amount")].toDouble();
169  } else {
170  QJsonObject nextSixHours = data[QStringLiteral("next_6_hours")].toObject();
171  symbolCode = nextSixHours[QStringLiteral("summary")].toObject()[QStringLiteral("symbol_code")].toString(QStringLiteral("unknown"));
172  precipitationAmount = nextSixHours[QStringLiteral("details")].toObject()[QStringLiteral("precipitation_amount")].toDouble();
173  }
174 
175  symbolCode = symbolCode.split(QLatin1Char('_'))[0]; // trim _[day/night] from end -
176  // https://api.met.no/weatherapi/weathericon/2.0/legends
177  HourlyWeatherForecast hourForecast(QDateTime::fromString(obj.value(QStringLiteral("time")).toString(), Qt::ISODate));
178  hourForecast.setNeutralWeatherIcon(self()->resolveAPIWeatherDesc(symbolCode + QStringLiteral("_neutral")).icon);
179  hourForecast.setTemperature(instant[QStringLiteral("air_temperature")].toDouble());
180  hourForecast.setPressure(instant[QStringLiteral("air_pressure_at_sea_level")].toDouble());
181  hourForecast.setWindDirection(getWindDeg(instant[QStringLiteral("wind_from_direction")].toDouble()));
182  hourForecast.setWindSpeed(instant[QStringLiteral("wind_speed")].toDouble());
183  hourForecast.setHumidity(instant[QStringLiteral("relative_humidity")].toDouble());
184  hourForecast.setFog(instant[QStringLiteral("fog_area_fraction")].toDouble());
185  hourForecast.setUvIndex(instant[QStringLiteral("ultraviolet_index_clear_sky")].toDouble());
186  hourForecast.setPrecipitationAmount(precipitationAmount);
187  hourForecast.setSymbolCode(symbolCode);
188  hourlyForecast.push_back(std::move(hourForecast));
189 }
190 
191 void PendingWeatherForecastPrivate::applySunriseToForecast()
192 {
193  // ************* Lambda *************** //
194  auto isDayTime = [](const QDateTime &date, const std::vector<Sunrise> &sunrise) {
195  for (auto &sr : sunrise) {
196  // if on the same day
197  if (sr.sunRise().date().daysTo(date.date()) == 0 && sr.sunRise().date().day() == date.date().day()) {
198  // 30 min threshold
199  return sr.sunRise().addSecs(-1800) <= date && sr.sunSet().addSecs(1800) >= date;
200  }
201  }
202 
203  // not found
204  return date.time().hour() >= 6 && date.time().hour() <= 18;
205  };
206 
207  auto getSymbolCodeDescription = [](bool isDay, const QString &symbolCode) {
208  return isDay ? self()->resolveAPIWeatherDesc(symbolCode + QStringLiteral("_day")).desc
209  : self()->resolveAPIWeatherDesc(symbolCode + QStringLiteral("_night")).desc;
210  };
211 
212  auto getSymbolCodeIcon = [](bool isDay, const QString &symbolCode) {
213  return isDay ? self()->resolveAPIWeatherDesc(symbolCode + QStringLiteral("_day")).icon
214  : self()->resolveAPIWeatherDesc(symbolCode + QStringLiteral("_night")).icon;
215  };
216 
217  // ******* code ******** //
218  for (auto &hourForecast : hourlyForecast) {
219  hourForecast.setDate(hourForecast.date().toTimeZone(QTimeZone(m_timezone.toUtf8())));
220 
221  bool isDay;
222  isDay = isDayTime(hourForecast.date(), m_sunriseSource->value());
223  hourForecast.setWeatherIcon(getSymbolCodeIcon(isDay, hourForecast.symbolCode())); // set day/night icon
224  hourForecast.setWeatherDescription(getSymbolCodeDescription(isDay, hourForecast.symbolCode()));
225  forecast += std::move(hourForecast);
226  }
227  forecast.setSunriseForecast(m_sunriseSource->value());
228  Q_EMIT finished();
229 
230  // save to cache
231 
232  QFile file(self()->getCacheDirectory(m_latitude, m_longitude).path() + QStringLiteral("/cache.json"));
233 
234  if (file.open(QIODevice::WriteOnly)) {
235  file.write(QJsonDocument(forecast.toJson()).toJson(QJsonDocument::Compact));
236  } else {
237  qWarning() << "write to cache failed";
238  }
239 }
240 
241 PendingWeatherForecast::PendingWeatherForecast(double latitude, double longitude, const QUrl &url, const QString &timezone, const std::vector<Sunrise> &sunrise)
242  : d(new PendingWeatherForecastPrivate(latitude, longitude, timezone, url, sunrise, this))
243 {
244 }
245 PendingWeatherForecast::PendingWeatherForecast(WeatherForecast data)
246  : d(new PendingWeatherForecastPrivate(data))
247 {
248 }
250 {
251  return d->isFinished;
252 }
253 
255 {
256  return d->forecast;
257 }
258 }
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
QString errorString() const const
QJsonObject object() const const
bool isObject() const const
QTime time() const const
int day() const const
void finished()
signals the call has finished
void ref()
QString toString() const const
The WeatherForecast class contains the weather information of one location for days.
bool isEmpty() const const
bool contains(const QString &key) const const
QByteArray readAll()
QStringList split(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
void networkError()
indicate there is a network error
void deleteLater()
void finished()
query finished
int hour() const const
QDateTime fromString(const QString &string, Qt::DateFormat format)
void finished()
signals the call has finished
bool isFinished() const
isFinished if the call has finished
Definition: pendingcap.cpp:63
QDate date() const const
void finished(QNetworkReply *reply)
void networkError()
indicate there is a network error
QNetworkReply::NetworkError error() const const
bool isFinished() const
isFinished if the call has finished
WeatherForecast value() const
value pointer to the shared weather data the pointer is nullptr until finished() raised ...
QJsonValue value(const QString &key) const const
QDateTime addSecs(qint64 s) const const
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
Q_EMITQ_EMIT
void finished(const QString &timezone)
finished emit when the timezone has been obtained
This file is part of the KDE documentation.
Documentation copyright © 1996-2021 The KDE developers.
Generated on Fri Oct 22 2021 23:03:35 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.