1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2024-12-22 17:45:48 -05:00

Plot SID paths on map.

This commit is contained in:
srcejon 2024-04-04 21:41:07 +01:00
parent 35603a8c25
commit 535f5c5e8f
12 changed files with 331 additions and 5 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 915 KiB

After

Width:  |  Height:  |  Size: 641 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 935 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -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.
<h3>Show Paths on Map</h3>
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.
<h2>Tips</h2>
In order to check that a peak in the spectrum is a real VLF signal, you can:

View File

@ -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<ObjectPipe*> mapPipes;
MainCore::instance()->getMessagePipes().getMessagePipes(m_sid, "mapitems", mapPipes);
if (mapPipes.size() > 0)
{
for (const auto& pipe : mapPipes)
{
MessageQueue *messageQueue = qobject_cast<MessageQueue*>(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<br>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<SWGSDRangel::SWGMapCoordinate *> *coords = new QList<SWGSDRangel::SWGMapCoordinate *>();
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();

View File

@ -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);

View File

@ -607,6 +607,20 @@
</item>
</widget>
</item>
<item>
<widget class="QToolButton" name="showPaths">
<property name="toolTip">
<string>Show propagation paths on map</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/world.png</normaloff>:/world.png</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
@ -725,17 +739,17 @@
</widget>
</widget>
<customwidgets>
<customwidget>
<class>ButtonSwitch</class>
<extends>QToolButton</extends>
<header>gui/buttonswitch.h</header>
</customwidget>
<customwidget>
<class>RollupContents</class>
<extends>QWidget</extends>
<header>gui/rollupcontents.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>ButtonSwitch</class>
<extends>QToolButton</extends>
<header>gui/buttonswitch.h</header>
</customwidget>
<customwidget>
<class>QChartView</class>
<extends>QGraphicsView</extends>

View File

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

View File

@ -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;

View File

@ -23,6 +23,7 @@
#include <QString>
#include <QJsonArray>
#include <QGeoCoordinate>
#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<int> &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);

View File

@ -0,0 +1,116 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// 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 <QTextStream>
#include <QStandardPaths>
#include <QDebug>
#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::Transmitter> 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<QString, const VLFTransmitters::Transmitter*> 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<QString, int> 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);
}
}

View File

@ -0,0 +1,56 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// 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_VLFTRANSMITTERS_H
#define INCLUDE_VLFTRANSMITTERS_H
#include <QString>
#include <QList>
#include <QHash>
#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<Transmitter> m_transmitters;
static QHash<QString, const Transmitter*> m_callsignHash;
private:
friend struct Init;
struct Init {
Init();
};
static Init m_init;
};
#endif /* VLFTransmitters */