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

0001 /*
0002     SPDX-FileCopyrightText: 2007-2009 Shawn Starr <shawn.starr@rogers.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 /* Ion for BBC's Weather from the UK Met Office */
0008 
0009 #include "ion_bbcukmet.h"
0010 
0011 #include "ion_bbcukmetdebug.h"
0012 
0013 #include <KIO/TransferJob>
0014 #include <KLocalizedString>
0015 #include <KUnitConversion/Converter>
0016 
0017 #include <QJsonArray>
0018 #include <QJsonDocument>
0019 #include <QJsonObject>
0020 #include <QRegularExpression>
0021 #include <QTimeZone>
0022 #include <QXmlStreamReader>
0023 
0024 WeatherData::WeatherData()
0025     : stationLatitude(qQNaN())
0026     , stationLongitude(qQNaN())
0027     , condition()
0028     , temperature_C(qQNaN())
0029     , windSpeed_miles(qQNaN())
0030     , humidity(qQNaN())
0031     , pressure(qQNaN())
0032 {
0033 }
0034 
0035 WeatherData::ForecastInfo::ForecastInfo()
0036     : tempHigh(qQNaN())
0037     , tempLow(qQNaN())
0038     , windSpeed(qQNaN())
0039 {
0040 }
0041 
0042 // ctor, dtor
0043 UKMETIon::UKMETIon(QObject *parent)
0044     : IonInterface(parent)
0045 
0046 {
0047     setInitialized(true);
0048 }
0049 
0050 UKMETIon::~UKMETIon()
0051 {
0052     deleteForecasts();
0053 }
0054 
0055 void UKMETIon::reset()
0056 {
0057     deleteForecasts();
0058     m_sourcesToReset = sources();
0059     updateAllSources();
0060 }
0061 
0062 void UKMETIon::deleteForecasts()
0063 {
0064     // Destroy each forecast stored in a QList
0065     QHash<QString, WeatherData>::iterator it = m_weatherData.begin(), end = m_weatherData.end();
0066     for (; it != end; ++it) {
0067         qDeleteAll(it.value().forecasts);
0068         it.value().forecasts.clear();
0069     }
0070 }
0071 
0072 QMap<QString, IonInterface::ConditionIcons> UKMETIon::setupDayIconMappings() const
0073 {
0074     //    ClearDay, FewCloudsDay, PartlyCloudyDay, Overcast,
0075     //    Showers, ScatteredShowers, Thunderstorm, Snow,
0076     //    FewCloudsNight, PartlyCloudyNight, ClearNight,
0077     //    Mist, NotAvailable
0078 
0079     return QMap<QString, ConditionIcons>{
0080         {QStringLiteral("sunny"), ClearDay},
0081         //    { QStringLiteral("sunny night"), ClearNight },
0082         {QStringLiteral("clear"), ClearDay},
0083         {QStringLiteral("clear sky"), ClearDay},
0084         {QStringLiteral("sunny intervals"), PartlyCloudyDay},
0085         //    { QStringLiteral("sunny intervals night"), ClearNight },
0086         {QStringLiteral("light cloud"), PartlyCloudyDay},
0087         {QStringLiteral("partly cloudy"), PartlyCloudyDay},
0088         {QStringLiteral("cloudy"), PartlyCloudyDay},
0089         {QStringLiteral("white cloud"), PartlyCloudyDay},
0090         {QStringLiteral("grey cloud"), Overcast},
0091         {QStringLiteral("thick cloud"), Overcast},
0092         //    { QStringLiteral("low level cloud"), NotAvailable },
0093         //    { QStringLiteral("medium level cloud"), NotAvailable },
0094         //    { QStringLiteral("sandstorm"), NotAvailable },
0095         {QStringLiteral("drizzle"), LightRain},
0096         {QStringLiteral("misty"), Mist},
0097         {QStringLiteral("mist"), Mist},
0098         {QStringLiteral("fog"), Mist},
0099         {QStringLiteral("foggy"), Mist},
0100         {QStringLiteral("tropical storm"), Thunderstorm},
0101         {QStringLiteral("hazy"), NotAvailable},
0102         {QStringLiteral("light shower"), Showers},
0103         {QStringLiteral("light rain shower"), Showers},
0104         {QStringLiteral("light rain showers"), Showers},
0105         {QStringLiteral("light showers"), Showers},
0106         {QStringLiteral("light rain"), Showers},
0107         {QStringLiteral("heavy rain"), Rain},
0108         {QStringLiteral("heavy showers"), Rain},
0109         {QStringLiteral("heavy shower"), Rain},
0110         {QStringLiteral("heavy rain shower"), Rain},
0111         {QStringLiteral("heavy rain showers"), Rain},
0112         {QStringLiteral("thundery shower"), Thunderstorm},
0113         {QStringLiteral("thundery showers"), Thunderstorm},
0114         {QStringLiteral("thunderstorm"), Thunderstorm},
0115         {QStringLiteral("cloudy with sleet"), RainSnow},
0116         {QStringLiteral("sleet shower"), RainSnow},
0117         {QStringLiteral("sleet showers"), RainSnow},
0118         {QStringLiteral("sleet"), RainSnow},
0119         {QStringLiteral("cloudy with hail"), Hail},
0120         {QStringLiteral("hail shower"), Hail},
0121         {QStringLiteral("hail showers"), Hail},
0122         {QStringLiteral("hail"), Hail},
0123         {QStringLiteral("light snow"), LightSnow},
0124         {QStringLiteral("light snow shower"), Flurries},
0125         {QStringLiteral("light snow showers"), Flurries},
0126         {QStringLiteral("cloudy with light snow"), LightSnow},
0127         {QStringLiteral("heavy snow"), Snow},
0128         {QStringLiteral("heavy snow shower"), Snow},
0129         {QStringLiteral("heavy snow showers"), Snow},
0130         {QStringLiteral("cloudy with heavy snow"), Snow},
0131         {QStringLiteral("na"), NotAvailable},
0132     };
0133 }
0134 
0135 QMap<QString, IonInterface::ConditionIcons> UKMETIon::setupNightIconMappings() const
0136 {
0137     return QMap<QString, ConditionIcons>{
0138         {QStringLiteral("clear"), ClearNight},
0139         {QStringLiteral("clear sky"), ClearNight},
0140         {QStringLiteral("clear intervals"), PartlyCloudyNight},
0141         {QStringLiteral("sunny intervals"), PartlyCloudyDay}, // it's not really sunny
0142         {QStringLiteral("sunny"), ClearDay},
0143         {QStringLiteral("light cloud"), PartlyCloudyNight},
0144         {QStringLiteral("partly cloudy"), PartlyCloudyNight},
0145         {QStringLiteral("cloudy"), PartlyCloudyNight},
0146         {QStringLiteral("white cloud"), PartlyCloudyNight},
0147         {QStringLiteral("grey cloud"), Overcast},
0148         {QStringLiteral("thick cloud"), Overcast},
0149         {QStringLiteral("drizzle"), LightRain},
0150         {QStringLiteral("misty"), Mist},
0151         {QStringLiteral("mist"), Mist},
0152         {QStringLiteral("fog"), Mist},
0153         {QStringLiteral("foggy"), Mist},
0154         {QStringLiteral("tropical storm"), Thunderstorm},
0155         {QStringLiteral("hazy"), NotAvailable},
0156         {QStringLiteral("light shower"), Showers},
0157         {QStringLiteral("light rain shower"), Showers},
0158         {QStringLiteral("light rain showers"), Showers},
0159         {QStringLiteral("light showers"), Showers},
0160         {QStringLiteral("light rain"), Showers},
0161         {QStringLiteral("heavy rain"), Rain},
0162         {QStringLiteral("heavy showers"), Rain},
0163         {QStringLiteral("heavy shower"), Rain},
0164         {QStringLiteral("heavy rain shower"), Rain},
0165         {QStringLiteral("heavy rain showers"), Rain},
0166         {QStringLiteral("thundery shower"), Thunderstorm},
0167         {QStringLiteral("thundery showers"), Thunderstorm},
0168         {QStringLiteral("thunderstorm"), Thunderstorm},
0169         {QStringLiteral("cloudy with sleet"), RainSnow},
0170         {QStringLiteral("sleet shower"), RainSnow},
0171         {QStringLiteral("sleet showers"), RainSnow},
0172         {QStringLiteral("sleet"), RainSnow},
0173         {QStringLiteral("cloudy with hail"), Hail},
0174         {QStringLiteral("hail shower"), Hail},
0175         {QStringLiteral("hail showers"), Hail},
0176         {QStringLiteral("hail"), Hail},
0177         {QStringLiteral("light snow"), LightSnow},
0178         {QStringLiteral("light snow shower"), Flurries},
0179         {QStringLiteral("light snow showers"), Flurries},
0180         {QStringLiteral("cloudy with light snow"), LightSnow},
0181         {QStringLiteral("heavy snow"), Snow},
0182         {QStringLiteral("heavy snow shower"), Snow},
0183         {QStringLiteral("heavy snow showers"), Snow},
0184         {QStringLiteral("cloudy with heavy snow"), Snow},
0185         {QStringLiteral("na"), NotAvailable},
0186     };
0187 }
0188 
0189 QMap<QString, IonInterface::WindDirections> UKMETIon::setupWindIconMappings() const
0190 {
0191     return QMap<QString, WindDirections>{
0192         {QStringLiteral("northerly"), N},
0193         {QStringLiteral("north north easterly"), NNE},
0194         {QStringLiteral("north easterly"), NE},
0195         {QStringLiteral("east north easterly"), ENE},
0196         {QStringLiteral("easterly"), E},
0197         {QStringLiteral("east south easterly"), ESE},
0198         {QStringLiteral("south easterly"), SE},
0199         {QStringLiteral("south south easterly"), SSE},
0200         {QStringLiteral("southerly"), S},
0201         {QStringLiteral("south south westerly"), SSW},
0202         {QStringLiteral("south westerly"), SW},
0203         {QStringLiteral("west south westerly"), WSW},
0204         {QStringLiteral("westerly"), W},
0205         {QStringLiteral("west north westerly"), WNW},
0206         {QStringLiteral("north westerly"), NW},
0207         {QStringLiteral("north north westerly"), NNW},
0208         {QStringLiteral("calm"), VR},
0209     };
0210 }
0211 
0212 QMap<QString, IonInterface::ConditionIcons> const &UKMETIon::dayIcons() const
0213 {
0214     static QMap<QString, ConditionIcons> const dval = setupDayIconMappings();
0215     return dval;
0216 }
0217 
0218 QMap<QString, IonInterface::ConditionIcons> const &UKMETIon::nightIcons() const
0219 {
0220     static QMap<QString, ConditionIcons> const nval = setupNightIconMappings();
0221     return nval;
0222 }
0223 
0224 QMap<QString, IonInterface::WindDirections> const &UKMETIon::windIcons() const
0225 {
0226     static QMap<QString, WindDirections> const wval = setupWindIconMappings();
0227     return wval;
0228 }
0229 
0230 // Get a specific Ion's data
0231 bool UKMETIon::updateIonSource(const QString &source)
0232 {
0233     // We expect the applet to send the source in the following tokenization:
0234     // ionname|validate|place_name - Triggers validation of place
0235     // ionname|weather|place_name - Triggers receiving weather of place
0236 
0237     const QStringList sourceAction = source.split(QLatin1Char('|'));
0238 
0239     // Guard: if the size of array is not 3 then we have bad data, return an error
0240     if (sourceAction.size() < 3) {
0241         setData(source, QStringLiteral("validate"), QStringLiteral("bbcukmet|malformed"));
0242         return true;
0243     }
0244 
0245     if (sourceAction[1] == QLatin1String("validate") && sourceAction.size() >= 3) {
0246         // Look for places to match
0247         findPlace(sourceAction[2], source);
0248         return true;
0249     }
0250 
0251     if (sourceAction[1] == QLatin1String("weather") && sourceAction.size() >= 3) {
0252         if (sourceAction.count() >= 3) {
0253             if (sourceAction[2].isEmpty()) {
0254                 setData(source, QStringLiteral("validate"), QStringLiteral("bbcukmet|malformed"));
0255                 return true;
0256             }
0257 
0258             XMLMapInfo &place = m_place[QLatin1String("bbcukmet|") + sourceAction[2]];
0259 
0260             // backward compatibility after rss feed url change in 2018/03
0261             place.sourceExtraArg = sourceAction[3];
0262             if (place.sourceExtraArg.startsWith(QLatin1String("http://open.live.bbc.co.uk/"))) {
0263                 // Old data source id stored the full (now outdated) observation feed url
0264                 // http://open.live.bbc.co.uk/weather/feeds/en/STATIOID/observations.rss
0265                 // as extra argument, so extract the id from that
0266                 place.stationId = place.sourceExtraArg.section(QLatin1Char('/'), -2, -2);
0267             } else {
0268                 place.stationId = place.sourceExtraArg;
0269             }
0270             getXMLData(sourceAction[0] + QLatin1Char('|') + sourceAction[2]);
0271             return true;
0272         }
0273         return false;
0274     }
0275 
0276     setData(source, QStringLiteral("validate"), QStringLiteral("bbcukmet|malformed"));
0277     return true;
0278 }
0279 
0280 // Gets specific city XML data
0281 void UKMETIon::getXMLData(const QString &source)
0282 {
0283     for (const QString &fetching : std::as_const(m_obsJobList)) {
0284         if (fetching == source) {
0285             // already getting this source and awaiting the data
0286             return;
0287         }
0288     }
0289 
0290     const QUrl url(QStringLiteral("https://weather-broker-cdn.api.bbci.co.uk/en/observation/rss/") + m_place[source].stationId);
0291 
0292     KIO::TransferJob *getJob = KIO::get(url, KIO::Reload, KIO::HideProgressInfo);
0293     getJob->addMetaData(QStringLiteral("cookies"), QStringLiteral("none")); // Disable displaying cookies
0294     m_obsJobXml.insert(getJob, new QXmlStreamReader);
0295     m_obsJobList.insert(getJob, source);
0296 
0297     connect(getJob, &KIO::TransferJob::data, this, &UKMETIon::observation_slotDataArrived);
0298     connect(getJob, &KJob::result, this, &UKMETIon::observation_slotJobFinished);
0299 }
0300 
0301 // Parses city list and gets the correct city based on ID number
0302 void UKMETIon::findPlace(const QString &place, const QString &source)
0303 {
0304     // the API needs auto=true for partial-text searching
0305     // but unlike the normal query, using auto=true doesn't show locations which match the text but with different unicode
0306     // for example "hyderabad" with no auto matches "Hyderabad" and "Hyderābād"
0307     // but with auto matches only "Hyderabad"
0308     // so we merge the two results
0309     const QUrl url(QLatin1String("https://open.live.bbc.co.uk/locator/locations?s=") + place + QLatin1String("&format=json"));
0310     const QUrl autoUrl(QLatin1String("https://open.live.bbc.co.uk/locator/locations?s=") + place + QLatin1String("&format=json&auto=true"));
0311 
0312     m_normalSearchArrived = false;
0313     m_autoSearchArrived = false;
0314 
0315     KIO::TransferJob *getJob = KIO::get(url, KIO::Reload, KIO::HideProgressInfo);
0316     getJob->addMetaData(QStringLiteral("cookies"), QStringLiteral("none")); // Disable displaying cookies
0317     m_jobHtml.insert(getJob, new QByteArray());
0318     m_jobList.insert(getJob, source);
0319 
0320     connect(getJob, &KIO::TransferJob::data, this, &UKMETIon::setup_slotDataArrived);
0321 
0322     KIO::TransferJob *autoGetJob = KIO::get(autoUrl, KIO::Reload, KIO::HideProgressInfo);
0323     autoGetJob->addMetaData(QStringLiteral("cookies"), QStringLiteral("none")); // Disable displaying cookies
0324     m_jobHtml.insert(autoGetJob, new QByteArray());
0325     m_jobList.insert(autoGetJob, source);
0326 
0327     connect(autoGetJob, &KIO::TransferJob::data, this, &UKMETIon::setup_slotDataArrived);
0328 
0329     connect(getJob, &KJob::result, this, [&](KJob *job) {
0330         setup_slotJobFinished(job, QStringLiteral("normal"));
0331     });
0332     connect(autoGetJob, &KJob::result, this, [&](KJob *job) {
0333         setup_slotJobFinished(job, QStringLiteral("auto"));
0334     });
0335 }
0336 
0337 void UKMETIon::getFiveDayForecast(const QString &source)
0338 {
0339     XMLMapInfo &place = m_place[source];
0340 
0341     const QUrl url(QStringLiteral("https://weather-broker-cdn.api.bbci.co.uk/en/forecast/rss/3day/") + place.stationId);
0342 
0343     KIO::TransferJob *getJob = KIO::get(url, KIO::Reload, KIO::HideProgressInfo);
0344     getJob->addMetaData(QStringLiteral("cookies"), QStringLiteral("none")); // Disable displaying cookies
0345     m_forecastJobXml.insert(getJob, new QXmlStreamReader);
0346     m_forecastJobList.insert(getJob, source);
0347 
0348     connect(getJob, &KIO::TransferJob::data, this, &UKMETIon::forecast_slotDataArrived);
0349     connect(getJob, &KJob::result, this, &UKMETIon::forecast_slotJobFinished);
0350 }
0351 
0352 void UKMETIon::readSearchHTMLData(const QString &source, const QList<QByteArray *> htmls)
0353 {
0354     int counter = 2;
0355 
0356     for (const QByteArray *html : htmls) {
0357         if (!html) {
0358             continue;
0359         }
0360 
0361         QJsonObject jsonDocumentObject = QJsonDocument::fromJson(*html).object().value(QStringLiteral("response")).toObject();
0362 
0363         if (!jsonDocumentObject.isEmpty()) {
0364             QJsonValue resultsVariant = jsonDocumentObject.value(QStringLiteral("locations"));
0365 
0366             if (resultsVariant.isUndefined()) {
0367                 // this is a response from an auto=true query
0368                 resultsVariant = jsonDocumentObject.value(QStringLiteral("results")).toObject().value(QStringLiteral("results"));
0369             }
0370 
0371             const QJsonArray results = resultsVariant.toArray();
0372 
0373             for (const QJsonValue &resultValue : results) {
0374                 QJsonObject result = resultValue.toObject();
0375                 const QString id = result.value(QStringLiteral("id")).toString();
0376                 const QString name = result.value(QStringLiteral("name")).toString();
0377                 const QString area = result.value(QStringLiteral("container")).toString();
0378                 const QString country = result.value(QStringLiteral("country")).toString();
0379 
0380                 if (!id.isEmpty() && !name.isEmpty() && !area.isEmpty() && !country.isEmpty()) {
0381                     const QString fullName = name + QLatin1String(", ") + area + QLatin1String(", ") + country;
0382                     QString tmp = QLatin1String("bbcukmet|") + fullName;
0383 
0384                     // Duplicate places can exist, show them too
0385                     // but not if they have the exact same id, which can happen since we're merging two results
0386                     if (m_locations.contains(tmp) && m_place[tmp].stationId != id) {
0387                         tmp += QLatin1String(" (#") + QString::number(counter) + QLatin1Char(')');
0388                         counter++;
0389                     }
0390                     XMLMapInfo &place = m_place[tmp];
0391                     place.stationId = id;
0392                     place.place = fullName;
0393                     m_locations.append(tmp);
0394                 }
0395             }
0396         }
0397     }
0398 
0399     validate(source);
0400 }
0401 
0402 // handle when no XML tag is found
0403 void UKMETIon::parseUnknownElement(QXmlStreamReader &xml) const
0404 {
0405     while (!xml.atEnd()) {
0406         xml.readNext();
0407 
0408         if (xml.isEndElement()) {
0409             break;
0410         }
0411 
0412         if (xml.isStartElement()) {
0413             parseUnknownElement(xml);
0414         }
0415     }
0416 }
0417 
0418 void UKMETIon::setup_slotDataArrived(KIO::Job *job, const QByteArray &data)
0419 {
0420     if (data.isEmpty() || !m_jobHtml.contains(job)) {
0421         return;
0422     }
0423 
0424     m_jobHtml[job]->append(data);
0425 }
0426 
0427 void UKMETIon::setup_slotJobFinished(KJob *job, const QString &type)
0428 {
0429     if (job->error() == KIO::ERR_SERVER_TIMEOUT) {
0430         setData(m_jobList[job], QStringLiteral("validate"), QStringLiteral("bbcukmet|timeout"));
0431         disconnectSource(m_jobList[job], this);
0432         m_jobList.remove(job);
0433         delete m_jobHtml[job];
0434         m_jobHtml.remove(job);
0435         return;
0436     }
0437 
0438     if (type == QStringLiteral("normal")) {
0439         m_normalSearchArrived = true;
0440     }
0441     if (type == QStringLiteral("auto")) {
0442         m_autoSearchArrived = true;
0443     }
0444     if (!(m_normalSearchArrived && m_autoSearchArrived)) {
0445         return;
0446     }
0447 
0448     // If Redirected, don't go to this routine
0449     if (!m_locations.contains(QLatin1String("bbcukmet|") + m_jobList[job])) {
0450         readSearchHTMLData(m_jobList[job] /* source is same for both */, m_jobHtml.values());
0451     }
0452 
0453     m_jobList.clear();
0454     for (auto html : m_jobHtml.values()) {
0455         delete html;
0456     }
0457     m_jobHtml.clear();
0458 }
0459 
0460 void UKMETIon::observation_slotDataArrived(KIO::Job *job, const QByteArray &data)
0461 {
0462     QByteArray local = data;
0463     if (data.isEmpty() || !m_obsJobXml.contains(job)) {
0464         return;
0465     }
0466 
0467     // Send to xml.
0468     m_obsJobXml[job]->addData(local);
0469 }
0470 
0471 void UKMETIon::observation_slotJobFinished(KJob *job)
0472 {
0473     const QString source = m_obsJobList.value(job);
0474     setData(source, Data());
0475 
0476     QXmlStreamReader *reader = m_obsJobXml.value(job);
0477     if (reader) {
0478         readObservationXMLData(m_obsJobList[job], *reader);
0479     }
0480 
0481     m_obsJobList.remove(job);
0482     delete m_obsJobXml[job];
0483     m_obsJobXml.remove(job);
0484 
0485     if (m_sourcesToReset.contains(source)) {
0486         m_sourcesToReset.removeAll(source);
0487         Q_EMIT forceUpdate(this, source);
0488     }
0489 }
0490 
0491 void UKMETIon::forecast_slotDataArrived(KIO::Job *job, const QByteArray &data)
0492 {
0493     QByteArray local = data;
0494     if (data.isEmpty() || !m_forecastJobXml.contains(job)) {
0495         return;
0496     }
0497 
0498     // Send to xml.
0499     m_forecastJobXml[job]->addData(local);
0500 }
0501 
0502 void UKMETIon::forecast_slotJobFinished(KJob *job)
0503 {
0504     setData(m_forecastJobList[job], Data());
0505     QXmlStreamReader *reader = m_forecastJobXml.value(job);
0506     if (reader) {
0507         readFiveDayForecastXMLData(m_forecastJobList[job], *reader);
0508     }
0509 
0510     m_forecastJobList.remove(job);
0511     delete m_forecastJobXml[job];
0512     m_forecastJobXml.remove(job);
0513 }
0514 
0515 void UKMETIon::parsePlaceObservation(const QString &source, WeatherData &data, QXmlStreamReader &xml)
0516 {
0517     Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("rss"));
0518 
0519     while (!xml.atEnd()) {
0520         xml.readNext();
0521 
0522         const auto elementName = xml.name();
0523 
0524         if (xml.isEndElement() && elementName == QLatin1String("rss")) {
0525             break;
0526         }
0527 
0528         if (xml.isStartElement() && elementName == QLatin1String("channel")) {
0529             parseWeatherChannel(source, data, xml);
0530         }
0531     }
0532 }
0533 
0534 void UKMETIon::parsePlaceForecast(const QString &source, QXmlStreamReader &xml)
0535 {
0536     Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("rss"));
0537 
0538     while (!xml.atEnd()) {
0539         xml.readNext();
0540 
0541         if (xml.isStartElement() && xml.name() == QLatin1String("channel")) {
0542             parseWeatherForecast(source, xml);
0543         }
0544     }
0545 }
0546 
0547 void UKMETIon::parseWeatherChannel(const QString &source, WeatherData &data, QXmlStreamReader &xml)
0548 {
0549     Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("channel"));
0550 
0551     while (!xml.atEnd()) {
0552         xml.readNext();
0553 
0554         const auto elementName = xml.name();
0555 
0556         if (xml.isEndElement() && elementName == QLatin1String("channel")) {
0557             break;
0558         }
0559 
0560         if (xml.isStartElement()) {
0561             if (elementName == QLatin1String("title")) {
0562                 data.stationName = xml.readElementText().section(QStringLiteral("Observations for"), 1, 1).trimmed();
0563                 data.stationName.replace(QStringLiteral("United Kingdom"), i18n("UK"));
0564                 data.stationName.replace(QStringLiteral("United States of America"), i18n("USA"));
0565 
0566             } else if (elementName == QLatin1String("item")) {
0567                 parseWeatherObservation(source, data, xml);
0568             } else {
0569                 parseUnknownElement(xml);
0570             }
0571         }
0572     }
0573 }
0574 
0575 void UKMETIon::parseWeatherForecast(const QString &source, QXmlStreamReader &xml)
0576 {
0577     Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("channel"));
0578 
0579     while (!xml.atEnd()) {
0580         xml.readNext();
0581 
0582         const auto elementName = xml.name();
0583 
0584         if (xml.isEndElement() && elementName == QLatin1String("channel")) {
0585             break;
0586         }
0587 
0588         if (xml.isStartElement()) {
0589             if (elementName == QLatin1String("item")) {
0590                 parseFiveDayForecast(source, xml);
0591             } else if (elementName == QLatin1String("link") && xml.namespaceUri().isEmpty()) {
0592                 m_place[source].forecastHTMLUrl = xml.readElementText();
0593             } else {
0594                 parseUnknownElement(xml);
0595             }
0596         }
0597     }
0598 }
0599 
0600 void UKMETIon::parseWeatherObservation(const QString &source, WeatherData &data, QXmlStreamReader &xml)
0601 {
0602     Q_UNUSED(source);
0603 
0604     Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("item"));
0605 
0606     while (!xml.atEnd()) {
0607         xml.readNext();
0608 
0609         const auto elementName = xml.name();
0610 
0611         if (xml.isEndElement() && elementName == QLatin1String("item")) {
0612             break;
0613         }
0614 
0615         if (xml.isStartElement()) {
0616             if (elementName == QLatin1String("title")) {
0617                 QString conditionString = xml.readElementText();
0618 
0619                 // Get the observation time and condition
0620                 int splitIndex = conditionString.lastIndexOf(QLatin1Char(':'));
0621                 if (splitIndex >= 0) {
0622                     QString conditionData = conditionString.mid(splitIndex + 1); // Skip ':'
0623                     data.obsTime = conditionString.left(splitIndex);
0624 
0625                     if (data.obsTime.contains(QLatin1Char('-'))) {
0626                         // Saturday - 13:00 CET
0627                         // Saturday - 12:00 GMT
0628                         // timezone parsing is not yet supported by QDateTime, also is there just a dayname
0629                         // so try manually
0630                         // guess date from day
0631                         const QString dayString = data.obsTime.section(QLatin1Char('-'), 0, 0).trimmed();
0632                         QDate date = QDate::currentDate();
0633                         const QString dayFormat = QStringLiteral("dddd");
0634                         const int testDayJumps[4] = {
0635                             -1, // first to weekday yesterday
0636                             2, // then to weekday tomorrow
0637                             -3, // then to weekday before yesterday, not sure if such day offset can happen?
0638                             4, // then to weekday after tomorrow, not sure if such day offset can happen?
0639                         };
0640                         const int dayJumps = sizeof(testDayJumps) / sizeof(testDayJumps[0]);
0641                         QLocale cLocale = QLocale::c();
0642                         int dayJump = 0;
0643                         while (true) {
0644                             if (cLocale.toString(date, dayFormat) == dayString) {
0645                                 break;
0646                             }
0647 
0648                             if (dayJump >= dayJumps) {
0649                                 // no weekday found near-by, set date invalid
0650                                 date = QDate();
0651                                 break;
0652                             }
0653                             date = date.addDays(testDayJumps[dayJump]);
0654                             ++dayJump;
0655                         }
0656 
0657                         if (date.isValid()) {
0658                             const QString timeString = data.obsTime.section(QLatin1Char('-'), 1, 1).trimmed();
0659                             const QTime time = QTime::fromString(timeString.section(QLatin1Char(' '), 0, 0), QStringLiteral("hh:mm"));
0660                             const QTimeZone timeZone = QTimeZone(timeString.section(QLatin1Char(' '), 1, 1).toUtf8());
0661                             // TODO: if non-IANA timezone id is not known, try to guess timezone from other data
0662 
0663                             if (time.isValid() && timeZone.isValid()) {
0664                                 data.observationDateTime = QDateTime(date, time, timeZone);
0665                             }
0666                         }
0667                     }
0668 
0669                     if (conditionData.contains(QLatin1Char(','))) {
0670                         data.condition = conditionData.section(QLatin1Char(','), 0, 0).trimmed();
0671 
0672                         if (data.condition == QLatin1String("null") || data.condition == QLatin1String("Not Available")) {
0673                             data.condition.clear();
0674                         }
0675                     }
0676                 }
0677 
0678             } else if (elementName == QLatin1String("description")) {
0679                 QString observeString = xml.readElementText();
0680                 const QStringList observeData = observeString.split(QLatin1Char(':'));
0681 
0682                 // FIXME: We should make this use a QRegExp but I need some help here :) -spstarr
0683 
0684                 QString temperature_C = observeData[1].section(QChar(176), 0, 0).trimmed();
0685                 parseFloat(data.temperature_C, temperature_C);
0686 
0687                 data.windDirection = observeData[2].section(QLatin1Char(','), 0, 0).trimmed();
0688                 if (data.windDirection.contains(QLatin1String("null"))) {
0689                     data.windDirection.clear();
0690                 }
0691 
0692                 QString windSpeed_miles = observeData[3].section(QLatin1Char(','), 0, 0).section(QLatin1Char(' '), 1, 1).remove(QStringLiteral("mph"));
0693                 parseFloat(data.windSpeed_miles, windSpeed_miles);
0694 
0695                 QString humidity = observeData[4].section(QLatin1Char(','), 0, 0).section(QLatin1Char(' '), 1, 1);
0696                 if (humidity.endsWith(QLatin1Char('%'))) {
0697                     humidity.chop(1);
0698                 }
0699                 parseFloat(data.humidity, humidity);
0700 
0701                 QString pressure = observeData[5].section(QLatin1Char(','), 0, 0).section(QLatin1Char(' '), 1, 1).section(QStringLiteral("mb"), 0, 0);
0702                 parseFloat(data.pressure, pressure);
0703 
0704                 data.pressureTendency = observeData[5].section(QLatin1Char(','), 1, 1).toLower().trimmed();
0705                 if (data.pressureTendency == QLatin1String("no change")) {
0706                     data.pressureTendency = QStringLiteral("steady");
0707                 }
0708 
0709                 data.visibilityStr = observeData[6].trimmed();
0710                 if (data.visibilityStr == QLatin1String("--")) {
0711                     data.visibilityStr.clear();
0712                 }
0713 
0714             } else if (elementName == QLatin1String("lat")) {
0715                 const QString ordinate = xml.readElementText();
0716                 data.stationLatitude = ordinate.toDouble();
0717             } else if (elementName == QLatin1String("long")) {
0718                 const QString ordinate = xml.readElementText();
0719                 data.stationLongitude = ordinate.toDouble();
0720             } else if (elementName == QLatin1String("point") && xml.namespaceUri() == QLatin1String("http://www.georss.org/georss")) {
0721                 const QStringList ordinates = xml.readElementText().split(QLatin1Char(' '));
0722                 data.stationLatitude = ordinates[0].toDouble();
0723                 data.stationLongitude = ordinates[1].toDouble();
0724             } else {
0725                 parseUnknownElement(xml);
0726             }
0727         }
0728     }
0729 }
0730 
0731 bool UKMETIon::readObservationXMLData(const QString &source, QXmlStreamReader &xml)
0732 {
0733     WeatherData data;
0734     data.isForecastsDataPending = true;
0735     bool haveObservation = false;
0736     while (!xml.atEnd()) {
0737         xml.readNext();
0738 
0739         if (xml.isEndElement()) {
0740             break;
0741         }
0742 
0743         if (xml.isStartElement()) {
0744             if (xml.name() == QLatin1String("rss")) {
0745                 parsePlaceObservation(source, data, xml);
0746                 haveObservation = true;
0747             } else {
0748                 parseUnknownElement(xml);
0749             }
0750         }
0751     }
0752 
0753     if (!haveObservation) {
0754         return false;
0755     }
0756 
0757     bool solarDataSourceNeedsConnect = false;
0758     Plasma5Support::DataEngine *timeEngine = dataEngine(QStringLiteral("time"));
0759     if (timeEngine) {
0760         const bool canCalculateElevation = (data.observationDateTime.isValid() && (!qIsNaN(data.stationLatitude) && !qIsNaN(data.stationLongitude)));
0761         if (canCalculateElevation) {
0762             data.solarDataTimeEngineSourceName = QStringLiteral("%1|Solar|Latitude=%2|Longitude=%3|DateTime=%4")
0763                                                      .arg(QString::fromUtf8(data.observationDateTime.timeZone().id()))
0764                                                      .arg(data.stationLatitude)
0765                                                      .arg(data.stationLongitude)
0766                                                      .arg(data.observationDateTime.toString(Qt::ISODate));
0767             solarDataSourceNeedsConnect = true;
0768         }
0769 
0770         // check any previous data
0771         const auto it = m_weatherData.constFind(source);
0772         if (it != m_weatherData.constEnd()) {
0773             const QString &oldSolarDataTimeEngineSource = it.value().solarDataTimeEngineSourceName;
0774 
0775             if (oldSolarDataTimeEngineSource == data.solarDataTimeEngineSourceName) {
0776                 // can reuse elevation source (if any), copy over data
0777                 data.isNight = it.value().isNight;
0778                 solarDataSourceNeedsConnect = false;
0779             } else if (!oldSolarDataTimeEngineSource.isEmpty()) {
0780                 // drop old elevation source
0781                 timeEngine->disconnectSource(oldSolarDataTimeEngineSource, this);
0782             }
0783         }
0784     }
0785 
0786     m_weatherData[source] = data;
0787 
0788     // connect only after m_weatherData has the data, so the instant data push handling can see it
0789     if (solarDataSourceNeedsConnect) {
0790         data.isSolarDataPending = true;
0791         timeEngine->connectSource(data.solarDataTimeEngineSourceName, this);
0792     }
0793 
0794     // Get the 5 day forecast info next.
0795     getFiveDayForecast(source);
0796 
0797     return !xml.error();
0798 }
0799 
0800 bool UKMETIon::readFiveDayForecastXMLData(const QString &source, QXmlStreamReader &xml)
0801 {
0802     bool haveFiveDay = false;
0803     while (!xml.atEnd()) {
0804         xml.readNext();
0805 
0806         if (xml.isEndElement()) {
0807             break;
0808         }
0809 
0810         if (xml.isStartElement()) {
0811             if (xml.name() == QLatin1String("rss")) {
0812                 parsePlaceForecast(source, xml);
0813                 haveFiveDay = true;
0814             } else {
0815                 parseUnknownElement(xml);
0816             }
0817         }
0818     }
0819     if (!haveFiveDay)
0820         return false;
0821     updateWeather(source);
0822     return !xml.error();
0823 }
0824 
0825 void UKMETIon::parseFiveDayForecast(const QString &source, QXmlStreamReader &xml)
0826 {
0827     Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("item"));
0828 
0829     WeatherData &weatherData = m_weatherData[source];
0830     QList<WeatherData::ForecastInfo *> &forecasts = weatherData.forecasts;
0831 
0832     // Flush out the old forecasts when updating.
0833     forecasts.clear();
0834 
0835     WeatherData::ForecastInfo *forecast = new WeatherData::ForecastInfo;
0836     QString line;
0837     QString period;
0838     QString summary;
0839     const QRegularExpression high(QStringLiteral("Maximum Temperature: (-?\\d+).C"), QRegularExpression::CaseInsensitiveOption);
0840     const QRegularExpression low(QStringLiteral("Minimum Temperature: (-?\\d+).C"), QRegularExpression::CaseInsensitiveOption);
0841     while (!xml.atEnd()) {
0842         xml.readNext();
0843         if (xml.name() == QLatin1String("title")) {
0844             line = xml.readElementText().trimmed();
0845 
0846             // FIXME: We should make this all use QRegExps in UKMETIon::parseFiveDayForecast() for forecast -spstarr
0847 
0848             const QString p = line.section(QLatin1Char(','), 0, 0);
0849             period = p.section(QLatin1Char(':'), 0, 0);
0850             summary = p.section(QLatin1Char(':'), 1, 1).trimmed();
0851 
0852             const QString temps = line.section(QLatin1Char(','), 1, 1);
0853             // Sometimes only one of min or max are reported
0854             QRegularExpressionMatch rmatch;
0855             if (temps.contains(high, &rmatch)) {
0856                 parseFloat(forecast->tempHigh, rmatch.captured(1));
0857             }
0858             if (temps.contains(low, &rmatch)) {
0859                 parseFloat(forecast->tempLow, rmatch.captured(1));
0860             }
0861 
0862             const QString summaryLC = summary.toLower();
0863             forecast->period = period;
0864             if (forecast->period == QLatin1String("Tonight")) {
0865                 forecast->iconName = getWeatherIcon(nightIcons(), summaryLC);
0866             } else {
0867                 forecast->iconName = getWeatherIcon(dayIcons(), summaryLC);
0868             }
0869             // db uses original strings normalized to lowercase, but we prefer the unnormalized if without translation
0870             const QString summaryTranslated = i18nc("weather forecast", summaryLC.toUtf8().data());
0871             forecast->summary = (summaryTranslated != summaryLC) ? summaryTranslated : summary;
0872             qCDebug(IONENGINE_BBCUKMET) << "i18n summary string: " << forecast->summary;
0873             forecasts.append(forecast);
0874             // prepare next
0875             forecast = new WeatherData::ForecastInfo;
0876         }
0877     }
0878 
0879     weatherData.isForecastsDataPending = false;
0880 
0881     // remove unused
0882     delete forecast;
0883 }
0884 
0885 void UKMETIon::parseFloat(float &value, const QString &string)
0886 {
0887     bool ok = false;
0888     const float result = string.toFloat(&ok);
0889     if (ok) {
0890         value = result;
0891     }
0892 }
0893 
0894 void UKMETIon::validate(const QString &source)
0895 {
0896     if (m_locations.isEmpty()) {
0897         const QString invalidPlace = source.section(QLatin1Char('|'), 2, 2);
0898         if (m_place[QStringLiteral("bbcukmet|") + invalidPlace].place.isEmpty()) {
0899             setData(source, QStringLiteral("validate"), QVariant(QStringLiteral("bbcukmet|invalid|multiple|") + invalidPlace));
0900         }
0901         return;
0902     }
0903 
0904     QString placeList;
0905     for (const QString &place : std::as_const(m_locations)) {
0906         const QString p = place.section(QLatin1Char('|'), 1, 1);
0907         placeList.append(QStringLiteral("|place|") + p + QStringLiteral("|extra|") + m_place[place].stationId);
0908     }
0909     if (m_locations.count() > 1) {
0910         setData(source, QStringLiteral("validate"), QVariant(QStringLiteral("bbcukmet|valid|multiple") + placeList));
0911     } else {
0912         placeList[7] = placeList[7].toUpper();
0913         setData(source, QStringLiteral("validate"), QVariant(QStringLiteral("bbcukmet|valid|single") + placeList));
0914     }
0915     m_locations.clear();
0916 }
0917 
0918 void UKMETIon::updateWeather(const QString &source)
0919 {
0920     const WeatherData &weatherData = m_weatherData[source];
0921 
0922     if (weatherData.isForecastsDataPending || weatherData.isSolarDataPending) {
0923         return;
0924     }
0925 
0926     const XMLMapInfo &place = m_place[source];
0927 
0928     QString weatherSource = source;
0929     // TODO: why the replacement here instead of just a new string?
0930     weatherSource.replace(QStringLiteral("bbcukmet|"), QStringLiteral("bbcukmet|weather|"));
0931     weatherSource.append(QLatin1Char('|') + place.sourceExtraArg);
0932 
0933     Plasma5Support::DataEngine::Data data;
0934 
0935     // work-around for buggy observation RSS feed missing the station name
0936     QString stationName = weatherData.stationName;
0937     if (stationName.isEmpty() || stationName == QLatin1Char(',')) {
0938         stationName = source.section(QLatin1Char('|'), 1, 1);
0939     }
0940 
0941     data.insert(QStringLiteral("Place"), stationName);
0942     data.insert(QStringLiteral("Station"), stationName);
0943     if (weatherData.observationDateTime.isValid()) {
0944         data.insert(QStringLiteral("Observation Timestamp"), weatherData.observationDateTime);
0945     }
0946     if (!weatherData.obsTime.isEmpty()) {
0947         data.insert(QStringLiteral("Observation Period"), weatherData.obsTime);
0948     }
0949     if (!weatherData.condition.isEmpty()) {
0950         // db uses original strings normalized to lowercase, but we prefer the unnormalized if without translation
0951         const QString conditionLC = weatherData.condition.toLower();
0952         const QString conditionTranslated = i18nc("weather condition", conditionLC.toUtf8().data());
0953         data.insert(QStringLiteral("Current Conditions"), (conditionTranslated != conditionLC) ? conditionTranslated : weatherData.condition);
0954     }
0955     //     qCDebug(IONENGINE_BBCUKMET) << "i18n condition string: " << i18nc("weather condition", weatherData.condition.toUtf8().data());
0956 
0957     const bool stationCoordsValid = (!qIsNaN(weatherData.stationLatitude) && !qIsNaN(weatherData.stationLongitude));
0958 
0959     if (stationCoordsValid) {
0960         data.insert(QStringLiteral("Latitude"), weatherData.stationLatitude);
0961         data.insert(QStringLiteral("Longitude"), weatherData.stationLongitude);
0962     }
0963 
0964     data.insert(QStringLiteral("Condition Icon"), getWeatherIcon(weatherData.isNight ? nightIcons() : dayIcons(), weatherData.condition));
0965 
0966     if (!qIsNaN(weatherData.humidity)) {
0967         data.insert(QStringLiteral("Humidity"), weatherData.humidity);
0968         data.insert(QStringLiteral("Humidity Unit"), KUnitConversion::Percent);
0969     }
0970 
0971     if (!weatherData.visibilityStr.isEmpty()) {
0972         data.insert(QStringLiteral("Visibility"), i18nc("visibility", weatherData.visibilityStr.toUtf8().data()));
0973         data.insert(QStringLiteral("Visibility Unit"), KUnitConversion::NoUnit);
0974     }
0975 
0976     if (!qIsNaN(weatherData.temperature_C)) {
0977         data.insert(QStringLiteral("Temperature"), weatherData.temperature_C);
0978     }
0979 
0980     // Used for all temperatures
0981     data.insert(QStringLiteral("Temperature Unit"), KUnitConversion::Celsius);
0982 
0983     if (!qIsNaN(weatherData.pressure)) {
0984         data.insert(QStringLiteral("Pressure"), weatherData.pressure);
0985         data.insert(QStringLiteral("Pressure Unit"), KUnitConversion::Millibar);
0986         if (!weatherData.pressureTendency.isEmpty()) {
0987             data.insert(QStringLiteral("Pressure Tendency"), weatherData.pressureTendency);
0988         }
0989     }
0990 
0991     if (!qIsNaN(weatherData.windSpeed_miles)) {
0992         data.insert(QStringLiteral("Wind Speed"), weatherData.windSpeed_miles);
0993         data.insert(QStringLiteral("Wind Speed Unit"), KUnitConversion::MilePerHour);
0994         if (!weatherData.windDirection.isEmpty()) {
0995             data.insert(QStringLiteral("Wind Direction"), getWindDirectionIcon(windIcons(), weatherData.windDirection.toLower()));
0996         }
0997     }
0998 
0999     // 5 Day forecast info
1000     const QList<WeatherData::ForecastInfo *> &forecasts = weatherData.forecasts;
1001 
1002     // Set number of forecasts per day/night supported
1003     data.insert(QStringLiteral("Total Weather Days"), forecasts.size());
1004 
1005     int i = 0;
1006     for (const WeatherData::ForecastInfo *forecastInfo : forecasts) {
1007         QString period = forecastInfo->period;
1008         // same day
1009         period.replace(QStringLiteral("Today"), i18nc("Short for Today", "Today"));
1010         period.replace(QStringLiteral("Tonight"), i18nc("Short for Tonight", "Tonight"));
1011         // upcoming days
1012         period.replace(QStringLiteral("Saturday"), i18nc("Short for Saturday", "Sat"));
1013         period.replace(QStringLiteral("Sunday"), i18nc("Short for Sunday", "Sun"));
1014         period.replace(QStringLiteral("Monday"), i18nc("Short for Monday", "Mon"));
1015         period.replace(QStringLiteral("Tuesday"), i18nc("Short for Tuesday", "Tue"));
1016         period.replace(QStringLiteral("Wednesday"), i18nc("Short for Wednesday", "Wed"));
1017         period.replace(QStringLiteral("Thursday"), i18nc("Short for Thursday", "Thu"));
1018         period.replace(QStringLiteral("Friday"), i18nc("Short for Friday", "Fri"));
1019 
1020         const QString tempHigh = qIsNaN(forecastInfo->tempHigh) ? QString() : QString::number(forecastInfo->tempHigh);
1021         const QString tempLow = qIsNaN(forecastInfo->tempLow) ? QString() : QString::number(forecastInfo->tempLow);
1022 
1023         data.insert(QStringLiteral("Short Forecast Day %1").arg(i),
1024                     QStringLiteral("%1|%2|%3|%4|%5|%6").arg(period, forecastInfo->iconName, forecastInfo->summary, tempHigh, tempLow, QString()));
1025         //.arg(forecastInfo->windSpeed)
1026         // arg(forecastInfo->windDirection));
1027 
1028         ++i;
1029     }
1030 
1031     data.insert(QStringLiteral("Credit"), i18nc("credit line, keep string short", "Data from BBC\302\240Weather"));
1032     data.insert(QStringLiteral("Credit Url"), place.forecastHTMLUrl);
1033 
1034     setData(weatherSource, data);
1035 }
1036 
1037 void UKMETIon::dataUpdated(const QString &sourceName, const Plasma5Support::DataEngine::Data &data)
1038 {
1039     const bool isNight = (data.value(QStringLiteral("Corrected Elevation")).toDouble() < 0.0);
1040 
1041     for (auto end = m_weatherData.end(), it = m_weatherData.begin(); it != end; ++it) {
1042         auto &weatherData = it.value();
1043         if (weatherData.solarDataTimeEngineSourceName == sourceName) {
1044             weatherData.isNight = isNight;
1045             weatherData.isSolarDataPending = false;
1046             updateWeather(it.key());
1047         }
1048     }
1049 }
1050 
1051 K_PLUGIN_CLASS_WITH_JSON(UKMETIon, "ion-bbcukmet.json")
1052 
1053 #include "ion_bbcukmet.moc"