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).
+
+
+
+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
+
+
RollupContents
QWidget
1
-
- ButtonSwitch
- QToolButton
-
-
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 */