File indexing completed on 2024-03-24 17:23:12

0001 // SPDX-License-Identifier: GPL-3.0-or-later
0002 /*
0003   Copyright 2017,2021 Martin Koller, kollix@aon.at
0004 
0005   This file is part of liquidshell.
0006 
0007   liquidshell is free software: you can redistribute it and/or modify
0008   it under the terms of the GNU General Public License as published by
0009   the Free Software Foundation, either version 3 of the License, or
0010   (at your option) any later version.
0011 
0012   liquidshell is distributed in the hope that it will be useful,
0013   but WITHOUT ANY WARRANTY; without even the implied warranty of
0014   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
0015   GNU General Public License for more details.
0016 
0017   You should have received a copy of the GNU General Public License
0018   along with liquidshell.  If not, see <http://www.gnu.org/licenses/>.
0019 */
0020 
0021 #include <WeatherApplet.hxx>
0022 #include <WeatherAppletConfigureDialog.hxx>
0023 #include <Moon.hxx>
0024 
0025 #include <QJsonDocument>
0026 #include <QJsonObject>
0027 #include <QJsonArray>
0028 #include <QUrl>
0029 #include <QGridLayout>
0030 #include <QVBoxLayout>
0031 #include <QHBoxLayout>
0032 #include <QDir>
0033 #include <QAction>
0034 #include <QDebug>
0035 
0036 #include <KConfig>
0037 #include <KConfigGroup>
0038 #include <KLocalizedString>
0039 #include <NetworkManagerQt/Manager>
0040 
0041 //--------------------------------------------------------------------------------
0042 
0043 QString WeatherApplet::apiKey;
0044 
0045 //--------------------------------------------------------------------------------
0046 
0047 WeatherApplet::WeatherApplet(QWidget *parent, const QString &theId)
0048   : DesktopApplet(parent, theId)
0049 {
0050   setAutoFillBackground(true);
0051 
0052   timer.setInterval(600000); // 10min smallest update interval for free data
0053   connect(&timer, &QTimer::timeout, this, &WeatherApplet::fetchData);
0054 
0055   QVBoxLayout *vbox = new QVBoxLayout(this);
0056   cityLabel = new QLabel(this);
0057   cityLabel->setObjectName("city");
0058   cityLabel->setWordWrap(true);
0059 
0060   QFont f = font();
0061   f.setPointSizeF(fontInfo().pointSizeF() * 2);
0062   f.setBold(true);
0063   cityLabel->setFont(f);
0064 
0065   moonLabel = new QLabel;
0066   moon.load(":/moon_56frames.png", "PNG");
0067 
0068   QHBoxLayout *topHbox = new QHBoxLayout;
0069   topHbox->addWidget(cityLabel);
0070   topHbox->addStretch();
0071   topHbox->addWidget(moonLabel);
0072 
0073   vbox->addLayout(topHbox);
0074 
0075   QGridLayout *grid = new QGridLayout;
0076   vbox->addLayout(grid);
0077 
0078   grid->addWidget(new QLabel(i18n("Temperature:"), this), 0, 0);
0079   grid->addWidget(tempLabel = new QLabel, 0, 1);
0080 
0081   grid->addWidget(new QLabel(i18n("Pressure:"), this), 1, 0);
0082   grid->addWidget(pressureLabel = new QLabel, 1, 1);
0083 
0084   grid->addWidget(new QLabel(i18n("Humidity:"), this), 2, 0);
0085   grid->addWidget(humidityLabel = new QLabel, 2, 1);
0086 
0087   grid->addWidget(new QLabel(i18n("Wind Speed:"), this), 3, 0);
0088   grid->addWidget(windSpeedLabel = new QLabel, 3, 1);
0089 
0090   grid->addWidget(new QLabel(i18n("Wind Direction:"), this), 4, 0);
0091   grid->addWidget(windDirectionLabel = new QLabel, 4, 1);
0092 
0093   for (int i = 0; i < 4; i++)
0094   {
0095     shortForecast[i] = new ForecastWidget(this, false);
0096     grid->addWidget(shortForecast[i], 0, 2 + i, 5, 1, Qt::AlignCenter);
0097   }
0098 
0099   QHBoxLayout *hbox = new QHBoxLayout;
0100   vbox->addLayout(hbox);
0101 
0102   for (int i = 0; i < 5; i++)
0103   {
0104     forecast[i] = new ForecastWidget(this);
0105     hbox->addWidget(forecast[i]);
0106 
0107     if ( i < 4 )
0108       hbox->addStretch();
0109   }
0110 
0111   connect(NetworkManager::notifier(), &NetworkManager::Notifier::connectivityChanged, this,
0112           [this](NetworkManager::Connectivity connectivity)
0113           {
0114             if ( connectivity == NetworkManager::Full )
0115               fetchData();
0116           });
0117 }
0118 
0119 //--------------------------------------------------------------------------------
0120 
0121 QSize WeatherApplet::sizeHint() const
0122 {
0123   return QSize(700, 300);
0124 }
0125 
0126 //--------------------------------------------------------------------------------
0127 
0128 void WeatherApplet::loadConfig()
0129 {
0130   KConfig config;
0131   KConfigGroup group = config.group("Weather");
0132   apiKey = group.readEntry("apiKey", QString());
0133   group = config.group(id);
0134   cityId = group.readEntry("cityId", QString());
0135   units = group.readEntry("units", QString("metric"));
0136 
0137   DesktopApplet::loadConfig();
0138 
0139   if ( apiKey.isEmpty() || cityId.isEmpty() )
0140     cityLabel->setText(i18n("Not configured"));
0141 }
0142 
0143 //--------------------------------------------------------------------------------
0144 
0145 void WeatherApplet::showEvent(QShowEvent *)
0146 {
0147   // only query every 10 minutes, which is the limit for free data
0148   if ( !timer.isActive() )
0149   {
0150     timer.start();
0151     fetchData();
0152   }
0153 }
0154 
0155 //--------------------------------------------------------------------------------
0156 
0157 void WeatherApplet::fetchData()
0158 {
0159   if ( !isVisible() )
0160     return;
0161 
0162   int x = 48 * Moon::phase(QDate::currentDate());
0163   moonLabel->setPixmap(moon.copy(x, 0, 48, 48));
0164 
0165   if ( apiKey.isEmpty() || cityId.isEmpty() )
0166     return;
0167 
0168   QString url = QString("http://api.openweathermap.org/data/2.5/weather?APPID=%1&units=%2&id=%3")
0169                         .arg(apiKey, units, cityId);
0170 
0171   KIO::StoredTransferJob *job = KIO::storedGet(QUrl(url), KIO::Reload, KIO::HideProgressInfo);
0172   connect(job, &KIO::Job::result, this, &WeatherApplet::gotData);
0173 
0174   url = QString("http://api.openweathermap.org/data/2.5/forecast?APPID=%1&units=%2&id=%3")
0175                 .arg(apiKey, units, cityId);
0176 
0177   job = KIO::storedGet(QUrl(url), KIO::Reload, KIO::HideProgressInfo);
0178   connect(job, &KIO::Job::result, this, &WeatherApplet::gotData);
0179 }
0180 
0181 //--------------------------------------------------------------------------------
0182 
0183 void WeatherApplet::gotData(KJob *job)
0184 {
0185   if ( job->error() )
0186   {
0187     cityLabel->setText(job->errorString());
0188     return;
0189   }
0190 
0191   QJsonDocument doc = QJsonDocument::fromJson(static_cast<KIO::StoredTransferJob *>(job)->data());
0192   if ( doc.isNull() || !doc.isObject() )
0193     return;
0194 
0195   QString tempUnit = i18n("K");
0196   if ( units == "metric" ) tempUnit = i18n("°C");
0197   else if ( units == "imperial" ) tempUnit = i18n("°F");
0198 
0199   QJsonObject data = doc.object();
0200 
0201   if ( data.contains("city") && data["city"].isObject() )
0202     cityLabel->setText(data["city"].toObject()["name"].toString());
0203 
0204   // current
0205   if ( data.contains("main") && data["main"].isObject() )
0206   {
0207     QJsonObject mainData = data["main"].toObject();
0208     double temp = mainData["temp"].toDouble();
0209 
0210     tempLabel->setText(i18n("%1 %2", locale().toString(temp, 'f', 1), tempUnit));
0211 
0212     double pressure = mainData["pressure"].toDouble();
0213     pressureLabel->setText(i18n("%1 hPa", locale().toString(pressure, 'f', 1)));
0214 
0215     double humidity = mainData["humidity"].toDouble();
0216     humidityLabel->setText(i18n("%1 %", locale().toString(humidity, 'f', 1)));
0217   }
0218 
0219   if ( data.contains("wind") && data["wind"].isObject() )
0220   {
0221     QJsonObject windData = data["wind"].toObject();
0222 
0223     QString speedUnit = "m/s";
0224     if ( units == "imperial" ) speedUnit = "mi/h";
0225 
0226     double speed = windData["speed"].toDouble();
0227     windSpeedLabel->setText(i18n("%1 %2", locale().toString(speed, 'f', 0), speedUnit));
0228 
0229     double deg = windData["deg"].toDouble();
0230     windDirectionLabel->setText(i18n("%1 °", locale().toString(deg, 'f', 0)));
0231   }
0232 
0233   if ( data.contains("weather") && data["weather"].isArray() )
0234   {
0235     QDateTime dt = QDateTime::fromMSecsSinceEpoch(qint64(data["dt"].toInt()) * 1000);
0236     shortForecast[0]->day->setText(dt.time().toString(Qt::SystemLocaleShortDate));
0237     setIcon(shortForecast[0]->icon, data["weather"].toArray()[0].toObject()["icon"].toString());
0238   }
0239 
0240   // forecast
0241   if ( data.contains("list") && data["list"].isArray() )
0242   {
0243     for (int i = 0; i < 5; i++)
0244       forecast[i]->hide();
0245 
0246     QJsonArray array = data["list"].toArray();
0247 
0248     // 3 hours short forecast
0249     for (int i = 0; i < 3; i++)
0250     {
0251       setIcon(shortForecast[1 + i]->icon, array[i].toObject()["weather"].toArray()[0].toObject()["icon"].toString());
0252       QDateTime dt = QDateTime::fromMSecsSinceEpoch(qint64(array[i].toObject()["dt"].toInt()) * 1000);
0253       shortForecast[1 + i]->day->setText(dt.time().toString(Qt::SystemLocaleShortDate));
0254       shortForecast[1 + i]->show();
0255     }
0256 
0257     QHash<int, double> minTemp, maxTemp; // key = day
0258 
0259     for (QJsonValue value : array)
0260     {
0261       QJsonObject obj = value.toObject();
0262 
0263       int day = QDateTime::fromMSecsSinceEpoch(qint64(obj["dt"].toInt()) * 1000).date().dayOfWeek();
0264       double temp = obj["main"].toObject()["temp"].toDouble();
0265 
0266       if ( !minTemp.contains(day) )
0267       {
0268         minTemp.insert(day, temp);
0269         maxTemp.insert(day, temp);
0270       }
0271       else
0272       {
0273         if ( temp < minTemp[day] ) minTemp[day] = temp;
0274         if ( temp > maxTemp[day] ) maxTemp[day] = temp;
0275       }
0276     }
0277 
0278     int idx = 0;
0279     for (QJsonValue value : array)
0280     {
0281       QJsonObject obj = value.toObject();
0282 
0283       if ( obj["dt_txt"].toString().contains("12:00") )
0284       {
0285         QString icon = obj["weather"].toArray()[0].toObject()["icon"].toString();
0286         setIcon(forecast[idx]->icon, icon);
0287 
0288         int day = QDateTime::fromMSecsSinceEpoch(qint64(obj["dt"].toInt()) * 1000).date().dayOfWeek();
0289         forecast[idx]->day->setText(locale().dayName(day, QLocale::ShortFormat));
0290         forecast[idx]->min->setText(i18n("%1 %2", locale().toString(minTemp[day], 'f', 1), tempUnit));
0291         forecast[idx]->max->setText(i18n("%1 %2", locale().toString(maxTemp[day], 'f', 1), tempUnit));
0292         forecast[idx]->show();
0293         idx++;
0294         if ( idx == 5 ) break;
0295       }
0296     }
0297   }
0298 
0299   timer.start();  // after showEvent make sure to wait another full timeout phase
0300 }
0301 
0302 //--------------------------------------------------------------------------------
0303 
0304 void WeatherApplet::setIcon(QLabel *label, const QString &icon)
0305 {
0306   QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
0307                      "/api.openweathermap.org/";
0308   QDir dir;
0309   dir.mkpath(cacheDir);
0310   QString filePath = cacheDir + icon + ".png";
0311 
0312   if ( QFile::exists(filePath) )
0313   {
0314     QPixmap pixmap(filePath);
0315     if ( !pixmap.isNull() )
0316       label->setPixmap(pixmap);
0317   }
0318   else
0319   {
0320     KIO::StoredTransferJob *job =
0321         KIO::storedGet(QUrl("http://api.openweathermap.org/img/w/" + icon), KIO::Reload, KIO::HideProgressInfo);
0322 
0323     connect(job, &KIO::Job::result, this,
0324             [label, filePath](KJob *job)
0325             {
0326               if ( job->error() )
0327                 return;
0328 
0329               QPixmap pixmap;
0330               pixmap.loadFromData(static_cast<KIO::StoredTransferJob *>(job)->data());
0331               if ( !pixmap.isNull() )
0332               {
0333                 label->setPixmap(pixmap);
0334                 pixmap.save(filePath, "PNG");
0335               }
0336             });
0337   }
0338 }
0339 
0340 //--------------------------------------------------------------------------------
0341 //--------------------------------------------------------------------------------
0342 //--------------------------------------------------------------------------------
0343 
0344 ForecastWidget::ForecastWidget(QWidget *parent, bool showMinMax)
0345   : QWidget(parent)
0346 {
0347   QGridLayout *grid = new QGridLayout(this);
0348 
0349   if ( showMinMax )
0350   {
0351     min = new QLabel(this);
0352     max = new QLabel(this);
0353 
0354     min->setAlignment(Qt::AlignRight);
0355     max->setAlignment(Qt::AlignRight);
0356 
0357     grid->addWidget(max, 0, 1);
0358     grid->addWidget(min, 1, 1);
0359   }
0360 
0361   day = new QLabel(this);
0362   icon = new QLabel(this);
0363 
0364   day->setAlignment(Qt::AlignCenter);
0365 
0366   icon->setFixedSize(64, 64);
0367   icon->setScaledContents(true);
0368 
0369   grid->addWidget(day, 2, 0, 1, 2);
0370   grid->addWidget(icon, 0, 0, 2, 1);
0371 
0372   setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
0373 }
0374 
0375 //--------------------------------------------------------------------------------
0376 
0377 void WeatherApplet::configure()
0378 {
0379   if ( dialog )
0380   {
0381     dialog->raise();
0382     dialog->activateWindow();
0383     return;
0384   }
0385 
0386   dialog = new WeatherAppletConfigureDialog(this);
0387   dialog->setWindowTitle(i18n("Configure Weather Applet"));
0388 
0389   dialog->setAttribute(Qt::WA_DeleteOnClose);
0390   dialog->show();
0391 
0392   connect(dialog.data(), &QDialog::accepted, this,
0393           [this]()
0394           {
0395             saveConfig();
0396 
0397             KConfig config;
0398             KConfigGroup group = config.group("Weather");
0399             group.writeEntry("apiKey", apiKey);
0400             group = config.group(id);
0401             group.writeEntry("cityId", cityId);
0402             group.writeEntry("units", units);
0403 
0404             if ( !apiKey.isEmpty() && !cityId.isEmpty() )
0405             {
0406               fetchData();
0407               timer.start();
0408             }
0409             else
0410             {
0411               cityLabel->setText(i18n("Not configured"));
0412             }
0413           });
0414 }
0415 
0416 //--------------------------------------------------------------------------------