BIN
doc/img/HeatMap_plugin_map.png
Normal file
After Width: | Height: | Size: 506 KiB |
BIN
doc/img/HeatMap_plugin_power.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
doc/img/HeatMap_plugin_settings.png
Normal file
After Width: | Height: | Size: 46 KiB |
@ -114,6 +114,8 @@ if (ENABLE_CHANNELRX_DEMODFT8 AND FT8_SUPPORT)
|
||||
endif()
|
||||
|
||||
if(NOT SERVER_MODE)
|
||||
add_subdirectory(heatmap)
|
||||
|
||||
if (ENABLE_CHANNELRX_CHANALYZER)
|
||||
add_subdirectory(chanalyzer)
|
||||
endif()
|
||||
|
@ -29,7 +29,6 @@ set(adsb_HEADERS
|
||||
include_directories(
|
||||
${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client
|
||||
${Boost_INCLUDE_DIRS}
|
||||
${Qt${QT_DEFAULT_MAJOR_VERSION}Gui_PRIVATE_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
if(NOT SERVER_MODE)
|
||||
|
@ -29,6 +29,7 @@ ADSBDemodDisplayDialog::ADSBDemodDisplayDialog(ADSBDemodSettings *settings, QWid
|
||||
{
|
||||
ui->setupUi(this);
|
||||
ui->timeout->setValue(settings->m_removeTimeout);
|
||||
ui->aircraftMinZoom->setValue(settings->m_aircraftMinZoom);
|
||||
ui->airportRange->setValue(settings->m_airportRange);
|
||||
ui->airportSize->setCurrentIndex((int)settings->m_airportMinimumSize);
|
||||
ui->heliports->setChecked(settings->m_displayHeliports);
|
||||
@ -61,6 +62,7 @@ ADSBDemodDisplayDialog::~ADSBDemodDisplayDialog()
|
||||
void ADSBDemodDisplayDialog::accept()
|
||||
{
|
||||
m_settings->m_removeTimeout = ui->timeout->value();
|
||||
m_settings->m_aircraftMinZoom = ui->aircraftMinZoom->value();
|
||||
m_settings->m_airportRange = ui->airportRange->value();
|
||||
m_settings->m_airportMinimumSize = (ADSBDemodSettings::AirportType)ui->airportSize->currentIndex();
|
||||
m_settings->m_displayHeliports = ui->heliports->isChecked();
|
||||
|
@ -524,6 +524,23 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="aircraftMinZoomLabel">
|
||||
<property name="text">
|
||||
<string>Zoom level for aircraft scaling</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QSpinBox" name="aircraftMinZoom">
|
||||
<property name="toolTip">
|
||||
<string>When map zoom (0 min zoom - 15 max zoom) is higher than this value, aircraft icon size will be scaled</string>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>15</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
@ -35,8 +35,6 @@
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include <QtGui/private/qzipreader_p.h>
|
||||
|
||||
#include "ui_adsbdemodgui.h"
|
||||
#include "device/deviceapi.h"
|
||||
#include "channel/channelwebapiutils.h"
|
||||
@ -531,14 +529,7 @@ QVariant AirportModel::data(const QModelIndex &index, int role) const
|
||||
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"));
|
||||
return QVariant::fromValue(m_airports[row]->getImageName());
|
||||
}
|
||||
else if (role == AirportModel::bubbleColourRole)
|
||||
{
|
||||
@ -618,23 +609,21 @@ QVariant AirspaceModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
else if (role == AirspaceModel::airspaceBorderColorRole)
|
||||
{
|
||||
if (m_airspaces[row]->m_category == "D")
|
||||
{
|
||||
return QVariant::fromValue(QColor("blue"));
|
||||
}
|
||||
else
|
||||
{
|
||||
return QVariant::fromValue(QColor("red"));
|
||||
if ((m_airspaces[row]->m_category == "D")
|
||||
|| (m_airspaces[row]->m_category == "G")
|
||||
|| (m_airspaces[row]->m_category == "CTR")) {
|
||||
return QVariant::fromValue(QColor(0x00, 0x00, 0xff, 0x00));
|
||||
} else {
|
||||
return QVariant::fromValue(QColor(0xff, 0x00, 0x00, 0x00));
|
||||
}
|
||||
}
|
||||
else if (role == AirspaceModel::airspaceFillColorRole)
|
||||
{
|
||||
if (m_airspaces[row]->m_category == "D")
|
||||
{
|
||||
if ((m_airspaces[row]->m_category == "D")
|
||||
|| (m_airspaces[row]->m_category == "G")
|
||||
|| (m_airspaces[row]->m_category == "CTR")) {
|
||||
return QVariant::fromValue(QColor(0x00, 0x00, 0xff, 0x10));
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
return QVariant::fromValue(QColor(0xff, 0x00, 0x00, 0x10));
|
||||
}
|
||||
}
|
||||
@ -789,6 +778,22 @@ bool ADSBDemodGUI::updateLocalPosition(Aircraft *aircraft, double latitude, doub
|
||||
}
|
||||
}
|
||||
|
||||
void ADSBDemodGUI::clearFromMap(const QString& name)
|
||||
{
|
||||
QList<ObjectPipe*> mapPipes;
|
||||
MainCore::instance()->getMessagePipes().getMessagePipes(m_adsbDemod, "mapitems", mapPipes);
|
||||
|
||||
for (const auto& pipe : mapPipes)
|
||||
{
|
||||
MessageQueue *messageQueue = qobject_cast<MessageQueue*>(pipe->m_element);
|
||||
SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem();
|
||||
swgMapItem->setName(new QString(name));
|
||||
swgMapItem->setImage(new QString(""));
|
||||
MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_adsbDemod, swgMapItem);
|
||||
messageQueue->push(msg);
|
||||
}
|
||||
}
|
||||
|
||||
void ADSBDemodGUI::sendToMap(Aircraft *aircraft, QList<SWGSDRangel::SWGMapAnimation *> *animations)
|
||||
{
|
||||
// Send to Map feature
|
||||
@ -817,6 +822,7 @@ void ADSBDemodGUI::sendToMap(Aircraft *aircraft, QList<SWGSDRangel::SWGMapAnimat
|
||||
swgMapItem->setAltitude(altitudeM);
|
||||
swgMapItem->setPositionDateTime(new QString(aircraft->m_positionDateTime.toString(Qt::ISODateWithMs)));
|
||||
swgMapItem->setFixedPosition(false);
|
||||
swgMapItem->setAvailableUntil(new QString(aircraft->m_positionDateTime.addSecs(60).toString(Qt::ISODateWithMs)));
|
||||
swgMapItem->setImage(new QString(QString("qrc:///map/%1").arg(aircraft->getImage())));
|
||||
swgMapItem->setImageRotation(aircraft->m_heading);
|
||||
swgMapItem->setText(new QString(aircraft->getText(true)));
|
||||
@ -996,9 +1002,12 @@ Aircraft *ADSBDemodGUI::getAircraft(int icao, bool &newAircraft)
|
||||
}
|
||||
}
|
||||
|
||||
if (m_settings.m_autoResizeTableColumns)
|
||||
ui->adsbData->resizeColumnsToContents();
|
||||
ui->adsbData->setSortingEnabled(true);
|
||||
if (!m_loadingData)
|
||||
{
|
||||
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);
|
||||
}
|
||||
@ -3095,9 +3104,32 @@ void ADSBDemodGUI::checkDynamicNotification(Aircraft *aircraft)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialise text to speech engine
|
||||
// This takes 10 seconds on some versions of Linux, so only do it, if user actually
|
||||
// has speech notifications configured
|
||||
void ADSBDemodGUI::enableSpeechIfNeeded()
|
||||
{
|
||||
if (m_speech) {
|
||||
return;
|
||||
}
|
||||
for (const auto& notification : m_settings.m_notificationSettings)
|
||||
{
|
||||
if (!notification->m_speech.isEmpty())
|
||||
{
|
||||
qDebug() << "ADSBDemodGUI: Enabling text to speech";
|
||||
m_speech = new QTextToSpeech(this);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ADSBDemodGUI::speechNotification(Aircraft *aircraft, const QString &speech)
|
||||
{
|
||||
m_speech->say(subAircraftString(aircraft, speech));
|
||||
if (m_speech) {
|
||||
m_speech->say(subAircraftString(aircraft, speech));
|
||||
} else {
|
||||
qDebug() << "ADSBDemodGUI::speechNotification: Unable to say " << speech;
|
||||
}
|
||||
}
|
||||
|
||||
void ADSBDemodGUI::commandNotification(Aircraft *aircraft, const QString &command)
|
||||
@ -3299,7 +3331,9 @@ void ADSBDemodGUI::on_feed_clicked(bool checked)
|
||||
void ADSBDemodGUI::on_notifications_clicked()
|
||||
{
|
||||
ADSBDemodNotificationDialog dialog(&m_settings);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
if (dialog.exec() == QDialog::Accepted)
|
||||
{
|
||||
enableSpeechIfNeeded();
|
||||
applySettings();
|
||||
}
|
||||
}
|
||||
@ -3586,17 +3620,10 @@ void ADSBDemodGUI::on_getOSNDB_clicked()
|
||||
// Don't try to download while already in progress
|
||||
if (m_progressDialog == nullptr)
|
||||
{
|
||||
QString osnDBFilename = AircraftInformation::getOSNDBZipFilename();
|
||||
if (confirmDownload(osnDBFilename))
|
||||
{
|
||||
// Download Opensky network database to a file
|
||||
QUrl dbURL(QString(OSNDB_URL));
|
||||
m_progressDialog = new QProgressDialog(this);
|
||||
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)));
|
||||
}
|
||||
m_progressDialog = new QProgressDialog(this);
|
||||
m_progressDialog->setCancelButton(nullptr);
|
||||
m_progressDialog->setWindowFlag(Qt::WindowCloseButtonHint, false);
|
||||
m_osnDB.downloadAircraftInformation();
|
||||
}
|
||||
}
|
||||
|
||||
@ -3605,17 +3632,10 @@ 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->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)));
|
||||
}
|
||||
m_progressDialog = new QProgressDialog(this);
|
||||
m_progressDialog->setCancelButton(nullptr);
|
||||
m_progressDialog->setWindowFlag(Qt::WindowCloseButtonHint, false);
|
||||
m_ourAirportsDB.downloadAirportInformation();
|
||||
}
|
||||
}
|
||||
|
||||
@ -3627,6 +3647,7 @@ void ADSBDemodGUI::on_getAirspacesDB_clicked()
|
||||
m_progressDialog = new QProgressDialog(this);
|
||||
m_progressDialog->setMaximum(OpenAIP::m_countryCodes.size());
|
||||
m_progressDialog->setCancelButton(nullptr);
|
||||
m_progressDialog->setWindowFlag(Qt::WindowCloseButtonHint, false);
|
||||
m_openAIP.downloadAirspaces();
|
||||
}
|
||||
}
|
||||
@ -3651,136 +3672,6 @@ QString ADSBDemodGUI::getDataDir()
|
||||
return locations[0];
|
||||
}
|
||||
|
||||
QString ADSBDemodGUI::getAirportDBFilename()
|
||||
{
|
||||
return getDataDir() + "/airportDatabase.csv";
|
||||
}
|
||||
|
||||
QString ADSBDemodGUI::getAirportFrequenciesDBFilename()
|
||||
{
|
||||
return getDataDir() + "/airportFrequenciesDatabase.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)
|
||||
{
|
||||
if (m_progressDialog)
|
||||
{
|
||||
m_progressDialog->setMaximum(totalBytes);
|
||||
m_progressDialog->setValue(bytesRead);
|
||||
}
|
||||
}
|
||||
|
||||
void ADSBDemodGUI::downloadFinished(const QString& filename, bool success)
|
||||
{
|
||||
bool closeDialog = true;
|
||||
if (success)
|
||||
{
|
||||
if (filename == AircraftInformation::getOSNDBZipFilename())
|
||||
{
|
||||
// Extract .csv file from .zip file
|
||||
QZipReader reader(filename);
|
||||
QByteArray database = reader.fileData("media/data/samples/metadata/aircraftDatabase.csv");
|
||||
if (database.size() > 0)
|
||||
{
|
||||
QFile file(AircraftInformation::getOSNDBFilename());
|
||||
if (file.open(QIODevice::WriteOnly))
|
||||
{
|
||||
file.write(database);
|
||||
file.close();
|
||||
}
|
||||
else
|
||||
{
|
||||
qWarning() << "ADSBDemodGUI::downloadFinished - Failed to open " << file.fileName() << " for writing";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qWarning() << "ADSBDemodGUI::downloadFinished - aircraftDatabase.csv not in expected dir. Extracting all.";
|
||||
if (!reader.extractAll(getDataDir())) {
|
||||
qWarning() << "ADSBDemodGUI::downloadFinished - Failed to extract files from " << filename;
|
||||
}
|
||||
}
|
||||
readOSNDB(AircraftInformation::getOSNDBFilename());
|
||||
// Convert to condensed format for faster loading later
|
||||
m_progressDialog->setLabelText("Processing.");
|
||||
AircraftInformation::writeFastDB(AircraftInformation::getFastDBFilename(), m_aircraftInfo);
|
||||
}
|
||||
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)));
|
||||
closeDialog = false;
|
||||
}
|
||||
else if (filename == getAirportFrequenciesDBFilename())
|
||||
{
|
||||
if (m_airportInfo != nullptr)
|
||||
{
|
||||
AirportInformation::readFrequenciesDB(filename, m_airportInfo);
|
||||
// Update airports on map
|
||||
updateAirports();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "ADSBDemodGUI::downloadFinished: Unexpected filename: " << filename;
|
||||
}
|
||||
}
|
||||
if (closeDialog && m_progressDialog)
|
||||
{
|
||||
m_progressDialog->close();
|
||||
delete m_progressDialog;
|
||||
m_progressDialog = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void ADSBDemodGUI::onWidgetRolled(QWidget* widget, bool rollDown)
|
||||
{
|
||||
(void) widget;
|
||||
@ -4335,12 +4226,12 @@ void ADSBDemodGUI::updateAirports()
|
||||
}
|
||||
|
||||
m_airportModel.removeAllAirports();
|
||||
QHash<int, AirportInformation *>::iterator i = m_airportInfo->begin();
|
||||
QHash<int, AirportInformation *>::const_iterator i = m_airportInfo->begin();
|
||||
AzEl azEl = m_azEl;
|
||||
|
||||
while (i != m_airportInfo->end())
|
||||
{
|
||||
AirportInformation *airportInfo = i.value();
|
||||
const 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));
|
||||
@ -4350,10 +4241,10 @@ void ADSBDemodGUI::updateAirports()
|
||||
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)
|
||||
if (airportInfo->m_type >= (AirportInformation::AirportType)m_settings.m_airportMinimumSize)
|
||||
{
|
||||
// Only display heliports if enabled
|
||||
if (m_settings.m_displayHeliports || (airportInfo->m_type != ADSBDemodSettings::AirportType::Heliport))
|
||||
if (m_settings.m_displayHeliports || (airportInfo->m_type != AirportInformation::AirportType::Heliport))
|
||||
{
|
||||
m_airportModel.addAirport(airportInfo, azEl.getAzimuth(), azEl.getElevation(), azEl.getDistance());
|
||||
}
|
||||
@ -4370,7 +4261,7 @@ void ADSBDemodGUI::updateAirspaces()
|
||||
{
|
||||
AzEl azEl = m_azEl;
|
||||
m_airspaceModel.removeAllAirspaces();
|
||||
for (const auto& airspace: m_airspaces)
|
||||
for (const auto airspace: *m_airspaces)
|
||||
{
|
||||
if (m_settings.m_airspaces.contains(airspace->m_category))
|
||||
{
|
||||
@ -4392,7 +4283,7 @@ void ADSBDemodGUI::updateNavAids()
|
||||
m_navAidModel.removeAllNavAids();
|
||||
if (m_settings.m_displayNavAids)
|
||||
{
|
||||
for (const auto& navAid: m_navAids)
|
||||
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));
|
||||
@ -4630,6 +4521,8 @@ void ADSBDemodGUI::applyMapSettings()
|
||||
}
|
||||
|
||||
// Create the map using the specified provider
|
||||
QQmlProperty::write(item, "smoothing", MainCore::instance()->getSettings().getMapSmoothing());
|
||||
QQmlProperty::write(item, "aircraftMinZoomLevel", m_settings.m_aircraftMinZoom);
|
||||
QQmlProperty::write(item, "mapProvider", m_settings.m_mapProvider);
|
||||
QVariantMap parameters;
|
||||
QString mapType;
|
||||
@ -4745,7 +4638,9 @@ ADSBDemodGUI::ADSBDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseb
|
||||
m_airspaceModel(this),
|
||||
m_trackAircraft(nullptr),
|
||||
m_highlightAircraft(nullptr),
|
||||
m_progressDialog(nullptr)
|
||||
m_speech(nullptr),
|
||||
m_progressDialog(nullptr),
|
||||
m_loadingData(false)
|
||||
{
|
||||
setAttribute(Qt::WA_DeleteOnClose, true);
|
||||
m_helpURL = "plugins/channelrx/demodadsb/readme.md";
|
||||
@ -4755,6 +4650,17 @@ ADSBDemodGUI::ADSBDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseb
|
||||
rollupContents->arrangeRollups();
|
||||
connect(rollupContents, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool)));
|
||||
|
||||
// Enable MSAA antialiasing on 2D map
|
||||
// This can be much faster than using layer.smooth in the QML, when there are many items
|
||||
// However, only seems to work when set to 16, and doesn't seem to be supported on all graphics cards
|
||||
int multisamples = MainCore::instance()->getSettings().getMapMultisampling();
|
||||
if (multisamples > 0)
|
||||
{
|
||||
QSurfaceFormat format;
|
||||
format.setSamples(multisamples);
|
||||
ui->map->setFormat(format);
|
||||
}
|
||||
|
||||
m_osmPort = 0; // Pick a free port
|
||||
m_templateServer = new ADSBOSMTemplateServer("q2RVNAe3eFKCH4XsrE3r", m_osmPort);
|
||||
|
||||
@ -4767,7 +4673,6 @@ ADSBDemodGUI::ADSBDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseb
|
||||
ui->map->setSource(QUrl(QStringLiteral("qrc:/map/map.qml")));
|
||||
|
||||
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());
|
||||
@ -4836,28 +4741,28 @@ ADSBDemodGUI::ADSBDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseb
|
||||
ui->flightDetails->setVisible(false);
|
||||
ui->aircraftDetails->setVisible(false);
|
||||
|
||||
AircraftInformation::init();
|
||||
|
||||
// Read aircraft information database, if it has previously been downloaded
|
||||
if (!readFastDB(AircraftInformation::getFastDBFilename()))
|
||||
{
|
||||
if (readOSNDB(AircraftInformation::getOSNDBFilename()))
|
||||
AircraftInformation::writeFastDB(AircraftInformation::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);
|
||||
AircraftInformation::init();
|
||||
connect(&m_osnDB, &OsnDB::downloadingURL, this, &ADSBDemodGUI::downloadingURL);
|
||||
connect(&m_osnDB, &OsnDB::downloadError, this, &ADSBDemodGUI::downloadError);
|
||||
connect(&m_osnDB, &OsnDB::downloadProgress, this, &ADSBDemodGUI::downloadProgress);
|
||||
connect(&m_osnDB, &OsnDB::downloadAircraftInformationFinished, this, &ADSBDemodGUI::downloadAircraftInformationFinished);
|
||||
m_aircraftInfo = OsnDB::getAircraftInformation();
|
||||
|
||||
// Read airport information database, if it has previously been downloaded
|
||||
connect(&m_ourAirportsDB, &OurAirportsDB::downloadingURL, this, &ADSBDemodGUI::downloadingURL);
|
||||
connect(&m_ourAirportsDB, &OurAirportsDB::downloadError, this, &ADSBDemodGUI::downloadError);
|
||||
connect(&m_ourAirportsDB, &OurAirportsDB::downloadProgress, this, &ADSBDemodGUI::downloadProgress);
|
||||
connect(&m_ourAirportsDB, &OurAirportsDB::downloadAirportInformationFinished, this, &ADSBDemodGUI::downloadAirportInformationFinished);
|
||||
m_airportInfo = OurAirportsDB::getAirportsById();
|
||||
|
||||
// Read airspaces and NAVAIDs
|
||||
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();
|
||||
m_airspaces = OpenAIP::getAirspaces();
|
||||
m_navAids = OpenAIP::getNavAids();
|
||||
|
||||
// Get station position
|
||||
Real stationLatitude = MainCore::instance()->getSettings().getLatitude();
|
||||
@ -4882,9 +4787,6 @@ ADSBDemodGUI::ADSBDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseb
|
||||
updateNavAids();
|
||||
update3DModels();
|
||||
|
||||
// Initialise text to speech engine
|
||||
m_speech = new QTextToSpeech(this);
|
||||
|
||||
m_flightInformation = nullptr;
|
||||
m_aviationWeather = nullptr;
|
||||
|
||||
@ -4929,26 +4831,24 @@ ADSBDemodGUI::~ADSBDemodGUI()
|
||||
disconnect(&m_openAIP, &OpenAIP::downloadNavAidsFinished, this, &ADSBDemodGUI::downloadNavAidsFinished);
|
||||
disconnect(&m_planeSpotters, &PlaneSpotters::aircraftPhoto, this, &ADSBDemodGUI::aircraftPhoto);
|
||||
disconnect(&m_redrawMapTimer, &QTimer::timeout, this, &ADSBDemodGUI::redrawMap);
|
||||
disconnect(&MainCore::instance()->getMasterTimer(), &QTimer::timeout, this, &ADSBDemodGUI::tick);
|
||||
m_redrawMapTimer.stop();
|
||||
// Remove aircraft from Map feature
|
||||
QHash<int, Aircraft *>::iterator i = m_aircraft.begin();
|
||||
while (i != m_aircraft.end())
|
||||
{
|
||||
Aircraft *aircraft = i.value();
|
||||
clearFromMap(QString("%1").arg(aircraft->m_icao, 0, 16));
|
||||
++i;
|
||||
}
|
||||
delete ui;
|
||||
qDeleteAll(m_aircraft);
|
||||
if (m_airportInfo) {
|
||||
qDeleteAll(*m_airportInfo);
|
||||
}
|
||||
if (m_aircraftInfo) {
|
||||
qDeleteAll(*m_aircraftInfo);
|
||||
}
|
||||
if (m_flightInformation)
|
||||
{
|
||||
disconnect(m_flightInformation, &FlightInformation::flightUpdated, this, &ADSBDemodGUI::flightInformationUpdated);
|
||||
delete m_flightInformation;
|
||||
}
|
||||
if (m_aviationWeather)
|
||||
{
|
||||
delete m_aviationWeather;
|
||||
}
|
||||
qDeleteAll(m_airspaces);
|
||||
qDeleteAll(m_navAids);
|
||||
delete m_aviationWeather;
|
||||
qDeleteAll(m_3DModelMatch);
|
||||
delete m_networkManager;
|
||||
}
|
||||
@ -5075,6 +4975,7 @@ void ADSBDemodGUI::displaySettings()
|
||||
|
||||
getRollupContents()->restoreState(m_rollupState);
|
||||
blockApplySettings(false);
|
||||
enableSpeechIfNeeded();
|
||||
}
|
||||
|
||||
void ADSBDemodGUI::leaveEvent(QEvent* event)
|
||||
@ -5116,7 +5017,7 @@ void ADSBDemodGUI::tick()
|
||||
// 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
|
||||
// Remove aircraft that haven't been heard of for a user-defined time, as probably out of range
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
qint64 nowSecs = now.toSecsSinceEpoch();
|
||||
QHash<int, Aircraft *>::iterator i = m_aircraft.begin();
|
||||
@ -5142,18 +5043,7 @@ void ADSBDemodGUI::tick()
|
||||
// Remove aircraft from hash
|
||||
i = m_aircraft.erase(i);
|
||||
// Remove from map feature
|
||||
QList<ObjectPipe*> mapPipes;
|
||||
MainCore::instance()->getMessagePipes().getMessagePipes(this, "mapitems", mapPipes);
|
||||
|
||||
for (const auto& pipe : mapPipes)
|
||||
{
|
||||
MessageQueue *messageQueue = qobject_cast<MessageQueue*>(pipe->m_element);
|
||||
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);
|
||||
messageQueue->push(msg);
|
||||
}
|
||||
clearFromMap(QString("%1").arg(aircraft->m_icao, 0, 16));
|
||||
|
||||
// And finally free its memory
|
||||
delete aircraft;
|
||||
@ -5376,6 +5266,9 @@ void ADSBDemodGUI::on_logOpen_clicked()
|
||||
QFile file(fileNames[0]);
|
||||
if (file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
{
|
||||
QDateTime startTime = QDateTime::currentDateTime();
|
||||
m_loadingData = true;
|
||||
ui->adsbData->blockSignals(true);
|
||||
QTextStream in(&file);
|
||||
QString error;
|
||||
QHash<QString, int> colIndexes = CSV::readHeader(in, {"Data", "Correlation"}, error);
|
||||
@ -5414,7 +5307,7 @@ void ADSBDemodGUI::on_logOpen_clicked()
|
||||
}
|
||||
//qDebug() << "bytes.szie " << bytes.size() << " crc " << Qt::hex << crcCalc;
|
||||
handleADSB(bytes, dateTime, correlation, correlation, crcCalc, false);
|
||||
if ((count > 0) && (count % 10000 == 0))
|
||||
if ((count > 0) && (count % 100000 == 0))
|
||||
{
|
||||
dialog.setText(QString("Reading ADS-B data\n%1 (Skipped %2)").arg(count).arg(countOtherDF));
|
||||
QApplication::processEvents();
|
||||
@ -5437,6 +5330,13 @@ void ADSBDemodGUI::on_logOpen_clicked()
|
||||
{
|
||||
QMessageBox::critical(this, "ADS-B", error);
|
||||
}
|
||||
ui->adsbData->blockSignals(false);
|
||||
m_loadingData = false;
|
||||
if (m_settings.m_autoResizeTableColumns)
|
||||
ui->adsbData->resizeColumnsToContents();
|
||||
ui->adsbData->setSortingEnabled(true);
|
||||
QDateTime finishTime = QDateTime::currentDateTime();
|
||||
qDebug() << "Read CSV in " << startTime.secsTo(finishTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -5455,6 +5355,15 @@ void ADSBDemodGUI::downloadingURL(const QString& url)
|
||||
}
|
||||
}
|
||||
|
||||
void ADSBDemodGUI::downloadProgress(qint64 bytesRead, qint64 totalBytes)
|
||||
{
|
||||
if (m_progressDialog)
|
||||
{
|
||||
m_progressDialog->setMaximum(totalBytes);
|
||||
m_progressDialog->setValue(bytesRead);
|
||||
}
|
||||
}
|
||||
|
||||
void ADSBDemodGUI::downloadError(const QString& error)
|
||||
{
|
||||
QMessageBox::critical(this, "ADS-B", error);
|
||||
@ -5471,7 +5380,7 @@ void ADSBDemodGUI::downloadAirspaceFinished()
|
||||
if (m_progressDialog) {
|
||||
m_progressDialog->setLabelText("Reading airspaces.");
|
||||
}
|
||||
m_airspaces = OpenAIP::readAirspaces();
|
||||
m_airspaces = OpenAIP::getAirspaces();
|
||||
updateAirspaces();
|
||||
m_openAIP.downloadNavAids();
|
||||
}
|
||||
@ -5481,7 +5390,7 @@ void ADSBDemodGUI::downloadNavAidsFinished()
|
||||
if (m_progressDialog) {
|
||||
m_progressDialog->setLabelText("Reading NAVAIDs.");
|
||||
}
|
||||
m_navAids = OpenAIP::readNavAids();
|
||||
m_navAids = OpenAIP::getNavAids();
|
||||
updateNavAids();
|
||||
if (m_progressDialog)
|
||||
{
|
||||
@ -5491,6 +5400,50 @@ void ADSBDemodGUI::downloadNavAidsFinished()
|
||||
}
|
||||
}
|
||||
|
||||
void ADSBDemodGUI::downloadAircraftInformationFinished()
|
||||
{
|
||||
if (m_progressDialog)
|
||||
{
|
||||
delete m_progressDialog;
|
||||
m_progressDialog = new QProgressDialog("Reading Aircraft Information.", "", 0, 1, this);
|
||||
m_progressDialog->setCancelButton(nullptr);
|
||||
m_progressDialog->setWindowFlag(Qt::WindowCloseButtonHint, false);
|
||||
m_progressDialog->setWindowModality(Qt::WindowModal);
|
||||
m_progressDialog->show();
|
||||
QApplication::processEvents();
|
||||
}
|
||||
m_aircraftInfo = OsnDB::getAircraftInformation();
|
||||
m_aircraftModel.updateAircraftInformation(m_aircraftInfo);
|
||||
if (m_progressDialog)
|
||||
{
|
||||
m_progressDialog->close();
|
||||
delete m_progressDialog;
|
||||
m_progressDialog = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void ADSBDemodGUI::downloadAirportInformationFinished()
|
||||
{
|
||||
if (m_progressDialog)
|
||||
{
|
||||
delete m_progressDialog;
|
||||
m_progressDialog = new QProgressDialog("Reading Airport Information.", "", 0, 1, this);
|
||||
m_progressDialog->setCancelButton(nullptr);
|
||||
m_progressDialog->setWindowFlag(Qt::WindowCloseButtonHint, false);
|
||||
m_progressDialog->setWindowModality(Qt::WindowModal);
|
||||
m_progressDialog->show();
|
||||
QApplication::processEvents();
|
||||
}
|
||||
m_airportInfo = OurAirportsDB::getAirportsById();
|
||||
updateAirports();
|
||||
if (m_progressDialog)
|
||||
{
|
||||
m_progressDialog->close();
|
||||
delete m_progressDialog;
|
||||
m_progressDialog = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
int ADSBDemodGUI::squawkDecode(int modeA) const
|
||||
{
|
||||
int a, b, c, d;
|
||||
@ -5771,16 +5724,21 @@ void ADSBDemodGUI::preferenceChanged(int elementType)
|
||||
Real stationLongitude = MainCore::instance()->getSettings().getLongitude();
|
||||
Real stationAltitude = MainCore::instance()->getSettings().getAltitude();
|
||||
|
||||
if ( (stationLatitude != m_azEl.getLocationSpherical().m_latitude)
|
||||
|| (stationLongitude != m_azEl.getLocationSpherical().m_longitude)
|
||||
|| (stationAltitude != m_azEl.getLocationSpherical().m_altitude))
|
||||
QGeoCoordinate stationPosition(stationLatitude, stationLongitude, stationAltitude);
|
||||
QGeoCoordinate previousPosition(m_azEl.getLocationSpherical().m_latitude, m_azEl.getLocationSpherical().m_longitude, m_azEl.getLocationSpherical().m_altitude);
|
||||
|
||||
if (stationPosition != previousPosition)
|
||||
{
|
||||
m_azEl.setLocation(stationLatitude, stationLongitude, stationAltitude);
|
||||
|
||||
// Update distances and what is visible
|
||||
updateAirports();
|
||||
updateAirspaces();
|
||||
updateNavAids();
|
||||
// Update distances and what is visible, but only do it if position has changed significantly
|
||||
if (!m_lastFullUpdatePosition.isValid() || (stationPosition.distanceTo(m_lastFullUpdatePosition) >= 1000))
|
||||
{
|
||||
updateAirports();
|
||||
updateAirspaces();
|
||||
updateNavAids();
|
||||
m_lastFullUpdatePosition = stationPosition;
|
||||
}
|
||||
|
||||
// Update icon position on Map
|
||||
QQuickItem *item = ui->map->rootObject();
|
||||
@ -5799,7 +5757,7 @@ void ADSBDemodGUI::preferenceChanged(int elementType)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pref == Preferences::StationName)
|
||||
else if (pref == Preferences::StationName)
|
||||
{
|
||||
// Update icon label on Map
|
||||
QQuickItem *item = ui->map->rootObject();
|
||||
@ -5812,6 +5770,11 @@ void ADSBDemodGUI::preferenceChanged(int elementType)
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (pref == Preferences::MapSmoothing)
|
||||
{
|
||||
QQuickItem *item = ui->map->rootObject();
|
||||
QQmlProperty::write(item, "smoothing", MainCore::instance()->getSettings().getMapSmoothing());
|
||||
}
|
||||
}
|
||||
|
||||
void ADSBDemodGUI::initAviationWeather()
|
||||
@ -5875,3 +5838,4 @@ void ADSBDemodGUI::updateAbsoluteCenterFrequency()
|
||||
{
|
||||
setStatusFrequency(m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset);
|
||||
}
|
||||
|
||||
|
@ -37,7 +37,6 @@
|
||||
#include "util/messagequeue.h"
|
||||
#include "util/azel.h"
|
||||
#include "util/movingaverage.h"
|
||||
#include "util/httpdownloadmanager.h"
|
||||
#include "util/flightinformation.h"
|
||||
#include "util/openaip.h"
|
||||
#include "util/planespotters.h"
|
||||
@ -47,7 +46,7 @@
|
||||
#include "SWGMapItem.h"
|
||||
|
||||
#include "adsbdemodsettings.h"
|
||||
#include "ourairportsdb.h"
|
||||
#include "util/ourairportsdb.h"
|
||||
#include "util/osndb.h"
|
||||
|
||||
class PluginAPI;
|
||||
@ -464,7 +463,19 @@ public:
|
||||
allAircraftUpdated();
|
||||
}
|
||||
|
||||
Q_INVOKABLE void findOnMap(int index);
|
||||
Q_INVOKABLE void findOnMap(int index);
|
||||
|
||||
void updateAircraftInformation(QSharedPointer<const QHash<int, AircraftInformation *>> aircraftInfo)
|
||||
{
|
||||
for (auto aircraft : m_aircrafts)
|
||||
{
|
||||
if (aircraftInfo->contains(aircraft->m_icao)) {
|
||||
aircraft->m_aircraftInfo = aircraftInfo->value(aircraft->m_icao);
|
||||
} else {
|
||||
aircraft->m_aircraftInfo = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
QList<Aircraft *> m_aircrafts;
|
||||
@ -493,7 +504,7 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
Q_INVOKABLE void addAirport(AirportInformation *airport, float az, float el, float distance) {
|
||||
Q_INVOKABLE void addAirport(const AirportInformation *airport, float az, float el, float distance) {
|
||||
QString text;
|
||||
int rows;
|
||||
|
||||
@ -558,7 +569,7 @@ public:
|
||||
return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable;
|
||||
}
|
||||
|
||||
void airportFreq(AirportInformation *airport, float az, float el, float distance, QString& text, int& rows) {
|
||||
void airportFreq(const AirportInformation *airport, float az, float el, float distance, QString& text, int& rows) {
|
||||
// Create the text to go in the bubble next to the airport
|
||||
// Display name and frequencies
|
||||
QStringList list;
|
||||
@ -567,7 +578,7 @@ public:
|
||||
rows = 1;
|
||||
for (int i = 0; i < airport->m_frequencies.size(); i++)
|
||||
{
|
||||
AirportInformation::FrequencyInformation *frequencyInfo = airport->m_frequencies[i];
|
||||
const AirportInformation::FrequencyInformation *frequencyInfo = airport->m_frequencies[i];
|
||||
list.append(QString("%1: %2 MHz").arg(frequencyInfo->m_type).arg(frequencyInfo->m_frequency));
|
||||
rows++;
|
||||
}
|
||||
@ -577,7 +588,7 @@ public:
|
||||
text = list.join("\n");
|
||||
}
|
||||
|
||||
void airportUpdated(AirportInformation *airport) {
|
||||
void airportUpdated(const AirportInformation *airport) {
|
||||
int row = m_airports.indexOf(airport);
|
||||
if (row >= 0)
|
||||
{
|
||||
@ -614,7 +625,7 @@ public:
|
||||
|
||||
private:
|
||||
ADSBDemodGUI *m_gui;
|
||||
QList<AirportInformation *> m_airports;
|
||||
QList<const AirportInformation *> m_airports;
|
||||
QList<QString> m_airportDataFreq;
|
||||
QList<int> m_airportDataFreqRows;
|
||||
QList<bool> m_showFreq;
|
||||
@ -894,14 +905,15 @@ private:
|
||||
MessageQueue m_inputMessageQueue;
|
||||
|
||||
QHash<int, Aircraft *> m_aircraft; // Hashed on ICAO
|
||||
QHash<int, AircraftInformation *> *m_aircraftInfo;
|
||||
QHash<int, AirportInformation *> *m_airportInfo; // Hashed on id
|
||||
QSharedPointer<const QHash<int, AircraftInformation *>> m_aircraftInfo;
|
||||
QSharedPointer<const QHash<int, AirportInformation *>> m_airportInfo; // Hashed on id
|
||||
AircraftModel m_aircraftModel;
|
||||
AirportModel m_airportModel;
|
||||
AirspaceModel m_airspaceModel;
|
||||
NavAidModel m_navAidModel;
|
||||
QList<Airspace *> m_airspaces;
|
||||
QList<NavAid *> m_navAids;
|
||||
QSharedPointer<const QList<Airspace *>> m_airspaces;
|
||||
QSharedPointer<const QList<NavAid *>> m_navAids;
|
||||
QGeoCoordinate m_lastFullUpdatePosition;
|
||||
|
||||
AzEl m_azEl; // Position of station
|
||||
Aircraft *m_trackAircraft; // Aircraft we want to track in Channel Report
|
||||
@ -920,10 +932,11 @@ private:
|
||||
AviationWeather *m_aviationWeather;
|
||||
QString m_photoLink;
|
||||
WebAPIAdapterInterface *m_webAPIAdapterInterface;
|
||||
HttpDownloadManager m_dlm;
|
||||
QProgressDialog *m_progressDialog;
|
||||
quint16 m_osmPort;
|
||||
OpenAIP m_openAIP;
|
||||
OsnDB m_osnDB;
|
||||
OurAirportsDB m_ourAirportsDB;
|
||||
ADSBOSMTemplateServer *m_templateServer;
|
||||
QRandomGenerator m_random;
|
||||
QHash<QString, QString> m_3DModels; // Hashed aircraft_icao or just aircraft
|
||||
@ -935,6 +948,7 @@ private:
|
||||
QTimer m_importTimer;
|
||||
QTimer m_redrawMapTimer;
|
||||
QNetworkAccessManager *m_networkManager;
|
||||
bool m_loadingData;
|
||||
|
||||
static const char m_idMap[];
|
||||
static const QString m_categorySetA[];
|
||||
@ -957,6 +971,7 @@ private:
|
||||
|
||||
void updatePosition(Aircraft *aircraft);
|
||||
bool updateLocalPosition(Aircraft *aircraft, double latitude, double longitude, bool surfacePosition);
|
||||
void clearFromMap(const QString& name);
|
||||
void sendToMap(Aircraft *aircraft, QList<SWGSDRangel::SWGMapAnimation *> *animations);
|
||||
Aircraft *getAircraft(int icao, bool &newAircraft);
|
||||
void callsignToFlight(Aircraft *aircraft);
|
||||
@ -979,22 +994,14 @@ private:
|
||||
SWGSDRangel::SWGMapAnimation *engineAnimation(QDateTime startDateTime, int engine, bool stop);
|
||||
void checkStaticNotification(Aircraft *aircraft);
|
||||
void checkDynamicNotification(Aircraft *aircraft);
|
||||
void enableSpeechIfNeeded();
|
||||
void speechNotification(Aircraft *aircraft, const QString &speech);
|
||||
void commandNotification(Aircraft *aircraft, const QString &command);
|
||||
QString subAircraftString(Aircraft *aircraft, const QString &string);
|
||||
void resizeTable();
|
||||
QString getDataDir();
|
||||
QString getAirportDBFilename();
|
||||
QString getAirportFrequenciesDBFilename();
|
||||
QString getOSNDBZipFilename();
|
||||
QString getOSNDBFilename();
|
||||
QString getFastDBFilename();
|
||||
qint64 fileAgeInDays(QString filename);
|
||||
bool confirmDownload(QString filename);
|
||||
void readAirportDB(const QString& filename);
|
||||
void readAirportFrequenciesDB(const QString& filename);
|
||||
bool readOSNDB(const QString& filename);
|
||||
bool readFastDB(const QString& filename);
|
||||
void update3DModels();
|
||||
void updateAirports();
|
||||
void updateAirspaces();
|
||||
@ -1047,8 +1054,6 @@ private slots:
|
||||
void onMenuDialogCalled(const QPoint& p);
|
||||
void handleInputMessages();
|
||||
void tick();
|
||||
void updateDownloadProgress(qint64 bytesRead, qint64 totalBytes);
|
||||
void downloadFinished(const QString& filename, bool success);
|
||||
void on_device_currentIndexChanged(int index);
|
||||
void feedSelect(const QPoint& p);
|
||||
void on_displaySettings_clicked();
|
||||
@ -1057,8 +1062,11 @@ private slots:
|
||||
void on_logOpen_clicked();
|
||||
void downloadingURL(const QString& url);
|
||||
void downloadError(const QString& error);
|
||||
void downloadProgress(qint64 bytesRead, qint64 totalBytes);
|
||||
void downloadAirspaceFinished();
|
||||
void downloadNavAidsFinished();
|
||||
void downloadAircraftInformationFinished();
|
||||
void downloadAirportInformationFinished();
|
||||
void photoClicked();
|
||||
virtual void showEvent(QShowEvent *event) override;
|
||||
virtual bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
@ -1073,3 +1081,4 @@ signals:
|
||||
};
|
||||
|
||||
#endif // INCLUDE_ADSBDEMODGUI_H
|
||||
|
||||
|
@ -844,6 +844,9 @@
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>ICAO ID</string>
|
||||
|
@ -90,7 +90,7 @@ void ADSBDemodSettings::resetToDefaults()
|
||||
}
|
||||
m_logFilename = "adsb_log.csv";
|
||||
m_logEnabled = false;
|
||||
m_airspaces = QStringList({"A", "D", "TMZ"});
|
||||
m_airspaces = QStringList({"CTR"});
|
||||
m_airspaceRange = 500.0f;
|
||||
#ifdef LINUX
|
||||
m_mapProvider = "mapboxgl"; // osm maps do not work in some versions of Linux https://github.com/f4exb/sdrangel/issues/1169 & 1369
|
||||
@ -102,6 +102,7 @@ void ADSBDemodSettings::resetToDefaults()
|
||||
m_displayPhotos = true;
|
||||
m_verboseModelMatching = false;
|
||||
m_airfieldElevation = 0;
|
||||
m_aircraftMinZoom = 11;
|
||||
m_workspaceIndex = 0;
|
||||
m_hidden = false;
|
||||
}
|
||||
@ -185,6 +186,7 @@ QByteArray ADSBDemodSettings::serialize() const
|
||||
s.writeBool(61, m_hidden);
|
||||
s.writeString(62, m_checkWXAPIKey);
|
||||
s.writeString(63, m_mapProvider);
|
||||
s.writeS32(64, m_aircraftMinZoom);
|
||||
|
||||
for (int i = 0; i < ADSBDEMOD_COLUMNS; i++) {
|
||||
s.writeS32(100 + i, m_columnIndexes[i]);
|
||||
@ -277,7 +279,7 @@ bool ADSBDemodSettings::deserialize(const QByteArray& data)
|
||||
d.readString(36, &m_logFilename, "adsb_log.csv");
|
||||
d.readBool(37, &m_logEnabled, false);
|
||||
|
||||
d.readString(38, &string, "A D TMZ");
|
||||
d.readString(38, &string, "CTR");
|
||||
m_airspaces = string.split(" ");
|
||||
d.readFloat(39, &m_airspaceRange, 500.0f);
|
||||
d.readS32(40, (int *)&m_mapType, (int)AVIATION_LIGHT);
|
||||
@ -316,6 +318,7 @@ bool ADSBDemodSettings::deserialize(const QByteArray& data)
|
||||
d.readBool(61, &m_hidden, false);
|
||||
d.readString(62, &m_checkWXAPIKey, "");
|
||||
d.readString(63, &m_mapProvider, "osm");
|
||||
d.readS32(64, &m_aircraftMinZoom, 11);
|
||||
#ifdef LINUX
|
||||
if (m_mapProvider == "osm") {
|
||||
m_mapProvider = "mapboxgl";
|
||||
|
@ -186,6 +186,7 @@ struct ADSBDemodSettings
|
||||
Serializable *m_rollupState;
|
||||
bool m_verboseModelMatching;
|
||||
int m_airfieldElevation; //!< QFE in ft so aircraft takeoff/land from correct position
|
||||
int m_aircraftMinZoom;
|
||||
|
||||
ADSBDemodSettings();
|
||||
void resetToDefaults();
|
||||
|
@ -8,12 +8,14 @@ import QtGraphicalEffects 1.12
|
||||
Item {
|
||||
id: qmlMap
|
||||
property int aircraftZoomLevel: 11
|
||||
property int aircraftMinZoomLevel: 11
|
||||
property int airportZoomLevel: 11
|
||||
property string mapProvider: "osm"
|
||||
property variant mapPtr
|
||||
property string requestedMapType
|
||||
property bool lightIcons
|
||||
property variant guiPtr
|
||||
property bool smoothing
|
||||
|
||||
function createMap(pluginParameters, requestedMap, gui) {
|
||||
requestedMapType = requestedMap
|
||||
@ -103,13 +105,16 @@ Item {
|
||||
}
|
||||
|
||||
onZoomLevelChanged: {
|
||||
if (zoomLevel > aircraftMinZoomLevel) {
|
||||
aircraftZoomLevel = zoomLevel
|
||||
} else {
|
||||
aircraftZoomLevel = aircraftMinZoomLevel
|
||||
}
|
||||
if (zoomLevel > 11) {
|
||||
station.zoomLevel = zoomLevel
|
||||
aircraftZoomLevel = zoomLevel
|
||||
airportZoomLevel = zoomLevel
|
||||
} else {
|
||||
station.zoomLevel = 11
|
||||
aircraftZoomLevel = 11
|
||||
airportZoomLevel = 11
|
||||
}
|
||||
}
|
||||
@ -140,8 +145,8 @@ Item {
|
||||
Grid {
|
||||
horizontalItemAlignment: Grid.AlignHCenter
|
||||
columnSpacing: 5
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
layer.enabled: smoothing
|
||||
layer.smooth: smoothing
|
||||
Image {
|
||||
id: image
|
||||
source: navAidImage
|
||||
@ -206,8 +211,8 @@ Item {
|
||||
sourceItem: Grid {
|
||||
columns: 1
|
||||
Grid {
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
layer.enabled: smoothing
|
||||
layer.smooth: smoothing
|
||||
horizontalItemAlignment: Grid.AlignHCenter
|
||||
Text {
|
||||
id: airspaceText
|
||||
@ -239,8 +244,8 @@ Item {
|
||||
sourceItem: Grid {
|
||||
columns: 1
|
||||
Grid {
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
layer.enabled: smoothing
|
||||
layer.smooth: smoothing
|
||||
horizontalItemAlignment: Grid.AlignHCenter
|
||||
Image {
|
||||
id: image
|
||||
@ -334,8 +339,8 @@ Item {
|
||||
columns: 1
|
||||
Grid {
|
||||
horizontalItemAlignment: Grid.AlignHCenter
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
layer.enabled: smoothing
|
||||
layer.smooth: smoothing
|
||||
Image {
|
||||
id: image
|
||||
source: airportImage
|
||||
|
@ -1,303 +0,0 @@
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// 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/>. //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef INCLUDE_OURAIRPORTSDB_H
|
||||
#define INCLUDE_OURAIRPORTSDB_H
|
||||
|
||||
#include <QString>
|
||||
#include <QFile>
|
||||
#include <QByteArray>
|
||||
#include <QHash>
|
||||
#include <QList>
|
||||
#include <QDebug>
|
||||
#include <QLocale>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "util/csv.h"
|
||||
#include "adsbdemodsettings.h"
|
||||
|
||||
#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 {
|
||||
|
||||
struct FrequencyInformation {
|
||||
QString m_type;
|
||||
QString m_description;
|
||||
float m_frequency; // In MHz
|
||||
};
|
||||
|
||||
int m_id;
|
||||
QString m_ident;
|
||||
ADSBDemodSettings::AirportType m_type;
|
||||
QString m_name;
|
||||
float m_latitude;
|
||||
float m_longitude;
|
||||
float m_elevation;
|
||||
QVector<FrequencyInformation *> m_frequencies;
|
||||
|
||||
~AirportInformation()
|
||||
{
|
||||
qDeleteAll(m_frequencies);
|
||||
}
|
||||
|
||||
static QString trimQuotes(const QString s)
|
||||
{
|
||||
if (s.startsWith('\"') && s.endsWith('\"'))
|
||||
return s.mid(1, s.size() - 2);
|
||||
else
|
||||
return s;
|
||||
}
|
||||
|
||||
// Read OurAirport's airport CSV file
|
||||
// See comments for readOSNDB
|
||||
static QHash<int, AirportInformation *> *readAirportsDB(const QString &filename)
|
||||
{
|
||||
int cnt = 0;
|
||||
QHash<int, AirportInformation *> *airportInfo = nullptr;
|
||||
|
||||
// Column numbers used for the data as of 2020/10/28
|
||||
int idCol = 0;
|
||||
int identCol = 1;
|
||||
int typeCol = 2;
|
||||
int nameCol = 3;
|
||||
int latitudeCol = 4;
|
||||
int longitudeCol = 5;
|
||||
int elevationCol = 6;
|
||||
|
||||
qDebug() << "AirportInformation::readAirportsDB: " << filename;
|
||||
|
||||
FILE *file;
|
||||
QByteArray utfFilename = filename.toUtf8();
|
||||
QLocale cLocale(QLocale::C);
|
||||
if ((file = fopen(utfFilename.constData(), "r")) != NULL)
|
||||
{
|
||||
char row[2048];
|
||||
|
||||
if (fgets(row, sizeof(row), file))
|
||||
{
|
||||
airportInfo = new QHash<int, AirportInformation *>();
|
||||
airportInfo->reserve(70000);
|
||||
|
||||
// Read header
|
||||
int idx = 0;
|
||||
char *p = strtok(row, ",");
|
||||
while (p != NULL)
|
||||
{
|
||||
if (!strcmp(p, "id"))
|
||||
idCol = idx;
|
||||
else if (!strcmp(p, "ident"))
|
||||
identCol = idx;
|
||||
else if (!strcmp(p, "type"))
|
||||
typeCol = idx;
|
||||
else if (!strcmp(p, "name"))
|
||||
nameCol = idx;
|
||||
else if (!strcmp(p, "latitude_deg"))
|
||||
latitudeCol = idx;
|
||||
else if (!strcmp(p, "longitude_deg"))
|
||||
longitudeCol = idx;
|
||||
else if (!strcmp(p, "elevation_ft"))
|
||||
elevationCol = idx;
|
||||
p = strtok(NULL, ",");
|
||||
idx++;
|
||||
}
|
||||
// Read data
|
||||
while (fgets(row, sizeof(row), file))
|
||||
{
|
||||
int id = 0;
|
||||
char *idString = NULL;
|
||||
char *ident = NULL;
|
||||
size_t identLen = 0;
|
||||
char *type = NULL;
|
||||
size_t typeLen = 0;
|
||||
char *name = NULL;
|
||||
size_t nameLen = 0;
|
||||
float latitude = 0.0f;
|
||||
char *latitudeString = NULL;
|
||||
size_t latitudeLen = 0;
|
||||
float longitude = 0.0f;
|
||||
char *longitudeString = NULL;
|
||||
size_t longitudeLen = 0;
|
||||
float elevation = 0.0f;
|
||||
char *elevationString = NULL;
|
||||
size_t elevationLen = 0;
|
||||
|
||||
p = strtok(row, ",");
|
||||
idx = 0;
|
||||
while (p != NULL)
|
||||
{
|
||||
// Read strings, stripping quotes
|
||||
if (idx == idCol)
|
||||
{
|
||||
idString = p;
|
||||
idString[strlen(idString)] = '\0';
|
||||
id = strtol(idString, NULL, 10);
|
||||
}
|
||||
else if (idx == identCol)
|
||||
{
|
||||
ident = p+1;
|
||||
identLen = strlen(ident)-1;
|
||||
ident[identLen] = '\0';
|
||||
}
|
||||
else if (idx == typeCol)
|
||||
{
|
||||
type = p+1;
|
||||
typeLen = strlen(type)-1;
|
||||
type[typeLen] = '\0';
|
||||
}
|
||||
else if (idx == nameCol)
|
||||
{
|
||||
name = p+1;
|
||||
nameLen = strlen(name)-1;
|
||||
name[nameLen] = '\0';
|
||||
}
|
||||
else if (idx == latitudeCol)
|
||||
{
|
||||
latitudeString = p;
|
||||
latitudeLen = strlen(latitudeString)-1;
|
||||
latitudeString[latitudeLen] = '\0';
|
||||
latitude = cLocale.toFloat(latitudeString);
|
||||
}
|
||||
else if (idx == longitudeCol)
|
||||
{
|
||||
longitudeString = p;
|
||||
longitudeLen = strlen(longitudeString)-1;
|
||||
longitudeString[longitudeLen] = '\0';
|
||||
longitude = cLocale.toFloat(longitudeString);
|
||||
}
|
||||
else if (idx == elevationCol)
|
||||
{
|
||||
elevationString = p;
|
||||
elevationLen = strlen(elevationString)-1;
|
||||
elevationString[elevationLen] = '\0';
|
||||
elevation = cLocale.toFloat(elevationString);
|
||||
}
|
||||
p = strtok(NULL, ",");
|
||||
idx++;
|
||||
}
|
||||
|
||||
// Only create the entry if we have some interesting data
|
||||
if (((latitude != 0.0f) || (longitude != 0.0f)) && (type && strcmp(type, "closed")))
|
||||
{
|
||||
AirportInformation *airport = new AirportInformation();
|
||||
airport->m_id = id;
|
||||
airport->m_ident = QString(ident);
|
||||
if (!strcmp(type, "small_airport"))
|
||||
airport->m_type = ADSBDemodSettings::AirportType::Small;
|
||||
else if (!strcmp(type, "medium_airport"))
|
||||
airport->m_type = ADSBDemodSettings::AirportType::Medium;
|
||||
else if (!strcmp(type, "large_airport"))
|
||||
airport->m_type = ADSBDemodSettings::AirportType::Large;
|
||||
else if (!strcmp(type, "heliport"))
|
||||
airport->m_type = ADSBDemodSettings::AirportType::Heliport;
|
||||
airport->m_name = QString(name);
|
||||
airport->m_latitude = latitude;
|
||||
airport->m_longitude = longitude;
|
||||
airport->m_elevation = elevation;
|
||||
airportInfo->insert(id, airport);
|
||||
cnt++;
|
||||
}
|
||||
}
|
||||
}
|
||||
fclose(file);
|
||||
}
|
||||
else
|
||||
qDebug() << "AirportInformation::readAirportsDB: Failed to open " << filename;
|
||||
|
||||
qDebug() << "AirportInformation::readAirportsDB: Read " << cnt << " airports";
|
||||
|
||||
return airportInfo;
|
||||
}
|
||||
|
||||
// Read OurAirport's airport frequencies CSV file
|
||||
static bool readFrequenciesDB(const QString &filename, QHash<int, AirportInformation *> *airportInfo)
|
||||
{
|
||||
int cnt = 0;
|
||||
|
||||
// Column numbers used for the data as of 2020/10/28
|
||||
int airportRefCol = 1;
|
||||
int typeCol = 3;
|
||||
int descriptionCol = 4;
|
||||
int frequencyCol = 5;
|
||||
|
||||
qDebug() << "AirportInformation::readFrequenciesDB: " << filename;
|
||||
|
||||
QFile file(filename);
|
||||
if (file.open(QIODevice::ReadOnly))
|
||||
{
|
||||
QList<QByteArray> colNames;
|
||||
int idx;
|
||||
|
||||
// Read header
|
||||
if (!file.atEnd())
|
||||
{
|
||||
QByteArray row = file.readLine().trimmed();
|
||||
colNames = row.split(',');
|
||||
// Work out which columns the data is in, based on the headers
|
||||
idx = colNames.indexOf("airport_ref");
|
||||
if (idx >= 0)
|
||||
airportRefCol = idx;
|
||||
idx = colNames.indexOf("type");
|
||||
if (idx >= 0)
|
||||
typeCol = idx;
|
||||
idx = colNames.indexOf("descrption");
|
||||
if (idx >= 0)
|
||||
descriptionCol = idx;
|
||||
idx = colNames.indexOf("frequency_mhz");
|
||||
if (idx >= 0)
|
||||
frequencyCol = idx;
|
||||
}
|
||||
// Read data
|
||||
while (!file.atEnd())
|
||||
{
|
||||
QByteArray row = file.readLine();
|
||||
QList<QByteArray> cols = row.split(',');
|
||||
|
||||
bool ok = false;
|
||||
int airportRef = cols[airportRefCol].toInt(&ok, 10);
|
||||
if (ok)
|
||||
{
|
||||
if (airportInfo->contains(airportRef))
|
||||
{
|
||||
QString type = trimQuotes(cols[typeCol]);
|
||||
QString description = trimQuotes(cols[descriptionCol]);
|
||||
float frequency = cols[frequencyCol].toFloat();
|
||||
|
||||
FrequencyInformation *frequencyInfo = new FrequencyInformation();
|
||||
frequencyInfo->m_type = type;
|
||||
frequencyInfo->m_description = description;
|
||||
frequencyInfo->m_frequency = frequency;
|
||||
airportInfo->value(airportRef)->m_frequencies.append(frequencyInfo);
|
||||
cnt++;
|
||||
}
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
else
|
||||
qDebug() << "Failed to open " << filename << " " << file.errorString();
|
||||
|
||||
qDebug() << "AirportInformation::readFrequenciesDB: - read " << cnt << " airports";
|
||||
|
||||
return airportInfo;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
#endif
|
@ -73,6 +73,7 @@ Clicking the Display Settings button will open the Display Settings dialog, whic
|
||||
* The units for altitude, speed and vertical climb rate. These can be either ft (feet), kn (knots) and ft/min (feet per minute), or m (metres), kph (kilometers per hour) and m/s (metres per second).
|
||||
* The map provider. This can be osm for OpenStreetMaps or mapboxgl for Mapbox. mapboxgl is not supported on Windows. mapboxgl should be used on Linux with Qt 5.15.3, as osm maps will cause SDRangel to hang, due to a bug in Qt.
|
||||
* The type of map that will be displayed. This can either be a light or dark aviation map (with no place names to reduce clutter), a street map or satellite imagery.
|
||||
* The minimum zoom level (0 min zoom - 15 max zoom) at which aircraft icons will be scaled.
|
||||
* 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. Displaying too many airports will slow down drawing of the map.
|
||||
|
@ -706,6 +706,13 @@ void APTDemodImageWorker::sendImageToMap(QImage image)
|
||||
swgMapItem->setImageTileNorth(m_tileNorth);
|
||||
swgMapItem->setImageTileSouth(m_tileSouth);
|
||||
|
||||
// FIXME: This isn't correct. Possibly need to use different projection
|
||||
double earthCircumference = 40075016.686;
|
||||
double latitude = m_tileSouth + (m_tileNorth - m_tileSouth) / 2.0;
|
||||
double scale = std::cos(Units::degreesToRadians(latitude));
|
||||
double zoom = std::log2(earthCircumference * scale * selectedChannel.width() / 2926600) - 8;
|
||||
swgMapItem->setImageZoomLevel(zoom);
|
||||
|
||||
MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_aptDemod, swgMapItem);
|
||||
messageQueue->push(msg);
|
||||
}
|
||||
|
@ -51,6 +51,7 @@ MESSAGE_CLASS_DEFINITION(DABDemod::MsgDABFIBQuality, Message)
|
||||
MESSAGE_CLASS_DEFINITION(DABDemod::MsgDABSampleRate, Message)
|
||||
MESSAGE_CLASS_DEFINITION(DABDemod::MsgDABData, Message)
|
||||
MESSAGE_CLASS_DEFINITION(DABDemod::MsgDABMOTData, Message)
|
||||
MESSAGE_CLASS_DEFINITION(DABDemod::MsgDABTII, Message)
|
||||
MESSAGE_CLASS_DEFINITION(DABDemod::MsgDABReset, Message)
|
||||
MESSAGE_CLASS_DEFINITION(DABDemod::MsgDABResetService, Message)
|
||||
|
||||
@ -238,6 +239,7 @@ bool DABDemod::handleMessage(const Message& cmd)
|
||||
{
|
||||
getMessageQueueToGUI()->push(new MsgDABProgramName(report));
|
||||
}
|
||||
m_basebandSink->getInputMessageQueue()->push(new MsgDABProgramName(report));
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -271,6 +273,16 @@ bool DABDemod::handleMessage(const Message& cmd)
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (MsgDABTII::match(cmd))
|
||||
{
|
||||
MsgDABTII& report = (MsgDABTII&)cmd;
|
||||
if (getMessageQueueToGUI())
|
||||
{
|
||||
getMessageQueueToGUI()->push(new MsgDABTII(report));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (MsgDABReset::match(cmd))
|
||||
{
|
||||
MsgDABReset& report = (MsgDABReset&)cmd;
|
||||
|
@ -275,6 +275,26 @@ public:
|
||||
{ }
|
||||
};
|
||||
|
||||
class MsgDABTII : public Message {
|
||||
MESSAGE_CLASS_DECLARATION
|
||||
|
||||
public:
|
||||
int getTII() const { return m_tii; }
|
||||
|
||||
static MsgDABTII* create(int tii)
|
||||
{
|
||||
return new MsgDABTII(tii);
|
||||
}
|
||||
|
||||
private:
|
||||
int m_tii;
|
||||
|
||||
MsgDABTII(int tii) :
|
||||
Message(),
|
||||
m_tii(tii)
|
||||
{ }
|
||||
};
|
||||
|
||||
class MsgDABReset : public Message {
|
||||
MESSAGE_CLASS_DECLARATION
|
||||
|
||||
|
@ -162,6 +162,12 @@ bool DABDemodBaseband::handleMessage(const Message& cmd)
|
||||
m_sink.resetService();
|
||||
return true;
|
||||
}
|
||||
else if (DABDemod::MsgDABProgramName::match(cmd))
|
||||
{
|
||||
DABDemod::MsgDABProgramName& report = (DABDemod::MsgDABProgramName&) cmd;
|
||||
m_sink.programAvailable(report.getName());
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
|
@ -38,6 +38,7 @@
|
||||
#include "gui/crightclickenabler.h"
|
||||
#include "gui/dialogpositioner.h"
|
||||
#include "channel/channelwebapiutils.h"
|
||||
#include "feature/featurewebapiutils.h"
|
||||
#include "maincore.h"
|
||||
|
||||
#include "dabdemod.h"
|
||||
@ -47,6 +48,7 @@
|
||||
#define PROGRAMS_COL_NAME 0
|
||||
#define PROGRAMS_COL_ID 1
|
||||
#define PROGRAMS_COL_FREQUENCY 2
|
||||
#define PROGRAMS_COL_ENSEMBLE 3
|
||||
|
||||
void DABDemodGUI::resizeTable()
|
||||
{
|
||||
@ -57,6 +59,7 @@ void DABDemodGUI::resizeTable()
|
||||
ui->programs->setItem(row, PROGRAMS_COL_NAME, new QTableWidgetItem("Some Random Radio Station"));
|
||||
ui->programs->setItem(row, PROGRAMS_COL_ID, new QTableWidgetItem("123456"));
|
||||
ui->programs->setItem(row, PROGRAMS_COL_FREQUENCY, new QTableWidgetItem("200.000"));
|
||||
ui->programs->setItem(row, PROGRAMS_COL_ENSEMBLE, new QTableWidgetItem("Some random ensemble"));
|
||||
ui->programs->resizeColumnsToContents();
|
||||
ui->programs->removeRow(row);
|
||||
}
|
||||
@ -142,9 +145,26 @@ bool DABDemodGUI::deserialize(const QByteArray& data)
|
||||
}
|
||||
}
|
||||
|
||||
int DABDemodGUI::findProgramRowById(int id)
|
||||
{
|
||||
QString idText = QString::number(id);
|
||||
for (int i = 0; i < ui->programs->rowCount(); i++)
|
||||
{
|
||||
if (ui->programs->item(i, PROGRAMS_COL_ID)->text() == idText) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Add row to table
|
||||
void DABDemodGUI::addProgramName(const DABDemod::MsgDABProgramName& program)
|
||||
{
|
||||
// Don't add duplicate
|
||||
if (findProgramRowById(program.getId()) != -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
ui->programs->setSortingEnabled(false);
|
||||
int row = ui->programs->rowCount();
|
||||
ui->programs->setRowCount(row + 1);
|
||||
@ -152,9 +172,11 @@ void DABDemodGUI::addProgramName(const DABDemod::MsgDABProgramName& program)
|
||||
QTableWidgetItem *nameItem = new QTableWidgetItem();
|
||||
QTableWidgetItem *idItem = new QTableWidgetItem();
|
||||
QTableWidgetItem *frequencyItem = new QTableWidgetItem();
|
||||
QTableWidgetItem *ensembleItem = new QTableWidgetItem();
|
||||
ui->programs->setItem(row, PROGRAMS_COL_NAME, nameItem);
|
||||
ui->programs->setItem(row, PROGRAMS_COL_ID, idItem);
|
||||
ui->programs->setItem(row, PROGRAMS_COL_FREQUENCY, frequencyItem);
|
||||
ui->programs->setItem(row, PROGRAMS_COL_ENSEMBLE, ensembleItem);
|
||||
nameItem->setText(program.getName());
|
||||
idItem->setText(QString::number(program.getId()));
|
||||
double frequencyInHz;
|
||||
@ -165,7 +187,10 @@ void DABDemodGUI::addProgramName(const DABDemod::MsgDABProgramName& program)
|
||||
frequencyItem->setData(Qt::UserRole, frequencyInHz+m_settings.m_inputFrequencyOffset);
|
||||
}
|
||||
else
|
||||
{
|
||||
frequencyItem->setData(Qt::UserRole, 0.0);
|
||||
}
|
||||
ensembleItem->setText(ui->ensemble->text());
|
||||
ui->programs->setSortingEnabled(true);
|
||||
filterRow(row);
|
||||
}
|
||||
@ -178,13 +203,29 @@ void DABDemodGUI::on_programs_cellDoubleClicked(int row, int column)
|
||||
m_settings.m_program = ui->programs->item(row, PROGRAMS_COL_NAME)->text();
|
||||
|
||||
double frequencyInHz = ui->programs->item(row, PROGRAMS_COL_FREQUENCY)->data(Qt::UserRole).toDouble();
|
||||
ChannelWebAPIUtils::setCenterFrequency(m_dabDemod->getDeviceSetIndex(), frequencyInHz-m_settings.m_inputFrequencyOffset);
|
||||
|
||||
double centreFreq = frequencyInHz-m_settings.m_inputFrequencyOffset;
|
||||
ChannelWebAPIUtils::setCenterFrequency(m_dabDemod->getDeviceSetIndex(), centreFreq);
|
||||
clearProgram();
|
||||
|
||||
applySettings();
|
||||
}
|
||||
|
||||
// Ensemble name can sometimes be decoded after program name, so we
|
||||
// need to update entries in the table where ensemble name is "-"
|
||||
void DABDemodGUI::updateEnsembleName(const QString& ensemble)
|
||||
{
|
||||
double frequencyInHz = m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset;
|
||||
for (int i = 0; i < ui->programs->rowCount(); i++)
|
||||
{
|
||||
if (ui->programs->item(i, PROGRAMS_COL_ENSEMBLE)->text() == "-")
|
||||
{
|
||||
if (ui->programs->item(i, PROGRAMS_COL_FREQUENCY)->data(Qt::UserRole).toDouble() == frequencyInHz)
|
||||
{
|
||||
ui->programs->item(i, PROGRAMS_COL_ENSEMBLE)->setText(ensemble);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool DABDemodGUI::handleMessage(const Message& message)
|
||||
{
|
||||
if (DABDemod::MsgConfigureDABDemod::match(message))
|
||||
@ -201,6 +242,11 @@ bool DABDemodGUI::handleMessage(const Message& message)
|
||||
else if (DSPSignalNotification::match(message))
|
||||
{
|
||||
DSPSignalNotification& notif = (DSPSignalNotification&) message;
|
||||
if (m_deviceCenterFrequency != notif.getCenterFrequency())
|
||||
{
|
||||
// Reset on frequency change, to get new ensemble name
|
||||
resetService();
|
||||
}
|
||||
m_deviceCenterFrequency = notif.getCenterFrequency();
|
||||
m_basebandSampleRate = notif.getSampleRate();
|
||||
ui->deltaFrequency->setValueRange(false, 7, -m_basebandSampleRate/2, m_basebandSampleRate/2);
|
||||
@ -223,6 +269,7 @@ bool DABDemodGUI::handleMessage(const Message& message)
|
||||
{
|
||||
DABDemod::MsgDABEnsembleName& report = (DABDemod::MsgDABEnsembleName&) message;
|
||||
ui->ensemble->setText(report.getName());
|
||||
updateEnsembleName(report.getName());
|
||||
return true;
|
||||
}
|
||||
else if (DABDemod::MsgDABProgramName::match(message))
|
||||
@ -293,7 +340,14 @@ bool DABDemodGUI::handleMessage(const Message& message)
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
else if (DABDemod::MsgDABTII::match(message))
|
||||
{
|
||||
DABDemod::MsgDABTII& report = (DABDemod::MsgDABTII&) message;
|
||||
int tii = report.getTII();
|
||||
ui->tiiMainId->setText(QStringLiteral("%1").arg((tii >> 8) & 0xff, 2, 16, QLatin1Char('0')).toUpper());
|
||||
ui->tiiSubId->setText(QStringLiteral("%1").arg(tii & 0xff, 2, 16, QLatin1Char('0')).toUpper());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -607,7 +661,6 @@ void DABDemodGUI::clearProgram()
|
||||
{
|
||||
// Clear current program
|
||||
ui->program->setText("-");
|
||||
ui->ensemble->setText("-");
|
||||
ui->programType->setText("-");
|
||||
ui->language->setText("-");
|
||||
ui->audio->setText("-");
|
||||
@ -621,10 +674,13 @@ void DABDemodGUI::clearProgram()
|
||||
|
||||
void DABDemodGUI::resetService()
|
||||
{
|
||||
// Reset DAB audio service, to avoid unpleasent noise when changing frequency
|
||||
DABDemod::MsgDABResetService* message = DABDemod::MsgDABResetService::create();
|
||||
m_dabDemod->getInputMessageQueue()->push(message);
|
||||
ui->ensemble->setText("-");
|
||||
ui->tiiMainId->setText("-");
|
||||
ui->tiiSubId->setText("-");
|
||||
clearProgram();
|
||||
// Reset DAB audio service, so we get new ensemble name
|
||||
DABDemod::MsgDABReset* message = DABDemod::MsgDABReset::create();
|
||||
m_dabDemod->getInputMessageQueue()->push(message);
|
||||
}
|
||||
|
||||
void DABDemodGUI::on_channel_currentIndexChanged(int index)
|
||||
@ -634,7 +690,6 @@ void DABDemodGUI::on_channel_currentIndexChanged(int index)
|
||||
QString text = ui->channel->currentText();
|
||||
if (!text.isEmpty())
|
||||
{
|
||||
resetService();
|
||||
// Tune to requested channel
|
||||
QString freq = text.split(" ")[2];
|
||||
m_channelFreq = freq.toDouble() * 1e6;
|
||||
@ -715,9 +770,30 @@ void DABDemodGUI::makeUIConnections()
|
||||
QObject::connect(ui->clearTable, &QPushButton::clicked, this, &DABDemodGUI::on_clearTable_clicked);
|
||||
QObject::connect(ui->programs, &QTableWidget::cellDoubleClicked, this, &DABDemodGUI::on_programs_cellDoubleClicked);
|
||||
QObject::connect(ui->channel, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &DABDemodGUI::on_channel_currentIndexChanged);
|
||||
QObject::connect(ui->findOnMap, &QToolButton::clicked, this, &DABDemodGUI::on_findOnMap_clicked);
|
||||
}
|
||||
|
||||
void DABDemodGUI::updateAbsoluteCenterFrequency()
|
||||
{
|
||||
setStatusFrequency(m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset);
|
||||
}
|
||||
|
||||
void DABDemodGUI::on_findOnMap_clicked()
|
||||
{
|
||||
QString mainID = ui->tiiMainId->text();
|
||||
if (mainID.isEmpty() || (mainID == "-")) {
|
||||
return;
|
||||
}
|
||||
QString subID = ui->tiiSubId->text();
|
||||
if (subID.isEmpty() || (subID == "-")) {
|
||||
return;
|
||||
}
|
||||
QString ensemble = ui->ensemble->text().trimmed();
|
||||
if (ensemble.isEmpty() || (ensemble == "-")) {
|
||||
return;
|
||||
}
|
||||
QString id = ensemble + " " + mainID + subID;
|
||||
qDebug() << "Finding " << id;
|
||||
FeatureWebAPIUtils::mapFind(id);
|
||||
}
|
||||
|
||||
|
@ -93,6 +93,8 @@ private:
|
||||
void blockApplySettings(bool block);
|
||||
void applySettings(bool force = false);
|
||||
void displaySettings();
|
||||
void updateEnsembleName(const QString& ensemble);
|
||||
int findProgramRowById(int id);
|
||||
void addProgramName(const DABDemod::MsgDABProgramName& program);
|
||||
bool handleMessage(const Message& message);
|
||||
void makeUIConnections();
|
||||
@ -115,6 +117,7 @@ private slots:
|
||||
void on_clearTable_clicked();
|
||||
void on_programs_cellDoubleClicked(int row, int column);
|
||||
void on_channel_currentIndexChanged(int index);
|
||||
void on_findOnMap_clicked();
|
||||
void filterRow(int row);
|
||||
void filter();
|
||||
void programs_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex);
|
||||
|
@ -7,7 +7,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>398</width>
|
||||
<height>612</height>
|
||||
<height>657</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
@ -673,6 +673,11 @@
|
||||
<string>Frequency</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Ensemble</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
@ -1006,6 +1011,106 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="transmitter" native="true">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>610</y>
|
||||
<width>361</width>
|
||||
<height>40</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Transmitter</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<property name="spacing">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QGridLayout" name="transmitterLayout">
|
||||
<item row="0" column="2">
|
||||
<widget class="QLabel" name="tiiSubIdLabel">
|
||||
<property name="text">
|
||||
<string>TII Sub ID:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLabel" name="tiiMainId">
|
||||
<property name="toolTip">
|
||||
<string>Transmitter Identifier Information</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>-</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="tiiMainIdLabel">
|
||||
<property name="text">
|
||||
<string>TII Main ID:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="3">
|
||||
<widget class="QWidget" name="widget" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="tiiSubId">
|
||||
<property name="toolTip">
|
||||
<string>Transmitter Identification Information</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>-</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="findOnMap">
|
||||
<property name="toolTip">
|
||||
<string>Find transmitter on map</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../../sdrgui/resources/res.qrc">
|
||||
<normaloff>:/gridpolar.png</normaloff>:/gridpolar.png</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
|
@ -279,6 +279,14 @@ void motDataHandler(uint8_t *data, int len, const char *filename, int contentsub
|
||||
sink->motData(data, len, QString::fromUtf8(filename), contentsubType);
|
||||
}
|
||||
|
||||
// Missing ctx for tiiDataHandler - https://github.com/JvanKatwijk/dab-cmdline/issues/89
|
||||
DABDemodSink *tiiSink;
|
||||
|
||||
void tiiDataHandler(int tii)
|
||||
{
|
||||
tiiSink->tii(tii);
|
||||
}
|
||||
|
||||
void DABDemodSink::systemData(bool sync, int16_t snr, int32_t freqOffset)
|
||||
{
|
||||
if (getMessageQueueToChannel())
|
||||
@ -287,6 +295,7 @@ void DABDemodSink::systemData(bool sync, int16_t snr, int32_t freqOffset)
|
||||
getMessageQueueToChannel()->push(msg);
|
||||
}
|
||||
}
|
||||
|
||||
void DABDemodSink::ensembleName(const QString& name, int id)
|
||||
{
|
||||
if (getMessageQueueToChannel())
|
||||
@ -351,6 +360,15 @@ void DABDemodSink::motData(const uint8_t *data, int len, const QString& filename
|
||||
}
|
||||
}
|
||||
|
||||
void DABDemodSink::tii(int tii)
|
||||
{
|
||||
if (getMessageQueueToChannel())
|
||||
{
|
||||
DABDemod::MsgDABTII *msg = DABDemod::MsgDABTII::create(tii);
|
||||
getMessageQueueToChannel()->push(msg);
|
||||
}
|
||||
}
|
||||
|
||||
static int16_t scale(int16_t sample, float factor)
|
||||
{
|
||||
int32_t prod = (int32_t)(((int32_t)sample) * factor);
|
||||
@ -473,6 +491,7 @@ DABDemodSink::DABDemodSink(DABDemod *packetDemod) :
|
||||
m_dabAudioSampleRate(10000), // Unused value to begin with
|
||||
m_channelSampleRate(DABDEMOD_CHANNEL_SAMPLE_RATE),
|
||||
m_channelFrequencyOffset(0),
|
||||
m_programSet(false),
|
||||
m_magsqSum(0.0f),
|
||||
m_magsqPeak(0.0f),
|
||||
m_magsqCount(0),
|
||||
@ -502,8 +521,9 @@ DABDemodSink::DABDemodSink(DABDemod *packetDemod) :
|
||||
m_api.programdata_Handler = programDataHandler;
|
||||
m_api.program_quality_Handler = programQualityHandler;
|
||||
m_api.motdata_Handler = motDataHandler;
|
||||
m_api.tii_data_Handler = nullptr;
|
||||
m_api.tii_data_Handler = tiiDataHandler;
|
||||
m_api.timeHandler = nullptr;
|
||||
tiiSink = this;
|
||||
m_dab = dabInit(&m_device,
|
||||
&m_api,
|
||||
nullptr,
|
||||
@ -607,29 +627,49 @@ void DABDemodSink::applySettings(const DABDemodSettings& settings, bool force)
|
||||
|
||||
if ((settings.m_program != m_settings.m_program) || force)
|
||||
{
|
||||
if (!settings.m_program.isEmpty())
|
||||
{
|
||||
QByteArray ba = settings.m_program.toUtf8();
|
||||
const char *program = ba.data();
|
||||
if (!is_audioService (m_dab, program))
|
||||
qWarning() << settings.m_program << " is not an audio service";
|
||||
else
|
||||
{
|
||||
dataforAudioService(m_dab, program, &m_ad, 0);
|
||||
if (!m_ad.defined)
|
||||
qWarning() << settings.m_program << " audio data is not defined";
|
||||
else
|
||||
{
|
||||
dabReset_msc(m_dab);
|
||||
set_audioChannel(m_dab, &m_ad);
|
||||
}
|
||||
}
|
||||
if (!settings.m_program.isEmpty()) {
|
||||
setProgram(settings.m_program);
|
||||
} else {
|
||||
m_programSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
m_settings = settings;
|
||||
}
|
||||
|
||||
// Can't call setProgram directly from callback, so we get here via a message
|
||||
void DABDemodSink::programAvailable(const QString& programName)
|
||||
{
|
||||
if (!m_programSet && (programName == m_settings.m_program)) {
|
||||
setProgram(m_settings.m_program);
|
||||
}
|
||||
}
|
||||
|
||||
void DABDemodSink::setProgram(const QString& name)
|
||||
{
|
||||
m_programSet = false;
|
||||
QByteArray ba = name.toUtf8();
|
||||
const char *program = ba.data();
|
||||
if (!is_audioService (m_dab, program))
|
||||
{
|
||||
qWarning() << name << " is not an audio service";
|
||||
}
|
||||
else
|
||||
{
|
||||
dataforAudioService(m_dab, program, &m_ad, 0);
|
||||
if (!m_ad.defined)
|
||||
{
|
||||
qWarning() << name << " audio data is not defined";
|
||||
}
|
||||
else
|
||||
{
|
||||
dabReset_msc(m_dab);
|
||||
set_audioChannel(m_dab, &m_ad);
|
||||
m_programSet = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Called when audio device sample rate changes
|
||||
void DABDemodSink::applyAudioSampleRate(int sampleRate)
|
||||
{
|
||||
|
@ -80,6 +80,7 @@ public:
|
||||
|
||||
void reset();
|
||||
void resetService();
|
||||
void programAvailable(const QString& programName);
|
||||
|
||||
// Callbacks
|
||||
void systemData(bool sync, int16_t snr, int32_t freqOffset);
|
||||
@ -91,6 +92,7 @@ public:
|
||||
void fibQuality(int16_t percent);
|
||||
void data(const QString& data);
|
||||
void motData(const uint8_t *data, int len, const QString& filename, int contentSubType);
|
||||
void tii(int tii);
|
||||
|
||||
private:
|
||||
struct MagSqLevelsStore
|
||||
@ -116,6 +118,7 @@ private:
|
||||
DABDemodDevice m_device;
|
||||
audiodata m_ad;
|
||||
API_struct m_api;
|
||||
bool m_programSet;
|
||||
|
||||
NCO m_nco;
|
||||
Interpolator m_interpolator;
|
||||
@ -145,6 +148,7 @@ private:
|
||||
void processOneSample(Complex &ci);
|
||||
void processOneAudioSample(Complex &ci);
|
||||
MessageQueue *getMessageQueueToChannel() { return m_messageQueueToChannel; }
|
||||
void setProgram(const QString& name);
|
||||
};
|
||||
|
||||
#endif // INCLUDE_DABDEMODSINK_H
|
||||
|
68
plugins/channelrx/heatmap/CMakeLists.txt
Normal file
@ -0,0 +1,68 @@
|
||||
project(heatmap)
|
||||
|
||||
set(heatmap_SOURCES
|
||||
heatmap.cpp
|
||||
heatmapsettings.cpp
|
||||
heatmapbaseband.cpp
|
||||
heatmapsink.cpp
|
||||
heatmapplugin.cpp
|
||||
heatmapwebapiadapter.cpp
|
||||
)
|
||||
|
||||
set(heatmap_HEADERS
|
||||
heatmap.h
|
||||
heatmapsettings.h
|
||||
heatmapbaseband.h
|
||||
heatmapsink.h
|
||||
heatmapplugin.h
|
||||
heatmapwebapiadapter.h
|
||||
)
|
||||
|
||||
include_directories(
|
||||
${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client
|
||||
)
|
||||
|
||||
if(NOT SERVER_MODE)
|
||||
set(heatmap_SOURCES
|
||||
${heatmap_SOURCES}
|
||||
heatmapgui.cpp
|
||||
heatmapgui.ui
|
||||
)
|
||||
set(heatmap_HEADERS
|
||||
${heatmap_HEADERS}
|
||||
heatmapgui.h
|
||||
)
|
||||
|
||||
set(TARGET_NAME heatmap)
|
||||
set(TARGET_LIB "Qt::Widgets" Qt::Charts)
|
||||
set(TARGET_LIB_GUI "sdrgui")
|
||||
set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR})
|
||||
else()
|
||||
set(TARGET_NAME heatmapsrv)
|
||||
set(TARGET_LIB "")
|
||||
set(TARGET_LIB_GUI "")
|
||||
set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR})
|
||||
endif()
|
||||
|
||||
add_library(${TARGET_NAME} SHARED
|
||||
${heatmap_SOURCES}
|
||||
)
|
||||
|
||||
target_link_libraries(${TARGET_NAME}
|
||||
Qt::Core
|
||||
${TARGET_LIB}
|
||||
sdrbase
|
||||
${TARGET_LIB_GUI}
|
||||
)
|
||||
|
||||
install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER})
|
||||
|
||||
# Install debug symbols
|
||||
if (WIN32)
|
||||
install(FILES $<TARGET_PDB_FILE:${TARGET_NAME}> CONFIGURATIONS Debug RelWithDebInfo DESTINATION ${INSTALL_FOLDER} )
|
||||
endif()
|
||||
|
||||
# Install debug symbols
|
||||
if (WIN32)
|
||||
install(FILES $<TARGET_PDB_FILE:${TARGET_NAME}> CONFIGURATIONS Debug RelWithDebInfo DESTINATION ${INSTALL_FOLDER} )
|
||||
endif()
|
578
plugins/channelrx/heatmap/heatmap.cpp
Normal file
@ -0,0 +1,578 @@
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (C) 2015-2018 Edouard Griffiths, F4EXB. //
|
||||
// Copyright (C) 2023 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 "heatmap.h"
|
||||
|
||||
#include <QTime>
|
||||
#include <QDebug>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QBuffer>
|
||||
#include <QThread>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <complex.h>
|
||||
|
||||
#include "SWGChannelSettings.h"
|
||||
#include "SWGWorkspaceInfo.h"
|
||||
#include "SWGChannelReport.h"
|
||||
|
||||
#include "dsp/dspengine.h"
|
||||
#include "dsp/dspcommands.h"
|
||||
#include "device/deviceapi.h"
|
||||
#include "feature/feature.h"
|
||||
#include "util/db.h"
|
||||
#include "maincore.h"
|
||||
|
||||
MESSAGE_CLASS_DEFINITION(HeatMap::MsgConfigureHeatMap, Message)
|
||||
|
||||
const char * const HeatMap::m_channelIdURI = "sdrangel.channel.heatmap";
|
||||
const char * const HeatMap::m_channelId = "HeatMap";
|
||||
|
||||
HeatMap::HeatMap(DeviceAPI *deviceAPI) :
|
||||
ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSink),
|
||||
m_deviceAPI(deviceAPI),
|
||||
m_basebandSampleRate(0)
|
||||
{
|
||||
setObjectName(m_channelId);
|
||||
|
||||
m_basebandSink = new HeatMapBaseband(this);
|
||||
m_basebandSink->setMessageQueueToChannel(getInputMessageQueue());
|
||||
m_basebandSink->setChannel(this);
|
||||
m_basebandSink->moveToThread(&m_thread);
|
||||
|
||||
applySettings(m_settings, true);
|
||||
|
||||
m_deviceAPI->addChannelSink(this);
|
||||
m_deviceAPI->addChannelSinkAPI(this);
|
||||
|
||||
m_networkManager = new QNetworkAccessManager();
|
||||
QObject::connect(
|
||||
m_networkManager,
|
||||
&QNetworkAccessManager::finished,
|
||||
this,
|
||||
&HeatMap::networkManagerFinished
|
||||
);
|
||||
QObject::connect(
|
||||
this,
|
||||
&ChannelAPI::indexInDeviceSetChanged,
|
||||
this,
|
||||
&HeatMap::handleIndexInDeviceSetChanged
|
||||
);
|
||||
}
|
||||
|
||||
HeatMap::~HeatMap()
|
||||
{
|
||||
qDebug("HeatMap::~HeatMap");
|
||||
QObject::disconnect(
|
||||
m_networkManager,
|
||||
&QNetworkAccessManager::finished,
|
||||
this,
|
||||
&HeatMap::networkManagerFinished
|
||||
);
|
||||
delete m_networkManager;
|
||||
m_deviceAPI->removeChannelSinkAPI(this);
|
||||
m_deviceAPI->removeChannelSink(this);
|
||||
|
||||
if (m_basebandSink->isRunning()) {
|
||||
stop();
|
||||
}
|
||||
|
||||
delete m_basebandSink;
|
||||
}
|
||||
|
||||
void HeatMap::setDeviceAPI(DeviceAPI *deviceAPI)
|
||||
{
|
||||
if (deviceAPI != m_deviceAPI)
|
||||
{
|
||||
m_deviceAPI->removeChannelSinkAPI(this);
|
||||
m_deviceAPI->removeChannelSink(this);
|
||||
m_deviceAPI = deviceAPI;
|
||||
m_deviceAPI->addChannelSink(this);
|
||||
m_deviceAPI->addChannelSinkAPI(this);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t HeatMap::getNumberOfDeviceStreams() const
|
||||
{
|
||||
return m_deviceAPI->getNbSourceStreams();
|
||||
}
|
||||
|
||||
void HeatMap::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool firstOfBurst)
|
||||
{
|
||||
(void) firstOfBurst;
|
||||
m_basebandSink->feed(begin, end);
|
||||
}
|
||||
|
||||
void HeatMap::start()
|
||||
{
|
||||
qDebug("HeatMap::start");
|
||||
|
||||
m_basebandSink->reset();
|
||||
m_basebandSink->startWork();
|
||||
m_thread.start();
|
||||
|
||||
DSPSignalNotification *dspMsg = new DSPSignalNotification(m_basebandSampleRate, m_centerFrequency);
|
||||
m_basebandSink->getInputMessageQueue()->push(dspMsg);
|
||||
|
||||
HeatMapBaseband::MsgConfigureHeatMapBaseband *msg = HeatMapBaseband::MsgConfigureHeatMapBaseband::create(m_settings, true);
|
||||
m_basebandSink->getInputMessageQueue()->push(msg);
|
||||
}
|
||||
|
||||
void HeatMap::stop()
|
||||
{
|
||||
qDebug("HeatMap::stop");
|
||||
m_basebandSink->stopWork();
|
||||
m_thread.quit();
|
||||
m_thread.wait();
|
||||
}
|
||||
|
||||
bool HeatMap::handleMessage(const Message& cmd)
|
||||
{
|
||||
if (MsgConfigureHeatMap::match(cmd))
|
||||
{
|
||||
MsgConfigureHeatMap& cfg = (MsgConfigureHeatMap&) cmd;
|
||||
qDebug() << "HeatMap::handleMessage: MsgConfigureHeatMap";
|
||||
applySettings(cfg.getSettings(), cfg.getForce());
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (DSPSignalNotification::match(cmd))
|
||||
{
|
||||
DSPSignalNotification& notif = (DSPSignalNotification&) cmd;
|
||||
m_basebandSampleRate = notif.getSampleRate();
|
||||
m_centerFrequency = notif.getCenterFrequency();
|
||||
// Forward to the sink
|
||||
DSPSignalNotification* rep = new DSPSignalNotification(notif); // make a copy
|
||||
qDebug() << "HeatMap::handleMessage: DSPSignalNotification";
|
||||
m_basebandSink->getInputMessageQueue()->push(rep);
|
||||
|
||||
if (getMessageQueueToGUI()) {
|
||||
getMessageQueueToGUI()->push(new DSPSignalNotification(notif));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ScopeVis *HeatMap::getScopeSink()
|
||||
{
|
||||
return m_basebandSink->getScopeSink();
|
||||
}
|
||||
|
||||
void HeatMap::setCenterFrequency(qint64 frequency)
|
||||
{
|
||||
HeatMapSettings settings = m_settings;
|
||||
settings.m_inputFrequencyOffset = frequency;
|
||||
applySettings(settings, false);
|
||||
|
||||
if (m_guiMessageQueue) // forward to GUI if any
|
||||
{
|
||||
MsgConfigureHeatMap *msgToGUI = MsgConfigureHeatMap::create(settings, false);
|
||||
m_guiMessageQueue->push(msgToGUI);
|
||||
}
|
||||
}
|
||||
|
||||
void HeatMap::applySettings(const HeatMapSettings& settings, bool force)
|
||||
{
|
||||
qDebug() << "HeatMap::applySettings:"
|
||||
<< " m_streamIndex: " << settings.m_streamIndex
|
||||
<< " m_useReverseAPI: " << settings.m_useReverseAPI
|
||||
<< " m_reverseAPIAddress: " << settings.m_reverseAPIAddress
|
||||
<< " m_reverseAPIPort: " << settings.m_reverseAPIPort
|
||||
<< " m_reverseAPIDeviceIndex: " << settings.m_reverseAPIDeviceIndex
|
||||
<< " m_reverseAPIChannelIndex: " << settings.m_reverseAPIChannelIndex
|
||||
<< " force: " << force;
|
||||
|
||||
QList<QString> reverseAPIKeys;
|
||||
|
||||
if ((settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force) {
|
||||
reverseAPIKeys.append("inputFrequencyOffset");
|
||||
}
|
||||
if ((settings.m_rfBandwidth != m_settings.m_rfBandwidth) || force) {
|
||||
reverseAPIKeys.append("rfBandwidth");
|
||||
}
|
||||
if (m_settings.m_streamIndex != settings.m_streamIndex)
|
||||
{
|
||||
if (m_deviceAPI->getSampleMIMO()) // change of stream is possible for MIMO devices only
|
||||
{
|
||||
m_deviceAPI->removeChannelSinkAPI(this);
|
||||
m_deviceAPI->removeChannelSink(this, m_settings.m_streamIndex);
|
||||
m_deviceAPI->addChannelSink(this, settings.m_streamIndex);
|
||||
m_deviceAPI->addChannelSinkAPI(this);
|
||||
}
|
||||
|
||||
reverseAPIKeys.append("streamIndex");
|
||||
}
|
||||
|
||||
HeatMapBaseband::MsgConfigureHeatMapBaseband *msg = HeatMapBaseband::MsgConfigureHeatMapBaseband::create(settings, force);
|
||||
m_basebandSink->getInputMessageQueue()->push(msg);
|
||||
|
||||
if (settings.m_useReverseAPI)
|
||||
{
|
||||
bool fullUpdate = ((m_settings.m_useReverseAPI != settings.m_useReverseAPI) && settings.m_useReverseAPI) ||
|
||||
(m_settings.m_reverseAPIAddress != settings.m_reverseAPIAddress) ||
|
||||
(m_settings.m_reverseAPIPort != settings.m_reverseAPIPort) ||
|
||||
(m_settings.m_reverseAPIDeviceIndex != settings.m_reverseAPIDeviceIndex) ||
|
||||
(m_settings.m_reverseAPIChannelIndex != settings.m_reverseAPIChannelIndex);
|
||||
webapiReverseSendSettings(reverseAPIKeys, settings, fullUpdate || force);
|
||||
}
|
||||
|
||||
m_settings = settings;
|
||||
}
|
||||
|
||||
QByteArray HeatMap::serialize() const
|
||||
{
|
||||
return m_settings.serialize();
|
||||
}
|
||||
|
||||
bool HeatMap::deserialize(const QByteArray& data)
|
||||
{
|
||||
if (m_settings.deserialize(data))
|
||||
{
|
||||
MsgConfigureHeatMap *msg = MsgConfigureHeatMap::create(m_settings, true);
|
||||
m_inputMessageQueue.push(msg);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_settings.resetToDefaults();
|
||||
MsgConfigureHeatMap *msg = MsgConfigureHeatMap::create(m_settings, true);
|
||||
m_inputMessageQueue.push(msg);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int HeatMap::webapiSettingsGet(
|
||||
SWGSDRangel::SWGChannelSettings& response,
|
||||
QString& errorMessage)
|
||||
{
|
||||
(void) errorMessage;
|
||||
response.setHeatMapSettings(new SWGSDRangel::SWGHeatMapSettings());
|
||||
response.getHeatMapSettings()->init();
|
||||
webapiFormatChannelSettings(response, m_settings);
|
||||
return 200;
|
||||
}
|
||||
|
||||
int HeatMap::webapiWorkspaceGet(
|
||||
SWGSDRangel::SWGWorkspaceInfo& response,
|
||||
QString& errorMessage)
|
||||
{
|
||||
(void) errorMessage;
|
||||
response.setIndex(m_settings.m_workspaceIndex);
|
||||
return 200;
|
||||
}
|
||||
|
||||
int HeatMap::webapiSettingsPutPatch(
|
||||
bool force,
|
||||
const QStringList& channelSettingsKeys,
|
||||
SWGSDRangel::SWGChannelSettings& response,
|
||||
QString& errorMessage)
|
||||
{
|
||||
(void) errorMessage;
|
||||
HeatMapSettings settings = m_settings;
|
||||
webapiUpdateChannelSettings(settings, channelSettingsKeys, response);
|
||||
|
||||
MsgConfigureHeatMap *msg = MsgConfigureHeatMap::create(settings, force);
|
||||
m_inputMessageQueue.push(msg);
|
||||
|
||||
qDebug("HeatMap::webapiSettingsPutPatch: forward to GUI: %p", m_guiMessageQueue);
|
||||
if (m_guiMessageQueue) // forward to GUI if any
|
||||
{
|
||||
MsgConfigureHeatMap *msgToGUI = MsgConfigureHeatMap::create(settings, force);
|
||||
m_guiMessageQueue->push(msgToGUI);
|
||||
}
|
||||
|
||||
webapiFormatChannelSettings(response, settings);
|
||||
|
||||
return 200;
|
||||
}
|
||||
|
||||
void HeatMap::webapiUpdateChannelSettings(
|
||||
HeatMapSettings& settings,
|
||||
const QStringList& channelSettingsKeys,
|
||||
SWGSDRangel::SWGChannelSettings& response)
|
||||
{
|
||||
if (channelSettingsKeys.contains("inputFrequencyOffset")) {
|
||||
settings.m_inputFrequencyOffset = response.getHeatMapSettings()->getInputFrequencyOffset();
|
||||
}
|
||||
if (channelSettingsKeys.contains("rfBandwidth")) {
|
||||
settings.m_rfBandwidth = response.getHeatMapSettings()->getRfBandwidth();
|
||||
}
|
||||
if (channelSettingsKeys.contains("minPower")) {
|
||||
settings.m_minPower = response.getHeatMapSettings()->getMinPower();
|
||||
}
|
||||
if (channelSettingsKeys.contains("maxPower")) {
|
||||
settings.m_maxPower = response.getHeatMapSettings()->getMaxPower();
|
||||
}
|
||||
if (channelSettingsKeys.contains("colorMapName")) {
|
||||
settings.m_colorMapName = *response.getHeatMapSettings()->getColorMapName();
|
||||
}
|
||||
if (channelSettingsKeys.contains("mode")) {
|
||||
settings.m_mode = (HeatMapSettings::Mode)response.getHeatMapSettings()->getMode();
|
||||
}
|
||||
if (channelSettingsKeys.contains("pulseThreshold")) {
|
||||
settings.m_pulseThreshold = response.getHeatMapSettings()->getPulseThreshold();
|
||||
}
|
||||
if (channelSettingsKeys.contains("averagePeriodUS")) {
|
||||
settings.m_averagePeriodUS = response.getHeatMapSettings()->getAveragePeriodUs();
|
||||
}
|
||||
if (channelSettingsKeys.contains("sampleRate")) {
|
||||
settings.m_sampleRate = response.getHeatMapSettings()->getSampleRate();
|
||||
}
|
||||
if (channelSettingsKeys.contains("rgbColor")) {
|
||||
settings.m_rgbColor = response.getHeatMapSettings()->getRgbColor();
|
||||
}
|
||||
if (channelSettingsKeys.contains("title")) {
|
||||
settings.m_title = *response.getHeatMapSettings()->getTitle();
|
||||
}
|
||||
if (channelSettingsKeys.contains("streamIndex")) {
|
||||
settings.m_streamIndex = response.getHeatMapSettings()->getStreamIndex();
|
||||
}
|
||||
if (channelSettingsKeys.contains("useReverseAPI")) {
|
||||
settings.m_useReverseAPI = response.getHeatMapSettings()->getUseReverseApi() != 0;
|
||||
}
|
||||
if (channelSettingsKeys.contains("reverseAPIAddress")) {
|
||||
settings.m_reverseAPIAddress = *response.getHeatMapSettings()->getReverseApiAddress();
|
||||
}
|
||||
if (channelSettingsKeys.contains("reverseAPIPort")) {
|
||||
settings.m_reverseAPIPort = response.getHeatMapSettings()->getReverseApiPort();
|
||||
}
|
||||
if (channelSettingsKeys.contains("reverseAPIDeviceIndex")) {
|
||||
settings.m_reverseAPIDeviceIndex = response.getHeatMapSettings()->getReverseApiDeviceIndex();
|
||||
}
|
||||
if (channelSettingsKeys.contains("reverseAPIChannelIndex")) {
|
||||
settings.m_reverseAPIChannelIndex = response.getHeatMapSettings()->getReverseApiChannelIndex();
|
||||
}
|
||||
if (settings.m_scopeGUI && channelSettingsKeys.contains("scopeConfig")) {
|
||||
settings.m_scopeGUI->updateFrom(channelSettingsKeys, response.getHeatMapSettings()->getScopeConfig());
|
||||
}
|
||||
if (settings.m_channelMarker && channelSettingsKeys.contains("channelMarker")) {
|
||||
settings.m_channelMarker->updateFrom(channelSettingsKeys, response.getHeatMapSettings()->getChannelMarker());
|
||||
}
|
||||
if (settings.m_rollupState && channelSettingsKeys.contains("rollupState")) {
|
||||
settings.m_rollupState->updateFrom(channelSettingsKeys, response.getHeatMapSettings()->getRollupState());
|
||||
}
|
||||
}
|
||||
|
||||
void HeatMap::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& response, const HeatMapSettings& settings)
|
||||
{
|
||||
response.getHeatMapSettings()->setInputFrequencyOffset(settings.m_inputFrequencyOffset);
|
||||
response.getHeatMapSettings()->setRfBandwidth(settings.m_rfBandwidth);
|
||||
response.getHeatMapSettings()->setMinPower(settings.m_minPower);
|
||||
response.getHeatMapSettings()->setMaxPower(settings.m_maxPower);
|
||||
response.getHeatMapSettings()->setColorMapName(new QString(settings.m_colorMapName));
|
||||
response.getHeatMapSettings()->setMode((int) settings.m_mode);
|
||||
response.getHeatMapSettings()->setPulseThreshold(settings.m_pulseThreshold);
|
||||
response.getHeatMapSettings()->setAveragePeriodUs(settings.m_averagePeriodUS);
|
||||
response.getHeatMapSettings()->setSampleRate(settings.m_sampleRate);
|
||||
|
||||
response.getHeatMapSettings()->setRgbColor(settings.m_rgbColor);
|
||||
if (response.getHeatMapSettings()->getTitle()) {
|
||||
*response.getHeatMapSettings()->getTitle() = settings.m_title;
|
||||
} else {
|
||||
response.getHeatMapSettings()->setTitle(new QString(settings.m_title));
|
||||
}
|
||||
|
||||
response.getHeatMapSettings()->setStreamIndex(settings.m_streamIndex);
|
||||
response.getHeatMapSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0);
|
||||
|
||||
if (response.getHeatMapSettings()->getReverseApiAddress()) {
|
||||
*response.getHeatMapSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress;
|
||||
} else {
|
||||
response.getHeatMapSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress));
|
||||
}
|
||||
|
||||
response.getHeatMapSettings()->setReverseApiPort(settings.m_reverseAPIPort);
|
||||
response.getHeatMapSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIDeviceIndex);
|
||||
response.getHeatMapSettings()->setReverseApiChannelIndex(settings.m_reverseAPIChannelIndex);
|
||||
|
||||
if (settings.m_scopeGUI)
|
||||
{
|
||||
if (response.getHeatMapSettings()->getScopeConfig())
|
||||
{
|
||||
settings.m_scopeGUI->formatTo(response.getHeatMapSettings()->getScopeConfig());
|
||||
}
|
||||
else
|
||||
{
|
||||
SWGSDRangel::SWGGLScope *swgGLScope = new SWGSDRangel::SWGGLScope();
|
||||
settings.m_scopeGUI->formatTo(swgGLScope);
|
||||
response.getHeatMapSettings()->setScopeConfig(swgGLScope);
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.m_channelMarker)
|
||||
{
|
||||
if (response.getHeatMapSettings()->getChannelMarker())
|
||||
{
|
||||
settings.m_channelMarker->formatTo(response.getHeatMapSettings()->getChannelMarker());
|
||||
}
|
||||
else
|
||||
{
|
||||
SWGSDRangel::SWGChannelMarker *swgChannelMarker = new SWGSDRangel::SWGChannelMarker();
|
||||
settings.m_channelMarker->formatTo(swgChannelMarker);
|
||||
response.getHeatMapSettings()->setChannelMarker(swgChannelMarker);
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.m_rollupState)
|
||||
{
|
||||
if (response.getHeatMapSettings()->getRollupState())
|
||||
{
|
||||
settings.m_rollupState->formatTo(response.getHeatMapSettings()->getRollupState());
|
||||
}
|
||||
else
|
||||
{
|
||||
SWGSDRangel::SWGRollupState *swgRollupState = new SWGSDRangel::SWGRollupState();
|
||||
settings.m_rollupState->formatTo(swgRollupState);
|
||||
response.getHeatMapSettings()->setRollupState(swgRollupState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HeatMap::webapiReverseSendSettings(QList<QString>& channelSettingsKeys, const HeatMapSettings& settings, bool force)
|
||||
{
|
||||
SWGSDRangel::SWGChannelSettings *swgChannelSettings = new SWGSDRangel::SWGChannelSettings();
|
||||
webapiFormatChannelSettings(channelSettingsKeys, swgChannelSettings, settings, force);
|
||||
|
||||
QString channelSettingsURL = QString("http://%1:%2/sdrangel/deviceset/%3/channel/%4/settings")
|
||||
.arg(settings.m_reverseAPIAddress)
|
||||
.arg(settings.m_reverseAPIPort)
|
||||
.arg(settings.m_reverseAPIDeviceIndex)
|
||||
.arg(settings.m_reverseAPIChannelIndex);
|
||||
m_networkRequest.setUrl(QUrl(channelSettingsURL));
|
||||
m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
QBuffer *buffer = new QBuffer();
|
||||
buffer->open((QBuffer::ReadWrite));
|
||||
buffer->write(swgChannelSettings->asJson().toUtf8());
|
||||
buffer->seek(0);
|
||||
|
||||
// Always use PATCH to avoid passing reverse API settings
|
||||
QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer);
|
||||
buffer->setParent(reply);
|
||||
|
||||
delete swgChannelSettings;
|
||||
}
|
||||
|
||||
void HeatMap::webapiFormatChannelSettings(
|
||||
QList<QString>& channelSettingsKeys,
|
||||
SWGSDRangel::SWGChannelSettings *swgChannelSettings,
|
||||
const HeatMapSettings& settings,
|
||||
bool force
|
||||
)
|
||||
{
|
||||
swgChannelSettings->setDirection(0); // Single sink (Rx)
|
||||
swgChannelSettings->setOriginatorChannelIndex(getIndexInDeviceSet());
|
||||
swgChannelSettings->setOriginatorDeviceSetIndex(getDeviceSetIndex());
|
||||
swgChannelSettings->setChannelType(new QString("HeatMap"));
|
||||
swgChannelSettings->setHeatMapSettings(new SWGSDRangel::SWGHeatMapSettings());
|
||||
SWGSDRangel::SWGHeatMapSettings *swgHeatMapSettings = swgChannelSettings->getHeatMapSettings();
|
||||
|
||||
// transfer data that has been modified. When force is on transfer all data except reverse API data
|
||||
|
||||
if (channelSettingsKeys.contains("inputFrequencyOffset") || force) {
|
||||
swgHeatMapSettings->setInputFrequencyOffset(settings.m_inputFrequencyOffset);
|
||||
}
|
||||
if (channelSettingsKeys.contains("rfBandwidth") || force) {
|
||||
swgHeatMapSettings->setRfBandwidth(settings.m_rfBandwidth);
|
||||
}
|
||||
if (channelSettingsKeys.contains("minPower") || force) {
|
||||
swgHeatMapSettings->setMinPower(settings.m_minPower);
|
||||
}
|
||||
if (channelSettingsKeys.contains("maxPower") || force) {
|
||||
swgHeatMapSettings->setMaxPower(settings.m_maxPower);
|
||||
}
|
||||
if (channelSettingsKeys.contains("colorMapName") || force) {
|
||||
swgHeatMapSettings->setColorMapName(new QString(settings.m_colorMapName));
|
||||
}
|
||||
if (channelSettingsKeys.contains("mode") || force) {
|
||||
swgHeatMapSettings->setMode((int) settings.m_mode);
|
||||
}
|
||||
if (channelSettingsKeys.contains("pulseThreshold") || force) {
|
||||
swgHeatMapSettings->setPulseThreshold(settings.m_pulseThreshold);
|
||||
}
|
||||
if (channelSettingsKeys.contains("averagePeriodUS") || force) {
|
||||
swgHeatMapSettings->setAveragePeriodUs(settings.m_averagePeriodUS);
|
||||
}
|
||||
if (channelSettingsKeys.contains("sampleRate") || force) {
|
||||
swgHeatMapSettings->setSampleRate(settings.m_sampleRate);
|
||||
}
|
||||
if (channelSettingsKeys.contains("rgbColor") || force) {
|
||||
swgHeatMapSettings->setRgbColor(settings.m_rgbColor);
|
||||
}
|
||||
if (channelSettingsKeys.contains("title") || force) {
|
||||
swgHeatMapSettings->setTitle(new QString(settings.m_title));
|
||||
}
|
||||
if (channelSettingsKeys.contains("streamIndex") || force) {
|
||||
swgHeatMapSettings->setStreamIndex(settings.m_streamIndex);
|
||||
}
|
||||
|
||||
if (settings.m_scopeGUI && (channelSettingsKeys.contains("scopeConfig") || force))
|
||||
{
|
||||
SWGSDRangel::SWGGLScope *swgGLScope = new SWGSDRangel::SWGGLScope();
|
||||
settings.m_scopeGUI->formatTo(swgGLScope);
|
||||
swgHeatMapSettings->setScopeConfig(swgGLScope);
|
||||
}
|
||||
|
||||
if (settings.m_channelMarker && (channelSettingsKeys.contains("channelMarker") || force))
|
||||
{
|
||||
SWGSDRangel::SWGChannelMarker *swgChannelMarker = new SWGSDRangel::SWGChannelMarker();
|
||||
settings.m_channelMarker->formatTo(swgChannelMarker);
|
||||
swgHeatMapSettings->setChannelMarker(swgChannelMarker);
|
||||
}
|
||||
}
|
||||
|
||||
void HeatMap::networkManagerFinished(QNetworkReply *reply)
|
||||
{
|
||||
QNetworkReply::NetworkError replyError = reply->error();
|
||||
|
||||
if (replyError)
|
||||
{
|
||||
qWarning() << "HeatMap::networkManagerFinished:"
|
||||
<< " error(" << (int) replyError
|
||||
<< "): " << replyError
|
||||
<< ": " << reply->errorString();
|
||||
}
|
||||
else
|
||||
{
|
||||
QString answer = reply->readAll();
|
||||
answer.chop(1); // remove last \n
|
||||
qDebug("HeatMap::networkManagerFinished: reply:\n%s", answer.toStdString().c_str());
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
void HeatMap::handleIndexInDeviceSetChanged(int index)
|
||||
{
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString fifoLabel = QString("%1 [%2:%3]")
|
||||
.arg(m_channelId)
|
||||
.arg(m_deviceAPI->getDeviceSetIndex())
|
||||
.arg(index);
|
||||
m_basebandSink->setFifoLabel(fifoLabel);
|
||||
}
|
||||
|
178
plugins/channelrx/heatmap/heatmap.h
Normal file
@ -0,0 +1,178 @@
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (C) 2015-2018 Edouard Griffiths, F4EXB. //
|
||||
// Copyright (C) 2023 Jon Beniston, M7RCE //
|
||||
// //
|
||||
// This program is free software; you can redistribute it and/or modify //
|
||||
// it under the terms of the GNU General Public License as published by //
|
||||
// the Free Software Foundation as version 3 of the License, or //
|
||||
// (at your option) any later version. //
|
||||
// //
|
||||
// This program is distributed in the hope that it will be useful, //
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
|
||||
// GNU General Public License V3 for more details. //
|
||||
// //
|
||||
// You should have received a copy of the GNU General Public License //
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef INCLUDE_HEATMAP_H
|
||||
#define INCLUDE_HEATMAP_H
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <QNetworkRequest>
|
||||
#include <QUdpSocket>
|
||||
#include <QThread>
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
|
||||
#include "dsp/basebandsamplesink.h"
|
||||
#include "channel/channelapi.h"
|
||||
#include "util/message.h"
|
||||
|
||||
#include "heatmapbaseband.h"
|
||||
#include "heatmapsettings.h"
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
class QThread;
|
||||
class DeviceAPI;
|
||||
class ScopeVis;
|
||||
|
||||
class HeatMap : public BasebandSampleSink, public ChannelAPI {
|
||||
public:
|
||||
class MsgConfigureHeatMap : public Message {
|
||||
MESSAGE_CLASS_DECLARATION
|
||||
|
||||
public:
|
||||
const HeatMapSettings& getSettings() const { return m_settings; }
|
||||
bool getForce() const { return m_force; }
|
||||
|
||||
static MsgConfigureHeatMap* create(const HeatMapSettings& settings, bool force)
|
||||
{
|
||||
return new MsgConfigureHeatMap(settings, force);
|
||||
}
|
||||
|
||||
private:
|
||||
HeatMapSettings m_settings;
|
||||
bool m_force;
|
||||
|
||||
MsgConfigureHeatMap(const HeatMapSettings& settings, bool force) :
|
||||
Message(),
|
||||
m_settings(settings),
|
||||
m_force(force)
|
||||
{ }
|
||||
};
|
||||
|
||||
HeatMap(DeviceAPI *deviceAPI);
|
||||
virtual ~HeatMap();
|
||||
virtual void destroy() { delete this; }
|
||||
virtual void setDeviceAPI(DeviceAPI *deviceAPI);
|
||||
virtual DeviceAPI *getDeviceAPI() { return m_deviceAPI; }
|
||||
|
||||
using BasebandSampleSink::feed;
|
||||
virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool po);
|
||||
virtual void start();
|
||||
virtual void stop();
|
||||
virtual void pushMessage(Message *msg) { m_inputMessageQueue.push(msg); }
|
||||
virtual QString getSinkName() { return objectName(); }
|
||||
|
||||
virtual void getIdentifier(QString& id) { id = objectName(); }
|
||||
virtual QString getIdentifier() const { return objectName(); }
|
||||
virtual const QString& getURI() const { return getName(); }
|
||||
virtual void getTitle(QString& title) { title = m_settings.m_title; }
|
||||
virtual qint64 getCenterFrequency() const { return m_settings.m_inputFrequencyOffset; }
|
||||
virtual void setCenterFrequency(qint64 frequency);
|
||||
|
||||
virtual QByteArray serialize() const;
|
||||
virtual bool deserialize(const QByteArray& data);
|
||||
|
||||
virtual int getNbSinkStreams() const { return 1; }
|
||||
virtual int getNbSourceStreams() const { return 0; }
|
||||
|
||||
virtual qint64 getStreamCenterFrequency(int streamIndex, bool sinkElseSource) const
|
||||
{
|
||||
(void) streamIndex;
|
||||
(void) sinkElseSource;
|
||||
return 0;
|
||||
}
|
||||
|
||||
virtual int webapiSettingsGet(
|
||||
SWGSDRangel::SWGChannelSettings& response,
|
||||
QString& errorMessage);
|
||||
|
||||
virtual int webapiWorkspaceGet(
|
||||
SWGSDRangel::SWGWorkspaceInfo& response,
|
||||
QString& errorMessage);
|
||||
|
||||
virtual int webapiSettingsPutPatch(
|
||||
bool force,
|
||||
const QStringList& channelSettingsKeys,
|
||||
SWGSDRangel::SWGChannelSettings& response,
|
||||
QString& errorMessage);
|
||||
|
||||
static void webapiFormatChannelSettings(
|
||||
SWGSDRangel::SWGChannelSettings& response,
|
||||
const HeatMapSettings& settings);
|
||||
|
||||
static void webapiUpdateChannelSettings(
|
||||
HeatMapSettings& settings,
|
||||
const QStringList& channelSettingsKeys,
|
||||
SWGSDRangel::SWGChannelSettings& response);
|
||||
|
||||
ScopeVis *getScopeSink();
|
||||
double getMagSq() const { return m_basebandSink->getMagSq(); }
|
||||
|
||||
void getMagSqLevels(double& avg, double& peak, int& nbSamples) {
|
||||
m_basebandSink->getMagSqLevels(avg, peak, nbSamples);
|
||||
}
|
||||
|
||||
void getMagLevels(double& avg, double& pulseAvg, double &maxPeak, double &minPeak)
|
||||
{
|
||||
m_basebandSink->getMagLevels(avg, pulseAvg, maxPeak, minPeak);
|
||||
}
|
||||
|
||||
void resetMagLevels() {
|
||||
m_basebandSink->resetMagLevels();
|
||||
}
|
||||
|
||||
/* void setMessageQueueToGUI(MessageQueue* queue) override {
|
||||
ChannelAPI::setMessageQueueToGUI(queue);
|
||||
m_basebandSink->setMessageQueueToGUI(queue);
|
||||
}*/
|
||||
|
||||
uint32_t getNumberOfDeviceStreams() const;
|
||||
|
||||
static const char * const m_channelIdURI;
|
||||
static const char * const m_channelId;
|
||||
|
||||
private:
|
||||
DeviceAPI *m_deviceAPI;
|
||||
QThread m_thread;
|
||||
HeatMapBaseband* m_basebandSink;
|
||||
HeatMapSettings m_settings;
|
||||
int m_basebandSampleRate; //!< stored from device message used when starting baseband sink
|
||||
qint64 m_centerFrequency;
|
||||
|
||||
QNetworkAccessManager *m_networkManager;
|
||||
QNetworkRequest m_networkRequest;
|
||||
|
||||
virtual bool handleMessage(const Message& cmd);
|
||||
void applySettings(const HeatMapSettings& settings, bool force = false);
|
||||
void webapiReverseSendSettings(QList<QString>& channelSettingsKeys, const HeatMapSettings& settings, bool force);
|
||||
void webapiFormatChannelSettings(
|
||||
QList<QString>& channelSettingsKeys,
|
||||
SWGSDRangel::SWGChannelSettings *swgChannelSettings,
|
||||
const HeatMapSettings& settings,
|
||||
bool force
|
||||
);
|
||||
|
||||
private slots:
|
||||
void networkManagerFinished(QNetworkReply *reply);
|
||||
void handleIndexInDeviceSetChanged(int index);
|
||||
};
|
||||
|
||||
#endif // INCLUDE_HEATMAP_H
|
||||
|
178
plugins/channelrx/heatmap/heatmapbaseband.cpp
Normal file
@ -0,0 +1,178 @@
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (C) 2019 Edouard Griffiths, F4EXB //
|
||||
// Copyright (C) 2023 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 <QDebug>
|
||||
|
||||
#include "dsp/dspengine.h"
|
||||
#include "dsp/dspcommands.h"
|
||||
#include "dsp/downchannelizer.h"
|
||||
|
||||
#include "heatmapbaseband.h"
|
||||
|
||||
MESSAGE_CLASS_DEFINITION(HeatMapBaseband::MsgConfigureHeatMapBaseband, Message)
|
||||
|
||||
HeatMapBaseband::HeatMapBaseband(HeatMap *heatDemod) :
|
||||
m_sink(heatDemod),
|
||||
m_running(false)
|
||||
{
|
||||
qDebug("HeatMapBaseband::HeatMapBaseband");
|
||||
|
||||
m_sink.setScopeSink(&m_scopeSink);
|
||||
m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(48000));
|
||||
m_channelizer = new DownChannelizer(&m_sink);
|
||||
}
|
||||
|
||||
HeatMapBaseband::~HeatMapBaseband()
|
||||
{
|
||||
m_inputMessageQueue.clear();
|
||||
|
||||
delete m_channelizer;
|
||||
}
|
||||
|
||||
void HeatMapBaseband::reset()
|
||||
{
|
||||
QMutexLocker mutexLocker(&m_mutex);
|
||||
m_inputMessageQueue.clear();
|
||||
m_sampleFifo.reset();
|
||||
}
|
||||
|
||||
void HeatMapBaseband::startWork()
|
||||
{
|
||||
QMutexLocker mutexLocker(&m_mutex);
|
||||
QObject::connect(
|
||||
&m_sampleFifo,
|
||||
&SampleSinkFifo::dataReady,
|
||||
this,
|
||||
&HeatMapBaseband::handleData,
|
||||
Qt::QueuedConnection
|
||||
);
|
||||
connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()));
|
||||
m_running = true;
|
||||
}
|
||||
|
||||
void HeatMapBaseband::stopWork()
|
||||
{
|
||||
QMutexLocker mutexLocker(&m_mutex);
|
||||
disconnect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()));
|
||||
QObject::disconnect(
|
||||
&m_sampleFifo,
|
||||
&SampleSinkFifo::dataReady,
|
||||
this,
|
||||
&HeatMapBaseband::handleData
|
||||
);
|
||||
m_running = false;
|
||||
}
|
||||
|
||||
void HeatMapBaseband::setChannel(ChannelAPI *channel)
|
||||
{
|
||||
m_sink.setChannel(channel);
|
||||
}
|
||||
|
||||
void HeatMapBaseband::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end)
|
||||
{
|
||||
m_sampleFifo.write(begin, end);
|
||||
}
|
||||
|
||||
void HeatMapBaseband::handleData()
|
||||
{
|
||||
QMutexLocker mutexLocker(&m_mutex);
|
||||
|
||||
while ((m_sampleFifo.fill() > 0) && (m_inputMessageQueue.size() == 0))
|
||||
{
|
||||
SampleVector::iterator part1begin;
|
||||
SampleVector::iterator part1end;
|
||||
SampleVector::iterator part2begin;
|
||||
SampleVector::iterator part2end;
|
||||
|
||||
std::size_t count = m_sampleFifo.readBegin(m_sampleFifo.fill(), &part1begin, &part1end, &part2begin, &part2end);
|
||||
|
||||
// first part of FIFO data
|
||||
if (part1begin != part1end) {
|
||||
m_channelizer->feed(part1begin, part1end);
|
||||
}
|
||||
|
||||
// second part of FIFO data (used when block wraps around)
|
||||
if(part2begin != part2end) {
|
||||
m_channelizer->feed(part2begin, part2end);
|
||||
}
|
||||
|
||||
m_sampleFifo.readCommit((unsigned int) count);
|
||||
}
|
||||
}
|
||||
|
||||
void HeatMapBaseband::handleInputMessages()
|
||||
{
|
||||
Message* message;
|
||||
|
||||
while ((message = m_inputMessageQueue.pop()) != nullptr)
|
||||
{
|
||||
if (handleMessage(*message)) {
|
||||
delete message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool HeatMapBaseband::handleMessage(const Message& cmd)
|
||||
{
|
||||
if (MsgConfigureHeatMapBaseband::match(cmd))
|
||||
{
|
||||
QMutexLocker mutexLocker(&m_mutex);
|
||||
MsgConfigureHeatMapBaseband& cfg = (MsgConfigureHeatMapBaseband&) cmd;
|
||||
qDebug() << "HeatMapBaseband::handleMessage: MsgConfigureHeatMapBaseband";
|
||||
|
||||
applySettings(cfg.getSettings(), cfg.getForce());
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (DSPSignalNotification::match(cmd))
|
||||
{
|
||||
QMutexLocker mutexLocker(&m_mutex);
|
||||
DSPSignalNotification& notif = (DSPSignalNotification&) cmd;
|
||||
qDebug() << "HeatMapBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate();
|
||||
setBasebandSampleRate(notif.getSampleRate());
|
||||
m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(notif.getSampleRate()));
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void HeatMapBaseband::applySettings(const HeatMapSettings& settings, bool force)
|
||||
{
|
||||
if ((settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset)
|
||||
|| (settings.m_sampleRate != m_settings.m_sampleRate)
|
||||
|| force)
|
||||
{
|
||||
m_channelizer->setChannelization(settings.m_sampleRate, settings.m_inputFrequencyOffset);
|
||||
m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset());
|
||||
}
|
||||
|
||||
m_sink.applySettings(settings, force);
|
||||
|
||||
m_settings = settings;
|
||||
}
|
||||
|
||||
void HeatMapBaseband::setBasebandSampleRate(int sampleRate)
|
||||
{
|
||||
m_channelizer->setBasebandSampleRate(sampleRate);
|
||||
m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset());
|
||||
}
|
||||
|
108
plugins/channelrx/heatmap/heatmapbaseband.h
Normal file
@ -0,0 +1,108 @@
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (C) 2019 Edouard Griffiths, F4EXB //
|
||||
// Copyright (C) 2023 Jon Beniston, M7RCE //
|
||||
// //
|
||||
// This program is free software; you can redistribute it and/or modify //
|
||||
// it under the terms of the GNU General Public License as published by //
|
||||
// the Free Software Foundation as version 3 of the License, or //
|
||||
// (at your option) any later version. //
|
||||
// //
|
||||
// This program is distributed in the hope that it will be useful, //
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
|
||||
// GNU General Public License V3 for more details. //
|
||||
// //
|
||||
// You should have received a copy of the GNU General Public License //
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef INCLUDE_HEATMAPBASEBAND_H
|
||||
#define INCLUDE_HEATMAPBASEBAND_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QRecursiveMutex>
|
||||
|
||||
#include "dsp/samplesinkfifo.h"
|
||||
#include "dsp/scopevis.h"
|
||||
#include "util/message.h"
|
||||
#include "util/messagequeue.h"
|
||||
|
||||
#include "heatmapsink.h"
|
||||
|
||||
class DownChannelizer;
|
||||
class ChannelAPI;
|
||||
class HeatMap;
|
||||
class ScopeVis;
|
||||
|
||||
class HeatMapBaseband : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
class MsgConfigureHeatMapBaseband : public Message {
|
||||
MESSAGE_CLASS_DECLARATION
|
||||
|
||||
public:
|
||||
const HeatMapSettings& getSettings() const { return m_settings; }
|
||||
bool getForce() const { return m_force; }
|
||||
|
||||
static MsgConfigureHeatMapBaseband* create(const HeatMapSettings& settings, bool force)
|
||||
{
|
||||
return new MsgConfigureHeatMapBaseband(settings, force);
|
||||
}
|
||||
|
||||
private:
|
||||
HeatMapSettings m_settings;
|
||||
bool m_force;
|
||||
|
||||
MsgConfigureHeatMapBaseband(const HeatMapSettings& settings, bool force) :
|
||||
Message(),
|
||||
m_settings(settings),
|
||||
m_force(force)
|
||||
{ }
|
||||
};
|
||||
|
||||
HeatMapBaseband(HeatMap *heatDemod);
|
||||
~HeatMapBaseband();
|
||||
void reset();
|
||||
void startWork();
|
||||
void stopWork();
|
||||
void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end);
|
||||
MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } //!< Get the queue for asynchronous inbound communication
|
||||
void getMagSqLevels(double& avg, double &peak, int& nbSamples) {
|
||||
m_sink.getMagSqLevels(avg, peak, nbSamples);
|
||||
}
|
||||
void getMagLevels(double& avg, double& pulseAvg, double &maxPeak, double &minPeak) {
|
||||
m_sink.getMagLevels(avg, pulseAvg, maxPeak, minPeak);
|
||||
}
|
||||
void resetMagLevels() {
|
||||
m_sink.resetMagLevels();
|
||||
}
|
||||
void setMessageQueueToChannel(MessageQueue *messageQueue) { m_sink.setMessageQueueToChannel(messageQueue); }
|
||||
void setBasebandSampleRate(int sampleRate);
|
||||
ScopeVis *getScopeSink() { return &m_scopeSink; }
|
||||
void setChannel(ChannelAPI *channel);
|
||||
double getMagSq() const { return m_sink.getMagSq(); }
|
||||
bool isRunning() const { return m_running; }
|
||||
void setFifoLabel(const QString& label) { m_sampleFifo.setLabel(label); }
|
||||
|
||||
private:
|
||||
SampleSinkFifo m_sampleFifo;
|
||||
DownChannelizer *m_channelizer;
|
||||
HeatMapSink m_sink;
|
||||
MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication
|
||||
HeatMapSettings m_settings;
|
||||
ScopeVis m_scopeSink;
|
||||
bool m_running;
|
||||
QRecursiveMutex m_mutex;
|
||||
|
||||
bool handleMessage(const Message& cmd);
|
||||
void calculateOffset(HeatMapSink *sink);
|
||||
void applySettings(const HeatMapSettings& settings, bool force = false);
|
||||
|
||||
private slots:
|
||||
void handleInputMessages();
|
||||
void handleData(); //!< Handle data when samples have to be processed
|
||||
};
|
||||
|
||||
#endif // INCLUDE_HEATMAPBASEBAND_H
|
||||
|
1468
plugins/channelrx/heatmap/heatmapgui.cpp
Normal file
221
plugins/channelrx/heatmap/heatmapgui.h
Normal file
@ -0,0 +1,221 @@
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (C) 2016 Edouard Griffiths, F4EXB //
|
||||
// Copyright (C) 2023 Jon Beniston, M7RCE //
|
||||
// //
|
||||
// This program is free software; you can redistribute it and/or modify //
|
||||
// it under the terms of the GNU General Public License as published by //
|
||||
// the Free Software Foundation as version 3 of the License, or //
|
||||
// (at your option) any later version. //
|
||||
// //
|
||||
// This program is distributed in the hope that it will be useful, //
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
|
||||
// GNU General Public License V3 for more details. //
|
||||
// //
|
||||
// You should have received a copy of the GNU General Public License //
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef INCLUDE_HEATMAPGUI_H
|
||||
#define INCLUDE_HEATMAPGUI_H
|
||||
|
||||
#include <QTableWidgetItem>
|
||||
#include <QPushButton>
|
||||
#include <QToolButton>
|
||||
#include <QHBoxLayout>
|
||||
#include <QMenu>
|
||||
#include <QPainter>
|
||||
#include <QFileDialog>
|
||||
#include <QtCharts>
|
||||
|
||||
#include "channel/channelgui.h"
|
||||
#include "dsp/channelmarker.h"
|
||||
#include "util/messagequeue.h"
|
||||
#include "util/colormap.h"
|
||||
#include "settings/rollupstate.h"
|
||||
|
||||
#include "heatmapsettings.h"
|
||||
#include "heatmap.h"
|
||||
|
||||
class PluginAPI;
|
||||
class DeviceUISet;
|
||||
class BasebandSampleSink;
|
||||
class ScopeVis;
|
||||
class HeatMap;
|
||||
class HeatMapGUI;
|
||||
|
||||
namespace Ui {
|
||||
class HeatMapGUI;
|
||||
}
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
using namespace QtCharts;
|
||||
#endif
|
||||
|
||||
class HeatMapGUI : public ChannelGUI {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static HeatMapGUI* create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel);
|
||||
virtual void destroy();
|
||||
|
||||
void resetToDefaults();
|
||||
QByteArray serialize() const;
|
||||
bool deserialize(const QByteArray& data);
|
||||
virtual MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; }
|
||||
virtual void setWorkspaceIndex(int index) { m_settings.m_workspaceIndex = index; };
|
||||
virtual int getWorkspaceIndex() const { return m_settings.m_workspaceIndex; };
|
||||
virtual void setGeometryBytes(const QByteArray& blob) { m_settings.m_geometryBytes = blob; };
|
||||
virtual QByteArray getGeometryBytes() const { return m_settings.m_geometryBytes; };
|
||||
virtual QString getTitle() const { return m_settings.m_title; };
|
||||
virtual QColor getTitleColor() const { return m_settings.m_rgbColor; };
|
||||
virtual void zetHidden(bool hidden) { m_settings.m_hidden = hidden; }
|
||||
virtual bool getHidden() const { return m_settings.m_hidden; }
|
||||
virtual ChannelMarker& getChannelMarker() { return m_channelMarker; }
|
||||
virtual int getStreamIndex() const { return m_settings.m_streamIndex; }
|
||||
virtual void setStreamIndex(int streamIndex) { m_settings.m_streamIndex = streamIndex; }
|
||||
|
||||
public slots:
|
||||
void channelMarkerChangedByCursor();
|
||||
void channelMarkerHighlightedByCursor();
|
||||
void preferenceChanged(int elementType);
|
||||
|
||||
private:
|
||||
Ui::HeatMapGUI* ui;
|
||||
PluginAPI* m_pluginAPI;
|
||||
DeviceUISet* m_deviceUISet;
|
||||
ChannelMarker m_channelMarker;
|
||||
RollupState m_rollupState;
|
||||
HeatMapSettings m_settings;
|
||||
qint64 m_deviceCenterFrequency;
|
||||
int m_basebandSampleRate;
|
||||
bool m_doApplySettings;
|
||||
ScopeVis* m_scopeVis;
|
||||
|
||||
HeatMap* m_heatMap;
|
||||
uint32_t m_tickCount;
|
||||
MessageQueue m_inputMessageQueue;
|
||||
|
||||
QMenu *messagesMenu; // Column select context menu
|
||||
QMenu *copyMenu;
|
||||
|
||||
int m_width; // In pixels
|
||||
int m_height;
|
||||
double m_resolution;
|
||||
double m_degreesLonPerPixel;
|
||||
double m_degreesLatPerPixel;
|
||||
float *m_powerAverage;
|
||||
float *m_powerPulseAverage;
|
||||
float *m_powerMaxPeak;
|
||||
float *m_powerMinPeak;
|
||||
float *m_powerPathLoss;
|
||||
QImage m_image;
|
||||
double m_east, m_west, m_north, m_south;
|
||||
double m_latitude;
|
||||
double m_longitude;
|
||||
double m_altitude;
|
||||
QPainter m_painter;
|
||||
QPen m_pen;
|
||||
const float *m_colorMap;
|
||||
int m_x; // Current position
|
||||
int m_y;
|
||||
|
||||
QChart *m_powerChart;
|
||||
QLineSeries *m_powerAverageSeries;
|
||||
QLineSeries *m_powerMaxPeakSeries;
|
||||
QLineSeries *m_powerMinPeakSeries;
|
||||
QLineSeries *m_powerPulseAverageSeries;
|
||||
QLineSeries *m_powerPathLossSeries;
|
||||
QDateTimeAxis *m_powerXAxis;
|
||||
QValueAxis *m_powerYAxis;
|
||||
|
||||
const static QStringList m_averagePeriodTexts;
|
||||
const static QStringList m_sampleRateTexts;
|
||||
|
||||
const static int m_blockSize = 512; // osm uses powers of 2
|
||||
const static int m_zoomLevel = 15; // 3m/pixel at lat=51 GPS accuracy ~5m at 1Hz
|
||||
|
||||
QFileDialog m_csvFileDialog;
|
||||
QFileDialog m_imageFileDialog;
|
||||
|
||||
explicit HeatMapGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent = 0);
|
||||
virtual ~HeatMapGUI();
|
||||
|
||||
void blockApplySettings(bool block);
|
||||
void applySettings(bool force = false);
|
||||
void displaySettings();
|
||||
void messageReceived(const QByteArray& message, const QDateTime& dateTime);
|
||||
bool handleMessage(const Message& message);
|
||||
void makeUIConnections();
|
||||
void updateAbsoluteCenterFrequency();
|
||||
|
||||
float *getCurrentModePowerData();
|
||||
void coordsToPixel(double latitude, double longitude, int& x, int& y) const;
|
||||
void pixelToCoords(int x, int y, double& latitude, double& longitude) const;
|
||||
bool pixelValid(int x, int y) const;
|
||||
|
||||
void createMap();
|
||||
void deleteMap();
|
||||
void resizeMap(int x, int y);
|
||||
void clearPower();
|
||||
void clearPower(float *power);
|
||||
void clearPower(float *power, int size);
|
||||
void clearImage();
|
||||
void updatePower(double latitude, double longitude, float power);
|
||||
void plotMap();
|
||||
void plotMap(float *power);
|
||||
void plotPixel(int x, int y, double power);
|
||||
qreal calcRange(double latitude, double longitude);
|
||||
void updateRange();
|
||||
void displayTXPosition(bool enabled);
|
||||
double calcFreeSpacePathLoss(double range, double frequency);
|
||||
|
||||
void displayPowerChart();
|
||||
void plotPowerVsTimeChart();
|
||||
void addToPowerSeries(QDateTime dateTime, double average, double pulseAverage, double max, double min, double pathLoss);
|
||||
void trimPowerSeries(QDateTime dateTime);
|
||||
void trimPowerSeries(QLineSeries *series, QDateTime dateTime);
|
||||
void updateAxis();
|
||||
|
||||
void deleteFromMap();
|
||||
void sendToMap();
|
||||
void sendTxToMap();
|
||||
void deleteTxFromMap();
|
||||
|
||||
void leaveEvent(QEvent*);
|
||||
void enterEvent(EnterEventType*);
|
||||
|
||||
private slots:
|
||||
void on_deltaFrequency_changed(qint64 value);
|
||||
void on_rfBW_valueChanged(int index);
|
||||
void on_displayChart_clicked(bool checked=false);
|
||||
void on_clearHeatMap_clicked();
|
||||
void on_writeCSV_clicked();
|
||||
void on_readCSV_clicked();
|
||||
void on_writeImage_clicked();
|
||||
void on_minPower_valueChanged(double value);
|
||||
void on_maxPower_valueChanged(double value);
|
||||
void on_colorMap_currentIndexChanged(int index);
|
||||
void on_pulseTH_valueChanged(int value);
|
||||
void on_averagePeriod_valueChanged(int value);
|
||||
void on_sampleRate_valueChanged(int value);
|
||||
void on_mode_currentIndexChanged(int index);
|
||||
void on_txPosition_clicked(bool checked=false);
|
||||
void on_txLatitude_editingFinished();
|
||||
void on_txLongitude_editingFinished();
|
||||
void on_txPower_valueChanged(double value);
|
||||
void on_txPositionSet_clicked(bool checked=false);
|
||||
void on_displayAverage_clicked(bool checked=false);
|
||||
void on_displayMax_clicked(bool checked=false);
|
||||
void on_displayMin_clicked(bool checked=false);
|
||||
void on_displayPulseAverage_clicked(bool checked=false);
|
||||
void on_displayPathLoss_clicked(bool checked=false);
|
||||
void on_displayMins_valueChanged(int value);
|
||||
void onWidgetRolled(QWidget* widget, bool rollDown);
|
||||
void onMenuDialogCalled(const QPoint& p);
|
||||
void handleInputMessages();
|
||||
void tick();
|
||||
};
|
||||
|
||||
#endif // INCLUDE_HEATMAPGUI_H
|
||||
|
1620
plugins/channelrx/heatmap/heatmapgui.ui
Normal file
92
plugins/channelrx/heatmap/heatmapplugin.cpp
Normal file
@ -0,0 +1,92 @@
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (C) 2016 Edouard Griffiths, F4EXB //
|
||||
// Copyright (C) 2023 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 <QtPlugin>
|
||||
#include "plugin/pluginapi.h"
|
||||
|
||||
#ifndef SERVER_MODE
|
||||
#include "heatmapgui.h"
|
||||
#endif
|
||||
#include "heatmap.h"
|
||||
#include "heatmapwebapiadapter.h"
|
||||
#include "heatmapplugin.h"
|
||||
|
||||
const PluginDescriptor HeatMapPlugin::m_pluginDescriptor = {
|
||||
HeatMap::m_channelId,
|
||||
QStringLiteral("Heat Map"),
|
||||
QStringLiteral("7.10.0"),
|
||||
QStringLiteral("(c) Jon Beniston, M7RCE"),
|
||||
QStringLiteral("https://github.com/f4exb/sdrangel"),
|
||||
true,
|
||||
QStringLiteral("https://github.com/f4exb/sdrangel")
|
||||
};
|
||||
|
||||
HeatMapPlugin::HeatMapPlugin(QObject* parent) :
|
||||
QObject(parent),
|
||||
m_pluginAPI(0)
|
||||
{
|
||||
}
|
||||
|
||||
const PluginDescriptor& HeatMapPlugin::getPluginDescriptor() const
|
||||
{
|
||||
return m_pluginDescriptor;
|
||||
}
|
||||
|
||||
void HeatMapPlugin::initPlugin(PluginAPI* pluginAPI)
|
||||
{
|
||||
m_pluginAPI = pluginAPI;
|
||||
|
||||
m_pluginAPI->registerRxChannel(HeatMap::m_channelIdURI, HeatMap::m_channelId, this);
|
||||
}
|
||||
|
||||
void HeatMapPlugin::createRxChannel(DeviceAPI *deviceAPI, BasebandSampleSink **bs, ChannelAPI **cs) const
|
||||
{
|
||||
if (bs || cs)
|
||||
{
|
||||
HeatMap *instance = new HeatMap(deviceAPI);
|
||||
|
||||
if (bs) {
|
||||
*bs = instance;
|
||||
}
|
||||
|
||||
if (cs) {
|
||||
*cs = instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef SERVER_MODE
|
||||
ChannelGUI* HeatMapPlugin::createRxChannelGUI(
|
||||
DeviceUISet *deviceUISet,
|
||||
BasebandSampleSink *rxChannel) const
|
||||
{
|
||||
(void) deviceUISet;
|
||||
(void) rxChannel;
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
ChannelGUI* HeatMapPlugin::createRxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) const
|
||||
{
|
||||
return HeatMapGUI::create(m_pluginAPI, deviceUISet, rxChannel);
|
||||
}
|
||||
#endif
|
||||
|
||||
ChannelWebAPIAdapter* HeatMapPlugin::createChannelWebAPIAdapter() const
|
||||
{
|
||||
return new HeatMapWebAPIAdapter();
|
||||
}
|
49
plugins/channelrx/heatmap/heatmapplugin.h
Normal file
@ -0,0 +1,49 @@
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (C) 2016 Edouard Griffiths, F4EXB //
|
||||
// Copyright (C) 2023 Jon Beniston, M7RCE //
|
||||
// //
|
||||
// This program is free software; you can redistribute it and/or modify //
|
||||
// it under the terms of the GNU General Public License as published by //
|
||||
// the Free Software Foundation as version 3 of the License, or //
|
||||
// (at your option) any later version. //
|
||||
// //
|
||||
// This program is distributed in the hope that it will be useful, //
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
|
||||
// GNU General Public License V3 for more details. //
|
||||
// //
|
||||
// You should have received a copy of the GNU General Public License //
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef INCLUDE_HEATMAPPLUGIN_H
|
||||
#define INCLUDE_HEATMAPPLUGIN_H
|
||||
|
||||
#include <QObject>
|
||||
#include "plugin/plugininterface.h"
|
||||
|
||||
class DeviceUISet;
|
||||
class BasebandSampleSink;
|
||||
|
||||
class HeatMapPlugin : public QObject, PluginInterface {
|
||||
Q_OBJECT
|
||||
Q_INTERFACES(PluginInterface)
|
||||
Q_PLUGIN_METADATA(IID "sdrangel.channel.heatmap")
|
||||
|
||||
public:
|
||||
explicit HeatMapPlugin(QObject* parent = NULL);
|
||||
|
||||
const PluginDescriptor& getPluginDescriptor() const;
|
||||
void initPlugin(PluginAPI* pluginAPI);
|
||||
|
||||
virtual void createRxChannel(DeviceAPI *deviceAPI, BasebandSampleSink **bs, ChannelAPI **cs) const;
|
||||
virtual ChannelGUI* createRxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) const;
|
||||
virtual ChannelWebAPIAdapter* createChannelWebAPIAdapter() const;
|
||||
|
||||
private:
|
||||
static const PluginDescriptor m_pluginDescriptor;
|
||||
|
||||
PluginAPI* m_pluginAPI;
|
||||
};
|
||||
|
||||
#endif // INCLUDE_HEATMAPPLUGIN_H
|
197
plugins/channelrx/heatmap/heatmapsettings.cpp
Normal file
@ -0,0 +1,197 @@
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (C) 2015 Edouard Griffiths, F4EXB. //
|
||||
// Copyright (C) 2023 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 <QColor>
|
||||
|
||||
#include "dsp/dspengine.h"
|
||||
#include "util/simpleserializer.h"
|
||||
#include "settings/serializable.h"
|
||||
#include "heatmapsettings.h"
|
||||
|
||||
HeatMapSettings::HeatMapSettings() :
|
||||
m_channelMarker(nullptr),
|
||||
m_rollupState(nullptr)
|
||||
{
|
||||
resetToDefaults();
|
||||
}
|
||||
|
||||
void HeatMapSettings::resetToDefaults()
|
||||
{
|
||||
m_inputFrequencyOffset = 0;
|
||||
m_rfBandwidth = 16000.0f;
|
||||
m_minPower = -100.0f;
|
||||
m_maxPower = 0.0f;
|
||||
m_colorMapName = "Jet";
|
||||
m_mode = Average;
|
||||
m_pulseThreshold= -50.0f;
|
||||
m_averagePeriodUS = 100000;
|
||||
m_sampleRate = 100;
|
||||
m_txPosValid = false;
|
||||
m_txLatitude = 0.0f;
|
||||
m_txLongitude = 0.0f;
|
||||
m_txPower = 0.0f;
|
||||
m_displayChart = true;
|
||||
m_displayAverage = true;
|
||||
m_displayMax = true;
|
||||
m_displayMin = true;
|
||||
m_displayPulseAverage = true;
|
||||
m_displayPathLoss = true;
|
||||
m_displayMins = 2;
|
||||
m_rgbColor = QColor(102, 40, 220).rgb();
|
||||
m_title = "Heat Map";
|
||||
m_streamIndex = 0;
|
||||
m_useReverseAPI = false;
|
||||
m_reverseAPIAddress = "127.0.0.1";
|
||||
m_reverseAPIPort = 8888;
|
||||
m_reverseAPIDeviceIndex = 0;
|
||||
m_reverseAPIChannelIndex = 0;
|
||||
m_workspaceIndex = 0;
|
||||
m_hidden = false;
|
||||
}
|
||||
|
||||
QByteArray HeatMapSettings::serialize() const
|
||||
{
|
||||
SimpleSerializer s(1);
|
||||
|
||||
s.writeS32(1, m_inputFrequencyOffset);
|
||||
s.writeFloat(2, m_rfBandwidth);
|
||||
s.writeFloat(3, m_minPower);
|
||||
s.writeFloat(4, m_maxPower);
|
||||
s.writeString(5, m_colorMapName);
|
||||
s.writeS32(6, (int)m_mode);
|
||||
s.writeFloat(7, m_pulseThreshold);
|
||||
s.writeS32(8, m_averagePeriodUS);
|
||||
s.writeS32(9, m_sampleRate);
|
||||
s.writeBool(10, m_txPosValid);
|
||||
s.writeFloat(11, m_txLatitude);
|
||||
s.writeFloat(12, m_txLongitude);
|
||||
s.writeFloat(13, m_txPower);
|
||||
s.writeBool(14, m_displayChart);
|
||||
s.writeBool(15, m_displayAverage);
|
||||
s.writeBool(16, m_displayMax);
|
||||
s.writeBool(17, m_displayMin);
|
||||
s.writeBool(18, m_displayPulseAverage);
|
||||
s.writeBool(19, m_displayPathLoss);
|
||||
s.writeS32(20, m_displayMins);
|
||||
|
||||
s.writeU32(21, m_rgbColor);
|
||||
s.writeString(22, m_title);
|
||||
|
||||
if (m_channelMarker) {
|
||||
s.writeBlob(23, m_channelMarker->serialize());
|
||||
}
|
||||
|
||||
s.writeS32(24, m_streamIndex);
|
||||
s.writeBool(25, m_useReverseAPI);
|
||||
s.writeString(26, m_reverseAPIAddress);
|
||||
s.writeU32(27, m_reverseAPIPort);
|
||||
s.writeU32(28, m_reverseAPIDeviceIndex);
|
||||
s.writeU32(29, m_reverseAPIChannelIndex);
|
||||
|
||||
if (m_rollupState) {
|
||||
s.writeBlob(30, m_rollupState->serialize());
|
||||
}
|
||||
|
||||
s.writeS32(32, m_workspaceIndex);
|
||||
s.writeBlob(33, m_geometryBytes);
|
||||
s.writeBool(34, m_hidden);
|
||||
|
||||
return s.final();
|
||||
}
|
||||
|
||||
bool HeatMapSettings::deserialize(const QByteArray& data)
|
||||
{
|
||||
SimpleDeserializer d(data);
|
||||
|
||||
if(!d.isValid())
|
||||
{
|
||||
resetToDefaults();
|
||||
return false;
|
||||
}
|
||||
|
||||
if(d.getVersion() == 1)
|
||||
{
|
||||
QByteArray bytetmp;
|
||||
uint32_t utmp;
|
||||
QString strtmp;
|
||||
|
||||
d.readS32(1, &m_inputFrequencyOffset, 0);
|
||||
d.readFloat(2, &m_rfBandwidth, 16000.0f);
|
||||
d.readFloat(3, &m_minPower, -100.0f);
|
||||
d.readFloat(4, &m_maxPower, 0.0f);
|
||||
d.readString(5, &m_colorMapName, "Jet");
|
||||
d.readS32(6, (int*)&m_mode, (int)Average);
|
||||
d.readFloat(7, &m_pulseThreshold, 50.0f);
|
||||
d.readS32(8, &m_averagePeriodUS, 100000);
|
||||
d.readS32(9, &m_sampleRate, 100);
|
||||
d.readBool(10, &m_txPosValid, false);
|
||||
d.readFloat(11, &m_txLatitude);
|
||||
d.readFloat(12, &m_txLongitude);
|
||||
d.readFloat(13, &m_txPower);
|
||||
d.readBool(14, &m_displayChart, true);
|
||||
d.readBool(15, &m_displayAverage, true);
|
||||
d.readBool(16, &m_displayMax, true);
|
||||
d.readBool(17, &m_displayMin, true);
|
||||
d.readBool(18, &m_displayPulseAverage, true);
|
||||
d.readBool(19, &m_displayPathLoss, true);
|
||||
d.readS32(20, &m_displayMins, 2);
|
||||
|
||||
d.readU32(21, &m_rgbColor, QColor(102, 40, 220).rgb());
|
||||
d.readString(22, &m_title, "Heat Map");
|
||||
|
||||
if (m_channelMarker)
|
||||
{
|
||||
d.readBlob(23, &bytetmp);
|
||||
m_channelMarker->deserialize(bytetmp);
|
||||
}
|
||||
|
||||
d.readS32(24, &m_streamIndex, 0);
|
||||
d.readBool(25, &m_useReverseAPI, false);
|
||||
d.readString(26, &m_reverseAPIAddress, "127.0.0.1");
|
||||
d.readU32(27, &utmp, 0);
|
||||
|
||||
if ((utmp > 1023) && (utmp < 65535)) {
|
||||
m_reverseAPIPort = utmp;
|
||||
} else {
|
||||
m_reverseAPIPort = 8888;
|
||||
}
|
||||
|
||||
d.readU32(28, &utmp, 0);
|
||||
m_reverseAPIDeviceIndex = utmp > 99 ? 99 : utmp;
|
||||
d.readU32(29, &utmp, 0);
|
||||
m_reverseAPIChannelIndex = utmp > 99 ? 99 : utmp;
|
||||
|
||||
if (m_rollupState)
|
||||
{
|
||||
d.readBlob(30, &bytetmp);
|
||||
m_rollupState->deserialize(bytetmp);
|
||||
}
|
||||
|
||||
d.readS32(31, &m_workspaceIndex, 0);
|
||||
d.readBlob(32, &m_geometryBytes);
|
||||
d.readBool(33, &m_hidden, false);
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
resetToDefaults();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
84
plugins/channelrx/heatmap/heatmapsettings.h
Normal file
@ -0,0 +1,84 @@
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (C) 2017 Edouard Griffiths, F4EXB. //
|
||||
// Copyright (C) 2023 Jon Beniston, M7RCE //
|
||||
// //
|
||||
// This program is free software; you can redistribute it and/or modify //
|
||||
// it under the terms of the GNU General Public License as published by //
|
||||
// the Free Software Foundation as version 3 of the License, or //
|
||||
// (at your option) any later version. //
|
||||
// //
|
||||
// This program is distributed in the hope that it will be useful, //
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
|
||||
// GNU General Public License V3 for more details. //
|
||||
// //
|
||||
// You should have received a copy of the GNU General Public License //
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef INCLUDE_HEATMAPSETTINGS_H
|
||||
#define INCLUDE_HEATMAPSETTINGS_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
|
||||
#include "dsp/dsptypes.h"
|
||||
|
||||
class Serializable;
|
||||
|
||||
struct HeatMapSettings
|
||||
{
|
||||
qint32 m_inputFrequencyOffset;
|
||||
Real m_rfBandwidth;
|
||||
float m_minPower;
|
||||
float m_maxPower;
|
||||
QString m_colorMapName;
|
||||
enum Mode {
|
||||
None,
|
||||
Average,
|
||||
Max,
|
||||
Min,
|
||||
PulseAverage,
|
||||
PathLoss,
|
||||
} m_mode;
|
||||
float m_pulseThreshold;
|
||||
int m_averagePeriodUS;
|
||||
int m_sampleRate;
|
||||
bool m_txPosValid;
|
||||
float m_txLatitude;
|
||||
float m_txLongitude;
|
||||
float m_txPower;
|
||||
bool m_displayChart;
|
||||
bool m_displayAverage;
|
||||
bool m_displayMax;
|
||||
bool m_displayMin;
|
||||
bool m_displayPulseAverage;
|
||||
bool m_displayPathLoss;
|
||||
int m_displayMins;
|
||||
|
||||
quint32 m_rgbColor;
|
||||
QString m_title;
|
||||
Serializable *m_channelMarker;
|
||||
int m_streamIndex; //!< MIMO channel. Not relevant when connected to SI (single Rx).
|
||||
bool m_useReverseAPI;
|
||||
QString m_reverseAPIAddress;
|
||||
uint16_t m_reverseAPIPort;
|
||||
uint16_t m_reverseAPIDeviceIndex;
|
||||
uint16_t m_reverseAPIChannelIndex;
|
||||
Serializable *m_scopeGUI;
|
||||
Serializable *m_rollupState;
|
||||
int m_workspaceIndex;
|
||||
QByteArray m_geometryBytes;
|
||||
bool m_hidden;
|
||||
|
||||
HeatMapSettings();
|
||||
void resetToDefaults();
|
||||
void setChannelMarker(Serializable *channelMarker) { m_channelMarker = channelMarker; }
|
||||
void setRollupState(Serializable *rollupState) { m_rollupState = rollupState; }
|
||||
void setScopeGUI(Serializable *scopeGUI) { m_scopeGUI = scopeGUI; }
|
||||
QByteArray serialize() const;
|
||||
bool deserialize(const QByteArray& data);
|
||||
};
|
||||
|
||||
#endif /* INCLUDE_HEATMAPSETTINGS_H */
|
||||
|
229
plugins/channelrx/heatmap/heatmapsink.cpp
Normal file
@ -0,0 +1,229 @@
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (C) 2019 Edouard Griffiths, F4EXB //
|
||||
// Copyright (C) 2023 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 <QDebug>
|
||||
|
||||
#include <complex.h>
|
||||
|
||||
#include "dsp/dspengine.h"
|
||||
#include "dsp/datafifo.h"
|
||||
#include "dsp/scopevis.h"
|
||||
#include "util/db.h"
|
||||
#include "util/stepfunctions.h"
|
||||
#include "maincore.h"
|
||||
|
||||
#include "heatmap.h"
|
||||
#include "heatmapsink.h"
|
||||
|
||||
HeatMapSink::HeatMapSink(HeatMap *heatMap) :
|
||||
m_scopeSink(nullptr),
|
||||
m_heatMap(heatMap),
|
||||
m_channelSampleRate(10000),
|
||||
m_channelFrequencyOffset(0),
|
||||
m_magsq(0.0),
|
||||
m_magsqSum(0.0),
|
||||
m_magsqPeak(0.0),
|
||||
m_magsqCount(0),
|
||||
m_messageQueueToChannel(nullptr),
|
||||
m_sampleBufferSize(1000),
|
||||
m_sampleBufferIndex(0)
|
||||
{
|
||||
resetMagLevels();
|
||||
m_sampleBuffer.resize(m_sampleBufferSize);
|
||||
|
||||
applySettings(m_settings, true);
|
||||
applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, true);
|
||||
}
|
||||
|
||||
HeatMapSink::~HeatMapSink()
|
||||
{
|
||||
}
|
||||
|
||||
void HeatMapSink::sampleToScope(Complex sample)
|
||||
{
|
||||
if (m_scopeSink)
|
||||
{
|
||||
Real r = std::real(sample) * SDR_RX_SCALEF;
|
||||
Real i = std::imag(sample) * SDR_RX_SCALEF;
|
||||
m_sampleBuffer[m_sampleBufferIndex++] = Sample(r, i);
|
||||
|
||||
if (m_sampleBufferIndex >= m_sampleBufferSize)
|
||||
{
|
||||
std::vector<SampleVector::const_iterator> vbegin;
|
||||
vbegin.push_back(m_sampleBuffer.begin());
|
||||
m_scopeSink->feed(vbegin, m_sampleBufferSize);
|
||||
m_sampleBufferIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HeatMapSink::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end)
|
||||
{
|
||||
QMutexLocker mutexLocker(&m_mutex); // Is this too coarse, depending on size of sample vector?
|
||||
Complex ci;
|
||||
|
||||
for (SampleVector::const_iterator it = begin; it != end; ++it)
|
||||
{
|
||||
Complex c(it->real(), it->imag());
|
||||
c *= m_nco.nextIQ();
|
||||
|
||||
if (m_interpolatorDistance < 1.0f) // interpolate
|
||||
{
|
||||
while (!m_interpolator.interpolate(&m_interpolatorDistanceRemain, c, &ci))
|
||||
{
|
||||
processOneSample(ci);
|
||||
m_interpolatorDistanceRemain += m_interpolatorDistance;
|
||||
}
|
||||
}
|
||||
else // decimate
|
||||
{
|
||||
if (m_interpolator.decimate(&m_interpolatorDistanceRemain, c, &ci))
|
||||
{
|
||||
processOneSample(ci);
|
||||
m_interpolatorDistanceRemain += m_interpolatorDistance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HeatMapSink::processOneSample(Complex &ci)
|
||||
{
|
||||
Real re = ci.real() / SDR_RX_SCALEF;
|
||||
Real im = ci.imag() / SDR_RX_SCALEF;
|
||||
Real magsq = re*re + im*im;
|
||||
m_movingAverage(magsq);
|
||||
m_magsq = m_movingAverage.asDouble();
|
||||
m_magsqSum += magsq;
|
||||
if (magsq > m_magsqPeak) {
|
||||
m_magsqPeak = magsq;
|
||||
}
|
||||
m_magsqCount++;
|
||||
|
||||
// Although computationally more expensive to take the square root here,
|
||||
// it possibly reduces problems of accumulating numbers
|
||||
// that may differ significantly in magnitude, for long averages
|
||||
double mag = sqrt((double)magsq);
|
||||
|
||||
m_magSum += mag;
|
||||
if (mag > m_pulseThresholdLinear)
|
||||
{
|
||||
m_magPulseSum += mag;
|
||||
m_magPulseCount++;
|
||||
if (m_magPulseCount >= m_averageCnt)
|
||||
{
|
||||
m_magPulseAvg = m_magPulseSum / m_magPulseCount;
|
||||
m_magPulseSum = 0.0;
|
||||
m_magPulseCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (mag > m_magMaxPeak) {
|
||||
m_magMaxPeak = mag;
|
||||
}
|
||||
if (mag < m_magMinPeak) {
|
||||
m_magMinPeak = mag;
|
||||
}
|
||||
|
||||
m_magCount++;
|
||||
if (m_magCount >= m_averageCnt)
|
||||
{
|
||||
m_magAvg = m_magSum / m_magCount;
|
||||
m_magSum = 0.0;
|
||||
m_magCount = 0;
|
||||
}
|
||||
|
||||
// Sample to feed to scope (so we can see power at channel sample rate)
|
||||
Complex scopeSample;
|
||||
scopeSample.real(ci.real() / SDR_RX_SCALEF);
|
||||
scopeSample.imag(ci.imag() / SDR_RX_SCALEF);
|
||||
sampleToScope(scopeSample);
|
||||
}
|
||||
|
||||
void HeatMapSink::applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force)
|
||||
{
|
||||
qDebug() << "HeatMapSink::applyChannelSettings:"
|
||||
<< " channelSampleRate: " << channelSampleRate
|
||||
<< " channelFrequencyOffset: " << channelFrequencyOffset;
|
||||
|
||||
if ((m_channelFrequencyOffset != channelFrequencyOffset) ||
|
||||
(m_channelSampleRate != channelSampleRate) || force)
|
||||
{
|
||||
m_nco.setFreq(-channelFrequencyOffset, channelSampleRate);
|
||||
}
|
||||
|
||||
if ((m_channelSampleRate != channelSampleRate) || force)
|
||||
{
|
||||
m_interpolator.create(16, channelSampleRate, m_settings.m_rfBandwidth / 2.2);
|
||||
m_interpolatorDistance = (Real) channelSampleRate / (Real) m_settings.m_sampleRate;
|
||||
m_interpolatorDistanceRemain = m_interpolatorDistance;
|
||||
}
|
||||
|
||||
m_channelSampleRate = channelSampleRate;
|
||||
m_channelFrequencyOffset = channelFrequencyOffset;
|
||||
}
|
||||
|
||||
void HeatMapSink::applySettings(const HeatMapSettings& settings, bool force)
|
||||
{
|
||||
qDebug() << "HeatMapSink::applySettings:"
|
||||
<< " sampleRate: " << settings.m_sampleRate
|
||||
<< " averagePeriodUS: " << settings.m_averagePeriodUS
|
||||
<< " pulseThreshold: " << settings.m_pulseThreshold
|
||||
<< " force: " << force;
|
||||
|
||||
if ((settings.m_rfBandwidth != m_settings.m_rfBandwidth)
|
||||
|| (settings.m_averagePeriodUS != m_settings.m_averagePeriodUS)
|
||||
|| (settings.m_sampleRate != m_settings.m_sampleRate)
|
||||
|| force)
|
||||
{
|
||||
m_interpolator.create(16, m_channelSampleRate, settings.m_rfBandwidth / 2.2);
|
||||
m_interpolatorDistance = (Real) m_channelSampleRate / (Real) settings.m_sampleRate;
|
||||
m_interpolatorDistanceRemain = m_interpolatorDistance;
|
||||
}
|
||||
|
||||
if ((settings.m_averagePeriodUS != m_settings.m_averagePeriodUS)
|
||||
|| (settings.m_sampleRate != m_settings.m_sampleRate)
|
||||
|| force)
|
||||
{
|
||||
m_averageCnt = (int)((settings.m_averagePeriodUS * settings.m_sampleRate / 1e6));
|
||||
// For low sample rates, we want a small buffer, so scope update isn't too slow
|
||||
if (settings.m_sampleRate < 100) {
|
||||
m_sampleBufferSize = 1;
|
||||
} else if (settings.m_sampleRate <= 100) {
|
||||
m_sampleBufferSize = 10;
|
||||
} else if (settings.m_sampleRate <= 10000) {
|
||||
m_sampleBufferSize = 100;
|
||||
} else {
|
||||
m_sampleBufferSize = 1000;
|
||||
}
|
||||
qDebug() << "m_averageCnt" << m_averageCnt;
|
||||
qDebug() << "m_sampleBufferSize" << m_sampleBufferSize;
|
||||
m_sampleBuffer.resize(m_sampleBufferSize);
|
||||
if (m_sampleBufferIndex >= m_sampleBufferSize) {
|
||||
m_sampleBufferIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if ((settings.m_pulseThreshold != m_settings.m_pulseThreshold) || force)
|
||||
{
|
||||
m_pulseThresholdLinear = std::pow(10.0, settings.m_pulseThreshold / 20.0);
|
||||
qDebug() << "m_pulseThresholdLinear" << m_pulseThresholdLinear;
|
||||
}
|
||||
|
||||
m_settings = settings;
|
||||
}
|
||||
|
159
plugins/channelrx/heatmap/heatmapsink.h
Normal file
@ -0,0 +1,159 @@
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (C) 2019 Edouard Griffiths, F4EXB //
|
||||
// Copyright (C) 2023 Jon Beniston, M7RCE //
|
||||
// //
|
||||
// This program is free software; you can redistribute it and/or modify //
|
||||
// it under the terms of the GNU General Public License as published by //
|
||||
// the Free Software Foundation as version 3 of the License, or //
|
||||
// (at your option) any later version. //
|
||||
// //
|
||||
// This program is distributed in the hope that it will be useful, //
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
|
||||
// GNU General Public License V3 for more details. //
|
||||
// //
|
||||
// You should have received a copy of the GNU General Public License //
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef INCLUDE_HEATMAPSINK_H
|
||||
#define INCLUDE_HEATMAPSINK_H
|
||||
|
||||
#include <QVector>
|
||||
#include <QMutex>
|
||||
|
||||
#include "dsp/channelsamplesink.h"
|
||||
#include "dsp/phasediscri.h"
|
||||
#include "dsp/nco.h"
|
||||
#include "dsp/interpolator.h"
|
||||
#include "dsp/firfilter.h"
|
||||
#include "dsp/gaussian.h"
|
||||
#include "util/movingaverage.h"
|
||||
#include "util/doublebufferfifo.h"
|
||||
#include "util/messagequeue.h"
|
||||
#include "util/crc.h"
|
||||
|
||||
#include "heatmapsettings.h"
|
||||
|
||||
#include <vector>
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
|
||||
class ChannelAPI;
|
||||
class HeatMap;
|
||||
class ScopeVis;
|
||||
|
||||
class HeatMapSink : public ChannelSampleSink {
|
||||
public:
|
||||
HeatMapSink(HeatMap *heatMap);
|
||||
~HeatMapSink();
|
||||
|
||||
virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end);
|
||||
|
||||
void setScopeSink(ScopeVis* scopeSink) { m_scopeSink = scopeSink; }
|
||||
void applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force = false);
|
||||
void applySettings(const HeatMapSettings& settings, bool force = false);
|
||||
void setMessageQueueToChannel(MessageQueue *messageQueue) { m_messageQueueToChannel = messageQueue; }
|
||||
void setChannel(ChannelAPI *channel) { m_channel = channel; }
|
||||
|
||||
double getMagSq() const { return m_magsq; }
|
||||
|
||||
void getMagSqLevels(double& avg, double &peak, int& nbSamples)
|
||||
{
|
||||
if (m_magsqCount > 0)
|
||||
{
|
||||
m_magsq = m_magsqSum / m_magsqCount;
|
||||
m_magSqLevelStore.m_magsq = m_magsq;
|
||||
m_magSqLevelStore.m_magsqPeak = m_magsqPeak;
|
||||
}
|
||||
|
||||
avg = m_magSqLevelStore.m_magsq;
|
||||
peak = m_magSqLevelStore.m_magsqPeak;
|
||||
nbSamples = m_magsqCount == 0 ? 1 : m_magsqCount;
|
||||
|
||||
m_magsqSum = 0.0;
|
||||
m_magsqPeak = 0.0;
|
||||
m_magsqCount = 0;
|
||||
}
|
||||
|
||||
void getMagLevels(double& avg, double& pulseAvg, double &maxPeak, double &minPeak)
|
||||
{
|
||||
QMutexLocker mutexLocker(&m_mutex);
|
||||
avg = m_magAvg;
|
||||
pulseAvg = m_magPulseAvg;
|
||||
maxPeak = m_magMaxPeak;
|
||||
minPeak = m_magMinPeak;
|
||||
}
|
||||
|
||||
void resetMagLevels()
|
||||
{
|
||||
QMutexLocker mutexLocker(&m_mutex);
|
||||
m_magSum = 0.0;
|
||||
m_magCount = 0;
|
||||
m_magAvg = std::numeric_limits<double>::quiet_NaN();
|
||||
m_magPulseSum = 0.0;
|
||||
m_magPulseCount = 0;
|
||||
m_magPulseAvg = std::numeric_limits<double>::quiet_NaN();
|
||||
m_magMinPeak = std::numeric_limits<double>::max();
|
||||
m_magMaxPeak = -std::numeric_limits<double>::max();
|
||||
}
|
||||
|
||||
private:
|
||||
struct MagSqLevelsStore
|
||||
{
|
||||
MagSqLevelsStore() :
|
||||
m_magsq(1e-12),
|
||||
m_magsqPeak(1e-12)
|
||||
{}
|
||||
double m_magsq;
|
||||
double m_magsqPeak;
|
||||
};
|
||||
|
||||
ScopeVis* m_scopeSink; // Scope GUI to display filtered power
|
||||
HeatMap *m_heatMap;
|
||||
HeatMapSettings m_settings;
|
||||
ChannelAPI *m_channel;
|
||||
int m_channelSampleRate;
|
||||
int m_channelFrequencyOffset;
|
||||
int m_sinkSampleRate;
|
||||
|
||||
NCO m_nco;
|
||||
Interpolator m_interpolator;
|
||||
Real m_interpolatorDistance;
|
||||
Real m_interpolatorDistanceRemain;
|
||||
|
||||
// For power meter in GUI (same as other channels)
|
||||
double m_magsq;
|
||||
double m_magsqSum;
|
||||
double m_magsqPeak;
|
||||
int m_magsqCount;
|
||||
MagSqLevelsStore m_magSqLevelStore;
|
||||
MovingAverageUtil<Real, double, 16> m_movingAverage;
|
||||
|
||||
// For heat map
|
||||
double m_magSum;
|
||||
double m_magCount;
|
||||
double m_magAvg;
|
||||
double m_magPulseSum;
|
||||
double m_magPulseCount;
|
||||
double m_magPulseAvg;
|
||||
double m_magMaxPeak;
|
||||
double m_magMinPeak;
|
||||
|
||||
int m_averageCnt;
|
||||
double m_pulseThresholdLinear;
|
||||
|
||||
MessageQueue *m_messageQueueToChannel;
|
||||
QMutex m_mutex;
|
||||
|
||||
SampleVector m_sampleBuffer;
|
||||
int m_sampleBufferSize;
|
||||
int m_sampleBufferIndex;
|
||||
|
||||
void processOneSample(Complex &ci);
|
||||
MessageQueue *getMessageQueueToChannel() { return m_messageQueueToChannel; }
|
||||
void sampleToScope(Complex sample);
|
||||
};
|
||||
|
||||
#endif // INCLUDE_HEATMAPSINK_H
|
||||
|
53
plugins/channelrx/heatmap/heatmapwebapiadapter.cpp
Normal file
@ -0,0 +1,53 @@
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (C) 2019 Edouard Griffiths, F4EXB. //
|
||||
// Copyright (C) 2023 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 "SWGChannelSettings.h"
|
||||
#include "heatmap.h"
|
||||
#include "heatmapwebapiadapter.h"
|
||||
|
||||
HeatMapWebAPIAdapter::HeatMapWebAPIAdapter()
|
||||
{}
|
||||
|
||||
HeatMapWebAPIAdapter::~HeatMapWebAPIAdapter()
|
||||
{}
|
||||
|
||||
int HeatMapWebAPIAdapter::webapiSettingsGet(
|
||||
SWGSDRangel::SWGChannelSettings& response,
|
||||
QString& errorMessage)
|
||||
{
|
||||
(void) errorMessage;
|
||||
response.setHeatMapSettings(new SWGSDRangel::SWGHeatMapSettings());
|
||||
response.getHeatMapSettings()->init();
|
||||
HeatMap::webapiFormatChannelSettings(response, m_settings);
|
||||
|
||||
return 200;
|
||||
}
|
||||
|
||||
int HeatMapWebAPIAdapter::webapiSettingsPutPatch(
|
||||
bool force,
|
||||
const QStringList& channelSettingsKeys,
|
||||
SWGSDRangel::SWGChannelSettings& response,
|
||||
QString& errorMessage)
|
||||
{
|
||||
(void) force;
|
||||
(void) errorMessage;
|
||||
HeatMap::webapiUpdateChannelSettings(m_settings, channelSettingsKeys, response);
|
||||
|
||||
return 200;
|
||||
}
|
||||
|
51
plugins/channelrx/heatmap/heatmapwebapiadapter.h
Normal file
@ -0,0 +1,51 @@
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (C) 2019 Edouard Griffiths, F4EXB. //
|
||||
// Copyright (C) 2023 Jon Beniston, M7RCE //
|
||||
// //
|
||||
// This program is free software; you can redistribute it and/or modify //
|
||||
// it under the terms of the GNU General Public License as published by //
|
||||
// the Free Software Foundation as version 3 of the License, or //
|
||||
// (at your option) any later version. //
|
||||
// //
|
||||
// This program is distributed in the hope that it will be useful, //
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
|
||||
// GNU General Public License V3 for more details. //
|
||||
// //
|
||||
// You should have received a copy of the GNU General Public License //
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef INCLUDE_HEATMAP_WEBAPIADAPTER_H
|
||||
#define INCLUDE_HEATMAP_WEBAPIADAPTER_H
|
||||
|
||||
#include "channel/channelwebapiadapter.h"
|
||||
#include "heatmapsettings.h"
|
||||
|
||||
/**
|
||||
* Standalone API adapter only for the settings
|
||||
*/
|
||||
class HeatMapWebAPIAdapter : public ChannelWebAPIAdapter {
|
||||
public:
|
||||
HeatMapWebAPIAdapter();
|
||||
virtual ~HeatMapWebAPIAdapter();
|
||||
|
||||
virtual QByteArray serialize() const { return m_settings.serialize(); }
|
||||
virtual bool deserialize(const QByteArray& data) { return m_settings.deserialize(data); }
|
||||
|
||||
virtual int webapiSettingsGet(
|
||||
SWGSDRangel::SWGChannelSettings& response,
|
||||
QString& errorMessage);
|
||||
|
||||
virtual int webapiSettingsPutPatch(
|
||||
bool force,
|
||||
const QStringList& channelSettingsKeys,
|
||||
SWGSDRangel::SWGChannelSettings& response,
|
||||
QString& errorMessage);
|
||||
|
||||
private:
|
||||
HeatMapSettings m_settings;
|
||||
};
|
||||
|
||||
#endif // INCLUDE_HEATMAP_WEBAPIADAPTER_H
|
||||
|
159
plugins/channelrx/heatmap/readme.md
Normal file
@ -0,0 +1,159 @@
|
||||
<h1>Heat Map plugin</h1>
|
||||
|
||||
<h2>Introduction</h2>
|
||||
|
||||
This plugin can be used to generate a heat map based on RF channel power. Channel power is measured as average power, maximum peak power, minimum peak power as well as pulse average power (i.e. average power above a threshold).
|
||||
|
||||
To view the Heat Map visually, the [Map Feature](../../feature/map/readme.md) should be opened. If using the 3D map, it is recommended to set the Terrain to Ellisoid (as the heat map is 2D).
|
||||
|
||||
To record data for a heat map, a GPS is required, and Preferences > My Position should have "Auto-update from GPS" enabled.
|
||||
|
||||
On Android, GPS setup should be automatic. On Windows/Linux/Mac, a GPS supporting NMEA via a serial port at 4800 baud is required.
|
||||
The COM port / serial device should be specfied via the QT_NMEA_SERIAL_PORT environment variable before SDRangel is started.
|
||||
|
||||
![A Heat Map](../../../doc/img/HeatMap_plugin_map.png)
|
||||
|
||||
<h2>Interface</h2>
|
||||
|
||||
The top and bottom bars of the channel window are described [here](../../../sdrgui/channel/readme.md)
|
||||
|
||||
![Heat Map plugin GUI](../../../doc/img/HeatMap_plugin_settings.png)
|
||||
|
||||
<h3>1: Frequency shift from center frequency of reception</h3>
|
||||
|
||||
Use the wheels to adjust the frequency shift in Hz from the center frequency of reception. Left click on a digit sets the cursor position at this digit. 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.
|
||||
|
||||
<h3>2: Channel power</h3>
|
||||
|
||||
Average total power in dB relative to a +/- 1.0 amplitude signal received in the pass band.
|
||||
|
||||
<h3>3: Level meter in dB</h3>
|
||||
|
||||
- top bar (green): average value
|
||||
- bottom bar (blue green): instantaneous peak value
|
||||
- tip vertical bar (bright green): peak hold value
|
||||
|
||||
<h3>4: BW - RF Bandwidth</h3>
|
||||
|
||||
Bandwidth in Hz of the channel for which power is to be measured.
|
||||
|
||||
<h3>5: Res - Resolution</h3>
|
||||
|
||||
Displays the heat map resolution in metres per pixel. Currently this is fixed at ~3m per pixel.
|
||||
|
||||
<h3>6: SR - Sample Rate</h3>
|
||||
|
||||
Sets the sample rate at which channel power is sampled and measured. Values range from 1MS/s to 100S/s in powers of 10.
|
||||
|
||||
<h3>7: Tavg - Average Time</h3>
|
||||
|
||||
Time period overwhich the channel power is averaged. Values range from 10us to 10s in powers of 10. The available values depend upon the sample rate.
|
||||
|
||||
<h3>8: THp - Pulse Threshold</h3>
|
||||
|
||||
The pulse threshold sets the power in dB for which the channel power needs to exceed, in order to be included in the pulse average power measurement.
|
||||
|
||||
<h3>9: TX - Enable Transmitter</h3>
|
||||
|
||||
When checked, enables the position of the transmitter of the signal being mapped to be set and its transmit power level. This enables range and path loss calculations.
|
||||
|
||||
<h3>10: Transmitter Latitude</h3>
|
||||
|
||||
Specifies the latitude of the transmitter in decimal degrees, North positive.
|
||||
|
||||
<h3>11: Transmitter Longitude</h3>
|
||||
|
||||
Specifies the longitude of the transmitter in decimal degrees, East postive.
|
||||
|
||||
<h3>12: Transmitter Power</h3>
|
||||
|
||||
Specifies the power of the transmitter in dB.
|
||||
|
||||
<h3>13: Set Transmitter Position</h3>
|
||||
|
||||
When clicked, sets the transmitter position (10, 11) to the current position.
|
||||
|
||||
<h3>14: Min - Colour Map Mininimum Power</h3>
|
||||
|
||||
The colour map minimum power field, specifies the power in dB, below which, power measurements are not plotted on the heat map, and above which are plotted using the colour map (16).
|
||||
|
||||
<h3>15: Max - Colour Map Maximum Power</h3>
|
||||
|
||||
The colour map maximum power field, specifies the power in dB, above which, all power measurements are mapped to the highest entry in the colour map (16).
|
||||
|
||||
<h3>16: Colour Map</h3>
|
||||
|
||||
Selects the colour map used to linearly map power measurements between the minimum (14) and maximum (15) fields to the colour of pixels that will be plotted on the map.
|
||||
|
||||
<h3>17: Latitude</h3>
|
||||
|
||||
Displays current latitude in decimal degrees. North positive.
|
||||
|
||||
<h3>18: Longitude</h3>
|
||||
|
||||
Displays current longitude in decimal degrees. East positive.
|
||||
|
||||
<h3>19: Range</h3>
|
||||
|
||||
Displays the range from the current position to the transmitter position in metres or kilometres.
|
||||
|
||||
<h3>20: Loss</h3>
|
||||
|
||||
Displays the free space path loss from the current position to the transmitter, based on the channel centre frequency, in dB.
|
||||
|
||||
<h3>21: Avg - Average Power</h3>
|
||||
|
||||
Displays the most recent average power measurement in dB.
|
||||
|
||||
<h3>22: Max - Max Peak Power</h3>
|
||||
|
||||
Displays the current maximum peak power measurement in dB.
|
||||
|
||||
<h3>23: Min - Min Peak Power</h3>
|
||||
|
||||
Displays the current minimum peak power measurement in dB.
|
||||
|
||||
<h3>24: Pulse - Pulse Average Power</h3>
|
||||
|
||||
Displays the most recent pulse average power measurement in dB.
|
||||
|
||||
<h3>25: Data</h3>
|
||||
|
||||
Selects which power data should be plotted on the map, and also which data will be saved (26, 27) or read (28).
|
||||
|
||||
<h3>26: Display Chart</h3>
|
||||
|
||||
Toggles whether the chart (31) is displayed.
|
||||
|
||||
<h3>27: Save to image</h3>
|
||||
|
||||
Saves the heat map power data selected by (25) to an image file.
|
||||
|
||||
<h3>28: Save to CSV file</h3>
|
||||
|
||||
Saves the heat map power data selected by (25) to a CSV file.
|
||||
|
||||
<h3>29: Read from CSV file</h3>
|
||||
|
||||
Reads a heat map from a CSV file.
|
||||
|
||||
<h3>30: Clear Heat Map</h3>
|
||||
|
||||
Clears all heat map power data.
|
||||
|
||||
<h3>31: Chart</h3>
|
||||
|
||||
The chart displays the most recent average, max peak, min peak and pulse average measurements as well as an estimated
|
||||
received signal level from the transmitter (9), taking in to account free space path loss.
|
||||
|
||||
Each series can be individually enabled or disabled.
|
||||
The last number of minutes worth of data displayed can be specified from 1 to 10 minutes.
|
||||
|
||||
<h2>Power Measurements</h2>
|
||||
|
||||
The following figure shows the difference between average power, max peak power, min peak power and pulse average power, when the averaging time is over the full time of the graph.
|
||||
|
||||
![Power Measurements](../../../doc/img/HeatMap_plugin_power.png)
|
||||
|
||||
The 'Path loss' map displays free space path loss based on the range to the transmitter (10, 11) and centre frequency.
|
||||
|
@ -239,6 +239,16 @@ AISGUI::AISGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur
|
||||
|
||||
AISGUI::~AISGUI()
|
||||
{
|
||||
// Remove all ships from map
|
||||
for (int row = ui->vessels->rowCount() - 1; row >= 0; row--)
|
||||
{
|
||||
QString mmsi = ui->vessels->item(row, VESSEL_COL_MMSI)->text();
|
||||
sendToMap(mmsi, "",
|
||||
"", "",
|
||||
"", 0.0f, 0.0f,
|
||||
0.0f, 0.0f, QDateTime(),
|
||||
0.0f);
|
||||
}
|
||||
qDeleteAll(m_vessels);
|
||||
delete ui;
|
||||
}
|
||||
@ -343,8 +353,8 @@ void AISGUI::removeOldVessels()
|
||||
0.0f);
|
||||
// Remove from table
|
||||
ui->vessels->removeRow(row);
|
||||
// Remove from hash
|
||||
m_vessels.remove(mmsi);
|
||||
// Remove from hash and free memory
|
||||
delete m_vessels.take(mmsi);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -458,8 +468,11 @@ void AISGUI::sendToMap(const QString &name, const QString &label,
|
||||
swgMapItem->setAltitude(0);
|
||||
swgMapItem->setAltitudeReference(1); // CLAMP_TO_GROUND
|
||||
|
||||
if (positionDateTime.isValid()) {
|
||||
if (positionDateTime.isValid())
|
||||
{
|
||||
swgMapItem->setPositionDateTime(new QString(positionDateTime.toString(Qt::ISODateWithMs)));
|
||||
swgMapItem->setOrientationDateTime(new QString(positionDateTime.toString(Qt::ISODateWithMs)));
|
||||
swgMapItem->setAvailableUntil(new QString(positionDateTime.addSecs(10*60).toString(Qt::ISODateWithMs)));
|
||||
}
|
||||
|
||||
swgMapItem->setImageRotation(heading);
|
||||
|
@ -355,21 +355,31 @@ bool APRSGUI::handleMessage(const Message& message)
|
||||
MessageQueue *messageQueue = qobject_cast<MessageQueue*>(pipe->m_element);
|
||||
SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem();
|
||||
|
||||
QString name;
|
||||
if (!aprs->m_objectName.isEmpty()) {
|
||||
swgMapItem->setName(new QString(aprs->m_objectName));
|
||||
name = aprs->m_objectName;
|
||||
} else {
|
||||
swgMapItem->setName(new QString(aprs->m_from));
|
||||
name = aprs->m_from;
|
||||
}
|
||||
swgMapItem->setName(new QString(name));
|
||||
|
||||
swgMapItem->setLatitude(aprs->m_latitude);
|
||||
swgMapItem->setLongitude(aprs->m_longitude);
|
||||
swgMapItem->setAltitude(aprs->m_hasAltitude ? Units::feetToMetres(aprs->m_altitudeFt) : 0);
|
||||
swgMapItem->setAltitudeReference(1); // CLAMP_TO_GROUND
|
||||
|
||||
swgMapItem->setFixedPosition(false);
|
||||
if (aprs->m_hasTimestamp) {
|
||||
swgMapItem->setPositionDateTime(new QString(aprs->m_timestamp.toString(Qt::ISODateWithMs)));
|
||||
} else {
|
||||
swgMapItem->setPositionDateTime(new QString(QDateTime::currentDateTime().toString(Qt::ISODateWithMs)));
|
||||
}
|
||||
// Need to set availableUntil for 3D track to be displayed
|
||||
swgMapItem->setAvailableUntil(new QString(QDateTime::currentDateTime().addDays(1).toString(Qt::ISODateWithMs)));
|
||||
if (aprs->m_objectKilled)
|
||||
{
|
||||
swgMapItem->setImage(new QString(""));
|
||||
swgMapItem->setText(new QString(""));
|
||||
m_mapItems.remove(name);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -385,6 +395,9 @@ bool APRSGUI::handleMessage(const Message& message)
|
||||
m_settings.m_rainfallUnits == APRSSettings::MILLIMETRE
|
||||
)
|
||||
));
|
||||
if (!m_mapItems.contains(name)) {
|
||||
m_mapItems.insert(name, true);
|
||||
}
|
||||
}
|
||||
|
||||
MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_aprs, swgMapItem);
|
||||
@ -577,9 +590,33 @@ APRSGUI::APRSGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feat
|
||||
|
||||
APRSGUI::~APRSGUI()
|
||||
{
|
||||
QHashIterator<QString, bool> itr(m_mapItems);
|
||||
while (itr.hasNext())
|
||||
{
|
||||
itr.next();
|
||||
removeFromMap(itr.key());
|
||||
}
|
||||
delete ui;
|
||||
}
|
||||
|
||||
void APRSGUI::removeFromMap(const QString& name)
|
||||
{
|
||||
QList<ObjectPipe*> mapPipes;
|
||||
MainCore::instance()->getMessagePipes().getMessagePipes(m_aprs, "mapitems", mapPipes);
|
||||
|
||||
for (const auto& pipe : mapPipes)
|
||||
{
|
||||
MessageQueue *messageQueue = qobject_cast<MessageQueue*>(pipe->m_element);
|
||||
SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem();
|
||||
|
||||
swgMapItem->setName(new QString(name));
|
||||
swgMapItem->setImage(new QString(""));
|
||||
|
||||
MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_aprs, swgMapItem);
|
||||
messageQueue->push(msg);
|
||||
}
|
||||
}
|
||||
|
||||
void APRSGUI::setWorkspaceIndex(int index)
|
||||
{
|
||||
m_settings.m_workspaceIndex = index;
|
||||
|
@ -151,9 +151,12 @@ private:
|
||||
QDateTimeAxis m_motionChartXAxis;
|
||||
QValueAxis m_motionChartYAxis;
|
||||
|
||||
QHash<QString, bool> m_mapItems;
|
||||
|
||||
explicit APRSGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent = nullptr);
|
||||
virtual ~APRSGUI();
|
||||
|
||||
void removeFromMap(const QString& name);
|
||||
void blockApplySettings(bool block);
|
||||
void applySettings(bool force = false);
|
||||
void displayTableSettings(QTableWidget *table, QMenu *menu, int *columnIndexes, int *columnSizes, int columns);
|
||||
|
@ -54,12 +54,14 @@ if(NOT SERVER_MODE)
|
||||
mapradiotimedialog.ui
|
||||
mapcolordialog.cpp
|
||||
mapmodel.cpp
|
||||
mapitem.cpp
|
||||
mapwebsocketserver.cpp
|
||||
cesiuminterface.cpp
|
||||
czml.cpp
|
||||
map.qrc
|
||||
icons.qrc
|
||||
cesium.qrc
|
||||
data.qrc
|
||||
)
|
||||
set(map_HEADERS
|
||||
${map_HEADERS}
|
||||
@ -72,6 +74,7 @@ if(NOT SERVER_MODE)
|
||||
mapradiotimedialog.h
|
||||
mapcolordialog.h
|
||||
mapmodel.h
|
||||
mapitem.h
|
||||
mapwebsocketserver.h
|
||||
cesiuminterface.h
|
||||
czml.h
|
||||
|
@ -231,8 +231,25 @@ void CesiumInterface::czml(QJsonObject &obj)
|
||||
send(obj);
|
||||
}
|
||||
|
||||
void CesiumInterface::update(MapItem *mapItem, bool isTarget, bool isSelected)
|
||||
void CesiumInterface::update(ObjectMapItem *mapItem, bool isTarget, bool isSelected)
|
||||
{
|
||||
QJsonObject obj = m_czml.update(mapItem, isTarget, isSelected);
|
||||
czml(obj);
|
||||
}
|
||||
|
||||
void CesiumInterface::update(PolygonMapItem *mapItem)
|
||||
{
|
||||
QJsonObject obj = m_czml.update(mapItem);
|
||||
czml(obj);
|
||||
}
|
||||
|
||||
void CesiumInterface::update(PolylineMapItem *mapItem)
|
||||
{
|
||||
QJsonObject obj = m_czml.update(mapItem);
|
||||
czml(obj);
|
||||
}
|
||||
|
||||
void CesiumInterface::setPosition(const QGeoCoordinate& position)
|
||||
{
|
||||
m_czml.setPosition(position);
|
||||
}
|
||||
|
@ -22,7 +22,9 @@
|
||||
#include "czml.h"
|
||||
#include "SWGMapAnimation.h"
|
||||
|
||||
class MapItem;
|
||||
class ObjectMapItem;
|
||||
class PolygonMapItem;
|
||||
class PolylineMapItem;
|
||||
|
||||
class CesiumInterface : public MapWebSocketServer
|
||||
{
|
||||
@ -72,7 +74,10 @@ public:
|
||||
void removeAllCZMLEntities();
|
||||
void initCZML();
|
||||
void czml(QJsonObject &obj);
|
||||
void update(MapItem *mapItem, bool isTarget, bool isSelected);
|
||||
void update(ObjectMapItem *mapItem, bool isTarget, bool isSelected);
|
||||
void update(PolygonMapItem *mapItem);
|
||||
void update(PolylineMapItem *mapItem);
|
||||
void setPosition(const QGeoCoordinate& position);
|
||||
|
||||
protected:
|
||||
|
||||
|
@ -28,6 +28,22 @@ CZML::CZML(const MapSettings *settings) :
|
||||
{
|
||||
}
|
||||
|
||||
// Set position from which distance filter is calculated
|
||||
void CZML::setPosition(const QGeoCoordinate& position)
|
||||
{
|
||||
m_position = position;
|
||||
}
|
||||
|
||||
bool CZML::filter(const MapItem *mapItem) const
|
||||
{
|
||||
return ( !mapItem->m_itemSettings->m_filterName.isEmpty()
|
||||
&& !mapItem->m_itemSettings->m_filterNameRE.match(mapItem->m_name).hasMatch()
|
||||
)
|
||||
|| ( (mapItem->m_itemSettings->m_filterDistance > 0)
|
||||
&& (m_position.distanceTo(QGeoCoordinate(mapItem->m_latitude, mapItem->m_longitude, mapItem->m_altitude)) > mapItem->m_itemSettings->m_filterDistance)
|
||||
);
|
||||
}
|
||||
|
||||
QJsonObject CZML::init()
|
||||
{
|
||||
QString start = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
|
||||
@ -48,11 +64,150 @@ QJsonObject CZML::init()
|
||||
return doc;
|
||||
}
|
||||
|
||||
QJsonObject CZML::update(PolygonMapItem *mapItem)
|
||||
{
|
||||
QString id = mapItem->m_name;
|
||||
|
||||
QJsonObject CZML::update(MapItem *mapItem, bool isTarget, bool isSelected)
|
||||
QJsonObject obj {
|
||||
{"id", id} // id must be unique
|
||||
};
|
||||
|
||||
if ( !mapItem->m_itemSettings->m_enabled
|
||||
|| !mapItem->m_itemSettings->m_display3DTrack
|
||||
|| filter(mapItem)
|
||||
)
|
||||
{
|
||||
// Delete obj completely (including any history)
|
||||
obj.insert("delete", true);
|
||||
return obj;
|
||||
}
|
||||
|
||||
QJsonArray positions;
|
||||
for (const auto c : mapItem->m_points)
|
||||
{
|
||||
positions.append(c->longitude());
|
||||
positions.append(c->latitude());
|
||||
positions.append(c->altitude());
|
||||
}
|
||||
|
||||
QJsonObject positionList {
|
||||
{"cartographicDegrees", positions},
|
||||
};
|
||||
|
||||
QColor color = QColor::fromRgba(mapItem->m_itemSettings->m_3DTrackColor);
|
||||
QJsonArray colorRGBA {
|
||||
color.red(), color.green(), color.blue(), color.alpha()
|
||||
};
|
||||
QJsonObject colorObj {
|
||||
{"rgba", colorRGBA}
|
||||
};
|
||||
|
||||
QJsonObject solidColor {
|
||||
{"color", colorObj},
|
||||
};
|
||||
|
||||
QJsonObject material {
|
||||
{"solidColor", solidColor}
|
||||
};
|
||||
|
||||
QJsonArray outlineColorRGBA {
|
||||
0, 0, 0, 255
|
||||
};
|
||||
QJsonObject outlineColor {
|
||||
{"rgba", outlineColorRGBA}
|
||||
};
|
||||
|
||||
QJsonObject polygon {
|
||||
{"positions", positionList},
|
||||
{"height", mapItem->m_altitude},
|
||||
{"extrudedHeight", mapItem->m_extrudedHeight},
|
||||
{"material", material},
|
||||
{"outline", true},
|
||||
{"outlineColor", outlineColor}
|
||||
};
|
||||
|
||||
obj.insert("polygon", polygon);
|
||||
obj.insert("description", mapItem->m_label);
|
||||
|
||||
//qDebug() << "Polygon " << obj;
|
||||
return obj;
|
||||
}
|
||||
|
||||
QJsonObject CZML::update(PolylineMapItem *mapItem)
|
||||
{
|
||||
QString id = mapItem->m_name;
|
||||
|
||||
QJsonObject obj {
|
||||
{"id", id} // id must be unique
|
||||
};
|
||||
|
||||
if ( !mapItem->m_itemSettings->m_enabled
|
||||
|| !mapItem->m_itemSettings->m_display3DTrack
|
||||
|| filter(mapItem)
|
||||
)
|
||||
{
|
||||
// Delete obj completely (including any history)
|
||||
obj.insert("delete", true);
|
||||
return obj;
|
||||
}
|
||||
|
||||
QJsonArray positions;
|
||||
for (const auto c : mapItem->m_points)
|
||||
{
|
||||
positions.append(c->longitude());
|
||||
positions.append(c->latitude());
|
||||
positions.append(c->altitude());
|
||||
}
|
||||
|
||||
QJsonObject positionList {
|
||||
{"cartographicDegrees", positions},
|
||||
};
|
||||
|
||||
QColor color = QColor::fromRgba(mapItem->m_itemSettings->m_3DTrackColor);
|
||||
QJsonArray colorRGBA {
|
||||
color.red(), color.green(), color.blue(), color.alpha()
|
||||
};
|
||||
QJsonObject colorObj {
|
||||
{"rgba", colorRGBA}
|
||||
};
|
||||
|
||||
QJsonObject solidColor {
|
||||
{"color", colorObj},
|
||||
};
|
||||
|
||||
QJsonObject material {
|
||||
{"solidColor", solidColor}
|
||||
};
|
||||
|
||||
QJsonObject polyline {
|
||||
{"positions", positionList},
|
||||
{"material", material}
|
||||
};
|
||||
|
||||
obj.insert("polyline", polyline);
|
||||
obj.insert("description", mapItem->m_label);
|
||||
|
||||
//qDebug() << "Polyline " << obj;
|
||||
return obj;
|
||||
}
|
||||
|
||||
QJsonObject CZML::update(ObjectMapItem *mapItem, bool isTarget, bool isSelected)
|
||||
{
|
||||
(void) isTarget;
|
||||
|
||||
QString id = mapItem->m_name;
|
||||
|
||||
QJsonObject obj {
|
||||
{"id", id} // id must be unique
|
||||
};
|
||||
|
||||
if (!mapItem->m_itemSettings->m_enabled || filter(mapItem))
|
||||
{
|
||||
// Delete obj completely (including any history)
|
||||
obj.insert("delete", true);
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Don't currently use CLIP_TO_GROUND in Cesium due to Jitter bug
|
||||
// https://github.com/CesiumGS/cesium/issues/4049
|
||||
// Instead we implement our own clipping code in map3d.html
|
||||
@ -65,8 +220,6 @@ QJsonObject CZML::update(MapItem *mapItem, bool isTarget, bool isSelected)
|
||||
dt = QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs);
|
||||
}
|
||||
|
||||
QString id = mapItem->m_name;
|
||||
|
||||
// Keep a hash of the time we first saw each item
|
||||
bool existingId = m_ids.contains(id);
|
||||
if (!existingId) {
|
||||
@ -77,7 +230,7 @@ QJsonObject CZML::update(MapItem *mapItem, bool isTarget, bool isSelected)
|
||||
bool fixedPosition = mapItem->m_fixedPosition;
|
||||
if (mapItem->m_image == "")
|
||||
{
|
||||
// Need to remove this from the map
|
||||
// Need to remove this from the map (but history is retained)
|
||||
removeObj = true;
|
||||
}
|
||||
|
||||
@ -143,10 +296,10 @@ QJsonObject CZML::update(MapItem *mapItem, bool isTarget, bool isSelected)
|
||||
hasMoved = true;
|
||||
m_hasMoved.insert(id, true);
|
||||
}
|
||||
if (hasMoved)
|
||||
if (hasMoved && (mapItem->m_itemSettings->m_extrapolate > 0))
|
||||
{
|
||||
position.insert("forwardExtrapolationType", "EXTRAPOLATE");
|
||||
position.insert("forwardExtrapolationDuration", 60);
|
||||
position.insert("forwardExtrapolationDuration", mapItem->m_itemSettings->m_extrapolate);
|
||||
// Use linear interpolation for now - other two can go crazy with aircraft on the ground
|
||||
//position.insert("interpolationAlgorithm", "HERMITE");
|
||||
//position.insert("interpolationDegree", "2");
|
||||
@ -182,7 +335,7 @@ QJsonObject CZML::update(MapItem *mapItem, bool isTarget, bool isSelected)
|
||||
QJsonObject orientation {
|
||||
{"unitQuaternion", quaternion},
|
||||
{"forwardExtrapolationType", "HOLD"}, // If we extrapolate, aircraft tend to spin around
|
||||
{"forwardExtrapolationDuration", 60},
|
||||
{"forwardExtrapolationDuration", mapItem->m_itemSettings->m_extrapolate},
|
||||
// {"interpolationAlgorithm", "LAGRANGE"}
|
||||
};
|
||||
QJsonObject orientationPosition {
|
||||
@ -269,7 +422,10 @@ QJsonObject CZML::update(MapItem *mapItem, bool isTarget, bool isSelected)
|
||||
// Prevent labels from being too cluttered when zoomed out
|
||||
// FIXME: These values should come from mapItem or mapItemSettings
|
||||
float displayDistanceMax = std::numeric_limits<float>::max();
|
||||
if ((mapItem->m_group == "Beacons") || (mapItem->m_group == "AM") || (mapItem->m_group == "FM") || (mapItem->m_group == "DAB")) {
|
||||
if ((mapItem->m_group == "Beacons")
|
||||
|| (mapItem->m_group == "AM") || (mapItem->m_group == "FM") || (mapItem->m_group == "DAB")
|
||||
|| (mapItem->m_group == "NavAid")
|
||||
) {
|
||||
displayDistanceMax = 1000000;
|
||||
} else if ((mapItem->m_group == "Station") || (mapItem->m_group == "Radar") || (mapItem->m_group == "Radio Time Transmitters")) {
|
||||
displayDistanceMax = 10000000;
|
||||
@ -330,10 +486,6 @@ QJsonObject CZML::update(MapItem *mapItem, bool isTarget, bool isSelected)
|
||||
{"verticalOrigin", "BOTTOM"} // To stop it being cut in half when zoomed out
|
||||
};
|
||||
|
||||
QJsonObject obj {
|
||||
{"id", id} // id must be unique
|
||||
};
|
||||
|
||||
if (!removeObj)
|
||||
{
|
||||
obj.insert("position", position);
|
||||
@ -368,9 +520,11 @@ QJsonObject CZML::update(MapItem *mapItem, bool isTarget, bool isSelected)
|
||||
}
|
||||
else
|
||||
{
|
||||
QString oneMin = QDateTime::currentDateTimeUtc().addSecs(60).toString(Qt::ISODateWithMs);
|
||||
QString createdToNow = QString("%1/%2").arg(m_ids[id]).arg(oneMin); // From when object was created to now
|
||||
obj.insert("availability", createdToNow);
|
||||
if (mapItem->m_availableUntil.isValid())
|
||||
{
|
||||
QString period = QString("%1/%2").arg(m_ids[id]).arg(mapItem->m_availableUntil.toString(Qt::ISODateWithMs));
|
||||
obj.insert("availability", period);
|
||||
}
|
||||
}
|
||||
}
|
||||
m_lastPosition.insert(id, coords);
|
||||
@ -391,3 +545,4 @@ QJsonObject CZML::update(MapItem *mapItem, bool isTarget, bool isSelected)
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
@ -21,9 +21,13 @@
|
||||
#include <QHash>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QGeoCoordinate>
|
||||
|
||||
struct MapSettings;
|
||||
class MapItem;
|
||||
class ObjectMapItem;
|
||||
class PolygonMapItem;
|
||||
class PolylineMapItem;
|
||||
|
||||
class CZML
|
||||
{
|
||||
@ -32,11 +36,16 @@ private:
|
||||
QHash<QString, QString> m_ids;
|
||||
QHash<QString, QJsonArray> m_lastPosition;
|
||||
QHash<QString, bool> m_hasMoved;
|
||||
QGeoCoordinate m_position;
|
||||
|
||||
public:
|
||||
CZML(const MapSettings *settings);
|
||||
QJsonObject init();
|
||||
QJsonObject update(MapItem *mapItem, bool isTarget, bool isSelected);
|
||||
QJsonObject update(ObjectMapItem *mapItem, bool isTarget, bool isSelected);
|
||||
QJsonObject update(PolygonMapItem *mapItem);
|
||||
QJsonObject update(PolylineMapItem *mapItem);
|
||||
bool filter(const MapItem *mapItem) const;
|
||||
void setPosition(const QGeoCoordinate& position);
|
||||
|
||||
signals:
|
||||
void connected();
|
||||
|
5
plugins/feature/map/data.qrc
Normal file
@ -0,0 +1,5 @@
|
||||
<RCC>
|
||||
<qresource prefix="/map/">
|
||||
<file>data/transmitters.csv</file>
|
||||
</qresource>
|
||||
</RCC>
|
3478
plugins/feature/map/data/transmitters.csv
Normal file
@ -5,5 +5,7 @@
|
||||
<file>icons/ibp.png</file>
|
||||
<file>icons/muf.png</file>
|
||||
<file>icons/fof2.png</file>
|
||||
<file>icons/controltower.png</file>
|
||||
<file>icons/vor.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
BIN
plugins/feature/map/icons/controltower.png
Normal file
After Width: | Height: | Size: 308 B |
BIN
plugins/feature/map/icons/vor.png
Normal file
After Width: | Height: | Size: 365 B |
@ -8,6 +8,18 @@
|
||||
<file>map/antennafm.png</file>
|
||||
<file>map/antennaam.png</file>
|
||||
<file>map/ionosonde.png</file>
|
||||
<file>map/VOR.png</file>
|
||||
<file>map/VOR-DME.png</file>
|
||||
<file>map/VORTAC.png</file>
|
||||
<file>map/DVOR.png</file>
|
||||
<file>map/DVOR-DME.png</file>
|
||||
<file>map/DVORTAC.png</file>
|
||||
<file>map/NDB.png</file>
|
||||
<file>map/DME.png</file>
|
||||
<file>map/airport_large.png</file>
|
||||
<file>map/airport_medium.png</file>
|
||||
<file>map/airport_small.png</file>
|
||||
<file>map/heliport.png</file>
|
||||
<file>map/map3d.html</file>
|
||||
</qresource>
|
||||
<qresource prefix="/">
|
||||
|
BIN
plugins/feature/map/map/DME.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
plugins/feature/map/map/DVOR-DME.png
Normal file
After Width: | Height: | Size: 652 B |
BIN
plugins/feature/map/map/DVOR.png
Normal file
After Width: | Height: | Size: 415 B |
BIN
plugins/feature/map/map/DVORTAC.png
Normal file
After Width: | Height: | Size: 847 B |
BIN
plugins/feature/map/map/NDB.png
Normal file
After Width: | Height: | Size: 674 B |
BIN
plugins/feature/map/map/VOR-DME.png
Normal file
After Width: | Height: | Size: 652 B |
BIN
plugins/feature/map/map/VOR.png
Normal file
After Width: | Height: | Size: 415 B |
BIN
plugins/feature/map/map/VORTAC.png
Normal file
After Width: | Height: | Size: 847 B |
BIN
plugins/feature/map/map/airport_large.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
plugins/feature/map/map/airport_medium.png
Normal file
After Width: | Height: | Size: 1015 B |
BIN
plugins/feature/map/map/airport_small.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
plugins/feature/map/map/heliport.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
@ -10,6 +10,7 @@ Item {
|
||||
property string mapProvider: "osm"
|
||||
property variant mapPtr
|
||||
property variant guiPtr
|
||||
property bool smoothing
|
||||
|
||||
function createMap(pluginParameters, gui) {
|
||||
guiPtr = gui
|
||||
@ -67,40 +68,73 @@ Item {
|
||||
gesture.enabled: true
|
||||
gesture.acceptedGestures: MapGestureArea.PinchGesture | MapGestureArea.PanGesture
|
||||
|
||||
MapItemView {
|
||||
model: imageModelFiltered
|
||||
delegate: imageComponent
|
||||
}
|
||||
|
||||
MapItemView {
|
||||
model: polygonModelFiltered
|
||||
delegate: polygonComponent
|
||||
}
|
||||
|
||||
MapItemView {
|
||||
model: polygonModelFiltered
|
||||
delegate: polygonNameComponent
|
||||
}
|
||||
|
||||
MapItemView {
|
||||
model: polylineModelFiltered
|
||||
delegate: polylineComponent
|
||||
}
|
||||
|
||||
MapItemView {
|
||||
model: polylineModelFiltered
|
||||
delegate: polylineNameComponent
|
||||
}
|
||||
|
||||
// Tracks first, so drawn under other items
|
||||
MapItemView {
|
||||
model: mapModel
|
||||
model: mapModelFiltered
|
||||
delegate: groundTrack1Component
|
||||
}
|
||||
|
||||
MapItemView {
|
||||
model: mapModel
|
||||
model: mapModelFiltered
|
||||
delegate: groundTrack2Component
|
||||
}
|
||||
|
||||
MapItemView {
|
||||
model: mapModel
|
||||
model: mapModelFiltered
|
||||
delegate: predictedGroundTrack1Component
|
||||
}
|
||||
|
||||
MapItemView {
|
||||
model: mapModel
|
||||
model: mapModelFiltered
|
||||
delegate: predictedGroundTrack2Component
|
||||
}
|
||||
|
||||
MapItemView {
|
||||
model: mapModel
|
||||
model: mapModelFiltered
|
||||
delegate: mapComponent
|
||||
}
|
||||
|
||||
onZoomLevelChanged: {
|
||||
mapZoomLevel = zoomLevel
|
||||
mapModelFiltered.viewChanged(visibleRegion.boundingGeoRectangle().topLeft.longitude, visibleRegion.boundingGeoRectangle().topLeft.latitude, visibleRegion.boundingGeoRectangle().bottomRight.longitude, visibleRegion.boundingGeoRectangle().bottomRight.latitude, zoomLevel);
|
||||
imageModelFiltered.viewChanged(visibleRegion.boundingGeoRectangle().topLeft.longitude, visibleRegion.boundingGeoRectangle().topLeft.latitude, visibleRegion.boundingGeoRectangle().bottomRight.longitude, visibleRegion.boundingGeoRectangle().bottomRight.latitude, zoomLevel);
|
||||
polygonModelFiltered.viewChanged(visibleRegion.boundingGeoRectangle().topLeft.longitude, visibleRegion.boundingGeoRectangle().topLeft.latitude, visibleRegion.boundingGeoRectangle().bottomRight.longitude, visibleRegion.boundingGeoRectangle().bottomRight.latitude, zoomLevel);
|
||||
polylineModelFiltered.viewChanged(visibleRegion.boundingGeoRectangle().topLeft.longitude, visibleRegion.boundingGeoRectangle().topLeft.latitude, visibleRegion.boundingGeoRectangle().bottomRight.longitude, visibleRegion.boundingGeoRectangle().bottomRight.latitude, zoomLevel);
|
||||
}
|
||||
|
||||
// The map displays MapPolyLines in the wrong place (+360 degrees) if
|
||||
// they start to the left of the visible region, so we need to
|
||||
// split them so they don't, each time the visible region is changed. meh.
|
||||
onCenterChanged: {
|
||||
polylineModelFiltered.viewChanged(visibleRegion.boundingGeoRectangle().topLeft.longitude, visibleRegion.boundingGeoRectangle().topLeft.latitude, visibleRegion.boundingGeoRectangle().bottomRight.longitude, visibleRegion.boundingGeoRectangle().bottomRight.latitude, zoomLevel);
|
||||
polygonModelFiltered.viewChanged(visibleRegion.boundingGeoRectangle().topLeft.longitude, visibleRegion.boundingGeoRectangle().topLeft.latitude, visibleRegion.boundingGeoRectangle().bottomRight.longitude, visibleRegion.boundingGeoRectangle().bottomRight.latitude, zoomLevel);
|
||||
imageModelFiltered.viewChanged(visibleRegion.boundingGeoRectangle().topLeft.longitude, visibleRegion.boundingGeoRectangle().topLeft.latitude, visibleRegion.boundingGeoRectangle().bottomRight.longitude, visibleRegion.boundingGeoRectangle().bottomRight.latitude, zoomLevel);
|
||||
mapModelFiltered.viewChanged(visibleRegion.boundingGeoRectangle().topLeft.longitude, visibleRegion.boundingGeoRectangle().topLeft.latitude, visibleRegion.boundingGeoRectangle().bottomRight.longitude, visibleRegion.boundingGeoRectangle().bottomRight.latitude, zoomLevel);
|
||||
mapModel.viewChanged(visibleRegion.boundingGeoRectangle().bottomLeft.longitude, visibleRegion.boundingGeoRectangle().bottomRight.longitude);
|
||||
}
|
||||
|
||||
@ -118,6 +152,86 @@ Item {
|
||||
return null;
|
||||
}
|
||||
|
||||
Component {
|
||||
id: imageComponent
|
||||
MapQuickItem {
|
||||
coordinate: position
|
||||
anchorPoint.x: imageId.width/2
|
||||
anchorPoint.y: imageId.height/2
|
||||
zoomLevel: imageZoomLevel
|
||||
sourceItem: Image {
|
||||
id: imageId
|
||||
source: imageData
|
||||
}
|
||||
autoFadeIn: false
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: polygonComponent
|
||||
MapPolygon {
|
||||
border.width: 1
|
||||
border.color: borderColor
|
||||
color: fillColor
|
||||
path: polygon
|
||||
autoFadeIn: false
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: polygonNameComponent
|
||||
MapQuickItem {
|
||||
coordinate: position
|
||||
anchorPoint.x: polygonText.width/2
|
||||
anchorPoint.y: polygonText.height/2
|
||||
zoomLevel: mapZoomLevel > 11 ? mapZoomLevel : 11
|
||||
sourceItem: Grid {
|
||||
columns: 1
|
||||
Grid {
|
||||
layer.enabled: smoothing
|
||||
layer.smooth: smoothing
|
||||
horizontalItemAlignment: Grid.AlignHCenter
|
||||
Text {
|
||||
id: polygonText
|
||||
text: label
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: polylineComponent
|
||||
MapPolyline {
|
||||
line.width: 1
|
||||
line.color: lineColor
|
||||
path: coordinates
|
||||
autoFadeIn: false
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: polylineNameComponent
|
||||
MapQuickItem {
|
||||
coordinate: position
|
||||
anchorPoint.x: polylineText.width/2
|
||||
anchorPoint.y: polylineText.height/2
|
||||
zoomLevel: mapZoomLevel > 11 ? mapZoomLevel : 11
|
||||
sourceItem: Grid {
|
||||
columns: 1
|
||||
Grid {
|
||||
layer.enabled: smoothing
|
||||
layer.smooth: smoothing
|
||||
horizontalItemAlignment: Grid.AlignHCenter
|
||||
Text {
|
||||
id: polylineText
|
||||
text: label
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: mapComponent
|
||||
MapQuickItem {
|
||||
@ -125,7 +239,8 @@ Item {
|
||||
anchorPoint.x: image.width/2
|
||||
anchorPoint.y: image.height/2
|
||||
coordinate: position
|
||||
zoomLevel: mapZoomLevel > mapImageMinZoom ? mapZoomLevel : mapImageMinZoom
|
||||
// when zooming, mapImageMinZoom can be temporarily undefined. Not sure why
|
||||
zoomLevel: (typeof mapImageMinZoom !== 'undefined') ? (mapZoomLevel > mapImageMinZoom ? mapZoomLevel : mapImageMinZoom) : zoomLevel
|
||||
autoFadeIn: false // not in 5.12
|
||||
|
||||
sourceItem: Grid {
|
||||
@ -134,8 +249,8 @@ Item {
|
||||
Grid {
|
||||
horizontalItemAlignment: Grid.AlignHCenter
|
||||
columnSpacing: 5
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
layer.enabled: smoothing
|
||||
layer.smooth: smoothing
|
||||
Image {
|
||||
id: image
|
||||
rotation: mapImageRotation
|
||||
@ -149,7 +264,7 @@ Item {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
selected = !selected
|
||||
if (selected) {
|
||||
mapModel.moveToFront(index)
|
||||
mapModel.moveToFront(mapModelFiltered.mapRowToSource(index))
|
||||
}
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
if (frequency > 0) {
|
||||
@ -159,6 +274,8 @@ Item {
|
||||
freqMenuItem.text = "No frequency available"
|
||||
freqMenuItem.enabled = false
|
||||
}
|
||||
var c = mapPtr.toCoordinate(Qt.point(mouse.x, mouse.y))
|
||||
coordsMenuItem.text = "Coords: " + c.latitude.toFixed(6) + ", " + c.longitude.toFixed(6)
|
||||
contextMenu.popup()
|
||||
}
|
||||
}
|
||||
@ -186,7 +303,7 @@ Item {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
selected = !selected
|
||||
if (selected) {
|
||||
mapModel.moveToFront(index)
|
||||
mapModel.moveToFront(mapModelFiltered.mapRowToSource(index))
|
||||
}
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
if (frequency > 0) {
|
||||
@ -196,6 +313,8 @@ Item {
|
||||
freqMenuItem.text = "No frequency available"
|
||||
freqMenuItem.enabled = false
|
||||
}
|
||||
var c = mapPtr.toCoordinate(Qt.point(mouse.x, mouse.y))
|
||||
coordsMenuItem.text = "Coords: " + c.latitude.toFixed(6) + ", " + c.longitude.toFixed(6)
|
||||
contextMenu.popup()
|
||||
}
|
||||
}
|
||||
@ -212,15 +331,19 @@ Item {
|
||||
}
|
||||
MenuItem {
|
||||
text: "Move to front"
|
||||
onTriggered: mapModel.moveToFront(index)
|
||||
onTriggered: mapModel.moveToFront(mapModelFiltered.mapRowToSource(index))
|
||||
}
|
||||
MenuItem {
|
||||
text: "Move to back"
|
||||
onTriggered: mapModel.moveToBack(index)
|
||||
onTriggered: mapModel.moveToBack(mapModelFiltered.mapRowToSource(index))
|
||||
}
|
||||
MenuItem {
|
||||
text: "Track on 3D map"
|
||||
onTriggered: mapModel.track3D(index)
|
||||
onTriggered: mapModel.track3D(mapModelFiltered.mapRowToSource(index))
|
||||
}
|
||||
MenuItem {
|
||||
id: coordsMenuItem
|
||||
text: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -147,6 +147,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Polygons (such as for airspaces) should be prioritized behind other entities
|
||||
function pickEntityPrioritized(e)
|
||||
{
|
||||
var picked = viewer.scene.drillPick(e.position);
|
||||
if (Cesium.defined(picked)) {
|
||||
var firstPolygon = null;
|
||||
for (let i = 0; i < picked.length; i++) {
|
||||
var id = Cesium.defaultValue(picked[i].id, picked[i].primitive.id);
|
||||
if (id instanceof Cesium.Entity) {
|
||||
if (!Cesium.defined(id.polygon)) {
|
||||
return id;
|
||||
} else if (firstPolygon == null) {
|
||||
firstPolygon = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
return firstPolygon;
|
||||
}
|
||||
}
|
||||
|
||||
function pickEntity(e) {
|
||||
viewer.selectedEntity = pickEntityPrioritized(e);
|
||||
}
|
||||
|
||||
Cesium.Ion.defaultAccessToken = '$CESIUM_ION_API_KEY$';
|
||||
|
||||
const viewer = new Cesium.Viewer('cesiumContainer', {
|
||||
@ -161,6 +185,7 @@
|
||||
terrainProviderViewModels: [] // User should adjust terrain via dialog, so depthTestAgainstTerrain doesn't get set
|
||||
});
|
||||
viewer.scene.globe.depthTestAgainstTerrain = false; // So labels/points aren't clipped by terrain
|
||||
viewer.screenSpaceEventHandler.setInputAction(pickEntity, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
||||
var buildings = undefined;
|
||||
const images = new Map();
|
||||
|
||||
@ -351,7 +376,7 @@
|
||||
fabric: {
|
||||
type: 'Image',
|
||||
uniforms: {
|
||||
image: 'data:image/png;base64,' + command.data,
|
||||
image: command.data,
|
||||
}
|
||||
},
|
||||
translucent: true
|
||||
|
@ -37,6 +37,7 @@
|
||||
#include "device/deviceuiset.h"
|
||||
#include "util/units.h"
|
||||
#include "util/maidenhead.h"
|
||||
#include "util/morse.h"
|
||||
#include "maplocationdialog.h"
|
||||
#include "mapmaidenheaddialog.h"
|
||||
#include "mapsettingsdialog.h"
|
||||
@ -181,7 +182,10 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur
|
||||
m_pluginAPI(pluginAPI),
|
||||
m_featureUISet(featureUISet),
|
||||
m_doApplySettings(true),
|
||||
m_mapModel(this),
|
||||
m_objectMapModel(this),
|
||||
m_imageMapModel(this),
|
||||
m_polygonMapModel(this),
|
||||
m_polylineMapModel(this),
|
||||
m_beacons(nullptr),
|
||||
m_beaconDialog(this),
|
||||
m_ibpBeaconDialog(this),
|
||||
@ -197,6 +201,17 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur
|
||||
rollupContents->arrangeRollups();
|
||||
connect(rollupContents, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool)));
|
||||
|
||||
// Enable MSAA antialiasing on 2D map
|
||||
// This can be much faster than using layer.smooth in the QML, when there are many items
|
||||
// However, only seems to work when set to 16, and doesn't seem to be supported on all graphics cards
|
||||
int multisamples = MainCore::instance()->getSettings().getMapMultisampling();
|
||||
if (multisamples > 0)
|
||||
{
|
||||
QSurfaceFormat format;
|
||||
format.setSamples(multisamples);
|
||||
ui->map->setFormat(format);
|
||||
}
|
||||
|
||||
m_osmPort = 0;
|
||||
m_templateServer = new OSMTemplateServer(thunderforestAPIKey(), maptilerAPIKey(), m_osmPort);
|
||||
|
||||
@ -206,13 +221,17 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur
|
||||
|
||||
ui->map->setAttribute(Qt::WA_AcceptTouchEvents, true);
|
||||
|
||||
ui->map->rootContext()->setContextProperty("mapModel", &m_mapModel);
|
||||
// 5.12 doesn't display map items when fully zoomed out
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
|
||||
ui->map->setSource(QUrl(QStringLiteral("qrc:/map/map/map_5_12.qml")));
|
||||
#else
|
||||
m_objectMapFilter.setSourceModel(&m_objectMapModel);
|
||||
m_imageMapFilter.setSourceModel(&m_imageMapModel);
|
||||
m_polygonMapFilter.setSourceModel(&m_polygonMapModel);
|
||||
m_polylineMapFilter.setSourceModel(&m_polylineMapModel);
|
||||
|
||||
ui->map->rootContext()->setContextProperty("mapModelFiltered", &m_objectMapFilter);
|
||||
ui->map->rootContext()->setContextProperty("mapModel", &m_objectMapModel);
|
||||
ui->map->rootContext()->setContextProperty("imageModelFiltered", &m_imageMapFilter);
|
||||
ui->map->rootContext()->setContextProperty("polygonModelFiltered", &m_polygonMapFilter);
|
||||
ui->map->rootContext()->setContextProperty("polylineModelFiltered", &m_polylineMapFilter);
|
||||
ui->map->setSource(QUrl(QStringLiteral("qrc:/map/map/map.qml")));
|
||||
#endif
|
||||
|
||||
m_settings.m_modelURL = QString("http://127.0.0.1:%1/3d/").arg(m_webPort);
|
||||
m_webServer->addPathSubstitution("3d", m_settings.m_modelDir);
|
||||
@ -236,6 +255,11 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur
|
||||
float stationLongitude = MainCore::instance()->getSettings().getLongitude();
|
||||
float stationAltitude = MainCore::instance()->getSettings().getAltitude();
|
||||
m_azEl.setLocation(stationLatitude, stationLongitude, stationAltitude);
|
||||
QGeoCoordinate stationPosition(stationLatitude, stationLongitude, stationAltitude);
|
||||
m_objectMapFilter.setPosition(stationPosition);
|
||||
m_imageMapFilter.setPosition(stationPosition);
|
||||
m_polygonMapFilter.setPosition(stationPosition);
|
||||
m_polylineMapFilter.setPosition(stationPosition);
|
||||
|
||||
// Centre map at My Position
|
||||
QQuickItem *item = ui->map->rootObject();
|
||||
@ -257,7 +281,9 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur
|
||||
m_antennaMapItem.setImageRotation(0);
|
||||
m_antennaMapItem.setText(new QString(MainCore::instance()->getSettings().getStationName()));
|
||||
m_antennaMapItem.setModel(new QString("antenna.glb"));
|
||||
m_antennaMapItem.setFixedPosition(true);
|
||||
m_antennaMapItem.setFixedPosition(false);
|
||||
m_antennaMapItem.setPositionDateTime(new QString(QDateTime::currentDateTime().toString(Qt::ISODateWithMs)));
|
||||
m_antennaMapItem.setAvailableUntil(new QString(QDateTime::currentDateTime().addDays(365).toString(Qt::ISODateWithMs)));
|
||||
m_antennaMapItem.setOrientation(0);
|
||||
m_antennaMapItem.setLabel(new QString(MainCore::instance()->getSettings().getStationName()));
|
||||
m_antennaMapItem.setLabelAltitudeOffset(4.5);
|
||||
@ -277,6 +303,10 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur
|
||||
addRadioTimeTransmitters();
|
||||
addRadar();
|
||||
addIonosonde();
|
||||
addBroadcast();
|
||||
addNavAids();
|
||||
addAirspace();
|
||||
addAirports();
|
||||
|
||||
displaySettings();
|
||||
applySettings(true);
|
||||
@ -317,31 +347,18 @@ void MapGUI::setWorkspaceIndex(int index)
|
||||
m_feature->setWorkspaceIndex(index);
|
||||
}
|
||||
|
||||
// Update a map item or image
|
||||
// Update a map item
|
||||
void MapGUI::update(const QObject *source, SWGSDRangel::SWGMapItem *swgMapItem, const QString &group)
|
||||
{
|
||||
if (swgMapItem->getType() == 0)
|
||||
{
|
||||
m_mapModel.update(source, swgMapItem, group);
|
||||
}
|
||||
else if (m_cesium)
|
||||
{
|
||||
QString name = *swgMapItem->getName();
|
||||
QString image = *swgMapItem->getImage();
|
||||
if (!image.isEmpty())
|
||||
{
|
||||
m_cesium->updateImage(name,
|
||||
swgMapItem->getImageTileEast(),
|
||||
swgMapItem->getImageTileWest(),
|
||||
swgMapItem->getImageTileNorth(),
|
||||
swgMapItem->getImageTileSouth(),
|
||||
swgMapItem->getAltitude(),
|
||||
image);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_cesium->removeImage(name);
|
||||
}
|
||||
int type = swgMapItem->getType();
|
||||
if (type == 0) {
|
||||
m_objectMapModel.update(source, swgMapItem, group);
|
||||
} else if (type == 1) {
|
||||
m_imageMapModel.update(source, swgMapItem, group);
|
||||
} else if (type == 2) {
|
||||
m_polygonMapModel.update(source, swgMapItem, group);
|
||||
} else if (type == 3) {
|
||||
m_polylineMapModel.update(source, swgMapItem, group);
|
||||
}
|
||||
}
|
||||
|
||||
@ -401,11 +418,15 @@ void MapGUI::addIBPBeacons()
|
||||
}
|
||||
|
||||
const QList<RadioTimeTransmitter> MapGUI::m_radioTimeTransmitters = {
|
||||
{"MSF", 60000, 54.9075f, -3.27333f, 17},
|
||||
{"DCF77", 77500, 50.01611111f, 9.00805556f, 50},
|
||||
{"TDF", 162000, 47.1694f, 2.2044f, 800},
|
||||
{"WWVB", 60000, 40.67805556f, -105.04666667f, 70},
|
||||
{"JJY", 60000, 33.465f, 130.175555f, 50}
|
||||
{"MSF", 60000, 54.9075f, -3.27333f, 17}, // UK
|
||||
{"DCF77", 77500, 50.01611111f, 9.00805556f, 50}, // Germany
|
||||
{"TDF", 162000, 47.1694f, 2.2044f, 800}, // France
|
||||
{"WWVB", 60000, 40.67805556f, -105.04666667f, 70}, // USA
|
||||
{"JJY-40", 40000, 37.3725f, 140.848889f, 50}, // Japan
|
||||
{"JJY-60", 60000, 33.465556f, 130.175556f, 50}, // Japan
|
||||
{"RTZ", 50000, 52.436111f, 103.685833f, 10}, // Russia - On 22:00 to 21:00 UTC
|
||||
{"RBU", 66666, 56.733333f, 37.663333f, 10}, // Russia
|
||||
{"BPC", 68500, 34.457f, 115.837f, 90}, // China - On 1:00 to 21:00 UTC
|
||||
};
|
||||
|
||||
void MapGUI::addRadioTimeTransmitters()
|
||||
@ -528,6 +549,111 @@ void MapGUI::foF2Updated(const QJsonDocument& document)
|
||||
}
|
||||
}
|
||||
|
||||
void MapGUI::addBroadcast()
|
||||
{
|
||||
QFile file(":/map/data/transmitters.csv");
|
||||
if (file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
{
|
||||
QTextStream in(&file);
|
||||
|
||||
QString error;
|
||||
QHash<QString, int> colIndexes = CSV::readHeader(in, {"Type", "Id", "Name", "Frequency (Hz)", "Latitude", "Longitude", "Altitude (m)", "Power", "TII Main", "TII Sub"}, error);
|
||||
if (error.isEmpty())
|
||||
{
|
||||
int typeCol = colIndexes.value("Type");
|
||||
int idCol = colIndexes.value("Id");
|
||||
int nameCol = colIndexes.value("Name");
|
||||
int frequencyCol = colIndexes.value("Frequency (Hz)");
|
||||
int latCol = colIndexes.value("Latitude");
|
||||
int longCol = colIndexes.value("Longitude");
|
||||
int altCol = colIndexes.value("Altitude (m)");
|
||||
int powerCol = colIndexes.value("Power");
|
||||
int tiiMainCol = colIndexes.value("TII Main");
|
||||
int tiiSubCol = colIndexes.value("TII Sub");
|
||||
|
||||
QStringList cols;
|
||||
while (CSV::readRow(in, &cols))
|
||||
{
|
||||
QString type = cols[typeCol];
|
||||
QString id = cols[idCol];
|
||||
QString name = cols[nameCol];
|
||||
QString frequency = cols[frequencyCol];
|
||||
double latitude = cols[latCol].toDouble();
|
||||
double longitude = cols[longCol].toDouble();
|
||||
double altitude = cols[altCol].toDouble();
|
||||
QString power = cols[powerCol];
|
||||
|
||||
SWGSDRangel::SWGMapItem mapItem;
|
||||
mapItem.setLatitude(latitude);
|
||||
mapItem.setLongitude(longitude);
|
||||
mapItem.setAltitude(altitude);
|
||||
mapItem.setImageRotation(0);
|
||||
mapItem.setModel(new QString("antenna.glb"));
|
||||
mapItem.setFixedPosition(true);
|
||||
mapItem.setOrientation(0);
|
||||
mapItem.setLabelAltitudeOffset(4.5);
|
||||
mapItem.setAltitudeReference(1);
|
||||
|
||||
if (type == "AM")
|
||||
{
|
||||
// Name should be unique
|
||||
mapItem.setName(new QString(id));
|
||||
mapItem.setImage(new QString("antennaam.png"));
|
||||
mapItem.setLabel(new QString(name));
|
||||
QString text = QString("%1 Transmitter\nStation: %2\nFrequency: %3 kHz\nPower (ERMP): %4 kW")
|
||||
.arg(type)
|
||||
.arg(name)
|
||||
.arg(frequency.toDouble() / 1e3)
|
||||
.arg(power)
|
||||
;
|
||||
mapItem.setText(new QString(text));
|
||||
update(m_map, &mapItem, "AM");
|
||||
}
|
||||
else if (type == "FM")
|
||||
{
|
||||
mapItem.setName(new QString(id));
|
||||
mapItem.setImage(new QString("antennafm.png"));
|
||||
mapItem.setLabel(new QString(name));
|
||||
QString text = QString("%1 Transmitter\nStation: %2\nFrequency: %3 MHz\nPower (ERP): %4 kW")
|
||||
.arg(type)
|
||||
.arg(name)
|
||||
.arg(frequency.toDouble() / 1e6)
|
||||
.arg(power)
|
||||
;
|
||||
mapItem.setText(new QString(text));
|
||||
update(m_map, &mapItem, "FM");
|
||||
}
|
||||
else if (type == "DAB")
|
||||
{
|
||||
mapItem.setName(new QString(id));
|
||||
mapItem.setImage(new QString("antennadab.png"));
|
||||
mapItem.setLabel(new QString(name));
|
||||
QString text = QString("%1 Transmitter\nEnsemble: %2\nFrequency: %3 MHz\nPower (ERP): %4 kW\nTII: %5 %6")
|
||||
.arg(type)
|
||||
.arg(name)
|
||||
.arg(frequency.toDouble() / 1e6)
|
||||
.arg(power)
|
||||
.arg(cols[tiiMainCol])
|
||||
.arg(cols[tiiSubCol])
|
||||
;
|
||||
mapItem.setText(new QString(text));
|
||||
update(m_map, &mapItem, "DAB");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qCritical() << "MapGUI::addBroadcast: Failed reading transmitters.csv - " << error;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qCritical() << "MapGUI::addBroadcast: Failed to open transmitters.csv";
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
static QString arrayToString(QJsonArray array)
|
||||
{
|
||||
QString s;
|
||||
@ -539,7 +665,7 @@ static QString arrayToString(QJsonArray array)
|
||||
return s;
|
||||
}
|
||||
|
||||
// Coming soon
|
||||
// Code for FM list / DAB list, should they allow access
|
||||
void MapGUI::addDAB()
|
||||
{
|
||||
QFile file("stationlist_SI.json");
|
||||
@ -663,6 +789,201 @@ void MapGUI::addDAB()
|
||||
qDebug() << "MapGUI::addDAB: Failed to open DAB json";
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
void MapGUI::addNavAids()
|
||||
{
|
||||
m_navAids = OpenAIP::getNavAids();
|
||||
|
||||
for (const auto navAid : *m_navAids)
|
||||
{
|
||||
SWGSDRangel::SWGMapItem navAidMapItem;
|
||||
navAidMapItem.setName(new QString(navAid->m_name + " " + navAid->m_ident)); // Neither name or ident are unique...
|
||||
navAidMapItem.setLatitude(navAid->m_latitude);
|
||||
navAidMapItem.setLongitude(navAid->m_longitude);
|
||||
navAidMapItem.setAltitude(Units::feetToMetres(navAid->m_elevation));
|
||||
QString image = QString("%1.png").arg(navAid->m_type);
|
||||
navAidMapItem.setImage(new QString(image));
|
||||
navAidMapItem.setImageRotation(0);
|
||||
QString text = QString("NAVAID\nName: %1").arg(navAid->m_name);
|
||||
if (navAid->m_type == "NDB") {
|
||||
text.append(QString("\nFrequency: %1 kHz").arg(navAid->m_frequencykHz, 0, 'f', 1));
|
||||
} else {
|
||||
text.append(QString("\nFrequency: %1 MHz").arg(navAid->m_frequencykHz / 1000.0f, 0, 'f', 2));
|
||||
}
|
||||
if (navAid->m_channel != "") {
|
||||
text.append(QString("\nChannel: %1").arg(navAid->m_channel));
|
||||
}
|
||||
text.append(QString("\nIdent: %1 %2").arg(navAid->m_ident).arg(Morse::toSpacedUnicodeMorse(navAid->m_ident)));
|
||||
text.append(QString("\nRange: %1 nm").arg(navAid->m_range));
|
||||
if (navAid->m_alignedTrueNorth) {
|
||||
text.append(QString("\nMagnetic declination: Aligned to true North"));
|
||||
} else if (navAid->m_magneticDeclination != 0.0f) {
|
||||
text.append(QString("\nMagnetic declination: %1%2").arg(std::round(navAid->m_magneticDeclination)).arg(QChar(0x00b0)));
|
||||
}
|
||||
navAidMapItem.setText(new QString(text));
|
||||
navAidMapItem.setModel(new QString("antenna.glb"));
|
||||
navAidMapItem.setFixedPosition(true);
|
||||
navAidMapItem.setOrientation(0);
|
||||
navAidMapItem.setLabel(new QString(navAid->m_name));
|
||||
navAidMapItem.setLabelAltitudeOffset(4.5);
|
||||
navAidMapItem.setAltitudeReference(1);
|
||||
update(m_map, &navAidMapItem, "NavAid");
|
||||
}
|
||||
}
|
||||
|
||||
void MapGUI::addAirspace(const Airspace *airspace, const QString& group, int cnt)
|
||||
{
|
||||
//MapSettings::MapItemSettings *itemSettings = getItemSettings(group);
|
||||
|
||||
QString details;
|
||||
details.append(airspace->m_name);
|
||||
details.append(QString("\n%1 - %2")
|
||||
.arg(airspace->getAlt(&airspace->m_bottom))
|
||||
.arg(airspace->getAlt(&airspace->m_top)));
|
||||
QString name = QString("Airspace %1 (%2)").arg(airspace->m_name).arg(cnt);
|
||||
|
||||
SWGSDRangel::SWGMapItem airspaceMapItem;
|
||||
airspaceMapItem.setName(new QString(name));
|
||||
airspaceMapItem.setLatitude(airspace->m_position.y());
|
||||
airspaceMapItem.setLongitude(airspace->m_position.x());
|
||||
airspaceMapItem.setAltitude(airspace->bottomHeightInMetres());
|
||||
QString image = QString("none");
|
||||
airspaceMapItem.setImage(new QString(image));
|
||||
airspaceMapItem.setImageRotation(0);
|
||||
airspaceMapItem.setText(new QString(details)); // Not used - label is used instead for now
|
||||
airspaceMapItem.setFixedPosition(true);
|
||||
airspaceMapItem.setLabel(new QString(details));
|
||||
airspaceMapItem.setAltitudeReference(0);
|
||||
QList<SWGSDRangel::SWGMapCoordinate *> *coords = new QList<SWGSDRangel::SWGMapCoordinate *>();
|
||||
for (const auto p : airspace->m_polygon)
|
||||
{
|
||||
SWGSDRangel::SWGMapCoordinate* c = new SWGSDRangel::SWGMapCoordinate();
|
||||
c->setLatitude(p.y());
|
||||
c->setLongitude(p.x());
|
||||
c->setAltitude(airspace->bottomHeightInMetres());
|
||||
coords->append(c);
|
||||
}
|
||||
airspaceMapItem.setCoordinates(coords);
|
||||
airspaceMapItem.setExtrudedHeight(airspace->topHeightInMetres());
|
||||
airspaceMapItem.setType(2);
|
||||
update(m_map, &airspaceMapItem, group);
|
||||
}
|
||||
|
||||
|
||||
void MapGUI::addAirspace()
|
||||
{
|
||||
m_airspaces = OpenAIP::getAirspaces();
|
||||
|
||||
int cnt = 0;
|
||||
for (const auto airspace : *m_airspaces)
|
||||
{
|
||||
static const QMap<QString, QString> groupMap {
|
||||
{"A", "Airspace (A)"},
|
||||
{"B", "Airspace (B)"},
|
||||
{"C", "Airspace (C)"},
|
||||
{"D", "Airspace (D)"},
|
||||
{"E", "Airspace (E)"},
|
||||
{"F", "Airspace (F)"},
|
||||
{"G", "Airspace (G)"},
|
||||
{"FIR", "Airspace (FIR)"},
|
||||
{"CTR", "Airspace (CTR)"},
|
||||
{"RMZ", "Airspace (RMZ)"},
|
||||
{"TMA", "Airspace (TMA)"},
|
||||
{"TMZ", "Airspace (TMZ)"},
|
||||
{"OTH", "Airspace (OTH)"},
|
||||
{"RESTRICTED", "Airspace (Restricted)"},
|
||||
{"GLIDING", "Airspace (Gliding)"},
|
||||
{"DANGER", "Airspace (Danger)"},
|
||||
{"PROHIBITED", "Airspace (Prohibited)"},
|
||||
{"WAVE", "Airspace (Wave)"},
|
||||
};
|
||||
|
||||
if (groupMap.contains(airspace->m_category))
|
||||
{
|
||||
QString group = groupMap[airspace->m_category];
|
||||
addAirspace(airspace, group, cnt);
|
||||
cnt++;
|
||||
|
||||
if ( (airspace->bottomHeightInMetres() == 0)
|
||||
&& ((airspace->m_category == "D") || (airspace->m_category == "G") || (airspace->m_category == "CTR"))
|
||||
&& (!((airspace->m_country == "IT") && (airspace->m_name.startsWith("FIR"))))
|
||||
&& (!((airspace->m_country == "PL") && (airspace->m_name.startsWith("FIS"))))
|
||||
)
|
||||
{
|
||||
group = "Airspace (Airports)";
|
||||
addAirspace(airspace, group, cnt);
|
||||
cnt++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "MapGUI::addAirspace: No group for airspace category " << airspace->m_category;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MapGUI::addAirports()
|
||||
{
|
||||
m_airportInfo = OurAirportsDB::getAirportsById();
|
||||
if (m_airportInfo)
|
||||
{
|
||||
QHashIterator<int, AirportInformation *> i(*m_airportInfo);
|
||||
while (i.hasNext())
|
||||
{
|
||||
i.next();
|
||||
AirportInformation *airport = i.value();
|
||||
|
||||
SWGSDRangel::SWGMapItem airportMapItem;
|
||||
airportMapItem.setName(new QString(airport->m_ident));
|
||||
airportMapItem.setLatitude(airport->m_latitude);
|
||||
airportMapItem.setLongitude(airport->m_longitude);
|
||||
airportMapItem.setAltitude(Units::feetToMetres(airport->m_elevation));
|
||||
airportMapItem.setImage(new QString(airport->getImageName()));
|
||||
airportMapItem.setImageRotation(0);
|
||||
QStringList list;
|
||||
list.append(QString("%1: %2").arg(airport->m_ident).arg(airport->m_name));
|
||||
for (int i = 0; i < airport->m_frequencies.size(); i++)
|
||||
{
|
||||
const AirportInformation::FrequencyInformation *frequencyInfo = airport->m_frequencies[i];
|
||||
list.append(QString("%1: %2 MHz").arg(frequencyInfo->m_type).arg(frequencyInfo->m_frequency));
|
||||
}
|
||||
airportMapItem.setText(new QString(list.join("\n")));
|
||||
airportMapItem.setModel(new QString("airport.glb")); // No such model currently, but we don't really want one
|
||||
airportMapItem.setFixedPosition(true);
|
||||
airportMapItem.setOrientation(0);
|
||||
airportMapItem.setLabel(new QString(airport->m_ident));
|
||||
airportMapItem.setLabelAltitudeOffset(4.5);
|
||||
airportMapItem.setAltitudeReference(1);
|
||||
QString group;
|
||||
if (airport->m_type == AirportInformation::Small) {
|
||||
group = "Airport (Small)";
|
||||
} else if (airport->m_type == AirportInformation::Medium) {
|
||||
group = "Airport (Medium)";
|
||||
} else if (airport->m_type == AirportInformation::Large) {
|
||||
group = "Airport (Large)";
|
||||
} else {
|
||||
group = "Heliport";
|
||||
}
|
||||
update(m_map, &airportMapItem, group);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MapGUI::navAidsUpdated()
|
||||
{
|
||||
addNavAids();
|
||||
}
|
||||
|
||||
void MapGUI::airspacesUpdated()
|
||||
{
|
||||
addAirspace();
|
||||
}
|
||||
|
||||
void MapGUI::airportsUpdated()
|
||||
{
|
||||
addAirports();
|
||||
}
|
||||
|
||||
void MapGUI::blockApplySettings(bool block)
|
||||
{
|
||||
@ -722,6 +1043,7 @@ void MapGUI::applyMap2DSettings(bool reloadMap)
|
||||
}
|
||||
|
||||
// Create the map using the specified provider
|
||||
QQmlProperty::write(item, "smoothing", MainCore::instance()->getSettings().getMapSmoothing());
|
||||
QQmlProperty::write(item, "mapProvider", m_settings.m_mapProvider);
|
||||
QVariantMap parameters;
|
||||
if (!m_settings.m_mapBoxAPIKey.isEmpty() && m_settings.m_mapProvider == "mapbox")
|
||||
@ -799,7 +1121,7 @@ void MapGUI::redrawMap()
|
||||
if (object)
|
||||
{
|
||||
double zoom = object->property("zoomLevel").value<double>();
|
||||
object->setProperty("zoomLevel", QVariant::fromValue(zoom+1));
|
||||
object->setProperty("zoomLevel", QVariant::fromValue(zoom+1.0));
|
||||
object->setProperty("zoomLevel", QVariant::fromValue(zoom));
|
||||
}
|
||||
}
|
||||
@ -834,6 +1156,15 @@ bool MapGUI::eventFilter(QObject *obj, QEvent *event)
|
||||
return FeatureGUI::eventFilter(obj, event);
|
||||
}
|
||||
|
||||
MapSettings::MapItemSettings *MapGUI::getItemSettings(const QString &group)
|
||||
{
|
||||
if (m_settings.m_itemSettings.contains(group)) {
|
||||
return m_settings.m_itemSettings[group];
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void MapGUI::supportedMapsChanged()
|
||||
{
|
||||
QQuickItem *item = ui->map->rootObject();
|
||||
@ -893,7 +1224,8 @@ void MapGUI::applyMap3DSettings(bool reloadMap)
|
||||
#ifdef QT_WEBENGINE_FOUND
|
||||
if (m_settings.m_map3DEnabled && ((m_cesium == nullptr) || reloadMap))
|
||||
{
|
||||
if (m_cesium == nullptr) {
|
||||
if (m_cesium == nullptr)
|
||||
{
|
||||
m_cesium = new CesiumInterface(&m_settings);
|
||||
connect(m_cesium, &CesiumInterface::connected, this, &MapGUI::init3DMap);
|
||||
connect(m_cesium, &CesiumInterface::received, this, &MapGUI::receivedCesiumEvent);
|
||||
@ -924,6 +1256,10 @@ void MapGUI::applyMap3DSettings(bool reloadMap)
|
||||
m_cesium->getDateTime();
|
||||
m_cesium->showMUF(m_settings.m_displayMUF);
|
||||
m_cesium->showfoF2(m_settings.m_displayfoF2);
|
||||
m_objectMapModel.allUpdated();
|
||||
m_imageMapModel.allUpdated();
|
||||
m_polygonMapModel.allUpdated();
|
||||
m_polylineMapModel.allUpdated();
|
||||
}
|
||||
MapSettings::MapItemSettings *ionosondeItemSettings = getItemSettings("Ionosonde Stations");
|
||||
if (ionosondeItemSettings) {
|
||||
@ -934,9 +1270,10 @@ void MapGUI::applyMap3DSettings(bool reloadMap)
|
||||
#else
|
||||
ui->displayMUF->setVisible(false);
|
||||
ui->displayfoF2->setVisible(false);
|
||||
m_mapModel.allUpdated();
|
||||
float stationLatitude = MainCore::instance()->getSettings().getLatitude();
|
||||
float stationLongitude = MainCore::instance()->getSettings().getLongitude();
|
||||
m_objectMapModel.allUpdated();
|
||||
m_imageMapModel.allUpdated();
|
||||
m_polygonMapModel.allUpdated();
|
||||
m_polylineMapModel.allUpdated();
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -947,6 +1284,11 @@ void MapGUI::init3DMap()
|
||||
|
||||
m_cesium->initCZML();
|
||||
|
||||
float stationLatitude = MainCore::instance()->getSettings().getLatitude();
|
||||
float stationLongitude = MainCore::instance()->getSettings().getLongitude();
|
||||
float stationAltitude = MainCore::instance()->getSettings().getLongitude();
|
||||
|
||||
m_cesium->setPosition(QGeoCoordinate(stationLatitude, stationLongitude, stationAltitude));
|
||||
m_cesium->setTerrain(m_settings.m_terrain, maptilerAPIKey());
|
||||
m_cesium->setBuildings(m_settings.m_buildings);
|
||||
m_cesium->setSunLight(m_settings.m_sunLightEnabled);
|
||||
@ -954,9 +1296,10 @@ void MapGUI::init3DMap()
|
||||
m_cesium->setAntiAliasing(m_settings.m_antiAliasing);
|
||||
m_cesium->getDateTime();
|
||||
|
||||
m_mapModel.allUpdated();
|
||||
float stationLatitude = MainCore::instance()->getSettings().getLatitude();
|
||||
float stationLongitude = MainCore::instance()->getSettings().getLongitude();
|
||||
m_objectMapModel.allUpdated();
|
||||
m_imageMapModel.allUpdated();
|
||||
m_polygonMapModel.allUpdated();
|
||||
m_polylineMapModel.allUpdated();
|
||||
|
||||
// Set 3D view after loading initial objects
|
||||
m_cesium->setHomeView(stationLatitude, stationLongitude);
|
||||
@ -977,10 +1320,13 @@ void MapGUI::displaySettings()
|
||||
ui->displayAllGroundTracks->setChecked(m_settings.m_displayAllGroundTracks);
|
||||
ui->displayMUF->setChecked(m_settings.m_displayMUF);
|
||||
ui->displayfoF2->setChecked(m_settings.m_displayfoF2);
|
||||
m_mapModel.setDisplayNames(m_settings.m_displayNames);
|
||||
m_mapModel.setDisplaySelectedGroundTracks(m_settings.m_displaySelectedGroundTracks);
|
||||
m_mapModel.setDisplayAllGroundTracks(m_settings.m_displayAllGroundTracks);
|
||||
m_mapModel.updateItemSettings(m_settings.m_itemSettings);
|
||||
m_objectMapModel.setDisplayNames(m_settings.m_displayNames);
|
||||
m_objectMapModel.setDisplaySelectedGroundTracks(m_settings.m_displaySelectedGroundTracks);
|
||||
m_objectMapModel.setDisplayAllGroundTracks(m_settings.m_displayAllGroundTracks);
|
||||
m_objectMapModel.updateItemSettings(m_settings.m_itemSettings);
|
||||
m_imageMapModel.updateItemSettings(m_settings.m_itemSettings);
|
||||
m_polygonMapModel.updateItemSettings(m_settings.m_itemSettings);
|
||||
m_polylineMapModel.updateItemSettings(m_settings.m_itemSettings);
|
||||
applyMap2DSettings(true);
|
||||
applyMap3DSettings(true);
|
||||
getRollupContents()->restoreState(m_rollupState);
|
||||
@ -1049,19 +1395,19 @@ void MapGUI::on_maidenhead_clicked()
|
||||
void MapGUI::on_displayNames_clicked(bool checked)
|
||||
{
|
||||
m_settings.m_displayNames = checked;
|
||||
m_mapModel.setDisplayNames(checked);
|
||||
m_objectMapModel.setDisplayNames(checked);
|
||||
}
|
||||
|
||||
void MapGUI::on_displaySelectedGroundTracks_clicked(bool checked)
|
||||
{
|
||||
m_settings.m_displaySelectedGroundTracks = checked;
|
||||
m_mapModel.setDisplaySelectedGroundTracks(checked);
|
||||
m_objectMapModel.setDisplaySelectedGroundTracks(checked);
|
||||
}
|
||||
|
||||
void MapGUI::on_displayAllGroundTracks_clicked(bool checked)
|
||||
{
|
||||
m_settings.m_displayAllGroundTracks = checked;
|
||||
m_mapModel.setDisplayAllGroundTracks(checked);
|
||||
m_objectMapModel.setDisplayAllGroundTracks(checked);
|
||||
}
|
||||
|
||||
void MapGUI::on_displayMUF_clicked(bool checked)
|
||||
@ -1183,13 +1529,15 @@ void MapGUI::find(const QString& target)
|
||||
}
|
||||
else
|
||||
{
|
||||
MapItem *mapItem = m_mapModel.findMapItem(target);
|
||||
// FIXME: Support polygon/polyline
|
||||
ObjectMapItem *mapItem = m_objectMapModel.findMapItem(target);
|
||||
if (mapItem != nullptr)
|
||||
{
|
||||
map->setProperty("center", QVariant::fromValue(mapItem->getCoordinates()));
|
||||
if (m_cesium) {
|
||||
m_cesium->track(target);
|
||||
}
|
||||
m_objectMapModel.moveToFront(m_objectMapModel.findMapItemIndex(target).row());
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -1224,7 +1572,10 @@ void MapGUI::track3D(const QString& target)
|
||||
|
||||
void MapGUI::on_deleteAll_clicked()
|
||||
{
|
||||
m_mapModel.removeAll();
|
||||
m_objectMapModel.removeAll();
|
||||
m_imageMapModel.removeAll();
|
||||
m_polygonMapModel.removeAll();
|
||||
m_polylineMapModel.removeAll();
|
||||
if (m_cesium)
|
||||
{
|
||||
m_cesium->removeAllCZMLEntities();
|
||||
@ -1235,6 +1586,9 @@ void MapGUI::on_deleteAll_clicked()
|
||||
void MapGUI::on_displaySettings_clicked()
|
||||
{
|
||||
MapSettingsDialog dialog(&m_settings);
|
||||
connect(&dialog, &MapSettingsDialog::navAidsUpdated, this, &MapGUI::navAidsUpdated);
|
||||
connect(&dialog, &MapSettingsDialog::airspacesUpdated, this, &MapGUI::airspacesUpdated);
|
||||
connect(&dialog, &MapSettingsDialog::airportsUpdated, this, &MapGUI::airportsUpdated);
|
||||
new DialogPositioner(&dialog, true);
|
||||
if (dialog.exec() == QDialog::Accepted)
|
||||
{
|
||||
@ -1244,7 +1598,10 @@ void MapGUI::on_displaySettings_clicked()
|
||||
applyMap2DSettings(dialog.m_map2DSettingsChanged);
|
||||
applyMap3DSettings(dialog.m_map3DSettingsChanged);
|
||||
applySettings();
|
||||
m_mapModel.allUpdated();
|
||||
m_objectMapModel.allUpdated();
|
||||
m_imageMapModel.allUpdated();
|
||||
m_polygonMapModel.allUpdated();
|
||||
m_polylineMapModel.allUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1289,17 +1646,17 @@ void MapGUI::receivedCesiumEvent(const QJsonObject &obj)
|
||||
if (event == "selected")
|
||||
{
|
||||
if (obj.contains("id")) {
|
||||
m_mapModel.setSelected3D(obj.value("id").toString());
|
||||
m_objectMapModel.setSelected3D(obj.value("id").toString());
|
||||
} else {
|
||||
m_mapModel.setSelected3D("");
|
||||
m_objectMapModel.setSelected3D("");
|
||||
}
|
||||
}
|
||||
else if (event == "tracking")
|
||||
{
|
||||
if (obj.contains("id")) {
|
||||
//m_mapModel.setTarget(obj.value("id").toString());
|
||||
//m_objectMapModel.setTarget(obj.value("id").toString());
|
||||
} else {
|
||||
//m_mapModel.setTarget("");
|
||||
//m_objectMapModel.setTarget("");
|
||||
}
|
||||
}
|
||||
else if (event == "clock")
|
||||
@ -1346,9 +1703,10 @@ void MapGUI::preferenceChanged(int elementType)
|
||||
float stationLongitude = MainCore::instance()->getSettings().getLongitude();
|
||||
float stationAltitude = MainCore::instance()->getSettings().getAltitude();
|
||||
|
||||
if ( (stationLatitude != m_azEl.getLocationSpherical().m_latitude)
|
||||
|| (stationLongitude != m_azEl.getLocationSpherical().m_longitude)
|
||||
|| (stationAltitude != m_azEl.getLocationSpherical().m_altitude))
|
||||
QGeoCoordinate stationPosition(stationLatitude, stationLongitude, stationAltitude);
|
||||
QGeoCoordinate previousPosition(m_azEl.getLocationSpherical().m_latitude, m_azEl.getLocationSpherical().m_longitude, m_azEl.getLocationSpherical().m_altitude);
|
||||
|
||||
if (stationPosition != previousPosition)
|
||||
{
|
||||
// Update position of station
|
||||
m_azEl.setLocation(stationLatitude, stationLongitude, stationAltitude);
|
||||
@ -1356,16 +1714,38 @@ void MapGUI::preferenceChanged(int elementType)
|
||||
m_antennaMapItem.setLatitude(stationLatitude);
|
||||
m_antennaMapItem.setLongitude(stationLongitude);
|
||||
m_antennaMapItem.setAltitude(stationAltitude);
|
||||
delete m_antennaMapItem.getPositionDateTime();
|
||||
m_antennaMapItem.setPositionDateTime(new QString(QDateTime::currentDateTime().toString(Qt::ISODateWithMs)));
|
||||
update(m_map, &m_antennaMapItem, "Station");
|
||||
|
||||
m_objectMapFilter.setPosition(stationPosition);
|
||||
m_imageMapFilter.setPosition(stationPosition);
|
||||
m_polygonMapFilter.setPosition(stationPosition);
|
||||
m_polylineMapFilter.setPosition(stationPosition);
|
||||
if (m_cesium)
|
||||
{
|
||||
m_cesium->setPosition(stationPosition);
|
||||
if (!m_lastFullUpdatePosition.isValid() || (stationPosition.distanceTo(m_lastFullUpdatePosition) >= 1000))
|
||||
{
|
||||
// Update all objects so distance filter is reapplied
|
||||
m_objectMapModel.allUpdated();
|
||||
m_lastFullUpdatePosition = stationPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pref == Preferences::StationName)
|
||||
else if (pref == Preferences::StationName)
|
||||
{
|
||||
// Update station name
|
||||
m_antennaMapItem.setLabel(new QString(MainCore::instance()->getSettings().getStationName()));
|
||||
m_antennaMapItem.setText(new QString(MainCore::instance()->getSettings().getStationName()));
|
||||
update(m_map, &m_antennaMapItem, "Station");
|
||||
}
|
||||
else if (pref == Preferences::MapSmoothing)
|
||||
{
|
||||
QQuickItem *item = ui->map->rootObject();
|
||||
QQmlProperty::write(item, "smoothing", MainCore::instance()->getSettings().getMapSmoothing());
|
||||
}
|
||||
}
|
||||
|
||||
void MapGUI::makeUIConnections()
|
||||
@ -1384,3 +1764,4 @@ void MapGUI::makeUIConnections()
|
||||
QObject::connect(ui->ibpBeacons, &QToolButton::clicked, this, &MapGUI::on_ibpBeacons_clicked);
|
||||
QObject::connect(ui->radiotime, &QToolButton::clicked, this, &MapGUI::on_radiotime_clicked);
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,8 @@
|
||||
#include "util/messagequeue.h"
|
||||
#include "util/giro.h"
|
||||
#include "util/azel.h"
|
||||
#include "util/openaip.h"
|
||||
#include "util/ourairportsdb.h"
|
||||
#include "settings/rollupstate.h"
|
||||
|
||||
#include "SWGMapItem.h"
|
||||
@ -152,11 +154,16 @@ public:
|
||||
void addRadioTimeTransmitters();
|
||||
void addRadar();
|
||||
void addIonosonde();
|
||||
void addBroadcast();
|
||||
void addDAB();
|
||||
void addNavAids();
|
||||
void addAirspace(const Airspace *airspace, const QString& group, int cnt);
|
||||
void addAirspace();
|
||||
void addAirports();
|
||||
void find(const QString& target);
|
||||
void track3D(const QString& target);
|
||||
Q_INVOKABLE void supportedMapsChanged();
|
||||
MapSettings::MapItemSettings *getItemSettings(const QString &group) { return m_settings.m_itemSettings[group]; }
|
||||
MapSettings::MapItemSettings *getItemSettings(const QString &group);
|
||||
CesiumInterface *cesium() { return m_cesium; }
|
||||
|
||||
private:
|
||||
@ -171,7 +178,14 @@ private:
|
||||
|
||||
Map* m_map;
|
||||
MessageQueue m_inputMessageQueue;
|
||||
MapModel m_mapModel;
|
||||
ObjectMapModel m_objectMapModel;
|
||||
ObjectMapFilter m_objectMapFilter;
|
||||
ImageMapModel m_imageMapModel;
|
||||
ImageFilter m_imageMapFilter;
|
||||
PolygonMapModel m_polygonMapModel;
|
||||
PolygonFilter m_polygonMapFilter;
|
||||
PolylineMapModel m_polylineMapModel;
|
||||
PolylineFilter m_polylineMapFilter;
|
||||
AzEl m_azEl; // Position of station
|
||||
SWGSDRangel::SWGMapItem m_antennaMapItem;
|
||||
QList<Beacon *> *m_beacons;
|
||||
@ -183,6 +197,10 @@ private:
|
||||
QTimer m_redrawMapTimer;
|
||||
GIRO *m_giro;
|
||||
QHash<QString, IonosondeStation *> m_ionosondeStations;
|
||||
QSharedPointer<const QList<NavAid *>> m_navAids;
|
||||
QSharedPointer<const QList<Airspace *>> m_airspaces;
|
||||
QSharedPointer<const QHash<int, AirportInformation *>> m_airportInfo;
|
||||
QGeoCoordinate m_lastFullUpdatePosition;
|
||||
|
||||
CesiumInterface *m_cesium;
|
||||
WebServer *m_webServer;
|
||||
@ -238,6 +256,9 @@ private slots:
|
||||
void giroDataUpdated(const GIRO::GIROStationData& data);
|
||||
void mufUpdated(const QJsonDocument& document);
|
||||
void foF2Updated(const QJsonDocument& document);
|
||||
void navAidsUpdated();
|
||||
void airspacesUpdated();
|
||||
void airportsUpdated();
|
||||
|
||||
};
|
||||
|
||||
|
288
plugins/feature/map/mapitem.cpp
Normal file
@ -0,0 +1,288 @@
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (C) 2023 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 "mapitem.h"
|
||||
|
||||
MapItem::MapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) :
|
||||
m_altitude(0.0)
|
||||
{
|
||||
m_sourcePipe = sourcePipe;
|
||||
m_group = group;
|
||||
m_itemSettings = itemSettings;
|
||||
m_name = *mapItem->getName();
|
||||
m_hashKey = m_sourcePipe->objectName() + m_name;
|
||||
}
|
||||
|
||||
void MapItem::update(SWGSDRangel::SWGMapItem *mapItem)
|
||||
{
|
||||
if (mapItem->getLabel()) {
|
||||
m_label = *mapItem->getLabel();
|
||||
} else {
|
||||
m_label = "";
|
||||
}
|
||||
m_latitude = mapItem->getLatitude();
|
||||
m_longitude = mapItem->getLongitude();
|
||||
m_altitude = mapItem->getAltitude();
|
||||
}
|
||||
|
||||
void ObjectMapItem::update(SWGSDRangel::SWGMapItem *mapItem)
|
||||
{
|
||||
MapItem::update(mapItem);
|
||||
if (mapItem->getPositionDateTime()) {
|
||||
m_positionDateTime = QDateTime::fromString(*mapItem->getPositionDateTime(), Qt::ISODateWithMs);
|
||||
} else {
|
||||
m_positionDateTime = QDateTime();
|
||||
}
|
||||
m_useHeadingPitchRoll = mapItem->getOrientation() == 1;
|
||||
m_heading = mapItem->getHeading();
|
||||
m_pitch = mapItem->getPitch();
|
||||
m_roll = mapItem->getRoll();
|
||||
if (mapItem->getOrientationDateTime()) {
|
||||
m_orientationDateTime = QDateTime::fromString(*mapItem->getOrientationDateTime(), Qt::ISODateWithMs);
|
||||
} else {
|
||||
m_orientationDateTime = QDateTime();
|
||||
}
|
||||
m_image = *mapItem->getImage();
|
||||
m_imageRotation = mapItem->getImageRotation();
|
||||
QString *text = mapItem->getText();
|
||||
if (text != nullptr) {
|
||||
m_text = text->replace("\n", "<br>"); // Convert to HTML
|
||||
} else {
|
||||
m_text = "";
|
||||
}
|
||||
if (mapItem->getModel()) {
|
||||
m_model = *mapItem->getModel();
|
||||
} else {
|
||||
m_model = "";
|
||||
}
|
||||
m_labelAltitudeOffset = mapItem->getLabelAltitudeOffset();
|
||||
m_modelAltitudeOffset = mapItem->getModelAltitudeOffset();
|
||||
m_altitudeReference = mapItem->getAltitudeReference();
|
||||
m_fixedPosition = mapItem->getFixedPosition();
|
||||
QList<SWGSDRangel::SWGMapAnimation *> *animations = mapItem->getAnimations();
|
||||
if (animations)
|
||||
{
|
||||
for (auto animation : *animations) {
|
||||
m_animations.append(new CesiumInterface::Animation(animation));
|
||||
}
|
||||
}
|
||||
findFrequency();
|
||||
if (!m_fixedPosition)
|
||||
{
|
||||
updateTrack(mapItem->getTrack());
|
||||
updatePredictedTrack(mapItem->getPredictedTrack());
|
||||
}
|
||||
if (mapItem->getAvailableUntil()) {
|
||||
m_availableUntil = QDateTime::fromString(*mapItem->getAvailableUntil(), Qt::ISODateWithMs);
|
||||
} else {
|
||||
m_availableUntil = QDateTime();
|
||||
}
|
||||
}
|
||||
|
||||
void ImageMapItem::update(SWGSDRangel::SWGMapItem *mapItem)
|
||||
{
|
||||
MapItem::update(mapItem);
|
||||
|
||||
m_image = "data:image/png;base64," + *mapItem->getImage();
|
||||
m_imageZoomLevel = mapItem->getImageZoomLevel();
|
||||
|
||||
float east = mapItem->getImageTileEast();
|
||||
float west = mapItem->getImageTileWest();
|
||||
float north = mapItem->getImageTileNorth();
|
||||
float south = mapItem->getImageTileSouth();
|
||||
m_latitude = north - (north - south) / 2.0;
|
||||
m_longitude = east - (east - west) / 2.0;
|
||||
m_bounds = QGeoRectangle(QGeoCoordinate(north, west), QGeoCoordinate(south, east));
|
||||
}
|
||||
|
||||
void PolygonMapItem::update(SWGSDRangel::SWGMapItem *mapItem)
|
||||
{
|
||||
MapItem::update(mapItem);
|
||||
m_extrudedHeight = mapItem->getExtrudedHeight();
|
||||
|
||||
qDeleteAll(m_points);
|
||||
m_points.clear();
|
||||
QList<SWGSDRangel::SWGMapCoordinate *> *coords = mapItem->getCoordinates();
|
||||
if (coords)
|
||||
{
|
||||
for (int i = 0; i < coords->size(); i++)
|
||||
{
|
||||
SWGSDRangel::SWGMapCoordinate* p = coords->at(i);
|
||||
QGeoCoordinate *c = new QGeoCoordinate(p->getLatitude(), p->getLongitude(), p->getAltitude());
|
||||
m_points.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate bounds
|
||||
m_polygon.clear();
|
||||
qreal latMin = 90.0, latMax = -90.0, lonMin = 180.0, lonMax = -180.0;
|
||||
for (const auto p : m_points)
|
||||
{
|
||||
QGeoCoordinate coord = *p;
|
||||
latMin = std::min(latMin, coord.latitude());
|
||||
latMax = std::max(latMax, coord.latitude());
|
||||
lonMin = std::min(lonMin, coord.longitude());
|
||||
lonMax = std::max(lonMax, coord.longitude());
|
||||
m_polygon.push_back(QVariant::fromValue(coord));
|
||||
}
|
||||
m_bounds = QGeoRectangle(QGeoCoordinate(latMax, lonMin), QGeoCoordinate(latMin, lonMax));
|
||||
}
|
||||
|
||||
void PolylineMapItem::update(SWGSDRangel::SWGMapItem *mapItem)
|
||||
{
|
||||
MapItem::update(mapItem);
|
||||
|
||||
qDeleteAll(m_points);
|
||||
m_points.clear();
|
||||
QList<SWGSDRangel::SWGMapCoordinate *> *coords = mapItem->getCoordinates();
|
||||
if (coords)
|
||||
{
|
||||
for (int i = 0; i < coords->size(); i++)
|
||||
{
|
||||
SWGSDRangel::SWGMapCoordinate* p = coords->at(i);
|
||||
QGeoCoordinate *c = new QGeoCoordinate(p->getLatitude(), p->getLongitude(), p->getAltitude());
|
||||
m_points.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate bounds
|
||||
m_polyline.clear();
|
||||
qreal latMin = 90.0, latMax = -90.0, lonMin = 180.0, lonMax = -180.0;
|
||||
for (const auto p : m_points)
|
||||
{
|
||||
QGeoCoordinate coord = *p;
|
||||
latMin = std::min(latMin, coord.latitude());
|
||||
latMax = std::max(latMax, coord.latitude());
|
||||
lonMin = std::min(lonMin, coord.longitude());
|
||||
lonMax = std::max(lonMax, coord.longitude());
|
||||
m_polyline.push_back(QVariant::fromValue(coord));
|
||||
}
|
||||
m_bounds = QGeoRectangle(QGeoCoordinate(latMax, lonMin), QGeoCoordinate(latMin, lonMax));
|
||||
}
|
||||
|
||||
QGeoCoordinate ObjectMapItem::getCoordinates()
|
||||
{
|
||||
QGeoCoordinate coords;
|
||||
coords.setLatitude(m_latitude);
|
||||
coords.setLongitude(m_longitude);
|
||||
return coords;
|
||||
}
|
||||
|
||||
void ObjectMapItem::findFrequency()
|
||||
{
|
||||
// Look for a frequency in the text for this object
|
||||
QRegExp re("(([0-9]+(\\.[0-9]+)?) *([kMG])?Hz)");
|
||||
if (re.indexIn(m_text) != -1)
|
||||
{
|
||||
QStringList capture = re.capturedTexts();
|
||||
m_frequency = capture[2].toDouble();
|
||||
if (capture.length() == 5)
|
||||
{
|
||||
QChar unit = capture[4][0];
|
||||
if (unit == 'k')
|
||||
m_frequency *= 1000.0;
|
||||
else if (unit == 'M')
|
||||
m_frequency *= 1000000.0;
|
||||
else if (unit == 'G')
|
||||
m_frequency *= 1000000000.0;
|
||||
}
|
||||
m_frequencyString = capture[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
m_frequency = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
void ObjectMapItem::updateTrack(QList<SWGSDRangel::SWGMapCoordinate *> *track)
|
||||
{
|
||||
if (track != nullptr)
|
||||
{
|
||||
qDeleteAll(m_takenTrackCoords);
|
||||
m_takenTrackCoords.clear();
|
||||
qDeleteAll(m_takenTrackDateTimes);
|
||||
m_takenTrackDateTimes.clear();
|
||||
m_takenTrack.clear();
|
||||
m_takenTrack1.clear();
|
||||
m_takenTrack2.clear();
|
||||
for (int i = 0; i < track->size(); i++)
|
||||
{
|
||||
SWGSDRangel::SWGMapCoordinate* p = track->at(i);
|
||||
QGeoCoordinate *c = new QGeoCoordinate(p->getLatitude(), p->getLongitude(), p->getAltitude());
|
||||
QDateTime *d = new QDateTime(QDateTime::fromString(*p->getDateTime(), Qt::ISODate));
|
||||
m_takenTrackCoords.push_back(c);
|
||||
m_takenTrackDateTimes.push_back(d);
|
||||
m_takenTrack.push_back(QVariant::fromValue(*c));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Automatically create a track
|
||||
if (m_takenTrackCoords.size() == 0)
|
||||
{
|
||||
QGeoCoordinate *c = new QGeoCoordinate(m_latitude, m_longitude, m_altitude);
|
||||
m_takenTrackCoords.push_back(c);
|
||||
if (m_positionDateTime.isValid()) {
|
||||
m_takenTrackDateTimes.push_back(new QDateTime(m_positionDateTime));
|
||||
} else {
|
||||
m_takenTrackDateTimes.push_back(new QDateTime(QDateTime::currentDateTime()));
|
||||
}
|
||||
m_takenTrack.push_back(QVariant::fromValue(*c));
|
||||
}
|
||||
else
|
||||
{
|
||||
QGeoCoordinate *prev = m_takenTrackCoords.last();
|
||||
QDateTime *prevDateTime = m_takenTrackDateTimes.last();
|
||||
if ((prev->latitude() != m_latitude) || (prev->longitude() != m_longitude)
|
||||
|| (prev->altitude() != m_altitude) || (*prevDateTime != m_positionDateTime))
|
||||
{
|
||||
QGeoCoordinate *c = new QGeoCoordinate(m_latitude, m_longitude, m_altitude);
|
||||
m_takenTrackCoords.push_back(c);
|
||||
if (m_positionDateTime.isValid()) {
|
||||
m_takenTrackDateTimes.push_back(new QDateTime(m_positionDateTime));
|
||||
} else {
|
||||
m_takenTrackDateTimes.push_back(new QDateTime(QDateTime::currentDateTime()));
|
||||
}
|
||||
m_takenTrack.push_back(QVariant::fromValue(*c));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ObjectMapItem::updatePredictedTrack(QList<SWGSDRangel::SWGMapCoordinate *> *track)
|
||||
{
|
||||
if (track != nullptr)
|
||||
{
|
||||
qDeleteAll(m_predictedTrackCoords);
|
||||
m_predictedTrackCoords.clear();
|
||||
qDeleteAll(m_predictedTrackDateTimes);
|
||||
m_predictedTrackDateTimes.clear();
|
||||
m_predictedTrack.clear();
|
||||
m_predictedTrack1.clear();
|
||||
m_predictedTrack2.clear();
|
||||
for (int i = 0; i < track->size(); i++)
|
||||
{
|
||||
SWGSDRangel::SWGMapCoordinate* p = track->at(i);
|
||||
QGeoCoordinate *c = new QGeoCoordinate(p->getLatitude(), p->getLongitude(), p->getAltitude());
|
||||
QDateTime *d = new QDateTime(QDateTime::fromString(*p->getDateTime(), Qt::ISODate));
|
||||
m_predictedTrackCoords.push_back(c);
|
||||
m_predictedTrackDateTimes.push_back(d);
|
||||
m_predictedTrack.push_back(QVariant::fromValue(*c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
184
plugins/feature/map/mapitem.h
Normal file
@ -0,0 +1,184 @@
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (C) 2023 Jon Beniston, M7RCE //
|
||||
// //
|
||||
// This program is free software; you can redistribute it and/or modify //
|
||||
// it under the terms of the GNU General Public License as published by //
|
||||
// the Free Software Foundation as version 3 of the License, or //
|
||||
// (at your option) any later version. //
|
||||
// //
|
||||
// This program is distributed in the hope that it will be useful, //
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
|
||||
// GNU General Public License V3 for more details. //
|
||||
// //
|
||||
// You should have received a copy of the GNU General Public License //
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef INCLUDE_FEATURE_MAPITEM_H_
|
||||
#define INCLUDE_FEATURE_MAPITEM_H_
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QGeoCoordinate>
|
||||
#include <QGeoRectangle>
|
||||
|
||||
#include "mapsettings.h"
|
||||
#include "cesiuminterface.h"
|
||||
|
||||
#include "SWGMapItem.h"
|
||||
|
||||
class MapModel;
|
||||
class ObjectMapModel;
|
||||
class PolygonMapModel;
|
||||
class PolylineMapModel;
|
||||
class ImageMapModel;
|
||||
class CZML;
|
||||
|
||||
class MapItem {
|
||||
|
||||
public:
|
||||
|
||||
MapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem);
|
||||
virtual void update(SWGSDRangel::SWGMapItem *mapItem);
|
||||
|
||||
protected:
|
||||
|
||||
friend CZML;
|
||||
friend MapModel;
|
||||
friend ObjectMapModel;
|
||||
friend PolygonMapModel;
|
||||
friend PolylineMapModel;
|
||||
|
||||
QString m_group;
|
||||
MapSettings::MapItemSettings *m_itemSettings;
|
||||
const QObject *m_sourcePipe; // Channel/feature that created the item
|
||||
QString m_hashKey;
|
||||
|
||||
QString m_name; // Unique id
|
||||
QString m_label;
|
||||
float m_latitude; // Position for label
|
||||
float m_longitude;
|
||||
float m_altitude; // In metres
|
||||
QDateTime m_availableUntil; // Date & time this item is visible until (for 3D map). Invalid date/time is forever
|
||||
};
|
||||
|
||||
|
||||
// Information required about each item displayed on the map
|
||||
class ObjectMapItem : public MapItem {
|
||||
|
||||
public:
|
||||
ObjectMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) :
|
||||
MapItem(sourcePipe, group, itemSettings, mapItem)
|
||||
{
|
||||
update(mapItem);
|
||||
}
|
||||
void update(SWGSDRangel::SWGMapItem *mapItem) override;
|
||||
QGeoCoordinate getCoordinates();
|
||||
|
||||
protected:
|
||||
void findFrequency();
|
||||
void updateTrack(QList<SWGSDRangel::SWGMapCoordinate *> *track);
|
||||
void updatePredictedTrack(QList<SWGSDRangel::SWGMapCoordinate *> *track);
|
||||
|
||||
friend ObjectMapModel;
|
||||
friend CZML;
|
||||
QDateTime m_positionDateTime;
|
||||
bool m_useHeadingPitchRoll;
|
||||
float m_heading;
|
||||
float m_pitch;
|
||||
float m_roll;
|
||||
QDateTime m_orientationDateTime;
|
||||
QString m_image;
|
||||
int m_imageRotation;
|
||||
QString m_text;
|
||||
double m_frequency; // Frequency to set
|
||||
QString m_frequencyString;
|
||||
bool m_fixedPosition; // Don't record/display track
|
||||
QList<QGeoCoordinate *> m_predictedTrackCoords;
|
||||
QList<QDateTime *> m_predictedTrackDateTimes;
|
||||
QVariantList m_predictedTrack; // Line showing where the object is going
|
||||
QVariantList m_predictedTrack1;
|
||||
QVariantList m_predictedTrack2;
|
||||
QGeoCoordinate m_predictedStart1;
|
||||
QGeoCoordinate m_predictedStart2;
|
||||
QGeoCoordinate m_predictedEnd1;
|
||||
QGeoCoordinate m_predictedEnd2;
|
||||
QList<QGeoCoordinate *> m_takenTrackCoords;
|
||||
QList<QDateTime *> m_takenTrackDateTimes;
|
||||
QVariantList m_takenTrack; // Line showing where the object has been
|
||||
QVariantList m_takenTrack1;
|
||||
QVariantList m_takenTrack2;
|
||||
QGeoCoordinate m_takenStart1;
|
||||
QGeoCoordinate m_takenStart2;
|
||||
QGeoCoordinate m_takenEnd1;
|
||||
QGeoCoordinate m_takenEnd2;
|
||||
|
||||
// For 3D map
|
||||
QString m_model;
|
||||
int m_altitudeReference;
|
||||
float m_labelAltitudeOffset;
|
||||
float m_modelAltitudeOffset;
|
||||
QList<CesiumInterface::Animation *> m_animations;
|
||||
};
|
||||
|
||||
class PolygonMapItem : public MapItem {
|
||||
|
||||
public:
|
||||
PolygonMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) :
|
||||
MapItem(sourcePipe, group, itemSettings, mapItem)
|
||||
{
|
||||
update(mapItem);
|
||||
}
|
||||
void update(SWGSDRangel::SWGMapItem *mapItem) override;
|
||||
|
||||
protected:
|
||||
friend PolygonMapModel;
|
||||
friend CZML;
|
||||
|
||||
QList<QGeoCoordinate *> m_points; // FIXME: Remove?
|
||||
float m_extrudedHeight; // In metres
|
||||
QVariantList m_polygon;
|
||||
QGeoRectangle m_bounds; // Bounding boxes for the polygons, for view clipping
|
||||
};
|
||||
|
||||
class PolylineMapItem : public MapItem {
|
||||
|
||||
public:
|
||||
PolylineMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) :
|
||||
MapItem(sourcePipe, group, itemSettings, mapItem)
|
||||
{
|
||||
update(mapItem);
|
||||
}
|
||||
void update(SWGSDRangel::SWGMapItem *mapItem) override;
|
||||
|
||||
protected:
|
||||
friend PolylineMapModel;
|
||||
friend CZML;
|
||||
|
||||
QList<QGeoCoordinate *> m_points; // FIXME: Remove?
|
||||
QVariantList m_polyline;
|
||||
QGeoRectangle m_bounds; // Bounding boxes for the polyline, for view clipping
|
||||
};
|
||||
|
||||
class ImageMapItem : public MapItem {
|
||||
|
||||
public:
|
||||
ImageMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) :
|
||||
MapItem(sourcePipe, group, itemSettings, mapItem)
|
||||
{
|
||||
update(mapItem);
|
||||
}
|
||||
void update(SWGSDRangel::SWGMapItem *mapItem) override;
|
||||
|
||||
protected:
|
||||
friend ImageMapModel;
|
||||
friend CZML;
|
||||
|
||||
QString m_image;
|
||||
float m_imageZoomLevel;
|
||||
QGeoRectangle m_bounds;
|
||||
|
||||
};
|
||||
|
||||
#endif // INCLUDE_FEATURE_MAPITEM_H_
|
||||
|
@ -19,135 +19,227 @@
|
||||
#define INCLUDE_FEATURE_MAPMODEL_H_
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QGeoCoordinate>
|
||||
#include <QGeoRectangle>
|
||||
#include <QColor>
|
||||
|
||||
#include "util/azel.h"
|
||||
#include "util/openaip.h"
|
||||
#include "mapsettings.h"
|
||||
#include "mapitem.h"
|
||||
#include "cesiuminterface.h"
|
||||
|
||||
#include "SWGMapItem.h"
|
||||
|
||||
class MapModel;
|
||||
class MapGUI;
|
||||
class CZML;
|
||||
|
||||
// Information required about each item displayed on the map
|
||||
class MapItem {
|
||||
|
||||
public:
|
||||
MapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem);
|
||||
void update(SWGSDRangel::SWGMapItem *mapItem);
|
||||
QGeoCoordinate getCoordinates();
|
||||
|
||||
private:
|
||||
void findFrequency();
|
||||
void updateTrack(QList<SWGSDRangel::SWGMapCoordinate *> *track);
|
||||
void updatePredictedTrack(QList<SWGSDRangel::SWGMapCoordinate *> *track);
|
||||
|
||||
friend MapModel;
|
||||
friend CZML;
|
||||
QString m_group;
|
||||
MapSettings::MapItemSettings *m_itemSettings;
|
||||
const QObject *m_sourcePipe; // Channel/feature that created the item
|
||||
QString m_name;
|
||||
QString m_label;
|
||||
float m_latitude;
|
||||
float m_longitude;
|
||||
float m_altitude; // In metres
|
||||
QDateTime m_positionDateTime;
|
||||
bool m_useHeadingPitchRoll;
|
||||
float m_heading;
|
||||
float m_pitch;
|
||||
float m_roll;
|
||||
QDateTime m_orientationDateTime;
|
||||
QString m_image;
|
||||
int m_imageRotation;
|
||||
QString m_text;
|
||||
double m_frequency; // Frequency to set
|
||||
QString m_frequencyString;
|
||||
QList<QGeoCoordinate *> m_predictedTrackCoords;
|
||||
QList<QDateTime *> m_predictedTrackDateTimes;
|
||||
QVariantList m_predictedTrack; // Line showing where the object is going
|
||||
QVariantList m_predictedTrack1;
|
||||
QVariantList m_predictedTrack2;
|
||||
QGeoCoordinate m_predictedStart1;
|
||||
QGeoCoordinate m_predictedStart2;
|
||||
QGeoCoordinate m_predictedEnd1;
|
||||
QGeoCoordinate m_predictedEnd2;
|
||||
QList<QGeoCoordinate *> m_takenTrackCoords;
|
||||
QList<QDateTime *> m_takenTrackDateTimes;
|
||||
QVariantList m_takenTrack; // Line showing where the object has been
|
||||
QVariantList m_takenTrack1;
|
||||
QVariantList m_takenTrack2;
|
||||
QGeoCoordinate m_takenStart1;
|
||||
QGeoCoordinate m_takenStart2;
|
||||
QGeoCoordinate m_takenEnd1;
|
||||
QGeoCoordinate m_takenEnd2;
|
||||
|
||||
// For 3D map
|
||||
QString m_model;
|
||||
int m_altitudeReference;
|
||||
float m_labelAltitudeOffset;
|
||||
float m_modelAltitudeOffset;
|
||||
bool m_fixedPosition;
|
||||
QList<CesiumInterface::Animation *> m_animations;
|
||||
};
|
||||
|
||||
// Model used for each item on the map
|
||||
class MapModel : public QAbstractListModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
using QAbstractListModel::QAbstractListModel;
|
||||
enum MarkerRoles {
|
||||
positionRole = Qt::UserRole + 1,
|
||||
mapTextRole = Qt::UserRole + 2,
|
||||
mapTextVisibleRole = Qt::UserRole + 3,
|
||||
mapImageVisibleRole = Qt::UserRole + 4,
|
||||
mapImageRole = Qt::UserRole + 5,
|
||||
mapImageRotationRole = Qt::UserRole + 6,
|
||||
mapImageMinZoomRole = Qt::UserRole + 7,
|
||||
bubbleColourRole = Qt::UserRole + 8,
|
||||
selectedRole = Qt::UserRole + 9,
|
||||
targetRole = Qt::UserRole + 10,
|
||||
frequencyRole = Qt::UserRole + 11,
|
||||
frequencyStringRole = Qt::UserRole + 12,
|
||||
predictedGroundTrack1Role = Qt::UserRole + 13,
|
||||
predictedGroundTrack2Role = Qt::UserRole + 14,
|
||||
groundTrack1Role = Qt::UserRole + 15,
|
||||
groundTrack2Role = Qt::UserRole + 16,
|
||||
groundTrackColorRole = Qt::UserRole + 17,
|
||||
predictedGroundTrackColorRole = Qt::UserRole + 18
|
||||
itemSettingsRole = Qt::UserRole + 1,
|
||||
nameRole = Qt::UserRole + 2,
|
||||
labelRole = Qt::UserRole + 3,
|
||||
positionRole = Qt::UserRole + 4,
|
||||
mapImageMinZoomRole = Qt::UserRole + 5,
|
||||
lastRole = Qt::UserRole + 6
|
||||
};
|
||||
|
||||
MapModel(MapGUI *gui);
|
||||
MapModel(MapGUI *gui) :
|
||||
m_gui(gui)
|
||||
{
|
||||
connect(this, &MapModel::dataChanged, this, &MapModel::update3DMap);
|
||||
}
|
||||
|
||||
void playAnimations(MapItem *item);
|
||||
|
||||
Q_INVOKABLE void add(MapItem *item);
|
||||
virtual void add(MapItem *item);
|
||||
virtual void remove(MapItem *item);
|
||||
virtual void removeAll();
|
||||
void update(const QObject *source, SWGSDRangel::SWGMapItem *swgMapItem, const QString &group="");
|
||||
void update(MapItem *item);
|
||||
void remove(MapItem *item);
|
||||
void allUpdated();
|
||||
void removeAll();
|
||||
void updateItemSettings(QHash<QString, MapSettings::MapItemSettings *> m_itemSettings);
|
||||
void allUpdated();
|
||||
|
||||
MapItem *findMapItem(const QObject *source, const QString& name);
|
||||
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
bool setData(const QModelIndex &index, const QVariant& value, int role = Qt::EditRole) override;
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override
|
||||
{
|
||||
(void) parent;
|
||||
return m_items.count();
|
||||
}
|
||||
|
||||
Qt::ItemFlags flags(const QModelIndex &index) const override
|
||||
{
|
||||
(void) index;
|
||||
return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable;
|
||||
}
|
||||
|
||||
public slots:
|
||||
void update3DMap(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles = QVector<int>());
|
||||
|
||||
protected:
|
||||
|
||||
MapGUI *m_gui;
|
||||
QList<MapItem *> m_items;
|
||||
QHash<QString, MapItem *> m_itemsHash;
|
||||
|
||||
virtual void update(MapItem *item);
|
||||
virtual void update3D(MapItem *item) = 0;
|
||||
virtual MapItem *newMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) = 0;
|
||||
|
||||
};
|
||||
|
||||
class ImageMapModel : public MapModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum MarkerRoles {
|
||||
imageRole = MapModel::lastRole + 0,
|
||||
imageZoomLevelRole = MapModel::lastRole + 1,
|
||||
boundsRole = MapModel::lastRole + 2
|
||||
};
|
||||
|
||||
ImageMapModel(MapGUI *gui) :
|
||||
MapModel(gui)
|
||||
{}
|
||||
|
||||
QHash<int, QByteArray> roleNames() const override
|
||||
{
|
||||
QHash<int, QByteArray> roles = MapModel::roleNames();
|
||||
roles[imageRole] = "imageData";
|
||||
roles[imageZoomLevelRole] = "imageZoomLevel";
|
||||
roles[boundsRole] = "bounds";
|
||||
return roles;
|
||||
}
|
||||
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
protected:
|
||||
MapItem *newMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) override;
|
||||
void update3D(MapItem *item) override;
|
||||
|
||||
};
|
||||
|
||||
class PolygonMapModel : public MapModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum MarkerRoles {
|
||||
borderColorRole = MapModel::lastRole + 0,
|
||||
fillColorRole = MapModel::lastRole + 1,
|
||||
polygonRole = MapModel::lastRole + 2,
|
||||
boundsRole = MapModel::lastRole + 3
|
||||
};
|
||||
|
||||
PolygonMapModel(MapGUI *gui) :
|
||||
MapModel(gui)
|
||||
{}
|
||||
|
||||
QHash<int, QByteArray> roleNames() const override
|
||||
{
|
||||
QHash<int, QByteArray> roles = MapModel::roleNames();
|
||||
roles[borderColorRole] = "borderColor";
|
||||
roles[fillColorRole] = "fillColor";
|
||||
roles[polygonRole] = "polygon";
|
||||
roles[boundsRole] = "bounds";
|
||||
return roles;
|
||||
}
|
||||
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
protected:
|
||||
void update3D(MapItem *item) override;
|
||||
MapItem *newMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) override;
|
||||
|
||||
};
|
||||
|
||||
class PolylineMapModel : public MapModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
enum MarkerRoles {
|
||||
lineColorRole = MapModel::lastRole + 0,
|
||||
coordinatesRole = MapModel::lastRole + 1,
|
||||
boundsRole = MapModel::lastRole + 2,
|
||||
};
|
||||
|
||||
PolylineMapModel(MapGUI *gui) :
|
||||
MapModel(gui)
|
||||
{}
|
||||
|
||||
QHash<int, QByteArray> roleNames() const override
|
||||
{
|
||||
QHash<int, QByteArray> roles = MapModel::roleNames();
|
||||
roles[lineColorRole] = "lineColor";
|
||||
roles[coordinatesRole] = "coordinates";
|
||||
roles[boundsRole] = "bounds";
|
||||
return roles;
|
||||
}
|
||||
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
protected:
|
||||
void update3D(MapItem *item) override;
|
||||
MapItem *newMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) override;
|
||||
};
|
||||
|
||||
// Model used for each item on the map
|
||||
class ObjectMapModel : public MapModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
enum MarkerRoles {
|
||||
//itemSettingsRole = Qt::UserRole + 1,
|
||||
//nameRole = Qt::UserRole + 2,
|
||||
//positionRole = Qt::UserRole + 3,
|
||||
// label? mapTextRole = Qt::UserRole + 4,
|
||||
mapTextVisibleRole =MapModel::lastRole + 0,
|
||||
mapImageVisibleRole = MapModel::lastRole + 1,
|
||||
mapImageRole = MapModel::lastRole + 2,
|
||||
mapImageRotationRole = MapModel::lastRole + 3,
|
||||
//mapImageMinZoomRole = MapModel::lastRole + 9,
|
||||
bubbleColourRole = MapModel::lastRole + 4,
|
||||
selectedRole = MapModel::lastRole + 5,
|
||||
targetRole = MapModel::lastRole + 6,
|
||||
frequencyRole = MapModel::lastRole + 7,
|
||||
frequencyStringRole = MapModel::lastRole + 8,
|
||||
predictedGroundTrack1Role = MapModel::lastRole + 9,
|
||||
predictedGroundTrack2Role = MapModel::lastRole + 10,
|
||||
groundTrack1Role = MapModel::lastRole + 11,
|
||||
groundTrack2Role = MapModel::lastRole + 12,
|
||||
groundTrackColorRole = MapModel::lastRole + 13,
|
||||
predictedGroundTrackColorRole = MapModel::lastRole + 14,
|
||||
hasTracksRole = MapModel::lastRole + 15
|
||||
};
|
||||
|
||||
ObjectMapModel(MapGUI *gui);
|
||||
|
||||
Q_INVOKABLE void add(MapItem *item) override;
|
||||
//void update(const QObject *source, SWGSDRangel::SWGMapItem *swgMapItem, const QString &group="");
|
||||
using MapModel::update;
|
||||
void remove(MapItem *item) override;
|
||||
void removeAll() override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
void updateTarget();
|
||||
void setTarget(const QString& name);
|
||||
bool isTarget(const MapItem *mapItem) const;
|
||||
bool isTarget(const ObjectMapItem *mapItem) const;
|
||||
|
||||
Q_INVOKABLE void moveToFront(int oldRow);
|
||||
Q_INVOKABLE void moveToBack(int oldRow);
|
||||
|
||||
MapItem *findMapItem(const QObject *source, const QString& name);
|
||||
MapItem *findMapItem(const QString& name);
|
||||
ObjectMapItem *findMapItem(const QString& name);
|
||||
QModelIndex findMapItemIndex(const QString& name);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
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 setDisplayNames(bool displayNames);
|
||||
void setDisplaySelectedGroundTracks(bool displayGroundTracks);
|
||||
@ -155,48 +247,9 @@ public:
|
||||
Q_INVOKABLE void setFrequency(double frequency);
|
||||
Q_INVOKABLE void track3D(int index);
|
||||
|
||||
void interpolateEast(QGeoCoordinate *c1, QGeoCoordinate *c2, double x, QGeoCoordinate *ci, bool offScreen);
|
||||
void interpolateWest(QGeoCoordinate *c1, QGeoCoordinate *c2, double x, QGeoCoordinate *ci, bool offScreen);
|
||||
void interpolate(QGeoCoordinate *c1, QGeoCoordinate *c2, double bottomLeftLongitude, double bottomRightLongitude, QGeoCoordinate* ci, bool offScreen);
|
||||
|
||||
void splitTracks(MapItem *item);
|
||||
void splitTrack(const QList<QGeoCoordinate *>& coords, const QVariantList& track,
|
||||
QVariantList& track1, QVariantList& track2,
|
||||
QGeoCoordinate& start1, QGeoCoordinate& start2,
|
||||
QGeoCoordinate& end1, QGeoCoordinate& end2);
|
||||
Q_INVOKABLE void viewChanged(double bottomLeftLongitude, double bottomRightLongitude);
|
||||
|
||||
QHash<int, QByteArray> roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[positionRole] = "position";
|
||||
roles[mapTextRole] = "mapText";
|
||||
roles[mapTextVisibleRole] = "mapTextVisible";
|
||||
roles[mapImageVisibleRole] = "mapImageVisible";
|
||||
roles[mapImageRole] = "mapImage";
|
||||
roles[mapImageRotationRole] = "mapImageRotation";
|
||||
roles[mapImageMinZoomRole] = "mapImageMinZoom";
|
||||
roles[bubbleColourRole] = "bubbleColour";
|
||||
roles[selectedRole] = "selected";
|
||||
roles[targetRole] = "target";
|
||||
roles[frequencyRole] = "frequency";
|
||||
roles[frequencyStringRole] = "frequencyString";
|
||||
roles[predictedGroundTrack1Role] = "predictedGroundTrack1";
|
||||
roles[predictedGroundTrack2Role] = "predictedGroundTrack2";
|
||||
roles[groundTrack1Role] = "groundTrack1";
|
||||
roles[groundTrack2Role] = "groundTrack2";
|
||||
roles[groundTrackColorRole] = "groundTrackColor";
|
||||
roles[predictedGroundTrackColorRole] = "predictedGroundTrackColor";
|
||||
return roles;
|
||||
}
|
||||
|
||||
// Linear interpolation
|
||||
double interpolate(double x0, double y0, double x1, double y1, double x)
|
||||
{
|
||||
return (y0*(x1-x) + y1*(x-x0)) / (x1-x0);
|
||||
}
|
||||
|
||||
bool isSelected3D(const MapItem *item) const
|
||||
bool isSelected3D(const ObjectMapItem *item) const
|
||||
{
|
||||
return m_selected3D == item->m_name;
|
||||
}
|
||||
@ -206,13 +259,24 @@ public:
|
||||
m_selected3D = selected;
|
||||
}
|
||||
|
||||
public slots:
|
||||
void update3DMap(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles = QVector<int>());
|
||||
//public slots:
|
||||
// void update3DMap(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles = QVector<int>()) override;
|
||||
|
||||
protected:
|
||||
void playAnimations(ObjectMapItem *item);
|
||||
MapItem *newMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) override;
|
||||
void update(MapItem *item) override;
|
||||
void update3D(MapItem *item) override;
|
||||
|
||||
// Linear interpolation
|
||||
double interpolate(double x0, double y0, double x1, double y1, double x) const
|
||||
{
|
||||
return (y0*(x1-x) + y1*(x-x0)) / (x1-x0);
|
||||
}
|
||||
|
||||
private:
|
||||
MapGUI *m_gui;
|
||||
QList<MapItem *> m_items;
|
||||
QList<bool> m_selected;
|
||||
QList<bool> m_selected; // Whether each item on 2D map is selected
|
||||
QString m_selected3D; // Name of item selected on 3D map - only supports 1 item, unlike 2D map
|
||||
int m_target; // Row number of current target, or -1 for none
|
||||
bool m_displayNames;
|
||||
bool m_displaySelectedGroundTracks;
|
||||
@ -221,8 +285,112 @@ private:
|
||||
double m_bottomLeftLongitude;
|
||||
double m_bottomRightLongitude;
|
||||
|
||||
QString m_selected3D; // Name of item selected on 3D map - only supports 1 item, unlike 2D map
|
||||
void interpolateEast(QGeoCoordinate *c1, QGeoCoordinate *c2, double x, QGeoCoordinate *ci, bool offScreen);
|
||||
void interpolateWest(QGeoCoordinate *c1, QGeoCoordinate *c2, double x, QGeoCoordinate *ci, bool offScreen);
|
||||
void interpolate(QGeoCoordinate *c1, QGeoCoordinate *c2, double bottomLeftLongitude, double bottomRightLongitude, QGeoCoordinate* ci, bool offScreen);
|
||||
|
||||
void splitTracks(ObjectMapItem *item);
|
||||
void splitTrack(const QList<QGeoCoordinate *>& coords, const QVariantList& track,
|
||||
QVariantList& track1, QVariantList& track2,
|
||||
QGeoCoordinate& start1, QGeoCoordinate& start2,
|
||||
QGeoCoordinate& end1, QGeoCoordinate& end2);
|
||||
};
|
||||
|
||||
class PolygonFilter : public QSortFilterProxyModel {
|
||||
Q_OBJECT
|
||||
public:
|
||||
PolygonFilter() :
|
||||
m_zoomLevel(10),
|
||||
m_settings(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const;
|
||||
Q_INVOKABLE void viewChanged(double topLeftLongitude, double topLeftLatitude, double bottomRightLongitude, double bottomRightLatitude, double zoomLevel);
|
||||
Q_INVOKABLE int mapRowToSource(int row);
|
||||
void setPosition(const QGeoCoordinate& position);
|
||||
|
||||
private:
|
||||
QGeoRectangle m_view;
|
||||
double m_zoomLevel;
|
||||
MapSettings *m_settings;
|
||||
QGeoCoordinate m_position;;
|
||||
|
||||
};
|
||||
|
||||
class PolylineFilter : public QSortFilterProxyModel {
|
||||
Q_OBJECT
|
||||
public:
|
||||
PolylineFilter() :
|
||||
m_zoomLevel(10),
|
||||
m_settings(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const;
|
||||
Q_INVOKABLE void viewChanged(double topLeftLongitude, double topLeftLatitude, double bottomRightLongitude, double bottomRightLatitude, double zoomLevel);
|
||||
Q_INVOKABLE int mapRowToSource(int row);
|
||||
void setPosition(const QGeoCoordinate& position);
|
||||
|
||||
private:
|
||||
QGeoRectangle m_view;
|
||||
double m_zoomLevel;
|
||||
MapSettings *m_settings;
|
||||
QGeoCoordinate m_position;;
|
||||
|
||||
};
|
||||
|
||||
class ObjectMapFilter : public QSortFilterProxyModel {
|
||||
Q_OBJECT
|
||||
public:
|
||||
ObjectMapFilter() :
|
||||
m_topLeftLongitude(0),
|
||||
m_topLeftLatitude(0),
|
||||
m_bottomRightLongitude(0),
|
||||
m_bottomRightLatitude(0),
|
||||
m_zoomLevel(10),
|
||||
m_settings(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const;
|
||||
Q_INVOKABLE void viewChanged(double topLeftLongitude, double topLeftLatitude, double bottomRightLongitude, double bottomRightLatitude, double zoomLevel);
|
||||
void applySettings(MapSettings *settings);
|
||||
Q_INVOKABLE int mapRowToSource(int row);
|
||||
void setPosition(const QGeoCoordinate& position);
|
||||
|
||||
private:
|
||||
double m_topLeftLongitude;
|
||||
double m_topLeftLatitude;
|
||||
double m_bottomRightLongitude;
|
||||
double m_bottomRightLatitude;
|
||||
double m_zoomLevel;
|
||||
MapSettings *m_settings;
|
||||
QGeoCoordinate m_position;
|
||||
|
||||
};
|
||||
|
||||
class ImageFilter : public QSortFilterProxyModel {
|
||||
Q_OBJECT
|
||||
public:
|
||||
ImageFilter() :
|
||||
m_zoomLevel(10),
|
||||
m_settings(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const;
|
||||
Q_INVOKABLE void viewChanged(double topLeftLongitude, double topLeftLatitude, double bottomRightLongitude, double bottomRightLatitude, double zoomLevel);
|
||||
Q_INVOKABLE int mapRowToSource(int row);
|
||||
void setPosition(const QGeoCoordinate& position);
|
||||
|
||||
private:
|
||||
QGeoRectangle m_view;
|
||||
double m_zoomLevel;
|
||||
MapSettings *m_settings;
|
||||
QGeoCoordinate m_position;;
|
||||
|
||||
};
|
||||
|
||||
#endif // INCLUDE_FEATURE_MAPMODEL_H_
|
||||
|
||||
|
@ -26,25 +26,31 @@
|
||||
#include "mapsettings.h"
|
||||
|
||||
const QStringList MapSettings::m_pipeTypes = {
|
||||
QStringLiteral("ACARSDemod"),
|
||||
QStringLiteral("ADSBDemod"),
|
||||
QStringLiteral("AIS"),
|
||||
QStringLiteral("APRS"),
|
||||
QStringLiteral("APTDemod"),
|
||||
QStringLiteral("FT8Demod"),
|
||||
QStringLiteral("HeatMap"),
|
||||
QStringLiteral("Radiosonde"),
|
||||
QStringLiteral("StarTracker"),
|
||||
QStringLiteral("SatelliteTracker")
|
||||
QStringLiteral("SatelliteTracker"),
|
||||
QStringLiteral("VORLocalizer")
|
||||
};
|
||||
|
||||
const QStringList MapSettings::m_pipeURIs = {
|
||||
QStringLiteral("sdrangel.channel.acarsdemod"),
|
||||
QStringLiteral("sdrangel.channel.adsbdemod"),
|
||||
QStringLiteral("sdrangel.feature.ais"),
|
||||
QStringLiteral("sdrangel.feature.aprs"),
|
||||
QStringLiteral("sdrangel.channel.aptdemod"),
|
||||
QStringLiteral("sdrangel.channel.ft8demod"),
|
||||
QStringLiteral("sdrangel.channel.heatmap"),
|
||||
QStringLiteral("sdrangel.feature.radiosonde"),
|
||||
QStringLiteral("sdrangel.feature.startracker"),
|
||||
QStringLiteral("sdrangel.feature.satellitetracker")
|
||||
QStringLiteral("sdrangel.feature.satellitetracker"),
|
||||
QStringLiteral("sdrangel.feature.vorlocalizer")
|
||||
};
|
||||
|
||||
// GUI combo box should match ordering in this list
|
||||
@ -62,24 +68,70 @@ MapSettings::MapSettings() :
|
||||
// Source names should match m_pipeTypes
|
||||
// Colors currently match color of rollup widget for that plugin
|
||||
int modelMinPixelSize = 50;
|
||||
m_itemSettings.insert("ADSBDemod", new MapItemSettings("ADSBDemod", QColor(244, 151, 57), false, 11, modelMinPixelSize));
|
||||
m_itemSettings.insert("AIS", new MapItemSettings("AIS", QColor(102, 0, 0), false, 11, modelMinPixelSize));
|
||||
m_itemSettings.insert("APRS", new MapItemSettings("APRS", QColor(255, 255, 0), false, 11));
|
||||
m_itemSettings.insert("StarTracker", new MapItemSettings("StarTracker", QColor(230, 230, 230), true, 3));
|
||||
m_itemSettings.insert("SatelliteTracker", new MapItemSettings("SatelliteTracker", QColor(0, 0, 255), false, 0, modelMinPixelSize));
|
||||
m_itemSettings.insert("Beacons", new MapItemSettings("Beacons", QColor(255, 0, 0), true, 8));
|
||||
m_itemSettings.insert("Radiosonde", new MapItemSettings("Radiosonde", QColor(102, 0, 102), false, 11, modelMinPixelSize));
|
||||
m_itemSettings.insert("Radio Time Transmitters", new MapItemSettings("Radio Time Transmitters", QColor(255, 0, 0), true, 8));
|
||||
m_itemSettings.insert("Radar", new MapItemSettings("Radar", QColor(255, 0, 0), true, 8));
|
||||
m_itemSettings.insert("FT8Demod", new MapItemSettings("FT8Demod", QColor(0, 192, 255), true, 8));
|
||||
MapItemSettings *aptSettings = new MapItemSettings("APTDemod", true, QColor(216, 112, 169), true, false, 11);
|
||||
aptSettings->m_display2DIcon = false; // Disabled as 2D projection is wrong
|
||||
m_itemSettings.insert("APTDemod", aptSettings);
|
||||
m_itemSettings.insert("ACARSDemod", new MapItemSettings("ACARSDemod", true, QColor(244, 151, 57), true, false, 11, modelMinPixelSize));
|
||||
m_itemSettings.insert("ADSBDemod", new MapItemSettings("ADSBDemod", true, QColor(244, 151, 57), true, false, 11, modelMinPixelSize));
|
||||
m_itemSettings.insert("AIS", new MapItemSettings("AIS", true, QColor(102, 0, 0), true, false, 11, modelMinPixelSize));
|
||||
MapItemSettings *aprsSettings = new MapItemSettings("APRS", true, QColor(255, 255, 0), true, false, 11);
|
||||
aprsSettings->m_extrapolate = 0;
|
||||
m_itemSettings.insert("APRS", aprsSettings);
|
||||
m_itemSettings.insert("StarTracker", new MapItemSettings("StarTracker", true, QColor(230, 230, 230), true, true, 3));
|
||||
m_itemSettings.insert("SatelliteTracker", new MapItemSettings("SatelliteTracker", true, QColor(0, 0, 255), true, false, 0, modelMinPixelSize));
|
||||
m_itemSettings.insert("Beacons", new MapItemSettings("Beacons", true, QColor(255, 0, 0), false, true, 8));
|
||||
m_itemSettings.insert("Radiosonde", new MapItemSettings("Radiosonde", true, QColor(102, 0, 102), true, false, 11, modelMinPixelSize));
|
||||
m_itemSettings.insert("Radio Time Transmitters", new MapItemSettings("Radio Time Transmitters", true, QColor(255, 0, 0), false, true, 8));
|
||||
m_itemSettings.insert("Radar", new MapItemSettings("Radar", true, QColor(255, 0, 0), false, true, 8));
|
||||
m_itemSettings.insert("FT8Demod", new MapItemSettings("FT8Demod", true, QColor(0, 192, 255), true, true, 8));
|
||||
m_itemSettings.insert("HeatMap", new MapItemSettings("HeatMap", true, QColor(102, 40, 220), true, true, 11));
|
||||
|
||||
MapItemSettings *ionosondeItemSettings = new MapItemSettings("Ionosonde Stations", QColor(255, 255, 0), true, 4);
|
||||
m_itemSettings.insert("AM", new MapItemSettings("AM", false, QColor(255, 0, 0), false, true, 10));
|
||||
MapItemSettings *fmSettings = new MapItemSettings("FM", false, QColor(255, 0, 0), false, true, 12);
|
||||
fmSettings->m_filterDistance = 150000;
|
||||
m_itemSettings.insert("FM", fmSettings);
|
||||
MapItemSettings *dabSettings = new MapItemSettings("DAB", false, QColor(255, 0, 0), false, true, 12);
|
||||
dabSettings->m_filterDistance = 75000;
|
||||
m_itemSettings.insert("DAB", dabSettings);
|
||||
|
||||
MapItemSettings *navAidSettings = new MapItemSettings("NavAid", false, QColor(255, 0, 255), false, true, 11);
|
||||
navAidSettings->m_filterDistance = 500000;
|
||||
m_itemSettings.insert("NavAid", navAidSettings);
|
||||
|
||||
m_itemSettings.insert("Airport (Large)", new MapItemSettings("Airport (Large)", false, QColor(255, 0, 255), false, true, 11));
|
||||
m_itemSettings.insert("Airport (Medium)", new MapItemSettings("Airport (Medium)", false, QColor(255, 0, 255), false, true, 11));
|
||||
m_itemSettings.insert("Airport (Small)", new MapItemSettings("Airport (Small)", false, QColor(255, 0, 255), false, true, 13));
|
||||
m_itemSettings.insert("Heliport", new MapItemSettings("Heliport", false, QColor(255, 0, 255), false, true, 12));
|
||||
MapItemSettings *stationSettings = new MapItemSettings("Station", true, QColor(255, 0, 0), false, true, 11);
|
||||
stationSettings->m_extrapolate = 0;
|
||||
stationSettings->m_display3DTrack = false;
|
||||
m_itemSettings.insert("Station", stationSettings);
|
||||
m_itemSettings.insert("VORLocalizer", new MapItemSettings("VORLocalizer", true, QColor(255, 255, 0), false, true, 11));
|
||||
|
||||
MapItemSettings *ionosondeItemSettings = new MapItemSettings("Ionosonde Stations", true, QColor(255, 255, 0), false, true, 4);
|
||||
ionosondeItemSettings->m_display2DIcon = false;
|
||||
m_itemSettings.insert("Ionosonde Stations", ionosondeItemSettings);
|
||||
|
||||
MapItemSettings *stationItemSettings = new MapItemSettings("Station", QColor(255, 0, 0), true, 11);
|
||||
stationItemSettings->m_display2DTrack = false;
|
||||
m_itemSettings.insert("Station", stationItemSettings);
|
||||
m_itemSettings.insert("Airspace (A)", new MapItemSettings("Airspace (A)", false, QColor(255, 0, 0, 0x20), false, false, 7));
|
||||
m_itemSettings.insert("Airspace (B)", new MapItemSettings("Airspace (B)", false, QColor(255, 0, 0, 0x20), false, false, 7));
|
||||
m_itemSettings.insert("Airspace (C)", new MapItemSettings("Airspace (C)", false, QColor(255, 0, 0, 0x20), false, false, 11));
|
||||
m_itemSettings.insert("Airspace (D)", new MapItemSettings("Airspace (D)", false, QColor(255, 0, 0, 0x20), false, false, 11));
|
||||
m_itemSettings.insert("Airspace (E)", new MapItemSettings("Airspace (E)", false, QColor(255, 0, 0, 0x20), false, false, 11));
|
||||
m_itemSettings.insert("Airspace (F)", new MapItemSettings("Airspace (F)", false, QColor(255, 0, 0, 0x20), false, false, 11));
|
||||
m_itemSettings.insert("Airspace (G)", new MapItemSettings("Airspace (G)", false, QColor(255, 0, 0, 0x20), false, false, 11));
|
||||
m_itemSettings.insert("Airspace (FIR)", new MapItemSettings("Airspace (FIR)", false, QColor(255, 0, 0, 0x20), false, false, 7));
|
||||
m_itemSettings.insert("Airspace (CTR)", new MapItemSettings("Airspace (CTR)", false, QColor(255, 0, 0, 0x20), false, false, 11));
|
||||
m_itemSettings.insert("Airspace (RMZ)", new MapItemSettings("Airspace (RMZ)", false, QColor(255, 0, 0, 0x20), false, false, 11));
|
||||
m_itemSettings.insert("Airspace (TMA)", new MapItemSettings("Airspace (TMA)", false, QColor(255, 0, 0, 0x20), false, false, 11));
|
||||
m_itemSettings.insert("Airspace (TMZ)", new MapItemSettings("Airspace (TMZ)", false, QColor(255, 0, 0, 0x20), false, false, 11));
|
||||
m_itemSettings.insert("Airspace (OTH)", new MapItemSettings("Airspace (OTH)", false, QColor(255, 0, 0, 0x20), false, false, 11));
|
||||
m_itemSettings.insert("Airspace (Restricted)", new MapItemSettings("Airspace (Restricted)", false, QColor(255, 0, 0, 0x20), false, false, 11));
|
||||
m_itemSettings.insert("Airspace (Gliding)", new MapItemSettings("Airspace (Gliding)", false, QColor(255, 0, 0, 0x20), false, false, 11));
|
||||
m_itemSettings.insert("Airspace (Danger)", new MapItemSettings("Airspace (Danger)", false, QColor(255, 0, 0, 0x20), false, false, 11));
|
||||
m_itemSettings.insert("Airspace (Prohibited)", new MapItemSettings("Airspace (Prohibited)", false, QColor(255, 0, 0, 0x20), false, false, 11));
|
||||
m_itemSettings.insert("Airspace (Wave)", new MapItemSettings("Airspace (Wave)", false, QColor(255, 0, 0, 0x20), false, false, 11));
|
||||
m_itemSettings.insert("Airspace (Airports)", new MapItemSettings("Airspace (Airports)", false, QColor(0, 0, 255, 0x20), false, false, 11));
|
||||
|
||||
resetToDefaults();
|
||||
}
|
||||
|
||||
@ -104,7 +156,7 @@ void MapSettings::resetToDefaults()
|
||||
m_displaySelectedGroundTracks = true;
|
||||
m_displayAllGroundTracks = true;
|
||||
m_title = "Map";
|
||||
m_displayAllGroundTracks = QColor(225, 25, 99).rgb();
|
||||
m_displayAllGroundTracks = QColor(225, 25, 99).rgba();
|
||||
m_useReverseAPI = false;
|
||||
m_reverseAPIAddress = "127.0.0.1";
|
||||
m_reverseAPIPort = 8888;
|
||||
@ -121,6 +173,7 @@ void MapSettings::resetToDefaults()
|
||||
m_displayMUF = false;
|
||||
m_displayfoF2 = false;
|
||||
m_workspaceIndex = 0;
|
||||
m_checkWXAPIKey = "";
|
||||
}
|
||||
|
||||
QByteArray MapSettings::serialize() const
|
||||
@ -165,6 +218,8 @@ QByteArray MapSettings::serialize() const
|
||||
s.writeBool(35, m_displayMUF);
|
||||
s.writeBool(36, m_displayfoF2);
|
||||
|
||||
s.writeString(46, m_checkWXAPIKey);
|
||||
|
||||
return s.final();
|
||||
}
|
||||
|
||||
@ -184,6 +239,7 @@ bool MapSettings::deserialize(const QByteArray& data)
|
||||
uint32_t utmp;
|
||||
QString strtmp;
|
||||
QByteArray blob;
|
||||
QString string;
|
||||
|
||||
d.readBool(1, &m_displayNames, true);
|
||||
d.readString(2, &m_mapProvider, "osm");
|
||||
@ -195,7 +251,7 @@ bool MapSettings::deserialize(const QByteArray& data)
|
||||
d.readString(3, &m_mapBoxAPIKey, "");
|
||||
d.readString(4, &m_mapBoxStyles, "");
|
||||
d.readString(8, &m_title, "Map");
|
||||
d.readU32(9, &m_rgbColor, QColor(225, 25, 99).rgb());
|
||||
d.readU32(9, &m_rgbColor, QColor(225, 25, 99).rgba());
|
||||
d.readBool(10, &m_useReverseAPI, false);
|
||||
d.readString(11, &m_reverseAPIAddress, "127.0.0.1");
|
||||
d.readU32(12, &utmp, 0);
|
||||
@ -241,6 +297,8 @@ bool MapSettings::deserialize(const QByteArray& data)
|
||||
d.readBool(35, &m_displayMUF, false);
|
||||
d.readBool(36, &m_displayfoF2, false);
|
||||
|
||||
d.readString(46, &m_checkWXAPIKey, "");
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
@ -251,16 +309,20 @@ bool MapSettings::deserialize(const QByteArray& data)
|
||||
}
|
||||
|
||||
MapSettings::MapItemSettings::MapItemSettings(const QString& group,
|
||||
bool enabled,
|
||||
const QColor color,
|
||||
bool display2DTrack,
|
||||
bool display3DPoint,
|
||||
int minZoom,
|
||||
int modelMinPixelSize)
|
||||
{
|
||||
m_group = group;
|
||||
resetToDefaults();
|
||||
m_3DPointColor = color.rgb();
|
||||
m_2DTrackColor = color.darker().rgb();
|
||||
m_3DTrackColor = color.darker().rgb();
|
||||
m_enabled = enabled,
|
||||
m_3DPointColor = color.rgba();
|
||||
m_2DTrackColor = color.darker().rgba();
|
||||
m_3DTrackColor = color.darker().rgba();
|
||||
m_display2DTrack = display2DTrack;
|
||||
m_display3DPoint = display3DPoint;
|
||||
m_2DMinZoom = minZoom;
|
||||
m_3DModelMinPixelSize = modelMinPixelSize;
|
||||
@ -277,16 +339,19 @@ void MapSettings::MapItemSettings::resetToDefaults()
|
||||
m_display2DIcon = true;
|
||||
m_display2DLabel = true;
|
||||
m_display2DTrack = true;
|
||||
m_2DTrackColor = QColor(150, 0, 20).rgb();
|
||||
m_2DTrackColor = QColor(150, 0, 20).rgba();
|
||||
m_2DMinZoom = 1;
|
||||
m_display3DModel = true;
|
||||
m_display3DPoint = false;
|
||||
m_3DPointColor = QColor(225, 0, 0).rgb();
|
||||
m_3DPointColor = QColor(225, 0, 0).rgba();
|
||||
m_display3DLabel = true;
|
||||
m_display3DTrack = true;
|
||||
m_3DTrackColor = QColor(150, 0, 20).rgb();
|
||||
m_3DTrackColor = QColor(150, 0, 20).rgba();
|
||||
m_3DModelMinPixelSize = 0;
|
||||
m_3DLabelScale = 0.5f;
|
||||
m_filterName = "";
|
||||
m_filterDistance = 0;
|
||||
m_extrapolate = 60;
|
||||
}
|
||||
|
||||
QByteArray MapSettings::MapItemSettings::serialize() const
|
||||
@ -308,6 +373,9 @@ QByteArray MapSettings::MapItemSettings::serialize() const
|
||||
s.writeU32(13, m_3DTrackColor);
|
||||
s.writeS32(14, m_3DModelMinPixelSize);
|
||||
s.writeFloat(15, m_3DLabelScale);
|
||||
s.writeString(16, m_filterName);
|
||||
s.writeS32(17, m_filterDistance);
|
||||
s.writeS32(18, m_extrapolate);
|
||||
|
||||
return s.final();
|
||||
}
|
||||
@ -329,16 +397,21 @@ bool MapSettings::MapItemSettings::deserialize(const QByteArray& data)
|
||||
d.readBool(3, &m_display2DIcon, true);
|
||||
d.readBool(4, &m_display2DLabel, true);
|
||||
d.readBool(5, &m_display2DTrack, true);
|
||||
d.readU32(6, &m_2DTrackColor, QColor(150, 0, 0).rgb());
|
||||
d.readU32(6, &m_2DTrackColor, QColor(150, 0, 0).rgba());
|
||||
d.readS32(7, &m_2DMinZoom, 1);
|
||||
d.readBool(8, &m_display3DModel, true);
|
||||
d.readBool(9, &m_display3DLabel, true);
|
||||
d.readBool(10, &m_display3DPoint, true);
|
||||
d.readU32(11, &m_3DPointColor, QColor(255, 0, 0).rgb());
|
||||
d.readU32(11, &m_3DPointColor, QColor(255, 0, 0).rgba());
|
||||
d.readBool(12, &m_display3DTrack, true);
|
||||
d.readU32(13, &m_3DTrackColor, QColor(150, 0, 20).rgb());
|
||||
d.readU32(13, &m_3DTrackColor, QColor(150, 0, 20).rgba());
|
||||
d.readS32(14, &m_3DModelMinPixelSize, 0);
|
||||
d.readFloat(15, &m_3DLabelScale, 0.5f);
|
||||
d.readString(16, &m_filterName, "");
|
||||
d.readS32(17, &m_filterDistance, 0);
|
||||
d.readS32(18, &m_extrapolate, 60);
|
||||
m_filterNameRE.setPattern(m_filterName);
|
||||
m_filterNameRE.optimize();
|
||||
return true;
|
||||
}
|
||||
else
|
||||
@ -567,3 +640,4 @@ QString MapSettings::getDebugString(const QStringList& settingsKeys, bool force)
|
||||
|
||||
return QString(ostr.str().c_str());
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,7 @@
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QHash>
|
||||
#include <QRegularExpression>
|
||||
|
||||
class Serializable;
|
||||
|
||||
@ -32,7 +33,7 @@ struct MapSettings
|
||||
bool m_enabled; // Whether enabled at all on 2D or 3D map
|
||||
bool m_display2DIcon; // Display image 2D map
|
||||
bool m_display2DLabel; // Display label on 2D map
|
||||
bool m_display2DTrack; // Display tracks on 2D map
|
||||
bool m_display2DTrack; // Display tracks (or polygon) on 2D map
|
||||
quint32 m_2DTrackColor;
|
||||
int m_2DMinZoom;
|
||||
bool m_display3DModel; // Draw 3D model for item
|
||||
@ -43,14 +44,26 @@ struct MapSettings
|
||||
quint32 m_3DTrackColor;
|
||||
int m_3DModelMinPixelSize;
|
||||
float m_3DLabelScale;
|
||||
QString m_filterName;
|
||||
QRegularExpression m_filterNameRE;
|
||||
int m_filterDistance; // Filter items > this distance in metres away from My Position. <= 0 don't filter
|
||||
int m_extrapolate; // Extrapolate duration in seconds on 3D map
|
||||
|
||||
MapItemSettings(const QString& group, const QColor color, bool display3DPoint=true, int minZoom=11, int modelMinPixelSize=0);
|
||||
MapItemSettings(const QString& group, bool enabled, const QColor color, bool display2DTrack=true, bool display3DPoint=true, int minZoom=11, int modelMinPixelSize=0);
|
||||
MapItemSettings(const QByteArray& data);
|
||||
void resetToDefaults();
|
||||
QByteArray serialize() const;
|
||||
bool deserialize(const QByteArray& data);
|
||||
};
|
||||
|
||||
struct MapPolygonSettings {
|
||||
QString m_group;
|
||||
bool m_enabled;
|
||||
quint32 m_2DColor;
|
||||
int m_2DMinZoom;
|
||||
quint32 m_3DColor;
|
||||
};
|
||||
|
||||
struct AvailableChannelOrFeature
|
||||
{
|
||||
QString m_kind; //!< "R" for channel, "F" for feature
|
||||
@ -107,6 +120,8 @@ struct MapSettings
|
||||
bool m_displayMUF; // Plot MUF contours
|
||||
bool m_displayfoF2; // Plot foF2 contours
|
||||
|
||||
QString m_checkWXAPIKey; //!< checkwxapi.com API key
|
||||
|
||||
// Per source settings
|
||||
QHash<QString, MapItemSettings *> m_itemSettings;
|
||||
|
||||
@ -127,4 +142,7 @@ struct MapSettings
|
||||
static const QStringList m_mapProviders;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(MapSettings::MapItemSettings *);
|
||||
|
||||
#endif // INCLUDE_FEATURE_MAPSETTINGS_H_
|
||||
|
||||
|
@ -109,6 +109,7 @@ MapSettingsDialog::MapSettingsDialog(MapSettings *settings, QWidget* parent) :
|
||||
QDialog(parent),
|
||||
m_settings(settings),
|
||||
m_downloadDialog(this),
|
||||
m_progressDialog(nullptr),
|
||||
ui(new Ui::MapSettingsDialog)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
@ -117,6 +118,7 @@ MapSettingsDialog::MapSettingsDialog(MapSettings *settings, QWidget* parent) :
|
||||
ui->maptilerAPIKey->setText(settings->m_maptilerAPIKey);
|
||||
ui->mapBoxAPIKey->setText(settings->m_mapBoxAPIKey);
|
||||
ui->cesiumIonAPIKey->setText(settings->m_cesiumIonAPIKey);
|
||||
ui->checkWXAPIKey->setText(settings->m_checkWXAPIKey);
|
||||
ui->osmURL->setText(settings->m_osmURL);
|
||||
ui->mapBoxStyles->setText(settings->m_mapBoxStyles);
|
||||
ui->map2DEnabled->setChecked(m_settings->m_map2DEnabled);
|
||||
@ -129,10 +131,10 @@ MapSettingsDialog::MapSettingsDialog(MapSettings *settings, QWidget* parent) :
|
||||
|
||||
// Sort groups in table alphabetically
|
||||
QList<MapSettings::MapItemSettings *> itemSettings = m_settings->m_itemSettings.values();
|
||||
std::sort(itemSettings.begin(), itemSettings.end(),
|
||||
[](const MapSettings::MapItemSettings* a, const MapSettings::MapItemSettings* b) -> bool {
|
||||
return a->m_group < b->m_group;
|
||||
});
|
||||
std::sort(itemSettings.begin(), itemSettings.end(),
|
||||
[](const MapSettings::MapItemSettings* a, const MapSettings::MapItemSettings* b) -> bool {
|
||||
return a->m_group < b->m_group;
|
||||
});
|
||||
QListIterator<MapSettings::MapItemSettings *> itr(itemSettings);
|
||||
while (itr.hasNext())
|
||||
{
|
||||
@ -163,6 +165,14 @@ MapSettingsDialog::MapSettingsDialog(MapSettings *settings, QWidget* parent) :
|
||||
item->setCheckState(itemSettings->m_display3DLabel ? Qt::Checked : Qt::Unchecked);
|
||||
ui->mapItemSettings->setItem(row, COL_3D_LABEL, item);
|
||||
|
||||
item = new QTableWidgetItem(itemSettings->m_filterName);
|
||||
ui->mapItemSettings->setItem(row, COL_FILTER_NAME, item);
|
||||
item = new QTableWidgetItem();
|
||||
if (itemSettings->m_filterDistance > 0) {
|
||||
item->setText(QString::number(itemSettings->m_filterDistance / 1000));
|
||||
}
|
||||
ui->mapItemSettings->setItem(row, COL_FILTER_DISTANCE, item);
|
||||
|
||||
MapItemSettingsGUI *gui = new MapItemSettingsGUI(ui->mapItemSettings, row, itemSettings);
|
||||
m_mapItemSettingsGUIs.append(gui);
|
||||
}
|
||||
@ -172,6 +182,15 @@ MapSettingsDialog::MapSettingsDialog(MapSettings *settings, QWidget* parent) :
|
||||
on_map3DEnabled_clicked(m_settings->m_map3DEnabled);
|
||||
|
||||
connect(&m_dlm, &HttpDownloadManagerGUI::downloadComplete, this, &MapSettingsDialog::downloadComplete);
|
||||
connect(&m_openAIP, &OpenAIP::downloadingURL, this, &MapSettingsDialog::downloadingURL);
|
||||
connect(&m_openAIP, &OpenAIP::downloadError, this, &MapSettingsDialog::downloadError);
|
||||
connect(&m_openAIP, &OpenAIP::downloadAirspaceFinished, this, &MapSettingsDialog::downloadAirspaceFinished);
|
||||
connect(&m_openAIP, &OpenAIP::downloadNavAidsFinished, this, &MapSettingsDialog::downloadNavAidsFinished);
|
||||
connect(&m_ourAirportsDB, &OurAirportsDB::downloadingURL, this, &MapSettingsDialog::downloadingURL);
|
||||
connect(&m_ourAirportsDB, &OurAirportsDB::downloadProgress, this, &MapSettingsDialog::downloadProgress);
|
||||
connect(&m_ourAirportsDB, &OurAirportsDB::downloadError, this, &MapSettingsDialog::downloadError);
|
||||
connect(&m_ourAirportsDB, &OurAirportsDB::downloadAirportInformationFinished, this, &MapSettingsDialog::downloadAirportInformationFinished);
|
||||
|
||||
#ifndef QT_WEBENGINE_FOUND
|
||||
ui->map3DSettings->setVisible(false);
|
||||
ui->downloadModels->setVisible(false);
|
||||
@ -257,6 +276,16 @@ void MapSettingsDialog::accept()
|
||||
itemSettings->m_3DTrackColor = gui->m_track3D.m_color;
|
||||
itemSettings->m_3DModelMinPixelSize = gui->m_minPixels->value();
|
||||
itemSettings->m_3DLabelScale = gui->m_labelScale->value();
|
||||
itemSettings->m_filterName = ui->mapItemSettings->item(row, COL_FILTER_NAME)->text();
|
||||
itemSettings->m_filterNameRE.setPattern(itemSettings->m_filterName);
|
||||
itemSettings->m_filterNameRE.optimize();
|
||||
bool ok;
|
||||
int filterDistance = ui->mapItemSettings->item(row, COL_FILTER_DISTANCE)->text().toInt(&ok);
|
||||
if (ok && filterDistance > 0) {
|
||||
itemSettings->m_filterDistance = filterDistance * 1000;
|
||||
} else {
|
||||
itemSettings->m_filterDistance = 0;
|
||||
}
|
||||
}
|
||||
|
||||
QDialog::accept();
|
||||
@ -307,6 +336,7 @@ void MapSettingsDialog::on_map3DEnabled_clicked(bool checked)
|
||||
ui->buildings->setEnabled(checked);
|
||||
ui->sunLightEnabled->setEnabled(checked);
|
||||
ui->eciCamera->setEnabled(checked);
|
||||
ui->antiAliasing->setEnabled(checked);
|
||||
}
|
||||
|
||||
// Models have individual licensing. See LICENSE on github
|
||||
@ -423,3 +453,95 @@ void MapSettingsDialog::downloadComplete(const QString &filename, bool success,
|
||||
QMessageBox::warning(this, "Download failed", QString("Failed to download %1 to %2\n%3").arg(url).arg(filename).arg(errorMessage));
|
||||
}
|
||||
}
|
||||
|
||||
void MapSettingsDialog::on_getAirportDB_clicked()
|
||||
{
|
||||
// Don't try to download while already in progress
|
||||
if (m_progressDialog == nullptr)
|
||||
{
|
||||
m_progressDialog = new QProgressDialog(this);
|
||||
m_progressDialog->setCancelButton(nullptr);
|
||||
m_progressDialog->setWindowFlag(Qt::WindowCloseButtonHint, false);
|
||||
m_ourAirportsDB.downloadAirportInformation();
|
||||
}
|
||||
}
|
||||
|
||||
void MapSettingsDialog::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_progressDialog->setWindowFlag(Qt::WindowCloseButtonHint, false);
|
||||
m_openAIP.downloadAirspaces();
|
||||
}
|
||||
}
|
||||
|
||||
void MapSettingsDialog::downloadingURL(const QString& url)
|
||||
{
|
||||
if (m_progressDialog)
|
||||
{
|
||||
m_progressDialog->setLabelText(QString("Downloading %1.").arg(url));
|
||||
m_progressDialog->setValue(m_progressDialog->value() + 1);
|
||||
}
|
||||
}
|
||||
|
||||
void MapSettingsDialog::downloadProgress(qint64 bytesRead, qint64 totalBytes)
|
||||
{
|
||||
if (m_progressDialog)
|
||||
{
|
||||
m_progressDialog->setMaximum(totalBytes);
|
||||
m_progressDialog->setValue(bytesRead);
|
||||
}
|
||||
}
|
||||
|
||||
void MapSettingsDialog::downloadError(const QString& error)
|
||||
{
|
||||
QMessageBox::critical(this, "Map", error);
|
||||
if (m_progressDialog)
|
||||
{
|
||||
m_progressDialog->close();
|
||||
delete m_progressDialog;
|
||||
m_progressDialog = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void MapSettingsDialog::downloadAirspaceFinished()
|
||||
{
|
||||
if (m_progressDialog) {
|
||||
m_progressDialog->setLabelText("Reading airspaces.");
|
||||
}
|
||||
emit airspacesUpdated();
|
||||
m_openAIP.downloadNavAids();
|
||||
}
|
||||
|
||||
void MapSettingsDialog::downloadNavAidsFinished()
|
||||
{
|
||||
if (m_progressDialog) {
|
||||
m_progressDialog->setLabelText("Reading NAVAIDs.");
|
||||
}
|
||||
emit navAidsUpdated();
|
||||
if (m_progressDialog)
|
||||
{
|
||||
m_progressDialog->close();
|
||||
delete m_progressDialog;
|
||||
m_progressDialog = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void MapSettingsDialog::downloadAirportInformationFinished()
|
||||
{
|
||||
if (m_progressDialog) {
|
||||
m_progressDialog->setLabelText("Reading airports.");
|
||||
}
|
||||
emit airportsUpdated();
|
||||
if (m_progressDialog)
|
||||
{
|
||||
m_progressDialog->close();
|
||||
delete m_progressDialog;
|
||||
m_progressDialog = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,8 +21,11 @@
|
||||
#include <QSpinBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QMessageBox>
|
||||
#include <QProgressDialog>
|
||||
|
||||
#include "gui/httpdownloadmanagergui.h"
|
||||
#include "util/openaip.h"
|
||||
#include "util/ourairportsdb.h"
|
||||
|
||||
#include "ui_mapsettingsdialog.h"
|
||||
#include "mapsettings.h"
|
||||
@ -78,7 +81,9 @@ public:
|
||||
COL_3D_LABEL,
|
||||
COL_3D_POINT,
|
||||
COL_3D_TRACK,
|
||||
COL_3D_LABEL_SCALE
|
||||
COL_3D_LABEL_SCALE,
|
||||
COL_FILTER_NAME,
|
||||
COL_FILTER_DISTANCE
|
||||
};
|
||||
|
||||
public:
|
||||
@ -92,6 +97,9 @@ private:
|
||||
HttpDownloadManagerGUI m_dlm;
|
||||
int m_fileIdx;
|
||||
QMessageBox m_downloadDialog;
|
||||
QProgressDialog *m_progressDialog;
|
||||
OpenAIP m_openAIP;
|
||||
OurAirportsDB m_ourAirportsDB;
|
||||
|
||||
void unzip(const QString &filename);
|
||||
|
||||
@ -100,7 +108,20 @@ private slots:
|
||||
void on_map2DEnabled_clicked(bool checked=false);
|
||||
void on_map3DEnabled_clicked(bool checked=false);
|
||||
void on_downloadModels_clicked();
|
||||
void on_getAirportDB_clicked();
|
||||
void on_getAirspacesDB_clicked();
|
||||
void downloadComplete(const QString &filename, bool success, const QString &url, const QString &errorMessage);
|
||||
void downloadingURL(const QString& url);
|
||||
void downloadProgress(qint64 bytesRead, qint64 totalBytes);
|
||||
void downloadError(const QString& error);
|
||||
void downloadAirspaceFinished();
|
||||
void downloadNavAidsFinished();
|
||||
void downloadAirportInformationFinished();
|
||||
|
||||
signals:
|
||||
void navAidsUpdated();
|
||||
void airspacesUpdated();
|
||||
void airportsUpdated();
|
||||
|
||||
private:
|
||||
Ui::MapSettingsDialog* ui;
|
||||
|
@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1016</width>
|
||||
<height>720</height>
|
||||
<width>1267</width>
|
||||
<height>648</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="font">
|
||||
@ -35,68 +35,6 @@
|
||||
<string>Maps</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<widget class="QTableWidget" name="mapItemSettings">
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::NoSelection</enum>
|
||||
</property>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Enabled</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>2D Icon</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>2D Label</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>2D Min Zoom</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>2D Track</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>3D Model</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>3D Min Pixels</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>3D Label</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>3D Point</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>3D Track</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>3D Label Scale</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="map2DSettings">
|
||||
<property name="title">
|
||||
@ -346,6 +284,19 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
@ -358,6 +309,34 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="getAirportDB">
|
||||
<property name="toolTip">
|
||||
<string>Download the latest OurAirports airport databases (10MB)</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="icons.qrc">
|
||||
<normaloff>:/map/icons/controltower.png</normaloff>:/map/icons/controltower.png</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="getAirspacesDB">
|
||||
<property name="toolTip">
|
||||
<string>Download airspaces and NAVAIDs from OpenAIP (40MB)</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="icons.qrc">
|
||||
<normaloff>:/map/icons/vor.png</normaloff>:/map/icons/vor.png</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
@ -375,6 +354,115 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="mapItemsTab">
|
||||
<attribute name="title">
|
||||
<string>Map Items</string>
|
||||
</attribute>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QTableWidget" name="mapItemSettings">
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::NoSelection</enum>
|
||||
</property>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Enabled</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Whether items of this type will be displayed on the maps</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>2D Icon</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Whether to display icon on 2D map</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>2D Label</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Whether to display label on 2D map</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>2D Min Zoom</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>2D Colour</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Track colour or 2D map</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>3D Model</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Whether to display model on 3D map</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>3D Min Pixels</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>3D Label</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Whether to display label on 3D map</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>3D Point</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Whether to display a point on 3D map</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>3D Colour</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>3D Label Scale</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Scale factor for label on 3D map</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Filter Name</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Regular expression to filter by name (Only items with names matching the pattern will be displayed)</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Filter Distance (km)</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Filter objects further than this distance in km away from My Position</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="apiKeysTab">
|
||||
<attribute name="title">
|
||||
<string>API Keys</string>
|
||||
@ -466,6 +554,20 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="checkWXAPIKeyLabel">
|
||||
<property name="text">
|
||||
<string>CheckWX API key</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QLineEdit" name="checkWXAPIKey">
|
||||
<property name="toolTip">
|
||||
<string>checkwxapi.com API key for accessing airport weather (METARs)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
@ -489,8 +591,6 @@
|
||||
</layout>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>tabWidget</tabstop>
|
||||
<tabstop>mapItemSettings</tabstop>
|
||||
<tabstop>map2DEnabled</tabstop>
|
||||
<tabstop>mapProvider</tabstop>
|
||||
<tabstop>osmURL</tabstop>
|
||||
@ -507,7 +607,10 @@
|
||||
<tabstop>mapBoxAPIKey</tabstop>
|
||||
<tabstop>cesiumIonAPIKey</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<resources>
|
||||
<include location="../../../sdrgui/resources/res.qrc"/>
|
||||
<include location="icons.qrc"/>
|
||||
</resources>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
|
@ -94,6 +94,25 @@ private slots:
|
||||
\"DataCopyRight\" : \"<a href='http://maptiler.com'>Maptiler</a>\"\
|
||||
}").arg(hiresURL).arg(m_maptilerAPIKey);
|
||||
}
|
||||
else if (tokens[1].contains("transit"))
|
||||
{
|
||||
QStringList map({"/night-transit", "/night-transit-hires", "/transit", "/transit-hires"});
|
||||
QStringList mapId({"thf-nighttransit", "thf-nighttransit-hires", "thf-transit", "thf-transit-hires"});
|
||||
QStringList mapUrl({"dark_nolabels", "dark_nolabels", "light_nolabels", "light_nolabels"});
|
||||
|
||||
// Use CartoDB maps without labels for aviation maps
|
||||
int idx = map.indexOf(tokens[1]);
|
||||
xml = QString("\
|
||||
{\
|
||||
\"UrlTemplate\" : \"http://1.basemaps.cartocdn.com/%2/%z/%x/%y.png%1\",\
|
||||
\"ImageFormat\" : \"png\",\
|
||||
\"QImageFormat\" : \"Indexed8\",\
|
||||
\"ID\" : \"%3\",\
|
||||
\"MaximumZoomLevel\" : 20,\
|
||||
\"MapCopyRight\" : \"<a href='https://carto.com'>CartoDB</a>\",\
|
||||
\"DataCopyRight\" : \"<a href='https://carto.com'>CartoDB</a>\"\
|
||||
}").arg(hiresURL).arg(mapUrl[idx]).arg(mapId[idx]);
|
||||
}
|
||||
else
|
||||
{
|
||||
int idx = map.indexOf(tokens[1]);
|
||||
|
@ -11,10 +11,13 @@ On top of this, it can plot data from other plugins, such as:
|
||||
* Satellites from the Satellite Tracker,
|
||||
* Weather imagery from APT Demodulator,
|
||||
* The Sun, Moon and Stars from the Star Tracker,
|
||||
* Weather ballons from the RadioSonde feature.
|
||||
* Weather ballons from the RadioSonde feature,
|
||||
* Radials and estimated position from the VOR localizer feature.
|
||||
|
||||
As well as other other data sources:
|
||||
As well as other data sources:
|
||||
|
||||
* AM, FM and DAB transmitters in the UK,
|
||||
* Airports, NavAids and airspaces,
|
||||
* Beacons based on the IARU Region 1 beacon database and International Beacon Project,
|
||||
* Radio time transmitters,
|
||||
* GRAVES radar,
|
||||
|
@ -200,7 +200,8 @@ RadiosondeGUI::RadiosondeGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, F
|
||||
|
||||
RadiosondeGUI::~RadiosondeGUI()
|
||||
{
|
||||
qDeleteAll(m_radiosondes);
|
||||
// Remove from map and free memory
|
||||
on_deleteAll_clicked();
|
||||
delete ui;
|
||||
}
|
||||
|
||||
@ -392,8 +393,11 @@ void RadiosondeGUI::sendToMap(const QString &name, const QString &label,
|
||||
swgMapItem->setAltitude(altitude);
|
||||
swgMapItem->setAltitudeReference(0); // ABSOLUTE
|
||||
|
||||
if (positionDateTime.isValid()) {
|
||||
if (positionDateTime.isValid())
|
||||
{
|
||||
swgMapItem->setPositionDateTime(new QString(positionDateTime.toString(Qt::ISODateWithMs)));
|
||||
swgMapItem->setOrientationDateTime(new QString(positionDateTime.toString(Qt::ISODateWithMs)));
|
||||
swgMapItem->setAvailableUntil(new QString(positionDateTime.addSecs(1*60*60).toString(Qt::ISODateWithMs)));
|
||||
}
|
||||
|
||||
swgMapItem->setImageRotation(heading);
|
||||
@ -877,8 +881,8 @@ void RadiosondeGUI::on_deleteAll_clicked()
|
||||
0.0f);
|
||||
// Remove from table
|
||||
ui->radiosondes->removeRow(row);
|
||||
// Remove from hash
|
||||
m_radiosondes.remove(serial);
|
||||
// Remove from hash and free memory
|
||||
delete m_radiosondes.take(serial);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -373,9 +373,9 @@ void SatelliteTrackerGUI::displaySettings()
|
||||
ui->target->blockSignals(false);
|
||||
ui->target->setCurrentIndex(ui->target->findText(m_settings.m_target));
|
||||
|
||||
ui->dateTime->setDateTime(QDateTime::fromString(m_settings.m_dateTime, Qt::ISODateWithMs));
|
||||
ui->dateTimeSelect->setCurrentIndex((int)m_settings.m_dateTimeSelect);
|
||||
ui->dateTime->setVisible(m_settings.m_dateTimeSelect == SatelliteTrackerSettings::CUSTOM);
|
||||
ui->dateTime->setDateTime(QDateTime::fromString(m_settings.m_dateTime, Qt::ISODateWithMs));
|
||||
ui->autoTarget->setChecked(m_settings.m_autoTarget);
|
||||
ui->darkTheme->setChecked(m_settings.m_chartsDarkTheme);
|
||||
ui->satTable->horizontalHeader()->setSortIndicator(m_settings.m_columnSort, m_settings.m_columnSortOrder);
|
||||
|
@ -68,6 +68,16 @@ SatelliteTrackerWorker::~SatelliteTrackerWorker()
|
||||
qDebug() << "SatelliteTrackerWorker::~SatelliteTrackerWorker";
|
||||
stopWork();
|
||||
m_inputMessageQueue.clear();
|
||||
// Remove satellites from Map
|
||||
QHashIterator<QString, SatWorkerState *> itr(m_workerState);
|
||||
while (itr.hasNext())
|
||||
{
|
||||
itr.next();
|
||||
if (m_settings.m_drawOnMap) {
|
||||
removeFromMap(itr.key());
|
||||
}
|
||||
}
|
||||
qDeleteAll(m_workerState);
|
||||
}
|
||||
|
||||
void SatelliteTrackerWorker::startWork()
|
||||
@ -187,7 +197,13 @@ void SatelliteTrackerWorker::applySettings(const SatelliteTrackerSettings& setti
|
||||
{
|
||||
itr.next();
|
||||
if (settings.m_satellites.indexOf(itr.key()) == -1)
|
||||
{
|
||||
if (m_settings.m_drawOnMap) {
|
||||
removeFromMap(itr.key());
|
||||
}
|
||||
delete itr.value();
|
||||
itr.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Add new satellites
|
||||
|
@ -76,9 +76,17 @@ void StarTrackerWorker::startWork()
|
||||
void StarTrackerWorker::stopWork()
|
||||
{
|
||||
QMutexLocker mutexLocker(&m_mutex);
|
||||
disconnect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()));
|
||||
restartServer(false, 0);
|
||||
m_pollTimer.stop();
|
||||
disconnect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()));
|
||||
|
||||
if (m_settings.m_drawSunOnMap)
|
||||
removeFromMap("Sun");
|
||||
if (m_settings.m_drawMoonOnMap)
|
||||
removeFromMap("Moon");
|
||||
if (m_settings.m_drawStarOnMap && (m_settings.m_target != "Sun") && (m_settings.m_target != "Moon"))
|
||||
removeFromMap("Star");
|
||||
|
||||
restartServer(false, 0);
|
||||
}
|
||||
|
||||
void StarTrackerWorker::handleInputMessages()
|
||||
|
@ -50,6 +50,8 @@
|
||||
#include "vorlocalizersettings.h"
|
||||
#include "vorlocalizergui.h"
|
||||
|
||||
#include "SWGMapItem.h"
|
||||
|
||||
// Lats and longs in decimal degrees. Distance in metres. Bearing in degrees.
|
||||
// https://www.movable-type.co.uk/scripts/latlong.html
|
||||
static void calcRadialEndPoint(float startLatitude, float startLongitude, float distance, float bearing, float &endLatitude, float &endLongitude)
|
||||
@ -351,6 +353,18 @@ bool VORModel::findIntersection(float &lat, float &lon)
|
||||
return false;
|
||||
}
|
||||
|
||||
QString VORModel::getRadials() const
|
||||
{
|
||||
QStringList text;
|
||||
for (int i = 0; i < m_vors.size(); i++)
|
||||
{
|
||||
if (m_radials[i] >= 0) {
|
||||
text.append(QString("%1: %2%3").arg(m_vors[i]->m_name).arg(std::round(m_radials[i])).arg(QChar(0xb0)));
|
||||
}
|
||||
}
|
||||
return text.join("\n");
|
||||
}
|
||||
|
||||
void VORLocalizerGUI::resizeTable()
|
||||
{
|
||||
// Fill table with a row of dummy data that will size the columns nicely
|
||||
@ -462,6 +476,8 @@ void VORLocalizerGUI::selectVOR(VORGUI *vorGUI, bool selected)
|
||||
}
|
||||
else
|
||||
{
|
||||
QString radialName = QString("VOR Radial %1").arg(vorGUI->m_navAid->m_name);
|
||||
|
||||
VORLocalizer::MsgRemoveVORChannel *msg = VORLocalizer::MsgRemoveVORChannel::create(navId);
|
||||
m_vorLocalizer->getInputMessageQueue()->push(msg);
|
||||
|
||||
@ -470,6 +486,10 @@ void VORLocalizerGUI::selectVOR(VORGUI *vorGUI, bool selected)
|
||||
// Remove from settings to remove corresponding demodulator
|
||||
m_settings.m_subChannelSettings.remove(navId);
|
||||
|
||||
// Remove radial from Map feature
|
||||
m_mapFeatureRadialNames.removeOne(radialName);
|
||||
clearFromMapFeature(radialName, 3);
|
||||
|
||||
applySettings();
|
||||
}
|
||||
}
|
||||
@ -479,7 +499,7 @@ void VORLocalizerGUI::updateVORs()
|
||||
m_vorModel.removeAllVORs();
|
||||
AzEl azEl = m_azEl;
|
||||
|
||||
for (auto vor : m_vors)
|
||||
for (const auto vor : *m_vors)
|
||||
{
|
||||
if (vor->m_type.contains("VOR")) // Exclude DMEs
|
||||
{
|
||||
@ -534,6 +554,130 @@ bool VORLocalizerGUI::deserialize(const QByteArray& data)
|
||||
}
|
||||
}
|
||||
|
||||
void VORLocalizerGUI::clearFromMapFeature(const QString& name, int type)
|
||||
{
|
||||
QList<ObjectPipe*> mapPipes;
|
||||
MainCore::instance()->getMessagePipes().getMessagePipes(m_vorLocalizer, "mapitems", mapPipes);
|
||||
for (const auto& pipe : mapPipes)
|
||||
{
|
||||
MessageQueue *messageQueue = qobject_cast<MessageQueue*>(pipe->m_element);
|
||||
SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem();
|
||||
swgMapItem->setName(new QString(name));
|
||||
swgMapItem->setImage(new QString(""));
|
||||
swgMapItem->setType(type);
|
||||
MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_vorLocalizer, swgMapItem);
|
||||
messageQueue->push(msg);
|
||||
}
|
||||
}
|
||||
|
||||
void VORLocalizerGUI::sendPositionToMapFeature(float lat, float lon)
|
||||
{
|
||||
// Send to Map feature
|
||||
QList<ObjectPipe*> mapPipes;
|
||||
MainCore::instance()->getMessagePipes().getMessagePipes(m_vorLocalizer, "mapitems", mapPipes);
|
||||
|
||||
if (mapPipes.size() > 0)
|
||||
{
|
||||
QString name = MainCore::instance()->getSettings().getStationName();
|
||||
if (name != m_mapFeaturePositionName)
|
||||
{
|
||||
clearFromMapFeature(m_mapFeaturePositionName, 0);
|
||||
m_mapFeaturePositionName = name;
|
||||
}
|
||||
|
||||
QString details = QString("%1\nEstimated position based on VORs\n").arg(name);
|
||||
details.append(m_vorModel.getRadials());
|
||||
|
||||
for (const auto& pipe : mapPipes)
|
||||
{
|
||||
MessageQueue *messageQueue = qobject_cast<MessageQueue*>(pipe->m_element);
|
||||
SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem();
|
||||
|
||||
swgMapItem->setName(new QString(name));
|
||||
swgMapItem->setLatitude(lat);
|
||||
swgMapItem->setLongitude(lon);
|
||||
swgMapItem->setAltitude(0);
|
||||
swgMapItem->setImage(new QString("antenna.png"));
|
||||
swgMapItem->setImageRotation(0);
|
||||
swgMapItem->setText(new QString(details));
|
||||
swgMapItem->setModel(new QString("antenna.glb"));
|
||||
swgMapItem->setFixedPosition(false);
|
||||
swgMapItem->setLabel(new QString(name));
|
||||
swgMapItem->setLabelAltitudeOffset(4.5);
|
||||
swgMapItem->setAltitudeReference(1);
|
||||
swgMapItem->setType(0);
|
||||
|
||||
MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_vorLocalizer, swgMapItem);
|
||||
messageQueue->push(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VORLocalizerGUI::sendRadialToMapFeature(VORGUI *vorGUI, Real radial)
|
||||
{
|
||||
// Send to Map feature
|
||||
QList<ObjectPipe*> mapPipes;
|
||||
MainCore::instance()->getMessagePipes().getMessagePipes(m_vorLocalizer, "mapitems", mapPipes);
|
||||
|
||||
if (mapPipes.size() > 0)
|
||||
{
|
||||
float endLat, endLong;
|
||||
float bearing;
|
||||
|
||||
if (m_settings.m_magDecAdjust && !vorGUI->m_navAid->m_alignedTrueNorth) {
|
||||
bearing = radial - vorGUI->m_navAid->m_magneticDeclination;
|
||||
} else {
|
||||
bearing = radial;
|
||||
}
|
||||
|
||||
calcRadialEndPoint(vorGUI->m_navAid->m_latitude, vorGUI->m_navAid->m_longitude, vorGUI->m_navAid->getRangeMetres(), bearing, endLat, endLong);
|
||||
|
||||
QString name = QString("VOR Radial %1").arg(vorGUI->m_navAid->m_name);
|
||||
QString details = QString("%1%2").arg(std::round(bearing)).arg(QChar(0x00b0));
|
||||
|
||||
if (!m_mapFeatureRadialNames.contains(name)) {
|
||||
m_mapFeatureRadialNames.append(name);
|
||||
}
|
||||
|
||||
for (const auto& pipe : mapPipes)
|
||||
{
|
||||
MessageQueue *messageQueue = qobject_cast<MessageQueue*>(pipe->m_element);
|
||||
SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem();
|
||||
|
||||
swgMapItem->setName(new QString(name));
|
||||
swgMapItem->setLatitude(vorGUI->m_navAid->m_latitude);
|
||||
swgMapItem->setLongitude(vorGUI->m_navAid->m_longitude);
|
||||
swgMapItem->setAltitude(Units::feetToMetres(vorGUI->m_navAid->m_elevation));
|
||||
QString image = QString("none");
|
||||
swgMapItem->setImage(new QString(image));
|
||||
swgMapItem->setImageRotation(0);
|
||||
swgMapItem->setText(new QString(details)); // Not used - label is used instead for now
|
||||
//swgMapItem->setFixedPosition(true);
|
||||
swgMapItem->setLabel(new QString(details));
|
||||
swgMapItem->setAltitudeReference(0);
|
||||
QList<SWGSDRangel::SWGMapCoordinate *> *coords = new QList<SWGSDRangel::SWGMapCoordinate *>();
|
||||
|
||||
SWGSDRangel::SWGMapCoordinate* c = new SWGSDRangel::SWGMapCoordinate();
|
||||
c->setLatitude(vorGUI->m_navAid->m_latitude);
|
||||
c->setLongitude(vorGUI->m_navAid->m_longitude); // Centre of VOR
|
||||
c->setAltitude(Units::feetToMetres(vorGUI->m_navAid->m_elevation));
|
||||
coords->append(c);
|
||||
|
||||
c = new SWGSDRangel::SWGMapCoordinate();
|
||||
c->setLatitude(endLat);
|
||||
c->setLongitude(endLong);
|
||||
c->setAltitude(Units::feetToMetres(vorGUI->m_navAid->m_elevation));
|
||||
coords->append(c);
|
||||
|
||||
swgMapItem->setCoordinates(coords);
|
||||
swgMapItem->setType(3);
|
||||
|
||||
MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_vorLocalizer, swgMapItem);
|
||||
messageQueue->push(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool VORLocalizerGUI::handleMessage(const Message& message)
|
||||
{
|
||||
if (VORLocalizer::MsgConfigureVORLocalizer::match(message))
|
||||
@ -598,6 +742,31 @@ bool VORLocalizerGUI::handleMessage(const Message& message)
|
||||
|
||||
// Update radial on map
|
||||
m_vorModel.setRadial(subChannelId, validRadial, report.getRadial());
|
||||
|
||||
// Send to map feature as well
|
||||
sendRadialToMapFeature(vorGUI, report.getRadial());
|
||||
|
||||
// Try to determine position, based on intersection of two radials
|
||||
float lat, lon;
|
||||
|
||||
if (m_vorModel.findIntersection(lat, lon))
|
||||
{
|
||||
// Move antenna icon to estimated position
|
||||
QQuickItem *item = ui->map->rootObject();
|
||||
QObject *stationObject = item->findChild<QObject*>("station");
|
||||
|
||||
if (stationObject != NULL)
|
||||
{
|
||||
QGeoCoordinate coords = stationObject->property("coordinate").value<QGeoCoordinate>();
|
||||
coords.setLatitude(lat);
|
||||
coords.setLongitude(lon);
|
||||
stationObject->setProperty("coordinate", QVariant::fromValue(coords));
|
||||
stationObject->setProperty("stationName", QVariant::fromValue(MainCore::instance()->getSettings().getStationName()));
|
||||
}
|
||||
|
||||
// Send estimated position to Map Feature as well
|
||||
sendPositionToMapFeature(lat, lon);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -738,7 +907,7 @@ void VORLocalizerGUI::on_getOpenAIPVORDB_clicked()
|
||||
|
||||
void VORLocalizerGUI::readNavAids()
|
||||
{
|
||||
m_vors = OpenAIP::readNavAids();
|
||||
m_vors = OpenAIP::getNavAids();
|
||||
updateVORs();
|
||||
}
|
||||
|
||||
@ -1052,10 +1221,13 @@ VORLocalizerGUI::VORLocalizerGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISe
|
||||
|
||||
VORLocalizerGUI::~VORLocalizerGUI()
|
||||
{
|
||||
clearFromMapFeature(m_mapFeaturePositionName, 0);
|
||||
for (auto const &radialName : m_mapFeatureRadialNames) {
|
||||
clearFromMapFeature(radialName, 3);
|
||||
}
|
||||
disconnect(&m_redrawMapTimer, &QTimer::timeout, this, &VORLocalizerGUI::redrawMap);
|
||||
m_redrawMapTimer.stop();
|
||||
delete ui;
|
||||
qDeleteAll(m_vors);
|
||||
}
|
||||
|
||||
void VORLocalizerGUI::setWorkspaceIndex(int index)
|
||||
@ -1145,27 +1317,8 @@ void VORLocalizerGUI::updateStatus()
|
||||
|
||||
void VORLocalizerGUI::tick()
|
||||
{
|
||||
// Try to determine position, based on intersection of two radials - every second
|
||||
if (++m_tickCount == 20)
|
||||
{
|
||||
float lat, lon;
|
||||
|
||||
if (m_vorModel.findIntersection(lat, lon))
|
||||
{
|
||||
// Move antenna icon to estimated position
|
||||
QQuickItem *item = ui->map->rootObject();
|
||||
QObject *stationObject = item->findChild<QObject*>("station");
|
||||
|
||||
if (stationObject != NULL)
|
||||
{
|
||||
QGeoCoordinate coords = stationObject->property("coordinate").value<QGeoCoordinate>();
|
||||
coords.setLatitude(lat);
|
||||
coords.setLongitude(lon);
|
||||
stationObject->setProperty("coordinate", QVariant::fromValue(coords));
|
||||
stationObject->setProperty("stationName", QVariant::fromValue(MainCore::instance()->getSettings().getStationName()));
|
||||
}
|
||||
}
|
||||
|
||||
m_rrSecondsCount++;
|
||||
ui->rrTurnTimeProgress->setMaximum(m_settings.m_rrTime);
|
||||
ui->rrTurnTimeProgress->setValue(m_rrSecondsCount <= m_settings.m_rrTime ? m_rrSecondsCount : m_settings.m_rrTime);
|
||||
@ -1286,3 +1439,4 @@ void VORLocalizerGUI::makeUIConnections()
|
||||
QObject::connect(ui->rrTime, &QDial::valueChanged, this, &VORLocalizerGUI::on_rrTime_valueChanged);
|
||||
QObject::connect(ui->centerShift, &QDial::valueChanged, this, &VORLocalizerGUI::on_centerShift_valueChanged);
|
||||
}
|
||||
|
||||
|
@ -191,6 +191,7 @@ public:
|
||||
}
|
||||
|
||||
bool findIntersection(float &lat, float &lon);
|
||||
QString getRadials() const;
|
||||
|
||||
private:
|
||||
VORLocalizerGUI *m_gui;
|
||||
@ -242,7 +243,7 @@ private:
|
||||
OpenAIP m_openAIP;
|
||||
int m_countryIndex;
|
||||
VORModel m_vorModel;
|
||||
QList<NavAid *> m_vors;
|
||||
QSharedPointer<const QList<NavAid *>> m_vors;
|
||||
QHash<int, VORGUI *> m_selectedVORs;
|
||||
AzEl m_azEl; // Position of station
|
||||
QIcon m_muteIcon;
|
||||
@ -250,6 +251,8 @@ private:
|
||||
int m_lastFeatureState;
|
||||
int m_rrSecondsCount;
|
||||
QTimer m_redrawMapTimer;
|
||||
QString m_mapFeaturePositionName;
|
||||
QStringList m_mapFeatureRadialNames;
|
||||
|
||||
explicit VORLocalizerGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent = nullptr);
|
||||
virtual ~VORLocalizerGUI();
|
||||
@ -270,6 +273,9 @@ private:
|
||||
void readNavAids();
|
||||
void updateChannelList();
|
||||
void applyMapSettings();
|
||||
void clearFromMapFeature(const QString& name, int type);
|
||||
void sendPositionToMapFeature(float lat, float lon);
|
||||
void sendRadialToMapFeature(VORGUI *vorGUI, Real radial);
|
||||
|
||||
private slots:
|
||||
void on_startStop_toggled(bool checked);
|
||||
@ -296,3 +302,4 @@ private slots:
|
||||
};
|
||||
|
||||
#endif // INCLUDE_VORLOCALIZERGUI_H
|
||||
|
||||
|
@ -197,6 +197,7 @@ set(sdrbase_SOURCES
|
||||
util/morse.cpp
|
||||
util/openaip.cpp
|
||||
util/osndb.cpp
|
||||
util/ourairportsdb.cpp
|
||||
util/peakfinder.cpp
|
||||
util/planespotters.cpp
|
||||
util/png.cpp
|
||||
@ -424,6 +425,7 @@ set(sdrbase_HEADERS
|
||||
util/movingaverage.h
|
||||
util/openaip.h
|
||||
util/osndb.h
|
||||
util/outairportsdb.h
|
||||
util/peakfinder.h
|
||||
util/planespotters.h
|
||||
util/png.h
|
||||
@ -466,6 +468,7 @@ include_directories(
|
||||
${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client
|
||||
${Boost_INCLUDE_DIRS}
|
||||
${OPUS_INCLUDE_DIRS}
|
||||
${Qt${QT_DEFAULT_MAJOR_VERSION}Gui_PRIVATE_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
add_library(sdrbase SHARED
|
||||
|
@ -378,7 +378,9 @@ void MainCore::positionUpdated(const QGeoPositionInfo &info)
|
||||
{
|
||||
m_settings.setLatitude(m_position.coordinate().latitude());
|
||||
m_settings.setLongitude(m_position.coordinate().longitude());
|
||||
m_settings.setAltitude(m_position.coordinate().altitude());
|
||||
if (!std::isnan(m_position.coordinate().altitude())) {
|
||||
m_settings.setAltitude(m_position.coordinate().altitude());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -196,6 +196,20 @@ public:
|
||||
emit preferenceChanged(Preferences::Multisampling);
|
||||
}
|
||||
|
||||
int getMapMultisampling() const { return m_preferences.getMapMultisampling(); }
|
||||
void setMapMultisampling(int samples)
|
||||
{
|
||||
m_preferences.setMapMultisampling(samples);
|
||||
emit preferenceChanged(Preferences::MapMultisampling);
|
||||
}
|
||||
|
||||
bool getMapSmoothing() const { return m_preferences.getMapSmoothing(); }
|
||||
void setMapSmoothing(bool smoothing)
|
||||
{
|
||||
m_preferences.setMapSmoothing(smoothing);
|
||||
emit preferenceChanged(Preferences::MapSmoothing);
|
||||
}
|
||||
|
||||
signals:
|
||||
void preferenceChanged(int);
|
||||
|
||||
|
@ -23,6 +23,8 @@ void Preferences::resetToDefaults()
|
||||
m_consoleMinLogLevel = QtDebugMsg;
|
||||
m_fileMinLogLevel = QtDebugMsg;
|
||||
m_multisampling = 0;
|
||||
m_mapMultisampling = 0;
|
||||
m_mapSmoothing = true;
|
||||
}
|
||||
|
||||
QByteArray Preferences::serialize() const
|
||||
@ -43,6 +45,8 @@ QByteArray Preferences::serialize() const
|
||||
s.writeS32((int) SourceItemIndex, m_sourceItemIndex);
|
||||
s.writeS32((int) Multisampling, m_multisampling);
|
||||
s.writeBool((int) AutoUpdatePosition, m_autoUpdatePosition);
|
||||
s.writeS32((int) MapMultisampling, m_mapMultisampling);
|
||||
s.writeBool((int) MapSmoothing, m_mapSmoothing);
|
||||
return s.final();
|
||||
}
|
||||
|
||||
@ -98,6 +102,8 @@ bool Preferences::deserialize(const QByteArray& data)
|
||||
|
||||
d.readS32((int) Multisampling, &m_multisampling, 0);
|
||||
d.readBool((int) AutoUpdatePosition, &m_autoUpdatePosition, true);
|
||||
d.readS32((int) MapMultisampling, &m_mapMultisampling, 16);
|
||||
d.readBool((int) MapSmoothing, &m_mapSmoothing, true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -23,7 +23,9 @@ public:
|
||||
Altitude,
|
||||
SourceItemIndex,
|
||||
Multisampling,
|
||||
AutoUpdatePosition
|
||||
AutoUpdatePosition,
|
||||
MapMultisampling,
|
||||
MapSmoothing
|
||||
};
|
||||
|
||||
Preferences();
|
||||
@ -79,6 +81,12 @@ public:
|
||||
int getMultisampling() const { return m_multisampling; }
|
||||
void setMultisampling(int samples) { m_multisampling = samples; }
|
||||
|
||||
int getMapMultisampling() const { return m_mapMultisampling; }
|
||||
void setMapMultisampling(int samples) { m_mapMultisampling = samples; }
|
||||
|
||||
bool getMapSmoothing() const { return m_mapSmoothing; }
|
||||
void setMapSmoothing(bool smoothing) { m_mapSmoothing = smoothing; }
|
||||
|
||||
protected:
|
||||
QString m_sourceDevice; //!< Identification of the source used in R0 tab (GUI flavor) at startup
|
||||
int m_sourceIndex; //!< Index of the source used in R0 tab (GUI flavor) at startup
|
||||
@ -98,7 +106,9 @@ protected:
|
||||
bool m_useLogFile;
|
||||
QString m_logFileName;
|
||||
|
||||
int m_multisampling; //!< Number of samples to use for multisampling anti-aliasing (typically 0 or 4)
|
||||
int m_multisampling; //!< Number of samples to use for multisampling anti-aliasing for spectrums (typically 0 or 4)
|
||||
int m_mapMultisampling; //!< Number of samples to use for multisampling anti-aliasing for 2D maps (16 gives best text, if not using mapSmoothing)
|
||||
bool m_mapSmoothing; //!< Whether to use smoothing for text boxes on 2D maps
|
||||
};
|
||||
|
||||
#endif // INCLUDE_PREFERENCES_H
|
||||
|
@ -17,6 +17,251 @@
|
||||
|
||||
#include "openaip.h"
|
||||
|
||||
QList<Airspace *> Airspace::readXML(const QString &filename)
|
||||
{
|
||||
QList<Airspace *> airspaces;
|
||||
QFile file(filename);
|
||||
if (file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
{
|
||||
QXmlStreamReader xmlReader(&file);
|
||||
|
||||
while(!xmlReader.atEnd() && !xmlReader.hasError())
|
||||
{
|
||||
if (xmlReader.readNextStartElement())
|
||||
{
|
||||
if (xmlReader.name() == QLatin1String("ASP"))
|
||||
{
|
||||
Airspace *airspace = new Airspace();
|
||||
|
||||
airspace->m_category = xmlReader.attributes().value("CATEGORY").toString();
|
||||
|
||||
while(xmlReader.readNextStartElement())
|
||||
{
|
||||
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
|
||||
{
|
||||
// Don't warn, as many countries don't have files
|
||||
//qDebug() << "Airspace::readXML: Could not open " << filename << " for reading.";
|
||||
}
|
||||
return airspaces;
|
||||
}
|
||||
|
||||
QList<NavAid *> NavAid::readXML(const QString &filename)
|
||||
{
|
||||
int uniqueId = 1;
|
||||
QList<NavAid *> navAidInfo;
|
||||
QFile file(filename);
|
||||
if (file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
{
|
||||
QXmlStreamReader xmlReader(&file);
|
||||
|
||||
while(!xmlReader.atEnd() && !xmlReader.hasError())
|
||||
{
|
||||
if (xmlReader.readNextStartElement())
|
||||
{
|
||||
if (xmlReader.name() == QLatin1String("NAVAID"))
|
||||
{
|
||||
QStringView typeRef = xmlReader.attributes().value("TYPE");
|
||||
if ((typeRef == QLatin1String("NDB"))
|
||||
|| (typeRef == QLatin1String("DME"))
|
||||
|| (typeRef == QLatin1String("VOR"))
|
||||
|| (typeRef == QLatin1String("VOR-DME"))
|
||||
|| (typeRef == QLatin1String("VORTAC"))
|
||||
|| (typeRef == QLatin1String("DVOR"))
|
||||
|| (typeRef == QLatin1String("DVOR-DME"))
|
||||
|| (typeRef == QLatin1String("DVORTAC")))
|
||||
{
|
||||
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_id = uniqueId++;
|
||||
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
|
||||
{
|
||||
// Don't warn, as many countries don't have files
|
||||
//qDebug() << "NavAid::readNavAidsXML: Could not open " << filename << " for reading.";
|
||||
}
|
||||
return navAidInfo;
|
||||
}
|
||||
|
||||
const QStringList OpenAIP::m_countryCodes = {
|
||||
"ad",
|
||||
"ae",
|
||||
@ -268,6 +513,12 @@ const QStringList OpenAIP::m_countryCodes = {
|
||||
"zw"
|
||||
};
|
||||
|
||||
QSharedPointer<QList<Airspace *>> OpenAIP::m_airspaces;
|
||||
QSharedPointer<QList<NavAid *>> OpenAIP::m_navAids;
|
||||
|
||||
QDateTime OpenAIP::m_airspacesModifiedDateTime;
|
||||
QDateTime OpenAIP::m_navAidsModifiedDateTime;
|
||||
|
||||
OpenAIP::OpenAIP(QObject *parent) :
|
||||
QObject(parent)
|
||||
{
|
||||
@ -390,11 +641,11 @@ void OpenAIP::downloadFinished(const QString& filename, bool success)
|
||||
}
|
||||
|
||||
// Read airspaces for all countries
|
||||
QList<Airspace *> OpenAIP::readAirspaces()
|
||||
QList<Airspace *> *OpenAIP::readAirspaces()
|
||||
{
|
||||
QList<Airspace *> airspaces;
|
||||
QList<Airspace *> *airspaces = new QList<Airspace *>();
|
||||
for (const auto& countryCode : m_countryCodes) {
|
||||
airspaces.append(readAirspaces(countryCode));
|
||||
airspaces->append(readAirspaces(countryCode));
|
||||
}
|
||||
return airspaces;
|
||||
}
|
||||
@ -406,11 +657,11 @@ QList<Airspace *> OpenAIP::readAirspaces(const QString& countryCode)
|
||||
}
|
||||
|
||||
// Read NavAids for all countries
|
||||
QList<NavAid *> OpenAIP::readNavAids()
|
||||
QList<NavAid *> *OpenAIP::readNavAids()
|
||||
{
|
||||
QList<NavAid *> navAids;
|
||||
QList<NavAid *> *navAids = new QList<NavAid *>();
|
||||
for (const auto& countryCode : m_countryCodes) {
|
||||
navAids.append(readNavAids(countryCode));
|
||||
navAids->append(readNavAids(countryCode));
|
||||
}
|
||||
return navAids;
|
||||
}
|
||||
@ -420,3 +671,60 @@ QList<NavAid *> OpenAIP::readNavAids(const QString& countryCode)
|
||||
{
|
||||
return NavAid::readXML(getNavAidsFilename(countryCode));
|
||||
}
|
||||
|
||||
QSharedPointer<const QList<Airspace *>> OpenAIP::getAirspaces()
|
||||
{
|
||||
QDateTime filesDateTime = getAirspacesModifiedDateTime();
|
||||
|
||||
if (!m_airspaces || (filesDateTime > m_airspacesModifiedDateTime))
|
||||
{
|
||||
// Using shared pointer, so old object, if it exists, will be deleted, when no longer user
|
||||
m_airspaces = QSharedPointer<QList<Airspace *>>(readAirspaces());
|
||||
m_airspacesModifiedDateTime = filesDateTime;
|
||||
}
|
||||
return m_airspaces;
|
||||
}
|
||||
|
||||
QSharedPointer<const QList<NavAid *>> OpenAIP::getNavAids()
|
||||
{
|
||||
QDateTime filesDateTime = getNavAidsModifiedDateTime();
|
||||
|
||||
if (!m_navAids || (filesDateTime > m_navAidsModifiedDateTime))
|
||||
{
|
||||
// Using shared pointer, so old object, if it exists, will be deleted, when no longer user
|
||||
m_navAids = QSharedPointer<QList<NavAid *>>(readNavAids());
|
||||
m_navAidsModifiedDateTime = filesDateTime;
|
||||
}
|
||||
return m_navAids;
|
||||
}
|
||||
|
||||
// Gets the date and time the airspaces files were most recently modified
|
||||
QDateTime OpenAIP::getAirspacesModifiedDateTime()
|
||||
{
|
||||
QDateTime dateTime;
|
||||
for (const auto& countryCode : m_countryCodes)
|
||||
{
|
||||
QFileInfo fileInfo(getAirspaceFilename(countryCode));
|
||||
QDateTime fileModifiedDateTime = fileInfo.lastModified();
|
||||
if (fileModifiedDateTime > dateTime) {
|
||||
dateTime = fileModifiedDateTime;
|
||||
}
|
||||
}
|
||||
return dateTime;
|
||||
}
|
||||
|
||||
// Gets the date and time the navaid files were most recently modified
|
||||
QDateTime OpenAIP::getNavAidsModifiedDateTime()
|
||||
{
|
||||
QDateTime dateTime;
|
||||
for (const auto& countryCode : m_countryCodes)
|
||||
{
|
||||
QFileInfo fileInfo(getNavAidsFilename(countryCode));
|
||||
QDateTime fileModifiedDateTime = fileInfo.lastModified();
|
||||
if (fileModifiedDateTime > dateTime) {
|
||||
dateTime = fileModifiedDateTime;
|
||||
}
|
||||
}
|
||||
return dateTime;
|
||||
}
|
||||
|
||||
|
@ -118,116 +118,7 @@ struct SDRBASE_API Airspace {
|
||||
}
|
||||
|
||||
// Read OpenAIP XML file
|
||||
static QList<Airspace *> readXML(const QString &filename)
|
||||
{
|
||||
QList<Airspace *> airspaces;
|
||||
QFile file(filename);
|
||||
if (file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
{
|
||||
QXmlStreamReader xmlReader(&file);
|
||||
|
||||
while(!xmlReader.atEnd() && !xmlReader.hasError())
|
||||
{
|
||||
if (xmlReader.readNextStartElement())
|
||||
{
|
||||
if (xmlReader.name() == QLatin1String("ASP"))
|
||||
{
|
||||
Airspace *airspace = new Airspace();
|
||||
|
||||
airspace->m_category = xmlReader.attributes().value("CATEGORY").toString();
|
||||
|
||||
while(xmlReader.readNextStartElement())
|
||||
{
|
||||
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
|
||||
{
|
||||
// Don't warn, as many countries don't have files
|
||||
//qDebug() << "Airspace::readXML: Could not open " << filename << " for reading.";
|
||||
}
|
||||
return airspaces;
|
||||
}
|
||||
static QList<Airspace *> readXML(const QString &filename);
|
||||
|
||||
};
|
||||
|
||||
@ -251,140 +142,7 @@ struct SDRBASE_API NavAid {
|
||||
}
|
||||
|
||||
// OpenAIP XML file
|
||||
static QList<NavAid *> readXML(const QString &filename)
|
||||
{
|
||||
int uniqueId = 1;
|
||||
QList<NavAid *> navAidInfo;
|
||||
QFile file(filename);
|
||||
if (file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
{
|
||||
QXmlStreamReader xmlReader(&file);
|
||||
|
||||
while(!xmlReader.atEnd() && !xmlReader.hasError())
|
||||
{
|
||||
if (xmlReader.readNextStartElement())
|
||||
{
|
||||
if (xmlReader.name() == QLatin1String("NAVAID"))
|
||||
{
|
||||
QStringView typeRef = xmlReader.attributes().value("TYPE");
|
||||
if ((typeRef == QLatin1String("NDB"))
|
||||
|| (typeRef == QLatin1String("DME"))
|
||||
|| (typeRef == QLatin1String("VOR"))
|
||||
|| (typeRef == QLatin1String("VOR-DME"))
|
||||
|| (typeRef == QLatin1String("VORTAC"))
|
||||
|| (typeRef == QLatin1String("DVOR"))
|
||||
|| (typeRef == QLatin1String("DVOR-DME"))
|
||||
|| (typeRef == QLatin1String("DVORTAC")))
|
||||
{
|
||||
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_id = uniqueId++;
|
||||
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
|
||||
{
|
||||
// Don't warn, as many countries don't have files
|
||||
//qDebug() << "NavAid::readNavAidsXML: Could not open " << filename << " for reading.";
|
||||
}
|
||||
return navAidInfo;
|
||||
}
|
||||
|
||||
static QList<NavAid *> readXML(const QString &filename);
|
||||
};
|
||||
|
||||
class SDRBASE_API OpenAIP : public QObject {
|
||||
@ -396,18 +154,27 @@ public:
|
||||
|
||||
static const QStringList m_countryCodes;
|
||||
|
||||
static QList<Airspace *> readAirspaces();
|
||||
static QList<Airspace *> readAirspaces(const QString& countryCode);
|
||||
static QList<NavAid *> readNavAids();
|
||||
static QList<NavAid *> readNavAids(const QString& countryCode);
|
||||
|
||||
void downloadAirspaces();
|
||||
void downloadNavAids();
|
||||
|
||||
static QSharedPointer<const QList<Airspace *>> getAirspaces();
|
||||
static QSharedPointer<const QList<NavAid *>> getNavAids();
|
||||
|
||||
private:
|
||||
HttpDownloadManager m_dlm;
|
||||
int m_countryIndex;
|
||||
|
||||
static QSharedPointer<QList<Airspace *>> m_airspaces;
|
||||
static QSharedPointer<QList<NavAid *>> m_navAids;
|
||||
|
||||
static QDateTime m_airspacesModifiedDateTime;
|
||||
static QDateTime m_navAidsModifiedDateTime;
|
||||
|
||||
static QList<Airspace *> *readAirspaces();
|
||||
static QList<Airspace *> readAirspaces(const QString& countryCode);
|
||||
static QList<NavAid *> *readNavAids();
|
||||
static QList<NavAid *> readNavAids(const QString& countryCode);
|
||||
|
||||
static QString getDataDir();
|
||||
static QString getAirspaceFilename(int i);
|
||||
static QString getAirspaceFilename(const QString& countryCode);
|
||||
@ -415,6 +182,8 @@ private:
|
||||
static QString getNavAidsFilename(int i);
|
||||
static QString getNavAidsFilename(const QString& countryCode);
|
||||
static QString getNavAidsURL(int i);
|
||||
static QDateTime getAirspacesModifiedDateTime();
|
||||
static QDateTime getNavAidsModifiedDateTime();
|
||||
|
||||
void downloadAirspace();
|
||||
void downloadNavAid();
|
||||
@ -431,3 +200,4 @@ signals:
|
||||
};
|
||||
|
||||
#endif // INCLUDE_OPENAIP_H
|
||||
|
||||
|
@ -15,6 +15,11 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QResource>
|
||||
|
||||
#include <QtGui/private/qzipreader_p.h>
|
||||
|
||||
#include "util/osndb.h"
|
||||
|
||||
QHash<QString, QIcon *> AircraftInformation::m_airlineIcons;
|
||||
@ -23,3 +28,544 @@ QHash<QString, QIcon *> AircraftInformation::m_flagIcons;
|
||||
QHash<QString, QString> *AircraftInformation::m_prefixMap;
|
||||
QHash<QString, QString> *AircraftInformation::m_militaryMap;
|
||||
QMutex AircraftInformation::m_mutex;
|
||||
|
||||
QSharedPointer<const QHash<int, AircraftInformation *>> OsnDB::m_aircraftInformation;
|
||||
QSharedPointer<const QHash<QString, AircraftInformation *>> OsnDB::m_aircraftInformationByReg;
|
||||
QDateTime OsnDB::m_modifiedDateTime;
|
||||
|
||||
|
||||
OsnDB::OsnDB(QObject *parent) :
|
||||
QObject(parent)
|
||||
{
|
||||
connect(&m_dlm, &HttpDownloadManager::downloadComplete, this, &OsnDB::downloadFinished);
|
||||
}
|
||||
|
||||
OsnDB::~OsnDB()
|
||||
{
|
||||
disconnect(&m_dlm, &HttpDownloadManager::downloadComplete, this, &OsnDB::downloadFinished);
|
||||
}
|
||||
|
||||
void OsnDB::downloadAircraftInformation()
|
||||
{
|
||||
QString filename = OsnDB::getOSNDBZipFilename();
|
||||
QString urlString = OSNDB_URL;
|
||||
QUrl dbURL(urlString);
|
||||
qDebug() << "OsnDB::downloadAircraftInformation: Downloading " << urlString;
|
||||
emit downloadingURL(urlString);
|
||||
QNetworkReply *reply = m_dlm.download(dbURL, filename);
|
||||
connect(reply, &QNetworkReply::downloadProgress, this, [this](qint64 bytesRead, qint64 totalBytes) {
|
||||
emit downloadProgress(bytesRead, totalBytes);
|
||||
});
|
||||
}
|
||||
|
||||
void OsnDB::downloadFinished(const QString& filename, bool success)
|
||||
{
|
||||
if (!success)
|
||||
{
|
||||
qWarning() << "OsnDB::downloadFinished: Failed to download: " << filename;
|
||||
emit downloadError(QString("Failed to download: %1").arg(filename));
|
||||
}
|
||||
else if (filename == OsnDB::getOSNDBZipFilename())
|
||||
{
|
||||
// Extract .csv file from .zip file
|
||||
QZipReader reader(filename);
|
||||
QByteArray database = reader.fileData("media/data/samples/metadata/aircraftDatabase.csv");
|
||||
if (database.size() > 0)
|
||||
{
|
||||
QFile file(OsnDB::getOSNDBFilename());
|
||||
if (file.open(QIODevice::WriteOnly))
|
||||
{
|
||||
file.write(database);
|
||||
file.close();
|
||||
emit downloadAircraftInformationFinished();
|
||||
}
|
||||
else
|
||||
{
|
||||
qWarning() << "OsnDB::downloadFinished - Failed to open " << file.fileName() << " for writing";
|
||||
emit downloadError(QString("Failed to open %1 for writing").arg(file.fileName()));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qWarning() << "OsnDB::downloadFinished - aircraftDatabase.csv not in expected dir. Extracting all.";
|
||||
if (reader.extractAll(getDataDir()))
|
||||
{
|
||||
emit downloadAircraftInformationFinished();
|
||||
}
|
||||
else
|
||||
{
|
||||
qWarning() << "OsnDB::downloadFinished - Failed to extract files from " << filename;
|
||||
emit downloadError(QString("Failed to extract files from ").arg(filename));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "OsnDB::downloadFinished: Unexpected filename: " << filename;
|
||||
emit downloadError(QString("Unexpected filename: %1").arg(filename));
|
||||
}
|
||||
}
|
||||
|
||||
QSharedPointer<const QHash<int, AircraftInformation *>> OsnDB::getAircraftInformation()
|
||||
{
|
||||
QFileInfo fastFileInfo(getFastDBFilename());
|
||||
QFileInfo fullFileInfo(getOSNDBFilename());
|
||||
QDateTime fastModifiedDateTime = fastFileInfo.lastModified();
|
||||
QDateTime fullModifiedDateTime = fullFileInfo.lastModified();
|
||||
|
||||
// Update fast database, if full database is newer
|
||||
if (fullModifiedDateTime > fastModifiedDateTime)
|
||||
{
|
||||
qDebug() << "AircraftInformation::getAircraftInformation: Creating fast database";
|
||||
m_aircraftInformation = QSharedPointer<const QHash<int, AircraftInformation *>>(OsnDB::readOSNDB(getOSNDBFilename()));
|
||||
if (m_aircraftInformation)
|
||||
{
|
||||
OsnDB::writeFastDB(OsnDB::getFastDBFilename(), m_aircraftInformation.get());
|
||||
fastModifiedDateTime = fastFileInfo.lastModified();
|
||||
m_modifiedDateTime = fastModifiedDateTime;
|
||||
m_aircraftInformationByReg = QSharedPointer<const QHash<QString, AircraftInformation *>> (OsnDB::registrationHash(m_aircraftInformation.get()));
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_aircraftInformation || (fastModifiedDateTime > m_modifiedDateTime))
|
||||
{
|
||||
m_aircraftInformation = QSharedPointer<const QHash<int, AircraftInformation *>>(OsnDB::readFastDB(getFastDBFilename()));
|
||||
if (m_aircraftInformation)
|
||||
{
|
||||
m_modifiedDateTime = fastModifiedDateTime;
|
||||
m_aircraftInformationByReg = QSharedPointer<const QHash<QString, AircraftInformation *>> (OsnDB::registrationHash(m_aircraftInformation.get()));
|
||||
}
|
||||
}
|
||||
|
||||
return m_aircraftInformation;
|
||||
}
|
||||
|
||||
QSharedPointer<const QHash<QString, AircraftInformation *>> OsnDB::getAircraftInformationByReg()
|
||||
{
|
||||
getAircraftInformation();
|
||||
return m_aircraftInformationByReg;
|
||||
}
|
||||
|
||||
QHash<int, AircraftInformation *> *OsnDB::readOSNDB(const QString &filename)
|
||||
{
|
||||
int cnt = 0;
|
||||
QHash<int, AircraftInformation *> *aircraftInfo = nullptr;
|
||||
|
||||
// Column numbers used for the data as of 2020/10/28
|
||||
int icaoCol = 0;
|
||||
int registrationCol = 1;
|
||||
int manufacturerNameCol = 3;
|
||||
int modelCol = 4;
|
||||
int ownerCol = 13;
|
||||
int operatorCol = 9;
|
||||
int operatorICAOCol = 11;
|
||||
int registeredCol = 15;
|
||||
|
||||
qDebug() << "AircraftInformation::readOSNDB: " << filename;
|
||||
|
||||
FILE *file;
|
||||
QByteArray utfFilename = filename.toUtf8();
|
||||
if ((file = fopen(utfFilename.constData(), "r")) != NULL)
|
||||
{
|
||||
char row[2048];
|
||||
|
||||
if (fgets(row, sizeof(row), file))
|
||||
{
|
||||
aircraftInfo = new QHash<int, AircraftInformation *>();
|
||||
aircraftInfo->reserve(500000);
|
||||
// Read header
|
||||
int idx = 0;
|
||||
char *p = strtok(row, ",");
|
||||
while (p != NULL)
|
||||
{
|
||||
if (!strcmp(p, "icao24"))
|
||||
icaoCol = idx;
|
||||
else if (!strcmp(p, "registration"))
|
||||
registrationCol = idx;
|
||||
else if (!strcmp(p, "manufacturername"))
|
||||
manufacturerNameCol = idx;
|
||||
else if (!strcmp(p, "model"))
|
||||
modelCol = idx;
|
||||
else if (!strcmp(p, "owner"))
|
||||
ownerCol = idx;
|
||||
else if (!strcmp(p, "operator"))
|
||||
operatorCol = idx;
|
||||
else if (!strcmp(p, "operatoricao"))
|
||||
operatorICAOCol = idx;
|
||||
else if (!strcmp(p, "registered"))
|
||||
registeredCol = idx;
|
||||
p = strtok(NULL, ",");
|
||||
idx++;
|
||||
}
|
||||
// Read data
|
||||
while (fgets(row, sizeof(row), file))
|
||||
{
|
||||
int icao = 0;
|
||||
char *icaoString = NULL;
|
||||
char *registration = NULL;
|
||||
size_t registrationLen = 0;
|
||||
char *manufacturerName = NULL;
|
||||
size_t manufacturerNameLen = 0;
|
||||
char *model = NULL;
|
||||
size_t modelLen = 0;
|
||||
char *owner = NULL;
|
||||
size_t ownerLen = 0;
|
||||
char *operatorName = NULL;
|
||||
size_t operatorNameLen = 0;
|
||||
char *operatorICAO = NULL;
|
||||
size_t operatorICAOLen = 0;
|
||||
char *registered = NULL;
|
||||
size_t registeredLen = 0;
|
||||
|
||||
p = strtok(row, ",");
|
||||
idx = 0;
|
||||
while (p != NULL)
|
||||
{
|
||||
// Read strings, stripping quotes
|
||||
if (idx == icaoCol)
|
||||
{
|
||||
icaoString = p+1;
|
||||
icaoString[strlen(icaoString)-1] = '\0';
|
||||
icao = strtol(icaoString, NULL, 16);
|
||||
// Ignore entries with uppercase - might be better to try and merge?
|
||||
// See: https://opensky-network.org/forum/bug-reports/652-are-the-aircraft-database-dumps-working
|
||||
for (int i = 0; icaoString[i] != '\0'; i++)
|
||||
{
|
||||
char c = icaoString[i];
|
||||
if ((c >= 'A') && (c <= 'F'))
|
||||
{
|
||||
icao = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (idx == registrationCol)
|
||||
{
|
||||
registration = p+1;
|
||||
registrationLen = strlen(registration)-1;
|
||||
registration[registrationLen] = '\0';
|
||||
}
|
||||
else if (idx == manufacturerNameCol)
|
||||
{
|
||||
manufacturerName = p+1;
|
||||
manufacturerNameLen = strlen(manufacturerName)-1;
|
||||
manufacturerName[manufacturerNameLen] = '\0';
|
||||
}
|
||||
else if (idx == modelCol)
|
||||
{
|
||||
model = p+1;
|
||||
modelLen = strlen(model)-1;
|
||||
model[modelLen] = '\0';
|
||||
}
|
||||
else if (idx == ownerCol)
|
||||
{
|
||||
owner = p+1;
|
||||
ownerLen = strlen(owner)-1;
|
||||
owner[ownerLen] = '\0';
|
||||
}
|
||||
else if (idx == operatorCol)
|
||||
{
|
||||
operatorName = p+1;
|
||||
operatorNameLen = strlen(operatorName)-1;
|
||||
operatorName[operatorNameLen] = '\0';
|
||||
}
|
||||
else if (idx == operatorICAOCol)
|
||||
{
|
||||
operatorICAO = p+1;
|
||||
operatorICAOLen = strlen(operatorICAO)-1;
|
||||
operatorICAO[operatorICAOLen] = '\0';
|
||||
}
|
||||
else if (idx == registeredCol)
|
||||
{
|
||||
registered = p+1;
|
||||
registeredLen = strlen(registered)-1;
|
||||
registered[registeredLen] = '\0';
|
||||
}
|
||||
p = strtok(NULL, ",");
|
||||
idx++;
|
||||
}
|
||||
|
||||
// Only create the entry if we have some interesting data
|
||||
if ((icao != 0) && (registrationLen > 0 || modelLen > 0 || ownerLen > 0 || operatorNameLen > 0 || operatorICAOLen > 0))
|
||||
{
|
||||
QString modelQ = QString(model);
|
||||
// Tidy up the model names
|
||||
if (modelQ.endsWith(" (Boeing)"))
|
||||
modelQ = modelQ.left(modelQ.size() - 9);
|
||||
else if (modelQ.startsWith("BOEING "))
|
||||
modelQ = modelQ.right(modelQ.size() - 7);
|
||||
else if (modelQ.startsWith("Boeing "))
|
||||
modelQ = modelQ.right(modelQ.size() - 7);
|
||||
else if (modelQ.startsWith("AIRBUS "))
|
||||
modelQ = modelQ.right(modelQ.size() - 7);
|
||||
else if (modelQ.startsWith("Airbus "))
|
||||
modelQ = modelQ.right(modelQ.size() - 7);
|
||||
else if (modelQ.endsWith(" (Cessna)"))
|
||||
modelQ = modelQ.left(modelQ.size() - 9);
|
||||
|
||||
AircraftInformation *aircraft = new AircraftInformation();
|
||||
aircraft->m_icao = icao;
|
||||
aircraft->m_registration = QString(registration);
|
||||
aircraft->m_manufacturerName = QString(manufacturerName);
|
||||
aircraft->m_model = modelQ;
|
||||
aircraft->m_owner = QString(owner);
|
||||
aircraft->m_operator = QString(operatorName);
|
||||
aircraft->m_operatorICAO = QString(operatorICAO);
|
||||
aircraft->m_registered = QString(registered);
|
||||
aircraftInfo->insert(icao, aircraft);
|
||||
cnt++;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
fclose(file);
|
||||
}
|
||||
else
|
||||
qDebug() << "AircraftInformation::readOSNDB: Failed to open " << filename;
|
||||
|
||||
qDebug() << "AircraftInformation::readOSNDB: Read " << cnt << " aircraft";
|
||||
|
||||
return aircraftInfo;
|
||||
}
|
||||
|
||||
QHash<QString, AircraftInformation *> *OsnDB::registrationHash(const QHash<int, AircraftInformation *> *in)
|
||||
{
|
||||
QHash<QString, AircraftInformation *> *out = new QHash<QString, AircraftInformation *>();
|
||||
QHashIterator<int, AircraftInformation *> i(*in);
|
||||
while (i.hasNext())
|
||||
{
|
||||
i.next();
|
||||
AircraftInformation *info = i.value();
|
||||
out->insert(info->m_registration, info);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool OsnDB::writeFastDB(const QString &filename, const QHash<int, AircraftInformation *> *aircraftInfo)
|
||||
{
|
||||
QFile file(filename);
|
||||
if (file.open(QIODevice::WriteOnly))
|
||||
{
|
||||
file.write("icao24,registration,manufacturername,model,owner,operator,operatoricao,registered\n");
|
||||
QHash<int, AircraftInformation *>::const_iterator i = aircraftInfo->begin();
|
||||
while (i != aircraftInfo->end())
|
||||
{
|
||||
AircraftInformation *info = i.value();
|
||||
file.write(QString("%1").arg(info->m_icao, 1, 16).toUtf8());
|
||||
file.write(",");
|
||||
file.write(info->m_registration.toUtf8());
|
||||
file.write(",");
|
||||
file.write(info->m_manufacturerName.toUtf8());
|
||||
file.write(",");
|
||||
file.write(info->m_model.toUtf8());
|
||||
file.write(",");
|
||||
file.write(info->m_owner.toUtf8());
|
||||
file.write(",");
|
||||
file.write(info->m_operator.toUtf8());
|
||||
file.write(",");
|
||||
file.write(info->m_operatorICAO.toUtf8());
|
||||
file.write(",");
|
||||
file.write(info->m_registered.toUtf8());
|
||||
file.write("\n");
|
||||
++i;
|
||||
}
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
qCritical() << "AircraftInformation::writeFastDB failed to open " << filename << " for writing: " << file.errorString();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
QHash<int, AircraftInformation *> *OsnDB::readFastDB(const QString &filename)
|
||||
{
|
||||
int cnt = 0;
|
||||
QHash<int, AircraftInformation *> *aircraftInfo = nullptr;
|
||||
|
||||
qDebug() << "AircraftInformation::readFastDB: " << filename;
|
||||
|
||||
FILE *file;
|
||||
QByteArray utfFilename = filename.toUtf8();
|
||||
if ((file = fopen(utfFilename.constData(), "r")) != NULL)
|
||||
{
|
||||
char row[2048];
|
||||
|
||||
if (fgets(row, sizeof(row), file))
|
||||
{
|
||||
// Check header
|
||||
if (!strcmp(row, "icao24,registration,manufacturername,model,owner,operator,operatoricao,registered\n"))
|
||||
{
|
||||
aircraftInfo = new QHash<int, AircraftInformation *>();
|
||||
aircraftInfo->reserve(500000);
|
||||
// Read data
|
||||
while (fgets(row, sizeof(row), file))
|
||||
{
|
||||
char *p = row;
|
||||
AircraftInformation *aircraft = new AircraftInformation();
|
||||
char *icaoString = csvNext(&p);
|
||||
int icao = strtol(icaoString, NULL, 16);
|
||||
aircraft->m_icao = icao;
|
||||
aircraft->m_registration = QString(csvNext(&p));
|
||||
aircraft->m_manufacturerName = QString(csvNext(&p));
|
||||
aircraft->m_model = QString(csvNext(&p));
|
||||
aircraft->m_owner = QString(csvNext(&p));
|
||||
aircraft->m_operator = QString(csvNext(&p));
|
||||
aircraft->m_operatorICAO = QString(csvNext(&p));
|
||||
aircraft->m_registered = QString(csvNext(&p));
|
||||
aircraftInfo->insert(icao, aircraft);
|
||||
cnt++;
|
||||
}
|
||||
}
|
||||
else
|
||||
qDebug() << "AircraftInformation::readFastDB: Unexpected header";
|
||||
}
|
||||
else
|
||||
qDebug() << "AircraftInformation::readFastDB: Empty file";
|
||||
fclose(file);
|
||||
}
|
||||
else
|
||||
qDebug() << "AircraftInformation::readFastDB: Failed to open " << filename;
|
||||
|
||||
qDebug() << "AircraftInformation::readFastDB - read " << cnt << " aircraft";
|
||||
|
||||
return aircraftInfo;
|
||||
}
|
||||
|
||||
QString AircraftInformation::getFlag() const
|
||||
{
|
||||
QString flag;
|
||||
if (m_prefixMap)
|
||||
{
|
||||
int idx = m_registration.indexOf('-');
|
||||
if (idx >= 0)
|
||||
{
|
||||
QString prefix;
|
||||
|
||||
// Some countries use AA-A - try these first as first letters are common
|
||||
prefix = m_registration.left(idx + 2);
|
||||
if (m_prefixMap->contains(prefix))
|
||||
{
|
||||
flag = m_prefixMap->value(prefix);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try letters before '-'
|
||||
prefix = m_registration.left(idx);
|
||||
if (m_prefixMap->contains(prefix)) {
|
||||
flag = m_prefixMap->value(prefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No '-' Could be one of a few countries or military.
|
||||
// See: https://en.wikipedia.org/wiki/List_of_aircraft_registration_prefixes
|
||||
if (m_registration.startsWith("N")) {
|
||||
flag = m_prefixMap->value("N"); // US
|
||||
} else if (m_registration.startsWith("JA")) {
|
||||
flag = m_prefixMap->value("JA"); // Japan
|
||||
} else if (m_registration.startsWith("HL")) {
|
||||
flag = m_prefixMap->value("HL"); // Korea
|
||||
} else if (m_registration.startsWith("YV")) {
|
||||
flag = m_prefixMap->value("YV"); // Venezuela
|
||||
} else if ((m_militaryMap != nullptr) && (m_militaryMap->contains(m_operator))) {
|
||||
flag = m_militaryMap->value(m_operator);
|
||||
}
|
||||
}
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
QString AircraftInformation::getAirlineIconPath(const QString &operatorICAO)
|
||||
{
|
||||
QString endPath = QString("/airlinelogos/%1.bmp").arg(operatorICAO);
|
||||
// Try in user directory first, so they can customise
|
||||
QString userIconPath = OsnDB::getDataDir() + endPath;
|
||||
QFile file(userIconPath);
|
||||
if (file.exists())
|
||||
{
|
||||
return userIconPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try in resources
|
||||
QString resourceIconPath = ":" + endPath;
|
||||
QResource resource(resourceIconPath);
|
||||
if (resource.isValid())
|
||||
{
|
||||
return resourceIconPath;
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
QIcon *AircraftInformation::getAirlineIcon(const QString &operatorICAO)
|
||||
{
|
||||
if (m_airlineIcons.contains(operatorICAO))
|
||||
{
|
||||
return m_airlineIcons.value(operatorICAO);
|
||||
}
|
||||
else
|
||||
{
|
||||
QIcon *icon = nullptr;
|
||||
QString path = getAirlineIconPath(operatorICAO);
|
||||
if (!path.isEmpty())
|
||||
{
|
||||
icon = new QIcon(path);
|
||||
m_airlineIcons.insert(operatorICAO, icon);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!m_airlineMissingIcons.contains(operatorICAO))
|
||||
{
|
||||
qDebug() << "ADSBDemodGUI: No airline logo for " << operatorICAO;
|
||||
m_airlineMissingIcons.insert(operatorICAO, true);
|
||||
}
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
QString AircraftInformation::getFlagIconPath(const QString &country)
|
||||
{
|
||||
QString endPath = QString("/flags/%1.bmp").arg(country);
|
||||
// Try in user directory first, so they can customise
|
||||
QString userIconPath = OsnDB::getDataDir() + endPath;
|
||||
QFile file(userIconPath);
|
||||
if (file.exists())
|
||||
{
|
||||
return userIconPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try in resources
|
||||
QString resourceIconPath = ":" + endPath;
|
||||
QResource resource(resourceIconPath);
|
||||
if (resource.isValid())
|
||||
{
|
||||
return resourceIconPath;
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
QIcon *AircraftInformation::getFlagIcon(const QString &country)
|
||||
{
|
||||
if (m_flagIcons.contains(country))
|
||||
{
|
||||
return m_flagIcons.value(country);
|
||||
}
|
||||
else
|
||||
{
|
||||
QIcon *icon = nullptr;
|
||||
QString path = getFlagIconPath(country);
|
||||
if (!path.isEmpty())
|
||||
{
|
||||
icon = new QIcon(path);
|
||||
m_flagIcons.insert(country, icon);
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,20 +19,19 @@
|
||||
#define INCLUDE_OSNDB_H
|
||||
|
||||
#include <QString>
|
||||
#include <QFile>
|
||||
#include <QByteArray>
|
||||
#include <QHash>
|
||||
#include <QList>
|
||||
#include <QDebug>
|
||||
#include <QIcon>
|
||||
#include <QMutex>
|
||||
#include <QStandardPaths>
|
||||
#include <QResource>
|
||||
#include <QDateTime>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "util/csv.h"
|
||||
#include "util/httpdownloadmanager.h"
|
||||
#include "export.h"
|
||||
|
||||
#define OSNDB_URL "https://opensky-network.org/datasets/metadata/aircraftDatabase.zip"
|
||||
@ -48,6 +47,34 @@ struct SDRBASE_API AircraftInformation {
|
||||
QString m_operatorICAO;
|
||||
QString m_registered;
|
||||
|
||||
static void init()
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
if (!m_prefixMap)
|
||||
{
|
||||
// Read registration prefix to country map
|
||||
m_prefixMap = CSV::hash(":/flags/regprefixmap.csv");
|
||||
// Read operator air force to military map
|
||||
m_militaryMap = CSV::hash(":/flags/militarymap.csv");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Get flag based on registration
|
||||
QString getFlag() const;
|
||||
|
||||
static QString getAirlineIconPath(const QString &operatorICAO);
|
||||
|
||||
// Try to find an airline logo based on ICAO
|
||||
static QIcon *getAirlineIcon(const QString &operatorICAO);
|
||||
|
||||
static QString getFlagIconPath(const QString &country);
|
||||
|
||||
// Try to find an flag logo based on a country
|
||||
static QIcon *getFlagIcon(const QString &country);
|
||||
|
||||
private:
|
||||
|
||||
static QHash<QString, QIcon *> m_airlineIcons; // Hashed on airline ICAO
|
||||
static QHash<QString, bool> m_airlineMissingIcons; // Hash containing which ICAOs we don't have icons for
|
||||
static QHash<QString, QIcon *> m_flagIcons; // Hashed on country
|
||||
@ -55,345 +82,20 @@ struct SDRBASE_API AircraftInformation {
|
||||
static QHash<QString, QString> *m_militaryMap; // Operator airforce to military (flag name)
|
||||
static QMutex m_mutex;
|
||||
|
||||
static void init()
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
};
|
||||
|
||||
// Read registration prefix to country map
|
||||
m_prefixMap = CSV::hash(":/flags/regprefixmap.csv");
|
||||
// Read operator air force to military map
|
||||
m_militaryMap = CSV::hash(":/flags/militarymap.csv");
|
||||
}
|
||||
class SDRBASE_API OsnDB : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
// Read OpenSky Network CSV file
|
||||
// This is large and contains lots of data we don't want, so we convert to
|
||||
// a smaller version to speed up loading time
|
||||
// Note that we use C file functions rather than QT, as these are ~30% faster
|
||||
// and the QT version seemed to occasionally crash
|
||||
static QHash<int, AircraftInformation *> *readOSNDB(const QString &filename)
|
||||
{
|
||||
int cnt = 0;
|
||||
QHash<int, AircraftInformation *> *aircraftInfo = nullptr;
|
||||
public:
|
||||
|
||||
// Column numbers used for the data as of 2020/10/28
|
||||
int icaoCol = 0;
|
||||
int registrationCol = 1;
|
||||
int manufacturerNameCol = 3;
|
||||
int modelCol = 4;
|
||||
int ownerCol = 13;
|
||||
int operatorCol = 9;
|
||||
int operatorICAOCol = 11;
|
||||
int registeredCol = 15;
|
||||
OsnDB(QObject* parent=nullptr);
|
||||
~OsnDB();
|
||||
|
||||
qDebug() << "AircraftInformation::readOSNDB: " << filename;
|
||||
void downloadAircraftInformation();
|
||||
|
||||
FILE *file;
|
||||
QByteArray utfFilename = filename.toUtf8();
|
||||
if ((file = fopen(utfFilename.constData(), "r")) != NULL)
|
||||
{
|
||||
char row[2048];
|
||||
|
||||
if (fgets(row, sizeof(row), file))
|
||||
{
|
||||
aircraftInfo = new QHash<int, AircraftInformation *>();
|
||||
aircraftInfo->reserve(500000);
|
||||
// Read header
|
||||
int idx = 0;
|
||||
char *p = strtok(row, ",");
|
||||
while (p != NULL)
|
||||
{
|
||||
if (!strcmp(p, "icao24"))
|
||||
icaoCol = idx;
|
||||
else if (!strcmp(p, "registration"))
|
||||
registrationCol = idx;
|
||||
else if (!strcmp(p, "manufacturername"))
|
||||
manufacturerNameCol = idx;
|
||||
else if (!strcmp(p, "model"))
|
||||
modelCol = idx;
|
||||
else if (!strcmp(p, "owner"))
|
||||
ownerCol = idx;
|
||||
else if (!strcmp(p, "operator"))
|
||||
operatorCol = idx;
|
||||
else if (!strcmp(p, "operatoricao"))
|
||||
operatorICAOCol = idx;
|
||||
else if (!strcmp(p, "registered"))
|
||||
registeredCol = idx;
|
||||
p = strtok(NULL, ",");
|
||||
idx++;
|
||||
}
|
||||
// Read data
|
||||
while (fgets(row, sizeof(row), file))
|
||||
{
|
||||
int icao = 0;
|
||||
char *icaoString = NULL;
|
||||
char *registration = NULL;
|
||||
size_t registrationLen = 0;
|
||||
char *manufacturerName = NULL;
|
||||
size_t manufacturerNameLen = 0;
|
||||
char *model = NULL;
|
||||
size_t modelLen = 0;
|
||||
char *owner = NULL;
|
||||
size_t ownerLen = 0;
|
||||
char *operatorName = NULL;
|
||||
size_t operatorNameLen = 0;
|
||||
char *operatorICAO = NULL;
|
||||
size_t operatorICAOLen = 0;
|
||||
char *registered = NULL;
|
||||
size_t registeredLen = 0;
|
||||
|
||||
p = strtok(row, ",");
|
||||
idx = 0;
|
||||
while (p != NULL)
|
||||
{
|
||||
// Read strings, stripping quotes
|
||||
if (idx == icaoCol)
|
||||
{
|
||||
icaoString = p+1;
|
||||
icaoString[strlen(icaoString)-1] = '\0';
|
||||
icao = strtol(icaoString, NULL, 16);
|
||||
}
|
||||
else if (idx == registrationCol)
|
||||
{
|
||||
registration = p+1;
|
||||
registrationLen = strlen(registration)-1;
|
||||
registration[registrationLen] = '\0';
|
||||
}
|
||||
else if (idx == manufacturerNameCol)
|
||||
{
|
||||
manufacturerName = p+1;
|
||||
manufacturerNameLen = strlen(manufacturerName)-1;
|
||||
manufacturerName[manufacturerNameLen] = '\0';
|
||||
}
|
||||
else if (idx == modelCol)
|
||||
{
|
||||
model = p+1;
|
||||
modelLen = strlen(model)-1;
|
||||
model[modelLen] = '\0';
|
||||
}
|
||||
else if (idx == ownerCol)
|
||||
{
|
||||
owner = p+1;
|
||||
ownerLen = strlen(owner)-1;
|
||||
owner[ownerLen] = '\0';
|
||||
}
|
||||
else if (idx == operatorCol)
|
||||
{
|
||||
operatorName = p+1;
|
||||
operatorNameLen = strlen(operatorName)-1;
|
||||
operatorName[operatorNameLen] = '\0';
|
||||
}
|
||||
else if (idx == operatorICAOCol)
|
||||
{
|
||||
operatorICAO = p+1;
|
||||
operatorICAOLen = strlen(operatorICAO)-1;
|
||||
operatorICAO[operatorICAOLen] = '\0';
|
||||
}
|
||||
else if (idx == registeredCol)
|
||||
{
|
||||
registered = p+1;
|
||||
registeredLen = strlen(registered)-1;
|
||||
registered[registeredLen] = '\0';
|
||||
}
|
||||
p = strtok(NULL, ",");
|
||||
idx++;
|
||||
}
|
||||
|
||||
// Only create the entry if we have some interesting data
|
||||
if ((icao != 0) && (registrationLen > 0 || modelLen > 0 || ownerLen > 0 || operatorNameLen > 0 || operatorICAOLen > 0))
|
||||
{
|
||||
QString modelQ = QString(model);
|
||||
// Tidy up the model names
|
||||
if (modelQ.endsWith(" (Boeing)"))
|
||||
modelQ = modelQ.left(modelQ.size() - 9);
|
||||
else if (modelQ.startsWith("BOEING "))
|
||||
modelQ = modelQ.right(modelQ.size() - 7);
|
||||
else if (modelQ.startsWith("Boeing "))
|
||||
modelQ = modelQ.right(modelQ.size() - 7);
|
||||
else if (modelQ.startsWith("AIRBUS "))
|
||||
modelQ = modelQ.right(modelQ.size() - 7);
|
||||
else if (modelQ.startsWith("Airbus "))
|
||||
modelQ = modelQ.right(modelQ.size() - 7);
|
||||
else if (modelQ.endsWith(" (Cessna)"))
|
||||
modelQ = modelQ.left(modelQ.size() - 9);
|
||||
|
||||
AircraftInformation *aircraft = new AircraftInformation();
|
||||
aircraft->m_icao = icao;
|
||||
aircraft->m_registration = QString(registration);
|
||||
aircraft->m_manufacturerName = QString(manufacturerName);
|
||||
aircraft->m_model = modelQ;
|
||||
aircraft->m_owner = QString(owner);
|
||||
aircraft->m_operator = QString(operatorName);
|
||||
aircraft->m_operatorICAO = QString(operatorICAO);
|
||||
aircraft->m_registered = QString(registered);
|
||||
aircraftInfo->insert(icao, aircraft);
|
||||
cnt++;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
fclose(file);
|
||||
}
|
||||
else
|
||||
qDebug() << "AircraftInformation::readOSNDB: Failed to open " << filename;
|
||||
|
||||
qDebug() << "AircraftInformation::readOSNDB: Read " << cnt << " aircraft";
|
||||
|
||||
return aircraftInfo;
|
||||
}
|
||||
|
||||
// Create hash table using registration as key
|
||||
static QHash<QString, AircraftInformation *> *registrationHash(const QHash<int, AircraftInformation *> *in)
|
||||
{
|
||||
QHash<QString, AircraftInformation *> *out = new QHash<QString, AircraftInformation *>();
|
||||
QHashIterator<int, AircraftInformation *> i(*in);
|
||||
while (i.hasNext())
|
||||
{
|
||||
i.next();
|
||||
AircraftInformation *info = i.value();
|
||||
out->insert(info->m_registration, info);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Write a reduced size and validated version of the DB, so it loads quicker
|
||||
static bool writeFastDB(const QString &filename, QHash<int, AircraftInformation *> *aircraftInfo)
|
||||
{
|
||||
QFile file(filename);
|
||||
if (file.open(QIODevice::WriteOnly))
|
||||
{
|
||||
file.write("icao24,registration,manufacturername,model,owner,operator,operatoricao,registered\n");
|
||||
QHash<int, AircraftInformation *>::iterator i = aircraftInfo->begin();
|
||||
while (i != aircraftInfo->end())
|
||||
{
|
||||
AircraftInformation *info = i.value();
|
||||
file.write(QString("%1").arg(info->m_icao, 1, 16).toUtf8());
|
||||
file.write(",");
|
||||
file.write(info->m_registration.toUtf8());
|
||||
file.write(",");
|
||||
file.write(info->m_manufacturerName.toUtf8());
|
||||
file.write(",");
|
||||
file.write(info->m_model.toUtf8());
|
||||
file.write(",");
|
||||
file.write(info->m_owner.toUtf8());
|
||||
file.write(",");
|
||||
file.write(info->m_operator.toUtf8());
|
||||
file.write(",");
|
||||
file.write(info->m_operatorICAO.toUtf8());
|
||||
file.write(",");
|
||||
file.write(info->m_registered.toUtf8());
|
||||
file.write("\n");
|
||||
++i;
|
||||
}
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
qCritical() << "AircraftInformation::writeFastDB failed to open " << filename << " for writing: " << file.errorString();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Read smaller CSV file with no validation. Takes about 0.5s instead of 2s.
|
||||
static QHash<int, AircraftInformation *> *readFastDB(const QString &filename)
|
||||
{
|
||||
int cnt = 0;
|
||||
QHash<int, AircraftInformation *> *aircraftInfo = nullptr;
|
||||
|
||||
qDebug() << "AircraftInformation::readFastDB: " << filename;
|
||||
|
||||
FILE *file;
|
||||
QByteArray utfFilename = filename.toUtf8();
|
||||
if ((file = fopen(utfFilename.constData(), "r")) != NULL)
|
||||
{
|
||||
char row[2048];
|
||||
|
||||
if (fgets(row, sizeof(row), file))
|
||||
{
|
||||
// Check header
|
||||
if (!strcmp(row, "icao24,registration,manufacturername,model,owner,operator,operatoricao,registered\n"))
|
||||
{
|
||||
aircraftInfo = new QHash<int, AircraftInformation *>();
|
||||
aircraftInfo->reserve(500000);
|
||||
// Read data
|
||||
while (fgets(row, sizeof(row), file))
|
||||
{
|
||||
char *p = row;
|
||||
AircraftInformation *aircraft = new AircraftInformation();
|
||||
char *icaoString = csvNext(&p);
|
||||
int icao = strtol(icaoString, NULL, 16);
|
||||
aircraft->m_icao = icao;
|
||||
aircraft->m_registration = QString(csvNext(&p));
|
||||
aircraft->m_manufacturerName = QString(csvNext(&p));
|
||||
aircraft->m_model = QString(csvNext(&p));
|
||||
aircraft->m_owner = QString(csvNext(&p));
|
||||
aircraft->m_operator = QString(csvNext(&p));
|
||||
aircraft->m_operatorICAO = QString(csvNext(&p));
|
||||
aircraft->m_registered = QString(csvNext(&p));
|
||||
aircraftInfo->insert(icao, aircraft);
|
||||
cnt++;
|
||||
}
|
||||
}
|
||||
else
|
||||
qDebug() << "AircraftInformation::readFastDB: Unexpected header";
|
||||
}
|
||||
else
|
||||
qDebug() << "AircraftInformation::readFastDB: Empty file";
|
||||
fclose(file);
|
||||
}
|
||||
else
|
||||
qDebug() << "AircraftInformation::readFastDB: Failed to open " << filename;
|
||||
|
||||
qDebug() << "AircraftInformation::readFastDB - read " << cnt << " aircraft";
|
||||
|
||||
return aircraftInfo;
|
||||
}
|
||||
|
||||
// Get flag based on registration
|
||||
QString getFlag() const
|
||||
{
|
||||
QString flag;
|
||||
if (m_prefixMap)
|
||||
{
|
||||
int idx = m_registration.indexOf('-');
|
||||
if (idx >= 0)
|
||||
{
|
||||
QString prefix;
|
||||
|
||||
// Some countries use AA-A - try these first as first letters are common
|
||||
prefix = m_registration.left(idx + 2);
|
||||
if (m_prefixMap->contains(prefix))
|
||||
{
|
||||
flag = m_prefixMap->value(prefix);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try letters before '-'
|
||||
prefix = m_registration.left(idx);
|
||||
if (m_prefixMap->contains(prefix)) {
|
||||
flag = m_prefixMap->value(prefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No '-' Could be one of a few countries or military.
|
||||
// See: https://en.wikipedia.org/wiki/List_of_aircraft_registration_prefixes
|
||||
if (m_registration.startsWith("N")) {
|
||||
flag = m_prefixMap->value("N"); // US
|
||||
} else if (m_registration.startsWith("JA")) {
|
||||
flag = m_prefixMap->value("JA"); // Japan
|
||||
} else if (m_registration.startsWith("HL")) {
|
||||
flag = m_prefixMap->value("HL"); // Korea
|
||||
} else if (m_registration.startsWith("YV")) {
|
||||
flag = m_prefixMap->value("YV"); // Venezuela
|
||||
} else if ((m_militaryMap != nullptr) && (m_militaryMap->contains(m_operator))) {
|
||||
flag = m_militaryMap->value(m_operator);
|
||||
}
|
||||
}
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
static QSharedPointer<const QHash<int, AircraftInformation *>> getAircraftInformation();
|
||||
static QSharedPointer<const QHash<QString, AircraftInformation *>> getAircraftInformationByReg();
|
||||
|
||||
static QString getOSNDBZipFilename()
|
||||
{
|
||||
@ -405,11 +107,6 @@ struct SDRBASE_API AircraftInformation {
|
||||
return getDataDir() + "/aircraftDatabase.csv";
|
||||
}
|
||||
|
||||
static QString getFastDBFilename()
|
||||
{
|
||||
return getDataDir() + "/aircraftDatabaseFast.csv";
|
||||
}
|
||||
|
||||
static QString getDataDir()
|
||||
{
|
||||
// Get directory to store app data in (aircraft & airport databases and user-definable icons)
|
||||
@ -418,100 +115,44 @@ struct SDRBASE_API AircraftInformation {
|
||||
return locations[0];
|
||||
}
|
||||
|
||||
static QString getAirlineIconPath(const QString &operatorICAO)
|
||||
private:
|
||||
HttpDownloadManager m_dlm;
|
||||
|
||||
static QSharedPointer<const QHash<int, AircraftInformation *>> m_aircraftInformation;
|
||||
static QSharedPointer<const QHash<QString, AircraftInformation *>> m_aircraftInformationByReg;
|
||||
static QDateTime m_modifiedDateTime;
|
||||
|
||||
// Write a reduced size and validated version of the DB, so it loads quicker
|
||||
static bool writeFastDB(const QString &filename, const QHash<int, AircraftInformation *> *aircraftInfo);
|
||||
|
||||
// Read smaller CSV file with no validation. Takes about 0.5s instead of 2s.
|
||||
static QHash<int, AircraftInformation *> *readFastDB(const QString &filename);
|
||||
|
||||
// Read OpenSky Network CSV file
|
||||
// This is large and contains lots of data we don't want, so we convert to
|
||||
// a smaller version to speed up loading time
|
||||
// Note that we use C file functions rather than QT, as these are ~30% faster
|
||||
// and the QT version seemed to occasionally crash
|
||||
static QHash<int, AircraftInformation *> *readOSNDB(const QString &filename);
|
||||
|
||||
static QString getFastDBFilename()
|
||||
{
|
||||
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())
|
||||
{
|
||||
return userIconPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try in resources
|
||||
QString resourceIconPath = ":" + endPath;
|
||||
QResource resource(resourceIconPath);
|
||||
if (resource.isValid())
|
||||
{
|
||||
return resourceIconPath;
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
return getDataDir() + "/aircraftDatabaseFast.csv";
|
||||
}
|
||||
|
||||
// Try to find an airline logo based on ICAO
|
||||
static QIcon *getAirlineIcon(const QString &operatorICAO)
|
||||
{
|
||||
if (m_airlineIcons.contains(operatorICAO))
|
||||
{
|
||||
return m_airlineIcons.value(operatorICAO);
|
||||
}
|
||||
else
|
||||
{
|
||||
QIcon *icon = nullptr;
|
||||
QString path = getAirlineIconPath(operatorICAO);
|
||||
if (!path.isEmpty())
|
||||
{
|
||||
icon = new QIcon(path);
|
||||
m_airlineIcons.insert(operatorICAO, icon);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!m_airlineMissingIcons.contains(operatorICAO))
|
||||
{
|
||||
qDebug() << "ADSBDemodGUI: No airline logo for " << operatorICAO;
|
||||
m_airlineMissingIcons.insert(operatorICAO, true);
|
||||
}
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
// Create hash table using registration as key
|
||||
static QHash<QString, AircraftInformation *> *registrationHash(const QHash<int, AircraftInformation *> *in);
|
||||
|
||||
static QString getFlagIconPath(const QString &country)
|
||||
{
|
||||
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())
|
||||
{
|
||||
return userIconPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try in resources
|
||||
QString resourceIconPath = ":" + endPath;
|
||||
QResource resource(resourceIconPath);
|
||||
if (resource.isValid())
|
||||
{
|
||||
return resourceIconPath;
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
private slots:
|
||||
void downloadFinished(const QString& filename, bool success);
|
||||
|
||||
// Try to find an flag logo based on a country
|
||||
static QIcon *getFlagIcon(const QString &country)
|
||||
{
|
||||
if (m_flagIcons.contains(country))
|
||||
{
|
||||
return m_flagIcons.value(country);
|
||||
}
|
||||
else
|
||||
{
|
||||
QIcon *icon = nullptr;
|
||||
QString path = getFlagIconPath(country);
|
||||
if (!path.isEmpty())
|
||||
{
|
||||
icon = new QIcon(path);
|
||||
m_flagIcons.insert(country, icon);
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
signals:
|
||||
void downloadingURL(const QString& url);
|
||||
void downloadError(const QString& error);
|
||||
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
|
||||
void downloadAircraftInformationFinished();
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
|
419
sdrbase/util/ourairportsdb.cpp
Normal file
@ -0,0 +1,419 @@
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// 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 <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QByteArray>
|
||||
#include <QList>
|
||||
#include <QDebug>
|
||||
#include <QLocale>
|
||||
#include <QStandardPaths>
|
||||
|
||||
#include "util/ourairportsdb.h"
|
||||
|
||||
QMutex OurAirportsDB::m_mutex;
|
||||
QSharedPointer<QHash<int, AirportInformation *>> OurAirportsDB::m_airportsById;
|
||||
QSharedPointer<QHash<QString, AirportInformation *>> OurAirportsDB::m_airportsByIdent;
|
||||
QDateTime OurAirportsDB::m_modifiedDateTime;
|
||||
|
||||
AirportInformation::~AirportInformation()
|
||||
{
|
||||
qDeleteAll(m_frequencies);
|
||||
}
|
||||
|
||||
QString AirportInformation::getImageName() const
|
||||
{
|
||||
switch (m_type)
|
||||
{
|
||||
case AirportType::Large:
|
||||
return "airport_large.png";
|
||||
case AirportType::Medium:
|
||||
return "airport_medium.png";
|
||||
case AirportType::Heliport:
|
||||
return "heliport.png";
|
||||
default:
|
||||
return "airport_small.png";
|
||||
}
|
||||
}
|
||||
|
||||
OurAirportsDB::OurAirportsDB(QObject *parent) :
|
||||
QObject(parent)
|
||||
{
|
||||
connect(&m_dlm, &HttpDownloadManager::downloadComplete, this, &OurAirportsDB::downloadFinished);
|
||||
}
|
||||
|
||||
OurAirportsDB::~OurAirportsDB()
|
||||
{
|
||||
disconnect(&m_dlm, &HttpDownloadManager::downloadComplete, this, &OurAirportsDB::downloadFinished);
|
||||
}
|
||||
|
||||
void OurAirportsDB::downloadAirportInformation()
|
||||
{
|
||||
// Download airport database
|
||||
QString urlString = AIRPORTS_URL;
|
||||
QUrl dbURL(urlString);
|
||||
qDebug() << "OurAirportsDB::downloadAirportInformation: Downloading " << urlString;
|
||||
emit downloadingURL(urlString);
|
||||
QNetworkReply *reply = m_dlm.download(dbURL, OurAirportsDB::getAirportDBFilename());
|
||||
connect(reply, &QNetworkReply::downloadProgress, this, [this](qint64 bytesRead, qint64 totalBytes) {
|
||||
emit downloadProgress(bytesRead, totalBytes);
|
||||
});
|
||||
}
|
||||
|
||||
void OurAirportsDB::downloadFinished(const QString& filename, bool success)
|
||||
{
|
||||
if (!success)
|
||||
{
|
||||
qWarning() << "OurAirportsDB::downloadFinished: Failed to download: " << filename;
|
||||
emit downloadError(QString("Failed to download: %1").arg(filename));
|
||||
}
|
||||
else if (filename == OurAirportsDB::getAirportDBFilename())
|
||||
{
|
||||
// Now download airport frequencies
|
||||
QString urlString = AIRPORT_FREQUENCIES_URL;
|
||||
QUrl dbURL(urlString);
|
||||
qDebug() << "OurAirportsDB::downloadFinished: Downloading " << urlString;
|
||||
emit downloadingURL(urlString);
|
||||
QNetworkReply *reply = m_dlm.download(dbURL, OurAirportsDB::getAirportFrequenciesDBFilename());
|
||||
connect(reply, &QNetworkReply::downloadProgress, this, [this](qint64 bytesRead, qint64 totalBytes) {
|
||||
emit downloadProgress(bytesRead, totalBytes);
|
||||
});
|
||||
}
|
||||
else if (filename == OurAirportsDB::getAirportFrequenciesDBFilename())
|
||||
{
|
||||
emit downloadAirportInformationFinished();
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "OurAirportsDB::downloadFinished: Unexpected filename: " << filename;
|
||||
emit downloadError(QString("Unexpected filename: %1").arg(filename));
|
||||
}
|
||||
}
|
||||
|
||||
QSharedPointer<const QHash<int, AirportInformation *>> OurAirportsDB::getAirportsById()
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
|
||||
readDB();
|
||||
return m_airportsById;
|
||||
}
|
||||
|
||||
QSharedPointer<const QHash<QString, AirportInformation *>> OurAirportsDB::getAirportsByIdent()
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
|
||||
readDB();
|
||||
return m_airportsByIdent;
|
||||
}
|
||||
|
||||
void OurAirportsDB::readDB()
|
||||
{
|
||||
QFileInfo airportDBFileInfo(getAirportDBFilename());
|
||||
QDateTime airportDBModifiedDateTime = airportDBFileInfo.lastModified();
|
||||
|
||||
if (!m_airportsById || (airportDBModifiedDateTime > m_modifiedDateTime))
|
||||
{
|
||||
// Using shared pointer, so old object, if it exists, will be deleted, when no longer user
|
||||
m_airportsById = QSharedPointer<QHash<int, AirportInformation *>>(OurAirportsDB::readAirportsDB(getAirportDBFilename()));
|
||||
if (m_airportsById != nullptr)
|
||||
{
|
||||
OurAirportsDB::readFrequenciesDB(getAirportFrequenciesDBFilename(), m_airportsById.get());
|
||||
m_airportsByIdent = QSharedPointer<QHash<QString, AirportInformation *>>(identHash(m_airportsById.get()));
|
||||
}
|
||||
|
||||
m_modifiedDateTime = airportDBModifiedDateTime;
|
||||
}
|
||||
}
|
||||
|
||||
QString OurAirportsDB::getDataDir()
|
||||
{
|
||||
// Get directory to store app data in
|
||||
QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
|
||||
// First dir is writable
|
||||
return locations[0];
|
||||
}
|
||||
|
||||
QString OurAirportsDB::getAirportDBFilename()
|
||||
{
|
||||
return getDataDir() + "/airportDatabase.csv";
|
||||
}
|
||||
|
||||
QString OurAirportsDB::getAirportFrequenciesDBFilename()
|
||||
{
|
||||
return getDataDir() + "/airportFrequenciesDatabase.csv";
|
||||
}
|
||||
|
||||
QString OurAirportsDB::trimQuotes(const QString s)
|
||||
{
|
||||
if (s.startsWith('\"') && s.endsWith('\"')) {
|
||||
return s.mid(1, s.size() - 2);
|
||||
} else {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
// Read OurAirport's airport CSV file
|
||||
// See comments for readOSNDB
|
||||
QHash<int, AirportInformation *> *OurAirportsDB::readAirportsDB(const QString &filename)
|
||||
{
|
||||
int cnt = 0;
|
||||
QHash<int, AirportInformation *> *airportInfo = nullptr;
|
||||
|
||||
// Column numbers used for the data as of 2020/10/28
|
||||
int idCol = 0;
|
||||
int identCol = 1;
|
||||
int typeCol = 2;
|
||||
int nameCol = 3;
|
||||
int latitudeCol = 4;
|
||||
int longitudeCol = 5;
|
||||
int elevationCol = 6;
|
||||
|
||||
qDebug() << "OurAirportsDB::readAirportsDB: " << filename;
|
||||
|
||||
FILE *file;
|
||||
QByteArray utfFilename = filename.toUtf8();
|
||||
QLocale cLocale(QLocale::C);
|
||||
if ((file = fopen(utfFilename.constData(), "r")) != NULL)
|
||||
{
|
||||
char row[2048];
|
||||
|
||||
if (fgets(row, sizeof(row), file))
|
||||
{
|
||||
airportInfo = new QHash<int, AirportInformation *>();
|
||||
airportInfo->reserve(70000);
|
||||
|
||||
// Read header
|
||||
int idx = 0;
|
||||
char *p = strtok(row, ",");
|
||||
while (p != NULL)
|
||||
{
|
||||
if (!strcmp(p, "id"))
|
||||
idCol = idx;
|
||||
else if (!strcmp(p, "ident"))
|
||||
identCol = idx;
|
||||
else if (!strcmp(p, "type"))
|
||||
typeCol = idx;
|
||||
else if (!strcmp(p, "name"))
|
||||
nameCol = idx;
|
||||
else if (!strcmp(p, "latitude_deg"))
|
||||
latitudeCol = idx;
|
||||
else if (!strcmp(p, "longitude_deg"))
|
||||
longitudeCol = idx;
|
||||
else if (!strcmp(p, "elevation_ft"))
|
||||
elevationCol = idx;
|
||||
p = strtok(NULL, ",");
|
||||
idx++;
|
||||
}
|
||||
// Read data
|
||||
while (fgets(row, sizeof(row), file))
|
||||
{
|
||||
int id = 0;
|
||||
char *idString = NULL;
|
||||
char *ident = NULL;
|
||||
size_t identLen = 0;
|
||||
char *type = NULL;
|
||||
size_t typeLen = 0;
|
||||
char *name = NULL;
|
||||
size_t nameLen = 0;
|
||||
float latitude = 0.0f;
|
||||
char *latitudeString = NULL;
|
||||
size_t latitudeLen = 0;
|
||||
float longitude = 0.0f;
|
||||
char *longitudeString = NULL;
|
||||
size_t longitudeLen = 0;
|
||||
float elevation = 0.0f;
|
||||
char *elevationString = NULL;
|
||||
size_t elevationLen = 0;
|
||||
|
||||
p = strtok(row, ",");
|
||||
idx = 0;
|
||||
while (p != NULL)
|
||||
{
|
||||
// Read strings, stripping quotes
|
||||
if (idx == idCol)
|
||||
{
|
||||
idString = p;
|
||||
idString[strlen(idString)] = '\0';
|
||||
id = strtol(idString, NULL, 10);
|
||||
}
|
||||
else if (idx == identCol)
|
||||
{
|
||||
ident = p+1;
|
||||
identLen = strlen(ident)-1;
|
||||
ident[identLen] = '\0';
|
||||
}
|
||||
else if (idx == typeCol)
|
||||
{
|
||||
type = p+1;
|
||||
typeLen = strlen(type)-1;
|
||||
type[typeLen] = '\0';
|
||||
}
|
||||
else if (idx == nameCol)
|
||||
{
|
||||
name = p+1;
|
||||
nameLen = strlen(name)-1;
|
||||
name[nameLen] = '\0';
|
||||
}
|
||||
else if (idx == latitudeCol)
|
||||
{
|
||||
latitudeString = p;
|
||||
latitudeLen = strlen(latitudeString)-1;
|
||||
latitudeString[latitudeLen] = '\0';
|
||||
latitude = cLocale.toFloat(latitudeString);
|
||||
}
|
||||
else if (idx == longitudeCol)
|
||||
{
|
||||
longitudeString = p;
|
||||
longitudeLen = strlen(longitudeString)-1;
|
||||
longitudeString[longitudeLen] = '\0';
|
||||
longitude = cLocale.toFloat(longitudeString);
|
||||
}
|
||||
else if (idx == elevationCol)
|
||||
{
|
||||
elevationString = p;
|
||||
elevationLen = strlen(elevationString)-1;
|
||||
elevationString[elevationLen] = '\0';
|
||||
elevation = cLocale.toFloat(elevationString);
|
||||
}
|
||||
p = strtok(NULL, ",");
|
||||
idx++;
|
||||
}
|
||||
|
||||
// Only create the entry if we have some interesting data
|
||||
if (((latitude != 0.0f) || (longitude != 0.0f)) && (type && strcmp(type, "closed")))
|
||||
{
|
||||
AirportInformation *airport = new AirportInformation();
|
||||
airport->m_id = id;
|
||||
airport->m_ident = QString(ident);
|
||||
if (!strcmp(type, "small_airport")) {
|
||||
airport->m_type = AirportInformation::AirportType::Small;
|
||||
} else if (!strcmp(type, "medium_airport")) {
|
||||
airport->m_type = AirportInformation::AirportType::Medium;
|
||||
} else if (!strcmp(type, "large_airport")) {
|
||||
airport->m_type = AirportInformation::AirportType::Large;
|
||||
} else if (!strcmp(type, "heliport")) {
|
||||
airport->m_type = AirportInformation::AirportType::Heliport;
|
||||
}
|
||||
airport->m_name = QString(name);
|
||||
airport->m_latitude = latitude;
|
||||
airport->m_longitude = longitude;
|
||||
airport->m_elevation = elevation;
|
||||
airportInfo->insert(id, airport);
|
||||
cnt++;
|
||||
}
|
||||
}
|
||||
}
|
||||
fclose(file);
|
||||
}
|
||||
else
|
||||
qDebug() << "OurAirportsDB::readAirportsDB: Failed to open " << filename;
|
||||
|
||||
qDebug() << "OurAirportsDB::readAirportsDB: Read " << cnt << " airports";
|
||||
|
||||
return airportInfo;
|
||||
}
|
||||
|
||||
// Create hash table using ICAO identifier as key
|
||||
QHash<QString, AirportInformation *> *OurAirportsDB::identHash(QHash<int, AirportInformation *> *in)
|
||||
{
|
||||
QHash<QString, AirportInformation *> *out = new QHash<QString, AirportInformation *>();
|
||||
QHashIterator<int, AirportInformation *> i(*in);
|
||||
while (i.hasNext())
|
||||
{
|
||||
i.next();
|
||||
AirportInformation *info = i.value();
|
||||
out->insert(info->m_ident, info);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Read OurAirport's airport frequencies CSV file
|
||||
bool OurAirportsDB::readFrequenciesDB(const QString &filename, QHash<int, AirportInformation *> *airportInfo)
|
||||
{
|
||||
int cnt = 0;
|
||||
|
||||
// Column numbers used for the data as of 2020/10/28
|
||||
int airportRefCol = 1;
|
||||
int typeCol = 3;
|
||||
int descriptionCol = 4;
|
||||
int frequencyCol = 5;
|
||||
|
||||
qDebug() << "OurAirportsDB::readFrequenciesDB: " << filename;
|
||||
|
||||
QFile file(filename);
|
||||
if (file.open(QIODevice::ReadOnly))
|
||||
{
|
||||
QList<QByteArray> colNames;
|
||||
int idx;
|
||||
|
||||
// Read header
|
||||
if (!file.atEnd())
|
||||
{
|
||||
QByteArray row = file.readLine().trimmed();
|
||||
colNames = row.split(',');
|
||||
// Work out which columns the data is in, based on the headers
|
||||
idx = colNames.indexOf("airport_ref");
|
||||
if (idx >= 0)
|
||||
airportRefCol = idx;
|
||||
idx = colNames.indexOf("type");
|
||||
if (idx >= 0)
|
||||
typeCol = idx;
|
||||
idx = colNames.indexOf("descrption");
|
||||
if (idx >= 0)
|
||||
descriptionCol = idx;
|
||||
idx = colNames.indexOf("frequency_mhz");
|
||||
if (idx >= 0)
|
||||
frequencyCol = idx;
|
||||
}
|
||||
// Read data
|
||||
while (!file.atEnd())
|
||||
{
|
||||
QByteArray row = file.readLine();
|
||||
QList<QByteArray> cols = row.split(',');
|
||||
|
||||
bool ok = false;
|
||||
int airportRef = cols[airportRefCol].toInt(&ok, 10);
|
||||
if (ok)
|
||||
{
|
||||
if (airportInfo->contains(airportRef))
|
||||
{
|
||||
QString type = trimQuotes(cols[typeCol]);
|
||||
QString description = trimQuotes(cols[descriptionCol]);
|
||||
float frequency = cols[frequencyCol].toFloat();
|
||||
|
||||
AirportInformation::FrequencyInformation *frequencyInfo = new AirportInformation::FrequencyInformation();
|
||||
frequencyInfo->m_type = type;
|
||||
frequencyInfo->m_description = description;
|
||||
frequencyInfo->m_frequency = frequency;
|
||||
airportInfo->value(airportRef)->m_frequencies.append(frequencyInfo);
|
||||
cnt++;
|
||||
}
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Failed to open " << filename << " " << file.errorString();
|
||||
return false;
|
||||
}
|
||||
|
||||
qDebug() << "OurAirportsDB::readFrequenciesDB: - read " << cnt << " airports";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
119
sdrbase/util/ourairportsdb.h
Normal file
@ -0,0 +1,119 @@
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// 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/>. //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef INCLUDE_UTIL_OURAIRPORTSDB_H
|
||||
#define INCLUDE_UTIL_OURAIRPORTSDB_H
|
||||
|
||||
#include <QString>
|
||||
#include <QMutex>
|
||||
#include <QDateTime>
|
||||
#include <QHash>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "util/csv.h"
|
||||
#include "util/httpdownloadmanager.h"
|
||||
#include "export.h"
|
||||
|
||||
#define AIRPORTS_URL "https://davidmegginson.github.io/ourairports-data/airports.csv"
|
||||
#define AIRPORT_FREQUENCIES_URL "https://davidmegginson.github.io/ourairports-data/airport-frequencies.csv"
|
||||
|
||||
class SDRBASE_API AirportInformation {
|
||||
|
||||
public:
|
||||
enum AirportType {
|
||||
Small,
|
||||
Medium,
|
||||
Large,
|
||||
Heliport
|
||||
};
|
||||
|
||||
struct FrequencyInformation {
|
||||
QString m_type;
|
||||
QString m_description;
|
||||
float m_frequency; // In MHz
|
||||
};
|
||||
|
||||
int m_id;
|
||||
QString m_ident;
|
||||
AirportType m_type;
|
||||
QString m_name;
|
||||
float m_latitude;
|
||||
float m_longitude;
|
||||
float m_elevation;
|
||||
QVector<FrequencyInformation *> m_frequencies;
|
||||
|
||||
~AirportInformation();
|
||||
QString getImageName() const;
|
||||
|
||||
};
|
||||
|
||||
class SDRBASE_API OurAirportsDB : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
OurAirportsDB(QObject *parent=nullptr);
|
||||
~OurAirportsDB();
|
||||
|
||||
void downloadAirportInformation();
|
||||
|
||||
static QSharedPointer<const QHash<int, AirportInformation *>> getAirportsById();
|
||||
static QSharedPointer<const QHash<QString, AirportInformation *>> getAirportsByIdent();
|
||||
|
||||
private:
|
||||
HttpDownloadManager m_dlm;
|
||||
|
||||
static QSharedPointer<QHash<int, AirportInformation *>> m_airportsById;
|
||||
static QSharedPointer<QHash<QString, AirportInformation *>> m_airportsByIdent;
|
||||
static QDateTime m_modifiedDateTime;
|
||||
|
||||
static QMutex m_mutex;
|
||||
|
||||
static QString getDataDir();
|
||||
|
||||
static void readDB();
|
||||
|
||||
// Read OurAirport's airport CSV file
|
||||
// See comments for readOSNDB
|
||||
static QHash<int, AirportInformation *> *readAirportsDB(const QString &filename);
|
||||
|
||||
// Create hash table using ICAO identifier as key
|
||||
static QHash<QString, AirportInformation *> *identHash(QHash<int, AirportInformation *> *in);
|
||||
|
||||
// Read OurAirport's airport frequencies CSV file
|
||||
static bool readFrequenciesDB(const QString &filename, QHash<int, AirportInformation *> *airportInfo);
|
||||
|
||||
static QString trimQuotes(const QString s);
|
||||
|
||||
static QString getAirportDBFilename();
|
||||
static QString getAirportFrequenciesDBFilename();
|
||||
|
||||
private slots:
|
||||
void downloadFinished(const QString& filename, bool success);
|
||||
|
||||
signals:
|
||||
void downloadingURL(const QString& url);
|
||||
void downloadError(const QString& error);
|
||||
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
|
||||
void downloadAirportInformationFinished();
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
|
@ -4528,6 +4528,11 @@ bool WebAPIRequestMapper::getChannelSettings(
|
||||
channelSettings->setFt8DemodSettings(new SWGSDRangel::SWGFT8DemodSettings());
|
||||
channelSettings->getFt8DemodSettings()->fromJsonObject(settingsJsonObject);
|
||||
}
|
||||
else if (channelSettingsKey == "HeatMapSettings")
|
||||
{
|
||||
channelSettings->setHeatMapSettings(new SWGSDRangel::SWGHeatMapSettings());
|
||||
channelSettings->getHeatMapSettings()->fromJsonObject(settingsJsonObject);
|
||||
}
|
||||
else if (channelSettingsKey == "IEEE_802_15_4_ModSettings")
|
||||
{
|
||||
channelSettings->setIeee802154ModSettings(new SWGSDRangel::SWGIEEE_802_15_4_ModSettings());
|
||||
@ -5375,6 +5380,7 @@ void WebAPIRequestMapper::resetChannelSettings(SWGSDRangel::SWGChannelSettings&
|
||||
channelSettings.setDatvModSettings(nullptr);
|
||||
channelSettings.setDabDemodSettings(nullptr);
|
||||
channelSettings.setDsdDemodSettings(nullptr);
|
||||
channelSettings.setHeatMapSettings(nullptr);
|
||||
channelSettings.setIeee802154ModSettings(nullptr);
|
||||
channelSettings.setNfmDemodSettings(nullptr);
|
||||
channelSettings.setNfmModSettings(nullptr);
|
||||
@ -5411,6 +5417,7 @@ void WebAPIRequestMapper::resetChannelReport(SWGSDRangel::SWGChannelReport& chan
|
||||
channelReport.setBfmDemodReport(nullptr);
|
||||
channelReport.setDatvModReport(nullptr);
|
||||
channelReport.setDsdDemodReport(nullptr);
|
||||
channelReport.setHeatMapReport(nullptr);
|
||||
channelReport.setNfmDemodReport(nullptr);
|
||||
channelReport.setNfmModReport(nullptr);
|
||||
channelReport.setNoiseFigureReport(nullptr);
|
||||
|