File indexing completed on 2024-04-21 05:46:40
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 //--------------------------------------------------------------------------------