File indexing completed on 2024-05-05 16:49:22

0001 /*
0002  * SPDX-FileCopyrightText: 2020-2021 Han Young <hanyoung@protonmail.com>
0003  * SPDX-FileCopyrightText: 2020 Devin Lin <espidev@gmail.com>
0004  *
0005  * SPDX-License-Identifier: LGPL-2.0-or-later
0006  */
0007 #include "pendingweatherforecast.h"
0008 #include "geotimezone.h"
0009 #include "kweathercore_p.h"
0010 #include "pendingweatherforecast_p.h"
0011 #include "sunrisesource.h"
0012 #include <KLocalizedString>
0013 #include <QExplicitlySharedDataPointer>
0014 #include <QJsonArray>
0015 #include <QJsonDocument>
0016 #include <QJsonObject>
0017 #include <QNetworkReply>
0018 #include <QTimeZone>
0019 namespace KWeatherCore
0020 {
0021 PendingWeatherForecastPrivate::PendingWeatherForecastPrivate(
0022     double latitude,
0023     double longitude,
0024     const QString &timezone,
0025     QNetworkReply *reply,
0026     const std::vector<Sunrise> &sunrise,
0027     PendingWeatherForecast *parent)
0028     : QObject(parent)
0029     , forecast(
0030           QExplicitlySharedDataPointer<WeatherForecast>(new WeatherForecast))
0031     , m_latitude(latitude)
0032     , m_longitude(longitude)
0033     , m_timezone(timezone)
0034 {
0035     connect(this, &PendingWeatherForecastPrivate::finished, [this] {
0036         this->isFinished = true;
0037     });
0038     connect(this,
0039             &PendingWeatherForecastPrivate::finished,
0040             parent,
0041             &PendingWeatherForecast::finished);
0042     connect(this,
0043             &PendingWeatherForecastPrivate::networkError,
0044             parent,
0045             &PendingWeatherForecast::networkError);
0046     if (reply) {
0047         connect(reply, &QNetworkReply::finished, [this, reply] {
0048             this->parseWeatherForecastResults(reply);
0049         });
0050     }
0051 
0052     m_sunriseSource = new SunriseSource(latitude, longitude, m_timezone, sunrise, this);
0053     if (timezone.isEmpty()) {
0054         hasTimezone = false;
0055         getTimezone(latitude, longitude);
0056     } else {
0057         hasTimezone = true;
0058         forecast->setTimezone(timezone);
0059         m_timezone = timezone;
0060         getSunrise();
0061     }
0062 }
0063 void PendingWeatherForecastPrivate::getTimezone(double latitude,
0064                                                 double longitude)
0065 {
0066     auto timezoneSource = new GeoTimezone(latitude, longitude, this);
0067     connect(timezoneSource,
0068             &GeoTimezone::finished,
0069             this,
0070             &PendingWeatherForecastPrivate::parseTimezoneResult);
0071 }
0072 void PendingWeatherForecastPrivate::parseTimezoneResult(const QString &result)
0073 {
0074     hasTimezone = true;
0075     forecast->setTimezone(result);
0076     m_timezone = result;
0077     getSunrise();
0078 }
0079 
0080 void PendingWeatherForecastPrivate::getSunrise()
0081 {
0082     connect(m_sunriseSource,
0083             &SunriseSource::finished,
0084             this,
0085             &PendingWeatherForecastPrivate::parseSunriseResults);
0086     m_sunriseSource->setTimezone(m_timezone);
0087     m_sunriseSource->requestData();
0088 }
0089 void PendingWeatherForecastPrivate::parseSunriseResults()
0090 {
0091     hasSunrise = true;
0092 
0093     // if this arrived later than forecast
0094     if (!hourlyForecast.empty())
0095         applySunriseToForecast();
0096 }
0097 void PendingWeatherForecastPrivate::parseWeatherForecastResults(
0098     QNetworkReply *reply)
0099 {
0100     reply->deleteLater();
0101     if (reply->error()) {
0102         qWarning() << "network error when fetching forecast:"
0103                    << reply->errorString();
0104         Q_EMIT networkError();
0105         return;
0106     }
0107 
0108     QJsonDocument jsonDocument = QJsonDocument::fromJson(reply->readAll());
0109 
0110     if (jsonDocument.isObject()) {
0111         QJsonObject obj = jsonDocument.object();
0112         QJsonObject prop = obj[QStringLiteral("properties")].toObject();
0113 
0114         if (prop.contains(QStringLiteral("timeseries"))) {
0115             QJsonArray timeseries =
0116                 prop[QStringLiteral("timeseries")].toArray();
0117 
0118             // loop over all forecast data
0119             for (const auto &ref : qAsConst(timeseries)) {
0120                 parseOneElement(ref.toObject(), hourlyForecast);
0121             }
0122         }
0123     }
0124 
0125     if (hasTimezone && hasSunrise)
0126         applySunriseToForecast();
0127     // Q_EMIT finished();
0128 }
0129 
0130 void PendingWeatherForecastPrivate::parseOneElement(
0131     const QJsonObject &obj,
0132     std::vector<HourlyWeatherForecast> &hourlyForecast)
0133 {
0134     /*~~~~~~~~~~ lambda ~~~~~~~~~~~*/
0135 
0136     auto getWindDeg = [](double deg) -> WindDirection {
0137         if (deg < 22.5 || deg >= 337.5) {
0138             return WindDirection::S; // from N
0139         } else if (deg > 22.5 || deg <= 67.5) {
0140             return WindDirection::SW; // from NE
0141         } else if (deg > 67.5 || deg <= 112.5) {
0142             return WindDirection::W; // from E
0143         } else if (deg > 112.5 || deg <= 157.5) {
0144             return WindDirection::NW; // from SE
0145         } else if (deg > 157.5 || deg <= 202.5) {
0146             return WindDirection::N; // from S
0147         } else if (deg > 202.5 || deg <= 247.5) {
0148             return WindDirection::NE; // from SW
0149         } else if (deg > 247.5 || deg <= 292.5) {
0150             return WindDirection::E; // from W
0151         } else if (deg > 292.5 || deg <= 337.5) {
0152             return WindDirection::SE; // from NW
0153         }
0154         return WindDirection::N;
0155     };
0156 
0157     /*================== actual code ======================*/
0158 
0159     QJsonObject data = obj[QStringLiteral("data")].toObject(),
0160                 instant = data[QStringLiteral("instant")]
0161                               .toObject()[QStringLiteral("details")]
0162                               .toObject();
0163     // ignore last forecast, which does not have enough data
0164     if (!data.contains(QStringLiteral("next_6_hours")) &&
0165         !data.contains(QStringLiteral("next_1_hours")))
0166         return;
0167 
0168     // get symbolCode and precipitation amount
0169     QString symbolCode;
0170     double precipitationAmount = 0;
0171     // some fields contain only "next_1_hours", and others may contain only
0172     // "next_6_hours"
0173     if (data.contains(QStringLiteral("next_1_hours"))) {
0174         QJsonObject nextOneHours =
0175             data[QStringLiteral("next_1_hours")].toObject();
0176         symbolCode = nextOneHours[QStringLiteral("summary")]
0177                          .toObject()[QStringLiteral("symbol_code")]
0178                          .toString(QStringLiteral("unknown"));
0179         precipitationAmount =
0180             nextOneHours[QStringLiteral("details")]
0181                 .toObject()[QStringLiteral("precipitation_amount")]
0182                 .toDouble();
0183     } else {
0184         QJsonObject nextSixHours =
0185             data[QStringLiteral("next_6_hours")].toObject();
0186         symbolCode = nextSixHours[QStringLiteral("summary")]
0187                          .toObject()[QStringLiteral("symbol_code")]
0188                          .toString(QStringLiteral("unknown"));
0189         precipitationAmount =
0190             nextSixHours[QStringLiteral("details")]
0191                 .toObject()[QStringLiteral("precipitation_amount")]
0192                 .toDouble();
0193     }
0194 
0195     symbolCode = symbolCode.split(QLatin1Char(
0196         '_'))[0]; // trim _[day/night] from end -
0197                   // https://api.met.no/weatherapi/weathericon/2.0/legends
0198     HourlyWeatherForecast hourForecast(QDateTime::fromString(
0199         obj.value(QStringLiteral("time")).toString(), Qt::ISODate));
0200     hourForecast.setNeutralWeatherIcon(
0201         apiDescMap[symbolCode + QStringLiteral("_neutral")].icon);
0202     hourForecast.setTemperature(
0203         instant[QStringLiteral("air_temperature")].toDouble());
0204     hourForecast.setPressure(
0205         instant[QStringLiteral("air_pressure_at_sea_level")].toDouble());
0206     hourForecast.setWindDirection(
0207         getWindDeg(instant[QStringLiteral("wind_from_direction")].toDouble()));
0208     hourForecast.setWindSpeed(instant[QStringLiteral("wind_speed")].toDouble());
0209     hourForecast.setHumidity(
0210         instant[QStringLiteral("relative_humidity")].toDouble());
0211     hourForecast.setFog(
0212         instant[QStringLiteral("fog_area_fraction")].toDouble());
0213     hourForecast.setUvIndex(
0214         instant[QStringLiteral("ultraviolet_index_clear_sky")].toDouble());
0215     hourForecast.setPrecipitationAmount(precipitationAmount);
0216     hourForecast.setSymbolCode(symbolCode);
0217     hourlyForecast.push_back(std::move(hourForecast));
0218 }
0219 
0220 void PendingWeatherForecastPrivate::applySunriseToForecast()
0221 {
0222     // ************* Lambda *************** //
0223     auto isDayTime = [](const QDateTime &date,
0224                         const std::vector<Sunrise> &sunrise) {
0225         for (auto &sr : sunrise) {
0226             // if on the same day
0227             if (sr.sunRise().date().daysTo(date.date()) == 0 &&
0228                 sr.sunRise().date().day() == date.date().day()) {
0229                 // 30 min threshold
0230                 return sr.sunRise().addSecs(-1800) <= date &&
0231                     sr.sunSet().addSecs(1800) >= date;
0232             }
0233         }
0234 
0235         // not found
0236         return date.time().hour() >= 6 && date.time().hour() <= 18;
0237     };
0238 
0239     auto getSymbolCodeDescription = [](bool isDay, const QString &symbolCode) {
0240         return isDay ? apiDescMap[symbolCode + QStringLiteral("_day")].desc
0241                      : apiDescMap[symbolCode + QStringLiteral("_night")].desc;
0242     };
0243 
0244     auto getSymbolCodeIcon = [](bool isDay, const QString &symbolCode) {
0245         return isDay ? apiDescMap[symbolCode + QStringLiteral("_day")].icon
0246                      : apiDescMap[symbolCode + QStringLiteral("_night")].icon;
0247     };
0248 
0249     // ******* code ******** //
0250     for (auto &hourForecast : hourlyForecast) {
0251         hourForecast.setDate(
0252             hourForecast.date().toTimeZone(QTimeZone(m_timezone.toUtf8())));
0253 
0254         bool isDay;
0255         isDay = isDayTime(hourForecast.date(), m_sunriseSource->value());
0256         hourForecast.setWeatherIcon(getSymbolCodeIcon(
0257             isDay, hourForecast.symbolCode())); // set day/night icon
0258         hourForecast.setWeatherDescription(
0259             getSymbolCodeDescription(isDay, hourForecast.symbolCode()));
0260         *forecast += std::move(hourForecast);
0261     }
0262     forecast->setSunriseForecast(m_sunriseSource->value());
0263     Q_EMIT finished();
0264 }
0265 
0266 PendingWeatherForecast::PendingWeatherForecast(
0267     double latitude,
0268     double longitude,
0269     QNetworkReply *reply,
0270     const QString &timezone,
0271     const std::vector<Sunrise> &sunrise)
0272     : d(new PendingWeatherForecastPrivate(latitude,
0273                                           longitude,
0274                                           timezone,
0275                                           reply,
0276                                           sunrise,
0277                                           this))
0278 {
0279 }
0280 bool PendingWeatherForecast::isFinished() const
0281 {
0282     return d->isFinished;
0283 }
0284 
0285 QExplicitlySharedDataPointer<WeatherForecast>
0286 PendingWeatherForecast::value() const
0287 {
0288     return d->forecast;
0289 }
0290 }