1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2025-02-03 09:44:01 -05:00

ADS-B: Add support for displaying airport weather (METARs) from CheckWX

This commit is contained in:
Jon Beniston 2022-06-06 10:53:10 +01:00
parent 76ed92c985
commit fb516d1ef1
10 changed files with 682 additions and 170 deletions

View File

@ -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<QListWidgetItem *> 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++)
{

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>417</width>
<height>692</height>
<height>714</height>
</rect>
</property>
<property name="font">
@ -22,26 +22,27 @@
<item>
<widget class="QGroupBox" name="groupBox">
<layout class="QGridLayout" name="gridLayout">
<item row="14" column="1">
<widget class="QCheckBox" name="displayStats">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item row="10" column="1">
<widget class="QPushButton" name="font">
<property name="toolTip">
<string>Display demodulator statistics</string>
<string>Select a font for the table</string>
</property>
<property name="text">
<string/>
<string>Select...</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="displayNavAids">
<item row="15" column="0">
<widget class="QLabel" name="verboseModelMatchingLabel">
<property name="text">
<string>Display NAVAIDs</string>
<string>Log 3D model matching information</string>
</property>
</widget>
</item>
<item row="13" column="0">
<widget class="QLabel" name="autoResizeTableColumnsLabel">
<property name="text">
<string>Resize columns after adding aircraft</string>
</property>
</widget>
</item>
@ -62,82 +63,112 @@
</item>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="airspacesLabel">
<item row="2" column="1">
<widget class="QComboBox" name="airportSize">
<property name="toolTip">
<string>Sets the minimum airport size that will be displayed on the map</string>
</property>
<item>
<property name="text">
<string>Small</string>
</property>
</item>
<item>
<property name="text">
<string>Medium</string>
</property>
</item>
<item>
<property name="text">
<string>Large</string>
</property>
</item>
</widget>
</item>
<item row="18" column="1">
<widget class="QSpinBox" name="airfieldElevation">
<property name="toolTip">
<string>Barometric altitude reported by aircraft when on airfield surface</string>
</property>
<property name="minimum">
<number>-10000</number>
</property>
<property name="maximum">
<number>30000</number>
</property>
<property name="singleStep">
<number>10</number>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="airspaceRangeLabel">
<property name="text">
<string>Airspaces to display</string>
<string>Airspace display distance (km)</string>
</property>
</widget>
</item>
<item row="16" column="1">
<widget class="QLineEdit" name="aviationstackAPIKey">
<property name="toolTip">
<string>aviationstack.com API key for accessing flight information</string>
</property>
</widget>
</item>
<item row="16" column="0">
<widget class="QLabel" name="apiKeyLabel">
<widget class="QLabel" name="aviationstackAPIKeyLabel">
<property name="text">
<string>avaitionstack API key</string>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QSpinBox" name="timeout">
<property name="toolTip">
<string>How long in seconds after not receiving any frames will an aircraft be removed from the table and map</string>
</property>
<property name="maximum">
<number>1000000</number>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="heliportsLabel">
<item row="0" column="0">
<widget class="QLabel" name="unitsLabel">
<property name="text">
<string>Display heliports</string>
<string>Units</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QSpinBox" name="airspaceRange">
<property name="toolTip">
<string>Displays airspace within the specified distance in kilometres from My Position</string>
</property>
<property name="maximum">
<number>20000</number>
</property>
</widget>
</item>
<item row="15" column="0">
<widget class="QLabel" name="verboseModelMatchingLabel">
<item row="7" column="0">
<widget class="QLabel" name="displayNavAids">
<property name="text">
<string>Log 3D model matching information</string>
<string>Display NAVAIDs</string>
</property>
</widget>
</item>
<item row="13" column="0">
<widget class="QLabel" name="autoResizeTableColumnsLabel">
<property name="text">
<string>Resize columns after adding aircraft</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QCheckBox" name="navAids">
<item row="8" column="1">
<widget class="QCheckBox" name="photos">
<property name="toolTip">
<string>Display NAVAIDs such as VORs and NDBs</string>
<string>Download and display photos of highlighted aircraft</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="mapTypeLabel">
<item row="14" column="1">
<widget class="QCheckBox" name="displayStats">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Display demodulator statistics</string>
</property>
<property name="text">
<string>Map type</string>
<string/>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="airportRangeLabel">
<item row="13" column="1">
<widget class="QCheckBox" name="autoResizeTableColumns">
<property name="toolTip">
<string>Resize the columns in the table after an aircraft is added to it</string>
</property>
<property name="text">
<string>Airport display distance (km)</string>
<string/>
</property>
</widget>
</item>
@ -151,34 +182,6 @@
</property>
</widget>
</item>
<item row="14" column="0">
<widget class="QLabel" name="displayStatsLabel">
<property name="text">
<string>Display demodulator statistics</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="airportSizeLabel">
<property name="text">
<string>Display airports with size</string>
</property>
</widget>
</item>
<item row="16" column="1">
<widget class="QLineEdit" name="apiKey">
<property name="toolTip">
<string>aviationstack.com API key for accessing flight information</string>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="photosLabel">
<property name="text">
<string>Display aircraft photos</string>
</property>
</widget>
</item>
<item row="15" column="1">
<widget class="QCheckBox" name="verboseModelMatching">
<property name="toolTip">
@ -189,17 +192,37 @@
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="timeoutLabel">
<property name="text">
<string>Aircraft timeout (s)</string>
<item row="6" column="1">
<widget class="QSpinBox" name="airspaceRange">
<property name="toolTip">
<string>Displays airspace within the specified distance in kilometres from My Position</string>
</property>
<property name="maximum">
<number>20000</number>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="unitsLabel">
<item row="1" column="0">
<widget class="QLabel" name="mapTypeLabel">
<property name="text">
<string>Units</string>
<string>Map type</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QCheckBox" name="navAids">
<property name="toolTip">
<string>Display NAVAIDs such as VORs and NDBs</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="14" column="0">
<widget class="QLabel" name="displayStatsLabel">
<property name="text">
<string>Display demodulator statistics</string>
</property>
</widget>
</item>
@ -210,6 +233,20 @@
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="airportRangeLabel">
<property name="text">
<string>Airport display distance (km)</string>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="photosLabel">
<property name="text">
<string>Display aircraft photos</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="mapType">
<property name="toolTip">
@ -394,62 +431,17 @@
</item>
</widget>
</item>
<item row="10" column="1">
<widget class="QPushButton" name="font">
<property name="toolTip">
<string>Select a font for the table</string>
</property>
<item row="2" column="0">
<widget class="QLabel" name="airportSizeLabel">
<property name="text">
<string>Select...</string>
<string>Display airports with size</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="airportSize">
<property name="toolTip">
<string>Sets the minimum airport size that will be displayed on the map</string>
</property>
<item>
<property name="text">
<string>Small</string>
</property>
</item>
<item>
<property name="text">
<string>Medium</string>
</property>
</item>
<item>
<property name="text">
<string>Large</string>
</property>
</item>
</widget>
</item>
<item row="8" column="1">
<widget class="QCheckBox" name="photos">
<property name="toolTip">
<string>Download and display photos of highlighted aircraft</string>
</property>
<item row="9" column="0">
<widget class="QLabel" name="timeoutLabel">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="airspaceRangeLabel">
<property name="text">
<string>Airspace display distance (km)</string>
</property>
</widget>
</item>
<item row="13" column="1">
<widget class="QCheckBox" name="autoResizeTableColumns">
<property name="toolTip">
<string>Resize the columns in the table after an aircraft is added to it</string>
</property>
<property name="text">
<string/>
<string>Aircraft timeout (s)</string>
</property>
</widget>
</item>
@ -463,26 +455,48 @@
</property>
</widget>
</item>
<item row="17" column="0">
<item row="3" column="0">
<widget class="QLabel" name="heliportsLabel">
<property name="text">
<string>Display heliports</string>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QSpinBox" name="timeout">
<property name="toolTip">
<string>How long in seconds after not receiving any frames will an aircraft be removed from the table and map</string>
</property>
<property name="maximum">
<number>1000000</number>
</property>
</widget>
</item>
<item row="18" column="0">
<widget class="QLabel" name="airfieldElevationLabel">
<property name="text">
<string>Airfield barometric altitude (ft)</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="airspacesLabel">
<property name="text">
<string>Airspaces to display</string>
</property>
</widget>
</item>
<item row="17" column="0">
<widget class="QLabel" name="checkWXAPIKeyLabel">
<property name="text">
<string>CheckWX API key</string>
</property>
</widget>
</item>
<item row="17" column="1">
<widget class="QSpinBox" name="airfieldElevation">
<widget class="QLineEdit" name="checkWXAPIKey">
<property name="toolTip">
<string>Barometric altitude reported by aircraft when on airfield surface</string>
</property>
<property name="minimum">
<number>-10000</number>
</property>
<property name="maximum">
<number>30000</number>
</property>
<property name="singleStep">
<number>10</number>
<string>checkwxapi.com API key for accessing airport weather (METARs)</string>
</property>
</widget>
</item>
@ -515,7 +529,10 @@
<tabstop>font</tabstop>
<tabstop>autoResizeTableColumns</tabstop>
<tabstop>displayStats</tabstop>
<tabstop>apiKey</tabstop>
<tabstop>verboseModelMatching</tabstop>
<tabstop>aviationstackAPIKey</tabstop>
<tabstop>checkWXAPIKey</tabstop>
<tabstop>airfieldElevation</tabstop>
</tabstops>
<resources/>
<connections>

View File

@ -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);

View File

@ -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<AirportInformation *> m_airports;
@ -529,6 +547,10 @@ private:
QList<float> m_azimuth;
QList<float> m_elevation;
QList<float> m_range;
QList<QString> 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();

View File

@ -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);

View File

@ -147,7 +147,8 @@ struct ADSBDemodSettings
float m_interpolatorTapsPerPhase;
QList<NotificationSettings *> 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;

View File

@ -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)
<h3>13: Display Flight Path</h3>
@ -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.
<h2>Attribution</h2>

View File

@ -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

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "aviationweather.h"
#include <QDebug>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
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";
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_AVIATIONWEATHER_H
#define INCLUDE_AVIATIONWEATHER_H
#include <QtCore>
#include <QTimer>
#include "export.h"
#include <cmath>
#include <QDateTime>
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 */