File indexing completed on 2024-04-28 04:42:43

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 
0008 #include "kweathercore_p.h"
0009 #include "metnoparser_p.h"
0010 
0011 #include <KHolidays/SunRiseSet>
0012 #include <kholidays_version.h>
0013 
0014 #include <QJsonArray>
0015 #include <QJsonDocument>
0016 #include <QJsonObject>
0017 
0018 using namespace KWeatherCore;
0019 
0020 void MetNoParser::parseLocationForecast(const QByteArray &data)
0021 {
0022     const QJsonDocument jsonDocument = QJsonDocument::fromJson(data);
0023 
0024     if (jsonDocument.isObject()) {
0025         const QJsonObject obj = jsonDocument.object();
0026         const QJsonObject prop = obj[QLatin1String("properties")].toObject();
0027 
0028         if (prop.contains(QLatin1String("timeseries"))) {
0029             const QJsonArray timeseries = prop[QLatin1String("timeseries")].toArray();
0030 
0031             // loop over all forecast data
0032             for (const auto &ref : std::as_const(timeseries)) {
0033                 parseOneElement(ref.toObject());
0034             }
0035         }
0036     }
0037 }
0038 
0039 void MetNoParser::parseOneElement(const QJsonObject &obj)
0040 {
0041     const QJsonObject data = obj[QLatin1String("data")].toObject();
0042     const QJsonObject instant = data[QLatin1String("instant")].toObject()[QLatin1String("details")].toObject();
0043     // ignore last forecast, which does not have enough data
0044     if (!data.contains(QLatin1String("next_6_hours")) && !data.contains(QLatin1String("next_1_hours"))) {
0045         return;
0046     }
0047 
0048     // get symbolCode and precipitation amount
0049     QString symbolCode;
0050     double precipitationAmount = 0;
0051     // some fields contain only "next_1_hours", and others may contain only
0052     // "next_6_hours"
0053     if (data.contains(QLatin1String("next_1_hours"))) {
0054         const QJsonObject nextOneHours = data[QLatin1String("next_1_hours")].toObject();
0055         symbolCode = nextOneHours[QLatin1String("summary")].toObject()[QLatin1String("symbol_code")].toString(QLatin1String("unknown"));
0056         precipitationAmount = nextOneHours[QLatin1String("details")].toObject()[QLatin1String("precipitation_amount")].toDouble();
0057     } else {
0058         const QJsonObject nextSixHours = data[QLatin1String("next_6_hours")].toObject();
0059         symbolCode = nextSixHours[QLatin1String("summary")].toObject()[QLatin1String("symbol_code")].toString(QLatin1String("unknown"));
0060         precipitationAmount = nextSixHours[QLatin1String("details")].toObject()[QLatin1String("precipitation_amount")].toDouble();
0061     }
0062 
0063     symbolCode = symbolCode.split(QLatin1Char('_'))[0]; // trim _[day/night] from end -
0064                                                         // https://api.met.no/weatherapi/weathericon/2.0/legends
0065     HourlyWeatherForecast hourForecast(QDateTime::fromString(obj.value(QLatin1String("time")).toString(), Qt::ISODate));
0066     hourForecast.setNeutralWeatherIcon(KWeatherCorePrivate::resolveAPIWeatherDesc(symbolCode + QLatin1String("_neutral")).icon);
0067     hourForecast.setTemperature(instant[QLatin1String("air_temperature")].toDouble());
0068     hourForecast.setPressure(instant[QLatin1String("air_pressure_at_sea_level")].toDouble());
0069     hourForecast.setWindDirectionDegree(instant[QLatin1String("wind_from_direction")].toDouble());
0070     hourForecast.setWindSpeed(instant[QLatin1String("wind_speed")].toDouble());
0071     hourForecast.setHumidity(instant[QLatin1String("relative_humidity")].toDouble());
0072     hourForecast.setFog(instant[QLatin1String("fog_area_fraction")].toDouble());
0073     hourForecast.setUvIndex(instant[QLatin1String("ultraviolet_index_clear_sky")].toDouble());
0074     hourForecast.setPrecipitationAmount(precipitationAmount);
0075     hourForecast.setSymbolCode(symbolCode);
0076     hourlyForecast.push_back(std::move(hourForecast));
0077 }
0078 
0079 bool MetNoParser::isDayTime(const QDateTime &dt) const
0080 {
0081     const auto sunriseTime = KHolidays::SunRiseSet::utcSunrise(dt.date(), forecast.latitude(), forecast.longitude());
0082     const auto sunsetTime = KHolidays::SunRiseSet::utcSunset(dt.date(), forecast.latitude(), forecast.longitude());
0083 
0084 #if KHOLIDAYS_VERSION >= QT_VERSION_CHECK(5, 97, 0)
0085     // polar day/night: there is no sunrise/sunset
0086     if (!sunriseTime.isValid() || !sunsetTime.isValid()) {
0087         return KHolidays::SunRiseSet::isPolarDay(dt.date(), forecast.latitude());
0088     }
0089 #endif
0090 
0091     auto sunrise = QDateTime(dt.date(), sunriseTime, Qt::UTC);
0092     auto sunset = QDateTime(dt.date(), sunsetTime, Qt::UTC);
0093 
0094     // sunset before sunrise means the sunset actually happens the next day
0095     if (dt >= sunrise && sunset < sunrise) {
0096         sunset = sunset.addDays(1);
0097     } else if (dt < sunrise && sunset < sunrise) {
0098         sunrise = sunrise.addDays(-1);
0099     }
0100 
0101     // 30 min threshold
0102     return sunrise.addSecs(-1800) <= dt && sunset.addSecs(1800) >= dt;
0103 }
0104 
0105 void MetNoParser::applySunriseToForecast(const QTimeZone &timezone)
0106 {
0107     // ************* Lambda *************** //
0108     auto getSymbolCodeDescription = [](bool isDay, const QString &symbolCode) {
0109         return isDay ? KWeatherCorePrivate::resolveAPIWeatherDesc(symbolCode + QStringLiteral("_day")).desc
0110                      : KWeatherCorePrivate::resolveAPIWeatherDesc(symbolCode + QStringLiteral("_night")).desc;
0111     };
0112 
0113     auto getSymbolCodeIcon = [](bool isDay, const QString &symbolCode) {
0114         return isDay ? KWeatherCorePrivate::resolveAPIWeatherDesc(symbolCode + QStringLiteral("_day")).icon
0115                      : KWeatherCorePrivate::resolveAPIWeatherDesc(symbolCode + QStringLiteral("_night")).icon;
0116     };
0117 
0118     // ******* code ******** //
0119     for (auto &hourForecast : hourlyForecast) {
0120         hourForecast.setDate(hourForecast.date().toTimeZone(timezone));
0121 
0122         bool isDay;
0123         isDay = isDayTime(hourForecast.date());
0124         hourForecast.setWeatherIcon(getSymbolCodeIcon(isDay, hourForecast.symbolCode())); // set day/night icon
0125         hourForecast.setWeatherDescription(getSymbolCodeDescription(isDay, hourForecast.symbolCode()));
0126         forecast += std::move(hourForecast);
0127     }
0128 
0129     // save to cache
0130     QFile file(KWeatherCorePrivate::getCacheDirectory(forecast.latitude(), forecast.longitude()).path() + QStringLiteral("/cache.json"));
0131 
0132     if (file.open(QIODevice::WriteOnly)) {
0133         file.write(QJsonDocument(forecast.toJson()).toJson(QJsonDocument::Compact));
0134     } else {
0135         qWarning() << "write to cache failed";
0136     }
0137 }