You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1051 lines
40 KiB

/***************************************************************************
* Copyright (C) 2007-2009 by Shawn Starr <shawn.starr@rogers.com> *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA *
***************************************************************************/
/* Ion for BBC's Weather from the UK Met Office */
#include "ion_bbcukmet.h"
#include "ion_bbcukmetdebug.h"
#include <KIO/Job>
#include <KUnitConversion/Converter>
#include <KLocalizedString>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QXmlStreamReader>
#include <QTimeZone>
WeatherData::WeatherData()
: stationLatitude(qQNaN())
, stationLongitude(qQNaN())
, condition()
, temperature_C(qQNaN())
, windSpeed_miles(qQNaN())
, humidity(qQNaN())
, pressure(qQNaN())
{
}
WeatherData::ForecastInfo::ForecastInfo()
: tempHigh(qQNaN())
, tempLow(qQNaN())
, windSpeed(qQNaN())
{
}
// ctor, dtor
UKMETIon::UKMETIon(QObject *parent, const QVariantList &args)
: IonInterface(parent, args)
{
setInitialized(true);
}
UKMETIon::~UKMETIon()
{
deleteForecasts();
}
void UKMETIon::reset()
{
deleteForecasts();
m_sourcesToReset = sources();
updateAllSources();
}
void UKMETIon::deleteForecasts()
{
// Destroy each forecast stored in a QVector
QHash<QString, WeatherData>::iterator
it = m_weatherData.begin(),
end = m_weatherData.end();
for (; it != end; ++it) {
qDeleteAll(it.value().forecasts);
it.value().forecasts.clear();
}
}
QMap<QString, IonInterface::ConditionIcons> UKMETIon::setupDayIconMappings() const
{
// ClearDay, FewCloudsDay, PartlyCloudyDay, Overcast,
// Showers, ScatteredShowers, Thunderstorm, Snow,
// FewCloudsNight, PartlyCloudyNight, ClearNight,
// Mist, NotAvailable
return QMap<QString, ConditionIcons> {
{ QStringLiteral("sunny"), ClearDay },
// { QStringLiteral("sunny night"), ClearNight },
{ QStringLiteral("clear"), ClearDay },
{ QStringLiteral("clear sky"), ClearDay },
{ QStringLiteral("sunny intervals"), PartlyCloudyDay },
// { QStringLiteral("sunny intervals night"), ClearNight },
{ QStringLiteral("light cloud"), PartlyCloudyDay },
{ QStringLiteral("partly cloudy"), PartlyCloudyDay },
{ QStringLiteral("cloudy"), PartlyCloudyDay },
{ QStringLiteral("white cloud"), PartlyCloudyDay },
{ QStringLiteral("grey cloud"), Overcast },
{ QStringLiteral("thick cloud"), Overcast },
// { QStringLiteral("low level cloud"), NotAvailable },
// { QStringLiteral("medium level cloud"), NotAvailable },
// { QStringLiteral("sandstorm"), NotAvailable },
{ QStringLiteral("drizzle"), LightRain },
{ QStringLiteral("misty"), Mist },
{ QStringLiteral("mist"), Mist },
{ QStringLiteral("fog"), Mist },
{ QStringLiteral("foggy"), Mist },
{ QStringLiteral("tropical storm"), Thunderstorm },
{ QStringLiteral("hazy"), NotAvailable },
{ QStringLiteral("light shower"), Showers },
{ QStringLiteral("light rain shower"), Showers },
{ QStringLiteral("light rain showers"), Showers },
{ QStringLiteral("light showers"), Showers },
{ QStringLiteral("light rain"), Showers },
{ QStringLiteral("heavy rain"), Rain },
{ QStringLiteral("heavy showers"), Rain },
{ QStringLiteral("heavy shower"), Rain },
{ QStringLiteral("heavy rain shower"), Rain },
{ QStringLiteral("heavy rain showers"), Rain },
{ QStringLiteral("thundery shower"), Thunderstorm },
{ QStringLiteral("thundery showers"), Thunderstorm },
{ QStringLiteral("thunderstorm"), Thunderstorm },
{ QStringLiteral("cloudy with sleet"), RainSnow },
{ QStringLiteral("sleet shower"), RainSnow },
{ QStringLiteral("sleet showers"), RainSnow },
{ QStringLiteral("sleet"), RainSnow },
{ QStringLiteral("cloudy with hail"), Hail },
{ QStringLiteral("hail shower"), Hail },
{ QStringLiteral("hail showers"), Hail },
{ QStringLiteral("hail"), Hail },
{ QStringLiteral("light snow"), LightSnow },
{ QStringLiteral("light snow shower"), Flurries },
{ QStringLiteral("light snow showers"), Flurries },
{ QStringLiteral("cloudy with light snow"), LightSnow },
{ QStringLiteral("heavy snow"), Snow },
{ QStringLiteral("heavy snow shower"), Snow },
{ QStringLiteral("heavy snow showers"), Snow },
{ QStringLiteral("cloudy with heavy snow"), Snow },
{ QStringLiteral("na"), NotAvailable },
};
}
QMap<QString, IonInterface::ConditionIcons> UKMETIon::setupNightIconMappings() const
{
return QMap<QString, ConditionIcons> {
{ QStringLiteral("clear"), ClearNight },
{ QStringLiteral("clear sky"), ClearNight },
{ QStringLiteral("clear intervals"), PartlyCloudyNight },
{ QStringLiteral("sunny intervals"), PartlyCloudyDay }, // it's not really sunny
{ QStringLiteral("sunny"), ClearDay },
{ QStringLiteral("light cloud"), PartlyCloudyNight },
{ QStringLiteral("partly cloudy"), PartlyCloudyNight },
{ QStringLiteral("cloudy"), PartlyCloudyNight },
{ QStringLiteral("white cloud"), PartlyCloudyNight },
{ QStringLiteral("grey cloud"), Overcast },
{ QStringLiteral("thick cloud"), Overcast },
{ QStringLiteral("drizzle"), LightRain },
{ QStringLiteral("misty"), Mist },
{ QStringLiteral("mist"), Mist },
{ QStringLiteral("fog"), Mist },
{ QStringLiteral("foggy"), Mist },
{ QStringLiteral("tropical storm"), Thunderstorm },
{ QStringLiteral("hazy"), NotAvailable },
{ QStringLiteral("light shower"), Showers },
{ QStringLiteral("light rain shower"), Showers },
{ QStringLiteral("light rain showers"), Showers },
{ QStringLiteral("light showers"), Showers },
{ QStringLiteral("light rain"), Showers },
{ QStringLiteral("heavy rain"), Rain },
{ QStringLiteral("heavy showers"), Rain },
{ QStringLiteral("heavy shower"), Rain },
{ QStringLiteral("heavy rain shower"), Rain },
{ QStringLiteral("heavy rain showers"), Rain },
{ QStringLiteral("thundery shower"), Thunderstorm },
{ QStringLiteral("thundery showers"), Thunderstorm },
{ QStringLiteral("thunderstorm"), Thunderstorm },
{ QStringLiteral("cloudy with sleet"), RainSnow },
{ QStringLiteral("sleet shower"), RainSnow },
{ QStringLiteral("sleet showers"), RainSnow },
{ QStringLiteral("sleet"), RainSnow },
{ QStringLiteral("cloudy with hail"), Hail },
{ QStringLiteral("hail shower"), Hail },
{ QStringLiteral("hail showers"), Hail },
{ QStringLiteral("hail"), Hail },
{ QStringLiteral("light snow"), LightSnow },
{ QStringLiteral("light snow shower"), Flurries },
{ QStringLiteral("light snow showers"), Flurries },
{ QStringLiteral("cloudy with light snow"), LightSnow },
{ QStringLiteral("heavy snow"), Snow },
{ QStringLiteral("heavy snow shower"), Snow },
{ QStringLiteral("heavy snow showers"), Snow },
{ QStringLiteral("cloudy with heavy snow"), Snow },
{ QStringLiteral("na"), NotAvailable },
};
}
QMap<QString, IonInterface::WindDirections> UKMETIon::setupWindIconMappings() const
{
return QMap<QString, WindDirections> {
{ QStringLiteral("northerly"), N },
{ QStringLiteral("north north easterly"), NNE },
{ QStringLiteral("north easterly"), NE },
{ QStringLiteral("east north easterly"), ENE },
{ QStringLiteral("easterly"), E },
{ QStringLiteral("east south easterly"), ESE },
{ QStringLiteral("south easterly"), SE },
{ QStringLiteral("south south easterly"), SSE },
{ QStringLiteral("southerly"), S },
{ QStringLiteral("south south westerly"), SSW },
{ QStringLiteral("south westerly"), SW },
{ QStringLiteral("west south westerly"), WSW },
{ QStringLiteral("westerly"), W },
{ QStringLiteral("west north westerly"), WNW },
{ QStringLiteral("north westerly"), NW },
{ QStringLiteral("north north westerly"), NNW },
{ QStringLiteral("calm"), VR },
};
}
QMap<QString, IonInterface::ConditionIcons> const& UKMETIon::dayIcons() const
{
static QMap<QString, ConditionIcons> const dval = setupDayIconMappings();
return dval;
}
QMap<QString, IonInterface::ConditionIcons> const& UKMETIon::nightIcons() const
{
static QMap<QString, ConditionIcons> const nval = setupNightIconMappings();
return nval;
}
QMap<QString, IonInterface::WindDirections> const& UKMETIon::windIcons() const
{
static QMap<QString, WindDirections> const wval = setupWindIconMappings();
return wval;
}
// Get a specific Ion's data
bool UKMETIon::updateIonSource(const QString& source)
{
// We expect the applet to send the source in the following tokenization:
// ionname|validate|place_name - Triggers validation of place
// ionname|weather|place_name - Triggers receiving weather of place
const QStringList sourceAction = source.split(QLatin1Char('|'));
// Guard: if the size of array is not 3 then we have bad data, return an error
if (sourceAction.size() < 3) {
setData(source, QStringLiteral("validate"), QStringLiteral("bbcukmet|malformed"));
return true;
}
if (sourceAction[1] == QLatin1String("validate") && sourceAction.size() >= 3) {
// Look for places to match
findPlace(sourceAction[2], source);
return true;
}
if (sourceAction[1] == QLatin1String("weather") && sourceAction.size() >= 3) {
if (sourceAction.count() >= 3) {
if (sourceAction[2].isEmpty()) {
setData(source, QStringLiteral("validate"), QStringLiteral("bbcukmet|malformed"));
return true;
}
XMLMapInfo& place = m_place[QStringLiteral("bbcukmet|") + sourceAction[2]];
// backward compatibility after rss feed url change in 2018/03
place.sourceExtraArg = sourceAction[3];
if (place.sourceExtraArg.startsWith(QLatin1String("http://open.live.bbc.co.uk/"))) {
// Old data source id stored the full (now outdated) observation feed url
// http://open.live.bbc.co.uk/weather/feeds/en/STATIOID/observations.rss
// as extra argument, so extract the id from that
place.stationId = place.sourceExtraArg.section(QLatin1Char('/'), -2, -2);
} else {
place.stationId = place.sourceExtraArg;
}
getXMLData(sourceAction[0] + QLatin1Char('|') + sourceAction[2]);
return true;
}
return false;
}
setData(source, QStringLiteral("validate"), QStringLiteral("bbcukmet|malformed"));
return true;
}
// Gets specific city XML data
void UKMETIon::getXMLData(const QString& source)
{
for (const QString& fetching : qAsConst(m_obsJobList)) {
if (fetching == source) {
// already getting this source and awaiting the data
return;
}
}
const QUrl url(QStringLiteral("https://weather-broker-cdn.api.bbci.co.uk/en/observation/rss/") + m_place[source].stationId);
KIO::TransferJob* getJob = KIO::get(url, KIO::Reload, KIO::HideProgressInfo);
getJob->addMetaData(QStringLiteral("cookies"), QStringLiteral("none")); // Disable displaying cookies
m_obsJobXml.insert(getJob, new QXmlStreamReader);
m_obsJobList.insert(getJob, source);
connect(getJob, &KIO::TransferJob::data,
this, &UKMETIon::observation_slotDataArrived);
connect(getJob, &KJob::result,
this, &UKMETIon::observation_slotJobFinished);
}
// Parses city list and gets the correct city based on ID number
void UKMETIon::findPlace(const QString& place, const QString& source)
{
/* There's a page= parameter, results are limited to 10 by page */
const QUrl url(QLatin1String("https://www.bbc.com/locator/default/en-GB/search.json?search=")+place+
QLatin1String("&filter=international&postcode_unit=false&postcode_district=true"));
KIO::TransferJob* getJob = KIO::get(url, KIO::Reload, KIO::HideProgressInfo);
getJob->addMetaData(QStringLiteral("cookies"), QStringLiteral("none")); // Disable displaying cookies
m_jobHtml.insert(getJob, new QByteArray());
m_jobList.insert(getJob, source);
connect(getJob, &KIO::TransferJob::data,
this, &UKMETIon::setup_slotDataArrived);
connect(getJob, &KJob::result,
this, &UKMETIon::setup_slotJobFinished);
/*
// Handle redirects for direct hit places.
connect(getJob, SIGNAL(redirection(KIO::Job*,KUrl)),
this, SLOT(setup_slotRedirected(KIO::Job*,KUrl)));
*/
}
void UKMETIon::getFiveDayForecast(const QString& source)
{
XMLMapInfo& place = m_place[source];
const QUrl url(QStringLiteral("https://weather-broker-cdn.api.bbci.co.uk/en/forecast/rss/3day/") + place.stationId);
KIO::TransferJob* getJob = KIO::get(url, KIO::Reload, KIO::HideProgressInfo);
getJob->addMetaData(QStringLiteral("cookies"), QStringLiteral("none")); // Disable displaying cookies
m_forecastJobXml.insert(getJob, new QXmlStreamReader);
m_forecastJobList.insert(getJob, source);
connect(getJob, &KIO::TransferJob::data,
this, &UKMETIon::forecast_slotDataArrived);
connect(getJob, &KJob::result,
this, &UKMETIon::forecast_slotJobFinished);
}
void UKMETIon::readSearchHTMLData(const QString& source, const QByteArray& html)
{
int counter = 2;
QJsonObject jsonDocumentObject = QJsonDocument::fromJson(html).object();
if (!jsonDocumentObject.isEmpty()) {
const QJsonArray results = jsonDocumentObject.value(QStringLiteral("results")).toArray();
for (const QJsonValue& resultValue : results) {
QJsonObject result = resultValue.toObject();
const QString id = result.value(QStringLiteral("id")).toString();
const QString fullName = result.value(QStringLiteral("fullName")).toString();
if (!id.isEmpty() && !fullName.isEmpty()) {
QString tmp = QStringLiteral("bbcukmet|") + fullName;
// Duplicate places can exist
if (m_locations.contains(tmp)) {
tmp += QStringLiteral(" (#") + QString::number(counter) + QLatin1Char(')');
counter++;
}
XMLMapInfo& place = m_place[tmp];
place.stationId = id;
place.place = fullName;
m_locations.append(tmp);
}
}
}
validate(source);
}
// handle when no XML tag is found
void UKMETIon::parseUnknownElement(QXmlStreamReader& xml) const
{
while (!xml.atEnd()) {
xml.readNext();
if (xml.isEndElement()) {
break;
}
if (xml.isStartElement()) {
parseUnknownElement(xml);
}
}
}
void UKMETIon::setup_slotDataArrived(KIO::Job *job, const QByteArray &data)
{
if (data.isEmpty() || !m_jobHtml.contains(job)) {
return;
}
m_jobHtml[job]->append(data);
}
void UKMETIon::setup_slotJobFinished(KJob *job)
{
if (job->error() == KIO::ERR_SERVER_TIMEOUT) {
setData(m_jobList[job], QStringLiteral("validate"), QStringLiteral("bbcukmet|timeout"));
disconnectSource(m_jobList[job], this);
m_jobList.remove(job);
delete m_jobHtml[job];
m_jobHtml.remove(job);
return;
}
// If Redirected, don't go to this routine
if (!m_locations.contains(QStringLiteral("bbcukmet|") + m_jobList[job])) {
QByteArray *reader = m_jobHtml.value(job);
if (reader) {
readSearchHTMLData(m_jobList[job], *reader);
}
}
m_jobList.remove(job);
delete m_jobHtml[job];
m_jobHtml.remove(job);
}
void UKMETIon::observation_slotDataArrived(KIO::Job *job, const QByteArray &data)
{
QByteArray local = data;
if (data.isEmpty() || !m_obsJobXml.contains(job)) {
return;
}
// Send to xml.
m_obsJobXml[job]->addData(local);
}
void UKMETIon::observation_slotJobFinished(KJob *job)
{
const QString source = m_obsJobList.value(job);
setData(source, Data());
QXmlStreamReader *reader = m_obsJobXml.value(job);
if (reader) {
readObservationXMLData(m_obsJobList[job], *reader);
}
m_obsJobList.remove(job);
delete m_obsJobXml[job];
m_obsJobXml.remove(job);
if (m_sourcesToReset.contains(source)) {
m_sourcesToReset.removeAll(source);
emit forceUpdate(this, source);
}
}
void UKMETIon::forecast_slotDataArrived(KIO::Job *job, const QByteArray &data)
{
QByteArray local = data;
if (data.isEmpty() || !m_forecastJobXml.contains(job)) {
return;
}
// Send to xml.
m_forecastJobXml[job]->addData(local);
}
void UKMETIon::forecast_slotJobFinished(KJob *job)
{
setData(m_forecastJobList[job], Data());
QXmlStreamReader *reader = m_forecastJobXml.value(job);
if (reader) {
readFiveDayForecastXMLData(m_forecastJobList[job], *reader);
}
m_forecastJobList.remove(job);
delete m_forecastJobXml[job];
m_forecastJobXml.remove(job);
}
void UKMETIon::parsePlaceObservation(const QString &source, WeatherData& data, QXmlStreamReader& xml)
{
Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("rss"));
while (!xml.atEnd()) {
xml.readNext();
const QStringRef elementName = xml.name();
if (xml.isEndElement() && elementName == QLatin1String("rss")) {
break;
}
if (xml.isStartElement() && elementName == QLatin1String("channel")) {
parseWeatherChannel(source, data, xml);
}
}
}
void UKMETIon::parsePlaceForecast(const QString &source, QXmlStreamReader& xml)
{
Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("rss"));
while (!xml.atEnd()) {
xml.readNext();
if (xml.isStartElement() && xml.name() == QLatin1String("channel")) {
parseWeatherForecast(source, xml);
}
}
}
void UKMETIon::parseWeatherChannel(const QString& source, WeatherData& data, QXmlStreamReader& xml)
{
Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("channel"));
while (!xml.atEnd()) {
xml.readNext();
const QStringRef elementName = xml.name();
if (xml.isEndElement() && elementName == QLatin1String("channel")) {
break;
}
if (xml.isStartElement()) {
if (elementName == QLatin1String("title")) {
data.stationName = xml.readElementText().section(QStringLiteral("Observations for"), 1, 1).trimmed();
data.stationName.replace(QStringLiteral("United Kingdom"), i18n("UK"));
data.stationName.replace(QStringLiteral("United States of America"), i18n("USA"));
} else if (elementName == QLatin1String("item")) {
parseWeatherObservation(source, data, xml);
} else {
parseUnknownElement(xml);
}
}
}
}
void UKMETIon::parseWeatherForecast(const QString& source, QXmlStreamReader& xml)
{
Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("channel"));
while (!xml.atEnd()) {
xml.readNext();
const QStringRef elementName = xml.name();
if (xml.isEndElement() && elementName == QLatin1String("channel")) {
break;
}
if (xml.isStartElement()) {
if (elementName == QLatin1String("item")) {
parseFiveDayForecast(source, xml);
} else if (elementName == QLatin1String("link") &&
xml.namespaceUri().isEmpty()) {
m_place[source].forecastHTMLUrl = xml.readElementText();
} else {
parseUnknownElement(xml);
}
}
}
}
void UKMETIon::parseWeatherObservation(const QString& source, WeatherData& data, QXmlStreamReader& xml)
{
Q_UNUSED(source);
Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("item"));
while (!xml.atEnd()) {
xml.readNext();
const QStringRef elementName = xml.name();
if (xml.isEndElement() && elementName == QLatin1String("item")) {
break;
}
if (xml.isStartElement()) {
if (elementName == QLatin1String("title")) {
QString conditionString = xml.readElementText();
// Get the observation time and condition
int splitIndex = conditionString.lastIndexOf(QLatin1Char(':'));
if (splitIndex >= 0) {
QString conditionData = conditionString.mid(splitIndex + 1); // Skip ':'
data.obsTime = conditionString.left(splitIndex);
if (data.obsTime.contains(QLatin1Char('-'))) {
// Saturday - 13:00 CET
// Saturday - 12:00 GMT
// timezone parsing is not yet supported by QDateTime, also is there just a dayname
// so try manually
// guess date from day
const QString dayString = data.obsTime.section(QLatin1Char('-'), 0, 0).trimmed();
QDate date = QDate::currentDate();
const QString dayFormat = QStringLiteral("dddd");
const int testDayJumps[4] = {
-1, // first to weekday yesterday
2, // then to weekday tomorrow
-3, // then to weekday before yesterday, not sure if such day offset can happen?
4, // then to weekday after tomorrow, not sure if such day offset can happen?
};
const int dayJumps = sizeof(testDayJumps)/sizeof(testDayJumps[0]);
QLocale cLocale = QLocale::c();
int dayJump = 0;
while (true) {
if (cLocale.toString(date, dayFormat) == dayString) {
break;
}
if (dayJump >= dayJumps) {
// no weekday found near-by, set date invalid
date = QDate();
break;
}
date = date.addDays(testDayJumps[dayJump]);
++dayJump;
}
if (date.isValid()) {
const QString timeString = data.obsTime.section(QLatin1Char('-'), 1, 1).trimmed();
const QTime time = QTime::fromString(timeString.section(QLatin1Char(' '), 0, 0), QStringLiteral("hh:mm"));
const QTimeZone timeZone = QTimeZone(timeString.section(QLatin1Char(' '), 1, 1).toUtf8());
// TODO: if non-IANA timezone id is not known, try to guess timezone from other data
if (time.isValid() && timeZone.isValid()) {
data.observationDateTime = QDateTime(date, time, timeZone);
}
}
}
if (conditionData.contains(QLatin1Char(','))) {
data.condition = conditionData.section(QLatin1Char(','), 0, 0).trimmed();
if (data.condition == QLatin1String("null") ||
data.condition == QLatin1String("Not Available")) {
data.condition.clear();
}
}
}
} else if (elementName == QLatin1String("description")) {
QString observeString = xml.readElementText();
const QStringList observeData = observeString.split(QLatin1Char(':'));
// FIXME: We should make this use a QRegExp but I need some help here :) -spstarr
QString temperature_C = observeData[1].section(QChar(176), 0, 0).trimmed();
parseFloat(data.temperature_C, temperature_C);
data.windDirection = observeData[2].section(QLatin1Char(','), 0, 0).trimmed();
if (data.windDirection.contains(QStringLiteral("null"))) {
data.windDirection.clear();
}
QString windSpeed_miles = observeData[3].section(QLatin1Char(','), 0, 0).section(QLatin1Char(' '),1 ,1).remove(QStringLiteral("mph"));
parseFloat(data.windSpeed_miles, windSpeed_miles);
QString humidity = observeData[4].section(QLatin1Char(','), 0, 0).section(QLatin1Char(' '),1 ,1);
if (humidity.endsWith(QLatin1Char('%'))) {
humidity.chop(1);
}
parseFloat(data.humidity, humidity);
QString pressure = observeData[5].section(QLatin1Char(','), 0, 0).section(QLatin1Char(' '),1 ,1).section(QStringLiteral("mb"), 0, 0);
parseFloat(data.pressure, pressure);
data.pressureTendency = observeData[5].section(QLatin1Char(','), 1, 1).toLower().trimmed();
if (data.pressureTendency == QLatin1String("no change")) {
data.pressureTendency = QStringLiteral("steady");
}
data.visibilityStr = observeData[6].trimmed();
if (data.visibilityStr == QLatin1String("--")) {
data.visibilityStr.clear();
}
} else if (elementName == QLatin1String("lat")) {
const QString ordinate = xml.readElementText();
data.stationLatitude = ordinate.toDouble();
} else if (elementName == QLatin1String("long")) {
const QString ordinate = xml.readElementText();
data.stationLongitude = ordinate.toDouble();
} else if (elementName == QLatin1String("point") &&
xml.namespaceUri() == QLatin1String("http://www.georss.org/georss")) {
const QStringList ordinates = xml.readElementText().split(QLatin1Char(' '));
data.stationLatitude = ordinates[0].toDouble();
data.stationLongitude = ordinates[1].toDouble();
} else {
parseUnknownElement(xml);
}
}
}
}
bool UKMETIon::readObservationXMLData(const QString& source, QXmlStreamReader& xml)
{
WeatherData data;
data.isForecastsDataPending = true;
bool haveObservation = false;
while (!xml.atEnd()) {
xml.readNext();
if (xml.isEndElement()) {
break;
}
if (xml.isStartElement()) {
if (xml.name() == QLatin1String("rss")) {
parsePlaceObservation(source, data, xml);
haveObservation = true;
} else {
parseUnknownElement(xml);
}
}
}
if (!haveObservation) {
return false;
}
bool solarDataSourceNeedsConnect = false;
Plasma::DataEngine* timeEngine = dataEngine(QStringLiteral("time"));
if (timeEngine) {
const bool canCalculateElevation =
(data.observationDateTime.isValid() &&
(!qIsNaN(data.stationLatitude) && !qIsNaN(data.stationLongitude)));
if (canCalculateElevation) {
data.solarDataTimeEngineSourceName = QStringLiteral("Local|Solar|Latitude=%1|Longitude=%2|DateTime=%3")
.arg(data.stationLatitude)
.arg(data.stationLongitude)
.arg(data.observationDateTime.toString(Qt::ISODate));
solarDataSourceNeedsConnect = true;
}
// check any previous data
const auto it = m_weatherData.constFind(source);
if (it != m_weatherData.constEnd()) {
const QString& oldSolarDataTimeEngineSource = it.value().solarDataTimeEngineSourceName;
if (oldSolarDataTimeEngineSource == data.solarDataTimeEngineSourceName) {
// can reuse elevation source (if any), copy over data
data.isNight = it.value().isNight;
solarDataSourceNeedsConnect = false;
} else if (!oldSolarDataTimeEngineSource.isEmpty()) {
// drop old elevation source
timeEngine->disconnectSource(oldSolarDataTimeEngineSource, this);
}
}
}
m_weatherData[source] = data;
// connect only after m_weatherData has the data, so the instant data push handling can see it
if (solarDataSourceNeedsConnect) {
data.isSolarDataPending = true;
timeEngine->connectSource(data.solarDataTimeEngineSourceName, this);
}
// Get the 5 day forecast info next.
getFiveDayForecast(source);
return !xml.error();
}
bool UKMETIon::readFiveDayForecastXMLData(const QString& source, QXmlStreamReader& xml)
{
bool haveFiveDay = false;
while (!xml.atEnd()) {
xml.readNext();
if (xml.isEndElement()) {
break;
}
if (xml.isStartElement()) {
if (xml.name() == QLatin1String("rss")) {
parsePlaceForecast(source, xml);
haveFiveDay = true;
} else {
parseUnknownElement(xml);
}
}
}
if (!haveFiveDay) return false;
updateWeather(source);
return !xml.error();
}
void UKMETIon::parseFiveDayForecast(const QString& source, QXmlStreamReader& xml)
{
Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("item"));
WeatherData& weatherData = m_weatherData[source];
QVector<WeatherData::ForecastInfo*>& forecasts = weatherData.forecasts;
// Flush out the old forecasts when updating.
forecasts.clear();
WeatherData::ForecastInfo *forecast = new WeatherData::ForecastInfo;
QString line;
QString period;
QString summary;
QRegExp high(QStringLiteral("Maximum Temperature: (-?\\d+).C"), Qt::CaseInsensitive);
QRegExp low(QStringLiteral("Minimum Temperature: (-?\\d+).C"), Qt::CaseInsensitive);
while (!xml.atEnd()) {
xml.readNext();
if (xml.name() == QLatin1String("title")) {
line = xml.readElementText().trimmed();
// FIXME: We should make this all use QRegExps in UKMETIon::parseFiveDayForecast() for forecast -spstarr
const QString p = line.section(QLatin1Char(','), 0, 0);
period = p.section(QLatin1Char(':'), 0, 0);
summary = p.section(QLatin1Char(':'), 1, 1).trimmed();
const QString temps = line.section(QLatin1Char(','), 1, 1);
// Sometimes only one of min or max are reported
if (high.indexIn(temps) != -1) {
parseFloat(forecast->tempHigh, high.cap(1));
}
if (low.indexIn(temps) != -1) {
parseFloat(forecast->tempLow, low.cap(1));
}
const QString summaryLC = summary.toLower();
forecast->period = period;
if (forecast->period == QLatin1String("Tonight")) {
forecast->iconName = getWeatherIcon(nightIcons(), summaryLC);
} else {
forecast->iconName = getWeatherIcon(dayIcons(), summaryLC);
}
// db uses original strings normalized to lowercase, but we prefer the unnormalized if without translation
const QString summaryTranslated = i18nc("weather forecast", summaryLC.toUtf8().data());
forecast->summary = (summaryTranslated != summaryLC) ? summaryTranslated : summary;
qCDebug(IONENGINE_BBCUKMET) << "i18n summary string: " << forecast->summary;
forecasts.append(forecast);
// prepare next
forecast = new WeatherData::ForecastInfo;
}
}
weatherData.isForecastsDataPending = false;
// remove unused
delete forecast;
}
void UKMETIon::parseFloat(float& value, const QString& string)
{
bool ok = false;
const float result = string.toFloat(&ok);
if (ok) {
value = result;
}
}
void UKMETIon::validate(const QString& source)
{
if (m_locations.isEmpty()) {
const QString invalidPlace = source.section(QLatin1Char('|'), 2, 2);
if (m_place[QStringLiteral("bbcukmet|")+invalidPlace].place.isEmpty()) {
setData(source, QStringLiteral("validate"),
QVariant(QStringLiteral("bbcukmet|invalid|multiple|") + invalidPlace));
}
return;
}
QString placeList;
for (const QString& place : qAsConst(m_locations)) {
const QString p = place.section(QLatin1Char('|'), 1, 1);
placeList.append(QStringLiteral("|place|") + p + QStringLiteral("|extra|") + m_place[place].stationId);
}
if (m_locations.count() > 1) {
setData(source, QStringLiteral("validate"),
QVariant(QStringLiteral("bbcukmet|valid|multiple") + placeList));
} else {
placeList[7] = placeList[7].toUpper();
setData(source, QStringLiteral("validate"),
QVariant(QStringLiteral("bbcukmet|valid|single") + placeList));
}
m_locations.clear();
}
void UKMETIon::updateWeather(const QString& source)
{
const WeatherData& weatherData = m_weatherData[source];
if (weatherData.isForecastsDataPending || weatherData.isSolarDataPending) {
return;
}
const XMLMapInfo& place = m_place[source];
QString weatherSource = source;
// TODO: why the replacement here instead of just a new string?
weatherSource.replace(QStringLiteral("bbcukmet|"), QStringLiteral("bbcukmet|weather|"));
weatherSource.append(QLatin1Char('|') + place.sourceExtraArg);
Plasma::DataEngine::Data data;
// work-around for buggy observation RSS feed missing the station name
QString stationName = weatherData.stationName;
if (stationName.isEmpty() || stationName == QLatin1String(",")) {
stationName = source.section(QLatin1Char('|'), 1, 1);
}
data.insert(QStringLiteral("Place"), stationName);
data.insert(QStringLiteral("Station"), stationName);
if (weatherData.observationDateTime.isValid()) {
data.insert(QStringLiteral("Observation Timestamp"), weatherData.observationDateTime);
}
if (!weatherData.obsTime.isEmpty()) {
data.insert(QStringLiteral("Observation Period"), weatherData.obsTime);
}
if (!weatherData.condition.isEmpty()) {
// db uses original strings normalized to lowercase, but we prefer the unnormalized if without translation
const QString conditionLC = weatherData.condition.toLower();
const QString conditionTranslated = i18nc("weather condition", conditionLC.toUtf8().data());
data.insert(QStringLiteral("Current Conditions"),
(conditionTranslated != conditionLC) ? conditionTranslated : weatherData.condition);
}
// qCDebug(IONENGINE_BBCUKMET) << "i18n condition string: " << i18nc("weather condition", weatherData.condition.toUtf8().data());
const bool stationCoordsValid = (!qIsNaN(weatherData.stationLatitude) && !qIsNaN(weatherData.stationLongitude));
if (stationCoordsValid) {
data.insert(QStringLiteral("Latitude"), weatherData.stationLatitude);
data.insert(QStringLiteral("Longitude"), weatherData.stationLongitude);
}
data.insert(QStringLiteral("Condition Icon"), getWeatherIcon(weatherData.isNight ? nightIcons() : dayIcons(), weatherData.condition));
if (!qIsNaN(weatherData.humidity)) {
data.insert(QStringLiteral("Humidity"), weatherData.humidity);
data.insert(QStringLiteral("Humidity Unit"), KUnitConversion::Percent);
}
if (!weatherData.visibilityStr.isEmpty()) {
data.insert(QStringLiteral("Visibility"), i18nc("visibility", weatherData.visibilityStr.toUtf8().data()));
data.insert(QStringLiteral("Visibility Unit"), KUnitConversion::NoUnit);
}
if (!qIsNaN(weatherData.temperature_C)) {
data.insert(QStringLiteral("Temperature"), weatherData.temperature_C);
}
// Used for all temperatures
data.insert(QStringLiteral("Temperature Unit"), KUnitConversion::Celsius);
if (!qIsNaN(weatherData.pressure)) {
data.insert(QStringLiteral("Pressure"), weatherData.pressure);
data.insert(QStringLiteral("Pressure Unit"), KUnitConversion::Millibar);
if (!weatherData.pressureTendency.isEmpty()) {
data.insert(QStringLiteral("Pressure Tendency"), weatherData.pressureTendency);
}
}
if (!qIsNaN(weatherData.windSpeed_miles)) {
data.insert(QStringLiteral("Wind Speed"), weatherData.windSpeed_miles);
data.insert(QStringLiteral("Wind Speed Unit"), KUnitConversion::MilePerHour);
if (!weatherData.windDirection.isEmpty()) {
data.insert(QStringLiteral("Wind Direction"),
getWindDirectionIcon(windIcons(), weatherData.windDirection.toLower()));
}
}
// 5 Day forecast info
const QVector <WeatherData::ForecastInfo *>& forecasts = weatherData.forecasts;
// Set number of forecasts per day/night supported
data.insert(QStringLiteral("Total Weather Days"), forecasts.size());
int i = 0;
for (const WeatherData::ForecastInfo* forecastInfo : forecasts) {
QString period = forecastInfo->period;
// same day
period.replace(QStringLiteral("Today"), i18nc("Short for Today", "Today"));
period.replace(QStringLiteral("Tonight"), i18nc("Short for Tonight", "Tonight"));
// upcoming days
period.replace(QStringLiteral("Saturday"), i18nc("Short for Saturday", "Sat"));
period.replace(QStringLiteral("Sunday"), i18nc("Short for Sunday", "Sun"));
period.replace(QStringLiteral("Monday"), i18nc("Short for Monday", "Mon"));
period.replace(QStringLiteral("Tuesday"), i18nc("Short for Tuesday", "Tue"));
period.replace(QStringLiteral("Wednesday"), i18nc("Short for Wednesday", "Wed"));
period.replace(QStringLiteral("Thursday"), i18nc("Short for Thursday", "Thu"));
period.replace(QStringLiteral("Friday"), i18nc("Short for Friday", "Fri"));
const QString tempHigh = qIsNaN(forecastInfo->tempHigh) ? QString() : QString::number(forecastInfo->tempHigh);
const QString tempLow = qIsNaN(forecastInfo->tempLow) ? QString() : QString::number(forecastInfo->tempLow);
data.insert(QStringLiteral("Short Forecast Day %1").arg(i),
QStringLiteral("%1|%2|%3|%4|%5|%6").arg(
period,
forecastInfo->iconName,
forecastInfo->summary,
tempHigh,
tempLow,
QString()));
//.arg(forecastInfo->windSpeed)
//arg(forecastInfo->windDirection));
++i;
}
data.insert(QStringLiteral("Credit"), i18nc("credit line, keep string short", "Data from BBC\302\240Weather"));
data.insert(QStringLiteral("Credit Url"), place.forecastHTMLUrl);
setData(weatherSource, data);
}
void UKMETIon::dataUpdated(const QString& sourceName, const Plasma::DataEngine::Data& data)
{
const bool isNight = (data.value(QStringLiteral("Corrected Elevation")).toDouble() < 0.0);
for (auto end = m_weatherData.end(), it = m_weatherData.begin(); it != end; ++it) {
auto& weatherData = it.value();
if (weatherData.solarDataTimeEngineSourceName == sourceName) {
weatherData.isNight = isNight;
weatherData.isSolarDataPending = false;
updateWeather(it.key());
}
}
}
K_EXPORT_PLASMA_DATAENGINE_WITH_JSON(bbcukmet, UKMETIon, "ion-bbcukmet.json")
#include "ion_bbcukmet.moc"