diff --git a/dataengines/weather/ions/noaa/ion_noaa.cpp b/dataengines/weather/ions/noaa/ion_noaa.cpp index 9130726e8..fe709f361 100644 --- a/dataengines/weather/ions/noaa/ion_noaa.cpp +++ b/dataengines/weather/ions/noaa/ion_noaa.cpp @@ -1,935 +1,935 @@ /*************************************************************************** - * Copyright (C) 2007-2009 by Shawn Starr * + * Copyright (C) 2007-2009,2019 by Shawn Starr * * * * 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 NOAA's National Weather Service XML data */ #include "ion_noaa.h" #include "ion_noaadebug.h" #include #include #include #include #include WeatherData::WeatherData() : stationLatitude(qQNaN()) , stationLongitude(qQNaN()) , temperature_F(qQNaN()) , temperature_C(qQNaN()) , humidity(qQNaN()) , windSpeed(qQNaN()) , windGust(qQNaN()) , pressure(qQNaN()) , dewpoint_F(qQNaN()) , dewpoint_C(qQNaN()) , heatindex_F(qQNaN()) , heatindex_C(qQNaN()) , windchill_F(qQNaN()) , windchill_C(qQNaN()) , visibility(qQNaN()) { } QMap NOAAIon::setupWindIconMappings() const { return QMap { { QStringLiteral("north"), N }, { QStringLiteral("northeast"), NE }, { QStringLiteral("south"), S }, { QStringLiteral("southwest"), SW }, { QStringLiteral("east"), E }, { QStringLiteral("southeast"), SE }, { QStringLiteral("west"), W }, { QStringLiteral("northwest"), NW }, { QStringLiteral("calm"), VR }, }; } QMap NOAAIon::setupConditionIconMappings() const { QMap conditionList; return conditionList; } QMap const& NOAAIon::conditionIcons() const { static QMap const condval = setupConditionIconMappings(); return condval; } QMap const& NOAAIon::windIcons() const { static QMap const wval = setupWindIconMappings(); return wval; } // ctor, dtor NOAAIon::NOAAIon(QObject *parent, const QVariantList &args) : IonInterface(parent, args) { // Get the real city XML URL so we can parse this getXMLSetup(); } void NOAAIon::reset() { m_sourcesToReset = sources(); getXMLSetup(); } NOAAIon::~NOAAIon() { //seems necessary to avoid crash removeAllSources(); } QStringList NOAAIon::validate(const QString& source) const { QStringList placeList; QString station; QString sourceNormalized = source.toUpper(); QHash::const_iterator it = m_places.constBegin(); // If the source name might look like a station ID, check these too and return the name bool checkState = source.count() == 2; while (it != m_places.constEnd()) { if (checkState) { if (it.value().stateName == source) { placeList.append(QStringLiteral("place|").append(it.key())); } } else if (it.key().toUpper().contains(sourceNormalized)) { placeList.append(QStringLiteral("place|").append(it.key())); } else if (it.value().stationID == sourceNormalized) { station = QStringLiteral("place|").append(it.key()); } ++it; } placeList.sort(); if (!station.isEmpty()) { placeList.prepend(station); } return placeList; } bool NOAAIon::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 QStringList sourceAction = source.split(QLatin1Char('|')); // Guard: if the size of array is not 2 then we have bad data, return an error if (sourceAction.size() < 2) { setData(source, QStringLiteral("validate"), QStringLiteral("noaa|malformed")); return true; } if (sourceAction[1] == QLatin1String("validate") && sourceAction.size() > 2) { QStringList result = validate(sourceAction[2]); if (result.size() == 1) { setData(source, QStringLiteral("validate"), QStringLiteral("noaa|valid|single|").append(result.join(QLatin1Char('|')))); return true; } if (result.size() > 1) { setData(source, QStringLiteral("validate"), QStringLiteral("noaa|valid|multiple|").append(result.join(QLatin1Char('|')))); return true; } // result.size() == 0 setData(source, QStringLiteral("validate"), QStringLiteral("noaa|invalid|single|").append(sourceAction[2])); return true; } if (sourceAction[1] == QLatin1String("weather") && sourceAction.size() > 2) { getXMLData(source); return true; } setData(source, QStringLiteral("validate"), QStringLiteral("noaa|malformed")); return true; } // Parses city list and gets the correct city based on ID number void NOAAIon::getXMLSetup() const { const QUrl url(QStringLiteral("https://www.weather.gov/data/current_obs/index.xml")); KIO::TransferJob* getJob = KIO::get(url, KIO::NoReload, KIO::HideProgressInfo); connect(getJob, &KIO::TransferJob::data, this, &NOAAIon::setup_slotDataArrived); connect(getJob, &KJob::result, this, &NOAAIon::setup_slotJobFinished); } // Gets specific city XML data void NOAAIon::getXMLData(const QString& source) { for (const QString& fetching : qAsConst(m_jobList)) { if (fetching == source) { // already getting this source and awaiting the data return; } } QString dataKey = source; dataKey.remove(QStringLiteral("noaa|weather|")); const QUrl url(m_places[dataKey].XMLurl); // If this is empty we have no valid data, send out an error and abort. if (url.url().isEmpty()) { setData(source, QStringLiteral("validate"), QStringLiteral("noaa|malformed")); return; } KIO::TransferJob* getJob = KIO::get(url, KIO::Reload, KIO::HideProgressInfo); m_jobXml.insert(getJob, new QXmlStreamReader); m_jobList.insert(getJob, source); connect(getJob, &KIO::TransferJob::data, this, &NOAAIon::slotDataArrived); connect(getJob, &KJob::result, this, &NOAAIon::slotJobFinished); } void NOAAIon::setup_slotDataArrived(KIO::Job *job, const QByteArray &data) { Q_UNUSED(job) if (data.isEmpty()) { return; } // Send to xml. m_xmlSetup.addData(data); } void NOAAIon::slotDataArrived(KIO::Job *job, const QByteArray &data) { if (data.isEmpty() || !m_jobXml.contains(job)) { return; } // Send to xml. m_jobXml[job]->addData(data); } void NOAAIon::slotJobFinished(KJob *job) { // Dual use method, if we're fetching location data to parse we need to do this first const QString source(m_jobList.value(job)); removeAllData(source); QXmlStreamReader *reader = m_jobXml.value(job); if (reader) { readXMLData(m_jobList[job], *reader); } // Now that we have the longitude and latitude, fetch the seven day forecast. getForecast(m_jobList[job]); m_jobList.remove(job); m_jobXml.remove(job); delete reader; } void NOAAIon::setup_slotJobFinished(KJob *job) { Q_UNUSED(job) const bool success = readXMLSetup(); setInitialized(success); for (const QString& source : qAsConst(m_sourcesToReset)) { updateSourceEvent(source); } } void NOAAIon::parseFloat(float& value, const QString& string) { bool ok = false; const float result = string.toFloat(&ok); if (ok) { value = result; } } void NOAAIon::parseFloat(float& value, QXmlStreamReader& xml) { bool ok = false; const float result = xml.readElementText().toFloat(&ok); if (ok) { value = result; } } void NOAAIon::parseDouble(double& value, QXmlStreamReader& xml) { bool ok = false; const double result = xml.readElementText().toDouble(&ok); if (ok) { value = result; } } void NOAAIon::parseStationID() { QString state; QString stationName; QString stationID; QString xmlurl; while (!m_xmlSetup.atEnd()) { m_xmlSetup.readNext(); const QStringRef elementName = m_xmlSetup.name(); if (m_xmlSetup.isEndElement() && elementName == QLatin1String("station")) { if (!xmlurl.isEmpty()) { NOAAIon::XMLMapInfo info; info.stateName = state; info.stationName = stationName; info.stationID = stationID; info.XMLurl = xmlurl; QString tmp = stationName + QStringLiteral(", ") + state; // Build the key name. m_places[tmp] = info; } break; } if (m_xmlSetup.isStartElement()) { if (elementName == QLatin1String("station_id")) { stationID = m_xmlSetup.readElementText(); } else if (elementName == QLatin1String("state")) { state = m_xmlSetup.readElementText(); } else if (elementName == QLatin1String("station_name")) { stationName = m_xmlSetup.readElementText(); } else if (elementName == QLatin1String("xml_url")) { xmlurl = m_xmlSetup.readElementText().replace(QStringLiteral("http://"), QStringLiteral("http://www.")); } else { parseUnknownElement(m_xmlSetup); } } } } void NOAAIon::parseStationList() { while (!m_xmlSetup.atEnd()) { m_xmlSetup.readNext(); if (m_xmlSetup.isEndElement()) { break; } if (m_xmlSetup.isStartElement()) { if (m_xmlSetup.name() == QLatin1String("station")) { parseStationID(); } else { parseUnknownElement(m_xmlSetup); } } } } // Parse the city list and store into a QMap bool NOAAIon::readXMLSetup() { bool success = false; while (!m_xmlSetup.atEnd()) { m_xmlSetup.readNext(); if (m_xmlSetup.isStartElement()) { if (m_xmlSetup.name() == QLatin1String("wx_station_index")) { parseStationList(); success = true; } } } return (!m_xmlSetup.error() && success); } void NOAAIon::parseWeatherSite(WeatherData& data, QXmlStreamReader& xml) { data.temperature_C = qQNaN(); data.temperature_F = qQNaN(); data.dewpoint_C = qQNaN(); data.dewpoint_F = qQNaN(); data.weather = QStringLiteral("N/A"); data.stationID = i18n("N/A"); data.pressure = qQNaN(); data.visibility = qQNaN(); data.humidity = qQNaN(); data.windSpeed = qQNaN(); data.windGust = qQNaN(); data.windchill_F = qQNaN(); data.windchill_C = qQNaN(); data.heatindex_F = qQNaN(); data.heatindex_C = qQNaN(); while (!xml.atEnd()) { xml.readNext(); const QStringRef elementName = xml.name(); if (xml.isStartElement()) { if (elementName == QLatin1String("location")) { data.locationName = xml.readElementText(); } else if (elementName == QLatin1String("station_id")) { data.stationID = xml.readElementText(); } else if (elementName == QLatin1String("latitude")) { parseDouble(data.stationLatitude, xml); } else if (elementName == QLatin1String("longitude")) { parseDouble(data.stationLongitude, xml); } else if (elementName == QLatin1String("observation_time_rfc822")) { data.observationDateTime = QDateTime::fromString(xml.readElementText(), Qt::RFC2822Date); } else if (elementName == QLatin1String("observation_time")) { data.observationTime = xml.readElementText(); QStringList tmpDateStr = data.observationTime.split(QLatin1Char(' ')); data.observationTime = QStringLiteral("%1 %2").arg(tmpDateStr[6], tmpDateStr[7]); } else if (elementName == QLatin1String("weather")) { const QString weather = xml.readElementText(); data.weather = (weather.isEmpty() || weather == QLatin1String("NA")) ? QStringLiteral("N/A") : weather; // Pick which icon set depending on period of day } else if (elementName == QLatin1String("temp_f")) { parseFloat(data.temperature_F, xml); } else if (elementName == QLatin1String("temp_c")) { parseFloat(data.temperature_C, xml); } else if (elementName == QLatin1String("relative_humidity")) { parseFloat(data.humidity, xml); } else if (elementName == QLatin1String("wind_dir")) { data.windDirection = xml.readElementText(); } else if (elementName == QLatin1String("wind_mph")) { const QString windSpeed = xml.readElementText(); if (windSpeed == QLatin1String("NA")) { data.windSpeed = 0.0; } else { parseFloat(data.windSpeed, windSpeed); } } else if (elementName == QLatin1String("wind_gust_mph")) { const QString windGust = xml.readElementText(); if (windGust == QLatin1String("NA") || windGust == QLatin1String("N/A")) { data.windGust = 0.0; } else { parseFloat(data.windGust, windGust); } } else if (elementName == QLatin1String("pressure_in")) { parseFloat(data.pressure, xml); } else if (elementName == QLatin1String("dewpoint_f")) { parseFloat(data.dewpoint_F, xml); } else if (elementName == QLatin1String("dewpoint_c")) { parseFloat(data.dewpoint_C, xml); } else if (elementName == QLatin1String("heat_index_f")) { parseFloat(data.heatindex_F, xml); } else if (elementName == QLatin1String("heat_index_c")) { parseFloat(data.heatindex_C, xml); } else if (elementName == QLatin1String("windchill_f")) { parseFloat(data.windchill_F, xml); } else if (elementName == QLatin1String("windchill_c")) { parseFloat(data.windchill_C, xml); } else if (elementName == QLatin1String("visibility_mi")) { parseFloat(data.visibility, xml); } else { parseUnknownElement(xml); } } } } // Parse Weather data main loop, from here we have to decend into each tag pair bool NOAAIon::readXMLData(const QString& source, QXmlStreamReader& xml) { WeatherData data; data.isForecastsDataPending = true; while (!xml.atEnd()) { xml.readNext(); if (xml.isEndElement()) { break; } if (xml.isStartElement()) { if (xml.name() == QLatin1String("current_observation")) { parseWeatherSite(data, xml); } else { parseUnknownElement(xml); } } } 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("%1|Solar|Latitude=%2|Longitude=%3|DateTime=%4") .arg(QString::fromUtf8(data.observationDateTime.timeZone().id())) .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); } return !xml.error(); } // handle when no XML tag is found void NOAAIon::parseUnknownElement(QXmlStreamReader& xml) const { while (!xml.atEnd()) { xml.readNext(); if (xml.isEndElement()) { break; } if (xml.isStartElement()) { parseUnknownElement(xml); } } } void NOAAIon::updateWeather(const QString& source) { const WeatherData& weatherData = m_weatherData[source]; if (weatherData.isForecastsDataPending || weatherData.isSolarDataPending) { return; } Plasma::DataEngine::Data data; data.insert(QStringLiteral("Place"), weatherData.locationName); data.insert(QStringLiteral("Station"), weatherData.stationID); const bool stationCoordValid = (!qIsNaN(weatherData.stationLatitude) && !qIsNaN(weatherData.stationLongitude)); if (stationCoordValid) { data.insert(QStringLiteral("Latitude"), weatherData.stationLatitude); data.insert(QStringLiteral("Longitude"), weatherData.stationLongitude); } // Real weather - Current conditions if (weatherData.observationDateTime.isValid()) { data.insert(QStringLiteral("Observation Timestamp"), weatherData.observationDateTime); } data.insert(QStringLiteral("Observation Period"), weatherData.observationTime); const QString conditionI18n = weatherData.weather == QLatin1String("N/A") ? i18n("N/A") : i18nc("weather condition", weatherData.weather.toUtf8().data()); data.insert(QStringLiteral("Current Conditions"), conditionI18n); qCDebug(IONENGINE_NOAA) << "i18n condition string: " << qPrintable(conditionI18n); const QString weather = weatherData.weather.toLower(); ConditionIcons condition = getConditionIcon(weather, !weatherData.isNight); data.insert(QStringLiteral("Condition Icon"), getWeatherIcon(condition)); if (!qIsNaN(weatherData.temperature_F)) { data.insert(QStringLiteral("Temperature"), weatherData.temperature_F); } // Used for all temperatures data.insert(QStringLiteral("Temperature Unit"), KUnitConversion::Fahrenheit); if (!qIsNaN(weatherData.windchill_F)) { data.insert(QStringLiteral("Windchill"), weatherData.windchill_F); } if (!qIsNaN(weatherData.heatindex_F)) { data.insert(QStringLiteral("Heat Index"), weatherData.heatindex_F); } if (!qIsNaN(weatherData.dewpoint_F)) { data.insert(QStringLiteral("Dewpoint"), weatherData.dewpoint_F); } if (!qIsNaN(weatherData.pressure)) { data.insert(QStringLiteral("Pressure"), weatherData.pressure); data.insert(QStringLiteral("Pressure Unit"), KUnitConversion::InchesOfMercury); } if (!qIsNaN(weatherData.visibility)) { data.insert(QStringLiteral("Visibility"), weatherData.visibility); data.insert(QStringLiteral("Visibility Unit"), KUnitConversion::Mile); } if (!qIsNaN(weatherData.humidity)) { data.insert(QStringLiteral("Humidity"), weatherData.humidity); data.insert(QStringLiteral("Humidity Unit"), KUnitConversion::Percent); } if (!qIsNaN(weatherData.windSpeed)) { data.insert(QStringLiteral("Wind Speed"), weatherData.windSpeed); } if (!qIsNaN(weatherData.windSpeed) || !qIsNaN(weatherData.windGust)) { data.insert(QStringLiteral("Wind Speed Unit"), KUnitConversion::MilePerHour); } if (!qIsNaN(weatherData.windGust)) { data.insert(QStringLiteral("Wind Gust"), weatherData.windGust); } if (!qIsNaN(weatherData.windSpeed) && static_cast(weatherData.windSpeed) == 0) { data.insert(QStringLiteral("Wind Direction"), QStringLiteral("VR")); // Variable/calm } else if (!weatherData.windDirection.isEmpty()) { data.insert(QStringLiteral("Wind Direction"), getWindDirectionIcon(windIcons(), weatherData.windDirection.toLower())); } // Set number of forecasts per day/night supported data.insert(QStringLiteral("Total Weather Days"), weatherData.forecasts.size()); int i = 0; for (const WeatherData::Forecast& forecast : weatherData.forecasts) { ConditionIcons icon = getConditionIcon(forecast.summary.toLower(), true); QString iconName = getWeatherIcon(icon); /* Sometimes the forecast for the later days is unavailable, if so skip remianing days * since their forecast data is probably unavailable. */ if (forecast.low.isEmpty() || forecast.high.isEmpty()) { break; } // Get the short day name for the forecast data.insert(QStringLiteral("Short Forecast Day %1").arg(i), QStringLiteral("%1|%2|%3|%4|%5|%6") .arg(forecast.day, iconName, i18nc("weather forecast", forecast.summary.toUtf8().data()), forecast.high, forecast.low, QString())); ++i; } data.insert(QStringLiteral("Credit"), i18nc("credit line, keep string short)", "Data from NOAA National\302\240Weather\302\240Service")); setData(source, data); } /** * Determine the condition icon based on the list of possible NOAA weather conditions as defined at * and * * Since the number of NOAA weather conditions need to be fitted into the narowly defined groups in IonInterface::ConditionIcons, we * try to group the NOAA conditions as best as we can based on their priorities/severity. * TODO: summaries "Hot" & "Cold" have no proper matching entry in ConditionIcons, consider extending it */ IonInterface::ConditionIcons NOAAIon::getConditionIcon(const QString& weather, bool isDayTime) const { IonInterface::ConditionIcons result; // Consider any type of storm, tornado or funnel to be a thunderstorm. if (weather.contains(QStringLiteral("thunderstorm")) || weather.contains(QStringLiteral("funnel")) || weather.contains(QStringLiteral("tornado")) || weather.contains(QStringLiteral("storm")) || weather.contains(QStringLiteral("tstms"))) { if (weather.contains(QStringLiteral("vicinity")) || weather.contains(QStringLiteral("chance"))) { result = isDayTime ? IonInterface::ChanceThunderstormDay : IonInterface::ChanceThunderstormNight; } else { result = IonInterface::Thunderstorm; } } else if (weather.contains(QStringLiteral("pellets")) || weather.contains(QStringLiteral("crystals")) || weather.contains(QStringLiteral("hail"))) { result = IonInterface::Hail; } else if (((weather.contains(QStringLiteral("rain")) || weather.contains(QStringLiteral("drizzle")) || weather.contains(QStringLiteral("showers"))) && weather.contains(QStringLiteral("snow"))) || weather.contains(QStringLiteral("wintry mix"))) { result = IonInterface::RainSnow; } else if (weather.contains(QStringLiteral("flurries"))) { result = IonInterface::Flurries; } else if (weather.contains(QStringLiteral("snow")) && weather.contains(QStringLiteral("light"))) { result = IonInterface::LightSnow; } else if (weather.contains(QStringLiteral("snow"))) { if (weather.contains(QStringLiteral("vicinity")) || weather.contains(QStringLiteral("chance"))) { result = isDayTime ? IonInterface::ChanceSnowDay : IonInterface::ChanceSnowNight; } else { result = IonInterface::Snow; } } else if (weather.contains(QStringLiteral("freezing rain"))) { result = IonInterface::FreezingRain; } else if (weather.contains(QStringLiteral("freezing drizzle"))) { result = IonInterface::FreezingDrizzle; } else if (weather.contains(QStringLiteral("cold"))) { // temperature condition has not hint about air ingredients, so let's assume chance of snow result = isDayTime ? IonInterface::ChanceSnowDay : IonInterface::ChanceSnowNight; } else if (weather.contains(QStringLiteral("showers"))) { if (weather.contains(QStringLiteral("vicinity")) || weather.contains(QStringLiteral("chance"))) { result = isDayTime ? IonInterface::ChanceShowersDay : IonInterface::ChanceShowersNight; } else { result = IonInterface::Showers; } } else if (weather.contains(QStringLiteral("light rain")) || weather.contains(QStringLiteral("drizzle"))) { result = IonInterface::LightRain; } else if (weather.contains(QStringLiteral("rain"))) { result = IonInterface::Rain; } else if (weather.contains(QStringLiteral("few clouds")) || weather.contains(QStringLiteral("mostly sunny")) || weather.contains(QStringLiteral("mostly clear")) || weather.contains(QStringLiteral("increasing clouds")) || weather.contains(QStringLiteral("becoming cloudy")) || weather.contains(QStringLiteral("clearing")) || weather.contains(QStringLiteral("decreasing clouds")) || weather.contains(QStringLiteral("becoming sunny"))) { result = isDayTime ? IonInterface::FewCloudsDay : IonInterface::FewCloudsNight; } else if (weather.contains(QStringLiteral("partly cloudy")) || weather.contains(QStringLiteral("partly sunny")) || weather.contains(QStringLiteral("partly clear"))) { result = isDayTime ? IonInterface::PartlyCloudyDay : IonInterface::PartlyCloudyNight; } else if (weather.contains(QStringLiteral("overcast")) || weather.contains(QStringLiteral("cloudy"))) { result = IonInterface::Overcast; } else if (weather.contains(QStringLiteral("haze")) || weather.contains(QStringLiteral("smoke")) || weather.contains(QStringLiteral("dust")) || weather.contains(QStringLiteral("sand"))) { result = IonInterface::Haze; } else if (weather.contains(QStringLiteral("fair")) || weather.contains(QStringLiteral("clear")) || weather.contains(QStringLiteral("sunny"))) { result = isDayTime ? IonInterface::ClearDay : IonInterface::ClearNight; } else if (weather.contains(QStringLiteral("fog"))) { result = IonInterface::Mist; } else if (weather.contains(QStringLiteral("hot"))) { // temperature condition has not hint about air ingredients, so let's assume the sky is clear when it is hot result = isDayTime ? IonInterface::ClearDay : IonInterface::ClearNight; } else { result = IonInterface::NotAvailable; } return result; } void NOAAIon::getForecast(const QString& source) { const double lat = m_weatherData[source].stationLatitude; const double lon = m_weatherData[source].stationLongitude; if (qIsNaN(lat) || qIsNaN(lon)) { return; } /* Assuming that we have the latitude and longitude data at this point, get the 7-day * forecast. */ const QUrl url(QLatin1String("https://graphical.weather.gov/xml/sample_products/browser_interface/" "ndfdBrowserClientByDay.php?lat=") + QString::number(lat) + QLatin1String("&lon=") + QString::number(lon) + QLatin1String("&format=24+hourly&numDays=7")); KIO::TransferJob* getJob = KIO::get(url, KIO::Reload, KIO::HideProgressInfo); m_jobXml.insert(getJob, new QXmlStreamReader); m_jobList.insert(getJob, source); connect(getJob, &KIO::TransferJob::data, this, &NOAAIon::forecast_slotDataArrived); connect(getJob, &KJob::result, this, &NOAAIon::forecast_slotJobFinished); } void NOAAIon::forecast_slotDataArrived(KIO::Job *job, const QByteArray &data) { if (data.isEmpty() || !m_jobXml.contains(job)) { return; } // Send to xml. m_jobXml[job]->addData(data); } void NOAAIon::forecast_slotJobFinished(KJob *job) { QXmlStreamReader *reader = m_jobXml.value(job); const QString source = m_jobList.value(job); if (reader) { readForecast(source, *reader); updateWeather(source); } m_jobList.remove(job); delete m_jobXml[job]; m_jobXml.remove(job); if (m_sourcesToReset.contains(source)) { m_sourcesToReset.removeAll(source); // so the weather engine updates it's data forceImmediateUpdateOfAllVisualizations(); // update the clients of our engine emit forceUpdate(this, source); } } void NOAAIon::readForecast(const QString& source, QXmlStreamReader& xml) { WeatherData& weatherData = m_weatherData[source]; QVector& forecasts = weatherData.forecasts; // Clear the current forecasts forecasts.clear(); while (!xml.atEnd()) { xml.readNext(); if (xml.isStartElement()) { /* Read all reported days from . We check for existence of a specific * which indicates the separate day listings. The schema defines it to be * the first item before the day listings. */ if (xml.name() == QLatin1String("layout-key") && xml.readElementText() == QLatin1String("k-p24h-n7-1")) { // Read days until we get to end of parent ()tag while (! (xml.isEndElement() && xml.name() == QLatin1String("time-layout"))) { xml.readNext(); if (xml.name() == QLatin1String("start-valid-time")) { QString data = xml.readElementText(); QDateTime date = QDateTime::fromString(data, Qt::ISODate); WeatherData::Forecast forecast; forecast.day = QLocale().toString(date.date().day()); forecasts.append(forecast); //qCDebug(IONENGINE_NOAA) << forecast.day; } } } else if (xml.name() == QLatin1String("temperature") && xml.attributes().value(QStringLiteral("type")) == QLatin1String("maximum")) { // Read max temps until we get to end tag int i = 0; while (! (xml.isEndElement() && xml.name() == QLatin1String("temperature")) && i < forecasts.count()) { xml.readNext(); if (xml.name() == QLatin1String("value")) { forecasts[i].high = xml.readElementText(); //qCDebug(IONENGINE_NOAA) << forecasts[i].high; i++; } } } else if (xml.name() == QLatin1String("temperature") && xml.attributes().value(QStringLiteral("type")) == QLatin1String("minimum")) { // Read min temps until we get to end tag int i = 0; while (! (xml.isEndElement() && xml.name() == QLatin1String("temperature")) && i < forecasts.count()) { xml.readNext(); if (xml.name() == QLatin1String("value")) { forecasts[i].low = xml.readElementText(); //qCDebug(IONENGINE_NOAA) << forecasts[i].low; i++; } } } else if (xml.name() == QLatin1String("weather")) { // Read weather conditions until we get to end tag int i = 0; while (! (xml.isEndElement() && xml.name() == QLatin1String("weather")) && i < forecasts.count()) { xml.readNext(); if (xml.name() == QLatin1String("weather-conditions") && xml.isStartElement()) { QString summary = xml.attributes().value(QStringLiteral("weather-summary")).toString(); forecasts[i].summary = summary; //qCDebug(IONENGINE_NOAA) << forecasts[i].summary; qCDebug(IONENGINE_NOAA) << "i18n summary string: " << i18nc("weather forecast", forecasts[i].summary.toUtf8().data()); i++; } } } } } weatherData.isForecastsDataPending = false; } void NOAAIon::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(noaa, NOAAIon, "ion-noaa.json") #include "ion_noaa.moc" diff --git a/dataengines/weather/ions/noaa/ion_noaa.h b/dataengines/weather/ions/noaa/ion_noaa.h index 8245abdae..ebe7afe52 100644 --- a/dataengines/weather/ions/noaa/ion_noaa.h +++ b/dataengines/weather/ions/noaa/ion_noaa.h @@ -1,183 +1,183 @@ /*************************************************************************** - * Copyright (C) 2007-2009 by Shawn Starr * + * Copyright (C) 2007-2009,2019 by Shawn Starr * * * * 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 NOAA's National Weather Service XML data */ #ifndef ION_NOAA_H #define ION_NOAA_H #include "../ion.h" #include #include #include class KJob; namespace KIO { class Job; } // namespace KIO class WeatherData { public: WeatherData(); QString locationName; QString stationID; double stationLatitude; double stationLongitude; QString stateName; // Current observation information. QString observationTime; QDateTime observationDateTime; QString weather; float temperature_F; float temperature_C; float humidity; QString windString; QString windDirection; float windSpeed; float windGust; float pressure; float dewpoint_F; float dewpoint_C; float heatindex_F; float heatindex_C; float windchill_F; float windchill_C; float visibility; struct Forecast { QString day; QString summary; QString low; QString high; }; QVector forecasts; bool isForecastsDataPending = false; QString solarDataTimeEngineSourceName; bool isNight = false; bool isSolarDataPending = false; }; Q_DECLARE_TYPEINFO(WeatherData::Forecast, Q_MOVABLE_TYPE); Q_DECLARE_TYPEINFO(WeatherData, Q_MOVABLE_TYPE); class Q_DECL_EXPORT NOAAIon : public IonInterface, public Plasma::DataEngineConsumer { Q_OBJECT public: NOAAIon(QObject *parent, const QVariantList &args); ~NOAAIon() override; public: // IonInterface API bool updateIonSource(const QString& source) override; public Q_SLOTS: // for solar data pushes from the time engine void dataUpdated(const QString& sourceName, const Plasma::DataEngine::Data& data); protected: // IonInterface API void reset() override; private Q_SLOTS: void setup_slotDataArrived(KIO::Job *, const QByteArray &); void setup_slotJobFinished(KJob *); void slotDataArrived(KIO::Job *, const QByteArray &); void slotJobFinished(KJob *); void forecast_slotDataArrived(KIO::Job *, const QByteArray &); void forecast_slotJobFinished(KJob *); private: void updateWeather(const QString& source); /* NOAA Methods - Internal for Ion */ QMap setupConditionIconMappings() const; QMap const & conditionIcons() const; QMap setupWindIconMappings() const; QMap const& windIcons() const; // Current Conditions Weather info //bool night(const QString& source); IonInterface::ConditionIcons getConditionIcon(const QString& weather, bool isDayTime) const; // Load and Parse the place XML listing void getXMLSetup() const; bool readXMLSetup(); // Load and parse the specific place(s) void getXMLData(const QString& source); bool readXMLData(const QString& source, QXmlStreamReader& xml); // Load and parse upcoming forecast for the next N days void getForecast(const QString& source); void readForecast(const QString& source, QXmlStreamReader& xml); // Check if place specified is valid or not QStringList validate(const QString& source) const; // Catchall for unknown XML tags void parseUnknownElement(QXmlStreamReader& xml) const; // Parse weather XML data void parseWeatherSite(WeatherData& data, QXmlStreamReader& xml); void parseStationID(); void parseStationList(); void parseFloat(float& value, const QString& string); void parseFloat(float& value, QXmlStreamReader& xml); void parseDouble(double& value, QXmlStreamReader& xml); private: struct XMLMapInfo { QString stateName; QString stationName; QString stationID; QString XMLurl; }; // Key dicts QHash m_places; // Weather information QHash m_weatherData; // Store KIO jobs QHash m_jobXml; QHash m_jobList; QXmlStreamReader m_xmlSetup; // bool emitWhenSetup; QStringList m_sourcesToReset; }; #endif