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

Map: Add support for Ionosonde stations

This commit is contained in:
Jon Beniston 2022-07-20 17:41:11 +01:00
parent 2a1476bb29
commit 22a30b5ea0
21 changed files with 725 additions and 47 deletions

BIN
doc/img/Map_plugin_muf.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

View File

@ -159,6 +159,24 @@ void CesiumInterface::setAntiAliasing(const QString &antiAliasing)
send(obj);
}
void CesiumInterface::showMUF(bool show)
{
QJsonObject obj {
{"command", "showMUF"},
{"show", show}
};
send(obj);
}
void CesiumInterface::showfoF2(bool show)
{
QJsonObject obj {
{"command", "showfoF2"},
{"show", show}
};
send(obj);
}
void CesiumInterface::updateImage(const QString &name, float east, float west, float north, float south, float altitude, const QString &data)
{
QJsonObject obj {

View File

@ -64,6 +64,8 @@ public:
void setCameraReferenceFrame(bool eci);
void setSunLight(bool useSunLight);
void setAntiAliasing(const QString &antiAliasing);
void showMUF(bool show);
void showfoF2(bool show);
void updateImage(const QString &name, float east, float west, float north, float south, float altitude, const QString &data);
void removeImage(const QString &name);
void removeAllImages();

View File

@ -75,13 +75,8 @@ QJsonObject CZML::update(MapItem *mapItem, bool isTarget, bool isSelected)
bool removeObj = false;
bool fixedPosition = mapItem->m_fixedPosition;
float displayDistanceMax = std::numeric_limits<float>::max();
QString image = mapItem->m_image;
if ((image == "antenna.png") || (image == "antennaam.png") || (image == "antennadab.png") || (image == "antennafm.png") || (image == "antennatime.png")) {
displayDistanceMax = 1000000;
}
if (image == "") {
if (mapItem->m_image == "")
{
// Need to remove this from the map
removeObj = true;
}
@ -212,12 +207,6 @@ QJsonObject CZML::update(MapItem *mapItem, bool isTarget, bool isSelected)
{"heightReference", heightReferences[mapItem->m_altitudeReference]},
{"show", mapItem->m_itemSettings->m_enabled && mapItem->m_itemSettings->m_display3DPoint}
};
// If clamping to ground, we need to disable depth test, so part of the point isn't clipped
// However, when the point isn't clamped to ground, we shouldn't use this, otherwise
// the point will become visible through the globe
if (mapItem->m_altitudeReference == 1) {
point.insert("disableDepthTestDistance", 100000000);
}
// Model
QJsonArray node0Cartesian {
@ -276,8 +265,26 @@ QJsonObject CZML::update(MapItem *mapItem, bool isTarget, bool isSelected)
};
// Label
// 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")) {
displayDistanceMax = 1000000;
} else if ((mapItem->m_group == "Station") || (mapItem->m_group == "Radar") || (mapItem->m_group == "Radio Time Transmitters")) {
displayDistanceMax = 10000000;
} else if (mapItem->m_group == "Ionosonde Stations") {
displayDistanceMax = 30000000;
}
QJsonArray labelPixelOffsetScaleArray {
1000000, 20, 10000000, 5
};
QJsonObject labelPixelOffsetScaleObject {
{"nearFarScalar", labelPixelOffsetScaleArray}
};
QJsonArray labelPixelOffsetArray {
20, 0
1, 0
};
QJsonObject labelPixelOffset {
{"cartesian2", labelPixelOffsetArray}
@ -302,14 +309,13 @@ QJsonObject CZML::update(MapItem *mapItem, bool isTarget, bool isSelected)
{"show", m_settings->m_displayNames && mapItem->m_itemSettings->m_enabled && mapItem->m_itemSettings->m_display3DLabel},
{"scale", mapItem->m_itemSettings->m_3DLabelScale},
{"pixelOffset", labelPixelOffset},
{"pixelOffsetScaleByDistance", labelPixelOffsetScaleObject},
{"eyeOffset", labelEyeOffset},
{"verticalOrigin", "BASELINE"},
{"horizontalOrigin", "LEFT"},
{"heightReference", heightReferences[mapItem->m_altitudeReference]},
};
if (displayDistanceMax != std::numeric_limits<float>::max())
{
label.insert("disableDepthTestDistance", 100000000.0);
if (displayDistanceMax != std::numeric_limits<float>::max()) {
label.insert("distanceDisplayCondition", labelDistanceDisplayCondition);
}
@ -323,9 +329,6 @@ QJsonObject CZML::update(MapItem *mapItem, bool isTarget, bool isSelected)
{"heightReference", heightReferences[mapItem->m_altitudeReference]},
{"verticalOrigin", "BOTTOM"} // To stop it being cut in half when zoomed out
};
if (mapItem->m_altitudeReference == 1) {
billboard.insert("disableDepthTestDistance", 100000000);
}
QJsonObject obj {
{"id", id} // id must be unique

View File

@ -3,5 +3,7 @@
<file>icons/groundtracks.png</file>
<file>icons/clock.png</file>
<file>icons/ibp.png</file>
<file>icons/muf.png</file>
<file>icons/fof2.png</file>
</qresource>
</RCC>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -7,6 +7,7 @@
<file>map/antennadab.png</file>
<file>map/antennafm.png</file>
<file>map/antennaam.png</file>
<file>map/ionosonde.png</file>
<file>map/map3d.html</file>
</qresource>
<qresource prefix="/">

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@ -157,11 +157,40 @@
geocoder: false,
fullscreenButton: true,
navigationHelpButton: false,
navigationInstructionsInitiallyVisible: false
navigationInstructionsInitiallyVisible: false,
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
var buildings = undefined;
const images = new Map();
var mufGeoJSONStream = null;
var foF2GeoJSONStream = null;
// Generate HTML for MUF contour info box from properties in GeoJSON
function describeMUF(properties, nameProperty) {
let html = "";
if (properties.hasOwnProperty("level-value")) {
const value = properties["level-value"];
if (Cesium.defined(value)) {
html = `<p>MUF: ${value} MHz<p>MUF (Maximum Usable Frequency) is the highest frequency that will reflect from the ionosphere on a 3000km path`;
}
}
return html;
}
// Generate HTML for foF2 contour info box from properties in GeoJSON
function describefoF2(properties, nameProperty) {
let html = "";
if (properties.hasOwnProperty("level-value")) {
const value = properties["level-value"];
if (Cesium.defined(value)) {
html = `<p>foF2: ${value} MHz<p>foF2 (F2 region critical frequency) is the highest frequency that will be reflected vertically from the F2 ionosphere region`;
}
}
return html;
}
// Use CZML to stream data from Map plugin to Cesium
var czmlStream = new Cesium.CzmlDataSource();
@ -238,6 +267,7 @@
} else {
console.log(`Unknown terrain ${command.terrain}`);
}
viewer.scene.globe.depthTestAgainstTerrain = false; // So labels/points aren't clipped by terrain
} else if (command.command == "setBuildings") {
if (command.buildings == "None") {
if (buildings !== undefined) {
@ -274,6 +304,32 @@
} else {
viewer.scene.postProcessStages.fxaa.enabled = false;
}
} else if (command.command == "showMUF") {
if (mufGeoJSONStream != null) {
viewer.dataSources.remove(mufGeoJSONStream, true);
mufGeoJSONStream = null;
}
if (command.show == true) {
viewer.dataSources.add(
Cesium.GeoJsonDataSource.load(
"muf.geojson",
{describe: describeMUF}
)
).then(function(dataSource) {mufGeoJSONStream = dataSource; });
}
} else if (command.command == "showfoF2") {
if (foF2GeoJSONStream != null) {
viewer.dataSources.remove(foF2GeoJSONStream, true);
foF2GeoJSONStream = null;
}
if (command.show == true) {
viewer.dataSources.add(
Cesium.GeoJsonDataSource.load(
"fof2.geojson",
{describe: describefoF2}
)
).then(function(dataSource) {foF2GeoJSONStream = dataSource; });
}
} else if (command.command == "updateImage") {
// Textures on entities can flash white when changed: https://github.com/CesiumGS/cesium/issues/1640
@ -426,7 +482,6 @@
reportClock();
};
</script>
</div>
</body>

View File

@ -275,6 +275,7 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur
addRadioTimeTransmitters();
addRadar();
addIonosonde();
displaySettings();
applySettings(true);
@ -302,6 +303,7 @@ MapGUI::~MapGUI()
m_webServer->close();
delete m_webServer;
}
delete m_giro;
delete ui;
}
@ -452,6 +454,72 @@ void MapGUI::addRadar()
update(m_map, &radarMapItem, "Radar");
}
// Ionosonde stations
void MapGUI::addIonosonde()
{
m_giro = GIRO::create();
if (m_giro)
{
connect(m_giro, &GIRO::dataUpdated, this, &MapGUI::giroDataUpdated);
connect(m_giro, &GIRO::mufUpdated, this, &MapGUI::mufUpdated);
connect(m_giro, &GIRO::foF2Updated, this, &MapGUI::foF2Updated);
}
}
void MapGUI::giroDataUpdated(const GIRO::GIROStationData& data)
{
if (!data.m_station.isEmpty())
{
IonosondeStation *station = nullptr;
// See if we already have the station in our hash
if (!m_ionosondeStations.contains(data.m_station))
{
// Create new station
station = new IonosondeStation(data);
m_ionosondeStations.insert(data.m_station, station);
}
else
{
station = m_ionosondeStations.value(data.m_station);
}
station->update(data);
// Add/update map
SWGSDRangel::SWGMapItem ionosondeStationMapItem;
ionosondeStationMapItem.setName(new QString(station->m_name));
ionosondeStationMapItem.setLatitude(station->m_latitude);
ionosondeStationMapItem.setLongitude(station->m_longitude);
ionosondeStationMapItem.setAltitude(0.0);
ionosondeStationMapItem.setImage(new QString("ionosonde.png"));
ionosondeStationMapItem.setImageRotation(0);
ionosondeStationMapItem.setText(new QString(station->m_text));
ionosondeStationMapItem.setModel(new QString("antenna.glb"));
ionosondeStationMapItem.setFixedPosition(true);
ionosondeStationMapItem.setOrientation(0);
ionosondeStationMapItem.setLabel(new QString(station->m_label));
ionosondeStationMapItem.setLabelAltitudeOffset(4.5);
ionosondeStationMapItem.setAltitudeReference(1);
update(m_map, &ionosondeStationMapItem, "Ionosonde Stations");
}
}
void MapGUI::mufUpdated(const QJsonDocument& document)
{
// Could possibly try render on 2D map, but contours
// that cross anti-meridian are not drawn properly
//${Qt5Location_PRIVATE_INCLUDE_DIRS}
//#include <QtLocation/private/qgeojson_p.h>
//QVariantList list = QGeoJson::importGeoJson(document);
m_webServer->addFile("/map/map/muf.geojson", document.toJson());
m_cesium->showMUF(m_settings.m_displayMUF);
}
void MapGUI::foF2Updated(const QJsonDocument& document)
{
m_webServer->addFile("/map/map/fof2.geojson", document.toJson());
m_cesium->showfoF2(m_settings.m_displayfoF2);
}
static QString arrayToString(QJsonArray array)
{
QString s;
@ -841,7 +909,15 @@ void MapGUI::applyMap3DSettings(bool reloadMap)
m_cesium->setCameraReferenceFrame(m_settings.m_eciCamera);
m_cesium->setAntiAliasing(m_settings.m_antiAliasing);
m_cesium->getDateTime();
m_cesium->showMUF(m_settings.m_displayMUF);
m_cesium->showfoF2(m_settings.m_displayfoF2);
}
MapSettings::MapItemSettings *ionosondeItemSettings = getItemSettings("Ionosonde Stations");
if (ionosondeItemSettings) {
m_giro->getDataPeriodically(ionosondeItemSettings->m_enabled ? 2 : 0);
}
m_giro->getMUFPeriodically(m_settings.m_displayMUF ? 15 : 0);
m_giro->getfoF2Periodically(m_settings.m_displayfoF2 ? 15 : 0);
}
void MapGUI::init3DMap()
@ -863,6 +939,9 @@ void MapGUI::init3DMap()
// Set 3D view after loading initial objects
m_cesium->setHomeView(stationLatitude, stationLongitude);
m_cesium->showMUF(m_settings.m_displayMUF);
m_cesium->showfoF2(m_settings.m_displayfoF2);
}
void MapGUI::displaySettings()
@ -874,6 +953,8 @@ void MapGUI::displaySettings()
ui->displayNames->setChecked(m_settings.m_displayNames);
ui->displaySelectedGroundTracks->setChecked(m_settings.m_displaySelectedGroundTracks);
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);
@ -949,6 +1030,26 @@ void MapGUI::on_displayAllGroundTracks_clicked(bool checked)
m_mapModel.setDisplayAllGroundTracks(checked);
}
void MapGUI::on_displayMUF_clicked(bool checked)
{
m_settings.m_displayMUF = checked;
// Only call show if disabling, so we don't get two updates
// (as getMUFPeriodically results in a call to showMUF when the data is available)
m_giro->getMUFPeriodically(m_settings.m_displayMUF ? 15 : 0);
if (m_cesium && !m_settings.m_displayMUF) {
m_cesium->showMUF(m_settings.m_displayMUF);
}
}
void MapGUI::on_displayfoF2_clicked(bool checked)
{
m_settings.m_displayfoF2 = checked;
m_giro->getfoF2Periodically(m_settings.m_displayfoF2 ? 15 : 0);
if (m_cesium && !m_settings.m_displayfoF2) {
m_cesium->showfoF2(m_settings.m_displayfoF2);
}
}
void MapGUI::on_find_returnPressed()
{
find(ui->find->text().trimmed());
@ -1234,6 +1335,8 @@ void MapGUI::makeUIConnections()
QObject::connect(ui->displayNames, &ButtonSwitch::clicked, this, &MapGUI::on_displayNames_clicked);
QObject::connect(ui->displayAllGroundTracks, &ButtonSwitch::clicked, this, &MapGUI::on_displayAllGroundTracks_clicked);
QObject::connect(ui->displaySelectedGroundTracks, &ButtonSwitch::clicked, this, &MapGUI::on_displaySelectedGroundTracks_clicked);
QObject::connect(ui->displayMUF, &ButtonSwitch::clicked, this, &MapGUI::on_displayMUF_clicked);
QObject::connect(ui->displayfoF2, &ButtonSwitch::clicked, this, &MapGUI::on_displayfoF2_clicked);
QObject::connect(ui->find, &QLineEdit::returnPressed, this, &MapGUI::on_find_returnPressed);
QObject::connect(ui->maidenhead, &QToolButton::clicked, this, &MapGUI::on_maidenhead_clicked);
QObject::connect(ui->deleteAll, &QToolButton::clicked, this, &MapGUI::on_deleteAll_clicked);

View File

@ -29,6 +29,7 @@
#include "feature/featuregui.h"
#include "util/messagequeue.h"
#include "util/giro.h"
#include "util/azel.h"
#include "settings/rollupstate.h"
@ -62,6 +63,68 @@ struct RadioTimeTransmitter {
int m_power; // In kW
};
struct IonosondeStation {
QString m_name;
float m_latitude; // In degrees
float m_longitude; // In degrees
QString m_text;
QString m_label;
IonosondeStation(const GIRO::GIROStationData& data) :
m_name(data.m_station)
{
update(data);
}
void update(const GIRO::GIROStationData& data)
{
m_latitude = data.m_latitude;
m_longitude = data.m_longitude;
QStringList text;
QStringList label;
text.append("Ionosonde Station");
text.append(QString("Name: %1").arg(m_name.split(",")[0]));
if (!isnan(data.m_mufd))
{
text.append(QString("MUF: %1 MHz").arg(data.m_mufd));
label.append(QString("%1").arg((int)round(data.m_mufd)));
}
else
{
label.append("-");
}
if (!isnan(data.m_md)) {
text.append(QString("M(D): %1").arg(data.m_md));
}
if (!isnan(data.m_foF2))
{
text.append(QString("foF2: %1 MHz").arg(data.m_foF2));
label.append(QString("%1").arg((int)round(data.m_foF2)));
}
else
{
label.append("-");
}
if (!isnan(data.m_hmF2)) {
text.append(QString("hmF2: %1 km").arg(data.m_hmF2));
}
if (!isnan(data.m_foE)) {
text.append(QString("foE: %1 MHz").arg(data.m_foE));
}
if (!isnan(data.m_tec)) {
text.append(QString("TEC: %1").arg(data.m_tec));
}
if (data.m_confidence >= 0) {
text.append(QString("Confidence: %1").arg(data.m_confidence));
}
if (data.m_dateTime.isValid()) {
text.append(data.m_dateTime.toString());
}
m_text = text.join("\n");
m_label = label.join("/");
}
};
class MapGUI : public FeatureGUI {
Q_OBJECT
public:
@ -86,6 +149,7 @@ public:
QList<RadioTimeTransmitter> getRadioTimeTransmitters() { return m_radioTimeTransmitters; }
void addRadioTimeTransmitters();
void addRadar();
void addIonosonde();
void addDAB();
void find(const QString& target);
void track3D(const QString& target);
@ -114,6 +178,8 @@ private:
quint16 m_osmPort;
OSMTemplateServer *m_templateServer;
QTimer m_redrawMapTimer;
GIRO *m_giro;
QHash<QString, IonosondeStation *> m_ionosondeStations;
CesiumInterface *m_cesium;
WebServer *m_webServer;
@ -149,6 +215,8 @@ private slots:
void on_displayNames_clicked(bool checked=false);
void on_displayAllGroundTracks_clicked(bool checked=false);
void on_displaySelectedGroundTracks_clicked(bool checked=false);
void on_displayMUF_clicked(bool checked=false);
void on_displayfoF2_clicked(bool checked=false);
void on_find_returnPressed();
void on_maidenhead_clicked();
void on_deleteAll_clicked();
@ -162,6 +230,9 @@ private slots:
virtual bool eventFilter(QObject *obj, QEvent *event);
void fullScreenRequested(QWebEngineFullScreenRequest fullScreenRequest);
void preferenceChanged(int elementType);
void giroDataUpdated(const GIRO::GIROStationData& data);
void mufUpdated(const QJsonDocument& document);
void foF2Updated(const QJsonDocument& document);
};

View File

@ -39,13 +39,13 @@
<rect>
<x>0</x>
<y>0</y>
<width>471</width>
<width>480</width>
<height>41</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>350</width>
<width>480</width>
<height>0</height>
</size>
</property>
@ -165,6 +165,46 @@
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displayMUF">
<property name="toolTip">
<string>Display MUF contours</string>
</property>
<property name="text">
<string>^</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/map/icons/muf.png</normaloff>:/map/icons/muf.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displayfoF2">
<property name="toolTip">
<string>Display foF2 contours</string>
</property>
<property name="text">
<string>^</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/map/icons/fof2.png</normaloff>:/map/icons/fof2.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displayNames">
<property name="toolTip">
@ -187,12 +227,6 @@
</item>
<item>
<widget class="ButtonSwitch" name="displaySelectedGroundTracks">
<property name="font">
<font>
<family>Adobe Devanagari</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="toolTip">
<string>Display ground tracks for selected item</string>
</property>
@ -213,12 +247,6 @@
</item>
<item>
<widget class="ButtonSwitch" name="displayAllGroundTracks">
<property name="font">
<font>
<family>Adobe Devanagari</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="toolTip">
<string>Display all ground tracks</string>
</property>
@ -339,7 +367,7 @@
</url>
</property>
</widget>
<widget class="QWebEngineView" name="web">
<widget class="QWebEngineView" name="web" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>

View File

@ -69,6 +69,11 @@ MapSettings::MapSettings() :
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));
MapItemSettings *ionosondeItemSettings = new MapItemSettings("Ionosonde Stations", QColor(255, 255, 0), 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);
@ -110,6 +115,8 @@ void MapSettings::resetToDefaults()
m_eciCamera = false;
m_modelDir = HttpDownloadManager::downloadDir() + "/3d";
m_antiAliasing = "None";
m_displayMUF = false;
m_displayfoF2 = false;
m_workspaceIndex = 0;
}
@ -152,6 +159,9 @@ QByteArray MapSettings::serialize() const
s.writeS32(33, m_workspaceIndex);
s.writeBlob(34, m_geometryBytes);
s.writeBool(35, m_displayMUF);
s.writeBool(36, m_displayfoF2);
return s.final();
}
@ -224,6 +234,9 @@ bool MapSettings::deserialize(const QByteArray& data)
d.readS32(33, &m_workspaceIndex, 0);
d.readBlob(34, &m_geometryBytes);
d.readBool(35, &m_displayMUF, false);
d.readBool(36, &m_displayfoF2, false);
return true;
}
else

View File

@ -104,6 +104,9 @@ struct MapSettings
QString m_cesiumIonAPIKey;
QString m_antiAliasing;
bool m_displayMUF; // Plot MUF contours
bool m_displayfoF2; // Plot foF2 contours
// Per source settings
QHash<QString, MapItemSettings *> m_itemSettings;

View File

@ -11,10 +11,14 @@ 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.
As well as other other data sources:
* Beacons based on the IARU Region 1 beacon database and International Beacon Project,
* Radio time transmitters,
* GRAVES radar.
* GRAVES radar,
* Ionosonde station data.
It can also create tracks showing the path aircraft, ships and APRS objects have taken, as well as predicted paths for satellites.
@ -22,7 +26,7 @@ It can also create tracks showing the path aircraft, ships and APRS objects have
![3D Map feature](../../../doc/img/Map_plugin_apt.png)
3D Models are not included with SDRangel. They must be downloaded by pressing the Download 3D Models button in the Display Settings dialog (11).
3D Models are not included with SDRangel. They must be downloaded by pressing the Download 3D Models button in the Display Settings dialog (13).
<h2>Interface</h2>
@ -78,23 +82,33 @@ When clicked, opens the Radio Time Transmitters dialog.
![Radio Time transmitters dialog](../../../doc/img/Map_plugin_radiotime_dialog.png)
<h3>7: Display Names</h3>
<h3>7: Display MUF Contours</h3>
When checked, contours will be downloaded and displayed on the 3D map, showing the MUF (Maximum Usable Frequency) for a 3000km path that reflects off the ionosphere.
The contours will be updated every 15 minutes. The latest contour data will always be displayed, irrespective of the time set on the 3D Map.
<h3>8: Display coF2 Contours</h3>
When checked, contours will be downloaded and displayed on the 3D map, showing coF2 (F2 layer critical frequency), the maximum frequency at which radio waves will be reflected vertically from the F2 region of the ionosphere.
The contours will be updated every 15 minutes. The latest contour data will always be displayed, irrespective of the time set on the 3D Map.
<h3>8: Display Names</h3>
When checked, names of objects are displayed in a bubble next to each object.
<h3>8: Display tracks for selected object</h3>
<h3>9: Display tracks for selected object</h3>
When checked, displays the track (taken or predicted) for the selected object.
<h3>9: Display tracks for all objects</h3>
<h3>10: Display tracks for all objects</h3>
When checked, displays the track (taken or predicted) for the all objects.
<h3>10: Delete</h3>
<h3>11: Delete</h3>
When clicked, all items will be deleted from the map.
<h3>11: Display settings</h3>
<h3>12: Display settings</h3>
When clicked, opens the Map Display Settings dialog:
@ -154,6 +168,25 @@ The 2D map will only display the last reported positions for objects.
The 3D map, however, has a timeline that allows replaying how objects have moved over time.
To the right of the timeline is the fullscreen toggle button, which allows the 3D map to be displayed fullscreen.
<h4>Ionosonde Stations</h4>
When Ionosonde Stations are displayed, data is downloaded and displayed every 2 minutes. The data includes:
* MUF - Maximum Usable Frequency in MHz for 3000km path.
* M(D) - M-factor (~MUF/foF2) for 3000km path.
* foF2 - F2 region critical frequency in MHz.
* hmF2 - F2 region height in km.
* foE - E region critical frequency in MHz.
* TEC - Total Electron Content.
Each station is labelled on the maps as "MUF/foF2".
MUF and foF2 can be displayed as countors:
![MUF contours](../../../doc/img/Map_plugin_muf.png)
The contours can be clicked on which will display the data for that contour in the info box.
<h2>Attribution</h2>
IARU Region 1 beacon list used with permission from: https://iaru-r1-c5-beacons.org/ To add or update a beacon, see: https://iaru-r1-c5-beacons.org/index.php/beacon-update/
@ -161,7 +194,10 @@ IARU Region 1 beacon list used with permission from: https://iaru-r1-c5-beacons.
Mapping and geolocation services are by Open Street Map: https://www.openstreetmap.org/ esri: https://www.esri.com/
Mapbox: https://www.mapbox.com/ Cesium: https://www.cesium.com Bing: https://www.bing.com/maps/
Ionosonde data and MUF/coF2 contours from [KC2G](https://prop.kc2g.com/) with source data from [GIRO](https://giro.uml.edu/) and [NOAA NCEI](https://www.ngdc.noaa.gov/stp/iono/ionohome.html).
Icons made by Google from Flaticon https://www.flaticon.com
World icons created by turkkub from Flaticon https://www.flaticon.com
3D models are by various artists under a variety of liceneses. See: https://github.com/srcejon/sdrangel-3d-models

View File

@ -37,6 +37,7 @@ WebServer::WebServer(quint16 &port, QObject* parent) :
m_mimeTypes.insert(".js", new MimeType("text/javascript"));
m_mimeTypes.insert(".css", new MimeType("text/css"));
m_mimeTypes.insert(".json", new MimeType("application/json"));
m_mimeTypes.insert(".geojson", new MimeType("application/geo+json"));
}
void WebServer::incomingConnection(qintptr socket)
@ -88,6 +89,11 @@ QString WebServer::substitute(QString path, QString html)
return html;
}
void WebServer::addFile(const QString &path, const QByteArray &data)
{
m_files.insert(path, data);
}
void WebServer::sendFile(QTcpSocket* socket, const QByteArray &data, MimeType *mimeType, const QString &path)
{
QString header = QString("HTTP/1.0 200 Ok\r\nContent-Type: %1\r\n\r\n").arg(mimeType->m_type);
@ -163,9 +169,14 @@ void WebServer::readClient()
sendFile(socket, data, mimeType, path);
}
#endif
else if (m_files.contains(path))
{
// Path is a file held in memory
sendFile(socket, m_files.value(path).data(), mimeType, path);
}
else
{
// See if we can find a file
// See if we can find a file on disk
QFile file(path);
if (file.open(QIODevice::ReadOnly))
{

View File

@ -49,12 +49,15 @@ class WebServer : public QTcpServer
private:
// Hash of a list of paths to substitude
// Hash of a list of paths to substitute
QHash<QString, QString> m_pathSubstitutions;
// Hash of path to a list of substitutions to make in the file
QHash<QString, QList<Substitution *>*> m_substitutions;
// Hash of files held in memory
QHash<QString, QByteArray> m_files;
// Hash of filename extension to MIME type information
QHash<QString, MimeType *> m_mimeTypes;
MimeType m_defaultMimeType;
@ -64,6 +67,7 @@ public:
void incomingConnection(qintptr socket) override;
void addPathSubstitution(const QString &from, const QString &to);
void addSubstitution(QString path, QString from, QString to);
void addFile(const QString &path, const QByteArray &data);
QString substitute(QString path, QString html);
void sendFile(QTcpSocket* socket, const QByteArray &data, MimeType *mimeType, const QString &path);

View File

@ -181,6 +181,7 @@ set(sdrbase_SOURCES
util/fixedtraits.cpp
util/fits.cpp
util/flightinformation.cpp
util/giro.cpp
util/golay2312.cpp
util/httpdownloadmanager.cpp
util/interpolation.cpp
@ -395,6 +396,7 @@ set(sdrbase_HEADERS
util/fixedtraits.h
util/fits.h
util/flightinformation.h
util/giro.h
util/golay2312.h
util/httpdownloadmanager.h
util/incrementalarray.h

227
sdrbase/util/giro.cpp Normal file
View File

@ -0,0 +1,227 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2022 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 "giro.h"
#include <QDebug>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkReply>
#include <QJsonObject>
GIRO::GIRO()
{
connect(&m_dataTimer, &QTimer::timeout, this, &GIRO::getData);
connect(&m_mufTimer, &QTimer::timeout, this, &GIRO::getMUF);
connect(&m_foF2Timer, &QTimer::timeout, this, &GIRO::getfoF2);
m_networkManager = new QNetworkAccessManager();
connect(m_networkManager, &QNetworkAccessManager::finished, this, &GIRO::handleReply);
}
GIRO::~GIRO()
{
disconnect(&m_dataTimer, &QTimer::timeout, this, &GIRO::getData);
disconnect(&m_mufTimer, &QTimer::timeout, this, &GIRO::getMUF);
disconnect(&m_foF2Timer, &QTimer::timeout, this, &GIRO::getfoF2);
disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &GIRO::handleReply);
delete m_networkManager;
}
GIRO* GIRO::create(const QString& service)
{
if (service == "prop.kc2g.com")
{
return new GIRO();
}
else
{
qDebug() << "GIRO::create: Unsupported service: " << service;
return nullptr;
}
}
void GIRO::getDataPeriodically(int periodInMins)
{
if (periodInMins > 0)
{
m_dataTimer.setInterval(periodInMins*60*1000);
m_dataTimer.start();
getData();
}
else
{
m_dataTimer.stop();
}
}
void GIRO::getMUFPeriodically(int periodInMins)
{
if (periodInMins > 0)
{
m_mufTimer.setInterval(periodInMins*60*1000);
m_mufTimer.start();
getMUF();
}
else
{
m_mufTimer.stop();
}
}
void GIRO::getfoF2Periodically(int periodInMins)
{
if (periodInMins > 0)
{
m_foF2Timer.setInterval(periodInMins*60*1000);
m_foF2Timer.start();
getfoF2();
}
else
{
m_foF2Timer.stop();
}
}
void GIRO::getData()
{
QUrl url(QString("https://prop.kc2g.com/api/stations.json"));
m_networkManager->get(QNetworkRequest(url));
}
void GIRO::getMUF()
{
QUrl url(QString("https://prop.kc2g.com/renders/current/mufd-normal-now.geojson"));
m_networkManager->get(QNetworkRequest(url));
}
void GIRO::getfoF2()
{
QUrl url(QString("https://prop.kc2g.com/renders/current/fof2-normal-now.geojson"));
m_networkManager->get(QNetworkRequest(url));
}
bool GIRO::containsNonNull(const QJsonObject& obj, const QString &key) const
{
if (obj.contains(key))
{
QJsonValue val = obj.value(key);
return !val.isNull();
}
return false;
}
void GIRO::handleReply(QNetworkReply* reply)
{
if (reply)
{
if (!reply->error())
{
QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
if (reply->url().fileName() == "stations.json")
{
if (document.isArray())
{
QJsonArray array = document.array();
for (auto valRef : array)
{
if (valRef.isObject())
{
QJsonObject obj = valRef.toObject();
GIROStationData data;
if (obj.contains(QStringLiteral("station")))
{
QJsonObject stationObj = obj.value(QStringLiteral("station")).toObject();
if (stationObj.contains(QStringLiteral("name"))) {
data.m_station = stationObj.value(QStringLiteral("name")).toString();
}
if (stationObj.contains(QStringLiteral("latitude"))) {
data.m_latitude = (float)stationObj.value(QStringLiteral("latitude")).toString().toFloat();
}
if (stationObj.contains(QStringLiteral("longitude"))) {
data.m_longitude = (float)stationObj.value(QStringLiteral("longitude")).toString().toFloat();
if (data.m_longitude >= 180.0f) {
data.m_longitude -= 360.0f;
}
}
}
if (containsNonNull(obj, QStringLiteral("time"))) {
data.m_dateTime = QDateTime::fromString(obj.value(QStringLiteral("time")).toString(), Qt::ISODateWithMs);
}
if (containsNonNull(obj, QStringLiteral("mufd"))) {
data.m_mufd = (float)obj.value(QStringLiteral("mufd")).toDouble();
}
if (containsNonNull(obj, QStringLiteral("md"))) {
data.m_md = obj.value(QStringLiteral("md")).toString().toFloat();
}
if (containsNonNull(obj, QStringLiteral("tec"))) {
data.m_tec = (float)obj.value(QStringLiteral("tec")).toDouble();
}
if (containsNonNull(obj, QStringLiteral("fof2"))) {
data.m_foF2 = (float)obj.value(QStringLiteral("fof2")).toDouble();
}
if (containsNonNull(obj, QStringLiteral("hmf2"))) {
data.m_hmF2 = (float)obj.value(QStringLiteral("hmf2")).toDouble();
}
if (containsNonNull(obj, QStringLiteral("foe"))) {
data.m_foE = (float)obj.value(QStringLiteral("foe")).toDouble();
}
if (containsNonNull(obj, QStringLiteral("cs"))) {
data.m_confidence = (int)obj.value(QStringLiteral("cs")).toDouble();
}
emit dataUpdated(data);
}
else
{
qDebug() << "GIRO::handleReply: Array element is not an object: " << valRef;
}
}
}
else
{
qDebug() << "GIRO::handleReply: Document is not an array: " << document;
}
}
else if (reply->url().fileName() == "mufd-normal-now.geojson")
{
emit mufUpdated(document);
}
else if (reply->url().fileName() == "fof2-normal-now.geojson")
{
emit foF2Updated(document);
}
else
{
qDebug() << "GIRO::handleReply: unexpected filename: " << reply->url().fileName();
}
}
else
{
qDebug() << "GIRO::handleReply: error: " << reply->error();
}
reply->deleteLater();
}
else
{
qDebug() << "GIRO::handleReply: reply is null";
}
}

99
sdrbase/util/giro.h Normal file
View File

@ -0,0 +1,99 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2022 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_GIRO_H
#define INCLUDE_GIRO_H
#include <QtCore>
#include <QTimer>
#include <QJsonDocument>
#include "export.h"
class QNetworkAccessManager;
class QNetworkReply;
// GIRO - Global Ionosphere Radio Observatory
// Gets MUFD, TEC, foF2 and other data for various stations around the world
// Also gets MUF and foF2 contours as GeoJSON
// Data from https://prop.kc2g.com/stations/
class SDRBASE_API GIRO : public QObject
{
Q_OBJECT
protected:
GIRO();
public:
// See the following paper for an explanation of some of these variables
// https://sbgf.org.br/mysbgf/eventos/expanded_abstracts/13th_CISBGf/A%20Simple%20Method%20to%20Calculate%20the%20Maximum%20Usable%20Frequency.pdf
struct GIROStationData {
QString m_station;
float m_latitude;
float m_longitude;
QDateTime m_dateTime;
float m_mufd; // Maximum usable frequency
float m_md; // Propagation coefficient? D=3000km?
float m_tec; // Total electron content
float m_foF2; // Critical frequency of F2 layer in ionosphere (highest frequency to be reflected)
float m_hmF2; // F2 layer height of peak electron density (km?)
float m_foE; // Critical frequency of E layer
int m_confidence;
GIROStationData() :
m_latitude(NAN),
m_longitude(NAN),
m_mufd(NAN),
m_md(NAN),
m_tec(NAN),
m_foF2(NAN),
m_hmF2(NAN),
m_foE(NAN),
m_confidence(-1)
{
}
};
static GIRO* create(const QString& service="prop.kc2g.com");
~GIRO();
void getDataPeriodically(int periodInMins);
void getMUFPeriodically(int periodInMins);
void getfoF2Periodically(int periodInMins);
private slots:
void getData();
void getMUF();
void getfoF2();
private slots:
void handleReply(QNetworkReply* reply);
signals:
void dataUpdated(const GIROStationData& data); // Called when new data available.
void mufUpdated(const QJsonDocument& doc);
void foF2Updated(const QJsonDocument& doc);
private:
bool GIRO::containsNonNull(const QJsonObject& obj, const QString &key) const;
QTimer m_dataTimer; // Timer for periodic updates
QTimer m_mufTimer;
QTimer m_foF2Timer;
QNetworkAccessManager *m_networkManager;
};
#endif /* INCLUDE_GIRO_H */