1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2024-12-23 10:05:46 -05:00

Map: Add save to KML. Support MUF/foF2 varying with time. Support VLF transmitters being read from .csv.

This commit is contained in:
srcejon 2024-04-05 10:41:24 +01:00
parent c137faf012
commit 4955e6ab08
20 changed files with 449 additions and 350 deletions

View File

@ -54,7 +54,6 @@ if(NOT SERVER_MODE)
mapibpbeacondialog.ui mapibpbeacondialog.ui
mapradiotimedialog.cpp mapradiotimedialog.cpp
mapradiotimedialog.ui mapradiotimedialog.ui
mapcolordialog.cpp
mapmodel.cpp mapmodel.cpp
mapitem.cpp mapitem.cpp
mapwebsocketserver.cpp mapwebsocketserver.cpp
@ -75,7 +74,6 @@ if(NOT SERVER_MODE)
mapbeacondialog.h mapbeacondialog.h
mapibpbeacon.h mapibpbeacon.h
mapradiotimedialog.h mapradiotimedialog.h
mapcolordialog.h
mapmodel.h mapmodel.h
mapitem.h mapitem.h
mapwebsocketserver.h mapwebsocketserver.h

View File

@ -275,3 +275,13 @@ void CesiumInterface::setPosition(const QGeoCoordinate& position)
{ {
m_czml.setPosition(position); m_czml.setPosition(position);
} }
void CesiumInterface::save(const QString& filename, const QString& dataDir)
{
QJsonObject obj {
{"command", "save"},
{"filename", filename},
{"dataDir", dataDir}
};
send(obj);
}

View File

@ -80,6 +80,7 @@ public:
void update(PolygonMapItem *mapItem); void update(PolygonMapItem *mapItem);
void update(PolylineMapItem *mapItem); void update(PolylineMapItem *mapItem);
void setPosition(const QGeoCoordinate& position); void setPosition(const QGeoCoordinate& position);
void save(const QString& filename, const QString& dataDir);
protected: protected:

View File

@ -264,7 +264,9 @@ QJsonObject CZML::update(ObjectMapItem *mapItem, bool isTarget, bool isSelected)
const QStringList heightReferences = {"NONE", "CLAMP_TO_GROUND", "RELATIVE_TO_GROUND", "NONE"}; const QStringList heightReferences = {"NONE", "CLAMP_TO_GROUND", "RELATIVE_TO_GROUND", "NONE"};
QString dt; QString dt;
if (mapItem->m_takenTrackDateTimes.size() > 0) { if (mapItem->m_availableFrom.isValid()) {
dt = mapItem->m_availableFrom.toString(Qt::ISODateWithMs);
} else if (mapItem->m_takenTrackDateTimes.size() > 0) {
dt = mapItem->m_takenTrackDateTimes.last()->toString(Qt::ISODateWithMs); dt = mapItem->m_takenTrackDateTimes.last()->toString(Qt::ISODateWithMs);
} else { } else {
dt = QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs); dt = QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs);
@ -580,6 +582,14 @@ QJsonObject CZML::update(ObjectMapItem *mapItem, bool isTarget, bool isSelected)
} }
} }
} }
else
{
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); m_lastPosition.insert(id, coords);
} }
else else

View File

@ -364,6 +364,52 @@
["railways", railwaysLayer] ["railways", railwaysLayer]
]); ]);
function downloadBlob(filename, blob) {
if (window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveBlob(blob, filename);
} else {
const elem = window.document.createElement("a");
elem.href = window.URL.createObjectURL(blob);
elem.download = filename;
document.body.appendChild(elem);
elem.click();
document.body.removeChild(elem);
}
}
function downloadText(filename, text) {
var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
var dataDir = ""; // Directory where 3D models are stored
function modelCallback(modelGraphics, time, externalFiles) {
const resource = modelGraphics.uri.getValue(time);
console.log("modelcallback " + resource);
const regex = /http:\/\/127.0.0.1:\d+/;
var file = resource.url.replace(regex, dataDir);
// KML only supports Collada files. User will have to convert the models if required
file = file.replace(/glb$/, "dae");
file = file.replace(/gltf$/, "dae");
if (navigator.platform.indexOf('Win') > -1) {
file = file.replace(/\//g, "\\");
}
return file;
}
// Use WebSockets for handling commands from MapPlugin // Use WebSockets for handling commands from MapPlugin
// (CZML doesn't support camera control, for example) // (CZML doesn't support camera control, for example)
// and sending events back to it // and sending events back to it
@ -469,30 +515,40 @@
viewer.scene.postProcessStages.fxaa.enabled = false; viewer.scene.postProcessStages.fxaa.enabled = false;
} }
} else if (command.command == "showMUF") { } else if (command.command == "showMUF") {
if (mufGeoJSONStream != null) {
viewer.dataSources.remove(mufGeoJSONStream, true);
mufGeoJSONStream = null;
}
if (command.show == true) { if (command.show == true) {
viewer.dataSources.add( viewer.dataSources.add(
Cesium.GeoJsonDataSource.load( Cesium.GeoJsonDataSource.load(
"muf.geojson", "muf.geojson",
{ describe: describeMUF } { describe: describeMUF }
) )
).then(function (dataSource) { mufGeoJSONStream = dataSource; }); ).then(function (dataSource) {
if (mufGeoJSONStream != null) {
viewer.dataSources.remove(mufGeoJSONStream, true);
mufGeoJSONStream = null;
}
mufGeoJSONStream = dataSource;
});
} else {
viewer.dataSources.remove(mufGeoJSONStream, true);
mufGeoJSONStream = null;
} }
} else if (command.command == "showfoF2") { } else if (command.command == "showfoF2") {
if (foF2GeoJSONStream != null) {
viewer.dataSources.remove(foF2GeoJSONStream, true);
foF2GeoJSONStream = null;
}
if (command.show == true) { if (command.show == true) {
viewer.dataSources.add( viewer.dataSources.add(
Cesium.GeoJsonDataSource.load( Cesium.GeoJsonDataSource.load(
"fof2.geojson", "fof2.geojson",
{ describe: describefoF2 } { describe: describefoF2 }
) )
).then(function (dataSource) { foF2GeoJSONStream = dataSource; }); ).then(function (dataSource) {
if (foF2GeoJSONStream != null) {
viewer.dataSources.remove(foF2GeoJSONStream, true);
foF2GeoJSONStream = null;
}
foF2GeoJSONStream = dataSource;
});
} else {
viewer.dataSources.remove(foF2GeoJSONStream, true);
foF2GeoJSONStream = null;
} }
} else if (command.command == "showLayer") { } else if (command.command == "showLayer") {
layers.get(command.layer).show = command.show; layers.get(command.layer).show = command.show;
@ -639,7 +695,7 @@
czmlStream.process(command); czmlStream.process(command);
} else { } else {
var promise = Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, [position]); var promise = Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, [position]);
Cesium.when(promise, function(updatedPositions) { Cesium.when(promise, function (updatedPositions) {
if (height < updatedPositions[0].height) { if (height < updatedPositions[0].height) {
if (size == 3) { if (size == 3) {
command.position.cartographicDegrees[2] = updatedPositions[0].height; command.position.cartographicDegrees[2] = updatedPositions[0].height;
@ -648,7 +704,7 @@
} }
} }
czmlStream.process(command); czmlStream.process(command);
}, function() { }, function () {
console.log(`Terrain doesn't support sampleTerrainMostDetailed`); console.log(`Terrain doesn't support sampleTerrainMostDetailed`);
czmlStream.process(command); czmlStream.process(command);
}); });
@ -657,47 +713,47 @@
console.log(`Can't currently use altitudeReference when more than one position`); console.log(`Can't currently use altitudeReference when more than one position`);
czmlStream.process(command); czmlStream.process(command);
} }
} else if ( (command.hasOwnProperty('polygon') && command.polygon.hasOwnProperty('altitudeReference')) } else if ((command.hasOwnProperty('polygon') && command.polygon.hasOwnProperty('altitudeReference'))
|| (command.hasOwnProperty('polyline') && command.polyline.hasOwnProperty('altitudeReference'))) { || (command.hasOwnProperty('polyline') && command.polyline.hasOwnProperty('altitudeReference'))) {
// Support per vertex height reference in polygons and CLIP_TO_GROUND in polylines // Support per vertex height reference in polygons and CLIP_TO_GROUND in polylines
var prim = command.hasOwnProperty('polygon') ? command.polygon : command.polyline; var prim = command.hasOwnProperty('polygon') ? command.polygon : command.polyline;
var clipToGround = prim.altitudeReference == "CLIP_TO_GROUND"; var clipToGround = prim.altitudeReference == "CLIP_TO_GROUND";
var clampToGround = prim.altitudeReference == "CLAMP_TO_GROUND"; var clampToGround = prim.altitudeReference == "CLAMP_TO_GROUND";
var size = prim.positions.cartographicDegrees.length; var size = prim.positions.cartographicDegrees.length;
var positionCount = size/3; var positionCount = size / 3;
var positions = new Array(positionCount); var positions = new Array(positionCount);
if (viewer.terrainProvider instanceof Cesium.EllipsoidTerrainProvider) { if (viewer.terrainProvider instanceof Cesium.EllipsoidTerrainProvider) {
if (clampToGround) { if (clampToGround) {
for (let i = 0; i < positionCount; i++) { for (let i = 0; i < positionCount; i++) {
prim.positions.cartographicDegrees[i*3+2] = 0; prim.positions.cartographicDegrees[i * 3 + 2] = 0;
} }
} else if (clipToGround) { } else if (clipToGround) {
for (let i = 0; i < positionCount; i++) { for (let i = 0; i < positionCount; i++) {
if (prim.positions.cartographicDegrees[i*3+2] < 0) { if (prim.positions.cartographicDegrees[i * 3 + 2] < 0) {
prim.positions.cartographicDegrees[i*3+2] = 0; prim.positions.cartographicDegrees[i * 3 + 2] = 0;
} }
} }
} }
czmlStream.process(command); czmlStream.process(command);
} else { } else {
for (let i = 0; i < positionCount; i++) { for (let i = 0; i < positionCount; i++) {
positions[i] = Cesium.Cartographic.fromDegrees(prim.positions.cartographicDegrees[i*3+0], prim.positions.cartographicDegrees[i*3+1]); positions[i] = Cesium.Cartographic.fromDegrees(prim.positions.cartographicDegrees[i * 3 + 0], prim.positions.cartographicDegrees[i * 3 + 1]);
} }
var promise = Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, positions); var promise = Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, positions);
Cesium.when(promise, function(updatedPositions) { Cesium.when(promise, function (updatedPositions) {
if (clampToGround) { if (clampToGround) {
for (let i = 0; i < positionCount; i++) { for (let i = 0; i < positionCount; i++) {
prim.positions.cartographicDegrees[i*3+2] = updatedPositions[i].height; prim.positions.cartographicDegrees[i * 3 + 2] = updatedPositions[i].height;
} }
} else if (clipToGround) { } else if (clipToGround) {
for (let i = 0; i < positionCount; i++) { for (let i = 0; i < positionCount; i++) {
if (prim.positions.cartographicDegrees[i*3+2] < updatedPositions[i].height) { if (prim.positions.cartographicDegrees[i * 3 + 2] < updatedPositions[i].height) {
prim.positions.cartographicDegrees[i*3+2] = updatedPositions[i].height; prim.positions.cartographicDegrees[i * 3 + 2] = updatedPositions[i].height;
} }
} }
} }
czmlStream.process(command); czmlStream.process(command);
}, function() { }, function () {
console.log(`Terrain doesn't support sampleTerrainMostDetailed`); console.log(`Terrain doesn't support sampleTerrainMostDetailed`);
czmlStream.process(command); czmlStream.process(command);
}); });
@ -705,7 +761,20 @@
} else { } else {
czmlStream.process(command); czmlStream.process(command);
} }
} else if (command.command == "save") {
// Export to kml/kmz
dataDir = command.dataDir;
Cesium.exportKml({
entities: czmlStream.entities,
kmz: command.filename.endsWith("kmz"),
modelCallback: modelCallback
}).then(function (result) {
if (command.filename.endsWith("kmz")) {
downloadBlob(command.filename, result.kmz);
} else {
downloadText(command.filename, result.kml);
}
});
} else { } else {
console.log(`Unknown command ${command.command}`); console.log(`Unknown command ${command.command}`);
} }
@ -759,10 +828,13 @@
Cesium.knockout.getObservable(viewer.clockViewModel, 'multiplier').subscribe(function(multiplier) { Cesium.knockout.getObservable(viewer.clockViewModel, 'multiplier').subscribe(function(multiplier) {
reportClock(); reportClock();
}); });
// This is called every frame // This is called every frame, which is too fast, so instead use setInterval with 1 second period
//Cesium.knockout.getObservable(viewer.clockViewModel, 'currentTime').subscribe(function(currentTime) { //Cesium.knockout.getObservable(viewer.clockViewModel, 'currentTime').subscribe(function(currentTime) {
//reportClock(); //reportClock();
//}); //});
setInterval(function () {
reportClock();
}, 1000);
viewer.timeline.addEventListener('settime', reportClock, false); viewer.timeline.addEventListener('settime', reportClock, false);
socket.onopen = () => { socket.onopen = () => {

View File

@ -1,71 +0,0 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2022 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 <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QDebug>
#include "mapcolordialog.h"
MapColorDialog::MapColorDialog(const QColor &initial, QWidget *parent) :
QDialog(parent)
{
m_colorDialog = new QColorDialog();
m_colorDialog->setWindowFlags(Qt::Widget);
m_colorDialog->setOptions(QColorDialog::ShowAlphaChannel | QColorDialog::NoButtons | QColorDialog::DontUseNativeDialog);
m_colorDialog->setCurrentColor(initial); // Needs to be set after setOptions on Linux, which seems to overwrite QColorDialog(initial)
QVBoxLayout *v = new QVBoxLayout(this);
v->addWidget(m_colorDialog);
QHBoxLayout *h = new QHBoxLayout();
m_noColorButton = new QPushButton("No Color");
m_cancelButton = new QPushButton("Cancel");
m_okButton = new QPushButton("OK");
h->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Expanding));
h->addWidget(m_noColorButton);
h->addWidget(m_cancelButton);
h->addWidget(m_okButton);
v->addLayout(h);
connect(m_noColorButton, &QPushButton::clicked, this, &MapColorDialog::noColorClicked);
connect(m_cancelButton, &QPushButton::clicked, this, &QDialog::reject);
connect(m_okButton, &QPushButton::clicked, this, &QDialog::accept);
m_noColorSelected = false;
}
QColor MapColorDialog::selectedColor() const
{
return m_colorDialog->selectedColor();
}
bool MapColorDialog::noColorSelected() const
{
return m_noColorSelected;
}
void MapColorDialog::accept()
{
m_colorDialog->accept();
QDialog::accept();
}
void MapColorDialog::noColorClicked()
{
m_noColorSelected = true;
accept();
}

View File

@ -1,48 +0,0 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany //
// written by Christian Daniel //
// Copyright (C) 2015-2019 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// Copyright (C) 2021-2022 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_FEATURE_MAPCOLORDIALOG_H
#define INCLUDE_FEATURE_MAPCOLORDIALOG_H
#include <QColorDialog>
class MapColorDialog : public QDialog {
Q_OBJECT
public:
explicit MapColorDialog(const QColor &initial, QWidget *parent = nullptr);
QColor selectedColor() const;
bool noColorSelected() const;
public slots:
virtual void accept() override;
void noColorClicked();
private:
QColorDialog *m_colorDialog;
QPushButton *m_noColorButton;
QPushButton *m_cancelButton;
QPushButton *m_okButton;
bool m_noColorSelected;
};
#endif // INCLUDE_FEATURE_MAPCOLORDIALOG_H

View File

@ -48,6 +48,7 @@
#include "util/maidenhead.h" #include "util/maidenhead.h"
#include "util/morse.h" #include "util/morse.h"
#include "util/navtex.h" #include "util/navtex.h"
#include "util/vlftransmitters.h"
#include "maplocationdialog.h" #include "maplocationdialog.h"
#include "mapmaidenheaddialog.h" #include "mapmaidenheaddialog.h"
#include "mapsettingsdialog.h" #include "mapsettingsdialog.h"
@ -306,6 +307,9 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur
connect(ui->web->page(), &QWebEnginePage::loadingChanged, this, &MapGUI::loadingChanged); connect(ui->web->page(), &QWebEnginePage::loadingChanged, this, &MapGUI::loadingChanged);
connect(ui->web, &QWebEngineView::renderProcessTerminated, this, &MapGUI::renderProcessTerminated); connect(ui->web, &QWebEngineView::renderProcessTerminated, this, &MapGUI::renderProcessTerminated);
#endif #endif
QWebEngineProfile *profile = QWebEngineProfile::defaultProfile();
connect(profile, &QWebEngineProfile::downloadRequested, this, &MapGUI::downloadRequested);
#endif #endif
// Get station position // Get station position
@ -489,41 +493,24 @@ void MapGUI::addIBPBeacons()
} }
} }
// 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
const QList<RadioTimeTransmitter> MapGUI::m_vlfTransmitters = {
// Other signals possibly seen: 13800, 19000
{"VTX2", 17000, 8.387015, 77.752762, -1}, // South Vijayanarayanam, India
{"GQD", 19580, 54.911643, -3.278456, 100}, // Anthorn, UK, Often referred to as GBZ
{"NWC", 19800, -21.816325, 114.16546, 1000}, // Exmouth, Aus
{"ICV", 20270, 40.922946, 9.731881, 50}, // Isola di Tavolara, Italy (Can be distorted on 3D map if terrain used)
{"FTA", 20900, 48.544632, 2.579429, 50}, // Sainte-Assise, France (Satellite imagary obfuscated)
{"NPM", 21400, 21.420166, -158.151140, 600}, // Pearl Harbour, Lualuahei, USA (Not seen?)
{"HWU", 21750, 46.713129, 1.245248, 200}, // Rosnay, France
{"GQZ", 22100, 54.731799, -2.883033, 100}, // Skelton, UK (GVT in paper)
{"DHO38", 23400, 53.078900, 7.615000, 300}, // Rhauderfehn, Germany - Off air 7-8 UTC - Not seen on air!
{"NAA", 24000, 44.644506, -67.284565, 1000}, // Cutler, Maine, USA
{"TFK/NRK", 37500, 63.850365, -22.466773, 100}, // Grindavik, Iceland
{"SRC/SHR", 38000, 57.120328, 16.153083, -1}, // Ruda, Sweden
};
void MapGUI::addVLF() void MapGUI::addVLF()
{ {
for (int i = 0; i < m_vlfTransmitters.size(); i++) for (int i = 0; i < VLFTransmitters::m_transmitters.size(); i++)
{ {
SWGSDRangel::SWGMapItem vlfMapItem; SWGSDRangel::SWGMapItem vlfMapItem;
// Need to suffix frequency, as there are multiple becaons with same callsign at different locations QString name = QString("%1").arg(VLFTransmitters::m_transmitters[i].m_callsign);
QString name = QString("%1").arg(m_vlfTransmitters[i].m_callsign);
vlfMapItem.setName(new QString(name)); vlfMapItem.setName(new QString(name));
vlfMapItem.setLatitude(m_vlfTransmitters[i].m_latitude); vlfMapItem.setLatitude(VLFTransmitters::m_transmitters[i].m_latitude);
vlfMapItem.setLongitude(m_vlfTransmitters[i].m_longitude); vlfMapItem.setLongitude(VLFTransmitters::m_transmitters[i].m_longitude);
vlfMapItem.setAltitude(0.0); vlfMapItem.setAltitude(0.0);
vlfMapItem.setImage(new QString("antenna.png")); vlfMapItem.setImage(new QString("antenna.png"));
vlfMapItem.setImageRotation(0); vlfMapItem.setImageRotation(0);
QString text = QString("VLF Transmitter\nCallsign: %1\nFrequency: %2 kHz") QString text = QString("VLF Transmitter\nCallsign: %1\nFrequency: %2 kHz")
.arg(m_vlfTransmitters[i].m_callsign) .arg(VLFTransmitters::m_transmitters[i].m_callsign)
.arg(m_vlfTransmitters[i].m_frequency/1000.0); .arg(VLFTransmitters::m_transmitters[i].m_frequency/1000.0);
if (VLFTransmitters::m_transmitters[i].m_power > 0) {
text.append(QString("\nPower: %1 kW").arg(VLFTransmitters::m_transmitters[i].m_power));
}
vlfMapItem.setText(new QString(text)); vlfMapItem.setText(new QString(text));
vlfMapItem.setModel(new QString("antenna.glb")); vlfMapItem.setModel(new QString("antenna.glb"));
vlfMapItem.setFixedPosition(true); vlfMapItem.setFixedPosition(true);
@ -535,7 +522,6 @@ void MapGUI::addVLF()
} }
} }
const QList<RadioTimeTransmitter> MapGUI::m_radioTimeTransmitters = { const QList<RadioTimeTransmitter> MapGUI::m_radioTimeTransmitters = {
{"MSF", 60000, 54.9075f, -3.27333f, 17}, // UK {"MSF", 60000, 54.9075f, -3.27333f, 17}, // UK
{"DCF77", 77500, 50.01611111f, 9.00805556f, 50}, // Germany {"DCF77", 77500, 50.01611111f, 9.00805556f, 50}, // Germany
@ -722,12 +708,17 @@ void MapGUI::addIonosonde()
m_giro = GIRO::create(); m_giro = GIRO::create();
if (m_giro) if (m_giro)
{ {
connect(m_giro, &GIRO::indexUpdated, this, &MapGUI::giroIndexUpdated);
connect(m_giro, &GIRO::dataUpdated, this, &MapGUI::giroDataUpdated); connect(m_giro, &GIRO::dataUpdated, this, &MapGUI::giroDataUpdated);
connect(m_giro, &GIRO::mufUpdated, this, &MapGUI::mufUpdated); connect(m_giro, &GIRO::mufUpdated, this, &MapGUI::mufUpdated);
connect(m_giro, &GIRO::foF2Updated, this, &MapGUI::foF2Updated); connect(m_giro, &GIRO::foF2Updated, this, &MapGUI::foF2Updated);
} }
} }
void MapGUI::giroIndexUpdated(const QList<GIRO::DataSet>& data)
{
}
void MapGUI::giroDataUpdated(const GIRO::GIROStationData& data) void MapGUI::giroDataUpdated(const GIRO::GIROStationData& data)
{ {
if (!data.m_station.isEmpty()) if (!data.m_station.isEmpty())
@ -761,6 +752,8 @@ void MapGUI::giroDataUpdated(const GIRO::GIROStationData& data)
ionosondeStationMapItem.setLabel(new QString(station->m_label)); ionosondeStationMapItem.setLabel(new QString(station->m_label));
ionosondeStationMapItem.setLabelAltitudeOffset(4.5); ionosondeStationMapItem.setLabelAltitudeOffset(4.5);
ionosondeStationMapItem.setAltitudeReference(1); ionosondeStationMapItem.setAltitudeReference(1);
ionosondeStationMapItem.setAvailableFrom(new QString(data.m_dateTime.toString(Qt::ISODateWithMs)));
ionosondeStationMapItem.setAvailableUntil(new QString(data.m_dateTime.addDays(3).toString(Qt::ISODateWithMs))); // Remove after data is too old
update(m_map, &ionosondeStationMapItem, "Ionosonde Stations"); update(m_map, &ionosondeStationMapItem, "Ionosonde Stations");
} }
} }
@ -786,6 +779,24 @@ void MapGUI::foF2Updated(const QJsonDocument& document)
} }
} }
void MapGUI::updateGIRO(const QDateTime& mapDateTime)
{
if (m_giro)
{
if (m_settings.m_displayMUF || m_settings.m_displayfoF2)
{
QString giroRunId = m_giro->getRunId(mapDateTime);
if (m_giroRunId.isEmpty() || (!giroRunId.isEmpty() && (giroRunId != m_giroRunId)))
{
m_giro->getMUF(giroRunId);
m_giro->getMUF(giroRunId);
m_giroRunId = giroRunId;
m_giroDateTime = mapDateTime;
}
}
}
}
void MapGUI::pathUpdated(const QString& radarPath, const QString& satellitePath) void MapGUI::pathUpdated(const QString& radarPath, const QString& satellitePath)
{ {
m_radarPath = radarPath; m_radarPath = radarPath;
@ -1683,6 +1694,7 @@ void MapGUI::displayToolbar()
ui->displayNASAGlobalImagery->setVisible(overlayButtons); ui->displayNASAGlobalImagery->setVisible(overlayButtons);
ui->displayMUF->setVisible(!narrow && m_settings.m_map3DEnabled); ui->displayMUF->setVisible(!narrow && m_settings.m_map3DEnabled);
ui->displayfoF2->setVisible(!narrow && m_settings.m_map3DEnabled); ui->displayfoF2->setVisible(!narrow && m_settings.m_map3DEnabled);
ui->save->setVisible(m_settings.m_map3DEnabled);
} }
void MapGUI::setEnableOverlay() void MapGUI::setEnableOverlay()
@ -1803,11 +1815,10 @@ void MapGUI::applyMap3DSettings(bool reloadMap)
m_polylineMapModel.allUpdated(); m_polylineMapModel.allUpdated();
} }
MapSettings::MapItemSettings *ionosondeItemSettings = getItemSettings("Ionosonde Stations"); MapSettings::MapItemSettings *ionosondeItemSettings = getItemSettings("Ionosonde Stations");
m_giro->getIndexPeriodically((m_settings.m_displayMUF || m_settings.m_displayfoF2) ? 15 : 0);
if (ionosondeItemSettings) { if (ionosondeItemSettings) {
m_giro->getDataPeriodically(ionosondeItemSettings->m_enabled ? 2 : 0); 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);
#else #else
ui->displayMUF->setVisible(false); ui->displayMUF->setVisible(false);
ui->displayfoF2->setVisible(false); ui->displayfoF2->setVisible(false);
@ -2211,7 +2222,7 @@ void MapGUI::on_displayMUF_clicked(bool checked)
m_settings.m_displayMUF = checked; m_settings.m_displayMUF = checked;
// Only call show if disabling, so we don't get two updates // 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) // (as getMUFPeriodically results in a call to showMUF when the data is available)
m_giro->getMUFPeriodically(m_settings.m_displayMUF ? 15 : 0); m_giro->getIndexPeriodically((m_settings.m_displayMUF || m_settings.m_displayfoF2) ? 15 : 0);
if (m_cesium && !m_settings.m_displayMUF) { if (m_cesium && !m_settings.m_displayMUF) {
m_cesium->showMUF(m_settings.m_displayMUF); m_cesium->showMUF(m_settings.m_displayMUF);
} }
@ -2226,7 +2237,7 @@ void MapGUI::on_displayfoF2_clicked(bool checked)
m_displayfoF2->setChecked(checked); m_displayfoF2->setChecked(checked);
} }
m_settings.m_displayfoF2 = checked; m_settings.m_displayfoF2 = checked;
m_giro->getfoF2Periodically(m_settings.m_displayfoF2 ? 15 : 0); m_giro->getIndexPeriodically((m_settings.m_displayMUF || m_settings.m_displayfoF2) ? 15 : 0);
if (m_cesium && !m_settings.m_displayfoF2) { if (m_cesium && !m_settings.m_displayfoF2) {
m_cesium->showfoF2(m_settings.m_displayfoF2); m_cesium->showfoF2(m_settings.m_displayfoF2);
} }
@ -2442,6 +2453,22 @@ void MapGUI::track3D(const QString& target)
} }
} }
void MapGUI::on_save_clicked()
{
if (m_cesium)
{
m_fileDialog.setAcceptMode(QFileDialog::AcceptSave);
m_fileDialog.setNameFilter("*.kml *.kmz");
if (m_fileDialog.exec())
{
QStringList fileNames = m_fileDialog.selectedFiles();
if (fileNames.size() > 0) {
m_cesium->save(fileNames[0], getDataDir());
}
}
}
}
void MapGUI::on_deleteAll_clicked() void MapGUI::on_deleteAll_clicked()
{ {
m_objectMapModel.removeAll(); m_objectMapModel.removeAll();
@ -2543,6 +2570,7 @@ void MapGUI::receivedCesiumEvent(const QJsonObject &obj)
bool canAnimate = obj.value("canAnimate").toBool(); bool canAnimate = obj.value("canAnimate").toBool();
bool shouldAnimate = obj.value("shouldAnimate").toBool(); bool shouldAnimate = obj.value("shouldAnimate").toBool();
m_map->setMapDateTime(mapDateTime, systemDateTime, canAnimate && shouldAnimate ? multiplier : 0.0); m_map->setMapDateTime(mapDateTime, systemDateTime, canAnimate && shouldAnimate ? multiplier : 0.0);
updateGIRO(mapDateTime);
} }
} }
else if (event == "link") else if (event == "link")
@ -2715,6 +2743,11 @@ void MapGUI::fullScreenRequested(QWebEngineFullScreenRequest fullScreenRequest)
ui->splitter->addWidget(ui->web); ui->splitter->addWidget(ui->web);
} }
} }
void MapGUI::downloadRequested(QWebEngineDownloadItem *download)
{
download->accept();
}
#endif #endif
void MapGUI::preferenceChanged(int elementType) void MapGUI::preferenceChanged(int elementType)
@ -2787,6 +2820,7 @@ void MapGUI::makeUIConnections()
QObject::connect(ui->displayfoF2, &ButtonSwitch::clicked, this, &MapGUI::on_displayfoF2_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->find, &QLineEdit::returnPressed, this, &MapGUI::on_find_returnPressed);
QObject::connect(ui->maidenhead, &QToolButton::clicked, this, &MapGUI::on_maidenhead_clicked); QObject::connect(ui->maidenhead, &QToolButton::clicked, this, &MapGUI::on_maidenhead_clicked);
QObject::connect(ui->save, &QToolButton::clicked, this, &MapGUI::on_save_clicked);
QObject::connect(ui->deleteAll, &QToolButton::clicked, this, &MapGUI::on_deleteAll_clicked); QObject::connect(ui->deleteAll, &QToolButton::clicked, this, &MapGUI::on_deleteAll_clicked);
QObject::connect(ui->displaySettings, &QToolButton::clicked, this, &MapGUI::on_displaySettings_clicked); QObject::connect(ui->displaySettings, &QToolButton::clicked, this, &MapGUI::on_displaySettings_clicked);
QObject::connect(ui->mapTypes, qOverload<int>(&QComboBox::currentIndexChanged), this, &MapGUI::on_mapTypes_currentIndexChanged); QObject::connect(ui->mapTypes, qOverload<int>(&QComboBox::currentIndexChanged), this, &MapGUI::on_mapTypes_currentIndexChanged);

View File

@ -24,6 +24,7 @@
#include <QQuickWidget> #include <QQuickWidget>
#include <QTextEdit> #include <QTextEdit>
#include <QJsonObject> #include <QJsonObject>
#include <QFileDialog>
#ifdef QT_WEBENGINE_FOUND #ifdef QT_WEBENGINE_FOUND
#include <QWebEngineFullScreenRequest> #include <QWebEngineFullScreenRequest>
#include <QWebEnginePage> #include <QWebEnginePage>
@ -194,6 +195,7 @@ private:
RollupState m_rollupState; RollupState m_rollupState;
bool m_doApplySettings; bool m_doApplySettings;
AvailableChannelOrFeatureList m_availableChannelOrFeatures; AvailableChannelOrFeatureList m_availableChannelOrFeatures;
QFileDialog m_fileDialog;
Map* m_map; Map* m_map;
MessageQueue m_inputMessageQueue; MessageQueue m_inputMessageQueue;
@ -217,6 +219,8 @@ private:
MapTileServer *m_mapTileServer; MapTileServer *m_mapTileServer;
QTimer m_redrawMapTimer; QTimer m_redrawMapTimer;
GIRO *m_giro; GIRO *m_giro;
QDateTime m_giroDateTime;
QString m_giroRunId;
QHash<QString, IonosondeStation *> m_ionosondeStations; QHash<QString, IonosondeStation *> m_ionosondeStations;
QSharedPointer<const QList<NavAid *>> m_navAids; QSharedPointer<const QList<NavAid *>> m_navAids;
QSharedPointer<const QList<Airspace *>> m_airspaces; QSharedPointer<const QList<Airspace *>> m_airspaces;
@ -279,6 +283,7 @@ private:
void openKiwiSDR(const QString& url); void openKiwiSDR(const QString& url);
void openSpyServer(const QString& url); void openSpyServer(const QString& url);
QString formatFrequency(qint64 frequency) const; QString formatFrequency(qint64 frequency) const;
void updateGIRO(const QDateTime& mapDateTime);
static QString getDataDir(); static QString getDataDir();
static const QList<RadioTimeTransmitter> m_radioTimeTransmitters; static const QList<RadioTimeTransmitter> m_radioTimeTransmitters;
@ -315,6 +320,7 @@ private slots:
void on_layersMenu_clicked(); void on_layersMenu_clicked();
void on_find_returnPressed(); void on_find_returnPressed();
void on_maidenhead_clicked(); void on_maidenhead_clicked();
void on_save_clicked();
void on_deleteAll_clicked(); void on_deleteAll_clicked();
void on_displaySettings_clicked(); void on_displaySettings_clicked();
void on_mapTypes_currentIndexChanged(int index); void on_mapTypes_currentIndexChanged(int index);
@ -331,9 +337,11 @@ private slots:
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
void loadingChanged(const QWebEngineLoadingInfo &loadingInfo); void loadingChanged(const QWebEngineLoadingInfo &loadingInfo);
#endif #endif
void downloadRequested(QWebEngineDownloadItem *download);
#endif #endif
void statusChanged(QQuickWidget::Status status); void statusChanged(QQuickWidget::Status status);
void preferenceChanged(int elementType); void preferenceChanged(int elementType);
void giroIndexUpdated(const QList<GIRO::DataSet>& data);
void giroDataUpdated(const GIRO::GIROStationData& data); void giroDataUpdated(const GIRO::GIROStationData& data);
void mufUpdated(const QJsonDocument& document); void mufUpdated(const QJsonDocument& document);
void foF2Updated(const QJsonDocument& document); void foF2Updated(const QJsonDocument& document);

View File

@ -424,6 +424,20 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QToolButton" name="save">
<property name="toolTip">
<string>Save to .kml</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/save.png</normaloff>:/save.png</iconset>
</property>
</widget>
</item>
<item> <item>
<widget class="QToolButton" name="deleteAll"> <widget class="QToolButton" name="deleteAll">
<property name="toolTip"> <property name="toolTip">

View File

@ -96,6 +96,11 @@ void ObjectMapItem::update(SWGSDRangel::SWGMapItem *mapItem)
updateTrack(mapItem->getTrack()); updateTrack(mapItem->getTrack());
updatePredictedTrack(mapItem->getPredictedTrack()); updatePredictedTrack(mapItem->getPredictedTrack());
} }
if (mapItem->getAvailableFrom()) {
m_availableFrom = QDateTime::fromString(*mapItem->getAvailableFrom(), Qt::ISODateWithMs);
} else {
m_availableFrom = QDateTime();
}
if (mapItem->getAvailableUntil()) { if (mapItem->getAvailableUntil()) {
m_availableUntil = QDateTime::fromString(*mapItem->getAvailableUntil(), Qt::ISODateWithMs); m_availableUntil = QDateTime::fromString(*mapItem->getAvailableUntil(), Qt::ISODateWithMs);
} else { } else {

View File

@ -61,6 +61,7 @@ protected:
float m_latitude; // Position for label float m_latitude; // Position for label
float m_longitude; float m_longitude;
float m_altitude; // In metres float m_altitude; // In metres
QDateTime m_availableFrom; // Date & time this item is visible from. Invalid date/time is forever
QDateTime m_availableUntil; // Date & time this item is visible until (for 3D map). Invalid date/time is forever QDateTime m_availableUntil; // Date & time this item is visible until (for 3D map). Invalid date/time is forever
}; };

View File

@ -41,6 +41,7 @@ const QStringList MapSettings::m_pipeTypes = {
QStringLiteral("Radiosonde"), QStringLiteral("Radiosonde"),
QStringLiteral("StarTracker"), QStringLiteral("StarTracker"),
QStringLiteral("SatelliteTracker"), QStringLiteral("SatelliteTracker"),
QStringLiteral("SID"),
QStringLiteral("VORLocalizer") QStringLiteral("VORLocalizer")
}; };
@ -57,6 +58,7 @@ const QStringList MapSettings::m_pipeURIs = {
QStringLiteral("sdrangel.feature.radiosonde"), QStringLiteral("sdrangel.feature.radiosonde"),
QStringLiteral("sdrangel.feature.startracker"), QStringLiteral("sdrangel.feature.startracker"),
QStringLiteral("sdrangel.feature.satellitetracker"), QStringLiteral("sdrangel.feature.satellitetracker"),
QStringLiteral("sdrangel.feature.sid"),
QStringLiteral("sdrangel.feature.vorlocalizer") QStringLiteral("sdrangel.feature.vorlocalizer")
}; };
@ -125,6 +127,7 @@ MapSettings::MapSettings() :
stationSettings->m_display3DTrack = false; stationSettings->m_display3DTrack = false;
m_itemSettings.insert("Station", stationSettings); m_itemSettings.insert("Station", stationSettings);
m_itemSettings.insert("VORLocalizer", new MapItemSettings("VORLocalizer", true, QColor(255, 255, 0), false, true, 11)); m_itemSettings.insert("VORLocalizer", new MapItemSettings("VORLocalizer", true, QColor(255, 255, 0), false, true, 11));
m_itemSettings.insert("SID", new MapItemSettings("SID", true, QColor(255, 255, 0), false, true, 3));
MapItemSettings *ionosondeItemSettings = new MapItemSettings("Ionosonde Stations", true, QColor(255, 255, 0), false, true, 4); MapItemSettings *ionosondeItemSettings = new MapItemSettings("Ionosonde Stations", true, QColor(255, 255, 0), false, true, 4);
ionosondeItemSettings->m_display2DIcon = false; ionosondeItemSettings->m_display2DIcon = false;

View File

@ -28,66 +28,10 @@
#endif #endif
#include "util/units.h" #include "util/units.h"
#include "gui/colordialog.h"
#include "mapsettingsdialog.h" #include "mapsettingsdialog.h"
#include "maplocationdialog.h" #include "maplocationdialog.h"
#include "mapcolordialog.h"
static QString rgbToColor(quint32 rgb)
{
QColor color = QColor::fromRgba(rgb);
return QString("%1,%2,%3").arg(color.red()).arg(color.green()).arg(color.blue());
}
static QString backgroundCSS(quint32 rgb)
{
// Must specify a border, otherwise we end up with a gradient instead of solid background
return QString("QToolButton { background-color: rgb(%1); border: none; }").arg(rgbToColor(rgb));
}
static QString noColorCSS()
{
return "QToolButton { background-color: black; border: none; }";
}
MapColorGUI::MapColorGUI(QTableWidget *table, int row, int col, bool noColor, quint32 color) :
m_noColor(noColor),
m_color(color)
{
m_colorButton = new QToolButton(table);
m_colorButton->setFixedSize(22, 22);
if (!m_noColor)
{
m_colorButton->setStyleSheet(backgroundCSS(m_color));
}
else
{
m_colorButton->setStyleSheet(noColorCSS());
m_colorButton->setText("-");
}
table->setCellWidget(row, col, m_colorButton);
connect(m_colorButton, &QToolButton::clicked, this, &MapColorGUI::on_color_clicked);
}
void MapColorGUI::on_color_clicked()
{
MapColorDialog dialog(QColor::fromRgba(m_color), m_colorButton);
if (dialog.exec() == QDialog::Accepted)
{
m_noColor = dialog.noColorSelected();
if (!m_noColor)
{
m_colorButton->setText("");
m_color = dialog.selectedColor().rgba();
m_colorButton->setStyleSheet(backgroundCSS(m_color));
}
else
{
m_colorButton->setText("-");
m_colorButton->setStyleSheet(noColorCSS());
}
}
}
MapItemSettingsGUI::MapItemSettingsGUI(QTableWidget *table, int row, MapSettings::MapItemSettings *settings) : MapItemSettingsGUI::MapItemSettingsGUI(QTableWidget *table, int row, MapSettings::MapItemSettings *settings) :
m_track2D(table, row, MapSettingsDialog::COL_2D_TRACK, !settings->m_display2DTrack, settings->m_2DTrackColor), m_track2D(table, row, MapSettingsDialog::COL_2D_TRACK, !settings->m_display2DTrack, settings->m_2DTrackColor),

View File

@ -27,6 +27,7 @@
#include <QProgressDialog> #include <QProgressDialog>
#include "gui/httpdownloadmanagergui.h" #include "gui/httpdownloadmanagergui.h"
#include "gui/tablecolorchooser.h"
#include "util/openaip.h" #include "util/openaip.h"
#include "util/ourairportsdb.h" #include "util/ourairportsdb.h"
#include "util/waypoints.h" #include "util/waypoints.h"
@ -34,34 +35,15 @@
#include "ui_mapsettingsdialog.h" #include "ui_mapsettingsdialog.h"
#include "mapsettings.h" #include "mapsettings.h"
class MapColorGUI : public QObject {
Q_OBJECT
public:
MapColorGUI(QTableWidget *table, int row, int col, bool noColor, quint32 color);
public slots:
void on_color_clicked();
private:
QToolButton *m_colorButton;
public:
// Have copies of settings, so we don't change unless main dialog is accepted
bool m_noColor;
quint32 m_color;
};
class MapItemSettingsGUI : public QObject { class MapItemSettingsGUI : public QObject {
Q_OBJECT Q_OBJECT
public: public:
MapItemSettingsGUI(QTableWidget *table, int row, MapSettings::MapItemSettings *settings); MapItemSettingsGUI(QTableWidget *table, int row, MapSettings::MapItemSettings *settings);
MapColorGUI m_track2D; TableColorChooser m_track2D;
MapColorGUI m_point3D; TableColorChooser m_point3D;
MapColorGUI m_track3D; TableColorChooser m_track3D;
QSpinBox *m_minZoom; QSpinBox *m_minZoom;
QSpinBox *m_minPixels; QSpinBox *m_minPixels;
QDoubleSpinBox *m_labelScale; QDoubleSpinBox *m_labelScale;

View File

@ -14,10 +14,11 @@ On top of this, it can plot data from other plugins, such as:
* Weather balloons from the Radiosonde feature, * Weather balloons from the Radiosonde feature,
* RF Heat Maps from the Heap Map channel, * RF Heat Maps from the Heap Map channel,
* Radials and estimated position from the VOR localizer feature, * Radials and estimated position from the VOR localizer feature,
* ILS course line and glide path from the ILS Demodulator. * ILS course line and glide path from the ILS Demodulator,
* DSC geographic call areas. * DSC geographic call areas,
* SID paths.
As well as internet data sources: As well as internet and built-in data sources:
* AM, FM and DAB transmitters in the UK and DAB transmitters in France, * AM, FM and DAB transmitters in the UK and DAB transmitters in France,
* Airports, NavAids and airspaces, * Airports, NavAids and airspaces,
@ -40,7 +41,7 @@ It can also create tracks showing the path aircraft, ships, radiosondes and APRS
![3D Map feature](../../../doc/img/Map_plugin_apt.png) ![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 (20). 3D Models are not included with SDRangel. They must be downloaded by pressing the Download 3D Models button in the Display Settings dialog (21).
<h2>Interface</h2> <h2>Interface</h2>
@ -143,14 +144,14 @@ This is only supported on 2D raster maps and the 3D map.
<h3>11: Display MUF Contours</h3> <h3>11: 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. 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. The contours will be updated every 15 minutes. MUF contour data is available for the preceeding 5 days.
![MUF contours](../../../doc/img/Map_plugin_muf.png) ![MUF contours](../../../doc/img/Map_plugin_muf.png)
<h3>12: Display coF2 Contours</h3> <h3>12: 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. 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. The contours will be updated every 15 minutes. coF2 contour data is available for the preceeding 5 days.
<h3>13: Display NASA GIBS Data</h3> <h3>13: Display NASA GIBS Data</h3>
@ -185,11 +186,19 @@ When checked, displays the track (taken or predicted) for the selected object.
When checked, displays the track (taken or predicted) for the all objects. When checked, displays the track (taken or predicted) for the all objects.
<h3>19: Delete</h3> <h3>19: Save to .kml</h3>
When clicked, items and tracks on the map will be saved to a [KML](https://en.wikipedia.org/wiki/Keyhole_Markup_Language) (.kml or .kmz) file, for use in other applications.
Note that the KML format requires 3D models in the Collada (.dae) format. However, SDRangel's models are in glTF (.glb or .gltf) format.
If you wish to view the models in a KML viewer, you will need to manually convert them. Note that you should still be able to view tracks without the models.
Note that the .glbe files cannot be converted to .dae.
<h3>20: Delete</h3>
When clicked, all items will be deleted from the map. When clicked, all items will be deleted from the map.
<h3>20: Display settings</h3> <h3>21: Display settings</h3>
When clicked, opens the Map Display Settings dialog: When clicked, opens the Map Display Settings dialog:
@ -281,6 +290,17 @@ MUF and foF2 can be displayed as contours:
The contours can be clicked on which will display the data for that contour in the info box. The contours can be clicked on which will display the data for that contour in the info box.
<h4>VLF Transmitters</h4>
The Map contains a built-in list of VLF transmitters. This can be overridden by a user-defined list contained in a file `vlftransmitters.csv` in the application data directory.
The file must have the following columns:
```
Callsign,Frequency,Latitude,Longitude,Power
GQD,19580,54.911643,-3.278456,10
```
<h2>Attribution</h2> <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/ 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/

View File

@ -53,7 +53,7 @@ void WebServer::incomingConnection(qintptr socket)
// Don't include leading or trailing / in from // Don't include leading or trailing / in from
void WebServer::addPathSubstitution(const QString &from, const QString &to) void WebServer::addPathSubstitution(const QString &from, const QString &to)
{ {
qDebug() << "Mapping " << from << " to " << to; //qDebug() << "Mapping " << from << " to " << to;
m_pathSubstitutions.insert(from, to); m_pathSubstitutions.insert(from, to);
} }
@ -125,7 +125,7 @@ void WebServer::readClient()
if (socket->canReadLine()) if (socket->canReadLine())
{ {
QString line = socket->readLine(); QString line = socket->readLine();
qDebug() << "WebServer HTTP Request: " << line; //qDebug() << "WebServer HTTP Request: " << line;
QStringList tokens = QString(line).split(QRegularExpression("[ \r\n][ \r\n]*")); QStringList tokens = QString(line).split(QRegularExpression("[ \r\n][ \r\n]*"));
if (tokens[0] == "GET") if (tokens[0] == "GET")

View File

@ -161,7 +161,7 @@ Selects which image / wavelength to view.
| 211 Å | Active corona | | 211 Å | Active corona |
| 304 Å | Chromosphere, transition region | | 304 Å | Chromosphere, transition region |
| 335 Å | Active corona | | 335 Å | Active corona |
| 1600 Å | Transition region, uppoer photoshere | | 1600 Å | Transition region, upper photoshere |
| 1700 Å | Temperature minimum, photosphere | | 1700 Å | Temperature minimum, photosphere |
[Ref](https://sdo.gsfc.nasa.gov/data/channels.php) [Ref](https://sdo.gsfc.nasa.gov/data/channels.php)

View File

@ -21,22 +21,36 @@
#include <QUrl> #include <QUrl>
#include <QUrlQuery> #include <QUrlQuery>
#include <QNetworkReply> #include <QNetworkReply>
#include <QNetworkDiskCache>
#include <QJsonObject> #include <QJsonObject>
GIRO::GIRO() GIRO::GIRO()
{ {
connect(&m_indexTimer, &QTimer::timeout, this, &GIRO::getIndex);
connect(&m_dataTimer, &QTimer::timeout, this, &GIRO::getData); connect(&m_dataTimer, &QTimer::timeout, this, &GIRO::getData);
connect(&m_mufTimer, &QTimer::timeout, this, &GIRO::getMUF); connect(&m_mufTimer, &QTimer::timeout, this, qOverload<>(&GIRO::getMUF));
connect(&m_foF2Timer, &QTimer::timeout, this, &GIRO::getfoF2); connect(&m_foF2Timer, &QTimer::timeout, this, qOverload<>(&GIRO::getfoF2));
m_networkManager = new QNetworkAccessManager(); m_networkManager = new QNetworkAccessManager();
connect(m_networkManager, &QNetworkAccessManager::finished, this, &GIRO::handleReply); connect(m_networkManager, &QNetworkAccessManager::finished, this, &GIRO::handleReply);
QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
QDir writeableDir(locations[0]);
if (!writeableDir.mkpath(QStringLiteral("cache") + QDir::separator() + QStringLiteral("giro"))) {
qDebug() << "Failed to create cache/giro";
}
m_cache = new QNetworkDiskCache();
m_cache->setCacheDirectory(locations[0] + QDir::separator() + QStringLiteral("cache") + QDir::separator() + QStringLiteral("giro"));
m_cache->setMaximumCacheSize(100000000);
m_networkManager->setCache(m_cache);
} }
GIRO::~GIRO() GIRO::~GIRO()
{ {
disconnect(&m_indexTimer, &QTimer::timeout, this, &GIRO::getIndex);
disconnect(&m_dataTimer, &QTimer::timeout, this, &GIRO::getData); disconnect(&m_dataTimer, &QTimer::timeout, this, &GIRO::getData);
disconnect(&m_mufTimer, &QTimer::timeout, this, &GIRO::getMUF); disconnect(&m_mufTimer, &QTimer::timeout, this, qOverload<>(&GIRO::getMUF));
disconnect(&m_foF2Timer, &QTimer::timeout, this, &GIRO::getfoF2); disconnect(&m_foF2Timer, &QTimer::timeout, this, qOverload<>(&GIRO::getfoF2));
disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &GIRO::handleReply); disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &GIRO::handleReply);
delete m_networkManager; delete m_networkManager;
} }
@ -54,6 +68,20 @@ GIRO* GIRO::create(const QString& service)
} }
} }
void GIRO::getIndexPeriodically(int periodInMins)
{
if (periodInMins > 0)
{
m_indexTimer.setInterval(periodInMins*60*1000);
m_indexTimer.start();
getIndex();
}
else
{
m_indexTimer.stop();
}
}
void GIRO::getDataPeriodically(int periodInMins) void GIRO::getDataPeriodically(int periodInMins)
{ {
if (periodInMins > 0) if (periodInMins > 0)
@ -96,21 +124,37 @@ void GIRO::getfoF2Periodically(int periodInMins)
} }
} }
void GIRO::getIndex()
{
QUrl url(QString("https://prop.kc2g.com/api/available_nowcasts.json?days=5"));
m_networkManager->get(QNetworkRequest(url));
}
void GIRO::getData() void GIRO::getData()
{ {
QUrl url(QString("https://prop.kc2g.com/api/stations.json")); QUrl url(QString("https://prop.kc2g.com/api/stations.json"));
m_networkManager->get(QNetworkRequest(url)); m_networkManager->get(QNetworkRequest(url));
} }
void GIRO::getfoF2()
{
getMUF("current");
}
void GIRO::getMUF() void GIRO::getMUF()
{ {
QUrl url(QString("https://prop.kc2g.com/renders/current/mufd-normal-now.geojson")); getMUF("current");
}
void GIRO::getfoF2(const QString& runId)
{
QUrl url(QString("https://prop.kc2g.com/renders/%1/fof2-normal-now.geojson").arg(runId));
m_networkManager->get(QNetworkRequest(url)); m_networkManager->get(QNetworkRequest(url));
} }
void GIRO::getfoF2() void GIRO::getMUF(const QString& runId)
{ {
QUrl url(QString("https://prop.kc2g.com/renders/current/fof2-normal-now.geojson")); QUrl url(QString("https://prop.kc2g.com/renders/%1/mufd-normal-now.geojson").arg(runId));
m_networkManager->get(QNetworkRequest(url)); m_networkManager->get(QNetworkRequest(url));
} }
@ -132,86 +176,26 @@ void GIRO::handleReply(QNetworkReply* reply)
{ {
QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
if (reply->url().fileName() == "stations.json") QString fileName = reply->url().fileName();
if (fileName == "available_nowcasts.json")
{ {
if (document.isArray()) handleIndex(document);
{
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") else if (fileName == "stations.json")
{
handleStations(document);
}
else if (fileName == "mufd-normal-now.geojson")
{ {
emit mufUpdated(document); emit mufUpdated(document);
} }
else if (reply->url().fileName() == "fof2-normal-now.geojson") else if (fileName == "fof2-normal-now.geojson")
{ {
emit foF2Updated(document); emit foF2Updated(document);
} }
else else
{ {
qDebug() << "GIRO::handleReply: unexpected filename: " << reply->url().fileName(); qDebug() << "GIRO::handleReply: unexpected filename: " << fileName;
} }
} }
else else
@ -225,3 +209,116 @@ void GIRO::handleReply(QNetworkReply* reply)
qDebug() << "GIRO::handleReply: reply is null"; qDebug() << "GIRO::handleReply: reply is null";
} }
} }
void GIRO::handleStations(QJsonDocument& document)
{
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;
}
}
void GIRO::handleIndex(QJsonDocument& document)
{
if (document.isArray())
{
QJsonArray array = document.array();
m_index.clear();
for (auto valRef : array)
{
if (valRef.isObject())
{
QJsonObject obj = valRef.toObject();
DataSet item;
int ts = obj.value(QStringLiteral("ts")).toInt();
item.m_dateTime = QDateTime::fromSecsSinceEpoch(ts);
item.m_runId = QString::number(obj.value(QStringLiteral("run_id")).toInt());
qDebug() << item.m_dateTime << item.m_runId;
m_index.append(item);
}
}
emit indexUpdated(m_index);
}
}
QString GIRO::getRunId(const QDateTime& dateTime)
{
// Index is ordered newest first
for (int i = 0; i < m_index.size(); i++)
{
if (dateTime > m_index[i].m_dateTime) {
return m_index[i].m_runId;
}
}
return "";
}

View File

@ -26,6 +26,7 @@
class QNetworkAccessManager; class QNetworkAccessManager;
class QNetworkReply; class QNetworkReply;
class QNetworkDiskCache;
// GIRO - Global Ionosphere Radio Observatory // GIRO - Global Ionosphere Radio Observatory
// Gets MUFD, TEC, foF2 and other data for various stations around the world // Gets MUFD, TEC, foF2 and other data for various stations around the world
@ -66,14 +67,26 @@ public:
} }
}; };
struct DataSet {
QDateTime m_dateTime;
QString m_runId;
};
static GIRO* create(const QString& service="prop.kc2g.com"); static GIRO* create(const QString& service="prop.kc2g.com");
~GIRO(); ~GIRO();
void getDataPeriodically(int periodInMins); void getIndexPeriodically(int periodInMins=15);
void getMUFPeriodically(int periodInMins); void getDataPeriodically(int periodInMins=2);
void getfoF2Periodically(int periodInMins); void getMUFPeriodically(int periodInMins=15);
void getfoF2Periodically(int periodInMins=15);
private slots: void getMUF(const QString& runId);
void getfoF2(const QString& runId);
QString getRunId(const QDateTime& dateTime);
public slots:
void getIndex();
void getData(); void getData();
void getMUF(); void getMUF();
void getfoF2(); void getfoF2();
@ -82,17 +95,23 @@ private slots:
void handleReply(QNetworkReply* reply); void handleReply(QNetworkReply* reply);
signals: signals:
void indexUpdated(const QList<DataSet>& data);
void dataUpdated(const GIROStationData& data); // Called when new data available. void dataUpdated(const GIROStationData& data); // Called when new data available.
void mufUpdated(const QJsonDocument& doc); void mufUpdated(const QJsonDocument& doc);
void foF2Updated(const QJsonDocument& doc); void foF2Updated(const QJsonDocument& doc);
private: private:
bool containsNonNull(const QJsonObject& obj, const QString &key) const; bool containsNonNull(const QJsonObject& obj, const QString &key) const;
void handleIndex(QJsonDocument& document);
void handleStations(QJsonDocument& document);
QTimer m_indexTimer;
QTimer m_dataTimer; // Timer for periodic updates QTimer m_dataTimer; // Timer for periodic updates
QTimer m_mufTimer; QTimer m_mufTimer;
QTimer m_foF2Timer; QTimer m_foF2Timer;
QNetworkAccessManager *m_networkManager; QNetworkAccessManager *m_networkManager;
QNetworkDiskCache *m_cache;
QList<DataSet> m_index;
}; };