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 }