mirror of
https://github.com/f4exb/sdrangel.git
synced 2024-11-13 20:01:46 -05:00
2508 lines
98 KiB
C++
2508 lines
98 KiB
C++
///////////////////////////////////////////////////////////////////////////////////
|
|
// Copyright (C) 2020 Edouard Griffiths, F4EXB //
|
|
// Copyright (C) 2020 Jon Beniston, M7RCE //
|
|
// //
|
|
// This program is free software; you can redistribute it and/or modify //
|
|
// it under the terms of the GNU General Public License as published by //
|
|
// the Free Software Foundation as version 3 of the License, or //
|
|
// (at your option) any later version. //
|
|
// //
|
|
// This program is distributed in the hope that it will be useful, //
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
|
|
// GNU General Public License V3 for more details. //
|
|
// //
|
|
// You should have received a copy of the GNU General Public License //
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
|
|
///////////////////////////////////////////////////////////////////////////////////
|
|
|
|
#include <cmath>
|
|
|
|
#include "device/deviceuiset.h"
|
|
#include <QDockWidget>
|
|
#include <QMainWindow>
|
|
#include <QQuickItem>
|
|
#include <QGeoLocation>
|
|
#include <QQmlContext>
|
|
#include <QDesktopServices>
|
|
#include <QUrl>
|
|
#include <QMessageBox>
|
|
#include <QDebug>
|
|
#include <QProcess>
|
|
|
|
#include "SWGMapItem.h"
|
|
|
|
#include "ui_adsbdemodgui.h"
|
|
#include "channel/channelwebapiutils.h"
|
|
#include "plugin/pluginapi.h"
|
|
#include "util/simpleserializer.h"
|
|
#include "util/db.h"
|
|
#include "util/units.h"
|
|
#include "gui/basicchannelsettingsdialog.h"
|
|
#include "gui/devicestreamselectiondialog.h"
|
|
#include "gui/crightclickenabler.h"
|
|
#include "dsp/dspengine.h"
|
|
#include "mainwindow.h"
|
|
|
|
#include "adsbdemodreport.h"
|
|
#include "adsbdemod.h"
|
|
#include "adsbdemodgui.h"
|
|
#include "adsbdemodfeeddialog.h"
|
|
#include "adsbdemoddisplaydialog.h"
|
|
#include "adsbdemodnotificationdialog.h"
|
|
#include "adsb.h"
|
|
|
|
const char *Aircraft::m_speedTypeNames[] = {
|
|
"GS", "TAS", "IAS"
|
|
};
|
|
|
|
ADSBDemodGUI* ADSBDemodGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel)
|
|
{
|
|
ADSBDemodGUI* gui = new ADSBDemodGUI(pluginAPI, deviceUISet, rxChannel);
|
|
return gui;
|
|
}
|
|
|
|
void ADSBDemodGUI::destroy()
|
|
{
|
|
delete this;
|
|
}
|
|
|
|
void ADSBDemodGUI::resetToDefaults()
|
|
{
|
|
m_settings.resetToDefaults();
|
|
displaySettings();
|
|
applySettings();
|
|
}
|
|
|
|
QByteArray ADSBDemodGUI::serialize() const
|
|
{
|
|
return m_settings.serialize();
|
|
}
|
|
|
|
bool ADSBDemodGUI::deserialize(const QByteArray& data)
|
|
{
|
|
if(m_settings.deserialize(data)) {
|
|
updateDeviceSetList();
|
|
displaySettings();
|
|
applySettings(true);
|
|
return true;
|
|
} else {
|
|
resetToDefaults();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Longitude zone (returns value in range [1,59]
|
|
static int cprNL(double lat)
|
|
{
|
|
if (lat == 0.0)
|
|
return 59;
|
|
else if ((lat == 87.0) || (lat == -87.0))
|
|
return 2;
|
|
else if ((lat > 87.0) || (lat < -87.0))
|
|
return 1;
|
|
else
|
|
{
|
|
double nz = 15.0;
|
|
double n = 1 - std::cos(M_PI / (2.0 * nz));
|
|
double d = std::cos(std::fabs(lat) * M_PI/180.0);
|
|
return std::floor((M_PI * 2.0) / std::acos(1.0 - (n/(d*d))));
|
|
}
|
|
}
|
|
|
|
static int cprN(double lat, int odd)
|
|
{
|
|
int nl = cprNL(lat) - odd;
|
|
if (nl > 1)
|
|
return nl;
|
|
else
|
|
return 1;
|
|
}
|
|
|
|
// Can't use std::fmod, as that works differently for negative numbers (See C.2.6.2)
|
|
static Real modulus(double x, double y)
|
|
{
|
|
return x - y * std::floor(x/y);
|
|
}
|
|
|
|
QString Aircraft::getImage()
|
|
{
|
|
if (m_emitterCategory.length() > 0)
|
|
{
|
|
if (!m_emitterCategory.compare("Heavy"))
|
|
return QString("aircraft_4engine.png");
|
|
else if (!m_emitterCategory.compare("Large"))
|
|
return QString("aircraft_2engine.png");
|
|
else if (!m_emitterCategory.compare("Small"))
|
|
return QString("aircraft_2enginesmall.png");
|
|
else if (!m_emitterCategory.compare("Rotorcraft"))
|
|
return QString("aircraft_helicopter.png");
|
|
else if (!m_emitterCategory.compare("High performance"))
|
|
return QString("aircraft_fighter.png");
|
|
else if (!m_emitterCategory.compare("Light")
|
|
|| !m_emitterCategory.compare("Ultralight")
|
|
|| !m_emitterCategory.compare("Glider/sailplane"))
|
|
return QString("aircraft_light.png");
|
|
else if (!m_emitterCategory.compare("Space vehicle"))
|
|
return QString("aircraft_space.png");
|
|
else if (!m_emitterCategory.compare("UAV"))
|
|
return QString("aircraft_drone.png");
|
|
else if (!m_emitterCategory.compare("Emergency vehicle")
|
|
|| !m_emitterCategory.compare("Service vehicle"))
|
|
return QString("truck.png");
|
|
else
|
|
return QString("aircraft_2engine.png");
|
|
}
|
|
else
|
|
return QString("aircraft_2engine.png");
|
|
}
|
|
|
|
QString Aircraft::getText(bool all)
|
|
{
|
|
QStringList list;
|
|
if (m_callsign.length() > 0)
|
|
{
|
|
list.append(QString("Callsign: %1").arg(m_callsign));
|
|
}
|
|
else
|
|
{
|
|
list.append(QString("ICAO: %1").arg(m_icao, 1, 16));
|
|
}
|
|
if (m_showAll || m_isHighlighted || all)
|
|
{
|
|
if (m_aircraftInfo != nullptr)
|
|
{
|
|
if (m_aircraftInfo->m_model.size() > 0)
|
|
{
|
|
list.append(QString("Aircraft: %1").arg(m_aircraftInfo->m_model));
|
|
}
|
|
}
|
|
if (m_altitudeValid)
|
|
{
|
|
if (m_gui->useSIUints())
|
|
list.append(QString("Altitude: %1 (m)").arg(Units::feetToIntegerMetres(m_altitude)));
|
|
else
|
|
list.append(QString("Altitude: %1 (ft)").arg(m_altitude));
|
|
}
|
|
if (m_speedValid)
|
|
{
|
|
if (m_gui->useSIUints())
|
|
list.append(QString("%1: %2 (kph)").arg(m_speedTypeNames[m_speedType]).arg(Units::knotsToIntegerKPH(m_speed)));
|
|
else
|
|
list.append(QString("%1: %2 (kn)").arg(m_speedTypeNames[m_speedType]).arg(m_speed));
|
|
}
|
|
if (m_verticalRateValid)
|
|
{
|
|
QString desc;
|
|
Real rate;
|
|
QString units;
|
|
|
|
if (m_gui->useSIUints())
|
|
{
|
|
rate = Units::feetPerMinToIntegerMetresPerSecond(m_verticalRate);
|
|
units = QString("m/s");
|
|
}
|
|
else
|
|
{
|
|
rate = m_verticalRate;
|
|
units = QString("ft/min");
|
|
}
|
|
if (m_verticalRate == 0)
|
|
desc = "Level flight";
|
|
else if (rate > 0)
|
|
desc = QString("Climbing: %1 (%2)").arg(rate).arg(units);
|
|
else
|
|
desc = QString("Descending: %1 (%2)").arg(rate).arg(units);
|
|
list.append(QString(desc));
|
|
}
|
|
if ((m_status.length() > 0) && m_status.compare("No emergency")) {
|
|
list.append(m_status);
|
|
}
|
|
|
|
QString flightStatus = m_flightStatusItem->text();
|
|
if (!flightStatus.isEmpty()) {
|
|
list.append(QString("Flight status: %1").arg(flightStatus));
|
|
}
|
|
QString dep = m_depItem->text();
|
|
if (!dep.isEmpty()) {
|
|
list.append(QString("Departed: %1").arg(dep));
|
|
}
|
|
QString std = m_stdItem->text();
|
|
if (!std.isEmpty()) {
|
|
list.append(QString("STD: %1").arg(std));
|
|
}
|
|
QString atd = m_atdItem->text();
|
|
if (!atd.isEmpty())
|
|
{
|
|
list.append(QString("ATD: %1").arg(atd));
|
|
}
|
|
else
|
|
{
|
|
QString etd = m_etdItem->text();
|
|
if (!etd.isEmpty()) {
|
|
list.append(QString("ETD: %1").arg(etd));
|
|
}
|
|
}
|
|
QString arr = m_arrItem->text();
|
|
if (!arr.isEmpty()) {
|
|
list.append(QString("Arrival: %1").arg(arr));
|
|
}
|
|
QString sta = m_staItem->text();
|
|
if (!sta.isEmpty()) {
|
|
list.append(QString("STA: %1").arg(sta));
|
|
}
|
|
QString ata = m_ataItem->text();
|
|
if (!ata.isEmpty())
|
|
{
|
|
list.append(QString("ATA: %1").arg(ata));
|
|
}
|
|
else
|
|
{
|
|
QString eta = m_etaItem->text();
|
|
if (!eta.isEmpty()) {
|
|
list.append(QString("ETA: %1").arg(eta));
|
|
}
|
|
}
|
|
}
|
|
return list.join("\n");
|
|
}
|
|
|
|
QVariant AircraftModel::data(const QModelIndex &index, int role) const
|
|
{
|
|
int row = index.row();
|
|
if ((row < 0) || (row >= m_aircrafts.count()))
|
|
return QVariant();
|
|
if (role == AircraftModel::positionRole)
|
|
{
|
|
// Coordinates to display the aircraft icon at
|
|
QGeoCoordinate coords;
|
|
coords.setLatitude(m_aircrafts[row]->m_latitude);
|
|
coords.setLongitude(m_aircrafts[row]->m_longitude);
|
|
coords.setAltitude(Units::feetToMetres(m_aircrafts[row]->m_altitude));
|
|
return QVariant::fromValue(coords);
|
|
}
|
|
else if (role == AircraftModel::headingRole)
|
|
{
|
|
// What rotation to draw the aircraft icon at
|
|
return QVariant::fromValue(m_aircrafts[row]->m_heading);
|
|
}
|
|
else if (role == AircraftModel::adsbDataRole)
|
|
{
|
|
// Create the text to go in the bubble next to the aircraft
|
|
return QVariant::fromValue(m_aircrafts[row]->getText());
|
|
}
|
|
else if (role == AircraftModel::aircraftImageRole)
|
|
{
|
|
// Select an image to use for the aircraft
|
|
return QVariant::fromValue(m_aircrafts[row]->getImage());
|
|
}
|
|
else if (role == AircraftModel::bubbleColourRole)
|
|
{
|
|
// Select a background colour for the text bubble next to the aircraft
|
|
if (m_aircrafts[row]->m_isTarget)
|
|
return QVariant::fromValue(QColor("lightgreen"));
|
|
else if (m_aircrafts[row]->m_isHighlighted)
|
|
return QVariant::fromValue(QColor("orange"));
|
|
else if ((m_aircrafts[row]->m_status.length() > 0) && m_aircrafts[row]->m_status.compare("No emergency"))
|
|
return QVariant::fromValue(QColor("lightred"));
|
|
else
|
|
return QVariant::fromValue(QColor("lightblue"));
|
|
}
|
|
else if (role == AircraftModel::aircraftPathRole)
|
|
{
|
|
if ((m_flightPaths && m_aircrafts[row]->m_isHighlighted) || m_allFlightPaths)
|
|
return m_aircrafts[row]->m_coordinates;
|
|
else
|
|
return QVariantList();
|
|
}
|
|
else if (role == AircraftModel::showAllRole)
|
|
return QVariant::fromValue(m_aircrafts[row]->m_showAll);
|
|
else if (role == AircraftModel::highlightedRole)
|
|
return QVariant::fromValue(m_aircrafts[row]->m_isHighlighted);
|
|
else if (role == AircraftModel::targetRole)
|
|
return QVariant::fromValue(m_aircrafts[row]->m_isTarget);
|
|
return QVariant();
|
|
}
|
|
|
|
bool AircraftModel::setData(const QModelIndex &index, const QVariant& value, int role)
|
|
{
|
|
int row = index.row();
|
|
if ((row < 0) || (row >= m_aircrafts.count()))
|
|
return false;
|
|
if (role == AircraftModel::showAllRole)
|
|
{
|
|
bool showAll = value.toBool();
|
|
if (showAll != m_aircrafts[row]->m_showAll)
|
|
{
|
|
m_aircrafts[row]->m_showAll = showAll;
|
|
emit dataChanged(index, index);
|
|
}
|
|
return true;
|
|
}
|
|
else if (role == AircraftModel::highlightedRole)
|
|
{
|
|
bool highlight = value.toBool();
|
|
if (highlight != m_aircrafts[row]->m_isHighlighted)
|
|
{
|
|
m_aircrafts[row]->m_gui->highlightAircraft(m_aircrafts[row]);
|
|
emit dataChanged(index, index);
|
|
}
|
|
return true;
|
|
}
|
|
else if (role == AircraftModel::targetRole)
|
|
{
|
|
bool target = value.toBool();
|
|
if (target != m_aircrafts[row]->m_isTarget)
|
|
{
|
|
m_aircrafts[row]->m_gui->targetAircraft(m_aircrafts[row]);
|
|
emit dataChanged(index, index);
|
|
}
|
|
return true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
QVariant AirportModel::data(const QModelIndex &index, int role) const
|
|
{
|
|
int row = index.row();
|
|
if ((row < 0) || (row >= m_airports.count()))
|
|
return QVariant();
|
|
if (role == AirportModel::positionRole)
|
|
{
|
|
// Coordinates to display the airport icon at
|
|
QGeoCoordinate coords;
|
|
coords.setLatitude(m_airports[row]->m_latitude);
|
|
coords.setLongitude(m_airports[row]->m_longitude);
|
|
coords.setAltitude(Units::feetToMetres(m_airports[row]->m_elevation));
|
|
return QVariant::fromValue(coords);
|
|
}
|
|
else if (role == AirportModel::airportDataRole)
|
|
{
|
|
if (m_showFreq[row])
|
|
return QVariant::fromValue(m_airportDataFreq[row]);
|
|
else
|
|
return QVariant::fromValue(m_airports[row]->m_ident);
|
|
}
|
|
else if (role == AirportModel::airportDataRowsRole)
|
|
{
|
|
if (m_showFreq[row])
|
|
return QVariant::fromValue(m_airportDataFreqRows[row]);
|
|
else
|
|
return 1;
|
|
}
|
|
else if (role == AirportModel::airportImageRole)
|
|
{
|
|
// Select an image to use for the airport
|
|
if (m_airports[row]->m_type == ADSBDemodSettings::AirportType::Large)
|
|
return QVariant::fromValue(QString("airport_large.png"));
|
|
else if (m_airports[row]->m_type == ADSBDemodSettings::AirportType::Medium)
|
|
return QVariant::fromValue(QString("airport_medium.png"));
|
|
else if (m_airports[row]->m_type == ADSBDemodSettings::AirportType::Heliport)
|
|
return QVariant::fromValue(QString("heliport.png"));
|
|
else
|
|
return QVariant::fromValue(QString("airport_small.png"));
|
|
}
|
|
else if (role == AirportModel::bubbleColourRole)
|
|
{
|
|
// Select a background colour for the text bubble next to the airport
|
|
return QVariant::fromValue(QColor("lightyellow"));
|
|
}
|
|
else if (role == AirportModel::showFreqRole)
|
|
{
|
|
return QVariant::fromValue(m_showFreq[row]);
|
|
}
|
|
return QVariant();
|
|
}
|
|
|
|
bool AirportModel::setData(const QModelIndex &index, const QVariant& value, int role)
|
|
{
|
|
int row = index.row();
|
|
if ((row < 0) || (row >= m_airports.count()))
|
|
return false;
|
|
if (role == AirportModel::showFreqRole)
|
|
{
|
|
bool showFreq = value.toBool();
|
|
if (showFreq != m_showFreq[row])
|
|
{
|
|
m_showFreq[row] = showFreq;
|
|
emit dataChanged(index, index);
|
|
}
|
|
return true;
|
|
}
|
|
else if (role == AirportModel::selectedFreqRole)
|
|
{
|
|
int idx = value.toInt();
|
|
if ((idx >= 0) && (idx < m_airports[row]->m_frequencies.size()))
|
|
m_gui->setFrequency(m_airports[row]->m_frequencies[idx]->m_frequency * 1000000);
|
|
else if (idx == m_airports[row]->m_frequencies.size())
|
|
{
|
|
// Set airport as target
|
|
m_gui->target(m_airports[row]->m_name, m_azimuth[row], m_elevation[row]);
|
|
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)
|
|
{
|
|
return ChannelWebAPIUtils::setCenterFrequency(m_settings.m_deviceIndex, targetFrequencyHz);
|
|
}
|
|
|
|
// Called when we have both lat & long
|
|
void ADSBDemodGUI::updatePosition(Aircraft *aircraft)
|
|
{
|
|
if (!aircraft->m_positionValid)
|
|
{
|
|
aircraft->m_positionValid = true;
|
|
// Now we have a position, add a plane to the map
|
|
QGeoCoordinate coords;
|
|
coords.setLatitude(aircraft->m_latitude);
|
|
coords.setLongitude(aircraft->m_longitude);
|
|
m_aircraftModel.addAircraft(aircraft);
|
|
}
|
|
// Calculate range, azimuth and elevation to aircraft from station
|
|
m_azEl.setTarget(aircraft->m_latitude, aircraft->m_longitude, Units::feetToMetres(aircraft->m_altitude));
|
|
m_azEl.calculate();
|
|
aircraft->m_range = m_azEl.getDistance();
|
|
aircraft->m_azimuth = m_azEl.getAzimuth();
|
|
aircraft->m_elevation = m_azEl.getElevation();
|
|
aircraft->m_rangeItem->setText(QString::number(aircraft->m_range/1000.0, 'f', 1));
|
|
aircraft->m_azElItem->setText(QString("%1/%2").arg(std::round(aircraft->m_azimuth)).arg(std::round(aircraft->m_elevation)));
|
|
if (aircraft == m_trackAircraft)
|
|
m_adsbDemod->setTarget(aircraft->targetName(), aircraft->m_azimuth, aircraft->m_elevation);
|
|
|
|
// Send to Map feature
|
|
MessagePipes& messagePipes = MainCore::instance()->getMessagePipes();
|
|
QList<MessageQueue*> *mapMessageQueues = messagePipes.getMessageQueues(m_adsbDemod, "mapitems");
|
|
if (mapMessageQueues)
|
|
{
|
|
QList<MessageQueue*>::iterator it = mapMessageQueues->begin();
|
|
|
|
for (; it != mapMessageQueues->end(); ++it)
|
|
{
|
|
SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem();
|
|
swgMapItem->setName(new QString(QString("%1").arg(aircraft->m_icao, 0, 16)));
|
|
swgMapItem->setLatitude(aircraft->m_latitude);
|
|
swgMapItem->setLongitude(aircraft->m_longitude);
|
|
swgMapItem->setAltitude(Units::feetToMetres(aircraft->m_altitude));
|
|
swgMapItem->setImage(new QString(QString("qrc:///map/%1").arg(aircraft->getImage())));
|
|
swgMapItem->setImageRotation(aircraft->m_heading);
|
|
swgMapItem->setImageMinZoom(11);
|
|
swgMapItem->setText(new QString(aircraft->getText(true)));
|
|
|
|
MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_adsbDemod, swgMapItem);
|
|
(*it)->push(msg);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Called when we have lat & long from local decode and we need to check if it is in a valid range (<180nm/333km)
|
|
// or 1/4 of that for surface positions
|
|
bool ADSBDemodGUI::updateLocalPosition(Aircraft *aircraft, double latitude, double longitude, bool surfacePosition)
|
|
{
|
|
// Calculate range to aircraft from station
|
|
m_azEl.setTarget(latitude, longitude, Units::feetToMetres(aircraft->m_altitude));
|
|
m_azEl.calculate();
|
|
|
|
// Don't use the full 333km, as there may be some error in station position
|
|
if (m_azEl.getDistance() < (surfacePosition ? 80000 : 320000))
|
|
{
|
|
aircraft->m_latitude = latitude;
|
|
aircraft->m_longitude = longitude;
|
|
updatePosition(aircraft);
|
|
return true;
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
|
|
// Try to find an airline logo based on ICAO
|
|
QIcon *ADSBDemodGUI::getAirlineIcon(const QString &operatorICAO)
|
|
{
|
|
if (m_airlineIcons.contains(operatorICAO))
|
|
return m_airlineIcons.value(operatorICAO);
|
|
else
|
|
{
|
|
QIcon *icon = nullptr;
|
|
QString endPath = QString("/airlinelogos/%1.bmp").arg(operatorICAO);
|
|
// Try in user directory first, so they can customise
|
|
QString userIconPath = getDataDir() + endPath;
|
|
QFile file(userIconPath);
|
|
if (file.exists())
|
|
{
|
|
icon = new QIcon(userIconPath);
|
|
m_airlineIcons.insert(operatorICAO, icon);
|
|
}
|
|
else
|
|
{
|
|
// Try in resources
|
|
QResource resourceIconPath(QString(":%1").arg(endPath));
|
|
if (resourceIconPath.isValid())
|
|
{
|
|
icon = new QIcon(":" + endPath);
|
|
m_airlineIcons.insert(operatorICAO, icon);
|
|
}
|
|
else
|
|
qDebug() << "ADSBDemodGUI: No airline logo for " << operatorICAO;
|
|
}
|
|
return icon;
|
|
}
|
|
}
|
|
|
|
// Try to find an flag logo based on a country
|
|
QIcon *ADSBDemodGUI::getFlagIcon(const QString &country)
|
|
{
|
|
if (m_flagIcons.contains(country))
|
|
return m_flagIcons.value(country);
|
|
else
|
|
{
|
|
QIcon *icon = nullptr;
|
|
QString endPath = QString("/flags/%1.bmp").arg(country);
|
|
// Try in user directory first, so they can customise
|
|
QString userIconPath = getDataDir() + endPath;
|
|
QFile file(userIconPath);
|
|
if (file.exists())
|
|
{
|
|
icon = new QIcon(userIconPath);
|
|
m_flagIcons.insert(country, icon);
|
|
}
|
|
else
|
|
{
|
|
// Try in resources
|
|
QResource resourceIconPath(QString(":%1").arg(endPath));
|
|
if (resourceIconPath.isValid())
|
|
{
|
|
icon = new QIcon(":" + endPath);
|
|
m_flagIcons.insert(country, icon);
|
|
}
|
|
}
|
|
return icon;
|
|
}
|
|
}
|
|
|
|
void ADSBDemodGUI::handleADSB(
|
|
const QByteArray data,
|
|
const QDateTime dateTime,
|
|
float correlation,
|
|
float correlationOnes)
|
|
{
|
|
const char idMap[] = "?ABCDEFGHIJKLMNOPQRSTUVWXYZ????? ???????????????0123456789??????";
|
|
const QString categorySetA[] = {
|
|
"None",
|
|
"Light",
|
|
"Small",
|
|
"Large",
|
|
"High vortex",
|
|
"Heavy",
|
|
"High performance",
|
|
"Rotorcraft"
|
|
};
|
|
const QString categorySetB[] = {
|
|
"None",
|
|
"Glider/sailplane",
|
|
"Lighter-than-air",
|
|
"Parachutist",
|
|
"Ultralight",
|
|
"Reserved",
|
|
"UAV",
|
|
"Space vehicle"
|
|
};
|
|
const QString categorySetC[] = {
|
|
"None",
|
|
"Emergency vehicle",
|
|
"Service vehicle",
|
|
"Ground obstruction",
|
|
"Cluster obstacle",
|
|
"Line obstacle",
|
|
"Reserved",
|
|
"Reserved"
|
|
};
|
|
const QString emergencyStatus[] = {
|
|
"No emergency",
|
|
"General emergency",
|
|
"Lifeguard/Medical",
|
|
"Minimum fuel",
|
|
"No communications",
|
|
"Unlawful interference",
|
|
"Downed aircraft",
|
|
"Reserved"
|
|
};
|
|
|
|
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
|
|
int tc = (data[4] >> 3) & 0x1f; // Type code
|
|
|
|
Aircraft *aircraft;
|
|
if (m_aircraft.contains(icao))
|
|
{
|
|
// Update existing aircraft info
|
|
aircraft = m_aircraft.value(icao);
|
|
}
|
|
else
|
|
{
|
|
// Add new aircraft
|
|
aircraft = new Aircraft(this);
|
|
aircraft->m_icao = icao;
|
|
m_aircraft.insert(icao, aircraft);
|
|
// Check for TIS-B addresses
|
|
if ((df == 18) && !((df == 18) && ((ca == 0) || (ca == 1) || (ca == 6))))
|
|
aircraft->m_icaoItem->setText(QString("TIS-B %1").arg(aircraft->m_icao, 1, 16));
|
|
else
|
|
aircraft->m_icaoItem->setText(QString("%1").arg(aircraft->m_icao, 1, 16));
|
|
ui->adsbData->setSortingEnabled(false);
|
|
int row = ui->adsbData->rowCount();
|
|
ui->adsbData->setRowCount(row + 1);
|
|
ui->adsbData->setItem(row, ADSB_COL_ICAO, aircraft->m_icaoItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_CALLSIGN, aircraft->m_callsignItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_MODEL, aircraft->m_modelItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_AIRLINE, aircraft->m_airlineItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_ALTITUDE, aircraft->m_altitudeItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_SPEED, aircraft->m_speedItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_HEADING, aircraft->m_headingItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_VERTICALRATE, aircraft->m_verticalRateItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_RANGE, aircraft->m_rangeItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_AZEL, aircraft->m_azElItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_LATITUDE, aircraft->m_latitudeItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_LONGITUDE, aircraft->m_longitudeItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_CATEGORY, aircraft->m_emitterCategoryItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_STATUS, aircraft->m_statusItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_SQUAWK, aircraft->m_squawkItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_REGISTRATION, aircraft->m_registrationItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_COUNTRY, aircraft->m_countryItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_REGISTERED, aircraft->m_registeredItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_MANUFACTURER, aircraft->m_manufacturerNameItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_OWNER, aircraft->m_ownerItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_OPERATOR_ICAO, aircraft->m_operatorICAOItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_TIME, aircraft->m_timeItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_FRAMECOUNT, aircraft->m_adsbFrameCountItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_CORRELATION, aircraft->m_correlationItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_RSSI, aircraft->m_rssiItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_FLIGHT_STATUS, aircraft->m_flightStatusItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_DEP, aircraft->m_depItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_ARR, aircraft->m_arrItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_STD, aircraft->m_stdItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_ETD, aircraft->m_etdItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_ATD, aircraft->m_atdItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_STA, aircraft->m_staItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_ETA, aircraft->m_etaItem);
|
|
ui->adsbData->setItem(row, ADSB_COL_ATA, aircraft->m_ataItem);
|
|
// Look aircraft up in database
|
|
if (m_aircraftInfo != nullptr)
|
|
{
|
|
if (m_aircraftInfo->contains(icao))
|
|
{
|
|
aircraft->m_aircraftInfo = m_aircraftInfo->value(icao);
|
|
aircraft->m_modelItem->setText(aircraft->m_aircraftInfo->m_model);
|
|
aircraft->m_registrationItem->setText(aircraft->m_aircraftInfo->m_registration);
|
|
aircraft->m_manufacturerNameItem->setText(aircraft->m_aircraftInfo->m_manufacturerName);
|
|
aircraft->m_ownerItem->setText(aircraft->m_aircraftInfo->m_owner);
|
|
aircraft->m_operatorICAOItem->setText(aircraft->m_aircraftInfo->m_operatorICAO);
|
|
aircraft->m_registeredItem->setText(aircraft->m_aircraftInfo->m_registered);
|
|
// Try loading an airline logo based on operator ICAO
|
|
QIcon *icon = nullptr;
|
|
if (aircraft->m_aircraftInfo->m_operatorICAO.size() > 0)
|
|
{
|
|
icon = getAirlineIcon(aircraft->m_aircraftInfo->m_operatorICAO);
|
|
if (icon != nullptr)
|
|
{
|
|
aircraft->m_airlineItem->setSizeHint(QSize(85, 20));
|
|
aircraft->m_airlineItem->setIcon(*icon);
|
|
}
|
|
}
|
|
if (icon == nullptr)
|
|
{
|
|
if (aircraft->m_aircraftInfo->m_operator.size() > 0)
|
|
aircraft->m_airlineItem->setText(aircraft->m_aircraftInfo->m_operator);
|
|
else
|
|
aircraft->m_airlineItem->setText(aircraft->m_aircraftInfo->m_owner);
|
|
}
|
|
// Try loading a flag based on registration
|
|
if ((aircraft->m_aircraftInfo->m_registration.size() > 0) && (m_prefixMap != nullptr))
|
|
{
|
|
QString flag;
|
|
int idx = aircraft->m_aircraftInfo->m_registration.indexOf('-');
|
|
if (idx >= 0)
|
|
{
|
|
QString prefix;
|
|
|
|
// Some countries use AA-A - try these first as first letters are common
|
|
prefix = aircraft->m_aircraftInfo->m_registration.left(idx + 2);
|
|
if (m_prefixMap->contains(prefix))
|
|
flag = m_prefixMap->value(prefix);
|
|
else
|
|
{
|
|
// Try letters before '-'
|
|
prefix = aircraft->m_aircraftInfo->m_registration.left(idx);
|
|
if (m_prefixMap->contains(prefix))
|
|
flag = m_prefixMap->value(prefix);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// No '-' Could be military
|
|
if ((m_militaryMap != nullptr) && (m_militaryMap->contains(aircraft->m_aircraftInfo->m_operator)))
|
|
flag = m_militaryMap->value(aircraft->m_aircraftInfo->m_operator);
|
|
}
|
|
if (flag != "")
|
|
{
|
|
icon = getFlagIcon(flag);
|
|
if (icon != nullptr)
|
|
{
|
|
aircraft->m_countryItem->setSizeHint(QSize(40, 20));
|
|
aircraft->m_countryItem->setIcon(*icon);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (m_settings.m_autoResizeTableColumns)
|
|
ui->adsbData->resizeColumnsToContents();
|
|
ui->adsbData->setSortingEnabled(true);
|
|
// Check to see if we need to emit a notification about this new aircraft
|
|
checkStaticNotification(aircraft);
|
|
}
|
|
aircraft->m_time = dateTime;
|
|
QTime time = dateTime.time();
|
|
aircraft->m_timeItem->setText(QString("%1:%2:%3").arg(time.hour(), 2, 10, QLatin1Char('0')).arg(time.minute(), 2, 10, QLatin1Char('0')).arg(time.second(), 2, 10, QLatin1Char('0')));
|
|
aircraft->m_adsbFrameCount++;
|
|
aircraft->m_adsbFrameCountItem->setData(Qt::DisplayRole, aircraft->m_adsbFrameCount);
|
|
|
|
if (correlation < aircraft->m_minCorrelation)
|
|
aircraft->m_minCorrelation = correlation;
|
|
if (correlation > aircraft->m_maxCorrelation)
|
|
aircraft->m_maxCorrelation = correlation;
|
|
m_correlationAvg(correlation);
|
|
aircraft->m_correlationAvg(correlation);
|
|
aircraft->m_correlation = aircraft->m_correlationAvg.instantAverage();
|
|
aircraft->m_correlationItem->setText(QString("%1/%2/%3")
|
|
.arg(CalcDb::dbPower(aircraft->m_minCorrelation), 3, 'f', 1)
|
|
.arg(CalcDb::dbPower(aircraft->m_correlation), 3, 'f', 1)
|
|
.arg(CalcDb::dbPower(aircraft->m_maxCorrelation), 3, 'f', 1));
|
|
m_correlationOnesAvg(correlationOnes);
|
|
aircraft->m_rssiItem->setText(QString("%1")
|
|
.arg(CalcDb::dbPower(m_correlationOnesAvg.instantAverage()), 3, 'f', 1));
|
|
|
|
// ADS-B, non-transponder ADS-B or TIS-B rebroadcast of ADS-B (ADS-R)
|
|
if ((df == 17) || ((df == 18) && ((ca == 0) || (ca == 1) || (ca == 6))))
|
|
{
|
|
if ((tc >= 1) && ((tc <= 4)))
|
|
{
|
|
// Aircraft identification
|
|
int ec = data[4] & 0x7; // Emitter category
|
|
if (tc == 4)
|
|
aircraft->m_emitterCategory = categorySetA[ec];
|
|
else if (tc == 3)
|
|
aircraft->m_emitterCategory = categorySetB[ec];
|
|
else if (tc == 2)
|
|
aircraft->m_emitterCategory = categorySetC[ec];
|
|
else
|
|
aircraft->m_emitterCategory = QString("Reserved");
|
|
aircraft->m_emitterCategoryItem->setText(aircraft->m_emitterCategory);
|
|
|
|
// Flight/callsign - Extract 8 6-bit characters from 6 8-bit bytes, MSB first
|
|
unsigned char c[8];
|
|
char callsign[9];
|
|
c[0] = (data[5] >> 2) & 0x3f; // 6
|
|
c[1] = ((data[5] & 0x3) << 4) | ((data[6] & 0xf0) >> 4); // 2+4
|
|
c[2] = ((data[6] & 0xf) << 2) | ((data[7] & 0xc0) >> 6); // 4+2
|
|
c[3] = (data[7] & 0x3f); // 6
|
|
c[4] = (data[8] >> 2) & 0x3f;
|
|
c[5] = ((data[8] & 0x3) << 4) | ((data[9] & 0xf0) >> 4);
|
|
c[6] = ((data[9] & 0xf) << 2) | ((data[10] & 0xc0) >> 6);
|
|
c[7] = (data[10] & 0x3f);
|
|
// Map to ASCII
|
|
for (int i = 0; i < 8; i++)
|
|
callsign[i] = idMap[c[i]];
|
|
callsign[8] = '\0';
|
|
|
|
aircraft->m_callsign = QString(callsign).trimmed();
|
|
aircraft->m_callsignItem->setText(aircraft->m_callsign);
|
|
|
|
// Attempt to map callsign to flight number
|
|
if (!aircraft->m_callsign.isEmpty())
|
|
{
|
|
QRegularExpression flightNoExp("^[A-Z]{2,3}[0-9]{1,4}$");
|
|
// Airlines line BA add a single charater suffix that can be stripped
|
|
// If the suffix is two characters, then it typically means a digit
|
|
// has been replaced which I don't know how to guess
|
|
// E.g Easyjet might use callsign EZY67JQ for flight EZY6267
|
|
// BA use BAW90BG for BA890
|
|
QRegularExpression suffixedFlightNoExp("^([A-Z]{2,3})([0-9]{1,4})[A-Z]?$");
|
|
QRegularExpressionMatch suffixMatch;
|
|
|
|
if (flightNoExp.match(aircraft->m_callsign).hasMatch())
|
|
{
|
|
aircraft->m_flight = aircraft->m_callsign;
|
|
}
|
|
else if ((suffixMatch = suffixedFlightNoExp.match(aircraft->m_callsign)).hasMatch())
|
|
{
|
|
aircraft->m_flight = QString("%1%2").arg(suffixMatch.captured(1)).arg(suffixMatch.captured(2));
|
|
}
|
|
else
|
|
{
|
|
// Don't guess, to save wasting API calls
|
|
aircraft->m_flight = "";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
aircraft->m_flight = "";
|
|
}
|
|
}
|
|
else if (((tc >= 5) && (tc <= 18)) || ((tc >= 20) && (tc <= 22)))
|
|
{
|
|
bool surfacePosition = (tc >= 5) && (tc <= 8);
|
|
if (surfacePosition)
|
|
{
|
|
// Surface position
|
|
|
|
// Set altitude to 0, if we're on the surface
|
|
// Actual altitude may of course depend on airport elevation
|
|
aircraft->m_altitudeValid = true;
|
|
aircraft->m_altitudeItem->setData(Qt::DisplayRole, 0);
|
|
|
|
int movement = ((data[4] & 0x7) << 4) | ((data[5] >> 4) & 0xf);
|
|
if (movement == 0)
|
|
{
|
|
// No information available
|
|
aircraft->m_speedValid = false;
|
|
aircraft->m_speedItem->setData(Qt::DisplayRole, "");
|
|
}
|
|
else if (movement == 1)
|
|
{
|
|
// Aircraft stopped
|
|
aircraft->m_speedValid = true;
|
|
aircraft->m_speedItem->setData(Qt::DisplayRole, 0);
|
|
}
|
|
else if ((movement >= 2) && (movement <= 123))
|
|
{
|
|
// float base, step; // In knts
|
|
// int adjust;
|
|
// if ((movement >= 2) && (movement <= 8))
|
|
// {
|
|
// base = 0.125f;
|
|
// step = 0.125f;
|
|
// adjust = 2;
|
|
// }
|
|
// else if ((movement >= 9) && (movement <= 12))
|
|
// {
|
|
// base = 1.0f;
|
|
// step = 0.25f;
|
|
// adjust = 9;
|
|
// }
|
|
// else if ((movement >= 13) && (movement <= 38))
|
|
// {
|
|
// base = 2.0f;
|
|
// step = 0.5f;
|
|
// adjust = 13;
|
|
// }
|
|
// else if ((movement >= 39) && (movement <= 93))
|
|
// {
|
|
// base = 15.0f;
|
|
// step = 1.0f;
|
|
// adjust = 39;
|
|
// }
|
|
// else if ((movement >= 94) && (movement <= 108))
|
|
// {
|
|
// base = 70.0f;
|
|
// step = 2.0f;
|
|
// adjust = 94;
|
|
// }
|
|
// else
|
|
// {
|
|
// base = 100.0f;
|
|
// step = 5.0f;
|
|
// adjust = 109;
|
|
// }
|
|
aircraft->m_speedType = Aircraft::GS;
|
|
aircraft->m_speedValid = true;
|
|
aircraft->m_speedItem->setData(Qt::DisplayRole, m_settings.m_siUnits ? Units::knotsToIntegerKPH(aircraft->m_speed) : (int)std::round(aircraft->m_speed));
|
|
}
|
|
else if (movement == 124)
|
|
{
|
|
aircraft->m_speedValid = true;
|
|
aircraft->m_speedItem->setData(Qt::DisplayRole, m_settings.m_siUnits ? 324 : 175); // Actually greater than this
|
|
}
|
|
|
|
int groundTrackStatus = (data[5] >> 3) & 1;
|
|
int groundTrackValue = ((data[5] & 0x7) << 4) | ((data[6] >> 4) & 0xf);
|
|
if (groundTrackStatus)
|
|
{
|
|
aircraft->m_heading = std::round((groundTrackValue * 360.0/128.0));
|
|
aircraft->m_headingValid = true;
|
|
aircraft->m_headingItem->setData(Qt::DisplayRole, aircraft->m_heading);
|
|
}
|
|
}
|
|
else if (((tc >= 9) && (tc <= 18)) || ((tc >= 20) && (tc <= 22)))
|
|
{
|
|
// Airbourne position (9-18 baro, 20-22 GNSS)
|
|
int alt = ((data[5] & 0xff) << 4) | ((data[6] >> 4) & 0xf); // Altitude
|
|
int n = ((alt >> 1) & 0x7f0) | (alt & 0xf);
|
|
int alt_ft = n * ((alt & 0x10) ? 25 : 100) - 1000;
|
|
|
|
aircraft->m_altitude = alt_ft;
|
|
aircraft->m_altitudeValid = true;
|
|
// setData rather than setText so it sorts numerically
|
|
aircraft->m_altitudeItem->setData(Qt::DisplayRole, m_settings.m_siUnits ? Units::feetToIntegerMetres(aircraft->m_altitude) : aircraft->m_altitude);
|
|
}
|
|
|
|
int f = (data[6] >> 2) & 1; // CPR odd/even frame - should alternate every 0.2s
|
|
int lat_cpr = ((data[6] & 3) << 15) | ((data[7] & 0xff) << 7) | ((data[8] >> 1) & 0x7f);
|
|
int lon_cpr = ((data[8] & 1) << 16) | ((data[9] & 0xff) << 8) | (data[10] & 0xff);
|
|
|
|
aircraft->m_cprValid[f] = true;
|
|
aircraft->m_cprLat[f] = lat_cpr/131072.0f;
|
|
aircraft->m_cprLong[f] = lon_cpr/131072.0f;
|
|
aircraft->m_cprTime[f] = dateTime;
|
|
|
|
// CPR decoding
|
|
// Refer to Technical Provisions for Mode S Services and Extended Squitter - Appendix C2.6
|
|
// See also: https://mode-s.org/decode/adsb/airborne-position.html
|
|
// For global decoding, we need both odd and even frames
|
|
// We also need to check that both frames aren't greater than 10s apart in time (C.2.6.7), otherwise position may be out by ~10deg
|
|
// We could compare global + local methods to see if the positions are sensible
|
|
if (aircraft->m_cprValid[0] && aircraft->m_cprValid[1]
|
|
&& (std::abs(aircraft->m_cprTime[0].toSecsSinceEpoch() - aircraft->m_cprTime[1].toSecsSinceEpoch()) < 10)
|
|
&& !surfacePosition)
|
|
{
|
|
// Global decode using odd and even frames (C.2.6)
|
|
|
|
// Calculate latitude
|
|
const double dLatEven = 360.0/60.0;
|
|
const double dLatOdd = 360.0/59.0;
|
|
double latEven, latOdd;
|
|
double latitude, longitude;
|
|
int ni, m;
|
|
|
|
int j = std::floor(59.0f*aircraft->m_cprLat[0] - 60.0f*aircraft->m_cprLat[1] + 0.5);
|
|
latEven = dLatEven * (modulus(j, 60) + aircraft->m_cprLat[0]);
|
|
// Southern hemisphere is in range 270-360, so adjust to -90-0
|
|
if (latEven >= 270.0f)
|
|
latEven -= 360.0f;
|
|
latOdd = dLatOdd * (modulus(j, 59) + aircraft->m_cprLat[1]);
|
|
if (latOdd >= 270.0f)
|
|
latOdd -= 360.0f;
|
|
if (aircraft->m_cprTime[0] >= aircraft->m_cprTime[1])
|
|
latitude = latEven;
|
|
else
|
|
latitude = latOdd;
|
|
if ((latitude <= 90.0) && (latitude >= -90.0))
|
|
{
|
|
// Check if both frames in same latitude zone
|
|
int latEvenNL = cprNL(latEven);
|
|
int latOddNL = cprNL(latOdd);
|
|
if (latEvenNL == latOddNL)
|
|
{
|
|
// Calculate longitude
|
|
if (!f)
|
|
{
|
|
ni = cprN(latEven, 0);
|
|
m = std::floor(aircraft->m_cprLong[0] * (latEvenNL - 1) - aircraft->m_cprLong[1] * latEvenNL + 0.5f);
|
|
longitude = (360.0f/ni) * (modulus(m, ni) + aircraft->m_cprLong[0]);
|
|
}
|
|
else
|
|
{
|
|
ni = cprN(latOdd, 1);
|
|
m = std::floor(aircraft->m_cprLong[0] * (latOddNL - 1) - aircraft->m_cprLong[1] * latOddNL + 0.5f);
|
|
longitude = (360.0f/ni) * (modulus(m, ni) + aircraft->m_cprLong[1]);
|
|
}
|
|
if (longitude > 180.0f)
|
|
longitude -= 360.0f;
|
|
aircraft->m_latitude = latitude;
|
|
aircraft->m_latitudeItem->setData(Qt::DisplayRole, aircraft->m_latitude);
|
|
aircraft->m_longitude = longitude;
|
|
aircraft->m_longitudeItem->setData(Qt::DisplayRole, aircraft->m_longitude);
|
|
aircraft->m_coordinates.push_back(QVariant::fromValue(*new QGeoCoordinate(aircraft->m_latitude, aircraft->m_longitude, aircraft->m_altitude)));
|
|
updatePosition(aircraft);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
qDebug() << "ADSBDemodGUI::handleADSB: Invalid latitude " << latitude << " for " << QString("%1").arg(aircraft->m_icao, 1, 16)
|
|
<< " m_cprLat[0] " << aircraft->m_cprLat[0]
|
|
<< " m_cprLat[1] " << aircraft->m_cprLat[1];
|
|
aircraft->m_cprValid[0] = false;
|
|
aircraft->m_cprValid[1] = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Local decode using a single aircraft position + location of receiver
|
|
// Only valid if airbourne within 180nm/333km (C.2.6.4) or 45nm for surface
|
|
|
|
// Caclulate latitude
|
|
const double maxDeg = surfacePosition ? 90.0 : 360.0;
|
|
const double dLatEven = maxDeg/60.0;
|
|
const double dLatOdd = maxDeg/59.0;
|
|
double dLat = f ? dLatOdd : dLatEven;
|
|
double latitude, longitude;
|
|
|
|
int j = std::floor(m_azEl.getLocationSpherical().m_latitude/dLat) + std::floor(modulus(m_azEl.getLocationSpherical().m_latitude, dLat)/dLat - aircraft->m_cprLat[f] + 0.5);
|
|
latitude = dLat * (j + aircraft->m_cprLat[f]);
|
|
|
|
// Caclulate longitude
|
|
double dLong;
|
|
int latNL = cprNL(latitude);
|
|
if (f == 0)
|
|
{
|
|
if (latNL > 0)
|
|
dLong = maxDeg / latNL;
|
|
else
|
|
dLong = maxDeg;
|
|
}
|
|
else
|
|
{
|
|
if ((latNL - 1) > 0)
|
|
dLong = maxDeg / (latNL - 1);
|
|
else
|
|
dLong = maxDeg;
|
|
}
|
|
int m = std::floor(m_azEl.getLocationSpherical().m_longitude/dLong) + std::floor(modulus(m_azEl.getLocationSpherical().m_longitude, dLong)/dLong - aircraft->m_cprLong[f] + 0.5);
|
|
longitude = dLong * (m + aircraft->m_cprLong[f]);
|
|
|
|
if (updateLocalPosition(aircraft, latitude, longitude, surfacePosition))
|
|
{
|
|
aircraft->m_latitude = latitude;
|
|
aircraft->m_latitudeItem->setData(Qt::DisplayRole, aircraft->m_latitude);
|
|
aircraft->m_longitude = longitude;
|
|
aircraft->m_longitudeItem->setData(Qt::DisplayRole, aircraft->m_longitude);
|
|
aircraft->m_coordinates.push_back(QVariant::fromValue(*new QGeoCoordinate(aircraft->m_latitude, aircraft->m_longitude, aircraft->m_altitude)));
|
|
}
|
|
}
|
|
}
|
|
else if (tc == 19)
|
|
{
|
|
// Airbourne velocity
|
|
int st = data[4] & 0x7; // Subtype
|
|
if ((st == 1) || (st == 2))
|
|
{
|
|
// Ground speed
|
|
int s_ew = (data[5] >> 2) & 1; // East-west velocity sign
|
|
int v_ew = ((data[5] & 0x3) << 8) | (data[6] & 0xff); // East-west velocity
|
|
int s_ns = (data[7] >> 7) & 1; // North-south velocity sign
|
|
int v_ns = ((data[7] & 0x7f) << 3) | ((data[8] >> 5) & 0x7); // North-south velocity
|
|
|
|
int v_we;
|
|
int v_sn;
|
|
int v;
|
|
int h;
|
|
|
|
if (s_ew)
|
|
v_we = -1 * (v_ew - 1);
|
|
else
|
|
v_we = v_ew - 1;
|
|
if (s_ns)
|
|
v_sn = -1 * (v_ns - 1);
|
|
else
|
|
v_sn = v_ns - 1;
|
|
v = (int)std::sqrt(v_we*v_we + v_sn*v_sn);
|
|
h = std::atan2(v_we, v_sn) * 360.0/(2.0*M_PI);
|
|
if (h < 0)
|
|
h += 360;
|
|
|
|
aircraft->m_heading = h;
|
|
aircraft->m_headingValid = true;
|
|
aircraft->m_speed = v;
|
|
aircraft->m_speedType = Aircraft::GS;
|
|
aircraft->m_speedValid = true;
|
|
aircraft->m_headingItem->setData(Qt::DisplayRole, aircraft->m_heading);
|
|
aircraft->m_speedItem->setData(Qt::DisplayRole, m_settings.m_siUnits ? Units::knotsToIntegerKPH(aircraft->m_speed) : aircraft->m_speed);
|
|
}
|
|
else
|
|
{
|
|
// Airspeed
|
|
int s_hdg = (data[5] >> 2) & 1; // Heading status
|
|
int hdg = ((data[5] & 0x3) << 8) | (data[6] & 0xff); // Heading
|
|
if (s_hdg)
|
|
{
|
|
aircraft->m_heading = hdg/1024.0f*360.0f;
|
|
aircraft->m_headingValid = true;
|
|
aircraft->m_headingItem->setData(Qt::DisplayRole, aircraft->m_heading);
|
|
}
|
|
|
|
int as_t = (data[7] >> 7) & 1; // Airspeed type
|
|
int as = ((data[7] & 0x7f) << 3) | ((data[8] >> 5) & 0x7); // Airspeed
|
|
|
|
aircraft->m_speed = as;
|
|
aircraft->m_speedType = as_t ? Aircraft::IAS : Aircraft::TAS;
|
|
aircraft->m_speedValid = true;
|
|
aircraft->m_speedItem->setData(Qt::DisplayRole, m_settings.m_siUnits ? Units::knotsToIntegerKPH(aircraft->m_speed) : aircraft->m_speed);
|
|
}
|
|
int s_vr = (data[8] >> 3) & 1; // Vertical rate sign
|
|
int vr = ((data[8] & 0x7) << 6) | ((data[9] >> 2) & 0x3f); // Vertical rate
|
|
aircraft->m_verticalRate = (vr-1)*64*(s_vr?-1:1);
|
|
aircraft->m_verticalRateValid = true;
|
|
if (m_settings.m_siUnits)
|
|
aircraft->m_verticalRateItem->setData(Qt::DisplayRole, Units::feetPerMinToIntegerMetresPerSecond(aircraft->m_verticalRate));
|
|
else
|
|
aircraft->m_verticalRateItem->setData(Qt::DisplayRole, aircraft->m_verticalRate);
|
|
}
|
|
else if (tc == 28)
|
|
{
|
|
// Aircraft status
|
|
int st = data[4] & 0x7; // Subtype
|
|
if (st == 1)
|
|
{
|
|
int es = (data[5] >> 5) & 0x7; // Emergency state
|
|
int modeA = ((data[5] << 8) & 0x1f00) | (data[6] & 0xff); // Mode-A code (squawk)
|
|
aircraft->m_status = emergencyStatus[es];
|
|
aircraft->m_statusItem->setText(aircraft->m_status);
|
|
int a, b, c, d;
|
|
c = ((modeA >> 12) & 1) | ((modeA >> (10-1)) & 0x2) | ((modeA >> (8-2)) & 0x4);
|
|
a = ((modeA >> 11) & 1) | ((modeA >> (9-1)) & 0x2) | ((modeA >> (7-2)) & 0x4);
|
|
b = ((modeA >> 5) & 1) | ((modeA >> (3-1)) & 0x2) | ((modeA << (1)) & 0x4);
|
|
d = ((modeA >> 4) & 1) | ((modeA >> (2-1)) & 0x2) | ((modeA << (2)) & 0x4);
|
|
aircraft->m_squawk = a*1000 + b*100 + c*10 + d;
|
|
if (modeA & 0x40)
|
|
aircraft->m_squawkItem->setText(QString("%1 IDENT").arg(aircraft->m_squawk, 4, 10, QLatin1Char('0')));
|
|
else
|
|
aircraft->m_squawkItem->setText(QString("%1").arg(aircraft->m_squawk, 4, 10, QLatin1Char('0')));
|
|
}
|
|
else if (st == 2)
|
|
{
|
|
// TCAS/ACAS RA Broadcast
|
|
}
|
|
}
|
|
else if (tc == 29)
|
|
{
|
|
// Target state and status
|
|
}
|
|
else if (tc == 31)
|
|
{
|
|
// Aircraft operation status
|
|
}
|
|
|
|
// Update aircraft in map
|
|
if (aircraft->m_positionValid)
|
|
{
|
|
m_aircraftModel.aircraftUpdated(aircraft);
|
|
}
|
|
}
|
|
else if (df == 18)
|
|
{
|
|
// TIS-B
|
|
qDebug() << "TIS B message cf=" << ca << " icao: " << icao;
|
|
}
|
|
|
|
// Check to see if we need to emit a notification about this aircraft
|
|
checkDynamicNotification(aircraft);
|
|
}
|
|
|
|
void ADSBDemodGUI::checkStaticNotification(Aircraft *aircraft)
|
|
{
|
|
for (int i = 0; i < m_settings.m_notificationSettings.size(); i++)
|
|
{
|
|
QString match;
|
|
switch (m_settings.m_notificationSettings[i]->m_matchColumn)
|
|
{
|
|
case ADSB_COL_ICAO:
|
|
match = aircraft->m_icaoItem->data(Qt::DisplayRole).toString();
|
|
break;
|
|
case ADSB_COL_MODEL:
|
|
match = aircraft->m_modelItem->data(Qt::DisplayRole).toString();
|
|
break;
|
|
case ADSB_COL_REGISTRATION:
|
|
match = aircraft->m_registrationItem->data(Qt::DisplayRole).toString();
|
|
break;
|
|
case ADSB_COL_MANUFACTURER:
|
|
match = aircraft->m_manufacturerNameItem->data(Qt::DisplayRole).toString();
|
|
break;
|
|
case ADSB_COL_OWNER:
|
|
match = aircraft->m_ownerItem->data(Qt::DisplayRole).toString();
|
|
break;
|
|
case ADSB_COL_OPERATOR_ICAO:
|
|
match = aircraft->m_operatorICAOItem->data(Qt::DisplayRole).toString();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
if (!match.isEmpty())
|
|
{
|
|
if (m_settings.m_notificationSettings[i]->m_regularExpression.isValid())
|
|
{
|
|
if (m_settings.m_notificationSettings[i]->m_regularExpression.match(match).hasMatch())
|
|
{
|
|
highlightAircraft(aircraft);
|
|
if (!m_settings.m_notificationSettings[i]->m_speech.isEmpty()) {
|
|
speechNotification(aircraft, m_settings.m_notificationSettings[i]->m_speech);
|
|
}
|
|
if (!m_settings.m_notificationSettings[i]->m_command.isEmpty()) {
|
|
commandNotification(aircraft, m_settings.m_notificationSettings[i]->m_command);
|
|
}
|
|
aircraft->m_notified = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ADSBDemodGUI::checkDynamicNotification(Aircraft *aircraft)
|
|
{
|
|
if (!aircraft->m_notified)
|
|
{
|
|
for (int i = 0; i < m_settings.m_notificationSettings.size(); i++)
|
|
{
|
|
if ( (m_settings.m_notificationSettings[i]->m_matchColumn == ADSB_COL_CALLSIGN)
|
|
|| (m_settings.m_notificationSettings[i]->m_matchColumn == ADSB_COL_ALTITUDE)
|
|
|| (m_settings.m_notificationSettings[i]->m_matchColumn == ADSB_COL_SPEED)
|
|
|| (m_settings.m_notificationSettings[i]->m_matchColumn == ADSB_COL_RANGE)
|
|
|| (m_settings.m_notificationSettings[i]->m_matchColumn == ADSB_COL_CATEGORY)
|
|
|| (m_settings.m_notificationSettings[i]->m_matchColumn == ADSB_COL_STATUS)
|
|
|| (m_settings.m_notificationSettings[i]->m_matchColumn == ADSB_COL_SQUAWK)
|
|
)
|
|
{
|
|
QString match;
|
|
switch (m_settings.m_notificationSettings[i]->m_matchColumn)
|
|
{
|
|
case ADSB_COL_CALLSIGN:
|
|
match = aircraft->m_callsignItem->data(Qt::DisplayRole).toString();
|
|
break;
|
|
case ADSB_COL_ALTITUDE:
|
|
match = aircraft->m_altitudeItem->data(Qt::DisplayRole).toString();
|
|
break;
|
|
case ADSB_COL_SPEED:
|
|
match = aircraft->m_speedItem->data(Qt::DisplayRole).toString();
|
|
break;
|
|
case ADSB_COL_RANGE:
|
|
match = aircraft->m_rangeItem->data(Qt::DisplayRole).toString();
|
|
break;
|
|
case ADSB_COL_CATEGORY:
|
|
match = aircraft->m_emitterCategoryItem->data(Qt::DisplayRole).toString();
|
|
break;
|
|
case ADSB_COL_STATUS:
|
|
match = aircraft->m_statusItem->data(Qt::DisplayRole).toString();
|
|
break;
|
|
case ADSB_COL_SQUAWK:
|
|
match = aircraft->m_squawkItem->data(Qt::DisplayRole).toString();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
if (!match.isEmpty())
|
|
{
|
|
if (m_settings.m_notificationSettings[i]->m_regularExpression.isValid())
|
|
{
|
|
if (m_settings.m_notificationSettings[i]->m_regularExpression.match(match).hasMatch())
|
|
{
|
|
highlightAircraft(aircraft);
|
|
if (!m_settings.m_notificationSettings[i]->m_speech.isEmpty()) {
|
|
speechNotification(aircraft, m_settings.m_notificationSettings[i]->m_speech);
|
|
}
|
|
if (!m_settings.m_notificationSettings[i]->m_command.isEmpty()) {
|
|
commandNotification(aircraft, m_settings.m_notificationSettings[i]->m_command);
|
|
}
|
|
aircraft->m_notified = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ADSBDemodGUI::speechNotification(Aircraft *aircraft, const QString &speech)
|
|
{
|
|
m_speech->say(subAircraftString(aircraft, speech));
|
|
}
|
|
|
|
void ADSBDemodGUI::commandNotification(Aircraft *aircraft, const QString &command)
|
|
{
|
|
QProcess::startDetached(subAircraftString(aircraft, command));
|
|
}
|
|
|
|
QString ADSBDemodGUI::subAircraftString(Aircraft *aircraft, const QString &string)
|
|
{
|
|
QString s = string;
|
|
s = s.replace("${icao}", aircraft->m_icaoItem->data(Qt::DisplayRole).toString());
|
|
s = s.replace("${callsign}", aircraft->m_callsignItem->data(Qt::DisplayRole).toString());
|
|
s = s.replace("${aircraft}", aircraft->m_modelItem->data(Qt::DisplayRole).toString());
|
|
s = s.replace("${latitude}", aircraft->m_latitudeItem->data(Qt::DisplayRole).toString());
|
|
s = s.replace("${longitude}", aircraft->m_longitudeItem->data(Qt::DisplayRole).toString());
|
|
s = s.replace("${altitude}", aircraft->m_altitudeItem->data(Qt::DisplayRole).toString());
|
|
s = s.replace("${speed}", aircraft->m_speedItem->data(Qt::DisplayRole).toString());
|
|
s = s.replace("${heading}", aircraft->m_headingItem->data(Qt::DisplayRole).toString());
|
|
s = s.replace("${range}", aircraft->m_rangeItem->data(Qt::DisplayRole).toString());
|
|
s = s.replace("${category}", aircraft->m_emitterCategoryItem->data(Qt::DisplayRole).toString());
|
|
s = s.replace("${status}", aircraft->m_statusItem->data(Qt::DisplayRole).toString());
|
|
s = s.replace("${squawk}", aircraft->m_squawkItem->data(Qt::DisplayRole).toString());
|
|
s = s.replace("${registration}", aircraft->m_registrationItem->data(Qt::DisplayRole).toString());
|
|
s = s.replace("${manufacturer}", aircraft->m_manufacturerNameItem->data(Qt::DisplayRole).toString());
|
|
s = s.replace("${owner}", aircraft->m_ownerItem->data(Qt::DisplayRole).toString());
|
|
s = s.replace("${operator}", aircraft->m_operatorICAOItem->data(Qt::DisplayRole).toString());
|
|
s = s.replace("${flightstatus}", aircraft->m_flightStatusItem->data(Qt::DisplayRole).toString());
|
|
s = s.replace("${departure}", aircraft->m_depItem->data(Qt::DisplayRole).toString());
|
|
s = s.replace("${arrival}", aircraft->m_arrItem->data(Qt::DisplayRole).toString());
|
|
s = s.replace("${std}", aircraft->m_stdItem->data(Qt::DisplayRole).toString());
|
|
s = s.replace("${etd}", aircraft->m_etdItem->data(Qt::DisplayRole).toString());
|
|
s = s.replace("${atd}", aircraft->m_atdItem->data(Qt::DisplayRole).toString());
|
|
s = s.replace("${sta}", aircraft->m_staItem->data(Qt::DisplayRole).toString());
|
|
s = s.replace("${eta}", aircraft->m_etaItem->data(Qt::DisplayRole).toString());
|
|
s = s.replace("${ata}", aircraft->m_ataItem->data(Qt::DisplayRole).toString());
|
|
return s;
|
|
}
|
|
|
|
bool ADSBDemodGUI::handleMessage(const Message& message)
|
|
{
|
|
if (ADSBDemodReport::MsgReportADSB::match(message))
|
|
{
|
|
ADSBDemodReport::MsgReportADSB& report = (ADSBDemodReport::MsgReportADSB&) message;
|
|
handleADSB(
|
|
report.getData(),
|
|
report.getDateTime(),
|
|
report.getPreambleCorrelation(),
|
|
report.getCorrelationOnes());
|
|
return true;
|
|
}
|
|
else if (ADSBDemodReport::MsgReportDemodStats::match(message))
|
|
{
|
|
ADSBDemodReport::MsgReportDemodStats& report = (ADSBDemodReport::MsgReportDemodStats&) message;
|
|
if (m_settings.m_displayDemodStats)
|
|
{
|
|
ADSBDemodStats stats = report.getDemodStats();
|
|
ui->stats->setText(QString("ADS-B: %1 Mode-S: %2 Matches: %3 CRC: %4 Type: %5 Avg Corr: %6 Demod Time: %7 Feed Time: %8").arg(stats.m_adsbFrames).arg(stats.m_modesFrames).arg(stats.m_correlatorMatches).arg(stats.m_crcFails).arg(stats.m_typeFails).arg(CalcDb::dbPower(m_correlationAvg.instantAverage()), 1, 'f', 1).arg(stats.m_demodTime, 1, 'f', 3).arg(stats.m_feedTime, 1, 'f', 3));
|
|
}
|
|
return true;
|
|
}
|
|
else if (ADSBDemod::MsgConfigureADSBDemod::match(message))
|
|
{
|
|
qDebug("ADSBDemodGUI::handleMessage: ADSBDemod::MsgConfigureADSBDemod");
|
|
const ADSBDemod::MsgConfigureADSBDemod& cfg = (ADSBDemod::MsgConfigureADSBDemod&) message;
|
|
m_settings = cfg.getSettings();
|
|
blockApplySettings(true);
|
|
displaySettings();
|
|
blockApplySettings(false);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void ADSBDemodGUI::handleInputMessages()
|
|
{
|
|
Message* message;
|
|
|
|
while ((message = getInputMessageQueue()->pop()) != 0)
|
|
{
|
|
if (handleMessage(*message))
|
|
{
|
|
delete message;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ADSBDemodGUI::channelMarkerChangedByCursor()
|
|
{
|
|
ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency());
|
|
m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency();
|
|
applySettings();
|
|
}
|
|
|
|
void ADSBDemodGUI::channelMarkerHighlightedByCursor()
|
|
{
|
|
setHighlighted(m_channelMarker.getHighlighted());
|
|
}
|
|
|
|
void ADSBDemodGUI::on_deltaFrequency_changed(qint64 value)
|
|
{
|
|
m_channelMarker.setCenterFrequency(value);
|
|
m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency();
|
|
applySettings();
|
|
}
|
|
|
|
void ADSBDemodGUI::on_rfBW_valueChanged(int value)
|
|
{
|
|
Real bw = (Real)value;
|
|
ui->rfBWText->setText(QString("%1M").arg(bw / 1000000.0, 0, 'f', 1));
|
|
m_channelMarker.setBandwidth(bw);
|
|
m_settings.m_rfBandwidth = bw;
|
|
applySettings();
|
|
}
|
|
|
|
void ADSBDemodGUI::on_threshold_valueChanged(int value)
|
|
{
|
|
Real thresholddB = ((Real)value)/10.0f;
|
|
ui->thresholdText->setText(QString("%1").arg(thresholddB, 0, 'f', 1));
|
|
m_settings.m_correlationThreshold = thresholddB;
|
|
applySettings();
|
|
}
|
|
|
|
void ADSBDemodGUI::on_phaseSteps_valueChanged(int value)
|
|
{
|
|
ui->phaseStepsText->setText(QString("%1").arg(value));
|
|
m_settings.m_interpolatorPhaseSteps = value;
|
|
applySettings();
|
|
}
|
|
|
|
void ADSBDemodGUI::on_tapsPerPhase_valueChanged(int value)
|
|
{
|
|
Real tapsPerPhase = ((Real)value)/10.0f;
|
|
ui->tapsPerPhaseText->setText(QString("%1").arg(tapsPerPhase, 0, 'f', 1));
|
|
m_settings.m_interpolatorTapsPerPhase = tapsPerPhase;
|
|
applySettings();
|
|
}
|
|
|
|
void ADSBDemodGUI::on_feed_clicked(bool checked)
|
|
{
|
|
m_settings.m_feedEnabled = checked;
|
|
// Don't disable host/port - so they can be entered before connecting
|
|
applySettings();
|
|
}
|
|
|
|
void ADSBDemodGUI::on_notifications_clicked()
|
|
{
|
|
ADSBDemodNotificationDialog dialog(&m_settings);
|
|
if (dialog.exec() == QDialog::Accepted) {
|
|
applySettings();
|
|
}
|
|
}
|
|
|
|
void ADSBDemodGUI::on_flightInfo_clicked()
|
|
{
|
|
if (m_flightInformation)
|
|
{
|
|
// Selection mode is single, so only a single row should be returned
|
|
QModelIndexList indexList = ui->adsbData->selectionModel()->selectedRows();
|
|
if (!indexList.isEmpty())
|
|
{
|
|
int row = indexList.at(0).row();
|
|
int icao = ui->adsbData->item(row, 0)->text().toInt(nullptr, 16);
|
|
if (m_aircraft.contains(icao))
|
|
{
|
|
Aircraft *aircraft = m_aircraft.value(icao);
|
|
if (!aircraft->m_flight.isEmpty())
|
|
{
|
|
// Download flight information
|
|
m_flightInformation->getFlightInformation(aircraft->m_flight);
|
|
}
|
|
else
|
|
{
|
|
qDebug() << "ADSBDemodGUI::on_flightInfo_clicked - No flight number for selected aircraft";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
qDebug() << "ADSBDemodGUI::on_flightInfo_clicked - No aircraft with icao " << icao;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
qDebug() << "ADSBDemodGUI::on_flightInfo_clicked - No aircraft selected";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
qDebug() << "ADSBDemodGUI::on_flightInfo_clicked - No flight information service - have you set an API key?";
|
|
}
|
|
}
|
|
|
|
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))
|
|
highlightAircraft(m_aircraft.value(icao));
|
|
}
|
|
|
|
void ADSBDemodGUI::on_adsbData_cellDoubleClicked(int row, int column)
|
|
{
|
|
// Get ICAO of aircraft in row double clicked
|
|
int icao = ui->adsbData->item(row, 0)->text().toInt(nullptr, 16);
|
|
if (column == ADSB_COL_ICAO)
|
|
{
|
|
// Search for aircraft on planespotters.net
|
|
QString icaoUpper = QString("%1").arg(icao, 1, 16).toUpper();
|
|
QDesktopServices::openUrl(QUrl(QString("https://www.planespotters.net/hex/%1").arg(icaoUpper)));
|
|
}
|
|
else if (m_aircraft.contains(icao))
|
|
{
|
|
Aircraft *aircraft = m_aircraft.value(icao);
|
|
|
|
if (column == ADSB_COL_CALLSIGN)
|
|
{
|
|
if (!aircraft->m_callsign.isEmpty())
|
|
{
|
|
// Search for flight on flightradar24
|
|
QDesktopServices::openUrl(QUrl(QString("https://www.flightradar24.com/%1").arg(aircraft->m_callsign)));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (column == ADSB_COL_AZEL)
|
|
{
|
|
targetAircraft(aircraft);
|
|
}
|
|
// Center map view on aircraft if it has a valid position
|
|
if (aircraft->m_positionValid)
|
|
{
|
|
QQuickItem *item = ui->map->rootObject();
|
|
QObject *object = item->findChild<QObject*>("map");
|
|
if(object != NULL)
|
|
{
|
|
QGeoCoordinate geocoord = object->property("center").value<QGeoCoordinate>();
|
|
geocoord.setLatitude(aircraft->m_latitude);
|
|
geocoord.setLongitude(aircraft->m_longitude);
|
|
object->setProperty("center", QVariant::fromValue(geocoord));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Columns in table reordered
|
|
void ADSBDemodGUI::adsbData_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex)
|
|
{
|
|
(void) oldVisualIndex;
|
|
m_settings.m_columnIndexes[logicalIndex] = newVisualIndex;
|
|
}
|
|
|
|
// Column in table resized (when hidden size is 0)
|
|
void ADSBDemodGUI::adsbData_sectionResized(int logicalIndex, int oldSize, int newSize)
|
|
{
|
|
(void) oldSize;
|
|
m_settings.m_columnSizes[logicalIndex] = newSize;
|
|
}
|
|
|
|
// Right click in ADSB table header - show column select menu
|
|
void ADSBDemodGUI::columnSelectMenu(QPoint pos)
|
|
{
|
|
menu->popup(ui->adsbData->horizontalHeader()->viewport()->mapToGlobal(pos));
|
|
}
|
|
|
|
// Hide/show column when menu selected
|
|
void ADSBDemodGUI::columnSelectMenuChecked(bool checked)
|
|
{
|
|
(void) checked;
|
|
QAction* action = qobject_cast<QAction*>(sender());
|
|
if (action != nullptr)
|
|
{
|
|
int idx = action->data().toInt(nullptr);
|
|
ui->adsbData->setColumnHidden(idx, !action->isChecked());
|
|
}
|
|
}
|
|
|
|
// Create column select menu item
|
|
QAction *ADSBDemodGUI::createCheckableItem(QString &text, int idx, bool checked)
|
|
{
|
|
QAction *action = new QAction(text, this);
|
|
action->setCheckable(true);
|
|
action->setChecked(checked);
|
|
action->setData(QVariant(idx));
|
|
connect(action, SIGNAL(triggered()), this, SLOT(columnSelectMenuChecked()));
|
|
return action;
|
|
}
|
|
|
|
void ADSBDemodGUI::on_spb_currentIndexChanged(int value)
|
|
{
|
|
m_settings.m_samplesPerBit = (value + 1) * 2;
|
|
applySettings();
|
|
}
|
|
|
|
void ADSBDemodGUI::on_correlateFullPreamble_clicked(bool checked)
|
|
{
|
|
m_settings.m_correlateFullPreamble = checked;
|
|
applySettings();
|
|
}
|
|
|
|
void ADSBDemodGUI::on_demodModeS_clicked(bool checked)
|
|
{
|
|
m_settings.m_demodModeS = checked;
|
|
applySettings();
|
|
}
|
|
|
|
void ADSBDemodGUI::on_getOSNDB_clicked()
|
|
{
|
|
// Don't try to download while already in progress
|
|
if (m_progressDialog == nullptr)
|
|
{
|
|
QString osnDBFilename = getOSNDBFilename();
|
|
if (confirmDownload(osnDBFilename))
|
|
{
|
|
// Download Opensky network database to a file
|
|
QUrl dbURL(QString(OSNDB_URL));
|
|
m_progressDialog = new QProgressDialog(this);
|
|
m_progressDialog->setAttribute(Qt::WA_DeleteOnClose);
|
|
m_progressDialog->setCancelButton(nullptr);
|
|
m_progressDialog->setLabelText(QString("Downloading %1.").arg(OSNDB_URL));
|
|
QNetworkReply *reply = m_dlm.download(dbURL, osnDBFilename);
|
|
connect(reply, SIGNAL(downloadProgress(qint64,qint64)), this, SLOT(updateDownloadProgress(qint64,qint64)));
|
|
}
|
|
}
|
|
}
|
|
|
|
void ADSBDemodGUI::on_getAirportDB_clicked()
|
|
{
|
|
// Don't try to download while already in progress
|
|
if (m_progressDialog == nullptr)
|
|
{
|
|
QString airportDBFile = getAirportDBFilename();
|
|
if (confirmDownload(airportDBFile))
|
|
{
|
|
// Download Opensky network database to a file
|
|
QUrl dbURL(QString(AIRPORTS_URL));
|
|
m_progressDialog = new QProgressDialog(this);
|
|
m_progressDialog->setAttribute(Qt::WA_DeleteOnClose);
|
|
m_progressDialog->setCancelButton(nullptr);
|
|
m_progressDialog->setLabelText(QString("Downloading %1.").arg(AIRPORTS_URL));
|
|
QNetworkReply *reply = m_dlm.download(dbURL, airportDBFile);
|
|
connect(reply, SIGNAL(downloadProgress(qint64,qint64)), this, SLOT(updateDownloadProgress(qint64,qint64)));
|
|
}
|
|
}
|
|
}
|
|
|
|
void ADSBDemodGUI::on_flightPaths_clicked(bool checked)
|
|
{
|
|
m_settings.m_flightPaths = checked;
|
|
m_aircraftModel.setFlightPaths(checked);
|
|
}
|
|
|
|
void ADSBDemodGUI::on_allFlightPaths_clicked(bool checked)
|
|
{
|
|
m_settings.m_allFlightPaths = checked;
|
|
m_aircraftModel.setAllFlightPaths(checked);
|
|
}
|
|
|
|
QString ADSBDemodGUI::getDataDir()
|
|
{
|
|
// Get directory to store app data in (aircraft & airport databases and user-definable icons)
|
|
QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
|
|
// First dir is writable
|
|
return locations[0];
|
|
}
|
|
|
|
QString ADSBDemodGUI::getAirportDBFilename()
|
|
{
|
|
return getDataDir() + "/airportDatabase.csv";
|
|
}
|
|
|
|
QString ADSBDemodGUI::getAirportFrequenciesDBFilename()
|
|
{
|
|
return getDataDir() + "/airportFrequenciesDatabase.csv";
|
|
}
|
|
|
|
QString ADSBDemodGUI::getOSNDBFilename()
|
|
{
|
|
return getDataDir() + "/aircraftDatabase.csv";
|
|
}
|
|
|
|
QString ADSBDemodGUI::getFastDBFilename()
|
|
{
|
|
return getDataDir() + "/aircraftDatabaseFast.csv";
|
|
}
|
|
|
|
qint64 ADSBDemodGUI::fileAgeInDays(QString filename)
|
|
{
|
|
QFile file(filename);
|
|
if (file.exists())
|
|
{
|
|
QDateTime modified = file.fileTime(QFileDevice::FileModificationTime);
|
|
if (modified.isValid())
|
|
return modified.daysTo(QDateTime::currentDateTime());
|
|
else
|
|
return -1;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
bool ADSBDemodGUI::confirmDownload(QString filename)
|
|
{
|
|
qint64 age = fileAgeInDays(filename);
|
|
if ((age == -1) || (age > 100))
|
|
return true;
|
|
else
|
|
{
|
|
QMessageBox::StandardButton reply;
|
|
if (age == 0)
|
|
reply = QMessageBox::question(this, "Confirm download", "This file was last downloaded today. Are you sure you wish to redownload it?", QMessageBox::Yes|QMessageBox::No);
|
|
else if (age == 1)
|
|
reply = QMessageBox::question(this, "Confirm download", "This file was last downloaded yesterday. Are you sure you wish to redownload it?", QMessageBox::Yes|QMessageBox::No);
|
|
else
|
|
reply = QMessageBox::question(this, "Confirm download", QString("This file was last downloaded %1 days ago. Are you sure you wish to redownload this file?").arg(age), QMessageBox::Yes|QMessageBox::No);
|
|
return reply == QMessageBox::Yes;
|
|
}
|
|
}
|
|
|
|
bool ADSBDemodGUI::readOSNDB(const QString& filename)
|
|
{
|
|
m_aircraftInfo = AircraftInformation::readOSNDB(filename);
|
|
return m_aircraftInfo != nullptr;
|
|
}
|
|
|
|
bool ADSBDemodGUI::readFastDB(const QString& filename)
|
|
{
|
|
m_aircraftInfo = AircraftInformation::readFastDB(filename);
|
|
return m_aircraftInfo != nullptr;
|
|
}
|
|
|
|
void ADSBDemodGUI::updateDownloadProgress(qint64 bytesRead, qint64 totalBytes)
|
|
{
|
|
m_progressDialog->setMaximum(totalBytes);
|
|
m_progressDialog->setValue(bytesRead);
|
|
}
|
|
|
|
void ADSBDemodGUI::downloadFinished(const QString& filename, bool success)
|
|
{
|
|
if (success)
|
|
{
|
|
if (filename == getOSNDBFilename())
|
|
{
|
|
readOSNDB(filename);
|
|
// Convert to condensed format for faster loading later
|
|
m_progressDialog->setLabelText("Processing.");
|
|
AircraftInformation::writeFastDB(getFastDBFilename(), m_aircraftInfo);
|
|
m_progressDialog->close();
|
|
m_progressDialog = nullptr;
|
|
}
|
|
else if (filename == getAirportDBFilename())
|
|
{
|
|
m_airportInfo = AirportInformation::readAirportsDB(filename);
|
|
// Now download airport frequencies
|
|
QUrl dbURL(QString(AIRPORT_FREQUENCIES_URL));
|
|
m_progressDialog->setLabelText(QString("Downloading %1.").arg(AIRPORT_FREQUENCIES_URL));
|
|
QNetworkReply *reply = m_dlm.download(dbURL, getAirportFrequenciesDBFilename());
|
|
connect(reply, SIGNAL(downloadProgress(qint64,qint64)), this, SLOT(updateDownloadProgress(qint64,qint64)));
|
|
}
|
|
else if (filename == getAirportFrequenciesDBFilename())
|
|
{
|
|
if (m_airportInfo != nullptr)
|
|
{
|
|
AirportInformation::readFrequenciesDB(filename, m_airportInfo);
|
|
// Update airports on map
|
|
updateAirports();
|
|
}
|
|
m_progressDialog->close();
|
|
m_progressDialog = nullptr;
|
|
}
|
|
else
|
|
{
|
|
qDebug() << "ADSBDemodGUI::downloadFinished: Unexpected filename: " << filename;
|
|
m_progressDialog->close();
|
|
m_progressDialog = nullptr;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_progressDialog->close();
|
|
m_progressDialog = nullptr;
|
|
}
|
|
}
|
|
|
|
void ADSBDemodGUI::onWidgetRolled(QWidget* widget, bool rollDown)
|
|
{
|
|
(void) widget;
|
|
(void) rollDown;
|
|
}
|
|
|
|
void ADSBDemodGUI::onMenuDialogCalled(const QPoint &p)
|
|
{
|
|
if (m_contextMenuType == ContextMenuChannelSettings)
|
|
{
|
|
BasicChannelSettingsDialog dialog(&m_channelMarker, this);
|
|
dialog.setUseReverseAPI(m_settings.m_useReverseAPI);
|
|
dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress);
|
|
dialog.setReverseAPIPort(m_settings.m_reverseAPIPort);
|
|
dialog.setReverseAPIDeviceIndex(m_settings.m_reverseAPIDeviceIndex);
|
|
dialog.setReverseAPIChannelIndex(m_settings.m_reverseAPIChannelIndex);
|
|
dialog.move(p);
|
|
dialog.exec();
|
|
|
|
m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency();
|
|
m_settings.m_rgbColor = m_channelMarker.getColor().rgb();
|
|
m_settings.m_title = m_channelMarker.getTitle();
|
|
m_settings.m_useReverseAPI = dialog.useReverseAPI();
|
|
m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress();
|
|
m_settings.m_reverseAPIPort = dialog.getReverseAPIPort();
|
|
m_settings.m_reverseAPIDeviceIndex = dialog.getReverseAPIDeviceIndex();
|
|
m_settings.m_reverseAPIChannelIndex = dialog.getReverseAPIChannelIndex();
|
|
|
|
setWindowTitle(m_settings.m_title);
|
|
setTitleColor(m_settings.m_rgbColor);
|
|
|
|
applySettings();
|
|
}
|
|
else if ((m_contextMenuType == ContextMenuStreamSettings) && (m_deviceUISet->m_deviceMIMOEngine))
|
|
{
|
|
DeviceStreamSelectionDialog dialog(this);
|
|
dialog.setNumberOfStreams(m_adsbDemod->getNumberOfDeviceStreams());
|
|
dialog.setStreamIndex(m_settings.m_streamIndex);
|
|
dialog.move(p);
|
|
dialog.exec();
|
|
|
|
m_settings.m_streamIndex = dialog.getSelectedStreamIndex();
|
|
m_channelMarker.clearStreamIndexes();
|
|
m_channelMarker.addStreamIndex(m_settings.m_streamIndex);
|
|
displayStreamIndex();
|
|
applySettings();
|
|
}
|
|
|
|
resetContextMenuType();
|
|
}
|
|
|
|
void ADSBDemodGUI::updateDeviceSetList()
|
|
{
|
|
MainWindow *mainWindow = MainWindow::getInstance();
|
|
std::vector<DeviceUISet*>& deviceUISets = mainWindow->getDeviceUISets();
|
|
std::vector<DeviceUISet*>::const_iterator it = deviceUISets.begin();
|
|
|
|
ui->device->blockSignals(true);
|
|
|
|
ui->device->clear();
|
|
unsigned int deviceIndex = 0;
|
|
|
|
for (; it != deviceUISets.end(); ++it, deviceIndex++)
|
|
{
|
|
DSPDeviceSourceEngine *deviceSourceEngine = (*it)->m_deviceSourceEngine;
|
|
|
|
if (deviceSourceEngine) {
|
|
ui->device->addItem(QString("R%1").arg(deviceIndex), deviceIndex);
|
|
}
|
|
}
|
|
|
|
int newDeviceIndex;
|
|
|
|
if (it != deviceUISets.begin())
|
|
{
|
|
if (m_settings.m_deviceIndex < 0) {
|
|
ui->device->setCurrentIndex(0);
|
|
} else {
|
|
ui->device->setCurrentIndex(m_settings.m_deviceIndex);
|
|
}
|
|
|
|
newDeviceIndex = ui->device->currentData().toInt();
|
|
}
|
|
else
|
|
{
|
|
newDeviceIndex = -1;
|
|
}
|
|
|
|
if (newDeviceIndex != m_settings.m_deviceIndex)
|
|
{
|
|
qDebug("ADSBDemodGUI::updateDeviceSetLists: device index changed: %d", newDeviceIndex);
|
|
m_settings.m_deviceIndex = newDeviceIndex;
|
|
}
|
|
|
|
ui->device->blockSignals(false);
|
|
}
|
|
|
|
void ADSBDemodGUI::on_devicesRefresh_clicked()
|
|
{
|
|
updateDeviceSetList();
|
|
displaySettings();
|
|
applySettings();
|
|
}
|
|
|
|
void ADSBDemodGUI::on_device_currentIndexChanged(int index)
|
|
{
|
|
if (index >= 0)
|
|
{
|
|
m_settings.m_deviceIndex = ui->device->currentData().toInt();
|
|
applySettings();
|
|
}
|
|
}
|
|
|
|
void ADSBDemodGUI::updateAirports()
|
|
{
|
|
m_airportModel.removeAllAirports();
|
|
QHash<int, AirportInformation *>::iterator i = m_airportInfo->begin();
|
|
AzEl azEl = m_azEl;
|
|
|
|
while (i != m_airportInfo->end())
|
|
{
|
|
AirportInformation *airportInfo = i.value();
|
|
|
|
// Calculate distance and az/el to airport from My Position
|
|
azEl.setTarget(airportInfo->m_latitude, airportInfo->m_longitude, Units::feetToMetres(airportInfo->m_elevation));
|
|
azEl.calculate();
|
|
|
|
// Only display airport if in range
|
|
if (azEl.getDistance() <= m_settings.m_airportRange*1000.0f)
|
|
{
|
|
// Only display the airport if it's large enough
|
|
if (airportInfo->m_type >= m_settings.m_airportMinimumSize)
|
|
{
|
|
// Only display heliports if enabled
|
|
if (m_settings.m_displayHeliports || (airportInfo->m_type != ADSBDemodSettings::AirportType::Heliport))
|
|
{
|
|
m_airportModel.addAirport(airportInfo, azEl.getAzimuth(), azEl.getElevation(), azEl.getDistance());
|
|
}
|
|
}
|
|
}
|
|
++i;
|
|
}
|
|
// Save settings we've just used so we know if they've changed
|
|
m_currentAirportRange = m_currentAirportRange;
|
|
m_currentAirportMinimumSize = m_settings.m_airportMinimumSize;
|
|
m_currentDisplayHeliports = m_settings.m_displayHeliports;
|
|
}
|
|
|
|
// Set a static target, such as an airport
|
|
void ADSBDemodGUI::target(const QString& name, float az, float el)
|
|
{
|
|
if (m_trackAircraft)
|
|
{
|
|
// Restore colour of current target
|
|
m_trackAircraft->m_isTarget = false;
|
|
m_aircraftModel.aircraftUpdated(m_trackAircraft);
|
|
m_trackAircraft = nullptr;
|
|
}
|
|
m_adsbDemod->setTarget(name, az, el);
|
|
}
|
|
|
|
void ADSBDemodGUI::targetAircraft(Aircraft *aircraft)
|
|
{
|
|
if (aircraft != m_trackAircraft)
|
|
{
|
|
if (m_trackAircraft)
|
|
{
|
|
// Restore colour of current target
|
|
m_trackAircraft->m_isTarget = false;
|
|
m_aircraftModel.aircraftUpdated(m_trackAircraft);
|
|
}
|
|
// Track this aircraft
|
|
m_trackAircraft = aircraft;
|
|
if (aircraft->m_positionValid)
|
|
m_adsbDemod->setTarget(aircraft->targetName(), aircraft->m_azimuth, aircraft->m_elevation);
|
|
// Change colour of new target
|
|
aircraft->m_isTarget = true;
|
|
m_aircraftModel.aircraftUpdated(aircraft);
|
|
}
|
|
}
|
|
|
|
void ADSBDemodGUI::highlightAircraft(Aircraft *aircraft)
|
|
{
|
|
if (aircraft != m_highlightAircraft)
|
|
{
|
|
if (m_highlightAircraft)
|
|
{
|
|
// Restore colour
|
|
m_highlightAircraft->m_isHighlighted = false;
|
|
m_aircraftModel.aircraftUpdated(m_highlightAircraft);
|
|
}
|
|
// Highlight this aircraft
|
|
m_highlightAircraft = aircraft;
|
|
aircraft->m_isHighlighted = true;
|
|
m_aircraftModel.aircraftUpdated(aircraft);
|
|
}
|
|
// Highlight the row in the table - always do this, as it can become
|
|
// unselected
|
|
ui->adsbData->selectRow(aircraft->m_icaoItem->row());
|
|
}
|
|
|
|
// Show feed dialog
|
|
void ADSBDemodGUI::feedSelect()
|
|
{
|
|
ADSBDemodFeedDialog dialog(m_settings.m_feedHost, m_settings.m_feedPort, m_settings.m_feedFormat);
|
|
if (dialog.exec() == QDialog::Accepted)
|
|
{
|
|
m_settings.m_feedHost = dialog.m_feedHost;
|
|
m_settings.m_feedPort = dialog.m_feedPort;
|
|
m_settings.m_feedFormat = dialog.m_feedFormat;
|
|
applySettings();
|
|
}
|
|
}
|
|
|
|
// Show display settings dialog
|
|
void ADSBDemodGUI::on_displaySettings_clicked()
|
|
{
|
|
ADSBDemodDisplayDialog dialog(m_settings.m_removeTimeout, m_settings.m_airportRange, m_settings.m_airportMinimumSize,
|
|
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);
|
|
if (dialog.exec() == QDialog::Accepted)
|
|
{
|
|
bool unitsChanged = m_settings.m_siUnits != dialog.m_siUnits;
|
|
|
|
m_settings.m_removeTimeout = dialog.m_removeTimeout;
|
|
m_settings.m_airportRange = dialog.m_airportRange;
|
|
m_settings.m_airportMinimumSize = dialog.m_airportMinimumSize;
|
|
m_settings.m_displayHeliports = dialog.m_displayHeliports;
|
|
m_settings.m_siUnits = dialog.m_siUnits;
|
|
m_settings.m_tableFontName = dialog.m_fontName;
|
|
m_settings.m_tableFontSize = dialog.m_fontSize;
|
|
m_settings.m_displayDemodStats = dialog.m_displayDemodStats;
|
|
m_settings.m_autoResizeTableColumns = dialog.m_autoResizeTableColumns;
|
|
m_settings.m_apiKey = dialog.m_apiKey;
|
|
|
|
if (unitsChanged)
|
|
m_aircraftModel.allAircraftUpdated();
|
|
displaySettings();
|
|
applySettings();
|
|
}
|
|
}
|
|
|
|
ADSBDemodGUI::ADSBDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent) :
|
|
ChannelGUI(parent),
|
|
ui(new Ui::ADSBDemodGUI),
|
|
m_pluginAPI(pluginAPI),
|
|
m_deviceUISet(deviceUISet),
|
|
m_channelMarker(this),
|
|
m_basicSettingsShown(false),
|
|
m_doApplySettings(true),
|
|
m_tickCount(0),
|
|
m_aircraftInfo(nullptr),
|
|
m_airportModel(this),
|
|
m_trackAircraft(nullptr),
|
|
m_highlightAircraft(nullptr),
|
|
m_progressDialog(nullptr)
|
|
{
|
|
ui->setupUi(this);
|
|
|
|
ui->map->rootContext()->setContextProperty("aircraftModel", &m_aircraftModel);
|
|
ui->map->rootContext()->setContextProperty("airportModel", &m_airportModel);
|
|
ui->map->setSource(QUrl(QStringLiteral("qrc:/map/map.qml")));
|
|
|
|
setAttribute(Qt::WA_DeleteOnClose, true);
|
|
|
|
connect(this, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool)));
|
|
connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &)));
|
|
connect(&m_dlm, &HttpDownloadManager::downloadComplete, this, &ADSBDemodGUI::downloadFinished);
|
|
|
|
m_adsbDemod = reinterpret_cast<ADSBDemod*>(rxChannel); //new ADSBDemod(m_deviceUISet->m_deviceSourceAPI);
|
|
m_adsbDemod->setMessageQueueToGUI(getInputMessageQueue());
|
|
|
|
connect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick()));
|
|
|
|
CRightClickEnabler *feedRightClickEnabler = new CRightClickEnabler(ui->feed);
|
|
connect(feedRightClickEnabler, SIGNAL(rightClick(const QPoint &)), this, SLOT(feedSelect()));
|
|
|
|
ui->channelPowerMeter->setColorTheme(LevelMeterSignalDB::ColorGreenAndBlue);
|
|
|
|
ui->deltaFrequencyLabel->setText(QString("%1f").arg(QChar(0x94, 0x03)));
|
|
ui->deltaFrequency->setColorMapper(ColorMapper(ColorMapper::GrayGold));
|
|
ui->deltaFrequency->setValueRange(false, 7, -9999999, 9999999);
|
|
|
|
m_channelMarker.blockSignals(true);
|
|
m_channelMarker.setColor(Qt::red);
|
|
m_channelMarker.setBandwidth(5000);
|
|
m_channelMarker.setCenterFrequency(0);
|
|
m_channelMarker.setTitle("ADS-B Demodulator");
|
|
m_channelMarker.blockSignals(false);
|
|
m_channelMarker.setVisible(true); // activate signal on the last setting only
|
|
|
|
m_settings.setChannelMarker(&m_channelMarker);
|
|
|
|
m_deviceUISet->addChannelMarker(&m_channelMarker);
|
|
m_deviceUISet->addRollupWidget(this);
|
|
|
|
connect(&m_channelMarker, SIGNAL(changedByCursor()), this, SLOT(channelMarkerChangedByCursor()));
|
|
connect(&m_channelMarker, SIGNAL(highlightedByCursor()), this, SLOT(channelMarkerHighlightedByCursor()));
|
|
|
|
connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()));
|
|
|
|
// Set size of airline icons
|
|
ui->adsbData->setIconSize(QSize(85, 20));
|
|
// Resize the table using dummy data
|
|
resizeTable();
|
|
// Allow user to reorder columns
|
|
ui->adsbData->horizontalHeader()->setSectionsMovable(true);
|
|
// Allow user to sort table by clicking on headers
|
|
ui->adsbData->setSortingEnabled(true);
|
|
// Add context menu to allow hiding/showing of columns
|
|
menu = new QMenu(ui->adsbData);
|
|
for (int i = 0; i < ui->adsbData->horizontalHeader()->count(); i++)
|
|
{
|
|
QString text = ui->adsbData->horizontalHeaderItem(i)->text();
|
|
menu->addAction(createCheckableItem(text, i, true));
|
|
}
|
|
ui->adsbData->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
connect(ui->adsbData->horizontalHeader(), SIGNAL(customContextMenuRequested(QPoint)), SLOT(columnSelectMenu(QPoint)));
|
|
// Get signals when columns change
|
|
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)));
|
|
|
|
// Read aircraft information database, if it has previously been downloaded
|
|
if (!readFastDB(getFastDBFilename()))
|
|
{
|
|
if (readOSNDB(getOSNDBFilename()))
|
|
AircraftInformation::writeFastDB(getFastDBFilename(), m_aircraftInfo);
|
|
}
|
|
// Read airport information database, if it has previously been downloaded
|
|
m_airportInfo = AirportInformation::readAirportsDB(getAirportDBFilename());
|
|
if (m_airportInfo != nullptr)
|
|
AirportInformation::readFrequenciesDB(getAirportFrequenciesDBFilename(), m_airportInfo);
|
|
// Read registration prefix to country map
|
|
m_prefixMap = csvHash(":/flags/regprefixmap.csv");
|
|
// Read operator air force to military map
|
|
m_militaryMap = csvHash(":/flags/militarymap.csv");
|
|
|
|
// Get station position
|
|
Real stationLatitude = MainCore::instance()->getSettings().getLatitude();
|
|
Real stationLongitude = MainCore::instance()->getSettings().getLongitude();
|
|
Real stationAltitude = MainCore::instance()->getSettings().getAltitude();
|
|
m_azEl.setLocation(stationLatitude, stationLongitude, stationAltitude);
|
|
|
|
// Centre map at My Position
|
|
QQuickItem *item = ui->map->rootObject();
|
|
QObject *object = item->findChild<QObject*>("map");
|
|
if(object != NULL)
|
|
{
|
|
QGeoCoordinate coords = object->property("center").value<QGeoCoordinate>();
|
|
coords.setLatitude(stationLatitude);
|
|
coords.setLongitude(stationLongitude);
|
|
object->setProperty("center", QVariant::fromValue(coords));
|
|
}
|
|
// Move antenna icon to My Position
|
|
QObject *stationObject = item->findChild<QObject*>("station");
|
|
if(stationObject != NULL)
|
|
{
|
|
QGeoCoordinate coords = stationObject->property("coordinate").value<QGeoCoordinate>();
|
|
coords.setLatitude(stationLatitude);
|
|
coords.setLongitude(stationLongitude);
|
|
coords.setAltitude(stationAltitude);
|
|
stationObject->setProperty("coordinate", QVariant::fromValue(coords));
|
|
stationObject->setProperty("stationName", QVariant::fromValue(MainCore::instance()->getSettings().getStationName()));
|
|
}
|
|
// Add airports within range of My Position
|
|
if (m_airportInfo != nullptr)
|
|
updateAirports();
|
|
|
|
// Initialise text to speech engine
|
|
m_speech = new QTextToSpeech(this);
|
|
|
|
m_flightInformation = nullptr;
|
|
|
|
updateDeviceSetList();
|
|
displaySettings();
|
|
applySettings(true);
|
|
}
|
|
|
|
ADSBDemodGUI::~ADSBDemodGUI()
|
|
{
|
|
delete ui;
|
|
QHash<int,Aircraft *>::iterator i = m_aircraft.begin();
|
|
while (i != m_aircraft.end())
|
|
{
|
|
Aircraft *a = i.value();
|
|
delete a;
|
|
++i;
|
|
}
|
|
if (m_flightInformation)
|
|
{
|
|
disconnect(m_flightInformation, &FlightInformation::flightUpdated, this, &ADSBDemodGUI::flightInformationUpdated);
|
|
delete m_flightInformation;
|
|
}
|
|
}
|
|
|
|
void ADSBDemodGUI::applySettings(bool force)
|
|
{
|
|
if (m_doApplySettings)
|
|
{
|
|
qDebug() << "ADSBDemodGUI::applySettings";
|
|
|
|
ADSBDemod::MsgConfigureADSBDemod* message = ADSBDemod::MsgConfigureADSBDemod::create(m_settings, force);
|
|
m_adsbDemod->getInputMessageQueue()->push(message);
|
|
}
|
|
}
|
|
|
|
void ADSBDemodGUI::displaySettings()
|
|
{
|
|
m_channelMarker.blockSignals(true);
|
|
m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset);
|
|
m_channelMarker.setBandwidth(m_settings.m_rfBandwidth);
|
|
m_channelMarker.setTitle(m_settings.m_title);
|
|
m_channelMarker.blockSignals(false);
|
|
m_channelMarker.setColor(m_settings.m_rgbColor);
|
|
|
|
setTitleColor(m_settings.m_rgbColor);
|
|
setWindowTitle(m_channelMarker.getTitle());
|
|
|
|
blockApplySettings(true);
|
|
|
|
ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency());
|
|
|
|
ui->rfBWText->setText(QString("%1M").arg(m_settings.m_rfBandwidth / 1000000.0, 0, 'f', 1));
|
|
ui->rfBW->setValue((int)m_settings.m_rfBandwidth);
|
|
|
|
ui->spb->setCurrentIndex(m_settings.m_samplesPerBit/2-1);
|
|
ui->correlateFullPreamble->setChecked(m_settings.m_correlateFullPreamble);
|
|
ui->demodModeS->setChecked(m_settings.m_demodModeS);
|
|
|
|
ui->thresholdText->setText(QString("%1").arg(m_settings.m_correlationThreshold, 0, 'f', 1));
|
|
ui->threshold->setValue((int)(m_settings.m_correlationThreshold*10.0f));
|
|
|
|
ui->phaseStepsText->setText(QString("%1").arg(m_settings.m_interpolatorPhaseSteps));
|
|
ui->phaseSteps->setValue(m_settings.m_interpolatorPhaseSteps);
|
|
ui->tapsPerPhaseText->setText(QString("%1").arg(m_settings.m_interpolatorTapsPerPhase, 0, 'f', 1));
|
|
ui->tapsPerPhase->setValue((int)(m_settings.m_interpolatorTapsPerPhase*10.0f));
|
|
// Enable these controls only for developers
|
|
if (1)
|
|
{
|
|
ui->phaseStepsText->setVisible(false);
|
|
ui->phaseSteps->setVisible(false);
|
|
ui->tapsPerPhaseText->setVisible(false);
|
|
ui->tapsPerPhase->setVisible(false);
|
|
}
|
|
ui->feed->setChecked(m_settings.m_feedEnabled);
|
|
|
|
ui->flightPaths->setChecked(m_settings.m_flightPaths);
|
|
m_aircraftModel.setFlightPaths(m_settings.m_flightPaths);
|
|
ui->allFlightPaths->setChecked(m_settings.m_allFlightPaths);
|
|
m_aircraftModel.setAllFlightPaths(m_settings.m_allFlightPaths);
|
|
|
|
displayStreamIndex();
|
|
|
|
QFont font(m_settings.m_tableFontName, m_settings.m_tableFontSize);
|
|
ui->adsbData->setFont(font);
|
|
|
|
// Set units in column headers
|
|
if (m_settings.m_siUnits)
|
|
{
|
|
ui->adsbData->horizontalHeaderItem(ADSB_COL_ALTITUDE)->setText("Alt (m)");
|
|
ui->adsbData->horizontalHeaderItem(ADSB_COL_SPEED)->setText("Spd (kph)");
|
|
ui->adsbData->horizontalHeaderItem(ADSB_COL_VERTICALRATE)->setText("VR (m/s)");
|
|
}
|
|
else
|
|
{
|
|
ui->adsbData->horizontalHeaderItem(ADSB_COL_ALTITUDE)->setText("Alt (ft)");
|
|
ui->adsbData->horizontalHeaderItem(ADSB_COL_SPEED)->setText("Spd (kn)");
|
|
ui->adsbData->horizontalHeaderItem(ADSB_COL_VERTICALRATE)->setText("VR (ft/m)");
|
|
}
|
|
|
|
// Order and size columns
|
|
QHeaderView *header = ui->adsbData->horizontalHeader();
|
|
for (int i = 0; i < ADSBDEMOD_COLUMNS; i++)
|
|
{
|
|
bool hidden = m_settings.m_columnSizes[i] == 0;
|
|
header->setSectionHidden(i, hidden);
|
|
menu->actions().at(i)->setChecked(!hidden);
|
|
if (m_settings.m_columnSizes[i] > 0)
|
|
ui->adsbData->setColumnWidth(i, m_settings.m_columnSizes[i]);
|
|
header->moveSection(header->visualIndex(i), m_settings.m_columnIndexes[i]);
|
|
}
|
|
|
|
// Only update airports on map if settings have changed
|
|
if ((m_airportInfo != nullptr)
|
|
&& ((m_settings.m_airportRange != m_currentAirportRange)
|
|
|| (m_settings.m_airportMinimumSize != m_currentAirportMinimumSize)
|
|
|| (m_settings.m_displayHeliports != m_currentDisplayHeliports)))
|
|
updateAirports();
|
|
|
|
if (!m_settings.m_displayDemodStats)
|
|
ui->stats->setText("");
|
|
|
|
initFlightInformation();
|
|
|
|
blockApplySettings(false);
|
|
}
|
|
|
|
void ADSBDemodGUI::displayStreamIndex()
|
|
{
|
|
if (m_deviceUISet->m_deviceMIMOEngine) {
|
|
setStreamIndicator(tr("%1").arg(m_settings.m_streamIndex));
|
|
} else {
|
|
setStreamIndicator("S"); // single channel indicator
|
|
}
|
|
}
|
|
|
|
void ADSBDemodGUI::leaveEvent(QEvent*)
|
|
{
|
|
m_channelMarker.setHighlighted(false);
|
|
}
|
|
|
|
void ADSBDemodGUI::enterEvent(QEvent*)
|
|
{
|
|
m_channelMarker.setHighlighted(true);
|
|
}
|
|
|
|
void ADSBDemodGUI::blockApplySettings(bool block)
|
|
{
|
|
m_doApplySettings = !block;
|
|
}
|
|
|
|
void ADSBDemodGUI::tick()
|
|
{
|
|
double magsqAvg, magsqPeak;
|
|
int nbMagsqSamples;
|
|
m_adsbDemod->getMagSqLevels(magsqAvg, magsqPeak, nbMagsqSamples);
|
|
double powDbAvg = CalcDb::dbPower(magsqAvg);
|
|
double powDbPeak = CalcDb::dbPower(magsqPeak);
|
|
|
|
ui->channelPowerMeter->levelChanged(
|
|
(100.0f + powDbAvg) / 100.0f,
|
|
(100.0f + powDbPeak) / 100.0f,
|
|
nbMagsqSamples);
|
|
|
|
if (m_tickCount % 4 == 0) {
|
|
ui->channelPower->setText(tr("%1 dB").arg(powDbAvg, 0, 'f', 1));
|
|
}
|
|
|
|
m_tickCount++;
|
|
|
|
// Tick is called 20x a second - lets check this every 10 seconds
|
|
if (m_tickCount % (20*10) == 0)
|
|
{
|
|
// Remove aircraft that haven't been heard of for a minute as probably out of range
|
|
QDateTime now = QDateTime::currentDateTime();
|
|
qint64 nowSecs = now.toSecsSinceEpoch();
|
|
QHash<int, Aircraft *>::iterator i = m_aircraft.begin();
|
|
while (i != m_aircraft.end())
|
|
{
|
|
Aircraft *aircraft = i.value();
|
|
qint64 secondsSinceLastFrame = nowSecs - aircraft->m_time.toSecsSinceEpoch();
|
|
if (secondsSinceLastFrame >= m_settings.m_removeTimeout)
|
|
{
|
|
// Don't try to track it anymore
|
|
if (m_trackAircraft == aircraft)
|
|
{
|
|
m_adsbDemod->clearTarget();
|
|
m_trackAircraft = nullptr;
|
|
}
|
|
// Remove map model
|
|
m_aircraftModel.removeAircraft(aircraft);
|
|
// Remove row from table
|
|
ui->adsbData->removeRow(aircraft->m_icaoItem->row());
|
|
// Remove aircraft from hash
|
|
i = m_aircraft.erase(i);
|
|
// Remove from map feature
|
|
MessagePipes& messagePipes = MainCore::instance()->getMessagePipes();
|
|
QList<MessageQueue*> *mapMessageQueues = messagePipes.getMessageQueues(m_adsbDemod, "mapitems");
|
|
if (mapMessageQueues)
|
|
{
|
|
QList<MessageQueue*>::iterator it = mapMessageQueues->begin();
|
|
for (; it != mapMessageQueues->end(); ++it)
|
|
{
|
|
SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem();
|
|
swgMapItem->setName(new QString(QString("%1").arg(aircraft->m_icao, 0, 16)));
|
|
swgMapItem->setImage(new QString(""));
|
|
MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_adsbDemod, swgMapItem);
|
|
(*it)->push(msg);
|
|
}
|
|
}
|
|
// And finally free its memory
|
|
delete aircraft;
|
|
}
|
|
else
|
|
++i;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ADSBDemodGUI::resizeTable()
|
|
{
|
|
// Fill table with a row of dummy data that will size the columns nicely
|
|
int row = ui->adsbData->rowCount();
|
|
ui->adsbData->setRowCount(row + 1);
|
|
ui->adsbData->setItem(row, ADSB_COL_ICAO, new QTableWidgetItem("ICAO ID"));
|
|
ui->adsbData->setItem(row, ADSB_COL_CALLSIGN, new QTableWidgetItem("Callsign"));
|
|
ui->adsbData->setItem(row, ADSB_COL_MODEL, new QTableWidgetItem("Aircraft12345"));
|
|
ui->adsbData->setItem(row, ADSB_COL_AIRLINE, new QTableWidgetItem("airbrigdecargo1"));
|
|
ui->adsbData->setItem(row, ADSB_COL_ALTITUDE, new QTableWidgetItem("Alt (ft)"));
|
|
ui->adsbData->setItem(row, ADSB_COL_SPEED, new QTableWidgetItem("Spd (kn)"));
|
|
ui->adsbData->setItem(row, ADSB_COL_HEADING, new QTableWidgetItem("Hd (o)"));
|
|
ui->adsbData->setItem(row, ADSB_COL_VERTICALRATE, new QTableWidgetItem("VR (ft/m)"));
|
|
ui->adsbData->setItem(row, ADSB_COL_RANGE, new QTableWidgetItem("D (km)"));
|
|
ui->adsbData->setItem(row, ADSB_COL_AZEL, new QTableWidgetItem("Az/El (o)"));
|
|
ui->adsbData->setItem(row, ADSB_COL_LATITUDE, new QTableWidgetItem("-90.00000"));
|
|
ui->adsbData->setItem(row, ADSB_COL_LONGITUDE, new QTableWidgetItem("-180.000000"));
|
|
ui->adsbData->setItem(row, ADSB_COL_CATEGORY, new QTableWidgetItem("Heavy"));
|
|
ui->adsbData->setItem(row, ADSB_COL_STATUS, new QTableWidgetItem("No emergency"));
|
|
ui->adsbData->setItem(row, ADSB_COL_SQUAWK, new QTableWidgetItem("Squawk"));
|
|
ui->adsbData->setItem(row, ADSB_COL_REGISTRATION, new QTableWidgetItem("G-12345"));
|
|
ui->adsbData->setItem(row, ADSB_COL_COUNTRY, new QTableWidgetItem("Country"));
|
|
ui->adsbData->setItem(row, ADSB_COL_REGISTERED, new QTableWidgetItem("Registered"));
|
|
ui->adsbData->setItem(row, ADSB_COL_MANUFACTURER, new QTableWidgetItem("The Boeing Company"));
|
|
ui->adsbData->setItem(row, ADSB_COL_OWNER, new QTableWidgetItem("British Airways"));
|
|
ui->adsbData->setItem(row, ADSB_COL_OPERATOR_ICAO, new QTableWidgetItem("Operator"));
|
|
ui->adsbData->setItem(row, ADSB_COL_TIME, new QTableWidgetItem("99:99:99"));
|
|
ui->adsbData->setItem(row, ADSB_COL_FRAMECOUNT, new QTableWidgetItem("Frames"));
|
|
ui->adsbData->setItem(row, ADSB_COL_CORRELATION, new QTableWidgetItem("0.001/0.001/0.001"));
|
|
ui->adsbData->setItem(row, ADSB_COL_RSSI, new QTableWidgetItem("-100.0"));
|
|
ui->adsbData->setItem(row, ADSB_COL_FLIGHT_STATUS, new QTableWidgetItem("scheduled"));
|
|
ui->adsbData->setItem(row, ADSB_COL_DEP, new QTableWidgetItem("WWWW"));
|
|
ui->adsbData->setItem(row, ADSB_COL_ARR, new QTableWidgetItem("WWWW"));
|
|
ui->adsbData->setItem(row, ADSB_COL_STD, new QTableWidgetItem("12:00 -1"));
|
|
ui->adsbData->setItem(row, ADSB_COL_ETD, new QTableWidgetItem("12:00 -1"));
|
|
ui->adsbData->setItem(row, ADSB_COL_ATD, new QTableWidgetItem("12:00 -1"));
|
|
ui->adsbData->setItem(row, ADSB_COL_STA, new QTableWidgetItem("12:00 +1"));
|
|
ui->adsbData->setItem(row, ADSB_COL_ETA, new QTableWidgetItem("12:00 +1"));
|
|
ui->adsbData->setItem(row, ADSB_COL_ATA, new QTableWidgetItem("12:00 +1"));
|
|
ui->adsbData->resizeColumnsToContents();
|
|
ui->adsbData->setRowCount(row);
|
|
}
|
|
|
|
Aircraft* ADSBDemodGUI::findAircraftByFlight(const QString& flight)
|
|
{
|
|
QHash<int, Aircraft *>::iterator i = m_aircraft.begin();
|
|
while (i != m_aircraft.end())
|
|
{
|
|
Aircraft *aircraft = i.value();
|
|
if (aircraft->m_flight == flight) {
|
|
return aircraft;
|
|
}
|
|
++i;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// Convert to hh:mm (+/-days)
|
|
QString ADSBDemodGUI::dataTimeToShortString(QDateTime dt)
|
|
{
|
|
if (dt.isValid())
|
|
{
|
|
QDate currentDate = QDateTime::currentDateTimeUtc().date();
|
|
if (dt.date() == currentDate)
|
|
{
|
|
return dt.time().toString("hh:mm");
|
|
}
|
|
else
|
|
{
|
|
int days = currentDate.daysTo(dt.date());
|
|
if (days >= 0) {
|
|
return QString("%1 +%2").arg(dt.time().toString("hh:mm")).arg(days);
|
|
} else {
|
|
return QString("%1 %2").arg(dt.time().toString("hh:mm")).arg(days);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return "";
|
|
}
|
|
}
|
|
|
|
void ADSBDemodGUI::initFlightInformation()
|
|
{
|
|
if (m_flightInformation)
|
|
{
|
|
disconnect(m_flightInformation, &FlightInformation::flightUpdated, this, &ADSBDemodGUI::flightInformationUpdated);
|
|
delete m_flightInformation;
|
|
m_flightInformation = nullptr;
|
|
}
|
|
if (!m_settings.m_apiKey.isEmpty())
|
|
{
|
|
m_flightInformation = FlightInformation::create(m_settings.m_apiKey);
|
|
if (m_flightInformation) {
|
|
connect(m_flightInformation, &FlightInformation::flightUpdated, this, &ADSBDemodGUI::flightInformationUpdated);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ADSBDemodGUI::flightInformationUpdated(const FlightInformation::Flight& flight)
|
|
{
|
|
Aircraft* aircraft = findAircraftByFlight(flight.m_flightICAO);
|
|
if (aircraft)
|
|
{
|
|
aircraft->m_flightStatusItem->setText(flight.m_flightStatus);
|
|
aircraft->m_depItem->setText(flight.m_departureICAO);
|
|
aircraft->m_arrItem->setText(flight.m_arrivalICAO);
|
|
aircraft->m_stdItem->setText(dataTimeToShortString(flight.m_departureScheduled));
|
|
aircraft->m_etdItem->setText(dataTimeToShortString(flight.m_departureEstimated));
|
|
aircraft->m_atdItem->setText(dataTimeToShortString(flight.m_departureActual));
|
|
aircraft->m_staItem->setText(dataTimeToShortString(flight.m_arrivalScheduled));
|
|
aircraft->m_etaItem->setText(dataTimeToShortString(flight.m_arrivalEstimated));
|
|
aircraft->m_ataItem->setText(dataTimeToShortString(flight.m_arrivalActual));
|
|
if (aircraft->m_positionValid) {
|
|
m_aircraftModel.aircraftUpdated(aircraft);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
qDebug() << "ADSBDemodGUI::flightInformationUpdated - Flight not found in ADS-B table: " << flight.m_flightICAO;
|
|
}
|
|
}
|