weatherapplet.cpp 19.5 KB
Newer Older
1 2 3
/***************************************************************************
 *   Copyright (C) 2007-2009 by Shawn Starr <shawn.starr@rogers.com>       *
 *   Copyright (C) 2008 by Marco Martin <notmart@gmail.com>                *
Luís Gabriel Lima's avatar
Luís Gabriel Lima committed
4
 *   Copyright (C) 2012 by Luís Gabriel Lima <lampih@gmail.com>            *
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
 *                                                                         *
 *   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 .        *
 ***************************************************************************/

#include "weatherapplet.h"
Davide Bettio's avatar
Davide Bettio committed
23

24
#include <KLocalizedString>
Luís Gabriel Lima's avatar
Cleanup  
Luís Gabriel Lima committed
25
#include <KIconLoader>
26
#include <KConfigGroup>
27
#include <KUnitConversion/Value>
Davide Bettio's avatar
Davide Bettio committed
28

Luís Gabriel Lima's avatar
Luís Gabriel Lima committed
29
#include <Plasma/Package>
30

Christoph Feck's avatar
Christoph Feck committed
31 32
#include <cmath>

Luís Gabriel Lima's avatar
Cleanup  
Luís Gabriel Lima committed
33 34
template <typename T>
T clampValue(T value, int decimals)
35
{
Luís Gabriel Lima's avatar
Cleanup  
Luís Gabriel Lima committed
36 37
    const T mul = std::pow(static_cast<T>(10), decimals);
    return int(value * mul) / mul;
38
}
39

40
namespace {
41 42 43 44 45 46
namespace AppletConfigKeys {
inline QString services() { return QStringLiteral("services"); }
}
namespace StorageConfigKeys {
const char weatherServiceProviders[] = "weatherServiceProviders";
}
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
namespace PanelModelKeys {
inline QString location()                  { return QStringLiteral("location"); }
inline QString currentDayLowTemperature()  { return QStringLiteral("currentDayLowTemperature"); }
inline QString currentDayHighTemperature() { return QStringLiteral("currentDayHighTemperature"); }
inline QString currentConditions()         { return QStringLiteral("currentConditions"); }
inline QString currentTemperature()        { return QStringLiteral("currentTemperature"); }
inline QString currentConditionIcon()      { return QStringLiteral("currentConditionIcon"); }
inline QString totalDays()                 { return QStringLiteral("totalDays"); }
inline QString courtesy()                  { return QStringLiteral("courtesy"); }
inline QString creditUrl()                 { return QStringLiteral("creditUrl"); }
}
namespace NoticesKeys {
inline QString description() { return QStringLiteral("description"); }
inline QString info()        { return QStringLiteral("info"); }
}
}

64 65 66 67 68
/**
 * Returns the @p iconName if the current icon theme contains an icon with that name,
 * otherwise returns "weather-not-available" (expecting the icon theme to have that in any case).
 */
QString existingWeatherIconName(const QString &iconName)
69
{
70 71
    const bool isValid = !iconName.isEmpty() &&
           !KIconLoader::global()->loadIcon(iconName, KIconLoader::Desktop, 0,
72
                                            KIconLoader::DefaultState, QStringList(), nullptr, true).isNull();
73
    return isValid ? iconName : QStringLiteral("weather-not-available");
74 75
}

Luís Gabriel Lima's avatar
Cleanup  
Luís Gabriel Lima committed
76

77
WeatherApplet::WeatherApplet(QObject *parent, const QVariantList &args)
78
    : Plasma::WeatherPopupApplet(parent, args)
79 80 81
{
}

Petri Damstén's avatar
Petri Damstén committed
82 83
void WeatherApplet::init()
{
84 85
    resetPanelModel();

86
    Plasma::WeatherPopupApplet::init();
87 88 89 90 91 92
}

WeatherApplet::~WeatherApplet()
{
}

93
QString WeatherApplet::convertTemperature(const KUnitConversion::Unit& format, const QVariant& value,
Luís Gabriel Lima's avatar
Cleanup  
Luís Gabriel Lima committed
94
                                          int type, bool rounded, bool degreesOnly)
95
{
96 97 98
    return convertTemperature(format, value.toFloat(), type, rounded, degreesOnly);
}

99
QString WeatherApplet::convertTemperature(const KUnitConversion::Unit& format, float value,
100 101 102
                                          int type, bool rounded, bool degreesOnly)
{
    KUnitConversion::Value v(value, static_cast<KUnitConversion::UnitId>(type));
103
    v = v.convertTo(format);
104

105 106
    const QString unit = degreesOnly ? i18nc("Degree, unit symbol", "°") : v.unit().symbol();

107
    if (rounded) {
108
        int tempNumber = qRound(v.number());
109
        return i18nc("temperature, unit", "%1%2", tempNumber, unit);
110
    }
111 112 113

    const QString formattedTemp = QLocale().toString(clampValue(v.number(), 1), 'f', 1);
    return i18nc("temperature, unit", "%1%2", formattedTemp, unit);
114 115
}

116 117 118 119 120
bool WeatherApplet::isValidData(const QString &data) const
{
    return (!data.isEmpty() && data != i18n("N/A"));
}

Shawn Starr's avatar
Shawn Starr committed
121
bool WeatherApplet::isValidData(const QVariant &data) const
122
{
123
    return isValidData(data.toString());
124 125
}

126 127
void WeatherApplet::resetPanelModel()
{
128 129 130 131 132 133 134 135 136
    m_panelModel[PanelModelKeys::location()] = QString();
    m_panelModel[PanelModelKeys::currentDayLowTemperature()] = QString();
    m_panelModel[PanelModelKeys::currentDayHighTemperature()] = QString();
    m_panelModel[PanelModelKeys::currentConditions()] = QString();
    m_panelModel[PanelModelKeys::currentTemperature()] = QString();
    m_panelModel[PanelModelKeys::currentConditionIcon()] = QString();
    m_panelModel[PanelModelKeys::totalDays()] = QString();
    m_panelModel[PanelModelKeys::courtesy()] = QString();
    m_panelModel[PanelModelKeys::creditUrl()] = QString();
137 138 139 140 141
}

void WeatherApplet::updatePanelModel(const Plasma::DataEngine::Data &data)
{
    resetPanelModel();
142

143
    m_panelModel[PanelModelKeys::location()] = data[QStringLiteral("Place")].toString();
144

145
    const int reportTemperatureUnit = data[QStringLiteral("Temperature Unit")].toInt();
146 147
    const KUnitConversion::Unit displayTemperatureUnit = temperatureUnit();

148
    // Get current time period of day
149
    const QStringList fiveDayTokens = data[QStringLiteral("Short Forecast Day 0")].toString().split(QLatin1Char('|'));
150

151
    if (fiveDayTokens.count() == 6) {
152

153
        const QString& reportLowString = fiveDayTokens[4];
154
        if (reportLowString != QLatin1String("N/A") && !reportLowString.isEmpty()) {
155
            m_panelModel[PanelModelKeys::currentDayLowTemperature()] =
156
                convertTemperature(displayTemperatureUnit, reportLowString, reportTemperatureUnit, true);
157 158
        }

159
        const QString& reportHighString = fiveDayTokens[3];
160
        if (reportHighString != QLatin1String("N/A") && !reportHighString.isEmpty()) {
161
            m_panelModel[PanelModelKeys::currentDayHighTemperature()] =
162
                convertTemperature(displayTemperatureUnit, reportHighString, reportTemperatureUnit, true);
163 164 165
        }
    }

166
    m_panelModel[PanelModelKeys::currentConditions()] = data[QStringLiteral("Current Conditions")].toString().trimmed();
167

168
    const QVariant temperature = data[QStringLiteral("Temperature")];
169
    if (isValidData(temperature)) {
170
        m_panelModel[PanelModelKeys::currentTemperature()] = convertTemperature(displayTemperatureUnit, temperature, reportTemperatureUnit);
171 172
    }

173
    const QString conditionIconName = data[QStringLiteral("Condition Icon")].toString();
174
    QString weatherIconName;
175 176 177 178 179
    // specific icon?
    if (!conditionIconName.isEmpty() &&
        conditionIconName != QLatin1String("weather-none-available") &&
        conditionIconName != QLatin1String("N/U") && // TODO: N/U and N/A should not be used here, fix dataengines
        conditionIconName != QLatin1String("N/A")) {
180

181
        weatherIconName = existingWeatherIconName(conditionIconName);
182
    } else {
183 184 185
        // icon to use from current weather forecast?
        if (fiveDayTokens.count() == 6 && fiveDayTokens[1] != QLatin1String("N/U")) {
            // show the current weather
186
            weatherIconName = existingWeatherIconName(fiveDayTokens[1]);
187
        } else {
188
            weatherIconName = QStringLiteral("weather-none-available");
189 190
        }
    }
191
    m_panelModel[PanelModelKeys::currentConditionIcon()] = weatherIconName;
192

193 194
    m_panelModel[PanelModelKeys::courtesy()] =  data[QStringLiteral("Credit")].toString();
    m_panelModel[PanelModelKeys::creditUrl()] = data[QStringLiteral("Credit Url")].toString();
195 196
}

197 198
void WeatherApplet::updateFiveDaysModel(const Plasma::DataEngine::Data &data)
{
199 200
    const int foreCastDayCount = data[QStringLiteral("Total Weather Days")].toInt();
    if (foreCastDayCount <= 0) {
201 202 203 204 205
        return;
    }

    m_fiveDaysModel.clear();

206 207 208
    const int reportTemperatureUnit = data[QStringLiteral("Temperature Unit")].toInt();
    const KUnitConversion::Unit displayTemperatureUnit = temperatureUnit();

209 210 211 212 213
    QStringList dayItems;
    QStringList conditionItems; // Icon
    QStringList hiItems;
    QStringList lowItems;

214 215 216
    for (int i = 0; i < foreCastDayCount; ++i) {
        const QString foreCastDayKey = QStringLiteral("Short Forecast Day %1").arg(i);
        const QStringList fiveDayTokens = data[foreCastDayKey].toString().split(QLatin1Char('|'));
217 218 219 220 221 222

        if (fiveDayTokens.count() != 6) {
            // We don't have the right number of tokens, abort trying
            break;
        }

Luís Gabriel Lima's avatar
Luís Gabriel Lima committed
223
        dayItems << fiveDayTokens[0];
224 225

        // If we see N/U (Not Used) we skip the item
226
        const QString& weatherIconName = fiveDayTokens[1];
227
        if (weatherIconName != QLatin1String("N/U") && !weatherIconName.isEmpty()) {
228 229 230 231 232 233 234
            QString iconAndToolTip = existingWeatherIconName(weatherIconName);

            iconAndToolTip += QLatin1Char('|');

            const QString& condition = fiveDayTokens[2];
            const QString& probability = fiveDayTokens[5];
            if (probability != QLatin1String("N/U") &&
235 236
                probability != QLatin1String("N/A") &&
                !probability.isEmpty()) {
237 238
                iconAndToolTip += i18nc("certain weather condition (probability percentage)",
                                        "%1 (%2%)", condition, probability);
239
            } else {
240
                iconAndToolTip += condition;
241 242
            }
            conditionItems << iconAndToolTip;
243 244
        }

245 246
        const QString& tempHigh = fiveDayTokens[3];
        if (tempHigh != QLatin1String("N/U")) {
247
            if (tempHigh == QLatin1String("N/A") || tempHigh.isEmpty()) {
248 249
                hiItems << i18nc("Short for no data available", "-");
            } else {
250 251 252
                hiItems << convertTemperature(displayTemperatureUnit,
                                              tempHigh,
                                              reportTemperatureUnit,
253 254 255 256
                                              true);
            }
        }

257 258
        const QString& tempLow = fiveDayTokens[4];
        if (tempLow != QLatin1String("N/U")) {
259
            if (tempLow == QLatin1String("N/A") || tempLow.isEmpty()) {
260 261
                lowItems << i18nc("Short for no data available", "-");
            } else {
262 263 264
                lowItems << convertTemperature(displayTemperatureUnit,
                                               tempLow,
                                               reportTemperatureUnit,
265 266 267 268 269
                                               true);
            }
        }
    }

270
    if (!dayItems.isEmpty()) {
271 272
        m_fiveDaysModel << dayItems;
    }
273
    if (!conditionItems.isEmpty()) {
274 275
        m_fiveDaysModel << conditionItems;
    }
276
    if (!hiItems.isEmpty())  {
277 278
        m_fiveDaysModel << hiItems;
    }
279
    if (!lowItems.isEmpty()) {
280 281
        m_fiveDaysModel << lowItems;
    }
Luís Gabriel Lima's avatar
Luís Gabriel Lima committed
282

283 284
    m_panelModel[PanelModelKeys::totalDays()] = i18ncp("Forecast period timeframe", "1 Day",
                                                       "%1 Days", foreCastDayCount);
285 286
}

287 288 289 290
void WeatherApplet::updateDetailsModel(const Plasma::DataEngine::Data &data)
{
    m_detailsModel.clear();

291
    QLocale locale;
292 293 294 295
    const QString textId = QStringLiteral("text");
    const QString iconId = QStringLiteral("icon");

    // reused map for each row
296
    QVariantMap row;
297 298
    row.insert(iconId, QString());
    row.insert(textId, QString());
299

300 301
    const int reportTemperatureUnit = data[QStringLiteral("Temperature Unit")].toInt();
    const KUnitConversion::Unit displayTemperatureUnit = temperatureUnit();
302

303
    const QVariant windChill = data[QStringLiteral("Windchill")];
304
    if (isValidData(windChill)) {
305 306
        // Use temperature unit to convert windchill temperature
        // we only show degrees symbol not actual temperature unit
307 308 309
        const QString temp = convertTemperature(displayTemperatureUnit, windChill, reportTemperatureUnit, false, true);
        row[textId] = i18nc("windchill, unit", "Windchill: %1", temp);

310 311 312
        m_detailsModel << row;
    }

313
    const QString humidex = data[QStringLiteral("Humidex")].toString();
314
    if (isValidData(humidex)) {
315
        // TODO: this seems wrong, does the humidex have temperature as units?
316 317
        // Use temperature unit to convert humidex temperature
        // we only show degrees symbol not actual temperature unit
318 319 320
        QString temp = convertTemperature(displayTemperatureUnit, humidex, reportTemperatureUnit, false, true);
        row[textId] = i18nc("humidex, unit","Humidex: %1", temp);

321 322 323
        m_detailsModel << row;
    }

324
    const QVariant dewpoint = data[QStringLiteral("Dewpoint")];
325 326 327 328
    if (isValidData(dewpoint)) {
        QString temp = convertTemperature(displayTemperatureUnit, dewpoint, reportTemperatureUnit);
        row[textId] = i18nc("ground temperature, unit", "Dewpoint: %1", temp);

329 330 331
        m_detailsModel << row;
    }

332
    const QVariant pressure = data[QStringLiteral("Pressure")];
333 334
    if (isValidData(pressure)) {
        KUnitConversion::Value v(pressure.toDouble(),
335
                                 static_cast<KUnitConversion::UnitId>(data[QStringLiteral("Pressure Unit")].toInt()));
336
        v = v.convertTo(pressureUnit());
337
        row[textId] = i18nc("pressure, unit","Pressure: %1 %2",
338
                            locale.toString(clampValue(v.number(), 2), 'f', 2), v.unit().symbol());
339

340 341 342
        m_detailsModel << row;
    }

343
    const QString pressureTendency = data[QStringLiteral("Pressure Tendency")].toString();
344
    if (isValidData(pressureTendency)) {
345
        const QString i18nPressureTendency = i18nc("pressure tendency", pressureTendency.toUtf8().data());
346
        row[textId] = i18nc("pressure tendency, rising/falling/steady",
347
                            "Pressure Tendency: %1", i18nPressureTendency);
348

349 350 351
        m_detailsModel << row;
    }

352
    const QVariant visibility = data[QStringLiteral("Visibility")];
353
    if (isValidData(visibility)) {
354
        const KUnitConversion::UnitId unitId = static_cast<KUnitConversion::UnitId>(data[QStringLiteral("Visibility Unit")].toInt());
355 356
        if (unitId != KUnitConversion::NoUnit) {
            KUnitConversion::Value v(visibility.toDouble(), unitId);
357
            v = v.convertTo(visibilityUnit());
358
            row[textId] = i18nc("distance, unit","Visibility: %1 %2",
359
                                locale.toString(clampValue(v.number(), 1), 'f', 1), v.unit().symbol());
360
        } else {
361
            row[textId] = i18nc("visibility from distance", "Visibility: %1", visibility.toString());
362 363 364 365 366
        }

        m_detailsModel << row;
    }

367
    const QVariant humidity = data[QStringLiteral("Humidity")];
368 369
    if (isValidData(humidity)) {
        row[textId] = i18nc("content of water in air", "Humidity: %1%2",
370
                            locale.toString(clampValue(humidity.toFloat(), 0), 'f', 0), i18nc("Percent, measure unit", "%"));
371

372 373 374
        m_detailsModel << row;
    }

375
    const QVariant windSpeed = data[QStringLiteral("Wind Speed")];
376 377
    if (isValidData(windSpeed)) {
        // TODO: missing check for windDirection validness
378
        const QString windDirection = data[QStringLiteral("Wind Direction")].toString();
379
        row[iconId] = windDirection;
380

381 382 383 384 385
        bool isNumeric;
        const double windSpeedNumeric = windSpeed.toDouble(&isNumeric);
        if (isNumeric) {
            if (windSpeedNumeric != 0) {
                KUnitConversion::Value v(windSpeedNumeric,
386
                                        static_cast<KUnitConversion::UnitId>(data[QStringLiteral("Wind Speed Unit")].toInt()));
387
                v = v.convertTo(speedUnit());
388 389
                const QString i18nWindDirection = i18nc("wind direction", windDirection.toUtf8().data());
                row[textId] = i18nc("wind direction, speed","%1 %2 %3", i18nWindDirection,
390
                                    locale.toString(clampValue(v.number(), 1), 'f', 1), v.unit().symbol());
391 392 393
            } else {
                row[textId] = i18nc("Wind condition", "Calm");
            }
394
        } else {
395
            row[textId] = windSpeed.toString();
396 397 398
        }

        m_detailsModel << row;
399
        row[iconId] = QString(); // reset
400 401
    }

402
    const QVariant windGust = data[QStringLiteral("Wind Gust")];
403
    if (isValidData(windGust)) {
404
        // Convert the wind format for nonstandard types
405
        KUnitConversion::Value v(windGust.toDouble(),
406
                                 static_cast<KUnitConversion::UnitId>(data[QStringLiteral("Wind Speed Unit")].toInt()));
407
        v = v.convertTo(speedUnit());
408
        row[textId] = i18nc("winds exceeding wind speed briefly", "Wind Gust: %1 %2",
409
                            locale.toString(clampValue(v.number(), 1), 'f', 1), v.unit().symbol());
410

411 412 413 414
        m_detailsModel << row;
    }
}

Luís Gabriel Lima's avatar
Luís Gabriel Lima committed
415 416 417 418 419
void WeatherApplet::updateNoticesModel(const Plasma::DataEngine::Data &data)
{
    m_noticesModel.clear();

    QVariantList warnings;
420 421 422 423 424 425 426
    const int warningsCount = data[QStringLiteral("Total Warnings Issued")].toInt();
    warnings.reserve(warningsCount);
    for (int i = 0; i < warningsCount; ++i) {
        warnings << QVariantMap {
            { NoticesKeys::description(), data[QStringLiteral("Warning Description %1").arg(i)] },
            { NoticesKeys::info(),        data[QStringLiteral("Warning Info %1").arg(i)] },
        };
Luís Gabriel Lima's avatar
Luís Gabriel Lima committed
427 428 429 430
    }
    m_noticesModel << QVariant(warnings);

    QVariantList watches;
431 432 433 434 435 436 437
    const int watchesCount = data[QStringLiteral("Total Watches Issued")].toInt();
    watches.reserve(watchesCount);
    for (int i = 0; i < watchesCount; ++i) {
        watches << QVariantMap {
            { NoticesKeys::description(), data[QStringLiteral("Watch Description %1").arg(i)] },
            { NoticesKeys::info(),        data[QStringLiteral("Watch Info %1").arg(i)] },
        };
Luís Gabriel Lima's avatar
Luís Gabriel Lima committed
438 439 440 441
    }
    m_noticesModel << QVariant(watches);
}

442 443 444 445 446 447
void WeatherApplet::dataUpdated(const QString &source, const Plasma::DataEngine::Data &data)
{
    if (data.isEmpty()) {
        return;
    }

448
    updatePanelModel(data);
449
    updateFiveDaysModel(data);
450
    updateDetailsModel(data);
Luís Gabriel Lima's avatar
Luís Gabriel Lima committed
451
    updateNoticesModel(data);
452
    WeatherPopupApplet::dataUpdated(source, data);
453

454
    emit modelUpdated();
455 456
}

457 458 459 460 461 462 463 464 465 466
QVariantMap WeatherApplet::configValues() const
{
    QVariantMap config = WeatherPopupApplet::configValues();

    KConfigGroup cfg = this->config();
    config.insert(AppletConfigKeys::services(), cfg.readEntry(StorageConfigKeys::weatherServiceProviders, QStringList()));

    return config;
}

467
void WeatherApplet::saveConfig(const QVariantMap& configChanges)
468
{
469
    // TODO: if just units where changed there is no need to reset the complete model or reconnect to engine
470 471 472
    resetPanelModel();
    m_fiveDaysModel.clear();
    m_detailsModel.clear();
473

474
    emit modelUpdated();
475

476 477 478 479 480 481 482
    KConfigGroup cfg = config();

    auto it = configChanges.find(AppletConfigKeys::services());
    if (it != configChanges.end()) {
        cfg.writeEntry(StorageConfigKeys::weatherServiceProviders, it.value().toStringList());
    }

483
    WeatherPopupApplet::saveConfig(configChanges);
484
}
485

486 487 488
K_EXPORT_PLASMA_APPLET_WITH_JSON(weather, WeatherApplet, "metadata.json")

#include "weatherapplet.moc"