ADS-B Updates:

Support different map types (Aviation, Street and Satellite)
Add display of airspaces and NAVAIDs.
Display photo of highlighted aircraft.
This commit is contained in:
Jon Beniston 2021-11-12 16:51:23 +00:00
parent 006da4e872
commit 60a7b63cc1
36 changed files with 2863 additions and 239 deletions

View File

@ -82,7 +82,7 @@ for:
qttools5-dev qttools5-dev-tools qtmultimedia5-dev libqt5multimedia5-plugins libqt5websockets5-dev \
libqt5quick5 \
qml-module-qtlocation qml-module-qtpositioning qml-module-qtquick-window2 qml-module-qtquick-dialogs \
qml-module-qtquick-controls qml-module-qtquick-controls2 qml-module-qtquick-layouts \
qml-module-qtquick-controls qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qtgraphicaleffects \
libqt5serialport5-dev qtdeclarative5-dev qtpositioning5-dev qtlocation5-dev \
libqt5charts5-dev libqt5texttospeech5-dev libfaad-dev zlib1g-dev \
libusb-1.0-0-dev libboost-all-dev libasound2-dev libopencv-dev libopencv-imgcodecs-dev \

1
debian/control vendored
View File

@ -22,6 +22,7 @@ Build-Depends: debhelper (>= 9),
qml-module-qtquick-controls,
qml-module-qtquick-controls2,
qml-module-qtquick-layouts,
qml-module-qtgraphicaleffects,
libqt5serialport5-dev,
libqt5charts5-dev,
libqt5texttospeech5-dev,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 826 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 603 KiB

After

Width:  |  Height:  |  Size: 490 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -42,6 +42,7 @@ if(NOT SERVER_MODE)
adsbdemoddisplaydialog.ui
adsbdemodnotificationdialog.cpp
adsbdemodnotificationdialog.ui
adsbosmtemplateserver.cpp
airlinelogos.qrc
flags.qrc
map.qrc
@ -53,6 +54,7 @@ if(NOT SERVER_MODE)
adsbdemodfeeddialog.h
adsbdemoddisplaydialog.h
adsbdemodnotificationdialog.h
adsbosmtemplateserver.h
ourairports.h
osndb.h
)
@ -87,3 +89,8 @@ target_link_libraries(${TARGET_NAME}
install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER})
if(WIN32)
# Run deployqt for QtGraphicalEffects, which isn't used in other plugins
include(DeployQt)
windeployqt(${TARGET_NAME} ${SDRANGEL_BINARY_BIN_DIR} ${PROJECT_SOURCE_DIR}/map)
endif()

View File

@ -23,7 +23,8 @@
ADSBDemodDisplayDialog::ADSBDemodDisplayDialog(
int removeTimeout, float airportRange, ADSBDemodSettings::AirportType airportMinimumSize,
bool displayHeliports, bool siUnits, QString fontName, int fontSize, bool displayDemodStats,
bool autoResizeTableColumns, const QString& apiKey, QWidget* parent) :
bool autoResizeTableColumns, const QString& apiKey, QStringList airspaces, float airspaceRange,
ADSBDemodSettings::MapType mapType, bool displayNavAids, bool displayPhotos, QWidget* parent) :
QDialog(parent),
m_fontName(fontName),
m_fontSize(fontSize),
@ -38,6 +39,17 @@ ADSBDemodDisplayDialog::ADSBDemodDisplayDialog(
ui->displayStats->setChecked(displayDemodStats);
ui->autoResizeTableColumns->setChecked(autoResizeTableColumns);
ui->apiKey->setText(apiKey);
for (const auto& airspace: airspaces)
{
QList<QListWidgetItem *> items = ui->airspaces->findItems(airspace, Qt::MatchExactly);
for (const auto& item: items) {
item->setCheckState(Qt::Checked);
}
}
ui->airspaceRange->setValue(airspaceRange);
ui->mapType->setCurrentIndex((int)mapType);
ui->navAids->setChecked(displayNavAids);
ui->photos->setChecked(displayPhotos);
}
ADSBDemodDisplayDialog::~ADSBDemodDisplayDialog()
@ -55,6 +67,18 @@ void ADSBDemodDisplayDialog::accept()
m_displayDemodStats = ui->displayStats->isChecked();
m_autoResizeTableColumns = ui->autoResizeTableColumns->isChecked();
m_apiKey = ui->apiKey->text();
m_airspaces = QStringList();
for (int i = 0; i < ui->airspaces->count(); i++)
{
QListWidgetItem *item = ui->airspaces->item(i);
if (item->checkState() == Qt::Checked) {
m_airspaces.append(item->text());
}
}
m_airspaceRange = ui->airspaceRange->value();
m_mapType = (ADSBDemodSettings::MapType)ui->mapType->currentIndex();
m_displayNavAids = ui->navAids->isChecked();
m_displayPhotos = ui->photos->isChecked();
QDialog::accept();
}

View File

@ -27,7 +27,8 @@ class ADSBDemodDisplayDialog : public QDialog {
public:
explicit ADSBDemodDisplayDialog(int removeTimeout, float airportRange, ADSBDemodSettings::AirportType airportMinimumSize,
bool displayHeliports, bool siUnits, QString fontName, int fontSize, bool displayDemodStats,
bool autoResizeTableColumns, const QString& apiKey, QWidget* parent = 0);
bool autoResizeTableColumns, const QString& apiKey, QStringList airspaces, float airspaceRange,
ADSBDemodSettings::MapType mapType, bool displayNavAids, bool displayPhotos, QWidget* parent = 0);
~ADSBDemodDisplayDialog();
int m_removeTimeout;
@ -40,6 +41,11 @@ public:
bool m_displayDemodStats;
bool m_autoResizeTableColumns;
QString m_apiKey;
QStringList m_airspaces;
float m_airspaceRange;
ADSBDemodSettings::MapType m_mapType;
bool m_displayNavAids;
bool m_displayPhotos;
private slots:
void accept();

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>417</width>
<height>287</height>
<height>638</height>
</rect>
</property>
<property name="font">
@ -23,38 +23,14 @@
<item>
<widget class="QGroupBox" name="groupBox">
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<widget class="QLabel" name="airportSizeLabel">
<item row="14" column="0">
<widget class="QLabel" name="displayStatsLabel">
<property name="text">
<string>Display airports with size</string>
<string>Display demodulator statistics</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="5" column="0">
<widget class="QLabel" name="timeoutLabel">
<property name="text">
<string>Aircraft timeout (s)</string>
</property>
</widget>
</item>
<item row="3" column="1">
<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>
@ -64,7 +40,219 @@
</property>
</widget>
</item>
<item row="1" column="1">
<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="1" column="0">
<widget class="QLabel" name="mapTypeLabel">
<property name="text">
<string>Map type</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="15" 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="5" column="1">
<widget class="QListWidget" name="airspaces">
<property name="toolTip">
<string>Airspace categories to display</string>
</property>
<item>
<property name="text">
<string>A</string>
</property>
<property name="toolTip">
<string>IFR only</string>
</property>
<property name="checkState">
<enum>Unchecked</enum>
</property>
</item>
<item>
<property name="text">
<string>B</string>
</property>
<property name="toolTip">
<string>IFR and VFR with ATC clearance</string>
</property>
<property name="checkState">
<enum>Unchecked</enum>
</property>
</item>
<item>
<property name="text">
<string>C</string>
</property>
<property name="toolTip">
<string>IFR and VFR with ATC clearance</string>
</property>
<property name="checkState">
<enum>Unchecked</enum>
</property>
</item>
<item>
<property name="text">
<string>D</string>
</property>
<property name="toolTip">
<string>IFR and VFR with ATC clearance</string>
</property>
<property name="checkState">
<enum>Unchecked</enum>
</property>
</item>
<item>
<property name="text">
<string>E</string>
</property>
<property name="toolTip">
<string>IFR with clearance and VFR</string>
</property>
<property name="checkState">
<enum>Unchecked</enum>
</property>
</item>
<item>
<property name="text">
<string>G</string>
</property>
<property name="toolTip">
<string>Uncontrolled</string>
</property>
<property name="checkState">
<enum>Unchecked</enum>
</property>
</item>
<item>
<property name="text">
<string>FIR</string>
</property>
<property name="toolTip">
<string>Flight Information Region</string>
</property>
<property name="checkState">
<enum>Unchecked</enum>
</property>
</item>
<item>
<property name="text">
<string>CTR</string>
</property>
<property name="toolTip">
<string>Controlled Traffic Region</string>
</property>
<property name="checkState">
<enum>Unchecked</enum>
</property>
</item>
<item>
<property name="text">
<string>TMZ</string>
</property>
<property name="toolTip">
<string>Transponder Mandatory Zone</string>
</property>
<property name="checkState">
<enum>Unchecked</enum>
</property>
</item>
<item>
<property name="text">
<string>RMZ</string>
</property>
<property name="toolTip">
<string>Radio Mandatory Zone</string>
</property>
<property name="checkState">
<enum>Unchecked</enum>
</property>
</item>
<item>
<property name="text">
<string>RESTRICTED</string>
</property>
<property name="checkState">
<enum>Unchecked</enum>
</property>
</item>
<item>
<property name="text">
<string>GLIDING</string>
</property>
<property name="checkState">
<enum>Unchecked</enum>
</property>
</item>
<item>
<property name="text">
<string>DANGER</string>
</property>
<property name="checkState">
<enum>Unchecked</enum>
</property>
</item>
<item>
<property name="text">
<string>PROHIBITED</string>
</property>
<property name="checkState">
<enum>Unchecked</enum>
</property>
</item>
<item>
<property name="text">
<string>WAVE</string>
</property>
<property name="checkState">
<enum>Unchecked</enum>
</property>
</item>
</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="7" column="0">
<widget class="QLabel" name="displayNavAids">
<property name="text">
<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="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>
@ -86,95 +274,14 @@
</item>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="unitsLabel">
<property name="text">
<string>Units</string>
</property>
</widget>
</item>
<item row="6" 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="5" 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="airportRangeLabel">
<widget class="QLabel" name="heliportsLabel">
<property name="text">
<string>Airport display distance (km)</string>
<string>Display heliports</string>
</property>
</widget>
</item>
<item row="6" 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="apiKeyLabel">
<property name="text">
<string>avaitionstack API key</string>
</property>
</widget>
</item>
<item row="11" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="9" 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="9" column="0">
<widget class="QLabel" name="autoResizeTableColumnsLabel">
<property name="text">
<string>Resize columns after adding aircraft</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="heliports">
<property name="toolTip">
<string>When checked, heliports are displayed on the map</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="7" column="1">
<item row="14" column="1">
<widget class="QCheckBox" name="displayStats">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
@ -191,23 +298,152 @@
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="heliportsLabel">
<widget class="QLabel" name="airportSizeLabel">
<property name="text">
<string>Display heliports</string>
<string>Display airports with size</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="displayStatsLabel">
<item row="15" column="0">
<widget class="QLabel" name="apiKeyLabel">
<property name="text">
<string>Display demodulator statistics</string>
<string>avaitionstack API key</string>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="timeoutLabel">
<property name="text">
<string>Aircraft timeout (s)</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="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="0" column="0">
<widget class="QLabel" name="unitsLabel">
<property name="text">
<string>Units</string>
</property>
</widget>
</item>
<item row="10" column="1">
<widget class="QLineEdit" name="apiKey">
<widget class="QPushButton" name="font">
<property name="toolTip">
<string>aviationstack.com API key for accessing flight information</string>
<string>Select a font for the table</string>
</property>
<property name="text">
<string>Select...</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="1" column="1">
<widget class="QComboBox" name="mapType">
<property name="toolTip">
<string>Type of map to display</string>
</property>
<item>
<property name="text">
<string>Aviation</string>
</property>
</item>
<item>
<property name="text">
<string>Aviation (Dark)</string>
</property>
</item>
<item>
<property name="text">
<string>Street</string>
</property>
</item>
<item>
<property name="text">
<string>Satellite</string>
</property>
</item>
</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="3" column="1">
<widget class="QCheckBox" name="heliports">
<property name="toolTip">
<string>When checked, heliports are displayed on the map</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="fontLabel">
<property name="text">
<string>Table font</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="8" column="0">
<widget class="QLabel" name="photosLabel">
<property name="text">
<string>Display aircraft photos</string>
</property>
</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>
@ -226,6 +462,22 @@
</item>
</layout>
</widget>
<tabstops>
<tabstop>units</tabstop>
<tabstop>mapType</tabstop>
<tabstop>airportSize</tabstop>
<tabstop>heliports</tabstop>
<tabstop>airportRange</tabstop>
<tabstop>airspaces</tabstop>
<tabstop>airspaceRange</tabstop>
<tabstop>navAids</tabstop>
<tabstop>photos</tabstop>
<tabstop>timeout</tabstop>
<tabstop>font</tabstop>
<tabstop>autoResizeTableColumns</tabstop>
<tabstop>displayStats</tabstop>
<tabstop>apiKey</tabstop>
</tabstops>
<resources/>
<connections>
<connection>

View File

@ -30,6 +30,7 @@
#include <QDebug>
#include <QProcess>
#include <QFileDialog>
#include <QQmlProperty>
#include "SWGMapItem.h"
@ -39,9 +40,11 @@
#include "util/simpleserializer.h"
#include "util/db.h"
#include "util/units.h"
#include "util/morse.h"
#include "gui/basicchannelsettingsdialog.h"
#include "gui/devicestreamselectiondialog.h"
#include "gui/crightclickenabler.h"
#include "gui/clickablelabel.h"
#include "dsp/dspengine.h"
#include "mainwindow.h"
@ -52,6 +55,7 @@
#include "adsbdemoddisplaydialog.h"
#include "adsbdemodnotificationdialog.h"
#include "adsb.h"
#include "adsbosmtemplateserver.h"
const char *Aircraft::m_speedTypeNames[] = {
"GS", "TAS", "IAS"
@ -446,6 +450,152 @@ bool AirportModel::setData(const QModelIndex &index, const QVariant& value, int
return true;
}
QVariant AirspaceModel::data(const QModelIndex &index, int role) const
{
int row = index.row();
if ((row < 0) || (row >= m_airspaces.count())) {
return QVariant();
}
if (role == AirspaceModel::nameRole)
{
// Airspace name
return QVariant::fromValue(m_airspaces[row]->m_name);
}
else if (role == AirspaceModel::detailsRole)
{
// Airspace name and altitudes
QString details;
details.append(m_airspaces[row]->m_name);
details.append(QString("\n%1 - %2")
.arg(m_airspaces[row]->getAlt(&m_airspaces[row]->m_bottom))
.arg(m_airspaces[row]->getAlt(&m_airspaces[row]->m_top)));
return QVariant::fromValue(details);
}
else if (role == AirspaceModel::positionRole)
{
// Coordinates to display the airspace name at
QGeoCoordinate coords;
coords.setLatitude(m_airspaces[row]->m_position.y());
coords.setLongitude(m_airspaces[row]->m_position.x());
coords.setAltitude(m_airspaces[row]->topHeightInMetres());
return QVariant::fromValue(coords);
}
else if (role == AirspaceModel::airspaceBorderColorRole)
{
if (m_airspaces[row]->m_category == "D")
{
return QVariant::fromValue(QColor("blue"));
}
else
{
return QVariant::fromValue(QColor("red"));
}
}
else if (role == AirspaceModel::airspaceFillColorRole)
{
if (m_airspaces[row]->m_category == "D")
{
return QVariant::fromValue(QColor(0x00, 0x00, 0xff, 0x10));
}
else
{
return QVariant::fromValue(QColor(0xff, 0x00, 0x00, 0x10));
}
}
else if (role == AirspaceModel::airspacePolygonRole)
{
return m_polygons[row];
}
return QVariant();
}
bool AirspaceModel::setData(const QModelIndex &index, const QVariant& value, int role)
{
int row = index.row();
if ((row < 0) || (row >= m_airspaces.count())) {
return false;
}
return true;
}
QVariant NavAidModel::data(const QModelIndex &index, int role) const
{
int row = index.row();
if ((row < 0) || (row >= m_navAids.count())) {
return QVariant();
}
if (role == NavAidModel::positionRole)
{
// Coordinates to display the VOR icon at
QGeoCoordinate coords;
coords.setLatitude(m_navAids[row]->m_latitude);
coords.setLongitude(m_navAids[row]->m_longitude);
coords.setAltitude(Units::feetToMetres(m_navAids[row]->m_elevation));
return QVariant::fromValue(coords);
}
else if (role == NavAidModel::navAidDataRole)
{
// Create the text to go in the bubble next to the VOR
if (m_selected[row])
{
QStringList list;
list.append(QString("Name: %1").arg(m_navAids[row]->m_name));
if (m_navAids[row]->m_type == "NDB") {
list.append(QString("Frequency: %1 kHz").arg(m_navAids[row]->m_frequencykHz, 0, 'f', 1));
} else {
list.append(QString("Frequency: %1 MHz").arg(m_navAids[row]->m_frequencykHz / 1000.0f, 0, 'f', 2));
}
if (m_navAids[row]->m_channel != "") {
list.append(QString("Channel: %1").arg(m_navAids[row]->m_channel));
}
list.append(QString("Ident: %1 %2").arg(m_navAids[row]->m_ident).arg(Morse::toSpacedUnicodeMorse(m_navAids[row]->m_ident)));
list.append(QString("Range: %1 nm").arg(m_navAids[row]->m_range));
if (m_navAids[row]->m_alignedTrueNorth) {
list.append(QString("Magnetic declination: Aligned to true North"));
} else if (m_navAids[row]->m_magneticDeclination != 0.0f) {
list.append(QString("Magnetic declination: %1%2").arg(std::round(m_navAids[row]->m_magneticDeclination)).arg(QChar(0x00b0)));
}
QString data = list.join("\n");
return QVariant::fromValue(data);
}
else
{
return QVariant::fromValue(m_navAids[row]->m_name);
}
}
else if (role == NavAidModel::navAidImageRole)
{
// Select an image to use for the NavAid
return QVariant::fromValue(QString("%1.png").arg(m_navAids[row]->m_type));
}
else if (role == NavAidModel::bubbleColourRole)
{
// Select a background colour for the text bubble next to the NavAid
return QVariant::fromValue(QColor("lightgreen"));
}
else if (role == NavAidModel::selectedRole)
{
return QVariant::fromValue(m_selected[row]);
}
return QVariant();
}
bool NavAidModel::setData(const QModelIndex &index, const QVariant& value, int role)
{
int row = index.row();
if ((row < 0) || (row >= m_navAids.count())) {
return false;
}
if (role == NavAidModel::selectedRole)
{
bool selected = value.toBool();
m_selected[row] = selected;
emit dataChanged(index, index);
return true;
}
return true;
}
// Set selected device to the given centre frequency (used to tune to ATC selected from airports on map)
bool ADSBDemodGUI::setFrequency(float targetFrequencyHz)
{
@ -632,6 +782,9 @@ void ADSBDemodGUI::handleADSB(
"Reserved"
};
bool newAircraft = false;
bool updatedCallsign = false;
int df = (data[0] >> 3) & ADS_B_DF_MASK; // Downlink format
int ca = data[0] & 0x7; // Capability
unsigned icao = ((data[1] & 0xff) << 16) | ((data[2] & 0xff) << 8) | (data[3] & 0xff); // ICAO aircraft address
@ -646,6 +799,7 @@ void ADSBDemodGUI::handleADSB(
else
{
// Add new aircraft
newAircraft = true;
aircraft = new Aircraft(this);
aircraft->m_icao = icao;
m_aircraft.insert(icao, aircraft);
@ -819,8 +973,11 @@ void ADSBDemodGUI::handleADSB(
for (int i = 0; i < 8; i++)
callsign[i] = idMap[c[i]];
callsign[8] = '\0';
QString callsignTrimmed = QString(callsign).trimmed();
aircraft->m_callsign = QString(callsign).trimmed();
updatedCallsign = aircraft->m_callsign != callsignTrimmed;
aircraft->m_callsign = callsignTrimmed;
aircraft->m_callsignItem->setText(aircraft->m_callsign);
// Attempt to map callsign to flight number
@ -1193,6 +1350,11 @@ void ADSBDemodGUI::handleADSB(
// Check to see if we need to emit a notification about this aircraft
checkDynamicNotification(aircraft);
// Update text below photo if it's likely to have changed
if ((aircraft == m_highlightAircraft) && (newAircraft || updatedCallsign)) {
updatePhotoText(aircraft);
}
}
void ADSBDemodGUI::checkStaticNotification(Aircraft *aircraft)
@ -1527,8 +1689,9 @@ void ADSBDemodGUI::on_adsbData_cellClicked(int row, int column)
(void) column;
// Get ICAO of aircraft in row clicked
int icao = ui->adsbData->item(row, 0)->text().toInt(nullptr, 16);
if (m_aircraft.contains(icao))
if (m_aircraft.contains(icao)) {
highlightAircraft(m_aircraft.value(icao));
}
}
void ADSBDemodGUI::on_adsbData_cellDoubleClicked(int row, int column)
@ -1675,6 +1838,18 @@ void ADSBDemodGUI::on_getAirportDB_clicked()
}
}
void ADSBDemodGUI::on_getAirspacesDB_clicked()
{
// Don't try to download while already in progress
if (m_progressDialog == nullptr)
{
m_progressDialog = new QProgressDialog(this);
m_progressDialog->setMaximum(OpenAIP::m_countryCodes.size());
m_progressDialog->setCancelButton(nullptr);
m_openAIP.downloadAirspaces();
}
}
void ADSBDemodGUI::on_flightPaths_clicked(bool checked)
{
m_settings.m_flightPaths = checked;
@ -1960,6 +2135,46 @@ void ADSBDemodGUI::updateAirports()
m_currentDisplayHeliports = m_settings.m_displayHeliports;
}
void ADSBDemodGUI::updateAirspaces()
{
AzEl azEl = m_azEl;
m_airspaceModel.removeAllAirspaces();
for (const auto& airspace: m_airspaces)
{
if (m_settings.m_airspaces.contains(airspace->m_category))
{
// Calculate distance to airspace from My Position
azEl.setTarget(airspace->m_center.y(), airspace->m_center.x(), 0);
azEl.calculate();
// Only display airport if in range
if (azEl.getDistance() <= m_settings.m_airspaceRange*1000.0f) {
m_airspaceModel.addAirspace(airspace);
}
}
}
}
void ADSBDemodGUI::updateNavAids()
{
AzEl azEl = m_azEl;
m_navAidModel.removeAllNavAids();
if (m_settings.m_displayNavAids)
{
for (const auto& navAid: m_navAids)
{
// Calculate distance to NavAid from My Position
azEl.setTarget(navAid->m_latitude, navAid->m_longitude, Units::feetToMetres(navAid->m_elevation));
azEl.calculate();
// Only display NavAid if in range
if (azEl.getDistance() <= m_settings.m_airspaceRange*1000.0f) {
m_navAidModel.addNavAid(navAid);
}
}
}
}
// Set a static target, such as an airport
void ADSBDemodGUI::target(const QString& name, float az, float el, float range)
{
@ -1993,10 +2208,102 @@ void ADSBDemodGUI::targetAircraft(Aircraft *aircraft)
}
}
void ADSBDemodGUI::updatePhotoText(Aircraft *aircraft)
{
if (m_settings.m_displayPhotos)
{
QString callsign = aircraft->m_callsignItem->text().trimmed();
QString reg = aircraft->m_registrationItem->text().trimmed();
if (!callsign.isEmpty() && !reg.isEmpty()) {
ui->photoHeader->setText(QString("%1 - %2").arg(callsign).arg(reg));
} else if (!callsign.isEmpty()) {
ui->photoHeader->setText(QString("%1").arg(callsign));
} else if (!reg.isEmpty()) {
ui->photoHeader->setText(QString("%1").arg(reg));
}
QIcon icon = aircraft->m_countryItem->icon();
QList<QSize> sizes = icon.availableSizes();
if (sizes.size() > 0) {
ui->photoFlag->setPixmap(icon.pixmap(sizes[0]));
}
updatePhotoFlightInformation(aircraft);
QString aircraftDetails = "<table width=200>"; // Note, Qt seems to make the table bigger than this so text is cropped, not wrapped
QString manufacturer = aircraft->m_manufacturerNameItem->text();
if (!manufacturer.isEmpty()) {
aircraftDetails.append(QString("<tr><th align=left>Manufacturer:<td>%1").arg(manufacturer));
}
QString model = aircraft->m_modelItem->text();
if (!model.isEmpty()) {
aircraftDetails.append(QString("<tr><th align=left>Aircraft:<td>%1").arg(model));
}
QString owner = aircraft->m_ownerItem->text();
if (!owner.isEmpty()) {
aircraftDetails.append(QString("<tr><th align=left>Owner:<td>%1").arg(owner));
}
QString operatorICAO = aircraft->m_operatorICAOItem->text();
if (!operatorICAO.isEmpty()) {
aircraftDetails.append(QString("<tr><th align=left>Operator:<td>%1").arg(operatorICAO));
}
QString registered = aircraft->m_registeredItem->text();
if (!registered.isEmpty()) {
aircraftDetails.append(QString("<tr><th align=left>Registered:<td>%1").arg(registered));
}
aircraftDetails.append("</table>");
ui->aircraftDetails->setText(aircraftDetails);
}
}
void ADSBDemodGUI::updatePhotoFlightInformation(Aircraft *aircraft)
{
if (m_settings.m_displayPhotos)
{
QString dep = aircraft->m_depItem->text();
QString arr = aircraft->m_arrItem->text();
QString std = aircraft->m_stdItem->text();
QString etd = aircraft->m_etdItem->text();
QString atd = aircraft->m_atdItem->text();
QString sta = aircraft->m_staItem->text();
QString eta = aircraft->m_etaItem->text();
QString ata = aircraft->m_ataItem->text();
QString flightDetails;
if (!dep.isEmpty() && !arr.isEmpty())
{
flightDetails = QString("<center><table width=200><tr><th colspan=4>%1 - %2").arg(dep).arg(arr);
if (!std.isEmpty() && !sta.isEmpty()) {
flightDetails.append(QString("<tr><td>STD<td>%1<td>STA<td>%2").arg(std).arg(sta));
}
if ((!atd.isEmpty() || !etd.isEmpty()) && (!ata.isEmpty() || !eta.isEmpty()))
{
if (!atd.isEmpty()) {
flightDetails.append(QString("<tr><td>Actual<td>%1").arg(atd));
} else if (!etd.isEmpty()) {
flightDetails.append(QString("<tr><td>Estimated<td>%1").arg(etd));
}
if (!ata.isEmpty()) {
flightDetails.append(QString("<td>Actual<td>%1").arg(ata));
} else if (!eta.isEmpty()) {
flightDetails.append(QString("<td>Estimated<td>%1").arg(eta));
}
}
flightDetails.append("</center>");
}
ui->flightDetails->setText(flightDetails);
}
}
void ADSBDemodGUI::highlightAircraft(Aircraft *aircraft)
{
if (aircraft != m_highlightAircraft)
{
// Hide photo of old aircraft
ui->photoHeader->setVisible(false);
ui->photoFlag->setVisible(false);
ui->photo->setVisible(false);
ui->flightDetails->setVisible(false);
ui->aircraftDetails->setVisible(false);
if (m_highlightAircraft)
{
// Restore colour
@ -2005,12 +2312,28 @@ void ADSBDemodGUI::highlightAircraft(Aircraft *aircraft)
}
// Highlight this aircraft
m_highlightAircraft = aircraft;
aircraft->m_isHighlighted = true;
m_aircraftModel.aircraftUpdated(aircraft);
if (aircraft)
{
aircraft->m_isHighlighted = true;
m_aircraftModel.aircraftUpdated(aircraft);
if (m_settings.m_displayPhotos)
{
// Download photo
updatePhotoText(aircraft);
m_planeSpotters.getAircraftPhoto(QString::number(aircraft->m_icao, 16));
}
}
}
if (aircraft)
{
// Highlight the row in the table - always do this, as it can become
// unselected
ui->adsbData->selectRow(aircraft->m_icaoItem->row());
}
else
{
ui->adsbData->clearSelection();
}
// Highlight the row in the table - always do this, as it can become
// unselected
ui->adsbData->selectRow(aircraft->m_icaoItem->row());
}
// Show feed dialog
@ -2033,7 +2356,8 @@ void ADSBDemodGUI::on_displaySettings_clicked()
m_settings.m_displayHeliports, m_settings.m_siUnits,
m_settings.m_tableFontName, m_settings.m_tableFontSize,
m_settings.m_displayDemodStats, m_settings.m_autoResizeTableColumns,
m_settings.m_apiKey);
m_settings.m_apiKey, m_settings.m_airspaces, m_settings.m_airspaceRange,
m_settings.m_mapType, m_settings.m_displayNavAids, m_settings.m_displayPhotos);
if (dialog.exec() == QDialog::Accepted)
{
bool unitsChanged = m_settings.m_siUnits != dialog.m_siUnits;
@ -2048,14 +2372,79 @@ void ADSBDemodGUI::on_displaySettings_clicked()
m_settings.m_displayDemodStats = dialog.m_displayDemodStats;
m_settings.m_autoResizeTableColumns = dialog.m_autoResizeTableColumns;
m_settings.m_apiKey = dialog.m_apiKey;
m_settings.m_airspaces = dialog.m_airspaces;
m_settings.m_airspaceRange = dialog.m_airspaceRange;
m_settings.m_mapType = dialog.m_mapType;
m_settings.m_displayNavAids = dialog.m_displayNavAids;
m_settings.m_displayPhotos = dialog.m_displayPhotos;
if (unitsChanged)
if (unitsChanged) {
m_aircraftModel.allAircraftUpdated();
}
displaySettings();
applySettings();
}
}
void ADSBDemodGUI::applyMapSettings()
{
QQuickItem *item = ui->map->rootObject();
// Save existing position of map
QObject *object = item->findChild<QObject*>("map");
QGeoCoordinate coords;
double zoom;
if (object != nullptr)
{
coords = object->property("center").value<QGeoCoordinate>();
zoom = object->property("zoomLevel").value<double>();
}
// Create the map using the specified provider
QQmlProperty::write(item, "mapProvider", "osm");
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
parameters["osm.mapping.cache.directory"] = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + "/QtLocation/osm/sdrangel/adsb";
QString mapType;
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;
}
QMetaObject::invokeMethod(item, "createMap",
Q_ARG(QVariant, QVariant::fromValue(parameters)),
Q_ARG(QVariant, mapType),
Q_ARG(QVariant, QVariant::fromValue(this)));
// Restore position of map
object = item->findChild<QObject*>("map");
if ((object != nullptr) && coords.isValid())
{
qDebug() << "Restoring map " << coords.toString() << zoom;
object->setProperty("zoomLevel", QVariant::fromValue(zoom));
object->setProperty("center", QVariant::fromValue(coords));
}
}
// Called from QML when empty space clicked
void ADSBDemodGUI::clearHighlighted()
{
highlightAircraft(nullptr);
}
ADSBDemodGUI::ADSBDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent) :
ChannelGUI(parent),
ui(new Ui::ADSBDemodGUI),
@ -2067,14 +2456,20 @@ ADSBDemodGUI::ADSBDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseb
m_tickCount(0),
m_aircraftInfo(nullptr),
m_airportModel(this),
m_airspaceModel(this),
m_trackAircraft(nullptr),
m_highlightAircraft(nullptr),
m_progressDialog(nullptr)
{
ui->setupUi(this);
m_osmPort = 0; // Pick a free port
m_templateServer = new ADSBOSMTemplateServer("q2RVNAe3eFKCH4XsrE3r", m_osmPort);
ui->map->rootContext()->setContextProperty("aircraftModel", &m_aircraftModel);
ui->map->rootContext()->setContextProperty("airportModel", &m_airportModel);
ui->map->rootContext()->setContextProperty("airspaceModel", &m_airspaceModel);
ui->map->rootContext()->setContextProperty("navAidModel", &m_navAidModel);
ui->map->setSource(QUrl(QStringLiteral("qrc:/map/map.qml")));
setAttribute(Qt::WA_DeleteOnClose, true);
@ -2136,6 +2531,12 @@ ADSBDemodGUI::ADSBDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseb
connect(ui->adsbData->horizontalHeader(), SIGNAL(sectionMoved(int, int, int)), SLOT(adsbData_sectionMoved(int, int, int)));
connect(ui->adsbData->horizontalHeader(), SIGNAL(sectionResized(int, int, int)), SLOT(adsbData_sectionResized(int, int, int)));
ui->photoHeader->setVisible(false);
ui->photoFlag->setVisible(false);
ui->photo->setVisible(false);
ui->flightDetails->setVisible(false);
ui->aircraftDetails->setVisible(false);
// Read aircraft information database, if it has previously been downloaded
if (!readFastDB(getFastDBFilename()))
{
@ -2151,6 +2552,16 @@ ADSBDemodGUI::ADSBDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseb
// Read operator air force to military map
m_militaryMap = CSV::hash(":/flags/militarymap.csv");
connect(&m_openAIP, &OpenAIP::downloadingURL, this, &ADSBDemodGUI::downloadingURL);
connect(&m_openAIP, &OpenAIP::downloadError, this, &ADSBDemodGUI::downloadError);
connect(&m_openAIP, &OpenAIP::downloadAirspaceFinished, this, &ADSBDemodGUI::downloadAirspaceFinished);
connect(&m_openAIP, &OpenAIP::downloadNavAidsFinished, this, &ADSBDemodGUI::downloadNavAidsFinished);
// Read airspaces
m_airspaces = OpenAIP::readAirspaces();
// Read NavAids
m_navAids = OpenAIP::readNavAids();
// Get station position
Real stationLatitude = MainCore::instance()->getSettings().getLatitude();
Real stationLongitude = MainCore::instance()->getSettings().getLongitude();
@ -2179,14 +2590,20 @@ ADSBDemodGUI::ADSBDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseb
stationObject->setProperty("stationName", QVariant::fromValue(MainCore::instance()->getSettings().getStationName()));
}
// Add airports within range of My Position
if (m_airportInfo != nullptr)
if (m_airportInfo != nullptr) {
updateAirports();
}
updateAirspaces();
updateNavAids();
// Initialise text to speech engine
m_speech = new QTextToSpeech(this);
m_flightInformation = nullptr;
connect(&m_planeSpotters, &PlaneSpotters::aircraftPhoto, this, &ADSBDemodGUI::aircraftPhoto);
connect(ui->photo, &ClickableLabel::clicked, this, &ADSBDemodGUI::photoClicked);
updateDeviceSetList();
displaySettings();
applySettings(true);
@ -2194,6 +2611,16 @@ ADSBDemodGUI::ADSBDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseb
ADSBDemodGUI::~ADSBDemodGUI()
{
if (m_templateServer)
{
m_templateServer->close();
delete m_templateServer;
}
disconnect(&m_openAIP, &OpenAIP::downloadingURL, this, &ADSBDemodGUI::downloadingURL);
disconnect(&m_openAIP, &OpenAIP::downloadError, this, &ADSBDemodGUI::downloadError);
disconnect(&m_openAIP, &OpenAIP::downloadAirspaceFinished, this, &ADSBDemodGUI::downloadAirspaceFinished);
disconnect(&m_openAIP, &OpenAIP::downloadNavAidsFinished, this, &ADSBDemodGUI::downloadNavAidsFinished);
disconnect(&m_planeSpotters, &PlaneSpotters::aircraftPhoto, this, &ADSBDemodGUI::aircraftPhoto);
delete ui;
qDeleteAll(m_aircraft);
if (m_airportInfo) {
@ -2209,6 +2636,8 @@ ADSBDemodGUI::~ADSBDemodGUI()
disconnect(m_flightInformation, &FlightInformation::flightUpdated, this, &ADSBDemodGUI::flightInformationUpdated);
delete m_flightInformation;
}
qDeleteAll(m_airspaces);
qDeleteAll(m_navAids);
}
void ADSBDemodGUI::applySettings(bool force)
@ -2308,11 +2737,16 @@ void ADSBDemodGUI::displaySettings()
|| (m_settings.m_displayHeliports != m_currentDisplayHeliports)))
updateAirports();
updateAirspaces();
updateNavAids();
if (!m_settings.m_displayDemodStats)
ui->stats->setText("");
initFlightInformation();
applyMapSettings();
blockApplySettings(false);
}
@ -2525,6 +2959,7 @@ void ADSBDemodGUI::flightInformationUpdated(const FlightInformation::Flight& fli
if (aircraft->m_positionValid) {
m_aircraftModel.aircraftUpdated(aircraft);
}
updatePhotoFlightInformation(aircraft);
}
else
{
@ -2532,6 +2967,40 @@ void ADSBDemodGUI::flightInformationUpdated(const FlightInformation::Flight& fli
}
}
void ADSBDemodGUI::aircraftPhoto(const PlaneSpottersPhoto *photo)
{
// Make sure the photo is for the currently highlighted aircraft, as it may
// have taken a while to download
if (!photo->m_pixmap.isNull() && m_highlightAircraft && (m_highlightAircraft->m_icaoItem->text() == photo->m_icao))
{
ui->photo->setPixmap(photo->m_pixmap);
ui->photo->setToolTip(QString("Photographer: %1").arg(photo->m_photographer)); // Required by terms of use
ui->photoHeader->setVisible(true);
ui->photoFlag->setVisible(true);
ui->photo->setVisible(true);
ui->flightDetails->setVisible(true);
ui->aircraftDetails->setVisible(true);
m_photoLink = photo->m_link;
}
}
void ADSBDemodGUI::photoClicked()
{
// Photo needs to link back to PlaneSpotters, as per terms of use
if (m_highlightAircraft)
{
if (m_photoLink.isEmpty())
{
QString icaoUpper = QString("%1").arg(m_highlightAircraft->m_icao, 1, 16).toUpper();
QDesktopServices::openUrl(QUrl(QString("https://www.planespotters.net/hex/%1").arg(icaoUpper)));
}
else
{
QDesktopServices::openUrl(QUrl(m_photoLink));
}
}
}
void ADSBDemodGUI::on_logEnable_clicked(bool checked)
{
m_settings.m_logEnabled = checked;
@ -2616,3 +3085,48 @@ void ADSBDemodGUI::on_logOpen_clicked()
}
}
}
void ADSBDemodGUI::downloadingURL(const QString& url)
{
if (m_progressDialog)
{
m_progressDialog->setLabelText(QString("Downloading %1.").arg(url));
m_progressDialog->setValue(m_progressDialog->value() + 1);
}
}
void ADSBDemodGUI::downloadError(const QString& error)
{
QMessageBox::critical(this, "ADS-B", error);
if (m_progressDialog)
{
m_progressDialog->close();
delete m_progressDialog;
m_progressDialog = nullptr;
}
}
void ADSBDemodGUI::downloadAirspaceFinished()
{
if (m_progressDialog) {
m_progressDialog->setLabelText("Reading airspaces.");
}
m_airspaces = OpenAIP::readAirspaces();
updateAirspaces();
m_openAIP.downloadNavAids();
}
void ADSBDemodGUI::downloadNavAidsFinished()
{
if (m_progressDialog) {
m_progressDialog->setLabelText("Reading NAVAIDs.");
}
m_navAids = OpenAIP::readNavAids();
updateNavAids();
if (m_progressDialog)
{
m_progressDialog->close();
delete m_progressDialog;
m_progressDialog = nullptr;
}
}

View File

@ -36,6 +36,8 @@
#include "util/movingaverage.h"
#include "util/httpdownloadmanager.h"
#include "util/flightinformation.h"
#include "util/openaip.h"
#include "util/planespotters.h"
#include "maincore.h"
#include "adsbdemodsettings.h"
@ -49,6 +51,7 @@ class ADSBDemod;
class WebAPIAdapterInterface;
class HttpDownloadManager;
class ADSBDemodGUI;
class ADSBOSMTemplateServer;
namespace Ui {
class ADSBDemodGUI;
@ -484,6 +487,155 @@ private:
QList<float> m_range;
};
// Airspace data model used by QML map item
class AirspaceModel : public QAbstractListModel {
Q_OBJECT
public:
using QAbstractListModel::QAbstractListModel;
enum MarkerRoles {
nameRole = Qt::UserRole + 1,
detailsRole = Qt::UserRole + 2,
positionRole = Qt::UserRole + 3,
airspaceBorderColorRole = Qt::UserRole + 4,
airspaceFillColorRole = Qt::UserRole + 5,
airspacePolygonRole = Qt::UserRole + 6
};
Q_INVOKABLE void addAirspace(Airspace *airspace) {
beginInsertRows(QModelIndex(), rowCount(), rowCount());
m_airspaces.append(airspace);
// Convert QPointF to QVariantList of QGeoCoordinates
QVariantList polygon;
for (const auto p : airspace->m_polygon)
{
QGeoCoordinate coord(p.y(), p.x(), airspace->topHeightInMetres());
polygon.push_back(QVariant::fromValue(coord));
}
m_polygons.append(polygon);
endInsertRows();
}
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
Q_UNUSED(parent)
return m_airspaces.count();
}
void removeAllAirspaces() {
if (m_airspaces.count() > 0)
{
beginRemoveRows(QModelIndex(), 0, m_airspaces.count() - 1);
m_airspaces.clear();
m_polygons.clear();
endRemoveRows();
}
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant& value, int role = Qt::EditRole) override;
Qt::ItemFlags flags(const QModelIndex &index) const override
{
(void) index;
return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable;
}
QHash<int, QByteArray> roleNames() const {
QHash<int, QByteArray> roles;
roles[nameRole] = "name";
roles[detailsRole] = "details";
roles[positionRole] = "position";
roles[airspaceBorderColorRole] = "airspaceBorderColor";
roles[airspaceFillColorRole] = "airspaceFillColor";
roles[airspacePolygonRole] = "airspacePolygon";
return roles;
}
private:
QList<Airspace *> m_airspaces;
QList<QVariantList> m_polygons;
};
// NavAid model used for each NavAid on the map
class NavAidModel : public QAbstractListModel {
Q_OBJECT
public:
using QAbstractListModel::QAbstractListModel;
enum MarkerRoles{
positionRole = Qt::UserRole + 1,
navAidDataRole = Qt::UserRole + 2,
navAidImageRole = Qt::UserRole + 3,
bubbleColourRole = Qt::UserRole + 4,
selectedRole = Qt::UserRole + 5
};
Q_INVOKABLE void addNavAid(NavAid *vor) {
beginInsertRows(QModelIndex(), rowCount(), rowCount());
m_navAids.append(vor);
m_selected.append(false);
endInsertRows();
}
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
Q_UNUSED(parent)
return m_navAids.count();
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant& value, int role = Qt::EditRole) override;
Qt::ItemFlags flags(const QModelIndex &index) const override {
(void) index;
return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable;
}
void allNavAidsUpdated() {
for (int i = 0; i < m_navAids.count(); i++)
{
QModelIndex idx = index(i);
emit dataChanged(idx, idx);
}
}
void removeNavAid(NavAid *vor) {
int row = m_navAids.indexOf(vor);
if (row >= 0)
{
beginRemoveRows(QModelIndex(), row, row);
m_navAids.removeAt(row);
m_selected.removeAt(row);
endRemoveRows();
}
}
void removeAllNavAids() {
if (m_navAids.count() > 0)
{
beginRemoveRows(QModelIndex(), 0, m_navAids.count() - 1);
m_navAids.clear();
m_selected.clear();
endRemoveRows();
}
}
QHash<int, QByteArray> roleNames() const {
QHash<int, QByteArray> roles;
roles[positionRole] = "position";
roles[navAidDataRole] = "navAidData";
roles[navAidImageRole] = "navAidImage";
roles[bubbleColourRole] = "bubbleColour";
roles[selectedRole] = "selected";
return roles;
}
private:
QList<NavAid *> m_navAids;
QList<bool> m_selected;
};
class ADSBDemodGUI : public ChannelGUI {
Q_OBJECT
@ -500,11 +652,13 @@ public:
void target(const QString& name, float az, float el, float range);
bool setFrequency(float frequency);
bool useSIUints() { return m_settings.m_siUnits; }
Q_INVOKABLE void clearHighlighted();
public slots:
void channelMarkerChangedByCursor();
void channelMarkerHighlightedByCursor();
void flightInformationUpdated(const FlightInformation::Flight& flight);
void aircraftPhoto(const PlaneSpottersPhoto *photo);
private:
Ui::ADSBDemodGUI* ui;
@ -524,10 +678,14 @@ private:
QHash<int, AirportInformation *> *m_airportInfo; // Hashed on id
AircraftModel m_aircraftModel;
AirportModel m_airportModel;
AirspaceModel m_airspaceModel;
NavAidModel m_navAidModel;
QHash<QString, QIcon *> m_airlineIcons; // Hashed on airline ICAO
QHash<QString, QIcon *> m_flagIcons; // Hashed on country
QHash<QString, QString> *m_prefixMap; // Registration to country (flag name)
QHash<QString, QString> *m_militaryMap; // Operator airforce to military (flag name)
QList<Airspace *> m_airspaces;
QList<NavAid *> m_navAids;
AzEl m_azEl; // Position of station
Aircraft *m_trackAircraft; // Aircraft we want to track in Channel Report
@ -542,9 +700,14 @@ private:
QTextToSpeech *m_speech;
QMenu *menu; // Column select context menu
FlightInformation *m_flightInformation;
PlaneSpotters m_planeSpotters;
QString m_photoLink;
WebAPIAdapterInterface *m_webAPIAdapterInterface;
HttpDownloadManager m_dlm;
QProgressDialog *m_progressDialog;
quint16 m_osmPort;
OpenAIP m_openAIP;
ADSBOSMTemplateServer *m_templateServer;
explicit ADSBDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent = 0);
virtual ~ADSBDemodGUI();
@ -579,6 +742,8 @@ private:
bool readOSNDB(const QString& filename);
bool readFastDB(const QString& filename);
void updateAirports();
void updateAirspaces();
void updateNavAids();
QIcon *getAirlineIcon(const QString &operatorICAO);
QIcon *getFlagIcon(const QString &country);
void updateDeviceSetList();
@ -586,6 +751,9 @@ private:
Aircraft* findAircraftByFlight(const QString& flight);
QString dataTimeToShortString(QDateTime dt);
void initFlightInformation();
void applyMapSettings();
void updatePhotoText(Aircraft *aircraft);
void updatePhotoFlightInformation(Aircraft *aircraft);
void leaveEvent(QEvent*);
void enterEvent(QEvent*);
@ -610,6 +778,7 @@ private slots:
void on_flightInfo_clicked();
void on_getOSNDB_clicked();
void on_getAirportDB_clicked();
void on_getAirspacesDB_clicked();
void on_flightPaths_clicked(bool checked);
void on_allFlightPaths_clicked(bool checked);
void onWidgetRolled(QWidget* widget, bool rollDown);
@ -625,6 +794,12 @@ private slots:
void on_logEnable_clicked(bool checked=false);
void on_logFilename_clicked();
void on_logOpen_clicked();
void downloadingURL(const QString& url);
void downloadError(const QString& error);
void downloadAirspaceFinished();
void downloadNavAidsFinished();
void photoClicked();
signals:
void homePositionChanged();
};

View File

@ -518,6 +518,20 @@
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="getAirspacesDB">
<property name="toolTip">
<string>Download airspaces and NAVAIDs from OpenAIP (40MB)</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/icons/vor.png</normaloff>:/icons/vor.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="displaySettings">
<property name="toolTip">
@ -572,6 +586,23 @@
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="flightInfo">
<property name="toolTip">
<string>Download flight information for selected aircraft</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/info.png</normaloff>:/info.png</iconset>
</property>
<property name="checkable">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="feed">
<property name="toolTip">
@ -606,23 +637,6 @@
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="flightInfo">
<property name="toolTip">
<string>Download flight information for selected aircraft</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/info.png</normaloff>:/info.png</iconset>
</property>
<property name="checkable">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="logEnable">
<property name="maximumSize">
@ -738,7 +752,7 @@
<x>0</x>
<y>140</y>
<width>600</width>
<height>291</height>
<height>270</height>
</rect>
</property>
<property name="sizePolicy">
@ -750,13 +764,13 @@
<property name="minimumSize">
<size>
<width>600</width>
<height>0</height>
<height>270</height>
</size>
</property>
<property name="windowTitle">
<string>ADS-B Data</string>
</property>
<layout class="QVBoxLayout" name="verticalLayoutTable">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>2</number>
</property>
@ -774,6 +788,12 @@
</property>
<item>
<widget class="QTableWidget" name="adsbData">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
@ -1048,6 +1068,125 @@
</column>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="photoLayout">
<property name="spacing">
<number>4</number>
</property>
<item>
<layout class="QHBoxLayout" name="photoHeaderLayout">
<property name="spacing">
<number>4</number>
</property>
<item>
<widget class="QLabel" name="photoHeader">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string/>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="photoFlag">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string/>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="ClickableLabel" name="photo">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="flightDetails">
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="aircraftDetails">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<spacer name="photoVerticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="mapContainer" native="true">
@ -1132,6 +1271,16 @@
<header>gui/rollupwidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>ButtonSwitch</class>
<extends>QToolButton</extends>
<header>gui/buttonswitch.h</header>
</customwidget>
<customwidget>
<class>ClickableLabel</class>
<extends>QLabel</extends>
<header>gui/clickablelabel.h</header>
</customwidget>
<customwidget>
<class>ValueDialZ</class>
<extends>QWidget</extends>
@ -1144,11 +1293,6 @@
<header>gui/levelmeter.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>ButtonSwitch</class>
<extends>QToolButton</extends>
<header>gui/buttonswitch.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>deltaFrequency</tabstop>
@ -1161,9 +1305,16 @@
<tabstop>threshold</tabstop>
<tabstop>getOSNDB</tabstop>
<tabstop>getAirportDB</tabstop>
<tabstop>getAirspacesDB</tabstop>
<tabstop>displaySettings</tabstop>
<tabstop>flightPaths</tabstop>
<tabstop>allFlightPaths</tabstop>
<tabstop>flightInfo</tabstop>
<tabstop>feed</tabstop>
<tabstop>notifications</tabstop>
<tabstop>logEnable</tabstop>
<tabstop>logFilename</tabstop>
<tabstop>logOpen</tabstop>
<tabstop>devicesRefresh</tabstop>
<tabstop>device</tabstop>
<tabstop>adsbData</tabstop>

View File

@ -51,7 +51,7 @@ void ADSBDemodSettings::resetToDefaults()
m_reverseAPIPort = 8888;
m_reverseAPIDeviceIndex = 0;
m_reverseAPIChannelIndex = 0;
m_airportRange = 100;
m_airportRange = 100.0f;
m_airportMinimumSize = AirportType::Medium;
m_displayHeliports = false;
m_flightPaths = true;
@ -74,6 +74,11 @@ void ADSBDemodSettings::resetToDefaults()
}
m_logFilename = "adsb_log.csv";
m_logEnabled = false;
m_airspaces = QStringList({"A", "C", "TMZ"});
m_airspaceRange = 500.0f;
m_mapType = AVIATION_LIGHT;
m_displayNavAids = true;
m_displayPhotos = true;
}
QByteArray ADSBDemodSettings::serialize() const
@ -123,10 +128,18 @@ QByteArray ADSBDemodSettings::serialize() const
s.writeString(36, m_logFilename);
s.writeBool(37, m_logEnabled);
for (int i = 0; i < ADSBDEMOD_COLUMNS; i++)
s.writeString(38, m_airspaces.join(" "));
s.writeFloat(39, m_airspaceRange);
s.writeS32(40, (int)m_mapType);
s.writeBool(41, m_displayNavAids);
s.writeBool(42, m_displayPhotos);
for (int i = 0; i < ADSBDEMOD_COLUMNS; i++) {
s.writeS32(100 + i, m_columnIndexes[i]);
for (int i = 0; i < ADSBDEMOD_COLUMNS; i++)
}
for (int i = 0; i < ADSBDEMOD_COLUMNS; i++) {
s.writeS32(200 + i, m_columnSizes[i]);
}
return s.final();
}
@ -147,6 +160,7 @@ bool ADSBDemodSettings::deserialize(const QByteArray& data)
qint32 tmp;
uint32_t utmp;
QByteArray blob;
QString string;
if (m_channelMarker)
{
@ -187,7 +201,7 @@ bool ADSBDemodSettings::deserialize(const QByteArray& data)
m_reverseAPIChannelIndex = utmp > 99 ? 99 : utmp;
d.readS32(17, &m_streamIndex, 0);
d.readFloat(18, &m_airportRange, 100);
d.readFloat(18, &m_airportRange, 100.0f);
d.readS32(19, (int *)&m_airportMinimumSize, AirportType::Medium);
d.readBool(20, &m_displayHeliports, false);
d.readBool(21, &m_flightPaths, true);
@ -211,10 +225,19 @@ bool ADSBDemodSettings::deserialize(const QByteArray& data)
d.readString(36, &m_logFilename, "adsb_log.csv");
d.readBool(37, &m_logEnabled, false);
for (int i = 0; i < ADSBDEMOD_COLUMNS; i++)
d.readString(38, &string, "A C TMZ");
m_airspaces = string.split(" ");
d.readFloat(39, &m_airspaceRange, 500.0f);
d.readS32(40, (int *)&m_mapType, (int)AVIATION_LIGHT);
d.readBool(41, &m_displayNavAids, true);
d.readBool(42, &m_displayPhotos, true);
for (int i = 0; i < ADSBDEMOD_COLUMNS; i++) {
d.readS32(100 + i, &m_columnIndexes[i], i);
for (int i = 0; i < ADSBDEMOD_COLUMNS; i++)
}
for (int i = 0; i < ADSBDEMOD_COLUMNS; i++) {
d.readS32(200 + i, &m_columnSizes[i], -1);
}
return true;
}

View File

@ -134,6 +134,17 @@ struct ADSBDemodSettings
QString m_logFilename;
bool m_logEnabled;
QStringList m_airspaces; //!< Airspace names to display
float m_airspaceRange; //!< How far away we display airspace (mkm)
enum MapType {
AVIATION_LIGHT, //!< White map with no place names
AVIATION_DARK,
STREET,
SATELLITE
} m_mapType;
bool m_displayNavAids;
bool m_displayPhotos;
ADSBDemodSettings();
void resetToDefaults();
void setChannelMarker(Serializable *channelMarker) { m_channelMarker = channelMarker; }

View File

@ -0,0 +1,18 @@
///////////////////////////////////////////////////////////////////////////////////
// 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 "adsbosmtemplateserver.h"

View File

@ -0,0 +1,155 @@
///////////////////////////////////////////////////////////////////////////////////
// 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_ADSBOSMTEMPLATE_SERVER_H_
#define INCLUDE_ADSBOSMTEMPLATE_SERVER_H_
#include <QTcpServer>
#include <QTcpSocket>
#include <QDebug>
// Redirect OSM maps so we can support Street, Satellite and an Aviation map (CartoDB position)
class ADSBOSMTemplateServer : public QTcpServer
{
Q_OBJECT
private:
QString m_thunderforestAPIKey;
QString m_maptilerAPIKey;
public:
// port - port to listen on / is listening on. Use 0 for any free port.
ADSBOSMTemplateServer(const QString &maptilerAPIKey, quint16 &port, QObject* parent = 0) :
QTcpServer(parent),
m_maptilerAPIKey(maptilerAPIKey)
{
listen(QHostAddress::Any, port);
port = serverPort();
}
void incomingConnection(qintptr socket) override
{
QTcpSocket* s = new QTcpSocket(this);
connect(s, SIGNAL(readyRead()), this, SLOT(readClient()));
connect(s, SIGNAL(disconnected()), this, SLOT(discardClient()));
s->setSocketDescriptor(socket);
//addPendingConnection(socket);
}
private slots:
void readClient()
{
QTcpSocket* socket = (QTcpSocket*)sender();
if (socket->canReadLine())
{
QString line = socket->readLine();
QStringList tokens = QString(line).split(QRegExp("[ \r\n][ \r\n]*"));
if (tokens[0] == "GET")
{
bool hires = tokens[1].contains("hires");
QString hiresURL = hires ? "@2x" : "";
QString xml;
if ((tokens[1] == "/street") || (tokens[1] == "/street-hires"))
{
xml = QString("\
{\
\"UrlTemplate\" : \"https://maps.wikimedia.org/osm-intl/%z/%x/%y%1.png\",\
\"ImageFormat\" : \"png\",\
\"QImageFormat\" : \"Indexed8\",\
\"ID\" : \"wmf-intl-%2x\",\
\"MaximumZoomLevel\" : 18,\
\"MapCopyRight\" : \"<a href='https://wikimediafoundation.org/wiki/Terms_of_Use'>WikiMedia Foundation</a>\",\
\"DataCopyRight\" : \"<a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a> contributors\"\
}").arg(hiresURL).arg(hires ? 1 : 2);
}
else if (tokens[1] == "/satellite")
{
xml = QString("\
{\
\"Enabled\" : true,\
\"UrlTemplate\" : \"https://api.maptiler.com/tiles/satellite/%z/%x/%y%1.jpg?key=%2\",\
\"ImageFormat\" : \"jpg\",\
\"QImageFormat\" : \"RGB888\",\
\"ID\" : \"usgs-l7\",\
\"MaximumZoomLevel\" : 20,\
\"MapCopyRight\" : \"<a href='http://maptiler.com'>Maptiler</a>\",\
\"DataCopyRight\" : \"<a href='http://maptiler.com'>Maptiler</a>\"\
}").arg(hiresURL).arg(m_maptilerAPIKey);
}
else if (tokens[1].contains("transit"))
{
QStringList map({"/night-transit", "/night-transit-hires", "/transit", "/transit-hires"});
QStringList mapId({"thf-nighttransit", "thf-nighttransit-hires", "thf-transit", "thf-transit-hires"});
QStringList mapUrl({"dark_nolabels", "dark_nolabels", "light_nolabels", "light_nolabels"});
// Use CartoDB maps without labels for aviation maps
int idx = map.indexOf(tokens[1]);
xml = QString("\
{\
\"UrlTemplate\" : \"http://1.basemaps.cartocdn.com/%2/%z/%x/%y.png%1\",\
\"ImageFormat\" : \"png\",\
\"QImageFormat\" : \"Indexed8\",\
\"ID\" : \"%3\",\
\"MaximumZoomLevel\" : 20,\
\"MapCopyRight\" : \"<a href='https://carto.com'>CartoDB</a>\",\
\"DataCopyRight\" : \"<a href='https://carto.com'>CartoDB</a>\"\
}").arg(hiresURL).arg(mapUrl[idx]).arg(mapId[idx]);
}
else
{
QStringList map({"/cycle", "/cycle-hires", "/hiking", "/hiking-hires", "/night-transit", "/night-transit-hires", "/terrain", "/terrain-hires", "/transit", "/transit-hires"});
QStringList mapId({"thf-cycle", "thf-cycle-hires", "thf-hike", "thf-hike-hires", "thf-nighttransit", "thf-nighttransit-hires", "thf-landsc", "thf-landsc-hires", "thf-transit", "thf-transit-hires"});
QStringList mapUrl({"cycle", "cycle", "outdoors", "outdoors", "transport-dark", "transport-dark", "landscape", "landscape", "transport", "transport"});
int idx = map.indexOf(tokens[1]);
if (idx != -1)
{
xml = QString("\
{\
\"UrlTemplate\" : \"http://a.tile.thunderforest.com/%1/%z/%x/%y%4.png?apikey=%2\",\
\"ImageFormat\" : \"png\",\
\"QImageFormat\" : \"Indexed8\",\
\"ID\" : \"%3\",\
\"MaximumZoomLevel\" : 20,\
\"MapCopyRight\" : \"<a href='http://www.thunderforest.com/'>Thunderforest</a>\",\
\"DataCopyRight\" : \"<a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a> contributors\"\
}").arg(mapUrl[idx]).arg("3e1f614f78a345459931ba3c898e975e").arg(mapId[idx]).arg(hiresURL);
}
}
QTextStream os(socket);
os.setAutoDetectUnicode(true);
os << "HTTP/1.0 200 Ok\r\n"
"Content-Type: text/html; charset=\"utf-8\"\r\n"
"\r\n"
<< xml << "\n";
socket->close();
if (socket->state() == QTcpSocket::UnconnectedState) {
delete socket;
}
}
}
}
void discardClient()
{
QTcpSocket* socket = (QTcpSocket*)sender();
socket->deleteLater();
}
};
#endif

View File

@ -3,5 +3,6 @@
<file>icons/aircraft.png</file>
<file>icons/controltower.png</file>
<file>icons/allflightpaths.png</file>
<file>icons/vor.png</file>
</qresource>
</RCC>

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 B

View File

@ -16,5 +16,10 @@
<file>map/heliport.png</file>
<file>map/antenna.png</file>
<file>map/truck.png</file>
<file>map/VOR.png</file>
<file>map/VOR-DME.png</file>
<file>map/VORTAC.png</file>
<file>map/NDB.png</file>
<file>map/DME.png</file>
</qresource>
</RCC>

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 B

View File

@ -2,60 +2,220 @@ import QtQuick 2.12
import QtQuick.Window 2.12
import QtLocation 5.12
import QtPositioning 5.12
import QtGraphicalEffects 1.15
Item {
id: qmlMap
property int aircraftZoomLevel: 11
property int airportZoomLevel: 11
property string mapProvider: "osm"
property variant mapPtr
property string requestedMapType
property bool lightIcons
property variant guiPtr
Plugin {
id: mapPlugin
name: "osm"
function createMap(pluginParameters, requestedMap, gui) {
requestedMapType = requestedMap
guiPtr = gui
var paramString = ""
for (var prop in pluginParameters) {
var parameter = 'PluginParameter { name: "' + prop + '"; value: "' + pluginParameters[prop] + '"}'
paramString = paramString + parameter
}
var pluginString = 'import QtLocation 5.12; Plugin{ name:"' + mapProvider + '"; ' + paramString + '}'
var plugin = Qt.createQmlObject (pluginString, qmlMap)
if (mapPtr) {
// Objects aren't destroyed immediately, so rename the old
// map, so any C++ code that calls findChild("map") doesn't find
// the old map
mapPtr.objectName = "oldMap";
mapPtr.destroy()
mapPtr = null
}
mapPtr = actualMapComponent.createObject(page)
mapPtr.plugin = plugin;
mapPtr.forceActiveFocus()
mapPtr.objectName = "map";
}
Map {
id: map
objectName: "map"
Item {
id: page
anchors.fill: parent
plugin: mapPlugin
center: QtPositioning.coordinate(51.5, 0.125) // London
zoomLevel: 10
}
Component {
id: actualMapComponent
Map {
id: map
anchors.fill: parent
center: QtPositioning.coordinate(51.5, 0.125) // London
zoomLevel: 10
// Needs to come first, otherwise MouseAreas in the MapItemViews don't get clicked event first
// Setting z doesn't seem to work
MouseArea {
anchors.fill: parent
onClicked: {
// Unhighlight current aircraft
guiPtr.clearHighlighted()
}
}
MapStation {
id: station
objectName: "station"
stationName: "Home"
coordinate: QtPositioning.coordinate(51.5, 0.125)
}
MapItemView {
model: airspaceModel
delegate: airspaceComponent
}
MapItemView {
model: navAidModel
delegate: navAidComponent
}
MapItemView {
model: airspaceModel
delegate: airspaceNameComponent
}
MapItemView {
model: airportModel
delegate: airportComponent
}
// This needs to be before aircraftComponent MapItemView, so it's drawn underneath
MapItemView {
model: aircraftModel
delegate: aircraftPathComponent
}
MapItemView {
model: aircraftModel
delegate: aircraftComponent
}
onZoomLevelChanged: {
if (zoomLevel > 11) {
station.zoomLevel = zoomLevel
aircraftZoomLevel = zoomLevel
airportZoomLevel = zoomLevel
} else {
station.zoomLevel = 11
aircraftZoomLevel = 11
airportZoomLevel = 11
}
}
onSupportedMapTypesChanged : {
for (var i = 0; i < supportedMapTypes.length; i++) {
if (requestedMapType == supportedMapTypes[i].name) {
activeMapType = supportedMapTypes[i]
}
}
lightIcons = requestedMapType == "Night Transit Map"
}
MapStation {
id: station
objectName: "station"
stationName: "Home"
coordinate: QtPositioning.coordinate(51.5, 0.125)
}
}
// This needs to be before aircraftComponent MapItemView, so it's drawn underneath
MapItemView {
model: aircraftModel
delegate: aircraftPathComponent
}
Component {
id: navAidComponent
MapQuickItem {
id: navAid
anchorPoint.x: image.width/2
anchorPoint.y: image.height/2
coordinate: position
zoomLevel: airportZoomLevel
MapItemView {
model: airportModel
delegate: airportComponent
}
MapItemView {
model: aircraftModel
delegate: aircraftComponent
}
onZoomLevelChanged: {
if (zoomLevel > 11) {
station.zoomLevel = zoomLevel
aircraftZoomLevel = zoomLevel
airportZoomLevel = zoomLevel
} else {
station.zoomLevel = 11
aircraftZoomLevel = 11
airportZoomLevel = 11
sourceItem: Grid {
columns: 1
Grid {
horizontalItemAlignment: Grid.AlignHCenter
columnSpacing: 5
layer.enabled: true
layer.smooth: true
Image {
id: image
source: navAidImage
visible: !lightIcons
MouseArea {
anchors.fill: parent
onClicked: (mouse) => {
selected = !selected
}
}
}
ColorOverlay {
cached: true
width: image.width
height: image.height
source: image
color: "#c0ffffff"
visible: lightIcons
}
Rectangle {
id: bubble
color: bubbleColour
border.width: 1
width: text.width + 5
height: text.height + 5
radius: 5
Text {
id: text
anchors.centerIn: parent
text: navAidData
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: (mouse) => {
selected = !selected
}
}
}
}
}
}
}
Component {
id: airspaceComponent
MapPolygon {
border.width: 1
border.color: airspaceBorderColor
color: airspaceFillColor
path: airspacePolygon
}
}
Component {
id: airspaceNameComponent
MapQuickItem {
coordinate: position
anchorPoint.x: airspaceText.width/2
anchorPoint.y: airspaceText.height/2
zoomLevel: airportZoomLevel
sourceItem: Grid {
columns: 1
Grid {
layer.enabled: true
layer.smooth: true
horizontalItemAlignment: Grid.AlignHCenter
Text {
id: airspaceText
text: details
}
}
}
}
}
Component {
@ -86,9 +246,27 @@ Item {
id: image
rotation: heading
source: aircraftImage
visible: !lightIcons
MouseArea {
anchors.fill: parent
onClicked: {
highlighted = true
}
onDoubleClicked: {
target = true
}
}
}
ColorOverlay {
cached: true
width: image.width
height: image.height
rotation: heading
source: image
color: "#c0ffffff"
visible: lightIcons
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: {
highlighted = true
}
@ -111,7 +289,6 @@ Item {
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: {
showAll = !showAll
}
@ -140,6 +317,15 @@ Item {
Image {
id: image
source: airportImage
visible: !lightIcons
}
ColorOverlay {
cached: true
width: image.width
height: image.height
source: image
color: "#c0ffffff"
visible: lightIcons
}
Rectangle {
id: bubble
@ -155,7 +341,6 @@ Item {
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: (mouse) => {
if (showFreq) {
var freqIdx = Math.floor((mouse.y-5)/((height-10)/airportDataRows))

View File

@ -31,8 +31,8 @@
#include "util/csv.h"
#include "adsbdemodsettings.h"
#define AIRPORTS_URL "https://ourairports.com/data/airports.csv"
#define AIRPORT_FREQUENCIES_URL "https://ourairports.com/data/airport-frequencies.csv"
#define AIRPORTS_URL "https://davidmegginson.github.io/ourairports-data/airports.csv"
#define AIRPORT_FREQUENCIES_URL "https://davidmegginson.github.io/ourairports-data/airport-frequencies.csv"
struct AirportInformation {

View File

@ -4,10 +4,14 @@
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.
<h2>Interface</h2>
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.
![ADS-B Demodulator plugin GUI](../../../doc/img/ADSBDemod_plugin.png)
<h2>Interface</h2>
![ADS-B Demodulator plugin settings](../../../doc/img/ADSBDemod_plugin_settings.png)
<h3>1: Frequency shift from center frequency of reception value</h3>
Use the wheels to adjust the frequency shift in Hz from the center frequency of reception. Right click on a digit sets all digits on the right to zero. This effectively floors value at the digit position. Wheels are moved with the mousewheel while pointing at the wheel or by selecting the wheel with the left mouse click and using the keyboard arrows. Pressing shift simultaneously moves digit by 5 and pressing control moves it by 2. Left click on a digit sets the cursor position at this digit.
@ -48,20 +52,29 @@ This sets the correlation threshold in dB between the received signal and expect
<h3>9: Download Opensky-Network Aircraft Database</h3>
Clicking this will download the Opensky-Network (https://opensky-network.org/) aircraft database. This database contains information about aircrafts, such as registration, aircraft model and owner details, that is not broadcast via ADS-B. Once downloaded, this additional information will be displayed in the table alongside the ADS-B data. The database should only need to be downloaded once, as it is saved to disk, and it is recommended to download it before enabling the demodulator.
Clicking this will download the [Opensky-Network](https://opensky-network.org/) aircraft database. This database contains information about aircrafts, such as registration, aircraft model and owner details, that is not broadcast via ADS-B. Once downloaded, this additional information will be displayed in the table alongside the ADS-B data. The database should only need to be downloaded once, as it is saved to disk, and it is recommended to download it before enabling the demodulator.
<h3>10: Download OurAirports Airport Databases</h3>
Clicking this will download the OurAirports (https://ourairports.com/) airport databases. These contains names and locations for airports allowing them to be drawn on the map, as well as their corresponding ATC frequencies, which can also be displayed next to the airport on the map, by clicking the airport name. The size of airports that will be displayed on the map, and whether heliports are displayed, can be set in the Display Settings dialog.
Clicking this will download the [OurAirports](https://ourairports.com/) airport databases. These contains names and locations for airports allowing them to be drawn on the map, as well as their corresponding ATC frequencies, which can also be displayed next to the airport on the map, by clicking the airport name. The size of airports that will be displayed on the map, and whether heliports are displayed, can be set in the Display Settings dialog.
<h3>11: Display Settings</h3>
<h3>11: Download OpenAIP Airspace and NAVAID Databases</h3>
Clicking this will download the [OpenAIP](https://www.openaip.net/) airspace and NAVAID databases. These can be displayed on the map. The airspace categories to be displayed can individually be selected in the Display Settings dialog.
<h3>12: Display Settings</h3>
Clicking the Display Settings button will open the Display Settings dialog, which allows you to choose:
* The units for altitude, speed and vertical climb rate. These can be either ft (feet), kn (knots) and ft/min (feet per minute), or m (metres), kph (kilometers per hour) and m/s (metres per second).
* The type of map that will be displayed. This can either be a light or dark aviation map (with no place names to reduce clutter), a street map or satellite imagery.
* The minimum size airport that will be displayed on the map: small, medium or large. Use small to display GA airfields, medium for regional airports and large for international airports.
* Whether or not to display heliports.
* The distance (in kilometres), from the location set under Preferences > My Position, at which airports will be displayed on the map.
* The distance (in kilometres), from the location set under Preferences > My Position, at which airports will be displayed on the map. Displaying too many airports will slow down drawing of the map.
* What category of airspaces should be displayed.
* The distance (in kilometres), from the location set under Preferences > My Position, at which airspaces will be displayed on the map. Displaying too many airspaces will slow down drawing of the map.
* Whether NAVAIDs, such as VORs, are displayed on the map.
* Whether aircraft photos are displayed for the highlighted aircraft.
* The timeout, in seconds, after which an aircraft will be removed from the table and map, if an ADS-B frame has not been received from it.
* The font used for the table.
* Whether demodulator statistics are displayed (primarily an option for developers).
@ -69,11 +82,25 @@ 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).
<h3>12: Display Flight Paths</h3>
![ADS-B Demodulator display settings](../../../doc/img/ADSBDemod_plugin_displaysettings.png)
Checking this button draws a line on the map showing aircraft's flight paths, as determined from received ADS-B frames.
<h3>13: Display Flight Path</h3>
<h3>13: Feed</h3>
Checking this button draws a line on the map showing the highlighted aircraft's flight paths, as determined from received ADS-B frames.
<h3>14: Display Flight All Paths</h3>
Checking this button draws flight paths for all aircraft.
<h3>15: Download flight information for selected flight</h3>
When clicked, flight information (departure and arrival airport and times) is downloaded for the aircraft highlighted in the ADS-B data table using the aviationstack.com API.
To be able to use this, a callsign for the highlighted aircraft must have been received. Also, the callsign must be mappable to a flight number, which is not always possible (this is typically
the case for callsigns that end in two characters, as for these, some digits from the flight number will have been omitted).
To use this feature, an (aviationstack)[aviationstack.com] API Key must be entered in the Display Settings dialog (12). A free key giving 500 API calls per month is (available)[https://aviationstack.com/product].
<h3>16: Feed</h3>
Checking Feed enables feeding received ADS-B frames to aggregators such as ADS-B Exchange: https://www.adsbexchange.com or ADSBHub
: https://www.adsbhub.org. Right clicking on the Feed button opens the Feed Settings dialog.
@ -85,7 +112,7 @@ The server hostname and port to send the frames to should be entered in the Serv
The Beast binary and Hex formats are as detailed here: https://wiki.jetvision.de/wiki/Mode-S_Beast:Data_Output_Formats
<h3>Open Notifications Dialog</h3>
<h3>17: Open Notifications Dialog</h3>
When clicked, opens the Notifications Dialog, which allows speech notifications or programs/scripts to be run when aircraft matching user-defined rules are seen.
@ -159,31 +186,19 @@ In the Speech and Command strings, variables can be used to substitute in data f
* ${eta}
* ${ata}
<h3>Download flight information for selected flight</h3>
When clicked, flight information (departure and arrival airport and times) is downloaded for the aircraft highlighted in the ADS-B data table using the aviationstack.com API.
To be able to use this, a callsign for the highlighted aircraft must have been received. Also, the callsign must be mappable to a flight number, which is not always possible (this is typically
the case for callsigns that end in two characters, as for these, some digits from the flight number will have been omitted).
To use this feature, an (aviationstack)[aviationstack.com] API Key must be entered in the Display Settings dialog (11). A free key giving 500 API calls per month is (available)[https://aviationstack.com/product].
<h3>Start/stop Logging ADS-B frames to .csv File</h3>
<h3>18: Start/stop Logging ADS-B frames to .csv File</h3>
When checked, writes all received ADS-B frames to a .csv file.
<h3>.csv Log Filename</h3>
<h3>19: .csv Log Filename</h3>
Click to specify the name of the .csv file which received ADS-B frames are logged to.
<h3>Read Data from .csv File</h3>
<h3>20: Read Data from .csv File</h3>
Click to specify a previously written ADS-B .csv log file, which is read and used to update the ADS-B data table and map.
<h3>14: Refresh list of devices</h3>
Use this button to refresh the list of devices.
<h3>15: Select device set</h3>
<h3>21: Select device set</h3>
Specify the SDRangel device set that will be have its centre frequency set when an airport ATC frequency is clicked on the map. Typically, this device set would be a second SDR (as ATC frequencies are around 120MHz, so they can not be received simultaneously with 1090MHz for ADS-B) and have an AM Demodulator channel plugin.
@ -240,10 +255,12 @@ If an ADS-B frame has not been received from an aircraft for 60 seconds, the air
<h2>Map</h2>
The map displays aircraft locations and data geographically.
The map displays aircraft locations and data geographically. Four types of map can be chosen from in the Display Settings dialog: Aviation, Avation (Dark), Street and Satellite.
![ADS-B Demodulator Map](../../../doc/img/ADSBDemod_plugin_map.png)
![ADS-B Demodulator Map](../../../doc/img/ADSBDemod_plugin_map2.png)
The initial antenna location is placed according to My Position set under the Preferences > My Position menu. The position is only updated when the ADS-B demodulator plugin is first opened.
Aircraft are only placed upon the map when a position can be calculated, which can require several frames to be received.
@ -259,3 +276,5 @@ Aircraft are only placed upon the map when a position can be calculated, which c
Airline logos and flags are by Steve Hibberd from https://radarspotting.com
Map icons are by Alice Design, Alex Ahineev, Botho Willer, Verry Obito, Sean Maldjia, Tinashe Mugayi, Georgiana Ionescu, Andreas Vögele, Tom Fricker, Will Sullivan, Tim Tores, BGBOXXX Design, and Angriawan Ditya Zulkarnain from the Noun Project https://thenounproject.com/
NDB icon is by Inductiveload from WikiMedia.

View File

@ -199,6 +199,8 @@ set(sdrbase_SOURCES
util/message.cpp
util/messagequeue.cpp
util/morse.cpp
util/openaip.cpp
util/planespotters.cpp
util/png.cpp
util/prettyprint.cpp
util/rtpsink.cpp
@ -410,6 +412,8 @@ set(sdrbase_HEADERS
util/messagequeue.h
util/morse.h
util/movingaverage.h
util/openaip.h
util/planespotters.h
util/png.h
util/prettyprint.h
util/rtpsink.h

View File

@ -72,11 +72,6 @@ void AviationStack::getFlightInformation(const QString& flight)
url.setQuery(query);
m_networkManager->get(QNetworkRequest(url));
/*QFile file("flight.json");
if (file.open(QIODevice::ReadOnly))
{
parseJson(file.readAll());
}*/
}
void AviationStack::handleReply(QNetworkReply* reply)

422
sdrbase/util/openaip.cpp Normal file
View File

@ -0,0 +1,422 @@
///////////////////////////////////////////////////////////////////////////////////
// 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 "openaip.h"
const QStringList OpenAIP::m_countryCodes = {
"ad",
"ae",
"af",
"ag",
"ai",
"al",
"am",
"an",
"ao",
"aq",
"ar",
"as",
"at",
"au",
"aw",
"ax",
"az",
"ba",
"bb",
"bd",
"be",
"bf",
"bg",
"bh",
"bi",
"bj",
"bl",
"bm",
"bn",
"bo",
"bq",
"br",
"bs",
"bt",
"bv",
"bw",
"by",
"bz",
"ca",
"cc",
"cd",
"cf",
"cg",
"ch",
"ci",
"ck",
"cl",
"cm",
"cn",
"co",
"cr",
"cu",
"cv",
"cw",
"cx",
"cy",
"cz",
"de",
"dj",
"dk",
"dm",
"do",
"dz",
"ec",
"ee",
"eg",
"eh",
"er",
"es",
"et",
"fi",
"fj",
"fk",
"fm",
"fo",
"fr",
"ga",
"gb",
"ge",
"gf",
"gg",
"gh",
"gi",
"gl",
"gm",
"gn",
"gp",
"gq",
"gr",
"gs",
"gt",
"gu",
"gw",
"gy",
"hk",
"hm",
"hn",
"hr",
"hu",
"id",
"ie",
"il",
"im",
"in",
"io",
"iq",
"ir",
"is",
"it",
"je",
"jm",
"jo",
"jp",
"ke",
"kg",
"kh",
"ki",
"km",
"kn",
"kp",
"kr",
"kw",
"ky",
"kz",
"la",
"lb",
"lc",
"li",
"lk",
"lr",
"ls",
"lt",
"lu",
"lv",
"ly",
"ma",
"mc",
"md",
"me",
"mf",
"mg",
"mh",
"mk",
"ml",
"mm",
"mn",
"mo",
"mp",
"mq",
"mr",
"ms",
"mt",
"mu",
"mv",
"mw",
"mx",
"my",
"mz",
"na",
"nc",
"ne",
"nf",
"ng",
"ni",
"nl",
"no",
"np",
"nr",
"nu",
"nz",
"om",
"pa",
"pe",
"pf",
"pg",
"ph",
"pk",
"pl",
"pm",
"pn",
"pr",
"ps",
"pt",
"pw",
"py",
"qa",
"re",
"ro",
"rs",
"ru",
"rw",
"sa",
"sb",
"sc",
"sd",
"se",
"sg",
"sh",
"si",
"sj",
"sk",
"sl",
"sm",
"sn",
"so",
"sr",
"ss",
"st",
"sv",
"sx",
"sy",
"sz",
"tc",
"td",
"tf",
"tg",
"th",
"tj",
"tk",
"tl",
"tm",
"tn",
"to",
"tr",
"tt",
"tv",
"tw",
"tz",
"ua",
"ug",
"um",
"us",
"uy",
"uz",
"va",
"vc",
"ve",
"vg",
"vi",
"vn",
"vu",
"wf",
"ws",
"ye",
"yt",
"za",
"zm",
"zw"
};
OpenAIP::OpenAIP(QObject *parent) :
QObject(parent)
{
connect(&m_dlm, &HttpDownloadManager::downloadComplete, this, &OpenAIP::downloadFinished);
}
OpenAIP::~OpenAIP()
{
disconnect(&m_dlm, &HttpDownloadManager::downloadComplete, this, &OpenAIP::downloadFinished);
}
QString OpenAIP::getDataDir()
{
// Get directory to store app data in
QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
// First dir is writable
return locations[0];
}
QString OpenAIP::getAirspaceFilename(int i)
{
return getAirspaceFilename(m_countryCodes[i]);
}
QString OpenAIP::getAirspaceFilename(const QString& countryCode)
{
return getDataDir() + "/" + countryCode + "_asp.xml";
}
QString OpenAIP::getAirspaceURL(int i)
{
if (i < m_countryCodes.size()) {
return QString(OPENAIP_AIRSPACE_URL).arg(m_countryCodes[i]);
} else {
return QString();
}
}
void OpenAIP::downloadAirspaces()
{
m_countryIndex = 0;
downloadAirspace();
}
void OpenAIP::downloadAirspace()
{
QString filename = getAirspaceFilename(m_countryIndex);
QString urlString = getAirspaceURL(m_countryIndex);
QUrl dbURL(urlString);
qDebug() << "OpenAIP::downloadAirspace: Downloading " << urlString;
emit downloadingURL(urlString);
m_dlm.download(dbURL, filename);
}
QString OpenAIP::getNavAidsFilename(int i)
{
return getNavAidsFilename(m_countryCodes[i]);
}
QString OpenAIP::getNavAidsFilename(const QString& countryCode)
{
return getDataDir() + "/" + countryCode + "_nav.xml";
}
QString OpenAIP::getNavAidsURL(int i)
{
if (i < m_countryCodes.size()) {
return QString(OPENAIP_NAVAIDS_URL).arg(m_countryCodes[i]);
} else {
return QString();
}
}
void OpenAIP::downloadNavAids()
{
m_countryIndex = 0;
downloadNavAid();
}
void OpenAIP::downloadNavAid()
{
QString filename = getNavAidsFilename(m_countryIndex);
QString urlString = getNavAidsURL(m_countryIndex);
QUrl dbURL(urlString);
qDebug() << "OpenAIP::downloadNavAid: Downloading " << urlString;
emit downloadingURL(urlString);
m_dlm.download(dbURL, filename);
}
void OpenAIP::downloadFinished(const QString& filename, bool success)
{
// Not all countries have corresponding files, so we should expect some errors
if (!success) {
qDebug() << "OpenAIP::downloadFinished: Failed: " << filename;
}
if (filename == getNavAidsFilename(m_countryIndex))
{
m_countryIndex++;
if (m_countryIndex < m_countryCodes.size()) {
downloadNavAid();
} else {
emit downloadNavAidsFinished();
}
}
else if (filename == getAirspaceFilename(m_countryIndex))
{
m_countryIndex++;
if (m_countryIndex < m_countryCodes.size()) {
downloadAirspace();
} else {
emit downloadAirspaceFinished();
}
}
else
{
qDebug() << "OpenAIP::downloadFinished: Unexpected filename: " << filename;
emit downloadError(QString("Unexpected filename: %1").arg(filename));
}
}
// Read airspaces for all countries
QList<Airspace *> OpenAIP::readAirspaces()
{
QList<Airspace *> airspaces;
for (const auto& countryCode : m_countryCodes) {
airspaces.append(readAirspaces(countryCode));
}
return airspaces;
}
// Read airspaces for a single country
QList<Airspace *> OpenAIP::readAirspaces(const QString& countryCode)
{
return Airspace::readXML(getAirspaceFilename(countryCode));
}
// Read NavAids for all countries
QList<NavAid *> OpenAIP::readNavAids()
{
QList<NavAid *> navAids;
for (const auto& countryCode : m_countryCodes) {
navAids.append(readNavAids(countryCode));
}
return navAids;
}
// Read NavAids for a single country
QList<NavAid *> OpenAIP::readNavAids(const QString& countryCode)
{
return NavAid::readXML(getNavAidsFilename(countryCode));
}

431
sdrbase/util/openaip.h Normal file
View File

@ -0,0 +1,431 @@
///////////////////////////////////////////////////////////////////////////////////
// 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_OPENAIP_H
#define INCLUDE_OPENAIP_H
#include <QString>
#include <QFile>
#include <QByteArray>
#include <QHash>
#include <QList>
#include <QDebug>
#include <QXmlStreamReader>
#include <QPointF>
#include <stdio.h>
#include <string.h>
#include "export.h"
#include "util/units.h"
#include "util/httpdownloadmanager.h"
// Server is moving, use new URL and .xml extension instead of .aip
//#define OPENAIP_AIRSPACE_URL "https://www.openaip.net/customer_export_akfshb9237tgwiuvb4tgiwbf/%1_asp.aip"
//#define OPENAIP_NAVAIDS_URL "https://www.openaip.net/customer_export_akfshb9237tgwiuvb4tgiwbf/%1_nav.aip"
#define OPENAIP_AIRSPACE_URL "https://storage.googleapis.com/29f98e10-a489-4c82-ae5e-489dbcd4912f/%1_asp.xml"
#define OPENAIP_NAVAIDS_URL "https://storage.googleapis.com/29f98e10-a489-4c82-ae5e-489dbcd4912f/%1_nav.xml"
struct SDRBASE_API Airspace {
struct AltLimit {
QString m_reference; // STD, MSL
int m_alt; // Altitude
QString m_altUnit; // FL (Flight level) or F (Feet)
};
QString m_category; // A-G, GLIDING, DANGER, PROHIBITED, TMZ
int m_id;
QString m_country; // GB
QString m_name; // BIGGIN HILL ATZ 129.405 - TODO: Extract frequency so we can tune to it
AltLimit m_top; // Top of airspace
AltLimit m_bottom; // Bottom of airspace
QVector<QPointF> m_polygon;
QPointF m_center; // Center of polygon
QPointF m_position; // Position for text (not at the center, otherwise it will clash with airport)
void calculatePosition()
{
qreal minX, maxX;
qreal minY, maxY;
if (m_polygon.size() > 0)
{
minX = maxX = m_polygon[0].x();
minY = maxY = m_polygon[0].y();
for (int i = 1; i < m_polygon.size(); i++)
{
qreal x = m_polygon[i].x();
qreal y = m_polygon[i].y();
minX = std::min(minX, x);
maxX = std::max(maxX, x);
minY = std::min(minY, y);
maxY = std::max(maxY, y);
}
m_center.setX(minX + (maxX - minX) / 2.0);
m_center.setY(minY + (maxY - minY) * 2.0);
m_position.setX(minX + (maxX - minX) / 2.0);
m_position.setY(minY + (maxY - minY) * 3.0 / 4.0);
}
}
QString getAlt(const AltLimit *altlimit) const
{
// Format on UK charts
if (altlimit->m_alt == 0) {
return "SFC"; // Surface
} if (altlimit->m_altUnit == "FL") {
return QString("FL%1").arg(altlimit->m_alt);
} else if (altlimit->m_altUnit == "F") {
return QString("%1'").arg(altlimit->m_alt);
} else {
return QString("%1 %2").arg(altlimit->m_alt).arg(altlimit->m_altUnit);
}
}
double heightInMetres(const AltLimit *altlimit) const
{
if (altlimit->m_altUnit == "FL") {
return Units::feetToMetres(altlimit->m_alt * 100);
} else if (altlimit->m_altUnit == "F") {
return Units::feetToMetres(altlimit->m_alt);
} else {
return altlimit->m_alt;
}
}
double topHeightInMetres() const
{
return heightInMetres(&m_top);
}
double bottomHeightInMetres() const
{
return heightInMetres(&m_bottom);
}
// Read OpenAIP XML file
static QList<Airspace *> readXML(const QString &filename)
{
QList<Airspace *> airspaces;
QFile file(filename);
if (file.open(QIODevice::ReadOnly | QIODevice::Text))
{
QXmlStreamReader xmlReader(&file);
while(!xmlReader.atEnd() && !xmlReader.hasError())
{
if (xmlReader.readNextStartElement())
{
if (xmlReader.name() == "ASP")
{
Airspace *airspace = new Airspace();
airspace->m_category = xmlReader.attributes().value("CATEGORY").toString();
while(xmlReader.readNextStartElement())
{
if (xmlReader.name() == QLatin1String("ID"))
{
airspace->m_id = xmlReader.readElementText().toInt();
}
else if (xmlReader.name() == QLatin1String("COUNTRY"))
{
airspace->m_country = xmlReader.readElementText();
}
else if (xmlReader.name() == QLatin1String("NAME"))
{
airspace->m_name = xmlReader.readElementText();
}
else if (xmlReader.name() == QLatin1String("ALTLIMIT_TOP"))
{
while(xmlReader.readNextStartElement())
{
airspace->m_top.m_reference = xmlReader.attributes().value("REFERENCE").toString();
airspace->m_top.m_altUnit = xmlReader.attributes().value("UNIT").toString();
if (xmlReader.name() == QLatin1String("ALT"))
{
airspace->m_top.m_alt = xmlReader.readElementText().toInt();
}
else
{
xmlReader.skipCurrentElement();
}
}
}
else if (xmlReader.name() == QLatin1String("ALTLIMIT_BOTTOM"))
{
while(xmlReader.readNextStartElement())
{
airspace->m_bottom.m_reference = xmlReader.attributes().value("REFERENCE").toString();
airspace->m_bottom.m_altUnit = xmlReader.attributes().value("UNIT").toString();
if (xmlReader.name() == QLatin1String("ALT"))
{
airspace->m_bottom.m_alt = xmlReader.readElementText().toInt();
}
else
{
xmlReader.skipCurrentElement();
}
}
}
else if (xmlReader.name() == QLatin1String("GEOMETRY"))
{
while(xmlReader.readNextStartElement())
{
if (xmlReader.name() == QLatin1String("POLYGON"))
{
QString pointsString = xmlReader.readElementText();
QStringList points = pointsString.split(",");
for (const auto& ps : points)
{
QStringList split = ps.trimmed().split(" ");
if (split.size() == 2)
{
QPointF pf(split[0].toDouble(), split[1].toDouble());
airspace->m_polygon.append(pf);
}
else
{
qDebug() << "Airspace::readXML - Unexpected polygon point format: " << ps;
}
}
}
else
{
xmlReader.skipCurrentElement();
}
}
}
else
{
xmlReader.skipCurrentElement();
}
}
airspace->calculatePosition();
//qDebug() << "Adding airspace: " << airspace->m_name << " " << airspace->m_category;
airspaces.append(airspace);
}
}
}
file.close();
}
else
{
qDebug() << "Airspace::readXML: Could not open " << filename << " for reading.";
}
return airspaces;
}
};
struct SDRBASE_API NavAid {
QString m_ident; // 2 or 3 character ident
QString m_type; // NDB, VOR, VOR-DME or VORTAC
QString m_name;
float m_latitude;
float m_longitude;
float m_elevation;
float m_frequencykHz;
QString m_channel;
int m_range; // Nautical miles
float m_magneticDeclination;
bool m_alignedTrueNorth; // Is the VOR aligned to true North, rather than magnetic (may be the case at high latitudes)
int getRangeMetres() const
{
return Units::nauticalMilesToIntegerMetres((float)m_range);
}
// OpenAIP XML file
static QList<NavAid *> readXML(const QString &filename)
{
QList<NavAid *> navAidInfo;
QFile file(filename);
if (file.open(QIODevice::ReadOnly | QIODevice::Text))
{
QXmlStreamReader xmlReader(&file);
while(!xmlReader.atEnd() && !xmlReader.hasError())
{
if (xmlReader.readNextStartElement())
{
if (xmlReader.name() == "NAVAID")
{
QStringRef typeRef = xmlReader.attributes().value("TYPE");
if ((typeRef == QLatin1String("NDB"))
|| (typeRef == QLatin1String("DME"))
|| (typeRef == QLatin1String("VOR"))
|| (typeRef== QLatin1String("VOR-DME"))
|| (typeRef == QLatin1String("VORTAC")))
{
QString type = typeRef.toString();
QString name;
QString id;
float lat = 0.0f;
float lon = 0.0f;
float elevation = 0.0f;
float frequency = 0.0f;
QString channel;
int range = 25;
float declination = 0.0f;
bool alignedTrueNorth = false;
while(xmlReader.readNextStartElement())
{
if (xmlReader.name() == QLatin1String("NAME"))
{
name = xmlReader.readElementText();
}
else if (xmlReader.name() == QLatin1String("ID"))
{
id = xmlReader.readElementText();
}
else if (xmlReader.name() == QLatin1String("GEOLOCATION"))
{
while(xmlReader.readNextStartElement())
{
if (xmlReader.name() == QLatin1String("LAT")) {
lat = xmlReader.readElementText().toFloat();
} else if (xmlReader.name() == QLatin1String("LON")) {
lon = xmlReader.readElementText().toFloat();
} else if (xmlReader.name() == QLatin1String("ELEV")) {
elevation = xmlReader.readElementText().toFloat();
} else {
xmlReader.skipCurrentElement();
}
}
}
else if (xmlReader.name() == QLatin1String("RADIO"))
{
while(xmlReader.readNextStartElement())
{
if (xmlReader.name() == QLatin1String("FREQUENCY"))
{
if (type == "NDB") {
frequency = xmlReader.readElementText().toFloat();
} else {
frequency = xmlReader.readElementText().toFloat() * 1000.0;
}
} else if (xmlReader.name() == QLatin1String("CHANNEL")) {
channel = xmlReader.readElementText();
} else {
xmlReader.skipCurrentElement();
}
}
}
else if (xmlReader.name() == QLatin1String("PARAMS"))
{
while(xmlReader.readNextStartElement())
{
if (xmlReader.name() == QLatin1String("RANGE")) {
range = xmlReader.readElementText().toInt();
} else if (xmlReader.name() == QLatin1String("DECLINATION")) {
declination = xmlReader.readElementText().toFloat();
} else if (xmlReader.name() == QLatin1String("ALIGNEDTOTRUENORTH")) {
alignedTrueNorth = xmlReader.readElementText() == "TRUE";
} else {
xmlReader.skipCurrentElement();
}
}
}
else
{
xmlReader.skipCurrentElement();
}
}
NavAid *navAid = new NavAid();
navAid->m_ident = id;
// Check idents conform to our filtering rules
if (navAid->m_ident.size() < 2) {
qDebug() << "NavAid::readXML: Ident less than 2 characters: " << navAid->m_ident;
} else if (navAid->m_ident.size() > 3) {
qDebug() << "NavAid::readXML: Ident greater than 3 characters: " << navAid->m_ident;
}
navAid->m_type = type;
navAid->m_name = name;
navAid->m_frequencykHz = frequency;
navAid->m_channel = channel;
navAid->m_latitude = lat;
navAid->m_longitude = lon;
navAid->m_elevation = elevation;
navAid->m_range = range;
navAid->m_magneticDeclination = declination;
navAid->m_alignedTrueNorth = alignedTrueNorth;
navAidInfo.append(navAid);
}
}
}
}
file.close();
}
else
{
qDebug() << "NavAid::readNavAidsXML: Could not open " << filename << " for reading.";
}
return navAidInfo;
}
};
class SDRBASE_API OpenAIP : public QObject {
Q_OBJECT
public:
OpenAIP(QObject* parent = nullptr);
~OpenAIP();
static const QStringList m_countryCodes;
static QList<Airspace *> readAirspaces();
static QList<Airspace *> readAirspaces(const QString& countryCode);
static QList<NavAid *> readNavAids();
static QList<NavAid *> readNavAids(const QString& countryCode);
void downloadAirspaces();
void downloadNavAids();
private:
HttpDownloadManager m_dlm;
int m_countryIndex;
static QString getDataDir();
static QString getAirspaceFilename(int i);
static QString getAirspaceFilename(const QString& countryCode);
static QString getAirspaceURL(int i);
static QString getNavAidsFilename(int i);
static QString getNavAidsFilename(const QString& countryCode);
static QString getNavAidsURL(int i);
void downloadAirspace();
void downloadNavAid();
public slots:
void downloadFinished(const QString& filename, bool success);
signals:
void downloadingURL(const QString& url);
void downloadError(const QString& error);
void downloadAirspaceFinished();
void downloadNavAidsFinished();
};
#endif // INCLUDE_OPENAIP_H

View File

@ -0,0 +1,145 @@
///////////////////////////////////////////////////////////////////////////////////
// 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 "planespotters.h"
#include <QDebug>
#include <QFile>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
PlaneSpotters::PlaneSpotters()
{
m_networkManager = new QNetworkAccessManager();
connect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(handleReply(QNetworkReply*)));
}
PlaneSpotters::~PlaneSpotters()
{
disconnect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(handleReply(QNetworkReply*)));
delete m_networkManager;
}
void PlaneSpotters::getAircraftPhoto(const QString& icao)
{
if (m_photos.contains(icao))
{
emit aircraftPhoto(m_photos[icao]);
}
else
{
// Create a new photo hash table entry
PlaneSpottersPhoto *photo = new PlaneSpottersPhoto();
photo->m_icao = icao;
m_photos.insert(icao, photo);
// Fetch from network
QUrl url(QString("https://api.planespotters.net/pub/photos/hex/%1").arg(icao));
QNetworkRequest request(url);
request.setRawHeader("User-Agent", "SDRangel/1.0"); // Get 403 error without this
request.setOriginatingObject(photo);
m_networkManager->get(request);
}
}
void PlaneSpotters::handleReply(QNetworkReply* reply)
{
if (reply)
{
if (!reply->error())
{
if (reply->url().path().startsWith("/pub/photos/hex")) {
parseJson((PlaneSpottersPhoto *)reply->request().originatingObject(), reply->readAll());
} else {
parsePhoto((PlaneSpottersPhoto *)reply->request().originatingObject(), reply->readAll());
}
}
else
{
qDebug() << "PlaneSpotters::handleReply: error: " << reply->error();
}
reply->deleteLater();
}
else
{
qDebug() << "PlaneSpotters::handleReply: reply is null";
}
}
void PlaneSpotters::parsePhoto(PlaneSpottersPhoto *photo, QByteArray bytes)
{
photo->m_pixmap.loadFromData(bytes);
emit aircraftPhoto(photo);
}
void PlaneSpotters::parseJson(PlaneSpottersPhoto *photo, QByteArray bytes)
{
QJsonDocument document = QJsonDocument::fromJson(bytes);
if (document.isObject())
{
QJsonObject obj = document.object();
if (obj.contains(QStringLiteral("photos")))
{
QJsonArray photos = obj.value(QStringLiteral("photos")).toArray();
if (photos.size() > 0)
{
QJsonObject photoObj = photos[0].toObject();
if (photoObj.contains(QStringLiteral("id"))) {
photo->m_link = photoObj.value(QStringLiteral("id")).toString();
}
if (photoObj.contains(QStringLiteral("thumbnail")))
{
QJsonObject thumbnailObj = photoObj.value(QStringLiteral("thumbnail")).toObject();
photo->m_thumbnail.m_src = thumbnailObj.value(QStringLiteral("src")).toString();
QJsonObject sizeObj = thumbnailObj.value(QStringLiteral("size")).toObject();
photo->m_thumbnail.m_width = sizeObj.value(QStringLiteral("width")).toInt();
photo->m_thumbnail.m_height = sizeObj.value(QStringLiteral("width")).toInt();
}
if (photoObj.contains(QStringLiteral("link"))) {
photo->m_link = photoObj.value(QStringLiteral("link")).toString();
}
if (photoObj.contains(QStringLiteral("photographer"))) {
photo->m_photographer = photoObj.value(QStringLiteral("photographer")).toString();
}
if (!photo->m_thumbnail.m_src.isEmpty())
{
QUrl url(photo->m_thumbnail.m_src);
QNetworkRequest request(url);
request.setOriginatingObject(photo);
m_networkManager->get(request);
}
}
else
{
qDebug() << "PlaneSpotters::handleReply: data array is empty";
}
}
else
{
qDebug() << "PlaneSpotters::handleReply: Object doesn't contain data: " << obj;
}
}
else
{
qDebug() << "PlaneSpotters::handleReply: Document is not an object: " << document;
}
}

View File

@ -0,0 +1,80 @@
///////////////////////////////////////////////////////////////////////////////////
// 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_PLANESPOTTERS_H
#define INCLUDE_PLANESPOTTERS_H
#include <QtCore>
#include <QDateTime>
#include <QPixmap>
#include <QHash>
#include "export.h"
class QNetworkAccessManager;
class QNetworkReply;
class SDRBASE_API PlaneSpottersPhoto : public QObject {
Q_OBJECT
struct Thumbnail {
QString m_src;
int m_width;
int m_height;
};
public:
QString m_icao;
QString m_id;
Thumbnail m_thumbnail;
Thumbnail m_largeThumbnail;
QString m_link;
QString m_photographer
;
QPixmap m_pixmap;
};
// PlaneSpotters API wrapper
// Allows downloading of images of aircraft from https://www.planespotters.net/
// Note that API terms of use require us not to cache images on disk
class SDRBASE_API PlaneSpotters : public QObject
{
Q_OBJECT
public:
PlaneSpotters();
~PlaneSpotters();
void getAircraftPhoto(const QString& icao);
signals:
void aircraftPhoto(const PlaneSpottersPhoto *photo); // Called when photo is available.
private:
void parseJson(PlaneSpottersPhoto *photo, QByteArray bytes);
void parsePhoto(PlaneSpottersPhoto *photo, QByteArray bytes);
QNetworkAccessManager *m_networkManager;
QHash<QString, PlaneSpottersPhoto *> m_photos;
public slots:
void handleReply(QNetworkReply* reply);
};
#endif /* INCLUDE_PLANESPOTTERS_H */