File indexing completed on 2025-01-12 05:01:49

0001 /*
0002     SPDX-FileCopyrightText: 2021 Emily Ehlert
0003 
0004     Based upon BBC Weather Ion and ENV Canada Ion by Shawn Starr
0005     SPDX-FileCopyrightText: 2007-2009 Shawn Starr <shawn.starr@rogers.com>
0006 
0007     also
0008 
0009     the wetter.com Ion by Thilo-Alexander Ginkel
0010     SPDX-FileCopyrightText: 2009 Thilo-Alexander Ginkel <thilo@ginkel.com>
0011 
0012     SPDX-License-Identifier: GPL-2.0-or-later
0013 */
0014 
0015 /* Ion for weather data from Deutscher Wetterdienst (DWD) / German Weather Service */
0016 
0017 #include "ion_dwd.h"
0018 
0019 #include "ion_dwddebug.h"
0020 
0021 #include <KIO/TransferJob>
0022 #include <KLocalizedString>
0023 #include <KUnitConversion/Converter>
0024 
0025 #include <QDateTime>
0026 #include <QJsonArray>
0027 #include <QJsonDocument>
0028 #include <QJsonObject>
0029 #include <QLocale>
0030 #include <QVariant>
0031 
0032 /*
0033  * Initialization
0034  */
0035 
0036 WeatherData::WeatherData()
0037     : temperature(qQNaN())
0038     , humidity(qQNaN())
0039     , pressure(qQNaN())
0040     , windSpeed(qQNaN())
0041     , gustSpeed(qQNaN())
0042     , dewpoint(qQNaN())
0043     , windSpeedAlt(qQNaN())
0044     , gustSpeedAlt(qQNaN())
0045 {
0046 }
0047 
0048 WeatherData::ForecastInfo::ForecastInfo()
0049     : tempHigh(qQNaN())
0050     , tempLow(qQNaN())
0051     , windSpeed(qQNaN())
0052 {
0053 }
0054 
0055 DWDIon::DWDIon(QObject *parent)
0056     : IonInterface(parent)
0057 
0058 {
0059     setInitialized(true);
0060 }
0061 
0062 DWDIon::~DWDIon()
0063 {
0064     deleteForecasts();
0065 }
0066 
0067 void DWDIon::reset()
0068 {
0069     deleteForecasts();
0070     m_sourcesToReset = sources();
0071     updateAllSources();
0072 }
0073 
0074 void DWDIon::deleteForecasts()
0075 {
0076     // Destroy each forecast stored in a QList
0077     for (auto it = m_weatherData.begin(), end = m_weatherData.end(); it != end; ++it) {
0078         qDeleteAll(it.value().forecasts);
0079         it.value().forecasts.clear();
0080     }
0081 }
0082 
0083 QMap<QString, IonInterface::ConditionIcons> DWDIon::setupDayIconMappings() const
0084 {
0085     //    DWD supplies it's own icon number which we can use to determine a condition
0086 
0087     return QMap<QString, ConditionIcons>{{QStringLiteral("1"), ClearDay},
0088                                          {QStringLiteral("2"), PartlyCloudyDay},
0089                                          {QStringLiteral("3"), PartlyCloudyDay},
0090                                          {QStringLiteral("4"), Overcast},
0091                                          {QStringLiteral("5"), Mist},
0092                                          {QStringLiteral("6"), Mist},
0093                                          {QStringLiteral("7"), LightRain},
0094                                          {QStringLiteral("8"), Rain},
0095                                          {QStringLiteral("9"), Rain},
0096                                          {QStringLiteral("10"), LightRain},
0097                                          {QStringLiteral("11"), Rain},
0098                                          {QStringLiteral("12"), Flurries},
0099                                          {QStringLiteral("13"), RainSnow},
0100                                          {QStringLiteral("14"), LightSnow},
0101                                          {QStringLiteral("15"), Snow},
0102                                          {QStringLiteral("16"), Snow},
0103                                          {QStringLiteral("17"), Hail},
0104                                          {QStringLiteral("18"), LightRain},
0105                                          {QStringLiteral("19"), Rain},
0106                                          {QStringLiteral("20"), Flurries},
0107                                          {QStringLiteral("21"), RainSnow},
0108                                          {QStringLiteral("22"), LightSnow},
0109                                          {QStringLiteral("23"), Snow},
0110                                          {QStringLiteral("24"), Hail},
0111                                          {QStringLiteral("25"), Hail},
0112                                          {QStringLiteral("26"), Thunderstorm},
0113                                          {QStringLiteral("27"), Thunderstorm},
0114                                          {QStringLiteral("28"), Thunderstorm},
0115                                          {QStringLiteral("29"), Thunderstorm},
0116                                          {QStringLiteral("30"), Thunderstorm},
0117                                          {QStringLiteral("31"), ClearWindyDay}};
0118 }
0119 
0120 QMap<QString, IonInterface::WindDirections> DWDIon::setupWindIconMappings() const
0121 {
0122     return QMap<QString, WindDirections>{
0123         {QStringLiteral("0"), N},     {QStringLiteral("10"), N},    {QStringLiteral("20"), NNE},  {QStringLiteral("30"), NNE},  {QStringLiteral("40"), NE},
0124         {QStringLiteral("50"), NE},   {QStringLiteral("60"), ENE},  {QStringLiteral("70"), ENE},  {QStringLiteral("80"), E},    {QStringLiteral("90"), E},
0125         {QStringLiteral("100"), E},   {QStringLiteral("120"), ESE}, {QStringLiteral("130"), ESE}, {QStringLiteral("140"), SE},  {QStringLiteral("150"), SE},
0126         {QStringLiteral("160"), SSE}, {QStringLiteral("170"), SSE}, {QStringLiteral("180"), S},   {QStringLiteral("190"), S},   {QStringLiteral("200"), SSW},
0127         {QStringLiteral("210"), SSW}, {QStringLiteral("220"), SW},  {QStringLiteral("230"), SW},  {QStringLiteral("240"), WSW}, {QStringLiteral("250"), WSW},
0128         {QStringLiteral("260"), W},   {QStringLiteral("270"), W},   {QStringLiteral("280"), W},   {QStringLiteral("290"), WNW}, {QStringLiteral("300"), WNW},
0129         {QStringLiteral("310"), NW},  {QStringLiteral("320"), NW},  {QStringLiteral("330"), NNW}, {QStringLiteral("340"), NNW}, {QStringLiteral("350"), N},
0130         {QStringLiteral("360"), N},
0131     };
0132 }
0133 
0134 QMap<QString, IonInterface::ConditionIcons> const &DWDIon::dayIcons() const
0135 {
0136     static QMap<QString, ConditionIcons> const dval = setupDayIconMappings();
0137     return dval;
0138 }
0139 
0140 QMap<QString, IonInterface::WindDirections> const &DWDIon::windIcons() const
0141 {
0142     static QMap<QString, WindDirections> const wval = setupWindIconMappings();
0143     return wval;
0144 }
0145 
0146 bool DWDIon::updateIonSource(const QString &source)
0147 {
0148     // We expect the applet to send the source in the following tokenization:
0149     // ionname|validate|place_name|extra - Triggers validation (search) of place
0150     // ionname|weather|place_name|extra - Triggers receiving weather of place
0151     const QStringList sourceAction = source.split(QLatin1Char('|'));
0152 
0153     if (sourceAction.size() < 3) {
0154         setData(source, QStringLiteral("validate"), QStringLiteral("dwd|malformed"));
0155         return true;
0156     }
0157 
0158     if (sourceAction[1] == QLatin1String("validate") && sourceAction.size() >= 3) {
0159         // Look for places to match
0160         findPlace(sourceAction[2]);
0161         return true;
0162     }
0163     if (sourceAction[1] == QLatin1String("weather") && sourceAction.size() >= 3) {
0164         if (sourceAction.count() >= 4) {
0165             if (sourceAction[2].isEmpty()) {
0166                 setData(source, QStringLiteral("validate"), QStringLiteral("dwd|malformed"));
0167                 return true;
0168             }
0169 
0170             // Extra data: station_id
0171             m_place[sourceAction[2]] = sourceAction[3];
0172 
0173             qCDebug(IONENGINE_dwd) << "About to retrieve forecast for source: " << sourceAction[2];
0174 
0175             fetchWeather(sourceAction[2], m_place[sourceAction[2]]);
0176 
0177             return true;
0178         }
0179 
0180         return false;
0181     }
0182 
0183     setData(source, QStringLiteral("validate"), QStringLiteral("dwd|malformed"));
0184     return true;
0185 }
0186 
0187 void DWDIon::findPlace(const QString &searchText)
0188 {
0189     // Checks if the stations have already been loaded, always contains the currently active one
0190     if (m_place.size() > 1) {
0191         setData(QStringLiteral("dwd|validate|") + searchText, Data());
0192         searchInStationList(searchText);
0193     } else {
0194         const QUrl forecastURL(QStringLiteral(CATALOGUE_URL));
0195         KIO::TransferJob *getJob = KIO::get(forecastURL, KIO::Reload, KIO::HideProgressInfo);
0196         getJob->addMetaData(QStringLiteral("cookies"), QStringLiteral("none"));
0197 
0198         m_searchJobList.insert(getJob, searchText);
0199         m_searchJobData.insert(getJob, QByteArray(""));
0200 
0201         connect(getJob, &KIO::TransferJob::data, this, &DWDIon::setup_slotDataArrived);
0202         connect(getJob, &KJob::result, this, &DWDIon::setup_slotJobFinished);
0203     }
0204 }
0205 
0206 void DWDIon::fetchWeather(QString placeName, QString placeID)
0207 {
0208     for (const QString &fetching : std::as_const(m_forecastJobList)) {
0209         if (fetching == placeName) {
0210             // already fetching!
0211             return;
0212         }
0213     }
0214 
0215     // Fetch forecast data
0216 
0217     const QUrl forecastURL(QStringLiteral(FORECAST_URL).arg(placeID));
0218     KIO::TransferJob *getJob = KIO::get(forecastURL, KIO::Reload, KIO::HideProgressInfo);
0219     getJob->addMetaData(QStringLiteral("cookies"), QStringLiteral("none"));
0220 
0221     m_forecastJobList.insert(getJob, placeName);
0222     m_forecastJobJSON.insert(getJob, QByteArray(""));
0223 
0224     qCDebug(IONENGINE_dwd) << "Requesting URL: " << forecastURL;
0225 
0226     connect(getJob, &KIO::TransferJob::data, this, &DWDIon::forecast_slotDataArrived);
0227     connect(getJob, &KJob::result, this, &DWDIon::forecast_slotJobFinished);
0228     m_weatherData[placeName].isForecastsDataPending = true;
0229 
0230     // Fetch current measurements (different url AND different API, AMAZING)
0231 
0232     const QUrl measureURL(QStringLiteral(MEASURE_URL).arg(placeID));
0233     KIO::TransferJob *getMeasureJob = KIO::get(measureURL, KIO::Reload, KIO::HideProgressInfo);
0234     getMeasureJob->addMetaData(QStringLiteral("cookies"), QStringLiteral("none"));
0235 
0236     m_measureJobList.insert(getMeasureJob, placeName);
0237     m_measureJobJSON.insert(getMeasureJob, QByteArray(""));
0238 
0239     qCDebug(IONENGINE_dwd) << "Requesting URL: " << measureURL;
0240 
0241     connect(getMeasureJob, &KIO::TransferJob::data, this, &DWDIon::measure_slotDataArrived);
0242     connect(getMeasureJob, &KJob::result, this, &DWDIon::measure_slotJobFinished);
0243     m_weatherData[placeName].isMeasureDataPending = true;
0244 }
0245 
0246 void DWDIon::setup_slotDataArrived(KIO::Job *job, const QByteArray &data)
0247 {
0248     QByteArray local = data;
0249 
0250     if (data.isEmpty() || !m_searchJobData.contains(job)) {
0251         return;
0252     }
0253 
0254     m_searchJobData[job].append(local);
0255 }
0256 
0257 void DWDIon::measure_slotDataArrived(KIO::Job *job, const QByteArray &data)
0258 {
0259     QByteArray local = data;
0260 
0261     if (data.isEmpty() || !m_measureJobJSON.contains(job)) {
0262         return;
0263     }
0264 
0265     m_measureJobJSON[job].append(local);
0266 }
0267 
0268 void DWDIon::forecast_slotDataArrived(KIO::Job *job, const QByteArray &data)
0269 {
0270     QByteArray local = data;
0271 
0272     if (data.isEmpty() || !m_forecastJobJSON.contains(job)) {
0273         return;
0274     }
0275 
0276     m_forecastJobJSON[job].append(local);
0277 }
0278 
0279 void DWDIon::setup_slotJobFinished(KJob *job)
0280 {
0281     if (!job->error()) {
0282         const QString searchText(m_searchJobList.value(job));
0283         setData(QStringLiteral("dwd|validate|") + searchText, Data());
0284 
0285         QByteArray catalogueData = m_searchJobData[job];
0286         if (!catalogueData.isEmpty()) {
0287             parseStationData(catalogueData);
0288             searchInStationList(searchText);
0289         }
0290     } else {
0291         qCWarning(IONENGINE_dwd) << "error during setup" << job->errorText();
0292     }
0293 
0294     m_searchJobList.remove(job);
0295     m_searchJobData.remove(job);
0296 }
0297 
0298 void DWDIon::measure_slotJobFinished(KJob *job)
0299 {
0300     const QString source(m_measureJobList.value(job));
0301     const QByteArray &jsonData = m_measureJobJSON.value(job);
0302 
0303     if (!job->error() && !jsonData.isEmpty()) {
0304         setData(source, Data());
0305         QJsonDocument doc = QJsonDocument::fromJson(jsonData);
0306         parseMeasureData(source, doc);
0307     } else {
0308         qCWarning(IONENGINE_dwd) << "no measurements received" << job->errorText();
0309         m_weatherData[source].isMeasureDataPending = false;
0310         updateWeather(source);
0311     }
0312 
0313     m_measureJobList.remove(job);
0314     m_measureJobJSON.remove(job);
0315 }
0316 
0317 void DWDIon::forecast_slotJobFinished(KJob *job)
0318 {
0319     if (!job->error()) {
0320         const QString source(m_forecastJobList.value(job));
0321         setData(source, Data());
0322 
0323         QJsonDocument doc = QJsonDocument::fromJson(m_forecastJobJSON.value(job));
0324 
0325         if (!doc.isEmpty()) {
0326             parseForecastData(source, doc);
0327         }
0328 
0329         if (m_sourcesToReset.contains(source)) {
0330             m_sourcesToReset.removeAll(source);
0331             const QString weatherSource = QStringLiteral("dwd|weather|%1|%2").arg(source, m_place[source]);
0332 
0333             // so the weather engine updates it's data
0334             forceImmediateUpdateOfAllVisualizations();
0335 
0336             // update the clients of our engine
0337             Q_EMIT forceUpdate(this, weatherSource);
0338         }
0339     } else {
0340         qCWarning(IONENGINE_dwd) << "error during forecast" << job->errorText();
0341     }
0342 
0343     m_forecastJobList.remove(job);
0344     m_forecastJobJSON.remove(job);
0345 }
0346 
0347 void DWDIon::calculatePositions(QStringList lines, QList<int> &namePositionalInfo, QList<int> &stationIdPositionalInfo)
0348 {
0349     QStringList stringLengths = lines[1].split(QChar::Space);
0350     QList<int> lengths;
0351     for (const QString &length : std::as_const(stringLengths)) {
0352         lengths.append(length.count());
0353     }
0354 
0355     int curpos = 0;
0356 
0357     for (int labelLength : lengths) {
0358         QString label = lines[0].mid(curpos, labelLength).toLower();
0359 
0360         if (label.contains(QStringLiteral("name"))) {
0361             namePositionalInfo[0] = curpos;
0362             namePositionalInfo[1] = labelLength;
0363         } else if (label.contains(QStringLiteral("id"))) {
0364             stationIdPositionalInfo[0] = curpos;
0365             stationIdPositionalInfo[1] = labelLength;
0366         }
0367 
0368         curpos += labelLength + 1;
0369     }
0370 }
0371 
0372 void DWDIon::parseStationData(QByteArray data)
0373 {
0374     QString stringData = QString::fromLatin1(data);
0375     QStringList lines = stringData.split(QChar::LineFeed);
0376 
0377     QList<int> namePositionalInfo(2);
0378     QList<int> stationIdPositionalInfo(2);
0379     calculatePositions(lines, namePositionalInfo, stationIdPositionalInfo);
0380 
0381     // This loop parses the station file (https://www.dwd.de/DE/leistungen/met_verfahren_mosmix/mosmix_stationskatalog.cfg)
0382     // ID    ICAO NAME                 LAT    LON     ELEV
0383     // ----- ---- -------------------- -----  ------- -----
0384     // 01001 ENJA JAN MAYEN             70.56   -8.40    10
0385     // 01008 ENSB SVALBARD              78.15   15.28    29
0386     int lineIndex = 0;
0387     for (const QString &line : std::as_const(lines)) {
0388         QString name = line.mid(namePositionalInfo[0], namePositionalInfo[1]).trimmed();
0389         QString id = line.mid(stationIdPositionalInfo[0], stationIdPositionalInfo[1]).trimmed();
0390 
0391         // This checks if this station is a station we know is working
0392         // With this we remove all non working but also a lot of working ones.
0393         if (id.startsWith(QLatin1Char('0')) || id.startsWith(QLatin1Char('1'))) {
0394             m_place.insert(camelCaseString(name), id);
0395         } else if (lineIndex > 10) {
0396             // After header is passed and some more lines for safety, abort parse if filter fails, all acceptable stations were found
0397             break;
0398         }
0399 
0400         lineIndex += 1;
0401     }
0402     qCDebug(IONENGINE_dwd) << "Number of parsed stations: " << m_place.size();
0403 }
0404 
0405 void DWDIon::searchInStationList(const QString searchText)
0406 {
0407     qCDebug(IONENGINE_dwd) << searchText;
0408 
0409     QMap<QString, QString>::const_iterator it = m_place.constBegin();
0410     auto end = m_place.constEnd();
0411 
0412     while (it != end) {
0413         QString name = it.key();
0414         if (name.contains(searchText, Qt::CaseInsensitive)) {
0415             m_locations.append(it.key());
0416         }
0417         ++it;
0418     }
0419 
0420     validate(searchText);
0421 }
0422 
0423 void DWDIon::parseForecastData(const QString source, QJsonDocument doc)
0424 {
0425     QVariantMap weatherMap = doc.object().toVariantMap().first().toMap();
0426     if (!weatherMap.isEmpty()) {
0427         // Forecast data
0428         QVariantList daysList = weatherMap[QStringLiteral("days")].toList();
0429 
0430         WeatherData &weatherData = m_weatherData[source];
0431         QList<WeatherData::ForecastInfo *> &forecasts = weatherData.forecasts;
0432 
0433         // Flush out the old forecasts when updating.
0434         forecasts.clear();
0435 
0436         WeatherData::ForecastInfo *forecast = new WeatherData::ForecastInfo;
0437 
0438         int dayNumber = 0;
0439 
0440         for (const QVariant &day : daysList) {
0441             QMap dayMap = day.toMap();
0442             QString period = dayMap[QStringLiteral("dayDate")].toString();
0443             QString cond = dayMap[QStringLiteral("icon")].toString();
0444 
0445             forecast->period = QDateTime::fromString(period, QStringLiteral("yyyy-MM-dd"));
0446             forecast->tempHigh = parseNumber(dayMap[QStringLiteral("temperatureMax")]);
0447             forecast->tempLow = parseNumber(dayMap[QStringLiteral("temperatureMin")]);
0448             forecast->precipitation = dayMap[QStringLiteral("precipitation")].toInt();
0449             forecast->iconName = getWeatherIcon(dayIcons(), cond);
0450             ;
0451 
0452             if (dayNumber == 0) {
0453                 // These alternative measurements are used, when the stations doesn't have it's own measurements, uses forecast data from the current day
0454                 weatherData.windSpeedAlt = parseNumber(dayMap[QStringLiteral("windSpeed")]);
0455                 weatherData.gustSpeedAlt = parseNumber(dayMap[QStringLiteral("windGust")]);
0456                 QString windDirection = roundWindDirections(dayMap[QStringLiteral("windDirection")].toInt());
0457                 weatherData.windDirectionAlt = getWindDirectionIcon(windIcons(), windDirection);
0458             }
0459 
0460             forecasts.append(forecast);
0461             forecast = new WeatherData::ForecastInfo;
0462 
0463             dayNumber++;
0464             // Only get the next 7 days (including today)
0465             if (dayNumber == 7)
0466                 break;
0467         }
0468 
0469         delete forecast;
0470 
0471         // Warnings data
0472         QVariantList warningData = weatherMap[QStringLiteral("warnings")].toList();
0473 
0474         QList<WeatherData::WarningInfo *> &warningList = weatherData.warnings;
0475 
0476         // Flush out the old forecasts when updating.
0477         warningList.clear();
0478 
0479         WeatherData::WarningInfo *warning = new WeatherData::WarningInfo;
0480 
0481         for (const QVariant &warningElement : warningData) {
0482             QMap warningMap = warningElement.toMap();
0483 
0484             warning->headline = warningMap[QStringLiteral("headline")].toString();
0485             warning->description = warningMap[QStringLiteral("description")].toString();
0486             warning->priority = warningMap[QStringLiteral("level")].toInt();
0487             warning->type = warningMap[QStringLiteral("event")].toString();
0488             warning->timestamp = QDateTime::fromMSecsSinceEpoch(warningMap[QStringLiteral("start")].toLongLong());
0489 
0490             warningList.append(warning);
0491             warning = new WeatherData::WarningInfo;
0492         }
0493 
0494         delete warning;
0495 
0496         weatherData.isForecastsDataPending = false;
0497 
0498         updateWeather(source);
0499     }
0500 }
0501 
0502 void DWDIon::parseMeasureData(const QString source, QJsonDocument doc)
0503 {
0504     WeatherData &weatherData = m_weatherData[source];
0505     QVariantMap weatherMap = doc.object().toVariantMap();
0506 
0507     if (!weatherMap.isEmpty()) {
0508         QDateTime time = QDateTime::fromMSecsSinceEpoch(weatherMap[QStringLiteral("time")].toLongLong());
0509         weatherData.observationDateTime = time;
0510 
0511         QString condIconNumber = weatherMap[QStringLiteral("icon")].toString();
0512         if (condIconNumber != QLatin1String("")) {
0513             weatherData.conditionIcon = getWeatherIcon(dayIcons(), condIconNumber);
0514         }
0515 
0516         bool windIconValid = false;
0517         const int windDirection = weatherMap[QStringLiteral("winddirection")].toInt(&windIconValid);
0518         if (windIconValid) {
0519             weatherData.windDirection = getWindDirectionIcon(windIcons(), roundWindDirections(windDirection));
0520         }
0521 
0522         weatherData.temperature = parseNumber(weatherMap[QStringLiteral("temperature")]);
0523         weatherData.humidity = parseNumber(weatherMap[QStringLiteral("humidity")]);
0524         weatherData.pressure = parseNumber(weatherMap[QStringLiteral("pressure")]);
0525         weatherData.windSpeed = parseNumber(weatherMap[QStringLiteral("meanwind")]);
0526         weatherData.gustSpeed = parseNumber(weatherMap[QStringLiteral("maxwind")]);
0527         weatherData.dewpoint = parseNumber(weatherMap[QStringLiteral("dewpoint")]);
0528     }
0529 
0530     weatherData.isMeasureDataPending = false;
0531 
0532     updateWeather(source);
0533 }
0534 
0535 void DWDIon::validate(const QString &searchText)
0536 {
0537     const QString source(QStringLiteral("dwd|validate|") + searchText);
0538 
0539     if (m_locations.isEmpty()) {
0540         const QString invalidPlace = searchText;
0541         setData(source, QStringLiteral("validate"), QVariant(QStringLiteral("dwd|invalid|multiple|") + invalidPlace));
0542         return;
0543     }
0544 
0545     QString placeList;
0546     for (const QString &place : std::as_const(m_locations)) {
0547         placeList.append(QStringLiteral("|place|") + place + QStringLiteral("|extra|") + m_place[place]);
0548     }
0549     if (m_locations.count() > 1) {
0550         setData(source, QStringLiteral("validate"), QVariant(QStringLiteral("dwd|valid|multiple") + placeList));
0551     } else {
0552         placeList[7] = placeList[7].toUpper();
0553         setData(source, QStringLiteral("validate"), QVariant(QStringLiteral("dwd|valid|single") + placeList));
0554     }
0555     m_locations.clear();
0556 }
0557 
0558 void DWDIon::updateWeather(const QString &source)
0559 {
0560     const WeatherData &weatherData = m_weatherData[source];
0561 
0562     if (weatherData.isForecastsDataPending || weatherData.isMeasureDataPending) {
0563         return;
0564     }
0565 
0566     QString placeCode = m_place[source];
0567     QString weatherSource = QStringLiteral("dwd|weather|%1|%2").arg(source, placeCode);
0568 
0569     Plasma5Support::DataEngine::Data data;
0570 
0571     data.insert(QStringLiteral("Place"), source);
0572     data.insert(QStringLiteral("Station"), source);
0573 
0574     data.insert(QStringLiteral("Temperature Unit"), KUnitConversion::Celsius);
0575     data.insert(QStringLiteral("Wind Speed Unit"), KUnitConversion::KilometerPerHour);
0576     data.insert(QStringLiteral("Humidity Unit"), KUnitConversion::Percent);
0577     data.insert(QStringLiteral("Pressure Unit"), KUnitConversion::Hectopascal);
0578 
0579     if (!weatherData.observationDateTime.isNull())
0580         data.insert(QStringLiteral("Observation Timestamp"), weatherData.observationDateTime);
0581     else
0582         data.insert(QStringLiteral("Observation Timestamp"), QDateTime::currentDateTime());
0583 
0584     if (!weatherData.conditionIcon.isEmpty())
0585         data.insert(QStringLiteral("Condition Icon"), weatherData.conditionIcon);
0586 
0587     if (!qIsNaN(weatherData.temperature))
0588         data.insert(QStringLiteral("Temperature"), weatherData.temperature);
0589 
0590     if (!qIsNaN(weatherData.humidity))
0591         data.insert(QStringLiteral("Humidity"), weatherData.humidity);
0592 
0593     if (!qIsNaN(weatherData.pressure))
0594         data.insert(QStringLiteral("Pressure"), weatherData.pressure);
0595 
0596     if (!qIsNaN(weatherData.dewpoint))
0597         data.insert(QStringLiteral("Dewpoint"), weatherData.dewpoint);
0598 
0599     if (!qIsNaN(weatherData.windSpeed))
0600         data.insert(QStringLiteral("Wind Speed"), weatherData.windSpeed);
0601     else
0602         data.insert(QStringLiteral("Wind Speed"), weatherData.windSpeedAlt);
0603 
0604     if (!qIsNaN(weatherData.gustSpeed))
0605         data.insert(QStringLiteral("Wind Gust Speed"), weatherData.gustSpeed);
0606     else
0607         data.insert(QStringLiteral("Wind Gust Speed"), weatherData.gustSpeedAlt);
0608 
0609     if (!weatherData.windDirection.isEmpty()) {
0610         data.insert(QStringLiteral("Wind Direction"), weatherData.windDirection);
0611     } else {
0612         data.insert(QStringLiteral("Wind Direction"), weatherData.windDirectionAlt);
0613     }
0614 
0615     int dayNumber = 0;
0616     for (const WeatherData::ForecastInfo *dayForecast : weatherData.forecasts) {
0617         QString period;
0618         if (dayNumber == 0) {
0619             period = i18nc("Short for Today", "Today");
0620         } else {
0621             period = dayForecast->period.toString(QStringLiteral("dddd"));
0622 
0623             period.replace(QStringLiteral("Saturday"), i18nc("Short for Saturday", "Sat"));
0624             period.replace(QStringLiteral("Sunday"), i18nc("Short for Sunday", "Sun"));
0625             period.replace(QStringLiteral("Monday"), i18nc("Short for Monday", "Mon"));
0626             period.replace(QStringLiteral("Tuesday"), i18nc("Short for Tuesday", "Tue"));
0627             period.replace(QStringLiteral("Wednesday"), i18nc("Short for Wednesday", "Wed"));
0628             period.replace(QStringLiteral("Thursday"), i18nc("Short for Thursday", "Thu"));
0629             period.replace(QStringLiteral("Friday"), i18nc("Short for Friday", "Fri"));
0630         }
0631 
0632         data.insert(QStringLiteral("Short Forecast Day %1").arg(dayNumber),
0633                     QStringLiteral("%1|%2|%3|%4|%5|%6")
0634                         .arg(period, dayForecast->iconName, QLatin1String(""))
0635                         .arg(dayForecast->tempHigh)
0636                         .arg(dayForecast->tempLow)
0637                         .arg(dayForecast->precipitation));
0638         dayNumber++;
0639     }
0640 
0641     int k = 0;
0642 
0643     for (const WeatherData::WarningInfo *warning : weatherData.warnings) {
0644         const QString number = QString::number(k);
0645 
0646         data.insert(QStringLiteral("Warning Priority ") + number, warning->priority);
0647         data.insert(QStringLiteral("Warning Description ") + number, QStringLiteral("<p><b>%1</b></p>%2").arg(warning->headline, warning->description));
0648         data.insert(QStringLiteral("Warning Timestamp ") + number, warning->timestamp.toString(QStringLiteral("dd.MM.yyyy")));
0649 
0650         ++k;
0651     }
0652 
0653     data.insert(QStringLiteral("Total Weather Days"), weatherData.forecasts.size());
0654     data.insert(QStringLiteral("Total Warnings Issued"), weatherData.warnings.size());
0655     data.insert(QStringLiteral("Credit"), i18nc("credit line, don't change name!", "Source: Deutscher Wetterdienst"));
0656     data.insert(QStringLiteral("Credit Url"), QStringLiteral("https://www.dwd.de/"));
0657 
0658     setData(weatherSource, data);
0659 }
0660 
0661 /*
0662  * Helper methods
0663  */
0664 float DWDIon::parseNumber(QVariant number)
0665 {
0666     bool isValid = false;
0667     const int intValue = number.toInt(&isValid);
0668     if (!isValid) {
0669         return NAN;
0670     }
0671     if (intValue == 0x7fff) { // DWD uses 32767 to mark an error value
0672         return NAN;
0673     }
0674     // e.g. DWD API int 17 equals 1.7
0675     return static_cast<float>(intValue) / 10;
0676 }
0677 
0678 QString DWDIon::roundWindDirections(int windDirection)
0679 {
0680     QString roundedWindDirection = QString::number(qRound(((float)windDirection) / 100) * 10);
0681     return roundedWindDirection;
0682 }
0683 
0684 QString DWDIon::extractString(QByteArray array, int start, int length)
0685 {
0686     QString string;
0687 
0688     for (int i = start; i < start + length; i++) {
0689         string.append(QLatin1Char(array[i]));
0690     }
0691 
0692     return string;
0693 }
0694 
0695 QString DWDIon::camelCaseString(const QString text)
0696 {
0697     QString result;
0698     bool nextBig = true;
0699 
0700     for (QChar c : text) {
0701         if (c.isLetter()) {
0702             if (nextBig) {
0703                 result.append(c.toUpper());
0704                 nextBig = false;
0705             } else {
0706                 result.append(c.toLower());
0707             }
0708         } else {
0709             if (c == QChar::Space || c == QLatin1Char('-')) {
0710                 nextBig = true;
0711             }
0712             result.append(c);
0713         }
0714     }
0715 
0716     return result;
0717 }
0718 
0719 K_PLUGIN_CLASS_WITH_JSON(DWDIon, "ion-dwd.json")
0720 
0721 #include "ion_dwd.moc"