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 <QExplicitlySharedDataPointer>
15 #include <QFile>
16 #include <QJsonArray>
17 #include <QJsonDocument>
18 #include <QJsonObject>
19 #include <QNetworkReply>
20 #include <QStandardPaths>
21 #include <QTimeZone>
22 namespace KWeatherCore
23 {
24 PendingWeatherForecastPrivate::PendingWeatherForecastPrivate(
25  double latitude,
26  double longitude,
27  const QString &timezone,
28  const QUrl &url,
29  const std::vector<Sunrise> &sunrise,
30  PendingWeatherForecast *parent)
31  : QObject(parent)
32  , forecast(
33  QExplicitlySharedDataPointer<WeatherForecast>(new WeatherForecast))
34  , m_latitude(latitude)
35  , m_longitude(longitude)
36  , m_timezone(timezone)
37 {
38  connect(this, &PendingWeatherForecastPrivate::finished, [this] {
39  this->isFinished = true;
40  });
41  connect(this,
42  &PendingWeatherForecastPrivate::finished,
43  parent,
45  connect(this,
46  &PendingWeatherForecastPrivate::networkError,
47  parent,
49 
50  QNetworkRequest req(url);
53 
54  // see ┬žIdentification on https://api.met.no/conditions_service.html
56  QString(QStringLiteral("KWeatherCore/") + VERSION_NUMBER +
57  QStringLiteral(" [email protected]")));
58  connect(&m_manager, &QNetworkAccessManager::finished, this, &PendingWeatherForecastPrivate::parseWeatherForecastResults);
59  m_manager.get(req);
60 
61  forecast->setCoordinate(latitude, longitude);
62 
63  m_sunriseSource =
64  new SunriseSource(latitude, longitude, m_timezone, sunrise, this);
65  if (timezone.isEmpty()) {
66  hasTimezone = false;
67  getTimezone(latitude, longitude);
68  } else {
69  hasTimezone = true;
70  forecast->setTimezone(timezone);
71  m_timezone = timezone;
72  getSunrise();
73  }
74 }
75 PendingWeatherForecastPrivate::PendingWeatherForecastPrivate(
77  : forecast(data)
78  , isFinished(true)
79 {
80 }
81 void PendingWeatherForecastPrivate::getTimezone(double latitude,
82  double longitude)
83 {
84  auto timezoneSource = new GeoTimezone(latitude, longitude, this);
85  connect(timezoneSource,
87  this,
88  &PendingWeatherForecastPrivate::parseTimezoneResult);
89 }
90 void PendingWeatherForecastPrivate::parseTimezoneResult(const QString &result)
91 {
92  hasTimezone = true;
93  forecast->setTimezone(result);
94  m_timezone = result;
95  getSunrise();
96 }
97 
98 void PendingWeatherForecastPrivate::getSunrise()
99 {
100  connect(m_sunriseSource,
102  this,
103  &PendingWeatherForecastPrivate::parseSunriseResults);
104  m_sunriseSource->setTimezone(m_timezone);
105  m_sunriseSource->requestData();
106 }
107 void PendingWeatherForecastPrivate::parseSunriseResults()
108 {
109  hasSunrise = true;
110 
111  // if this arrived later than forecast
112  if (!hourlyForecast.empty())
113  applySunriseToForecast();
114 }
115 void PendingWeatherForecastPrivate::parseWeatherForecastResults(
116  QNetworkReply *reply)
117 {
118  reply->deleteLater();
119  if (reply->error()) {
120  qWarning() << "network error when fetching forecast:"
121  << reply->errorString();
123  return;
124  }
125 
126  QJsonDocument jsonDocument = QJsonDocument::fromJson(reply->readAll());
127 
128  if (jsonDocument.isObject()) {
129  QJsonObject obj = jsonDocument.object();
130  QJsonObject prop = obj[QStringLiteral("properties")].toObject();
131 
132  if (prop.contains(QStringLiteral("timeseries"))) {
133  QJsonArray timeseries =
134  prop[QStringLiteral("timeseries")].toArray();
135 
136  // loop over all forecast data
137  for (const auto &ref : qAsConst(timeseries)) {
138  parseOneElement(ref.toObject(), hourlyForecast);
139  }
140  }
141  }
142 
143  if (hasTimezone && hasSunrise)
144  applySunriseToForecast();
145  // Q_EMIT finished();
146 }
147 
148 void PendingWeatherForecastPrivate::parseOneElement(
149  const QJsonObject &obj,
150  std::vector<HourlyWeatherForecast> &hourlyForecast)
151 {
152  /*~~~~~~~~~~ lambda ~~~~~~~~~~~*/
153 
154  auto getWindDeg = [](double deg) -> WindDirection {
155  if (deg < 22.5 || deg >= 337.5) {
156  return WindDirection::S; // from N
157  } else if (deg > 22.5 || deg <= 67.5) {
158  return WindDirection::SW; // from NE
159  } else if (deg > 67.5 || deg <= 112.5) {
160  return WindDirection::W; // from E
161  } else if (deg > 112.5 || deg <= 157.5) {
162  return WindDirection::NW; // from SE
163  } else if (deg > 157.5 || deg <= 202.5) {
164  return WindDirection::N; // from S
165  } else if (deg > 202.5 || deg <= 247.5) {
166  return WindDirection::NE; // from SW
167  } else if (deg > 247.5 || deg <= 292.5) {
168  return WindDirection::E; // from W
169  } else if (deg > 292.5 || deg <= 337.5) {
170  return WindDirection::SE; // from NW
171  }
172  return WindDirection::N;
173  };
174 
175  /*================== actual code ======================*/
176 
177  QJsonObject data = obj[QStringLiteral("data")].toObject(),
178  instant = data[QStringLiteral("instant")]
179  .toObject()[QStringLiteral("details")]
180  .toObject();
181  // ignore last forecast, which does not have enough data
182  if (!data.contains(QStringLiteral("next_6_hours")) &&
183  !data.contains(QStringLiteral("next_1_hours")))
184  return;
185 
186  // get symbolCode and precipitation amount
187  QString symbolCode;
188  double precipitationAmount = 0;
189  // some fields contain only "next_1_hours", and others may contain only
190  // "next_6_hours"
191  if (data.contains(QStringLiteral("next_1_hours"))) {
192  QJsonObject nextOneHours =
193  data[QStringLiteral("next_1_hours")].toObject();
194  symbolCode = nextOneHours[QStringLiteral("summary")]
195  .toObject()[QStringLiteral("symbol_code")]
196  .toString(QStringLiteral("unknown"));
197  precipitationAmount =
198  nextOneHours[QStringLiteral("details")]
199  .toObject()[QStringLiteral("precipitation_amount")]
200  .toDouble();
201  } else {
202  QJsonObject nextSixHours =
203  data[QStringLiteral("next_6_hours")].toObject();
204  symbolCode = nextSixHours[QStringLiteral("summary")]
205  .toObject()[QStringLiteral("symbol_code")]
206  .toString(QStringLiteral("unknown"));
207  precipitationAmount =
208  nextSixHours[QStringLiteral("details")]
209  .toObject()[QStringLiteral("precipitation_amount")]
210  .toDouble();
211  }
212 
213  symbolCode = symbolCode.split(QLatin1Char(
214  '_'))[0]; // trim _[day/night] from end -
215  // https://api.met.no/weatherapi/weathericon/2.0/legends
216  HourlyWeatherForecast hourForecast(QDateTime::fromString(
217  obj.value(QStringLiteral("time")).toString(), Qt::ISODate));
218  hourForecast.setNeutralWeatherIcon(
219  self()->resolveAPIWeatherDesc(symbolCode + QStringLiteral("_neutral")).icon);
220  hourForecast.setTemperature(
221  instant[QStringLiteral("air_temperature")].toDouble());
222  hourForecast.setPressure(
223  instant[QStringLiteral("air_pressure_at_sea_level")].toDouble());
224  hourForecast.setWindDirection(
225  getWindDeg(instant[QStringLiteral("wind_from_direction")].toDouble()));
226  hourForecast.setWindSpeed(instant[QStringLiteral("wind_speed")].toDouble());
227  hourForecast.setHumidity(
228  instant[QStringLiteral("relative_humidity")].toDouble());
229  hourForecast.setFog(
230  instant[QStringLiteral("fog_area_fraction")].toDouble());
231  hourForecast.setUvIndex(
232  instant[QStringLiteral("ultraviolet_index_clear_sky")].toDouble());
233  hourForecast.setPrecipitationAmount(precipitationAmount);
234  hourForecast.setSymbolCode(symbolCode);
235  hourlyForecast.push_back(std::move(hourForecast));
236 }
237 
238 void PendingWeatherForecastPrivate::applySunriseToForecast()
239 {
240  // ************* Lambda *************** //
241  auto isDayTime = [](const QDateTime &date,
242  const std::vector<Sunrise> &sunrise) {
243  for (auto &sr : sunrise) {
244  // if on the same day
245  if (sr.sunRise().date().daysTo(date.date()) == 0 &&
246  sr.sunRise().date().day() == date.date().day()) {
247  // 30 min threshold
248  return sr.sunRise().addSecs(-1800) <= date &&
249  sr.sunSet().addSecs(1800) >= date;
250  }
251  }
252 
253  // not found
254  return date.time().hour() >= 6 && date.time().hour() <= 18;
255  };
256 
257  auto getSymbolCodeDescription = [](bool isDay, const QString &symbolCode) {
258  return isDay ? self()->resolveAPIWeatherDesc(symbolCode + QStringLiteral("_day")).desc
259  : self()->resolveAPIWeatherDesc(symbolCode + QStringLiteral("_night")).desc;
260  };
261 
262  auto getSymbolCodeIcon = [](bool isDay, const QString &symbolCode) {
263  return isDay ? self()->resolveAPIWeatherDesc(symbolCode + QStringLiteral("_day")).icon
264  : self()->resolveAPIWeatherDesc(symbolCode + QStringLiteral("_night")).icon;
265  };
266 
267  // ******* code ******** //
268  for (auto &hourForecast : hourlyForecast) {
269  hourForecast.setDate(
270  hourForecast.date().toTimeZone(QTimeZone(m_timezone.toUtf8())));
271 
272  bool isDay;
273  isDay = isDayTime(hourForecast.date(), m_sunriseSource->value());
274  hourForecast.setWeatherIcon(getSymbolCodeIcon(
275  isDay, hourForecast.symbolCode())); // set day/night icon
276  hourForecast.setWeatherDescription(
277  getSymbolCodeDescription(isDay, hourForecast.symbolCode()));
278  *forecast += std::move(hourForecast);
279  }
280  forecast->setSunriseForecast(m_sunriseSource->value());
281  Q_EMIT finished();
282 
283  // save to cache
284 
285  QFile file(self()->getCacheDirectory(m_latitude, m_longitude).path() + QStringLiteral("/cache.json"));
286 
287  if (file.open(QIODevice::WriteOnly)) {
288  file.write(
289  QJsonDocument(forecast->toJson()).toJson(QJsonDocument::Compact));
290  } else
291  qWarning() << "write to cache failed";
292 }
293 
294 PendingWeatherForecast::PendingWeatherForecast(
295  double latitude,
296  double longitude,
297  const QUrl &url,
298  const QString &timezone,
299  const std::vector<Sunrise> &sunrise)
300  : d(new PendingWeatherForecastPrivate(latitude,
301  longitude,
302  timezone,
303  url,
304  sunrise,
305  this))
306 {
307 }
308 PendingWeatherForecast::PendingWeatherForecast(
310  : d(new PendingWeatherForecastPrivate(data))
311 {
312 }
314 {
315  return d->isFinished;
316 }
317 
320 {
321  return d->forecast;
322 }
323 }
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
QExplicitlySharedDataPointer< WeatherForecast > value() const
value pointer to the shared weather data the pointer is nullptr until finished() raised ...
void finished()
signals the call has finished
void ref()
QString toString() const const
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:61
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
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 Thu Jul 29 2021 23:03:41 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.