diff --git a/.appveyor.yml b/.appveyor.yml index ac7c574c2..0ae8a6d99 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -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 \ diff --git a/debian/control b/debian/control index ad6bf58cb..33b328769 100644 --- a/debian/control +++ b/debian/control @@ -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, diff --git a/doc/img/ADSBDemod_plugin.png b/doc/img/ADSBDemod_plugin.png index ff66edc46..1525d5ea6 100644 Binary files a/doc/img/ADSBDemod_plugin.png and b/doc/img/ADSBDemod_plugin.png differ diff --git a/doc/img/ADSBDemod_plugin.xcf b/doc/img/ADSBDemod_plugin.xcf index 3abd861be..cc965a9b6 100644 Binary files a/doc/img/ADSBDemod_plugin.xcf and b/doc/img/ADSBDemod_plugin.xcf differ diff --git a/doc/img/ADSBDemod_plugin_displaysettings.png b/doc/img/ADSBDemod_plugin_displaysettings.png new file mode 100644 index 000000000..3738aa3e5 Binary files /dev/null and b/doc/img/ADSBDemod_plugin_displaysettings.png differ diff --git a/doc/img/ADSBDemod_plugin_map.png b/doc/img/ADSBDemod_plugin_map.png index 41b1c8690..b90d7f38f 100644 Binary files a/doc/img/ADSBDemod_plugin_map.png and b/doc/img/ADSBDemod_plugin_map.png differ diff --git a/doc/img/ADSBDemod_plugin_map2.png b/doc/img/ADSBDemod_plugin_map2.png new file mode 100644 index 000000000..7fec2cae9 Binary files /dev/null and b/doc/img/ADSBDemod_plugin_map2.png differ diff --git a/doc/img/ADSBDemod_plugin_settings.png b/doc/img/ADSBDemod_plugin_settings.png new file mode 100644 index 000000000..b6f77fe5d Binary files /dev/null and b/doc/img/ADSBDemod_plugin_settings.png differ diff --git a/plugins/channelrx/demodadsb/CMakeLists.txt b/plugins/channelrx/demodadsb/CMakeLists.txt index 6e190ece5..4691c1ca3 100644 --- a/plugins/channelrx/demodadsb/CMakeLists.txt +++ b/plugins/channelrx/demodadsb/CMakeLists.txt @@ -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() diff --git a/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.cpp b/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.cpp index f93bbfa6b..5a27fb46f 100644 --- a/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.cpp +++ b/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.cpp @@ -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 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(); } diff --git a/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.h b/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.h index 38820d0d2..66b9a04f6 100644 --- a/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.h +++ b/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.h @@ -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(); diff --git a/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.ui b/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.ui index 57beefb6b..5ae7f0222 100644 --- a/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.ui +++ b/plugins/channelrx/demodadsb/adsbdemoddisplaydialog.ui @@ -7,7 +7,7 @@ 0 0 417 - 287 + 638 @@ -23,38 +23,14 @@ - - + + - Display airports with size + Display demodulator statistics - - - - The units to use for altitude, speed and climb rate - - - - ft, kn, ft/min - - - - - m, kph, m/s - - - - - - - - Aircraft timeout (s) - - - - + Displays airports within the specified distance in kilometres from My Position @@ -64,7 +40,219 @@ - + + + + How long in seconds after not receiving any frames will an aircraft be removed from the table and map + + + 1000000 + + + + + + + Map type + + + + + + + Airspaces to display + + + + + + + aviationstack.com API key for accessing flight information + + + + + + + Airspace categories to display + + + + A + + + IFR only + + + Unchecked + + + + + B + + + IFR and VFR with ATC clearance + + + Unchecked + + + + + C + + + IFR and VFR with ATC clearance + + + Unchecked + + + + + D + + + IFR and VFR with ATC clearance + + + Unchecked + + + + + E + + + IFR with clearance and VFR + + + Unchecked + + + + + G + + + Uncontrolled + + + Unchecked + + + + + FIR + + + Flight Information Region + + + Unchecked + + + + + CTR + + + Controlled Traffic Region + + + Unchecked + + + + + TMZ + + + Transponder Mandatory Zone + + + Unchecked + + + + + RMZ + + + Radio Mandatory Zone + + + Unchecked + + + + + RESTRICTED + + + Unchecked + + + + + GLIDING + + + Unchecked + + + + + DANGER + + + Unchecked + + + + + PROHIBITED + + + Unchecked + + + + + WAVE + + + Unchecked + + + + + + + + Resize the columns in the table after an aircraft is added to it + + + + + + + + + + Display NAVAIDs + + + + + + + Resize columns after adding aircraft + + + + Sets the minimum airport size that will be displayed on the map @@ -86,95 +274,14 @@ - - - - Units - - - - - - - Select a font for the table - - - Select... - - - - - - - How long in seconds after not receiving any frames will an aircraft be removed from the table and map - - - 1000000 - - - - + - Airport display distance (km) + Display heliports - - - - Table font - - - - - - - avaitionstack API key - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Resize the columns in the table after an aircraft is added to it - - - - - - - - - - Resize columns after adding aircraft - - - - - - - When checked, heliports are displayed on the map - - - - - - - + @@ -191,23 +298,152 @@ - + - Display heliports + Display airports with size - - + + - Display demodulator statistics + avaitionstack API key + + + + + + + Aircraft timeout (s) + + + + + + + Displays airspace within the specified distance in kilometres from My Position + + + 20000 + + + + + + + The units to use for altitude, speed and climb rate + + + + ft, kn, ft/min + + + + + m, kph, m/s + + + + + + + + Units - + - aviationstack.com API key for accessing flight information + Select a font for the table + + + Select... + + + + + + + Airport display distance (km) + + + + + + + Type of map to display + + + + Aviation + + + + + Aviation (Dark) + + + + + Street + + + + + Satellite + + + + + + + + Airspace display distance (km) + + + + + + + When checked, heliports are displayed on the map + + + + + + + + + + Table font + + + + + + + Display NAVAIDs such as VORs and NDBs + + + + + + + + + + Display aircraft photos + + + + + + + Download and display photos of highlighted aircraft + + + @@ -226,6 +462,22 @@ + + units + mapType + airportSize + heliports + airportRange + airspaces + airspaceRange + navAids + photos + timeout + font + autoResizeTableColumns + displayStats + apiKey + diff --git a/plugins/channelrx/demodadsb/adsbdemodgui.cpp b/plugins/channelrx/demodadsb/adsbdemodgui.cpp index d6674ee4f..24658a767 100644 --- a/plugins/channelrx/demodadsb/adsbdemodgui.cpp +++ b/plugins/channelrx/demodadsb/adsbdemodgui.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #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 sizes = icon.availableSizes(); + if (sizes.size() > 0) { + ui->photoFlag->setPixmap(icon.pixmap(sizes[0])); + } + + updatePhotoFlightInformation(aircraft); + + QString aircraftDetails = ""; // 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("
Manufacturer:%1").arg(manufacturer)); + } + QString model = aircraft->m_modelItem->text(); + if (!model.isEmpty()) { + aircraftDetails.append(QString("
Aircraft:%1").arg(model)); + } + QString owner = aircraft->m_ownerItem->text(); + if (!owner.isEmpty()) { + aircraftDetails.append(QString("
Owner:%1").arg(owner)); + } + QString operatorICAO = aircraft->m_operatorICAOItem->text(); + if (!operatorICAO.isEmpty()) { + aircraftDetails.append(QString("
Operator:%1").arg(operatorICAO)); + } + QString registered = aircraft->m_registeredItem->text(); + if (!registered.isEmpty()) { + aircraftDetails.append(QString("
Registered:%1").arg(registered)); + } + aircraftDetails.append("
"); + 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("
%1 - %2").arg(dep).arg(arr); + if (!std.isEmpty() && !sta.isEmpty()) { + flightDetails.append(QString("
STD%1STA%2").arg(std).arg(sta)); + } + if ((!atd.isEmpty() || !etd.isEmpty()) && (!ata.isEmpty() || !eta.isEmpty())) + { + if (!atd.isEmpty()) { + flightDetails.append(QString("
Actual%1").arg(atd)); + } else if (!etd.isEmpty()) { + flightDetails.append(QString("
Estimated%1").arg(etd)); + } + if (!ata.isEmpty()) { + flightDetails.append(QString("Actual%1").arg(ata)); + } else if (!eta.isEmpty()) { + flightDetails.append(QString("Estimated%1").arg(eta)); + } + } + flightDetails.append(""); + } + 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("map"); + QGeoCoordinate coords; + double zoom; + if (object != nullptr) + { + coords = object->property("center").value(); + zoom = object->property("zoomLevel").value(); + } + + // 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("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; + } +} diff --git a/plugins/channelrx/demodadsb/adsbdemodgui.h b/plugins/channelrx/demodadsb/adsbdemodgui.h index 0e9a9be75..2821b5eef 100644 --- a/plugins/channelrx/demodadsb/adsbdemodgui.h +++ b/plugins/channelrx/demodadsb/adsbdemodgui.h @@ -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 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 roleNames() const { + QHash roles; + roles[nameRole] = "name"; + roles[detailsRole] = "details"; + roles[positionRole] = "position"; + roles[airspaceBorderColorRole] = "airspaceBorderColor"; + roles[airspaceFillColorRole] = "airspaceFillColor"; + roles[airspacePolygonRole] = "airspacePolygon"; + return roles; + } + +private: + QList m_airspaces; + QList 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 roleNames() const { + QHash roles; + roles[positionRole] = "position"; + roles[navAidDataRole] = "navAidData"; + roles[navAidImageRole] = "navAidImage"; + roles[bubbleColourRole] = "bubbleColour"; + roles[selectedRole] = "selected"; + return roles; + } + +private: + QList m_navAids; + QList 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 *m_airportInfo; // Hashed on id AircraftModel m_aircraftModel; AirportModel m_airportModel; + AirspaceModel m_airspaceModel; + NavAidModel m_navAidModel; QHash m_airlineIcons; // Hashed on airline ICAO QHash m_flagIcons; // Hashed on country QHash *m_prefixMap; // Registration to country (flag name) QHash *m_militaryMap; // Operator airforce to military (flag name) + QList m_airspaces; + QList 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(); }; diff --git a/plugins/channelrx/demodadsb/adsbdemodgui.ui b/plugins/channelrx/demodadsb/adsbdemodgui.ui index dc0a394cd..7856d1513 100644 --- a/plugins/channelrx/demodadsb/adsbdemodgui.ui +++ b/plugins/channelrx/demodadsb/adsbdemodgui.ui @@ -518,6 +518,20 @@ + + + + Download airspaces and NAVAIDs from OpenAIP (40MB) + + + ... + + + + :/icons/vor.png:/icons/vor.png + + + @@ -572,6 +586,23 @@ + + + + Download flight information for selected aircraft + + + ... + + + + :/info.png:/info.png + + + false + + + @@ -606,23 +637,6 @@ - - - - Download flight information for selected aircraft - - - ... - - - - :/info.png:/info.png - - - false - - - @@ -738,7 +752,7 @@ 0 140 600 - 291 + 270 @@ -750,13 +764,13 @@ 600 - 0 + 270 ADS-B Data - + 2 @@ -774,6 +788,12 @@ + + + 0 + 0 + + QAbstractItemView::NoEditTriggers @@ -1048,6 +1068,125 @@ + + + + 4 + + + + + 4 + + + + + + 0 + 0 + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + + + + + 200 + 0 + + + + + 200 + 16777215 + + + + + + + Qt::RichText + + + + + + + + 0 + 0 + + + + + 200 + 0 + + + + + 200 + 16777215 + + + + + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + Qt::Vertical + + + + 0 + 40 + + + + + + @@ -1132,6 +1271,16 @@
gui/rollupwidget.h
1 + + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
+ + ClickableLabel + QLabel +
gui/clickablelabel.h
+
ValueDialZ QWidget @@ -1144,11 +1293,6 @@
gui/levelmeter.h
1
- - ButtonSwitch - QToolButton -
gui/buttonswitch.h
-
deltaFrequency @@ -1161,9 +1305,16 @@ threshold getOSNDB getAirportDB + getAirspacesDB displaySettings flightPaths + allFlightPaths + flightInfo feed + notifications + logEnable + logFilename + logOpen devicesRefresh device adsbData diff --git a/plugins/channelrx/demodadsb/adsbdemodsettings.cpp b/plugins/channelrx/demodadsb/adsbdemodsettings.cpp index 581f54ac5..4b3e57464 100644 --- a/plugins/channelrx/demodadsb/adsbdemodsettings.cpp +++ b/plugins/channelrx/demodadsb/adsbdemodsettings.cpp @@ -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; } diff --git a/plugins/channelrx/demodadsb/adsbdemodsettings.h b/plugins/channelrx/demodadsb/adsbdemodsettings.h index 0ef95065a..ea6ed6f07 100644 --- a/plugins/channelrx/demodadsb/adsbdemodsettings.h +++ b/plugins/channelrx/demodadsb/adsbdemodsettings.h @@ -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; } diff --git a/plugins/channelrx/demodadsb/adsbosmtemplateserver.cpp b/plugins/channelrx/demodadsb/adsbosmtemplateserver.cpp new file mode 100644 index 000000000..71bdd4946 --- /dev/null +++ b/plugins/channelrx/demodadsb/adsbosmtemplateserver.cpp @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "adsbosmtemplateserver.h" diff --git a/plugins/channelrx/demodadsb/adsbosmtemplateserver.h b/plugins/channelrx/demodadsb/adsbosmtemplateserver.h new file mode 100644 index 000000000..4f744e578 --- /dev/null +++ b/plugins/channelrx/demodadsb/adsbosmtemplateserver.h @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_ADSBOSMTEMPLATE_SERVER_H_ +#define INCLUDE_ADSBOSMTEMPLATE_SERVER_H_ + +#include +#include +#include + +// 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\" : \"WikiMedia Foundation\",\ + \"DataCopyRight\" : \"OpenStreetMap 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\" : \"Maptiler\",\ + \"DataCopyRight\" : \"Maptiler\"\ + }").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\" : \"CartoDB\",\ + \"DataCopyRight\" : \"CartoDB\"\ + }").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\" : \"Thunderforest\",\ + \"DataCopyRight\" : \"OpenStreetMap 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 diff --git a/plugins/channelrx/demodadsb/icons.qrc b/plugins/channelrx/demodadsb/icons.qrc index 6923f2feb..78a629ff7 100644 --- a/plugins/channelrx/demodadsb/icons.qrc +++ b/plugins/channelrx/demodadsb/icons.qrc @@ -3,5 +3,6 @@ icons/aircraft.png icons/controltower.png icons/allflightpaths.png + icons/vor.png diff --git a/plugins/channelrx/demodadsb/icons/vor.png b/plugins/channelrx/demodadsb/icons/vor.png new file mode 100644 index 000000000..ac66aad04 Binary files /dev/null and b/plugins/channelrx/demodadsb/icons/vor.png differ diff --git a/plugins/channelrx/demodadsb/map.qrc b/plugins/channelrx/demodadsb/map.qrc index 508a51eb7..12209f5e4 100644 --- a/plugins/channelrx/demodadsb/map.qrc +++ b/plugins/channelrx/demodadsb/map.qrc @@ -16,5 +16,10 @@ map/heliport.png map/antenna.png map/truck.png + map/VOR.png + map/VOR-DME.png + map/VORTAC.png + map/NDB.png + map/DME.png diff --git a/plugins/channelrx/demodadsb/map/DME.png b/plugins/channelrx/demodadsb/map/DME.png new file mode 100644 index 000000000..f95bac447 Binary files /dev/null and b/plugins/channelrx/demodadsb/map/DME.png differ diff --git a/plugins/channelrx/demodadsb/map/NDB.png b/plugins/channelrx/demodadsb/map/NDB.png new file mode 100644 index 000000000..4987f3be0 Binary files /dev/null and b/plugins/channelrx/demodadsb/map/NDB.png differ diff --git a/plugins/channelrx/demodadsb/map/VOR-DME.png b/plugins/channelrx/demodadsb/map/VOR-DME.png new file mode 100644 index 000000000..c2e2c412d Binary files /dev/null and b/plugins/channelrx/demodadsb/map/VOR-DME.png differ diff --git a/plugins/channelrx/demodadsb/map/VOR.png b/plugins/channelrx/demodadsb/map/VOR.png new file mode 100644 index 000000000..0d6949fde Binary files /dev/null and b/plugins/channelrx/demodadsb/map/VOR.png differ diff --git a/plugins/channelrx/demodadsb/map/VORTAC.png b/plugins/channelrx/demodadsb/map/VORTAC.png new file mode 100644 index 000000000..f7df0a0b3 Binary files /dev/null and b/plugins/channelrx/demodadsb/map/VORTAC.png differ diff --git a/plugins/channelrx/demodadsb/map/map.qml b/plugins/channelrx/demodadsb/map/map.qml index b64fd2024..ab5416ee0 100644 --- a/plugins/channelrx/demodadsb/map/map.qml +++ b/plugins/channelrx/demodadsb/map/map.qml @@ -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)) diff --git a/plugins/channelrx/demodadsb/ourairportsdb.h b/plugins/channelrx/demodadsb/ourairportsdb.h index 8d186a8c6..f41e1a2ab 100644 --- a/plugins/channelrx/demodadsb/ourairportsdb.h +++ b/plugins/channelrx/demodadsb/ourairportsdb.h @@ -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 { diff --git a/plugins/channelrx/demodadsb/readme.md b/plugins/channelrx/demodadsb/readme.md index baf5c494a..37635eeed 100644 --- a/plugins/channelrx/demodadsb/readme.md +++ b/plugins/channelrx/demodadsb/readme.md @@ -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. -

Interface

+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) +

Interface

+ +![ADS-B Demodulator plugin settings](../../../doc/img/ADSBDemod_plugin_settings.png) +

1: Frequency shift from center frequency of reception value

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

9: Download Opensky-Network Aircraft Database

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

10: Download OurAirports Airport Databases

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

11: Display Settings

+

11: Download OpenAIP Airspace and NAVAID Databases

+ +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. + +

12: Display Settings

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). -

12: Display Flight Paths

+![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. +

13: Display Flight Path

-

13: Feed

+Checking this button draws a line on the map showing the highlighted aircraft's flight paths, as determined from received ADS-B frames. + +

14: Display Flight All Paths

+ +Checking this button draws flight paths for all aircraft. + +

15: Download flight information for selected flight

+ +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]. + +

16: Feed

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 -

Open Notifications Dialog

+

17: Open Notifications Dialog

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

Download flight information for selected flight

- -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]. - -

Start/stop Logging ADS-B frames to .csv File

+

18: Start/stop Logging ADS-B frames to .csv File

When checked, writes all received ADS-B frames to a .csv file. -

.csv Log Filename

+

19: .csv Log Filename

Click to specify the name of the .csv file which received ADS-B frames are logged to. -

Read Data from .csv File

+

20: Read Data from .csv File

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

14: Refresh list of devices

- -Use this button to refresh the list of devices. - -

15: Select device set

+

21: Select device set

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

Map

-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. diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt index 67b8e2c59..3189b9798 100644 --- a/sdrbase/CMakeLists.txt +++ b/sdrbase/CMakeLists.txt @@ -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 diff --git a/sdrbase/util/flightinformation.cpp b/sdrbase/util/flightinformation.cpp index e93e0dcd7..34470097e 100644 --- a/sdrbase/util/flightinformation.cpp +++ b/sdrbase/util/flightinformation.cpp @@ -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) diff --git a/sdrbase/util/openaip.cpp b/sdrbase/util/openaip.cpp new file mode 100644 index 000000000..0950ed568 --- /dev/null +++ b/sdrbase/util/openaip.cpp @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#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 OpenAIP::readAirspaces() +{ + QList airspaces; + for (const auto& countryCode : m_countryCodes) { + airspaces.append(readAirspaces(countryCode)); + } + return airspaces; +} + +// Read airspaces for a single country +QList OpenAIP::readAirspaces(const QString& countryCode) +{ + return Airspace::readXML(getAirspaceFilename(countryCode)); +} + +// Read NavAids for all countries +QList OpenAIP::readNavAids() +{ + QList navAids; + for (const auto& countryCode : m_countryCodes) { + navAids.append(readNavAids(countryCode)); + } + return navAids; +} + +// Read NavAids for a single country +QList OpenAIP::readNavAids(const QString& countryCode) +{ + return NavAid::readXML(getNavAidsFilename(countryCode)); +} diff --git a/sdrbase/util/openaip.h b/sdrbase/util/openaip.h new file mode 100644 index 000000000..003ed6033 --- /dev/null +++ b/sdrbase/util/openaip.h @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_OPENAIP_H +#define INCLUDE_OPENAIP_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#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 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 readXML(const QString &filename) + { + QList 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 readXML(const QString &filename) + { + QList 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 readAirspaces(); + static QList readAirspaces(const QString& countryCode); + static QList readNavAids(); + static QList 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 diff --git a/sdrbase/util/planespotters.cpp b/sdrbase/util/planespotters.cpp new file mode 100644 index 000000000..6e5401ffc --- /dev/null +++ b/sdrbase/util/planespotters.cpp @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "planespotters.h" + +#include +#include +#include +#include +#include +#include +#include + +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; + } +} diff --git a/sdrbase/util/planespotters.h b/sdrbase/util/planespotters.h new file mode 100644 index 000000000..11175b486 --- /dev/null +++ b/sdrbase/util/planespotters.h @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_PLANESPOTTERS_H +#define INCLUDE_PLANESPOTTERS_H + +#include +#include +#include +#include + +#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 m_photos; + +public slots: + void handleReply(QNetworkReply* reply); + +}; + +#endif /* INCLUDE_PLANESPOTTERS_H */