1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2025-07-30 12:42:25 -04:00

Merge pull request #1280 from srcejon/adsb_weather

ADS-B and Map updates
This commit is contained in:
Edouard Griffiths 2022-06-06 21:15:03 +02:00 committed by GitHub
commit dfc166fed5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 837 additions and 261 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 25 KiB

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

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>417</width>
<height>692</height>
<height>742</height>
</rect>
</property>
<property name="font">
@ -22,164 +22,50 @@
<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="20" column="1">
<widget class="QSpinBox" name="airfieldElevation">
<property name="toolTip">
<string>Display demodulator statistics</string>
<string>Barometric altitude reported by aircraft when on airfield surface</string>
</property>
<property name="text">
<string/>
<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="7" column="0">
<item row="9" column="0">
<widget class="QLabel" name="displayNavAids">
<property name="text">
<string>Display NAVAIDs</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="units">
<item row="10" column="1">
<widget class="QCheckBox" name="photos">
<property name="toolTip">
<string>The units to use for altitude, speed and climb rate</string>
</property>
<item>
<property name="text">
<string>ft, kn, ft/min</string>
</property>
</item>
<item>
<property name="text">
<string>m, kph, m/s</string>
</property>
</item>
</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="16" column="0">
<widget class="QLabel" name="apiKeyLabel">
<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">
<property name="text">
<string>Display heliports</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">
<property name="text">
<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>
<item row="7" column="1">
<widget class="QCheckBox" name="navAids">
<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">
<property name="text">
<string>Map type</string>
</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="4" column="1">
<widget class="QSpinBox" name="airportRange">
<property name="toolTip">
<string>Displays airports within the specified distance in kilometres from My Position</string>
</property>
<property name="maximum">
<number>20000</number>
</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="autoResizeTableColumns">
<property name="toolTip">
<string>Resize the columns in the table after an aircraft is added to it</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="17" column="1">
<widget class="QCheckBox" name="verboseModelMatching">
<property name="toolTip">
<string>Log information about how aircraft are matched to 3D models</string>
@ -189,28 +75,48 @@
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="timeoutLabel">
<property name="text">
<string>Aircraft timeout (s)</string>
<item row="8" 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="16" column="0">
<widget class="QLabel" name="displayStatsLabel">
<property name="text">
<string>Units</string>
<string>Display demodulator statistics</string>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="fontLabel">
<item row="9" column="1">
<widget class="QCheckBox" name="navAids">
<property name="toolTip">
<string>Display NAVAIDs such as VORs and NDBs</string>
</property>
<property name="text">
<string>Table font</string>
<string/>
</property>
</widget>
</item>
<item row="1" column="1">
<item row="2" column="0">
<widget class="QLabel" name="mapTypeLabel">
<property name="text">
<string>Map type</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="airportRangeLabel">
<property name="text">
<string>Airport display distance (km)</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="mapType">
<property name="toolTip">
<string>Type of map to display</string>
@ -237,7 +143,200 @@
</item>
</widget>
</item>
<item row="5" column="1">
<item row="4" column="0">
<widget class="QLabel" name="airportSizeLabel">
<property name="text">
<string>Display airports with size</string>
</property>
</widget>
</item>
<item row="11" column="0">
<widget class="QLabel" name="timeoutLabel">
<property name="text">
<string>Aircraft timeout (s)</string>
</property>
</widget>
</item>
<item row="19" column="0">
<widget class="QLabel" name="checkWXAPIKeyLabel">
<property name="text">
<string>CheckWX API key</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="heliportsLabel">
<property name="text">
<string>Display heliports</string>
</property>
</widget>
</item>
<item row="11" 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="12" column="1">
<widget class="QPushButton" name="font">
<property name="toolTip">
<string>Select a font for the table</string>
</property>
<property name="text">
<string>Select...</string>
</property>
</widget>
</item>
<item row="17" column="0">
<widget class="QLabel" name="verboseModelMatchingLabel">
<property name="text">
<string>Log 3D model matching information</string>
</property>
</widget>
</item>
<item row="15" column="0">
<widget class="QLabel" name="autoResizeTableColumnsLabel">
<property name="text">
<string>Resize columns after adding aircraft</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="units">
<property name="toolTip">
<string>The units to use for altitude, speed and climb rate</string>
</property>
<item>
<property name="text">
<string>ft, kn, ft/min</string>
</property>
</item>
<item>
<property name="text">
<string>m, kph, m/s</string>
</property>
</item>
</widget>
</item>
<item row="4" 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="1" column="1">
<widget class="QComboBox" name="mapProvider">
<property name="toolTip">
<string>Mapping service</string>
</property>
<item>
<property name="text">
<string>osm</string>
</property>
</item>
<item>
<property name="text">
<string>mapboxgl</string>
</property>
</item>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="airspaceRangeLabel">
<property name="text">
<string>Airspace display distance (km)</string>
</property>
</widget>
</item>
<item row="18" 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="18" column="0">
<widget class="QLabel" name="aviationstackAPIKeyLabel">
<property name="text">
<string>avaitionstack API key</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="mapProviderLabel">
<property name="text">
<string>Map provider</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="unitsLabel">
<property name="text">
<string>Units</string>
</property>
</widget>
</item>
<item row="16" 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/>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QSpinBox" name="airportRange">
<property name="toolTip">
<string>Displays airports within the specified distance in kilometres from My Position</string>
</property>
<property name="maximum">
<number>20000</number>
</property>
</widget>
</item>
<item row="12" column="0">
<widget class="QLabel" name="fontLabel">
<property name="text">
<string>Table font</string>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="photosLabel">
<property name="text">
<string>Display aircraft photos</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QListWidget" name="airspaces">
<property name="toolTip">
<string>Airspace categories to display</string>
@ -394,66 +493,14 @@
</item>
</widget>
</item>
<item row="10" column="1">
<widget class="QPushButton" name="font">
<item row="19" column="1">
<widget class="QLineEdit" name="checkWXAPIKey">
<property name="toolTip">
<string>Select a font for the table</string>
</property>
<property name="text">
<string>Select...</string>
<string>checkwxapi.com API key for accessing airport weather (METARs)</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>
<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/>
</property>
</widget>
</item>
<item row="3" column="1">
<item row="5" column="1">
<widget class="QCheckBox" name="heliports">
<property name="toolTip">
<string>When checked, heliports are displayed on the map</string>
@ -463,26 +510,17 @@
</property>
</widget>
</item>
<item row="17" column="0">
<item row="20" column="0">
<widget class="QLabel" name="airfieldElevationLabel">
<property name="text">
<string>Airfield barometric altitude (ft)</string>
</property>
</widget>
</item>
<item row="17" 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>
<item row="7" column="0">
<widget class="QLabel" name="airspacesLabel">
<property name="text">
<string>Airspaces to display</string>
</property>
</widget>
</item>
@ -503,6 +541,7 @@
</widget>
<tabstops>
<tabstop>units</tabstop>
<tabstop>mapProvider</tabstop>
<tabstop>mapType</tabstop>
<tabstop>airportSize</tabstop>
<tabstop>heliports</tabstop>
@ -515,7 +554,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;
}
@ -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);

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

View File

@ -147,13 +147,15 @@ 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;
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,

View File

@ -118,7 +118,7 @@ Item {
activeMapType = supportedMapTypes[i]
}
}
lightIcons = requestedMapType == "Night Transit Map"
lightIcons = (requestedMapType == "Night Transit Map") || (requestedMapType == "mapbox://styles/mapbox/dark-v9")
}
}

View File

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

View File

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

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 (!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 */