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"