From fb516d1ef1fa27410fd8d4a6d0ed7e7fc5858cc8 Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Mon, 6 Jun 2022 10:53:10 +0100 Subject: [PATCH 1/7] 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 */ From 3a71ba0fb906f2b101c80d7615bb4cdeae9a32a5 Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Mon, 6 Jun 2022 11:52:29 +0100 Subject: [PATCH 2/7] ADS-B: Add mapboxgl support, as osm maps do not work in Qt 5.15.3 --- doc/img/ADSBDemod_plugin_displaysettings.png | Bin 32837 -> 25905 bytes .../demodadsb/adsbdemoddisplaydialog.cpp | 4 + .../demodadsb/adsbdemoddisplaydialog.ui | 412 ++++++++++-------- plugins/channelrx/demodadsb/adsbdemodgui.cpp | 29 +- .../channelrx/demodadsb/adsbdemodsettings.cpp | 14 + .../channelrx/demodadsb/adsbdemodsettings.h | 4 +- plugins/channelrx/demodadsb/readme.md | 4 +- 7 files changed, 268 insertions(+), 199 deletions(-) diff --git a/doc/img/ADSBDemod_plugin_displaysettings.png b/doc/img/ADSBDemod_plugin_displaysettings.png index 3738aa3e55a93033ad29f21e8679a609ff6183bd..40148fb4b0b47c59f4935aa8326a55a7490cb654 100644 GIT binary patch literal 25905 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 z0Bb1P{uUq+fh```bpJ~oairspaceRange->setValue(settings->m_airspaceRange); + ui->mapProvider->setCurrentText(settings->m_mapProvider); ui->mapType->setCurrentIndex((int)settings->m_mapType); + ui->mapBoxAPIKey->setText(settings->m_mapBoxAPIKey); ui->navAids->setChecked(settings->m_displayNavAids); ui->photos->setChecked(settings->m_displayPhotos); ui->verboseModelMatching->setChecked(settings->m_verboseModelMatching); @@ -77,7 +79,9 @@ void ADSBDemodDisplayDialog::accept() } } m_settings->m_airspaceRange = ui->airspaceRange->value(); + m_settings->m_mapProvider = ui->mapProvider->currentText(); m_settings->m_mapType = (ADSBDemodSettings::MapType)ui->mapType->currentIndex(); + m_settings->m_mapBoxAPIKey = ui->mapBoxAPIKey->text(); m_settings->m_displayNavAids = ui->navAids->isChecked(); m_settings->m_displayPhotos = ui->photos->isChecked(); m_settings->m_verboseModelMatching = ui->verboseModelMatching->isChecked(); diff --git a/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.ui b/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.ui index 77b6a0f0c..657c9eb0b 100644 --- a/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.ui +++ b/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.ui @@ -7,7 +7,7 @@ 0 0 417 - 714 + 742 @@ -22,70 +22,7 @@ - - - - Select a font for the table - - - Select... - - - - - - - Log 3D model matching information - - - - - - - Resize columns after adding aircraft - - - - - - - The units to use for altitude, speed and climb rate - - - - ft, kn, ft/min - - - - - m, kph, m/s - - - - - - - - Sets the minimum airport size that will be displayed on the map - - - - Small - - - - - Medium - - - - - Large - - - - - + Barometric altitude reported by aircraft when on airfield surface @@ -101,42 +38,14 @@ - - - - Airspace display distance (km) - - - - - - - aviationstack.com API key for accessing flight information - - - - - - - avaitionstack API key - - - - - - - Units - - - - + Display NAVAIDs - + Download and display photos of highlighted aircraft @@ -146,23 +55,7 @@ - - - - - 0 - 0 - - - - Display demodulator statistics - - - - - - - + Resize the columns in the table after an aircraft is added to it @@ -172,17 +65,7 @@ - - - - Displays airports within the specified distance in kilometres from My Position - - - 20000 - - - - + Log information about how aircraft are matched to 3D models @@ -192,7 +75,7 @@ - + Displays airspace within the specified distance in kilometres from My Position @@ -202,14 +85,14 @@ - - + + - Map type + Display demodulator statistics - + Display NAVAIDs such as VORs and NDBs @@ -219,38 +102,24 @@ - - + + - Display demodulator statistics + Map type - - - - Table font - - - - + Airport display distance (km) - - - - Display aircraft photos - - - - + - Type of map to display + Type of map to display (for osm maps) @@ -274,7 +143,200 @@ - + + + + Display airports with size + + + + + + + Aircraft timeout (s) + + + + + + + CheckWX API key + + + + + + + Display heliports + + + + + + + How long in seconds after not receiving any frames will an aircraft be removed from the table and map + + + 1000000 + + + + + + + Select a font for the table + + + Select... + + + + + + + Log 3D model matching information + + + + + + + Resize columns after adding aircraft + + + + + + + The units to use for altitude, speed and climb rate + + + + ft, kn, ft/min + + + + + m, kph, m/s + + + + + + + + Sets the minimum airport size that will be displayed on the map + + + + Small + + + + + Medium + + + + + Large + + + + + + + + Mapping service + + + + osm + + + + + mapboxgl + + + + + + + + Airspace display distance (km) + + + + + + + aviationstack.com API key for accessing flight information + + + + + + + avaitionstack API key + + + + + + + Map provider + + + + + + + Units + + + + + + + + 0 + 0 + + + + Display demodulator statistics + + + + + + + + + + Displays airports within the specified distance in kilometres from My Position + + + 20000 + + + + + + + Table font + + + + + + + Display aircraft photos + + + + Airspace categories to display @@ -431,21 +493,14 @@ - - - - Display airports with size + + + + checkwxapi.com API key for accessing airport weather (METARs) - - - - Aircraft timeout (s) - - - - + When checked, heliports are displayed on the map @@ -455,48 +510,31 @@ - - - - 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 + Mapbox API key - - + + - checkwxapi.com API key for accessing airport weather (METARs) + mapbox.com API key for mapboxgl maps @@ -517,7 +555,9 @@ units + mapProvider mapType + mapBoxAPIKey airportSize heliports airportRange diff --git a/plugins/channelrx/demodadsb/adsbdemodgui.cpp b/plugins/channelrx/demodadsb/adsbdemodgui.cpp index df89ec5c5..b9259d771 100644 --- a/plugins/channelrx/demodadsb/adsbdemodgui.cpp +++ b/plugins/channelrx/demodadsb/adsbdemodgui.cpp @@ -3624,20 +3624,27 @@ void ADSBDemodGUI::applyMapSettings() } // Create the map using the specified provider - QQmlProperty::write(item, "mapProvider", "osm"); + QQmlProperty::write(item, "mapProvider", m_settings.m_mapProvider); QVariantMap parameters; - // Use our repo, so we can append API key and redefine transmit maps - parameters["osm.mapping.providersrepository.address"] = QString("http://127.0.0.1:%1/").arg(m_osmPort); - // Use ADS-B specific cache, as we use different transmit maps - QString cachePath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + "/QtLocation/5.8/tiles/osm/sdrangel_adsb"; - parameters["osm.mapping.cache.directory"] = cachePath; - // On Linux, we need to create the directory - QDir dir(cachePath); - if (!dir.exists()) { - dir.mkpath(cachePath); + if (m_settings.m_mapProvider == "osm") + { + // Use our repo, so we can append API key and redefine transmit maps + parameters["osm.mapping.providersrepository.address"] = QString("http://127.0.0.1:%1/").arg(m_osmPort); + // Use ADS-B specific cache, as we use different transmit maps + QString cachePath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + "/QtLocation/5.8/tiles/osm/sdrangel_adsb"; + parameters["osm.mapping.cache.directory"] = cachePath; + // On Linux, we need to create the directory + QDir dir(cachePath); + if (!dir.exists()) { + dir.mkpath(cachePath); + } + } + else if (m_settings.m_mapProvider == "mapboxgl") + { + parameters["mapboxgl.access_token"] = m_settings.m_mapBoxAPIKey; } - QString mapType; + QString mapType; // Only for osm maps switch (m_settings.m_mapType) { case ADSBDemodSettings::AVIATION_LIGHT: diff --git a/plugins/channelrx/demodadsb/adsbdemodsettings.cpp b/plugins/channelrx/demodadsb/adsbdemodsettings.cpp index f506e01e9..d02a31743 100644 --- a/plugins/channelrx/demodadsb/adsbdemodsettings.cpp +++ b/plugins/channelrx/demodadsb/adsbdemodsettings.cpp @@ -92,7 +92,13 @@ void ADSBDemodSettings::resetToDefaults() m_logEnabled = false; m_airspaces = QStringList({"A", "D", "TMZ"}); m_airspaceRange = 500.0f; +#if QT_VERSION == QT_VERSION_CHECK(5, 15, 3) + m_mapProvider = "mapboxgl"; // osm maps do not work in Qt 5.15.3 - https://github.com/f4exb/sdrangel/issues/1169 +#else + m_mapProvider = "osm"; +#endif m_mapType = AVIATION_LIGHT; + m_mapBoxAPIKey = ""; m_displayNavAids = true; m_displayPhotos = true; m_verboseModelMatching = false; @@ -179,6 +185,8 @@ QByteArray ADSBDemodSettings::serialize() const s.writeBlob(60, m_geometryBytes); s.writeBool(61, m_hidden); s.writeString(62, m_checkWXAPIKey); + s.writeString(63, m_mapProvider); + s.writeString(64, m_mapBoxAPIKey); for (int i = 0; i < ADSBDEMOD_COLUMNS; i++) { s.writeS32(100 + i, m_columnIndexes[i]); @@ -309,6 +317,12 @@ bool ADSBDemodSettings::deserialize(const QByteArray& data) d.readBlob(60, &m_geometryBytes); d.readBool(61, &m_hidden, false); d.readString(62, &m_checkWXAPIKey, ""); +#if QT_VERSION == QT_VERSION_CHECK(5, 15, 3) + d.readString(63, &m_mapProvider, "mapboxgl"); // osm maps do not work in Qt 5.15.3 - https://github.com/f4exb/sdrangel/issues/1169 +#else + d.readString(63, &m_mapProvider, "osm"); +#endif + d.readString(64, &m_mapBoxAPIKey, ""); 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 33f237bf5..1eab03b09 100644 --- a/plugins/channelrx/demodadsb/adsbdemodsettings.h +++ b/plugins/channelrx/demodadsb/adsbdemodsettings.h @@ -155,12 +155,14 @@ struct ADSBDemodSettings QStringList m_airspaces; //!< Airspace names to display float m_airspaceRange; //!< How far away we display airspace (mkm) + QString m_mapProvider; enum MapType { AVIATION_LIGHT, //!< White map with no place names AVIATION_DARK, STREET, SATELLITE - } m_mapType; + } m_mapType; //!< For osm maps + QString m_mapBoxAPIKey; bool m_displayNavAids; bool m_displayPhotos; Serializable *m_rollupState; diff --git a/plugins/channelrx/demodadsb/readme.md b/plugins/channelrx/demodadsb/readme.md index 3ce087429..ed1fc43c5 100644 --- a/plugins/channelrx/demodadsb/readme.md +++ b/plugins/channelrx/demodadsb/readme.md @@ -75,7 +75,9 @@ Clicking this will download the [OpenAIP](https://www.openaip.net/) airspace and Clicking the Display Settings button will open the Display Settings dialog, which allows you to choose: * The units for altitude, speed and vertical climb rate. These can be either ft (feet), kn (knots) and ft/min (feet per minute), or m (metres), kph (kilometers per hour) and m/s (metres per second). -* The type of map that will be displayed. This can either be a light or dark aviation map (with no place names to reduce clutter), a street map or satellite imagery. +* The map provider. This can be osm for OpenStreetMaps or mapboxgl for Mabbox. mapboxgl is not supported on Windows. mapboxgl should be used on Linux with Qt 5.15.3, as osm maps will cause SDRangel to hang with this specific version of Qt. +* The type of map that will be displayed, when the map provider is osm. This can either be a light or dark aviation map (with no place names to reduce clutter), a street map or satellite imagery. +* A [Mapbox](https://www.mapbox.com/) API key, as required to use mapboxgl map provider. * The minimum size airport that will be displayed on the map: small, medium or large. Use small to display GA airfields, medium for regional airports and large for international airports. * Whether or not to display heliports. * The distance (in kilometres), from the location set under Preferences > My Position, at which airports will be displayed on the map. Displaying too many airports will slow down drawing of the map. From a4fdd8449604fa1797e5bf9dac2b0ffcd5a9b3ed Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Mon, 6 Jun 2022 12:55:52 +0100 Subject: [PATCH 3/7] Fix linux compilation --- sdrbase/util/aviationweather.h | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sdrbase/util/aviationweather.h b/sdrbase/util/aviationweather.h index dff46cd34..34406d826 100644 --- a/sdrbase/util/aviationweather.h +++ b/sdrbase/util/aviationweather.h @@ -73,10 +73,10 @@ public: if (m_dateTime.isValid()) { s.append(QString("Observed: %1").arg(m_dateTime.toString())); } - if (!isnan(m_windDirection) && !isnan(m_windSpeed)) { + if (!std::isnan(m_windDirection) && !std::isnan(m_windSpeed)) { s.append(QString("Wind: %1%2 / %3 knts").arg(m_windDirection).arg(QChar(0xb0)).arg(m_windSpeed)); } - if (!isnan(m_windGusts) ) { + if (!std::isnan(m_windGusts) ) { s.append(QString("Gusts: %1 knts").arg(m_windGusts)); } if (!m_visibility.isEmpty()) { @@ -85,22 +85,22 @@ public: if (!m_conditions.isEmpty()) { s.append(QString("Conditions: %1").arg(m_conditions.join(", "))); } - if (!isnan(m_ceiling)) { + if (!std::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)) { + if (!std::isnan(m_temperature)) { s.append(QString("Temperature: %1 %2C").arg(m_temperature).arg(QChar(0xb0))); } - if (!isnan(m_dewpoint)) { + if (!std::isnan(m_dewpoint)) { s.append(QString("Dewpoint: %1 %2C").arg(m_dewpoint).arg(QChar(0xb0))); } - if (!isnan(m_pressure)) { + if (!std::isnan(m_pressure)) { s.append(QString("Pressure: %1 hPa").arg(m_pressure)); } - if (!isnan(m_humidity)) { + if (!std::isnan(m_humidity)) { s.append(QString("Humidity: %1 %").arg(m_humidity)); } if (!m_flightCateogory.isEmpty()) { From 90fe976d9a971df984b91d997c9d7549343dd2df Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Mon, 6 Jun 2022 13:03:17 +0100 Subject: [PATCH 4/7] Remove debug --- plugins/channelrx/demodadsb/adsbdemodgui.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/plugins/channelrx/demodadsb/adsbdemodgui.cpp b/plugins/channelrx/demodadsb/adsbdemodgui.cpp index b9259d771..c51729115 100644 --- a/plugins/channelrx/demodadsb/adsbdemodgui.cpp +++ b/plugins/channelrx/demodadsb/adsbdemodgui.cpp @@ -4727,14 +4727,9 @@ void ADSBDemodGUI::initAviationWeather() void ADSBDemodGUI::requestMetar(const QString& icao) { - if (m_aviationWeather) - { + if (m_aviationWeather) { m_aviationWeather->getWeather(icao); } - else - { - qDebug() << "ADSBDemodGUI::requestMetar - m_aviationWeather not initialised"; - } } void ADSBDemodGUI::weatherUpdated(const AviationWeather::METAR &metar) From 8a7113be192d3979f2106c4cbbf0653e9d80d1cd Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Mon, 6 Jun 2022 13:50:37 +0100 Subject: [PATCH 5/7] ADS-B: Support different map types for mapboxgl --- .../demodadsb/adsbdemoddisplaydialog.cpp | 2 - .../demodadsb/adsbdemoddisplaydialog.ui | 17 +------ plugins/channelrx/demodadsb/adsbdemodgui.cpp | 50 ++++++++++++------- .../channelrx/demodadsb/adsbdemodsettings.cpp | 3 -- .../channelrx/demodadsb/adsbdemodsettings.h | 3 +- plugins/channelrx/demodadsb/map/map.qml | 2 +- plugins/channelrx/demodadsb/readme.md | 5 +- 7 files changed, 37 insertions(+), 45 deletions(-) diff --git a/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.cpp b/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.cpp index 3feb1b2c4..b2e645fd2 100644 --- a/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.cpp +++ b/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.cpp @@ -47,7 +47,6 @@ ADSBDemodDisplayDialog::ADSBDemodDisplayDialog(ADSBDemodSettings *settings, QWid ui->airspaceRange->setValue(settings->m_airspaceRange); ui->mapProvider->setCurrentText(settings->m_mapProvider); ui->mapType->setCurrentIndex((int)settings->m_mapType); - ui->mapBoxAPIKey->setText(settings->m_mapBoxAPIKey); ui->navAids->setChecked(settings->m_displayNavAids); ui->photos->setChecked(settings->m_displayPhotos); ui->verboseModelMatching->setChecked(settings->m_verboseModelMatching); @@ -81,7 +80,6 @@ void ADSBDemodDisplayDialog::accept() m_settings->m_airspaceRange = ui->airspaceRange->value(); m_settings->m_mapProvider = ui->mapProvider->currentText(); m_settings->m_mapType = (ADSBDemodSettings::MapType)ui->mapType->currentIndex(); - m_settings->m_mapBoxAPIKey = ui->mapBoxAPIKey->text(); m_settings->m_displayNavAids = ui->navAids->isChecked(); m_settings->m_displayPhotos = ui->photos->isChecked(); m_settings->m_verboseModelMatching = ui->verboseModelMatching->isChecked(); diff --git a/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.ui b/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.ui index 657c9eb0b..417ba074e 100644 --- a/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.ui +++ b/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.ui @@ -119,7 +119,7 @@ - Type of map to display (for osm maps) + Type of map to display @@ -524,20 +524,6 @@ - - - - Mapbox API key - - - - - - - mapbox.com API key for mapboxgl maps - - - @@ -557,7 +543,6 @@ units mapProvider mapType - mapBoxAPIKey airportSize heliports airportRange diff --git a/plugins/channelrx/demodadsb/adsbdemodgui.cpp b/plugins/channelrx/demodadsb/adsbdemodgui.cpp index c51729115..80f1c865f 100644 --- a/plugins/channelrx/demodadsb/adsbdemodgui.cpp +++ b/plugins/channelrx/demodadsb/adsbdemodgui.cpp @@ -3626,6 +3626,8 @@ void ADSBDemodGUI::applyMapSettings() // Create the map using the specified provider QQmlProperty::write(item, "mapProvider", m_settings.m_mapProvider); QVariantMap parameters; + QString mapType; + if (m_settings.m_mapProvider == "osm") { // Use our repo, so we can append API key and redefine transmit maps @@ -3638,27 +3640,39 @@ void ADSBDemodGUI::applyMapSettings() if (!dir.exists()) { dir.mkpath(cachePath); } + switch (m_settings.m_mapType) + { + case ADSBDemodSettings::AVIATION_LIGHT: + mapType = "Transit Map"; + break; + case ADSBDemodSettings::AVIATION_DARK: + mapType = "Night Transit Map"; + break; + case ADSBDemodSettings::STREET: + mapType = "Street Map"; + break; + case ADSBDemodSettings::SATELLITE: + mapType = "Satellite Map"; + break; + } } else if (m_settings.m_mapProvider == "mapboxgl") { - parameters["mapboxgl.access_token"] = m_settings.m_mapBoxAPIKey; - } - - QString mapType; // Only for osm maps - switch (m_settings.m_mapType) - { - case ADSBDemodSettings::AVIATION_LIGHT: - mapType = "Transit Map"; - break; - case ADSBDemodSettings::AVIATION_DARK: - mapType = "Night Transit Map"; - break; - case ADSBDemodSettings::STREET: - mapType = "Street Map"; - break; - case ADSBDemodSettings::SATELLITE: - mapType = "Satellite Map"; - break; + switch (m_settings.m_mapType) + { + case ADSBDemodSettings::AVIATION_LIGHT: + mapType = "mapbox://styles/mapbox/light-v9"; + break; + case ADSBDemodSettings::AVIATION_DARK: + mapType = "mapbox://styles/mapbox/dark-v9"; + break; + case ADSBDemodSettings::STREET: + mapType = "mapbox://styles/mapbox/streets-v10"; + break; + case ADSBDemodSettings::SATELLITE: + mapType = "mapbox://styles/mapbox/satellite-v9"; + break; + } } QVariant retVal; diff --git a/plugins/channelrx/demodadsb/adsbdemodsettings.cpp b/plugins/channelrx/demodadsb/adsbdemodsettings.cpp index d02a31743..f6b7ebba4 100644 --- a/plugins/channelrx/demodadsb/adsbdemodsettings.cpp +++ b/plugins/channelrx/demodadsb/adsbdemodsettings.cpp @@ -98,7 +98,6 @@ void ADSBDemodSettings::resetToDefaults() m_mapProvider = "osm"; #endif m_mapType = AVIATION_LIGHT; - m_mapBoxAPIKey = ""; m_displayNavAids = true; m_displayPhotos = true; m_verboseModelMatching = false; @@ -186,7 +185,6 @@ QByteArray ADSBDemodSettings::serialize() const s.writeBool(61, m_hidden); s.writeString(62, m_checkWXAPIKey); s.writeString(63, m_mapProvider); - s.writeString(64, m_mapBoxAPIKey); for (int i = 0; i < ADSBDEMOD_COLUMNS; i++) { s.writeS32(100 + i, m_columnIndexes[i]); @@ -322,7 +320,6 @@ bool ADSBDemodSettings::deserialize(const QByteArray& data) #else d.readString(63, &m_mapProvider, "osm"); #endif - d.readString(64, &m_mapBoxAPIKey, ""); 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 1eab03b09..dc9fa0a65 100644 --- a/plugins/channelrx/demodadsb/adsbdemodsettings.h +++ b/plugins/channelrx/demodadsb/adsbdemodsettings.h @@ -161,8 +161,7 @@ struct ADSBDemodSettings AVIATION_DARK, STREET, SATELLITE - } m_mapType; //!< For osm maps - QString m_mapBoxAPIKey; + } m_mapType; bool m_displayNavAids; bool m_displayPhotos; Serializable *m_rollupState; diff --git a/plugins/channelrx/demodadsb/map/map.qml b/plugins/channelrx/demodadsb/map/map.qml index b4e3459d8..32b01947d 100644 --- a/plugins/channelrx/demodadsb/map/map.qml +++ b/plugins/channelrx/demodadsb/map/map.qml @@ -118,7 +118,7 @@ Item { activeMapType = supportedMapTypes[i] } } - lightIcons = requestedMapType == "Night Transit Map" + lightIcons = (requestedMapType == "Night Transit Map") || (requestedMapType == "mapbox://styles/mapbox/dark-v9") } } diff --git a/plugins/channelrx/demodadsb/readme.md b/plugins/channelrx/demodadsb/readme.md index ed1fc43c5..0f7375de5 100644 --- a/plugins/channelrx/demodadsb/readme.md +++ b/plugins/channelrx/demodadsb/readme.md @@ -75,9 +75,8 @@ Clicking this will download the [OpenAIP](https://www.openaip.net/) airspace and Clicking the Display Settings button will open the Display Settings dialog, which allows you to choose: * The units for altitude, speed and vertical climb rate. These can be either ft (feet), kn (knots) and ft/min (feet per minute), or m (metres), kph (kilometers per hour) and m/s (metres per second). -* The map provider. This can be osm for OpenStreetMaps or mapboxgl for Mabbox. mapboxgl is not supported on Windows. mapboxgl should be used on Linux with Qt 5.15.3, as osm maps will cause SDRangel to hang with this specific version of Qt. -* The type of map that will be displayed, when the map provider is osm. This can either be a light or dark aviation map (with no place names to reduce clutter), a street map or satellite imagery. -* A [Mapbox](https://www.mapbox.com/) API key, as required to use mapboxgl map provider. +* The map provider. This can be osm for OpenStreetMaps or mapboxgl for Mapbox. mapboxgl is not supported on Windows. mapboxgl should be used on Linux with Qt 5.15.3, as osm maps will cause SDRangel to hang, due to a bug in Qt. +* The type of map that will be displayed. This can either be a light or dark aviation map (with no place names to reduce clutter), a street map or satellite imagery. * The minimum size airport that will be displayed on the map: small, medium or large. Use small to display GA airfields, medium for regional airports and large for international airports. * Whether or not to display heliports. * The distance (in kilometres), from the location set under Preferences > My Position, at which airports will be displayed on the map. Displaying too many airports will slow down drawing of the map. From 511a17a6bb3f4aa9b0f4e1b5fad6f8ed9dc9ea69 Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Mon, 6 Jun 2022 13:51:08 +0100 Subject: [PATCH 6/7] Map: Default to mapboxgl for Qt 5.15.3, as osm doesn't work --- plugins/feature/map/mapsettings.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/feature/map/mapsettings.cpp b/plugins/feature/map/mapsettings.cpp index 8df0b3501..866499b49 100644 --- a/plugins/feature/map/mapsettings.cpp +++ b/plugins/feature/map/mapsettings.cpp @@ -83,7 +83,11 @@ MapSettings::~MapSettings() void MapSettings::resetToDefaults() { m_displayNames = true; +#if QT_VERSION == QT_VERSION_CHECK(5, 15, 3) + m_mapProvider = "mapboxgl"; // osm maps do not work in Qt 5.15.3 - https://github.com/f4exb/sdrangel/issues/1169 +#else m_mapProvider = "osm"; +#endif m_thunderforestAPIKey = ""; m_maptilerAPIKey = ""; m_mapBoxAPIKey = ""; @@ -169,7 +173,11 @@ bool MapSettings::deserialize(const QByteArray& data) QByteArray blob; d.readBool(1, &m_displayNames, true); +#if QT_VERSION == QT_VERSION_CHECK(5, 15, 3) + d.readString(2, &m_mapProvider, "mapboxgl"); // osm maps do not work in Qt 5.15.3 - https://github.com/f4exb/sdrangel/issues/1169 +#else d.readString(2, &m_mapProvider, "osm"); +#endif d.readString(3, &m_mapBoxAPIKey, ""); d.readString(4, &m_mapBoxStyles, ""); d.readString(8, &m_title, "Map"); From 50e5f7c251565e16167985218beb61f102f301e0 Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Mon, 6 Jun 2022 13:55:31 +0100 Subject: [PATCH 7/7] Update docs --- plugins/channelrx/demodadsb/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/channelrx/demodadsb/readme.md b/plugins/channelrx/demodadsb/readme.md index 0f7375de5..1b52d390b 100644 --- a/plugins/channelrx/demodadsb/readme.md +++ b/plugins/channelrx/demodadsb/readme.md @@ -6,7 +6,7 @@ The top and bottom bars of the device window are described [here](../../../sdrgu The ADS-B demodulator plugin can be used to receive and display ADS-B aircraft information. This is information about an aircraft, such as position, altitude, heading and speed, broadcast by aircraft on 1090MHz, in the 1090ES (Extended Squitter) format. 1090ES frames have a chip rate of 2Mchip/s, so the baseband sample rate should be set to be greater than 2MSa/s. -As well as displaying information received via ADS-B, the plugin can also combine information from a number of databases to display more information about the aircraft and flight. +As well as displaying information received via ADS-B, the plugin can also combine information from a number of databases to display more information about the aircraft and flight, airports and weather. ![ADS-B Demodulator plugin GUI](../../../doc/img/ADSBDemod_plugin.png)