From fb516d1ef1fa27410fd8d4a6d0ed7e7fc5858cc8 Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Mon, 6 Jun 2022 10:53:10 +0100 Subject: [PATCH] ADS-B: Add support for displaying airport weather (METARs) from CheckWX --- .../demodadsb/adsbdemoddisplaydialog.cpp | 6 +- .../demodadsb/adsbdemoddisplaydialog.ui | 335 +++++++++--------- plugins/channelrx/demodadsb/adsbdemodgui.cpp | 66 +++- plugins/channelrx/demodadsb/adsbdemodgui.h | 26 ++ .../channelrx/demodadsb/adsbdemodsettings.cpp | 9 +- .../channelrx/demodadsb/adsbdemodsettings.h | 3 +- plugins/channelrx/demodadsb/readme.md | 8 +- sdrbase/CMakeLists.txt | 2 + sdrbase/util/aviationweather.cpp | 245 +++++++++++++ sdrbase/util/aviationweather.h | 152 ++++++++ 10 files changed, 682 insertions(+), 170 deletions(-) create mode 100644 sdrbase/util/aviationweather.cpp create mode 100644 sdrbase/util/aviationweather.h diff --git a/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.cpp b/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.cpp index bec7c32de..cf9ce77ae 100644 --- a/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.cpp +++ b/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.cpp @@ -35,7 +35,8 @@ ADSBDemodDisplayDialog::ADSBDemodDisplayDialog(ADSBDemodSettings *settings, QWid ui->units->setCurrentIndex((int)settings->m_siUnits); ui->displayStats->setChecked(settings->m_displayDemodStats); ui->autoResizeTableColumns->setChecked(settings->m_autoResizeTableColumns); - ui->apiKey->setText(settings->m_apiKey); + ui->aviationstackAPIKey->setText(settings->m_aviationstackAPIKey); + ui->checkWXAPIKey->setText(settings->m_checkWXAPIKey); for (const auto& airspace: settings->m_airspaces) { QList items = ui->airspaces->findItems(airspace, Qt::MatchExactly); @@ -65,7 +66,8 @@ void ADSBDemodDisplayDialog::accept() m_settings->m_siUnits = ui->units->currentIndex() == 0 ? false : true; m_settings->m_displayDemodStats = ui->displayStats->isChecked(); m_settings->m_autoResizeTableColumns = ui->autoResizeTableColumns->isChecked(); - m_settings->m_apiKey = ui->apiKey->text(); + m_settings->m_aviationstackAPIKey = ui->aviationstackAPIKey->text(); + m_settings->m_checkWXAPIKey = ui->checkWXAPIKey->text(); m_settings->m_airspaces = QStringList(); for (int i = 0; i < ui->airspaces->count(); i++) { diff --git a/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.ui b/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.ui index 6e6426168..77b6a0f0c 100644 --- a/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.ui +++ b/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.ui @@ -7,7 +7,7 @@ 0 0 417 - 692 + 714 @@ -22,26 +22,27 @@ - - - - - 0 - 0 - - + + - Display demodulator statistics + Select a font for the table - + Select... - - + + - Display NAVAIDs + Log 3D model matching information + + + + + + + Resize columns after adding aircraft @@ -62,82 +63,112 @@ - - + + + + Sets the minimum airport size that will be displayed on the map + + + + Small + + + + + Medium + + + + + Large + + + + + + + + Barometric altitude reported by aircraft when on airfield surface + + + -10000 + + + 30000 + + + 10 + + + + + - Airspaces to display + Airspace display distance (km) + + + + + + + aviationstack.com API key for accessing flight information - + avaitionstack API key - - - - How long in seconds after not receiving any frames will an aircraft be removed from the table and map - - - 1000000 - - - - - + + - Display heliports + Units - - - - Displays airspace within the specified distance in kilometres from My Position - - - 20000 - - - - - + + - Log 3D model matching information + Display NAVAIDs - - - - Resize columns after adding aircraft - - - - - + + - Display NAVAIDs such as VORs and NDBs + Download and display photos of highlighted aircraft - - + + + + + 0 + 0 + + + + Display demodulator statistics + - Map type + - - + + + + Resize the columns in the table after an aircraft is added to it + - Airport display distance (km) + @@ -151,34 +182,6 @@ - - - - Display demodulator statistics - - - - - - - Display airports with size - - - - - - - aviationstack.com API key for accessing flight information - - - - - - - Display aircraft photos - - - @@ -189,17 +192,37 @@ - - - - Aircraft timeout (s) + + + + Displays airspace within the specified distance in kilometres from My Position + + + 20000 - - + + - Units + Map type + + + + + + + Display NAVAIDs such as VORs and NDBs + + + + + + + + + + Display demodulator statistics @@ -210,6 +233,20 @@ + + + + Airport display distance (km) + + + + + + + Display aircraft photos + + + @@ -394,62 +431,17 @@ - - - - Select a font for the table - + + - Select... + Display airports with size - - - - Sets the minimum airport size that will be displayed on the map - - - - Small - - - - - Medium - - - - - Large - - - - - - - - Download and display photos of highlighted aircraft - + + - - - - - - - - Airspace display distance (km) - - - - - - - Resize the columns in the table after an aircraft is added to it - - - + Aircraft timeout (s) @@ -463,26 +455,48 @@ - + + + + Display heliports + + + + + + + How long in seconds after not receiving any frames will an aircraft be removed from the table and map + + + 1000000 + + + + Airfield barometric altitude (ft) + + + + Airspaces to display + + + + + + + CheckWX API key + + + - + - Barometric altitude reported by aircraft when on airfield surface - - - -10000 - - - 30000 - - - 10 + checkwxapi.com API key for accessing airport weather (METARs) @@ -515,7 +529,10 @@ font autoResizeTableColumns displayStats - apiKey + verboseModelMatching + aviationstackAPIKey + checkWXAPIKey + airfieldElevation diff --git a/plugins/channelrx/demodadsb/adsbdemodgui.cpp b/plugins/channelrx/demodadsb/adsbdemodgui.cpp index 4e6ef181e..df89ec5c5 100644 --- a/plugins/channelrx/demodadsb/adsbdemodgui.cpp +++ b/plugins/channelrx/demodadsb/adsbdemodgui.cpp @@ -440,14 +440,26 @@ QVariant AirportModel::data(const QModelIndex &index, int role) const else if (role == AirportModel::airportDataRole) { if (m_showFreq[row]) - return QVariant::fromValue(m_airportDataFreq[row]); + { + QString text = m_airportDataFreq[row]; + if (!m_metar[row].isEmpty()) { + text = text + "\n" + m_metar[row]; + } + return QVariant::fromValue(text); + } else return QVariant::fromValue(m_airports[row]->m_ident); } else if (role == AirportModel::airportDataRowsRole) { if (m_showFreq[row]) - return QVariant::fromValue(m_airportDataFreqRows[row]); + { + int rows = m_airportDataFreqRows[row]; + if (!m_metar[row].isEmpty()) { + rows += 1 + m_metar[row].count("\n"); + } + return QVariant::fromValue(rows); + } else return 1; } @@ -487,6 +499,9 @@ bool AirportModel::setData(const QModelIndex &index, const QVariant& value, int { m_showFreq[row] = showFreq; emit dataChanged(index, index); + if (showFreq) { + emit requestMetar(m_airports[row]->m_ident); + } } return true; } @@ -3829,6 +3844,9 @@ ADSBDemodGUI::ADSBDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseb // Get updated when position changes connect(&MainCore::instance()->getSettings(), &MainSettings::preferenceChanged, this, &ADSBDemodGUI::preferenceChanged); + // Get airport weather when requested + connect(&m_airportModel, &AirportModel::requestMetar, this, &ADSBDemodGUI::requestMetar); + // Add airports within range of My Position if (m_airportInfo != nullptr) { updateAirports(); @@ -3841,6 +3859,7 @@ ADSBDemodGUI::ADSBDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseb m_speech = new QTextToSpeech(this); m_flightInformation = nullptr; + m_aviationWeather = nullptr; connect(&m_planeSpotters, &PlaneSpotters::aircraftPhoto, this, &ADSBDemodGUI::aircraftPhoto); connect(ui->photo, &ClickableLabel::clicked, this, &ADSBDemodGUI::photoClicked); @@ -3898,6 +3917,10 @@ ADSBDemodGUI::~ADSBDemodGUI() disconnect(m_flightInformation, &FlightInformation::flightUpdated, this, &ADSBDemodGUI::flightInformationUpdated); delete m_flightInformation; } + if (m_aviationWeather) + { + delete m_aviationWeather; + } qDeleteAll(m_airspaces); qDeleteAll(m_navAids); qDeleteAll(m_3DModelMatch); @@ -4009,6 +4032,7 @@ void ADSBDemodGUI::displaySettings() ui->stats->setText(""); initFlightInformation(); + initAviationWeather(); applyMapSettings(); applyImportSettings(); @@ -4197,9 +4221,9 @@ void ADSBDemodGUI::initFlightInformation() delete m_flightInformation; m_flightInformation = nullptr; } - if (!m_settings.m_apiKey.isEmpty()) + if (!m_settings.m_aviationstackAPIKey.isEmpty()) { - m_flightInformation = FlightInformation::create(m_settings.m_apiKey); + m_flightInformation = FlightInformation::create(m_settings.m_aviationstackAPIKey); if (m_flightInformation) { connect(m_flightInformation, &FlightInformation::flightUpdated, this, &ADSBDemodGUI::flightInformationUpdated); } @@ -4677,6 +4701,40 @@ void ADSBDemodGUI::preferenceChanged(int elementType) } } +void ADSBDemodGUI::initAviationWeather() +{ + if (m_aviationWeather) + { + disconnect(m_aviationWeather, &AviationWeather::weatherUpdated, this, &ADSBDemodGUI::weatherUpdated); + delete m_aviationWeather; + m_aviationWeather = nullptr; + } + if (!m_settings.m_checkWXAPIKey.isEmpty()) + { + m_aviationWeather = AviationWeather::create(m_settings.m_checkWXAPIKey); + if (m_aviationWeather) { + connect(m_aviationWeather, &AviationWeather::weatherUpdated, this, &ADSBDemodGUI::weatherUpdated); + } + } +} + +void ADSBDemodGUI::requestMetar(const QString& icao) +{ + if (m_aviationWeather) + { + m_aviationWeather->getWeather(icao); + } + else + { + qDebug() << "ADSBDemodGUI::requestMetar - m_aviationWeather not initialised"; + } +} + +void ADSBDemodGUI::weatherUpdated(const AviationWeather::METAR &metar) +{ + m_airportModel.updateWeather(metar.m_icao, metar.m_text, metar.decoded()); +} + void ADSBDemodGUI::makeUIConnections() { QObject::connect(ui->deltaFrequency, &ValueDialZ::changed, this, &ADSBDemodGUI::on_deltaFrequency_changed); diff --git a/plugins/channelrx/demodadsb/adsbdemodgui.h b/plugins/channelrx/demodadsb/adsbdemodgui.h index f582e02b9..4f90ae4db 100644 --- a/plugins/channelrx/demodadsb/adsbdemodgui.h +++ b/plugins/channelrx/demodadsb/adsbdemodgui.h @@ -33,6 +33,7 @@ #include "dsp/dsptypes.h" #include "dsp/channelmarker.h" #include "dsp/movingaverage.h" +#include "util/aviationweather.h" #include "util/messagequeue.h" #include "util/azel.h" #include "util/movingaverage.h" @@ -431,6 +432,7 @@ public: m_azimuth.append(az); m_elevation.append(el); m_range.append(distance); + m_metar.append(""); endInsertRows(); } @@ -446,6 +448,7 @@ public: m_azimuth.removeAt(row); m_elevation.removeAt(row); m_range.removeAt(row); + m_metar.removeAt(row); endRemoveRows(); } } @@ -461,6 +464,7 @@ public: m_azimuth.clear(); m_elevation.clear(); m_range.clear(); + m_metar.clear(); endRemoveRows(); } } @@ -520,6 +524,20 @@ public: return roles; } + void updateWeather(const QString &icao, const QString &text, const QString &decoded) + { + for (int i = 0; i < m_airports.size(); i++) + { + if (m_airports[i]->m_ident == icao) + { + m_metar[i] = "METAR: " + text + "\n" + decoded; + QModelIndex idx = index(i); + emit dataChanged(idx, idx); + break; + } + } + } + private: ADSBDemodGUI *m_gui; QList m_airports; @@ -529,6 +547,10 @@ private: QList m_azimuth; QList m_elevation; QList m_range; + QList m_metar; + +signals: + void requestMetar(const QString& icao); }; // Airspace data model used by QML map item @@ -826,6 +848,7 @@ private: QMenu *menu; // Column select context menu FlightInformation *m_flightInformation; PlaneSpotters m_planeSpotters; + AviationWeather *m_aviationWeather; QString m_photoLink; WebAPIAdapterInterface *m_webAPIAdapterInterface; HttpDownloadManager m_dlm; @@ -901,6 +924,7 @@ private: Aircraft* findAircraftByFlight(const QString& flight); QString dataTimeToShortString(QDateTime dt); void initFlightInformation(); + void initAviationWeather(); void applyMapSettings(); void updatePhotoText(Aircraft *aircraft); void updatePhotoFlightInformation(Aircraft *aircraft); @@ -959,6 +983,8 @@ private slots: void import(); void handleImportReply(QNetworkReply* reply); void preferenceChanged(int elementType); + void requestMetar(const QString& icao); + void weatherUpdated(const AviationWeather::METAR &metar); signals: void homePositionChanged(); diff --git a/plugins/channelrx/demodadsb/adsbdemodsettings.cpp b/plugins/channelrx/demodadsb/adsbdemodsettings.cpp index b23b0fb67..f506e01e9 100644 --- a/plugins/channelrx/demodadsb/adsbdemodsettings.cpp +++ b/plugins/channelrx/demodadsb/adsbdemodsettings.cpp @@ -81,7 +81,8 @@ void ADSBDemodSettings::resetToDefaults() m_autoResizeTableColumns = false; m_interpolatorPhaseSteps = 4; // Higher than these two values will struggle to run in real-time m_interpolatorTapsPerPhase = 3.5f; // without gaining much improvement in PER - m_apiKey = ""; + m_aviationstackAPIKey = ""; + m_checkWXAPIKey = ""; for (int i = 0; i < ADSBDEMOD_COLUMNS; i++) { m_columnIndexes[i] = i; @@ -143,7 +144,7 @@ QByteArray ADSBDemodSettings::serialize() const s.writeBool(33, m_allFlightPaths); s.writeBlob(34, serializeNotificationSettings(m_notificationSettings)); - s.writeString(35, m_apiKey); + s.writeString(35, m_aviationstackAPIKey); s.writeString(36, m_logFilename); s.writeBool(37, m_logEnabled); @@ -177,6 +178,7 @@ QByteArray ADSBDemodSettings::serialize() const s.writeS32(59, m_workspaceIndex); s.writeBlob(60, m_geometryBytes); s.writeBool(61, m_hidden); + s.writeString(62, m_checkWXAPIKey); for (int i = 0; i < ADSBDEMOD_COLUMNS; i++) { s.writeS32(100 + i, m_columnIndexes[i]); @@ -264,7 +266,7 @@ bool ADSBDemodSettings::deserialize(const QByteArray& data) d.readBlob(34, &blob); deserializeNotificationSettings(blob, m_notificationSettings); - d.readString(35, &m_apiKey, ""); + d.readString(35, &m_aviationstackAPIKey, ""); d.readString(36, &m_logFilename, "adsb_log.csv"); d.readBool(37, &m_logEnabled, false); @@ -306,6 +308,7 @@ bool ADSBDemodSettings::deserialize(const QByteArray& data) d.readS32(59, &m_workspaceIndex, 0); d.readBlob(60, &m_geometryBytes); d.readBool(61, &m_hidden, false); + d.readString(62, &m_checkWXAPIKey, ""); for (int i = 0; i < ADSBDEMOD_COLUMNS; i++) { d.readS32(100 + i, &m_columnIndexes[i], i); diff --git a/plugins/channelrx/demodadsb/adsbdemodsettings.h b/plugins/channelrx/demodadsb/adsbdemodsettings.h index 8fd12a054..33f237bf5 100644 --- a/plugins/channelrx/demodadsb/adsbdemodsettings.h +++ b/plugins/channelrx/demodadsb/adsbdemodsettings.h @@ -147,7 +147,8 @@ struct ADSBDemodSettings float m_interpolatorTapsPerPhase; QList m_notificationSettings; - QString m_apiKey; //!< aviationstack.com API key + QString m_aviationstackAPIKey; //!< aviationstack.com API key + QString m_checkWXAPIKey; //!< checkwxapi.com API key QString m_logFilename; bool m_logEnabled; diff --git a/plugins/channelrx/demodadsb/readme.md b/plugins/channelrx/demodadsb/readme.md index 2dc426524..3ce087429 100644 --- a/plugins/channelrx/demodadsb/readme.md +++ b/plugins/channelrx/demodadsb/readme.md @@ -90,6 +90,8 @@ Clicking the Display Settings button will open the Display Settings dialog, whic You can also enter an [avaiationstack](https://aviationstack.com/product) API key, needed to download flight information (such as departure and arrival airports and times). +A [CheckWX](https://www.checkwxapi.com/) API key can be entered in order to download airport weather (METARs) which can be displayed on the map. + ![ADS-B Demodulator display settings](../../../doc/img/ADSBDemod_plugin_displaysettings.png)

13: Display Flight Path

@@ -288,7 +290,11 @@ Aircraft are only placed upon the map when a position can be calculated, which c * Left clicking on an aircraft will highlight the corresponding row in the table for the aircraft and the information box on the map will be coloured orange, rather than blue. * Double clicking on an aircraft will set it as the active target and the information box will be coloured green. * Left clicking the information box next to an aircraft will reveal more information. It can be closed by clicking it again. -* Left clicking the information box next to an airport will reveal ATC frequencies for the airport (if the OurAirports database has been downloaded.). This information box can be closed by left clicking on the airport identifier. Double clicking on one of the listed frequencies, will set it as the centre frequency on the selected SDRangel device set (15). The Az/El row gives the azimuth and elevation of the airport from the location set under Preferences > My Position. Double clicking on this row will set the airport as the active target. +* Left clicking the information box next to an airport will reveal ATC frequencies for the airport (if the OurAirports database has been downloaded) and METAR weather information (if the CheckWX API key has been entered). +The METAR for the airport is downloaded each time the information box is opened. +This information box can be closed by left clicking on the airport identifier. +Double clicking on one of the listed frequencies, will set it as the centre frequency on the selected SDRangel device set (21). +The Az/El row gives the azimuth and elevation of the airport from the location set under Preferences > My Position. Double clicking on this row will set the airport as the active target.

Attribution

diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt index 094466cef..c34410632 100644 --- a/sdrbase/CMakeLists.txt +++ b/sdrbase/CMakeLists.txt @@ -167,6 +167,7 @@ set(sdrbase_SOURCES settings/rollupstate.cpp util/ais.cpp + util/aviationweather.cpp util/ax25.cpp util/aprs.cpp util/astronomy.cpp @@ -377,6 +378,7 @@ set(sdrbase_HEADERS settings/rollupstate.h util/ais.h + util/aviationweather.h util/ax25.h util/aprs.h util/astronomy.h diff --git a/sdrbase/util/aviationweather.cpp b/sdrbase/util/aviationweather.cpp new file mode 100644 index 000000000..5d38e3ee1 --- /dev/null +++ b/sdrbase/util/aviationweather.cpp @@ -0,0 +1,245 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// 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 as version 3 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "aviationweather.h" + +#include +#include +#include +#include +#include +#include + +AviationWeather::AviationWeather() +{ + connect(&m_timer, &QTimer::timeout, this, &AviationWeather::update); +} + +AviationWeather* AviationWeather::create(const QString& apiKey, const QString& service) +{ + if (service == "checkwxapi.com") + { + if (!apiKey.isEmpty()) + { + return new CheckWXAPI(apiKey); + } + else + { + qDebug() << "AviationWeather::create: An API key is required for: " << service; + return nullptr; + } + } + else + { + qDebug() << "AviationWeather::create: Unsupported service: " << service; + return nullptr; + } +} + +void AviationWeather::getWeatherPeriodically(const QString &icao, int periodInMins) +{ + m_icao = icao; + m_timer.setInterval(periodInMins*60*1000); + m_timer.start(); + update(); +} + +void AviationWeather::update() +{ + getWeather(m_icao); +} + +CheckWXAPI::CheckWXAPI(const QString& apiKey) : + m_apiKey(apiKey) +{ + m_networkManager = new QNetworkAccessManager(); + QObject::connect( + m_networkManager, + &QNetworkAccessManager::finished, + this, + &CheckWXAPI::handleReply + ); +} + +CheckWXAPI::~CheckWXAPI() +{ + QObject::disconnect( + m_networkManager, + &QNetworkAccessManager::finished, + this, + &CheckWXAPI::handleReply + ); + delete m_networkManager; +} + +void CheckWXAPI::getWeather(const QString &icao) +{ + QUrl url(QString("https://api.checkwx.com/metar/%1/decoded").arg(icao)); + QNetworkRequest req(url); + req.setRawHeader(QByteArray("X-API-Key"), m_apiKey.toUtf8()); + + m_networkManager->get(req); +} + +void CheckWXAPI::handleReply(QNetworkReply* reply) +{ + if (reply) + { + if (!reply->error()) + { + QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); + if (document.isObject()) + { + QJsonObject obj = document.object(); + if (obj.contains(QStringLiteral("data"))) + { + QJsonValue val = obj.value(QStringLiteral("data")); + if (val.isArray()) + { + QJsonArray array = val.toArray(); + for (auto mainObjRef : array) + { + QJsonObject mainObj = mainObjRef.toObject(); + METAR metar; + + if (mainObj.contains(QStringLiteral("icao"))) { + metar.m_icao = mainObj.value(QStringLiteral("icao")).toString(); + } + + if (mainObj.contains(QStringLiteral("raw_text"))) { + metar.m_text = mainObj.value(QStringLiteral("raw_text")).toString(); + } + + if (mainObj.contains(QStringLiteral("observed"))) { + metar.m_dateTime = QDateTime::fromString(mainObj.value(QStringLiteral("observed")).toString(), Qt::ISODate); + } + + if (mainObj.contains(QStringLiteral("wind"))) { + QJsonObject windObj = mainObj.value(QStringLiteral("wind")).toObject(); + if (windObj.contains(QStringLiteral("degrees"))) { + metar.m_windDirection = windObj.value(QStringLiteral("degrees")).toDouble(); + } + if (windObj.contains(QStringLiteral("speed_kts"))) { + metar.m_windSpeed = windObj.value(QStringLiteral("speed_kts")).toDouble(); + } + if (windObj.contains(QStringLiteral("wind.gust_kts"))) { + metar.m_windGusts = windObj.value(QStringLiteral("wind.gust_kts")).toDouble(); + } + } + if (mainObj.contains(QStringLiteral("visibility"))) { + QJsonObject visibiltyObj = mainObj.value(QStringLiteral("visibility")).toObject(); + if (visibiltyObj.contains(QStringLiteral("meters"))) { + metar.m_visibility = visibiltyObj.value(QStringLiteral("meters")).toString(); + } + } + + if (mainObj.contains(QStringLiteral("conditions"))) { + QJsonArray conditions = mainObj.value(QStringLiteral("conditions")).toArray(); + for (auto condition : conditions) { + QJsonObject conditionObj = condition.toObject(); + if (conditionObj.contains(QStringLiteral("text"))) { + metar.m_conditions.append(conditionObj.value(QStringLiteral("text")).toString()); + } + } + } + + if (mainObj.contains(QStringLiteral("ceiling"))) { + QJsonObject ceilingObj = mainObj.value(QStringLiteral("ceiling")).toObject(); + if (ceilingObj.contains(QStringLiteral("feet"))) { + metar.m_ceiling = ceilingObj.value(QStringLiteral("feet")).toDouble(); + } + } + + if (mainObj.contains(QStringLiteral("clouds"))) { + QJsonArray clouds = mainObj.value(QStringLiteral("clouds")).toArray(); + for (auto cloud : clouds) { + QJsonObject cloudObj = cloud.toObject(); + // "Clear skies" doesn't have an altitude + if (cloudObj.contains(QStringLiteral("text")) && cloudObj.contains(QStringLiteral("feet"))) { + metar.m_clouds.append(QString("%1 %2 ft").arg(cloudObj.value(QStringLiteral("text")).toString()) + .arg(cloudObj.value(QStringLiteral("feet")).toDouble())); + } else if (cloudObj.contains(QStringLiteral("text"))) { + metar.m_clouds.append(cloudObj.value(QStringLiteral("text")).toString()); + } + } + } + + if (mainObj.contains(QStringLiteral("temperature"))) { + QJsonObject tempObj = mainObj.value(QStringLiteral("temperature")).toObject(); + if (tempObj.contains(QStringLiteral("celsius"))) { + metar.m_temperature = tempObj.value(QStringLiteral("celsius")).toDouble(); + } + } + + if (mainObj.contains(QStringLiteral("dewpoint"))) { + QJsonObject dewpointObj = mainObj.value(QStringLiteral("dewpoint")).toObject(); + if (dewpointObj.contains(QStringLiteral("celsius"))) { + metar.m_dewpoint = dewpointObj.value(QStringLiteral("celsius")).toDouble(); + } + } + + if (mainObj.contains(QStringLiteral("barometer"))) { + QJsonObject pressureObj = mainObj.value(QStringLiteral("barometer")).toObject(); + if (pressureObj.contains(QStringLiteral("hpa"))) { + metar.m_pressure = pressureObj.value(QStringLiteral("hpa")).toDouble(); + } + } + + if (mainObj.contains(QStringLiteral("humidity"))) { + QJsonObject humidityObj = mainObj.value(QStringLiteral("humidity")).toObject(); + if (humidityObj.contains(QStringLiteral("percent"))) { + metar.m_humidity = humidityObj.value(QStringLiteral("percent")).toDouble(); + } + } + + if (mainObj.contains(QStringLiteral("flight_category"))) { + metar.m_flightCateogory = mainObj.value(QStringLiteral("flight_category")).toString(); + } + + if (!metar.m_icao.isEmpty()) { + emit weatherUpdated(metar); + } else { + qDebug() << "CheckWXAPI::handleReply: object doesn't contain icao: " << mainObj; + } + } + } + else + { + qDebug() << "CheckWXAPI::handleReply: data isn't an array: " << obj; + } + } + else + { + qDebug() << "CheckWXAPI::handleReply: Object doesn't contain data: " << obj; + } + } + else + { + qDebug() << "CheckWXAPI::handleReply: Document is not an object: " << document; + } + } + else + { + qDebug() << "CheckWXAPI::handleReply: error: " << reply->error(); + } + reply->deleteLater(); + } + else + { + qDebug() << "CheckWXAPI::handleReply: reply is null"; + } +} diff --git a/sdrbase/util/aviationweather.h b/sdrbase/util/aviationweather.h new file mode 100644 index 000000000..dff46cd34 --- /dev/null +++ b/sdrbase/util/aviationweather.h @@ -0,0 +1,152 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// 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 as version 3 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AVIATIONWEATHER_H +#define INCLUDE_AVIATIONWEATHER_H + +#include +#include + +#include "export.h" + +#include + +#include + +class QNetworkAccessManager; +class QNetworkReply; + +// Aviation Weather API wrapper +// Allows METAR information to be obtained for a given airport +// Currently supports checkwxapi.com +class SDRBASE_API AviationWeather : public QObject +{ + Q_OBJECT + +public: + struct METAR { + QString m_icao; // ICAO of reporting station/airport + QString m_text; // Raw METAR text + QDateTime m_dateTime; // Date&time of observation + float m_windDirection; // Direction wind is blowing from, in degrees + float m_windSpeed; // Wind speed in knots + float m_windGusts; // Wind gusts in knots + QString m_visibility; // Visibility in metres (may be non-numeric) + QStringList m_conditions; // Weather conditions (Rain, snow) + float m_ceiling; // Ceiling in feet + QStringList m_clouds; // Cloud types and altitudes + float m_temperature; // Air temperature in Celsuis + float m_dewpoint; // Dewpoint in Celsuius + float m_pressure; // Air pressure in hPa/mb + float m_humidity; // Humidity in % + QString m_flightCateogory; // VFR/MVFR/IFR/LIFR + + METAR() : + m_windDirection(NAN), + m_windSpeed(NAN), + m_windGusts(NAN), + m_ceiling(NAN), + m_temperature(NAN), + m_dewpoint(NAN), + m_pressure(NAN), + m_humidity(NAN) + { + } + + QString decoded(const QString joinArg="\n") const + { + QStringList s; + if (m_dateTime.isValid()) { + s.append(QString("Observed: %1").arg(m_dateTime.toString())); + } + if (!isnan(m_windDirection) && !isnan(m_windSpeed)) { + s.append(QString("Wind: %1%2 / %3 knts").arg(m_windDirection).arg(QChar(0xb0)).arg(m_windSpeed)); + } + if (!isnan(m_windGusts) ) { + s.append(QString("Gusts: %1 knts").arg(m_windGusts)); + } + if (!m_visibility.isEmpty()) { + s.append(QString("Visibility: %1 metres").arg(m_visibility)); + } + if (!m_conditions.isEmpty()) { + s.append(QString("Conditions: %1").arg(m_conditions.join(", "))); + } + if (!isnan(m_ceiling)) { + s.append(QString("Ceiling: %1 ft").arg(m_ceiling)); + } + if (!m_clouds.isEmpty()) { + s.append(QString("Clouds: %1").arg(m_clouds.join(", "))); + } + if (!isnan(m_temperature)) { + s.append(QString("Temperature: %1 %2C").arg(m_temperature).arg(QChar(0xb0))); + } + if (!isnan(m_dewpoint)) { + s.append(QString("Dewpoint: %1 %2C").arg(m_dewpoint).arg(QChar(0xb0))); + } + if (!isnan(m_pressure)) { + s.append(QString("Pressure: %1 hPa").arg(m_pressure)); + } + if (!isnan(m_humidity)) { + s.append(QString("Humidity: %1 %").arg(m_humidity)); + } + if (!m_flightCateogory.isEmpty()) { + s.append(QString("Category: %1").arg(m_flightCateogory)); + } + return s.join(joinArg); + } + }; + +protected: + AviationWeather(); + +public: + static AviationWeather* create(const QString& apiKey, const QString& service="checkwxapi.com"); + + virtual void getWeather(const QString &icao) = 0; + void getWeatherPeriodically(const QString &, int periodInMins); + +public slots: + void update(); + +signals: + void weatherUpdated(const METAR &metar); // Called when new data available. If no value is available, parameter will be NAN + +private: + QTimer m_timer; // Timer for periodic updates + QString m_icao; // Saved airport ICAO for period updates + +}; + +class SDRBASE_API CheckWXAPI : public AviationWeather { + Q_OBJECT +public: + + CheckWXAPI(const QString& apiKey); + ~CheckWXAPI(); + virtual void getWeather(const QString &icao) override; + +private: + + QString m_apiKey; + QNetworkAccessManager *m_networkManager; + +public slots: + void handleReply(QNetworkReply* reply); + +}; + +#endif /* INCLUDE_AVIATIONWEATHER_H */