diff --git a/doc/img/SID_plugin.jpg b/doc/img/SID_plugin.jpg index 7ce282b74..adeb0c088 100644 Binary files a/doc/img/SID_plugin.jpg and b/doc/img/SID_plugin.jpg differ diff --git a/doc/img/SID_plugin_paths.png b/doc/img/SID_plugin_paths.png new file mode 100644 index 000000000..004f1b6fd Binary files /dev/null and b/doc/img/SID_plugin_paths.png differ diff --git a/doc/img/SID_plugin_settings_dialog.jpg b/doc/img/SID_plugin_settings_dialog.png similarity index 99% rename from doc/img/SID_plugin_settings_dialog.jpg rename to doc/img/SID_plugin_settings_dialog.png index dad6af8dc..18e321021 100644 Binary files a/doc/img/SID_plugin_settings_dialog.jpg and b/doc/img/SID_plugin_settings_dialog.png differ diff --git a/plugins/feature/sid/readme.md b/plugins/feature/sid/readme.md index 8a7d7759e..00a0219d7 100644 --- a/plugins/feature/sid/readme.md +++ b/plugins/feature/sid/readme.md @@ -228,6 +228,18 @@ Specifies the date and time for which SDO imagery should be displayed. Images ar Select a Map to link to the SID feature. When a time is selected on the SID charts, the [Map](../../feature/map/readme.md) feature will have it's time set accordingly. This allows you, for example, to see the corresponding impact on MUF/foF2 displayed on the 3D map. +

Show Paths on Map

+ +When clicked, shows the great circle paths between transmitters and receivers on a [Map](../../feature/map/readme.md). + +![SID paths](../../../doc/img/SID_plugin_paths.png) + +The positions of the transmitters are taken from the Map's VLF database. The position of the receiver is for most devices taken from Preferences > My Position. +For KiwiSDRs, the position is taken from the GPS position indicated by the device. + +In order to match a transmitter in the Map's VLF database, the label used in the SID chart must match the transmitter's name. It is possible to add user-defined VLF transmitters via +a `vlftransmitters.csv` file. See the [Map](../../feature/map/readme.md) documentation. +

Tips

In order to check that a peak in the spectrum is a real VLF signal, you can: diff --git a/plugins/feature/sid/sidgui.cpp b/plugins/feature/sid/sidgui.cpp index aa15ec393..d8b04f698 100644 --- a/plugins/feature/sid/sidgui.cpp +++ b/plugins/feature/sid/sidgui.cpp @@ -33,12 +33,15 @@ #include "device/deviceuiset.h" #include "util/csv.h" #include "util/astronomy.h" +#include "util/vlftransmitters.h" #include "ui_sidgui.h" #include "sid.h" #include "sidgui.h" #include "sidsettingsdialog.h" +#include "SWGMapItem.h" + SIDGUI* SIDGUI::create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature) { SIDGUI* gui = new SIDGUI(pluginAPI, featureUISet, feature); @@ -1521,6 +1524,7 @@ void SIDGUI::makeUIConnections() QObject::connect(ui->sdoDateTime, &WrappingDateTimeEdit::dateTimeChanged, this, &SIDGUI::on_sdoDateTime_dateTimeChanged); QObject::connect(ui->showSats, &QToolButton::clicked, this, &SIDGUI::on_showSats_clicked); QObject::connect(ui->map, &QComboBox::currentTextChanged, this, &SIDGUI::on_map_currentTextChanged); + QObject::connect(ui->showPaths, &QToolButton::clicked, this, &SIDGUI::on_showPaths_clicked); QObject::connect(ui->autoscaleX, &QPushButton::clicked, this, &SIDGUI::on_autoscaleX_clicked); QObject::connect(ui->autoscaleY, &QPushButton::clicked, this, &SIDGUI::on_autoscaleY_clicked); QObject::connect(ui->today, &QPushButton::clicked, this, &SIDGUI::on_today_clicked); @@ -1955,6 +1959,96 @@ void SIDGUI::on_map_currentTextChanged(const QString& text) applyDateTime(); } +// Plot paths from transmitters to receivers on map +void SIDGUI::on_showPaths_clicked() +{ + + for (int i = 0; i < m_settings.m_channelSettings.size(); i++) + { + unsigned int deviceSetIndex; + unsigned int channelIndex; + + if (MainCore::getDeviceAndChannelIndexFromId(m_settings.m_channelSettings[i].m_id, deviceSetIndex, channelIndex)) + { + // Get position of device, defaulting to My Position + QGeoCoordinate rxPosition; + if (!ChannelWebAPIUtils::getDevicePosition(deviceSetIndex, rxPosition)) + { + rxPosition.setLatitude(MainCore::instance()->getSettings().getLatitude()); + rxPosition.setLongitude(MainCore::instance()->getSettings().getLongitude()); + rxPosition.setAltitude(MainCore::instance()->getSettings().getAltitude()); + } + + // Get position of transmitter + if (VLFTransmitters::m_callsignHash.contains(m_settings.m_channelSettings[i].m_label)) + { + const VLFTransmitters::Transmitter *transmitter = VLFTransmitters::m_callsignHash.value(m_settings.m_channelSettings[i].m_label); + QGeoCoordinate txPosition; + txPosition.setLatitude(transmitter->m_latitude); + txPosition.setLongitude(transmitter->m_longitude); + txPosition.setAltitude(0); + + // Calculate mid point for position of label + qreal distance = txPosition.distanceTo(rxPosition); + qreal az = txPosition.azimuthTo(rxPosition); + QGeoCoordinate midPoint = txPosition.atDistanceAndAzimuth(distance / 2.0, az); + + // Create a path from transmitter to receiver + QList mapPipes; + MainCore::instance()->getMessagePipes().getMessagePipes(m_sid, "mapitems", mapPipes); + if (mapPipes.size() > 0) + { + for (const auto& pipe : mapPipes) + { + MessageQueue *messageQueue = qobject_cast(pipe->m_element); + SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); + + QString deviceId = QString("%1%2").arg(m_settings.m_channelSettings[i].m_id[0]).arg(deviceSetIndex); + + QString name = QString("SID %1 to %2").arg(m_settings.m_channelSettings[i].m_label).arg(deviceId); + QString details = QString("%1
Distance: %2 km").arg(name).arg((int) std::round(distance / 1000.0)); + + swgMapItem->setName(new QString(name)); + swgMapItem->setLatitude(midPoint.latitude()); + swgMapItem->setLongitude(midPoint.longitude()); + swgMapItem->setAltitude(midPoint.altitude()); + 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 *coords = new QList(); + + SWGSDRangel::SWGMapCoordinate* c = new SWGSDRangel::SWGMapCoordinate(); + c->setLatitude(rxPosition.latitude()); + c->setLongitude(rxPosition.longitude()); + c->setAltitude(rxPosition.altitude()); + coords->append(c); + + c = new SWGSDRangel::SWGMapCoordinate(); + c->setLatitude(txPosition.latitude()); + c->setLongitude(txPosition.longitude()); + c->setAltitude(txPosition.altitude()); + coords->append(c); + + swgMapItem->setColorValid(1); + swgMapItem->setColor(m_settings.m_channelSettings[i].m_color.rgba()); + + swgMapItem->setCoordinates(coords); + swgMapItem->setType(3); + + MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_sid, swgMapItem); + messageQueue->push(msg); + } + } + } + } + } + +} + void SIDGUI::featuresChanged(const QStringList& renameFrom, const QStringList& renameTo) { const AvailableChannelOrFeatureList availableFeatures = m_availableFeatureHandler.getAvailableChannelOrFeatureList(); diff --git a/plugins/feature/sid/sidgui.h b/plugins/feature/sid/sidgui.h index bf7d14af7..89e025971 100644 --- a/plugins/feature/sid/sidgui.h +++ b/plugins/feature/sid/sidgui.h @@ -305,6 +305,7 @@ private slots: void on_showSats_clicked(); void onSatTrackerAdded(int featureSetIndex, Feature *feature); void on_map_currentTextChanged(const QString& text); + void on_showPaths_clicked(); void featuresChanged(const QStringList& renameFrom, const QStringList& renameTo); void channelsChanged(const QStringList& renameFrom, const QStringList& renameTo, const QStringList& removed, const QStringList& added); void removeChannels(const QStringList& ids); diff --git a/plugins/feature/sid/sidgui.ui b/plugins/feature/sid/sidgui.ui index 8fcf77445..71a17c413 100644 --- a/plugins/feature/sid/sidgui.ui +++ b/plugins/feature/sid/sidgui.ui @@ -607,6 +607,20 @@ + + + + Show propagation paths on map + + + + + + + :/world.png:/world.png + + + @@ -725,17 +739,17 @@ + + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
RollupContents QWidget
gui/rollupcontents.h
1
- - ButtonSwitch - QToolButton -
gui/buttonswitch.h
-
QChartView QGraphicsView diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt index 4553695a8..8de90e62f 100644 --- a/sdrbase/CMakeLists.txt +++ b/sdrbase/CMakeLists.txt @@ -277,6 +277,7 @@ set(sdrbase_SOURCES util/units.cpp util/timeutil.cpp util/visa.cpp + util/vlftransmitters.cpp util/waypoints.cpp util/weather.cpp util/iot/device.cpp @@ -534,6 +535,7 @@ set(sdrbase_HEADERS util/units.h util/timeutil.h util/visa.h + util/vlftransmitters.h util/waypoints.h util/weather.h util/iot/device.h diff --git a/sdrbase/channel/channelwebapiutils.cpp b/sdrbase/channel/channelwebapiutils.cpp index ad706b2bd..42292d54c 100644 --- a/sdrbase/channel/channelwebapiutils.cpp +++ b/sdrbase/channel/channelwebapiutils.cpp @@ -1156,6 +1156,35 @@ bool ChannelWebAPIUtils::getDeviceReportList(unsigned int deviceIndex, const QSt return false; } + +bool ChannelWebAPIUtils::getDevicePosition(unsigned int deviceIndex, QGeoCoordinate& position) +{ + SWGSDRangel::SWGDeviceReport deviceReport; + + if (getDeviceReport(deviceIndex, deviceReport)) + { + QJsonObject *jsonObj = deviceReport.asJsonObject(); + double latitude, longitude, altitude; + + if (WebAPIUtils::getSubObjectDouble(*jsonObj, "latitude", latitude) + && WebAPIUtils::getSubObjectDouble(*jsonObj, "longitude", longitude) + && WebAPIUtils::getSubObjectDouble(*jsonObj, "altitude", altitude)) + { + position.setLatitude(latitude); + position.setLongitude(longitude); + position.setAltitude(altitude); + // Done + return true; + } + else + { + //qWarning("ChannelWebAPIUtils::getDevicePosition: no latitude/longitude/altitude in device report"); + return false; + } + } + return false; +} + bool ChannelWebAPIUtils::runFeature(unsigned int featureSetIndex, unsigned int featureIndex) { SWGSDRangel::SWGDeviceState runResponse; diff --git a/sdrbase/channel/channelwebapiutils.h b/sdrbase/channel/channelwebapiutils.h index 2b9815c31..8a9d25c4c 100644 --- a/sdrbase/channel/channelwebapiutils.h +++ b/sdrbase/channel/channelwebapiutils.h @@ -23,6 +23,7 @@ #include #include +#include #include "SWGDeviceSettings.h" #include "SWGDeviceReport.h" @@ -71,6 +72,7 @@ public: static bool getDeviceSetting(unsigned int deviceIndex, const QString &setting, int &value); static bool getDeviceReportValue(unsigned int deviceIndex, const QString &key, QString &value); static bool getDeviceReportList(unsigned int deviceIndex, const QString &key, const QString &subKey, QList &values); + static bool getDevicePosition(unsigned int deviceIndex, QGeoCoordinate& position); static bool patchDeviceSetting(unsigned int deviceIndex, const QString &setting, int value); static bool runFeature(unsigned int featureSetIndex, unsigned int featureIndex); static bool stopFeature(unsigned int featureSetIndex, unsigned int featureIndex); diff --git a/sdrbase/util/vlftransmitters.cpp b/sdrbase/util/vlftransmitters.cpp new file mode 100644 index 000000000..8b8b73549 --- /dev/null +++ b/sdrbase/util/vlftransmitters.cpp @@ -0,0 +1,116 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include + +#include "util/csv.h" + +#include "vlftransmitters.h" + +// https://sidstation.loudet.org/stations-list-en.xhtml +// https://core.ac.uk/download/pdf/224769021.pdf -- Table 1 +// GQD/GQZ callsigns: https://groups.io/g/VLF/message/19212?p=%2C%2C%2C20%2C0%2C0%2C0%3A%3Arecentpostdate%2Fsticky%2C%2C19.6%2C20%2C2%2C0%2C38924431 +QList VLFTransmitters::m_transmitters = { + {QStringLiteral("JXN"), 16400, 66.974353, 13.873617, -1}, // Novik, Norway (Only transmits 6 times a day) + {QStringLiteral("VTX2"), 17000, 8.387015, 77.752762, -1}, // South Vijayanarayanam, India + {QStringLiteral("RDL"), 18100, 44.773333, 39.547222, -1}, // Krasnodar, Russia (Transmits short bursts, possibly FSK) + {QStringLiteral("GQD"), 19580, 54.911643, -3.278456, 100}, // Anthorn, UK, Often referred to as GBZ + {QStringLiteral("NWC"), 19800, -21.816325, 114.16546, 1000}, // Exmouth, Aus + {QStringLiteral("ICV"), 20270, 40.922946, 9.731881, 50}, // Isola di Tavolara, Italy (Can be distorted on 3D map if terrain used) + {QStringLiteral("FTA"), 20900, 48.544632, 2.579429, 50}, // Sainte-Assise, France (Satellite imagary obfuscated) + {QStringLiteral("NPM"), 21400, 21.420166, -158.151140, 600}, // Pearl Harbour, Lualuahei, USA (Not seen?) + {QStringLiteral("HWU"), 21750, 46.713129, 1.245248, 200}, // Rosnay, France + {QStringLiteral("GQZ"), 22100, 54.731799, -2.883033, 100}, // Skelton, UK (GVT in paper) + {QStringLiteral("DHO38"), 23400, 53.078900, 7.615000, 300}, // Rhauderfehn, Germany - Off air 7-8 UTC + {QStringLiteral("NAA"), 24000, 44.644506, -67.284565, 1000}, // Cutler, Maine, USA + {QStringLiteral("TBB"), 26700, 37.412725, 27.323342, -1}, // Bafa, Turkey + {QStringLiteral("TFK/NRK"), 37500, 63.850365, -22.466773, 100}, // Grindavik, Iceland + {QStringLiteral("SRC"), 40400, 57.120328, 16.153083, -1}, // Grimeton, Sweden + {QStringLiteral("NSY"), 45900, 37.125660, 14.436416, -1}, // Niscemi, Italy + {QStringLiteral("SXA"), 49000, 38.145155, 24.019718, -1}, // Marathon, Greece + {QStringLiteral("GYW1"), 51950, 57.617463, -1.887589, -1}, // Crimond, UK + {QStringLiteral("FUE"), 65800, 48.637673, -4.350758, -1}, // Kerlouan, France +}; + +QHash VLFTransmitters::m_callsignHash; + +VLFTransmitters::Init VLFTransmitters::m_init; + +VLFTransmitters::Init::Init() +{ + // Get directory to store app data in + QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation); + // First dir is writable + QString dir = locations[0]; + + // Try reading transmitters from .csv file + QString filename = QString("%1/%2/%3/vlftransmitters.csv").arg(dir).arg(COMPANY).arg(APPLICATION_NAME); // Because this method is called before main(), we need to add f4exb/SDRangel + QFile file(filename); + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + QTextStream in(&file); + + QString error; + QHash colIndexes = CSV::readHeader(in, { + QStringLiteral("Callsign"), + QStringLiteral("Frequency"), + QStringLiteral("Latitude"), + QStringLiteral("Longitude"), + QStringLiteral("Power") + }, error); + if (error.isEmpty()) + { + QStringList cols; + int callsignCol = colIndexes.value(QStringLiteral("Callsign")); + int frequencyCol = colIndexes.value(QStringLiteral("Frequency")); + int latitudeCol = colIndexes.value(QStringLiteral("Latitude")); + int longitudeCol = colIndexes.value(QStringLiteral("Longitude")); + int powerCol = colIndexes.value(QStringLiteral("Power")); + int maxCol = std::max({callsignCol, frequencyCol, latitudeCol, longitudeCol, powerCol}); + + m_transmitters.clear(); // Replace builtin list + + while(CSV::readRow(in, &cols)) + { + if (cols.size() > maxCol) + { + Transmitter transmitter; + + transmitter.m_callsign = cols[callsignCol]; + transmitter.m_frequency = cols[frequencyCol].toLongLong(); + transmitter.m_latitude = cols[latitudeCol].toFloat(); + transmitter.m_longitude = cols[longitudeCol].toFloat(); + transmitter.m_power = cols[powerCol].toInt(); + + m_transmitters.append(transmitter); + } + } + } + else + { + qWarning() << filename << "did not contain expected headers."; + } + } + + // Create hash table for faster searching + for (const auto& transmitter : VLFTransmitters::m_transmitters) { + VLFTransmitters::m_callsignHash.insert(transmitter.m_callsign, &transmitter); + } +} diff --git a/sdrbase/util/vlftransmitters.h b/sdrbase/util/vlftransmitters.h new file mode 100644 index 000000000..2fc7c1a7c --- /dev/null +++ b/sdrbase/util/vlftransmitters.h @@ -0,0 +1,56 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_VLFTRANSMITTERS_H +#define INCLUDE_VLFTRANSMITTERS_H + +#include +#include +#include + +#include "export.h" + +// List of VLF transmitters +// Built-in list can be overriden by user supplied vlftransmitters.csv file, that is read at startup, from the app data dir +class SDRBASE_API VLFTransmitters +{ + +public: + + struct Transmitter { + QString m_callsign; + qint64 m_frequency; // In Hz + float m_latitude; + float m_longitude; + int m_power; // In kW + }; + + static QList m_transmitters; + + static QHash m_callsignHash; + +private: + + friend struct Init; + struct Init { + Init(); + }; + static Init m_init; + +}; + +#endif /* VLFTransmitters */