diff --git a/doc/img/ADSBDemod_plugin_displaysettings.png b/doc/img/ADSBDemod_plugin_displaysettings.png index 3738aa3e5..40148fb4b 100644 Binary files a/doc/img/ADSBDemod_plugin_displaysettings.png and b/doc/img/ADSBDemod_plugin_displaysettings.png differ diff --git a/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.cpp b/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.cpp index bec7c32de..b2e645fd2 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); @@ -44,6 +45,7 @@ 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->navAids->setChecked(settings->m_displayNavAids); ui->photos->setChecked(settings->m_displayPhotos); @@ -65,7 +67,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++) { @@ -75,6 +78,7 @@ 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_displayNavAids = ui->navAids->isChecked(); m_settings->m_displayPhotos = ui->photos->isChecked(); diff --git a/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.ui b/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.ui index 6e6426168..417ba074e 100644 --- a/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.ui +++ b/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.ui @@ -7,7 +7,7 @@ 0 0 417 - 692 + 742 @@ -22,164 +22,50 @@ - - - - - 0 - 0 - - + + - Display demodulator statistics + Barometric altitude reported by aircraft when on airfield surface - - + + -10000 + + + 30000 + + + 10 - + Display NAVAIDs - - + + - The units to use for altitude, speed and climb rate - - - - ft, kn, ft/min - - - - - m, kph, m/s - - - - - - - - Airspaces to display - - - - - - - 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 - - - - - - - Displays airspace within the specified distance in kilometres from My Position - - - 20000 - - - - - - - Log 3D model matching information - - - - - - - Resize columns after adding aircraft - - - - - - - Display NAVAIDs such as VORs and NDBs + Download and display photos of highlighted aircraft - - - - Map type - - - - - - - Airport display distance (km) - - - - - - - Displays airports within the specified distance in kilometres from My Position - - - 20000 - - - - - - - Display demodulator statistics - - - - - - - Display airports with size - - - - - - - aviationstack.com API key for accessing flight information - - - - - - - Display aircraft photos - - - + + + Resize the columns in the table after an aircraft is added to it + + + + + + + Log information about how aircraft are matched to 3D models @@ -189,28 +75,48 @@ - - - - Aircraft timeout (s) + + + + Displays airspace within the specified distance in kilometres from My Position + + + 20000 - - + + - Units + Display demodulator statistics - - + + + + Display NAVAIDs such as VORs and NDBs + - Table font + - + + + + Map type + + + + + + + Airport display distance (km) + + + + Type of map to display @@ -237,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 @@ -394,66 +493,14 @@ - - + + - Select a font for the table - - - Select... + checkwxapi.com API key for accessing airport weather (METARs) - - - - 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 - - - - - - - + When checked, heliports are displayed on the map @@ -463,26 +510,17 @@ - + Airfield barometric altitude (ft) - - - - Barometric altitude reported by aircraft when on airfield surface - - - -10000 - - - 30000 - - - 10 + + + + Airspaces to display @@ -503,6 +541,7 @@ units + mapProvider mapType airportSize heliports @@ -515,7 +554,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..80f1c865f 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; } @@ -3609,34 +3624,55 @@ 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); - } - QString mapType; - switch (m_settings.m_mapType) + + if (m_settings.m_mapProvider == "osm") { - 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; + // 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); + } + 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") + { + 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; @@ -3829,6 +3865,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 +3880,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 +3938,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 +4053,7 @@ void ADSBDemodGUI::displaySettings() ui->stats->setText(""); initFlightInformation(); + initAviationWeather(); applyMapSettings(); applyImportSettings(); @@ -4197,9 +4242,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 +4722,35 @@ 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); + } +} + +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..f6b7ebba4 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; @@ -91,6 +92,11 @@ 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_displayNavAids = true; m_displayPhotos = true; @@ -143,7 +149,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 +183,8 @@ QByteArray ADSBDemodSettings::serialize() const s.writeS32(59, m_workspaceIndex); s.writeBlob(60, m_geometryBytes); s.writeBool(61, m_hidden); + s.writeString(62, m_checkWXAPIKey); + s.writeString(63, m_mapProvider); for (int i = 0; i < ADSBDEMOD_COLUMNS; i++) { s.writeS32(100 + i, m_columnIndexes[i]); @@ -264,7 +272,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 +314,12 @@ 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, ""); +#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 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..dc9fa0a65 100644 --- a/plugins/channelrx/demodadsb/adsbdemodsettings.h +++ b/plugins/channelrx/demodadsb/adsbdemodsettings.h @@ -147,13 +147,15 @@ 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; 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, 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 2dc426524..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) @@ -75,6 +75,7 @@ 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 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. @@ -90,6 +91,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 +291,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/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"); 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..34406d826 --- /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 (!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 (!std::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 (!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 (!std::isnan(m_temperature)) { + s.append(QString("Temperature: %1 %2C").arg(m_temperature).arg(QChar(0xb0))); + } + if (!std::isnan(m_dewpoint)) { + s.append(QString("Dewpoint: %1 %2C").arg(m_dewpoint).arg(QChar(0xb0))); + } + if (!std::isnan(m_pressure)) { + s.append(QString("Pressure: %1 hPa").arg(m_pressure)); + } + if (!std::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 */