mirror of
https://github.com/f4exb/sdrangel.git
synced 2024-12-23 01:55:48 -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:
parent
c137faf012
commit
4955e6ab08
@ -54,7 +54,6 @@ if(NOT SERVER_MODE)
|
||||
mapibpbeacondialog.ui
|
||||
mapradiotimedialog.cpp
|
||||
mapradiotimedialog.ui
|
||||
mapcolordialog.cpp
|
||||
mapmodel.cpp
|
||||
mapitem.cpp
|
||||
mapwebsocketserver.cpp
|
||||
@ -75,7 +74,6 @@ if(NOT SERVER_MODE)
|
||||
mapbeacondialog.h
|
||||
mapibpbeacon.h
|
||||
mapradiotimedialog.h
|
||||
mapcolordialog.h
|
||||
mapmodel.h
|
||||
mapitem.h
|
||||
mapwebsocketserver.h
|
||||
|
@ -275,3 +275,13 @@ void CesiumInterface::setPosition(const QGeoCoordinate& position)
|
||||
{
|
||||
m_czml.setPosition(position);
|
||||
}
|
||||
|
||||
void CesiumInterface::save(const QString& filename, const QString& dataDir)
|
||||
{
|
||||
QJsonObject obj {
|
||||
{"command", "save"},
|
||||
{"filename", filename},
|
||||
{"dataDir", dataDir}
|
||||
};
|
||||
send(obj);
|
||||
}
|
||||
|
@ -80,6 +80,7 @@ public:
|
||||
void update(PolygonMapItem *mapItem);
|
||||
void update(PolylineMapItem *mapItem);
|
||||
void setPosition(const QGeoCoordinate& position);
|
||||
void save(const QString& filename, const QString& dataDir);
|
||||
|
||||
protected:
|
||||
|
||||
|
@ -264,7 +264,9 @@ QJsonObject CZML::update(ObjectMapItem *mapItem, bool isTarget, bool isSelected)
|
||||
const QStringList heightReferences = {"NONE", "CLAMP_TO_GROUND", "RELATIVE_TO_GROUND", "NONE"};
|
||||
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);
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
else
|
||||
|
@ -364,6 +364,52 @@
|
||||
["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
|
||||
// (CZML doesn't support camera control, for example)
|
||||
// and sending events back to it
|
||||
@ -469,30 +515,40 @@
|
||||
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; });
|
||||
).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") {
|
||||
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; });
|
||||
).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") {
|
||||
layers.get(command.layer).show = command.show;
|
||||
@ -639,7 +695,7 @@
|
||||
czmlStream.process(command);
|
||||
} else {
|
||||
var promise = Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, [position]);
|
||||
Cesium.when(promise, function(updatedPositions) {
|
||||
Cesium.when(promise, function (updatedPositions) {
|
||||
if (height < updatedPositions[0].height) {
|
||||
if (size == 3) {
|
||||
command.position.cartographicDegrees[2] = updatedPositions[0].height;
|
||||
@ -648,7 +704,7 @@
|
||||
}
|
||||
}
|
||||
czmlStream.process(command);
|
||||
}, function() {
|
||||
}, function () {
|
||||
console.log(`Terrain doesn't support sampleTerrainMostDetailed`);
|
||||
czmlStream.process(command);
|
||||
});
|
||||
@ -657,47 +713,47 @@
|
||||
console.log(`Can't currently use altitudeReference when more than one position`);
|
||||
czmlStream.process(command);
|
||||
}
|
||||
} else if ( (command.hasOwnProperty('polygon') && command.polygon.hasOwnProperty('altitudeReference'))
|
||||
|| (command.hasOwnProperty('polyline') && command.polyline.hasOwnProperty('altitudeReference'))) {
|
||||
} else if ((command.hasOwnProperty('polygon') && command.polygon.hasOwnProperty('altitudeReference'))
|
||||
|| (command.hasOwnProperty('polyline') && command.polyline.hasOwnProperty('altitudeReference'))) {
|
||||
// Support per vertex height reference in polygons and CLIP_TO_GROUND in polylines
|
||||
var prim = command.hasOwnProperty('polygon') ? command.polygon : command.polyline;
|
||||
var clipToGround = prim.altitudeReference == "CLIP_TO_GROUND";
|
||||
var clampToGround = prim.altitudeReference == "CLAMP_TO_GROUND";
|
||||
var size = prim.positions.cartographicDegrees.length;
|
||||
var positionCount = size/3;
|
||||
var positionCount = size / 3;
|
||||
var positions = new Array(positionCount);
|
||||
if (viewer.terrainProvider instanceof Cesium.EllipsoidTerrainProvider) {
|
||||
if (clampToGround) {
|
||||
for (let i = 0; i < positionCount; i++) {
|
||||
prim.positions.cartographicDegrees[i*3+2] = 0;
|
||||
prim.positions.cartographicDegrees[i * 3 + 2] = 0;
|
||||
}
|
||||
} else if (clipToGround) {
|
||||
for (let i = 0; i < positionCount; i++) {
|
||||
if (prim.positions.cartographicDegrees[i*3+2] < 0) {
|
||||
prim.positions.cartographicDegrees[i*3+2] = 0;
|
||||
if (prim.positions.cartographicDegrees[i * 3 + 2] < 0) {
|
||||
prim.positions.cartographicDegrees[i * 3 + 2] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
czmlStream.process(command);
|
||||
} else {
|
||||
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);
|
||||
Cesium.when(promise, function(updatedPositions) {
|
||||
Cesium.when(promise, function (updatedPositions) {
|
||||
if (clampToGround) {
|
||||
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) {
|
||||
for (let i = 0; i < positionCount; i++) {
|
||||
if (prim.positions.cartographicDegrees[i*3+2] < updatedPositions[i].height) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
czmlStream.process(command);
|
||||
}, function() {
|
||||
}, function () {
|
||||
console.log(`Terrain doesn't support sampleTerrainMostDetailed`);
|
||||
czmlStream.process(command);
|
||||
});
|
||||
@ -705,7 +761,20 @@
|
||||
} else {
|
||||
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 {
|
||||
console.log(`Unknown command ${command.command}`);
|
||||
}
|
||||
@ -759,10 +828,13 @@
|
||||
Cesium.knockout.getObservable(viewer.clockViewModel, 'multiplier').subscribe(function(multiplier) {
|
||||
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) {
|
||||
//reportClock();
|
||||
//});
|
||||
setInterval(function () {
|
||||
reportClock();
|
||||
}, 1000);
|
||||
viewer.timeline.addEventListener('settime', reportClock, false);
|
||||
|
||||
socket.onopen = () => {
|
||||
|
@ -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();
|
||||
}
|
@ -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
|
@ -48,6 +48,7 @@
|
||||
#include "util/maidenhead.h"
|
||||
#include "util/morse.h"
|
||||
#include "util/navtex.h"
|
||||
#include "util/vlftransmitters.h"
|
||||
#include "maplocationdialog.h"
|
||||
#include "mapmaidenheaddialog.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, &QWebEngineView::renderProcessTerminated, this, &MapGUI::renderProcessTerminated);
|
||||
#endif
|
||||
|
||||
QWebEngineProfile *profile = QWebEngineProfile::defaultProfile();
|
||||
connect(profile, &QWebEngineProfile::downloadRequested, this, &MapGUI::downloadRequested);
|
||||
#endif
|
||||
|
||||
// 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()
|
||||
{
|
||||
for (int i = 0; i < m_vlfTransmitters.size(); i++)
|
||||
for (int i = 0; i < VLFTransmitters::m_transmitters.size(); i++)
|
||||
{
|
||||
SWGSDRangel::SWGMapItem vlfMapItem;
|
||||
// Need to suffix frequency, as there are multiple becaons with same callsign at different locations
|
||||
QString name = QString("%1").arg(m_vlfTransmitters[i].m_callsign);
|
||||
QString name = QString("%1").arg(VLFTransmitters::m_transmitters[i].m_callsign);
|
||||
vlfMapItem.setName(new QString(name));
|
||||
vlfMapItem.setLatitude(m_vlfTransmitters[i].m_latitude);
|
||||
vlfMapItem.setLongitude(m_vlfTransmitters[i].m_longitude);
|
||||
vlfMapItem.setLatitude(VLFTransmitters::m_transmitters[i].m_latitude);
|
||||
vlfMapItem.setLongitude(VLFTransmitters::m_transmitters[i].m_longitude);
|
||||
vlfMapItem.setAltitude(0.0);
|
||||
vlfMapItem.setImage(new QString("antenna.png"));
|
||||
vlfMapItem.setImageRotation(0);
|
||||
QString text = QString("VLF Transmitter\nCallsign: %1\nFrequency: %2 kHz")
|
||||
.arg(m_vlfTransmitters[i].m_callsign)
|
||||
.arg(m_vlfTransmitters[i].m_frequency/1000.0);
|
||||
.arg(VLFTransmitters::m_transmitters[i].m_callsign)
|
||||
.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.setModel(new QString("antenna.glb"));
|
||||
vlfMapItem.setFixedPosition(true);
|
||||
@ -535,7 +522,6 @@ void MapGUI::addVLF()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const QList<RadioTimeTransmitter> MapGUI::m_radioTimeTransmitters = {
|
||||
{"MSF", 60000, 54.9075f, -3.27333f, 17}, // UK
|
||||
{"DCF77", 77500, 50.01611111f, 9.00805556f, 50}, // Germany
|
||||
@ -722,12 +708,17 @@ void MapGUI::addIonosonde()
|
||||
m_giro = GIRO::create();
|
||||
if (m_giro)
|
||||
{
|
||||
connect(m_giro, &GIRO::indexUpdated, this, &MapGUI::giroIndexUpdated);
|
||||
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::giroIndexUpdated(const QList<GIRO::DataSet>& data)
|
||||
{
|
||||
}
|
||||
|
||||
void MapGUI::giroDataUpdated(const GIRO::GIROStationData& data)
|
||||
{
|
||||
if (!data.m_station.isEmpty())
|
||||
@ -761,6 +752,8 @@ void MapGUI::giroDataUpdated(const GIRO::GIROStationData& data)
|
||||
ionosondeStationMapItem.setLabel(new QString(station->m_label));
|
||||
ionosondeStationMapItem.setLabelAltitudeOffset(4.5);
|
||||
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");
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
{
|
||||
m_radarPath = radarPath;
|
||||
@ -1683,6 +1694,7 @@ void MapGUI::displayToolbar()
|
||||
ui->displayNASAGlobalImagery->setVisible(overlayButtons);
|
||||
ui->displayMUF->setVisible(!narrow && m_settings.m_map3DEnabled);
|
||||
ui->displayfoF2->setVisible(!narrow && m_settings.m_map3DEnabled);
|
||||
ui->save->setVisible(m_settings.m_map3DEnabled);
|
||||
}
|
||||
|
||||
void MapGUI::setEnableOverlay()
|
||||
@ -1803,11 +1815,10 @@ void MapGUI::applyMap3DSettings(bool reloadMap)
|
||||
m_polylineMapModel.allUpdated();
|
||||
}
|
||||
MapSettings::MapItemSettings *ionosondeItemSettings = getItemSettings("Ionosonde Stations");
|
||||
m_giro->getIndexPeriodically((m_settings.m_displayMUF || m_settings.m_displayfoF2) ? 15 : 0);
|
||||
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);
|
||||
#else
|
||||
ui->displayMUF->setVisible(false);
|
||||
ui->displayfoF2->setVisible(false);
|
||||
@ -2211,7 +2222,7 @@ 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);
|
||||
m_giro->getIndexPeriodically((m_settings.m_displayMUF || m_settings.m_displayfoF2) ? 15 : 0);
|
||||
if (m_cesium && !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_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) {
|
||||
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()
|
||||
{
|
||||
m_objectMapModel.removeAll();
|
||||
@ -2543,6 +2570,7 @@ void MapGUI::receivedCesiumEvent(const QJsonObject &obj)
|
||||
bool canAnimate = obj.value("canAnimate").toBool();
|
||||
bool shouldAnimate = obj.value("shouldAnimate").toBool();
|
||||
m_map->setMapDateTime(mapDateTime, systemDateTime, canAnimate && shouldAnimate ? multiplier : 0.0);
|
||||
updateGIRO(mapDateTime);
|
||||
}
|
||||
}
|
||||
else if (event == "link")
|
||||
@ -2715,6 +2743,11 @@ void MapGUI::fullScreenRequested(QWebEngineFullScreenRequest fullScreenRequest)
|
||||
ui->splitter->addWidget(ui->web);
|
||||
}
|
||||
}
|
||||
|
||||
void MapGUI::downloadRequested(QWebEngineDownloadItem *download)
|
||||
{
|
||||
download->accept();
|
||||
}
|
||||
#endif
|
||||
|
||||
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->find, &QLineEdit::returnPressed, this, &MapGUI::on_find_returnPressed);
|
||||
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->displaySettings, &QToolButton::clicked, this, &MapGUI::on_displaySettings_clicked);
|
||||
QObject::connect(ui->mapTypes, qOverload<int>(&QComboBox::currentIndexChanged), this, &MapGUI::on_mapTypes_currentIndexChanged);
|
||||
|
@ -24,6 +24,7 @@
|
||||
#include <QQuickWidget>
|
||||
#include <QTextEdit>
|
||||
#include <QJsonObject>
|
||||
#include <QFileDialog>
|
||||
#ifdef QT_WEBENGINE_FOUND
|
||||
#include <QWebEngineFullScreenRequest>
|
||||
#include <QWebEnginePage>
|
||||
@ -194,6 +195,7 @@ private:
|
||||
RollupState m_rollupState;
|
||||
bool m_doApplySettings;
|
||||
AvailableChannelOrFeatureList m_availableChannelOrFeatures;
|
||||
QFileDialog m_fileDialog;
|
||||
|
||||
Map* m_map;
|
||||
MessageQueue m_inputMessageQueue;
|
||||
@ -217,6 +219,8 @@ private:
|
||||
MapTileServer *m_mapTileServer;
|
||||
QTimer m_redrawMapTimer;
|
||||
GIRO *m_giro;
|
||||
QDateTime m_giroDateTime;
|
||||
QString m_giroRunId;
|
||||
QHash<QString, IonosondeStation *> m_ionosondeStations;
|
||||
QSharedPointer<const QList<NavAid *>> m_navAids;
|
||||
QSharedPointer<const QList<Airspace *>> m_airspaces;
|
||||
@ -279,6 +283,7 @@ private:
|
||||
void openKiwiSDR(const QString& url);
|
||||
void openSpyServer(const QString& url);
|
||||
QString formatFrequency(qint64 frequency) const;
|
||||
void updateGIRO(const QDateTime& mapDateTime);
|
||||
|
||||
static QString getDataDir();
|
||||
static const QList<RadioTimeTransmitter> m_radioTimeTransmitters;
|
||||
@ -315,6 +320,7 @@ private slots:
|
||||
void on_layersMenu_clicked();
|
||||
void on_find_returnPressed();
|
||||
void on_maidenhead_clicked();
|
||||
void on_save_clicked();
|
||||
void on_deleteAll_clicked();
|
||||
void on_displaySettings_clicked();
|
||||
void on_mapTypes_currentIndexChanged(int index);
|
||||
@ -331,9 +337,11 @@ private slots:
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
||||
void loadingChanged(const QWebEngineLoadingInfo &loadingInfo);
|
||||
#endif
|
||||
void downloadRequested(QWebEngineDownloadItem *download);
|
||||
#endif
|
||||
void statusChanged(QQuickWidget::Status status);
|
||||
void preferenceChanged(int elementType);
|
||||
void giroIndexUpdated(const QList<GIRO::DataSet>& data);
|
||||
void giroDataUpdated(const GIRO::GIROStationData& data);
|
||||
void mufUpdated(const QJsonDocument& document);
|
||||
void foF2Updated(const QJsonDocument& document);
|
||||
|
@ -424,6 +424,20 @@
|
||||
</property>
|
||||
</widget>
|
||||
</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>
|
||||
<widget class="QToolButton" name="deleteAll">
|
||||
<property name="toolTip">
|
||||
|
@ -96,6 +96,11 @@ void ObjectMapItem::update(SWGSDRangel::SWGMapItem *mapItem)
|
||||
updateTrack(mapItem->getTrack());
|
||||
updatePredictedTrack(mapItem->getPredictedTrack());
|
||||
}
|
||||
if (mapItem->getAvailableFrom()) {
|
||||
m_availableFrom = QDateTime::fromString(*mapItem->getAvailableFrom(), Qt::ISODateWithMs);
|
||||
} else {
|
||||
m_availableFrom = QDateTime();
|
||||
}
|
||||
if (mapItem->getAvailableUntil()) {
|
||||
m_availableUntil = QDateTime::fromString(*mapItem->getAvailableUntil(), Qt::ISODateWithMs);
|
||||
} else {
|
||||
|
@ -61,6 +61,7 @@ protected:
|
||||
float m_latitude; // Position for label
|
||||
float m_longitude;
|
||||
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
|
||||
};
|
||||
|
||||
|
@ -41,6 +41,7 @@ const QStringList MapSettings::m_pipeTypes = {
|
||||
QStringLiteral("Radiosonde"),
|
||||
QStringLiteral("StarTracker"),
|
||||
QStringLiteral("SatelliteTracker"),
|
||||
QStringLiteral("SID"),
|
||||
QStringLiteral("VORLocalizer")
|
||||
};
|
||||
|
||||
@ -57,6 +58,7 @@ const QStringList MapSettings::m_pipeURIs = {
|
||||
QStringLiteral("sdrangel.feature.radiosonde"),
|
||||
QStringLiteral("sdrangel.feature.startracker"),
|
||||
QStringLiteral("sdrangel.feature.satellitetracker"),
|
||||
QStringLiteral("sdrangel.feature.sid"),
|
||||
QStringLiteral("sdrangel.feature.vorlocalizer")
|
||||
};
|
||||
|
||||
@ -125,6 +127,7 @@ MapSettings::MapSettings() :
|
||||
stationSettings->m_display3DTrack = false;
|
||||
m_itemSettings.insert("Station", stationSettings);
|
||||
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);
|
||||
ionosondeItemSettings->m_display2DIcon = false;
|
||||
|
@ -28,66 +28,10 @@
|
||||
#endif
|
||||
|
||||
#include "util/units.h"
|
||||
#include "gui/colordialog.h"
|
||||
|
||||
#include "mapsettingsdialog.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) :
|
||||
m_track2D(table, row, MapSettingsDialog::COL_2D_TRACK, !settings->m_display2DTrack, settings->m_2DTrackColor),
|
||||
|
@ -27,6 +27,7 @@
|
||||
#include <QProgressDialog>
|
||||
|
||||
#include "gui/httpdownloadmanagergui.h"
|
||||
#include "gui/tablecolorchooser.h"
|
||||
#include "util/openaip.h"
|
||||
#include "util/ourairportsdb.h"
|
||||
#include "util/waypoints.h"
|
||||
@ -34,34 +35,15 @@
|
||||
#include "ui_mapsettingsdialog.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 {
|
||||
Q_OBJECT
|
||||
public:
|
||||
|
||||
MapItemSettingsGUI(QTableWidget *table, int row, MapSettings::MapItemSettings *settings);
|
||||
|
||||
MapColorGUI m_track2D;
|
||||
MapColorGUI m_point3D;
|
||||
MapColorGUI m_track3D;
|
||||
TableColorChooser m_track2D;
|
||||
TableColorChooser m_point3D;
|
||||
TableColorChooser m_track3D;
|
||||
QSpinBox *m_minZoom;
|
||||
QSpinBox *m_minPixels;
|
||||
QDoubleSpinBox *m_labelScale;
|
||||
|
@ -14,10 +14,11 @@ On top of this, it can plot data from other plugins, such as:
|
||||
* Weather balloons from the Radiosonde feature,
|
||||
* RF Heat Maps from the Heap Map channel,
|
||||
* Radials and estimated position from the VOR localizer feature,
|
||||
* ILS course line and glide path from the ILS Demodulator.
|
||||
* DSC geographic call areas.
|
||||
* ILS course line and glide path from the ILS Demodulator,
|
||||
* 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,
|
||||
* 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 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>
|
||||
|
||||
@ -143,14 +144,14 @@ This is only supported on 2D raster maps and the 3D map.
|
||||
<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.
|
||||
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)
|
||||
|
||||
<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.
|
||||
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>
|
||||
|
||||
@ -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.
|
||||
|
||||
<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.
|
||||
|
||||
<h3>20: Display settings</h3>
|
||||
<h3>21: Display settings</h3>
|
||||
|
||||
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.
|
||||
|
||||
<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>
|
||||
|
||||
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/
|
||||
|
@ -53,7 +53,7 @@ void WebServer::incomingConnection(qintptr socket)
|
||||
// Don't include leading or trailing / in from
|
||||
void WebServer::addPathSubstitution(const QString &from, const QString &to)
|
||||
{
|
||||
qDebug() << "Mapping " << from << " to " << to;
|
||||
//qDebug() << "Mapping " << from << " to " << to;
|
||||
m_pathSubstitutions.insert(from, to);
|
||||
}
|
||||
|
||||
@ -125,7 +125,7 @@ void WebServer::readClient()
|
||||
if (socket->canReadLine())
|
||||
{
|
||||
QString line = socket->readLine();
|
||||
qDebug() << "WebServer HTTP Request: " << line;
|
||||
//qDebug() << "WebServer HTTP Request: " << line;
|
||||
|
||||
QStringList tokens = QString(line).split(QRegularExpression("[ \r\n][ \r\n]*"));
|
||||
if (tokens[0] == "GET")
|
||||
|
@ -161,7 +161,7 @@ Selects which image / wavelength to view.
|
||||
| 211 Å | Active corona |
|
||||
| 304 Å | Chromosphere, transition region |
|
||||
| 335 Å | Active corona |
|
||||
| 1600 Å | Transition region, uppoer photoshere |
|
||||
| 1600 Å | Transition region, upper photoshere |
|
||||
| 1700 Å | Temperature minimum, photosphere |
|
||||
|
||||
[Ref](https://sdo.gsfc.nasa.gov/data/channels.php)
|
||||
|
@ -21,22 +21,36 @@
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkDiskCache>
|
||||
#include <QJsonObject>
|
||||
|
||||
GIRO::GIRO()
|
||||
{
|
||||
connect(&m_indexTimer, &QTimer::timeout, this, &GIRO::getIndex);
|
||||
connect(&m_dataTimer, &QTimer::timeout, this, &GIRO::getData);
|
||||
connect(&m_mufTimer, &QTimer::timeout, this, &GIRO::getMUF);
|
||||
connect(&m_foF2Timer, &QTimer::timeout, this, &GIRO::getfoF2);
|
||||
connect(&m_mufTimer, &QTimer::timeout, this, qOverload<>(&GIRO::getMUF));
|
||||
connect(&m_foF2Timer, &QTimer::timeout, this, qOverload<>(&GIRO::getfoF2));
|
||||
m_networkManager = new QNetworkAccessManager();
|
||||
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()
|
||||
{
|
||||
disconnect(&m_indexTimer, &QTimer::timeout, this, &GIRO::getIndex);
|
||||
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_mufTimer, &QTimer::timeout, this, qOverload<>(&GIRO::getMUF));
|
||||
disconnect(&m_foF2Timer, &QTimer::timeout, this, qOverload<>(&GIRO::getfoF2));
|
||||
disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &GIRO::handleReply);
|
||||
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)
|
||||
{
|
||||
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()
|
||||
{
|
||||
QUrl url(QString("https://prop.kc2g.com/api/stations.json"));
|
||||
m_networkManager->get(QNetworkRequest(url));
|
||||
}
|
||||
|
||||
void GIRO::getfoF2()
|
||||
{
|
||||
getMUF("current");
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
@ -132,86 +176,26 @@ void GIRO::handleReply(QNetworkReply* reply)
|
||||
{
|
||||
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())
|
||||
{
|
||||
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;
|
||||
}
|
||||
handleIndex(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);
|
||||
}
|
||||
else if (reply->url().fileName() == "fof2-normal-now.geojson")
|
||||
else if (fileName == "fof2-normal-now.geojson")
|
||||
{
|
||||
emit foF2Updated(document);
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "GIRO::handleReply: unexpected filename: " << reply->url().fileName();
|
||||
qDebug() << "GIRO::handleReply: unexpected filename: " << fileName;
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -225,3 +209,116 @@ void GIRO::handleReply(QNetworkReply* reply)
|
||||
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 "";
|
||||
}
|
||||
|
@ -26,6 +26,7 @@
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
class QNetworkDiskCache;
|
||||
|
||||
// GIRO - Global Ionosphere Radio Observatory
|
||||
// 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");
|
||||
|
||||
~GIRO();
|
||||
void getDataPeriodically(int periodInMins);
|
||||
void getMUFPeriodically(int periodInMins);
|
||||
void getfoF2Periodically(int periodInMins);
|
||||
void getIndexPeriodically(int periodInMins=15);
|
||||
void getDataPeriodically(int periodInMins=2);
|
||||
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 getMUF();
|
||||
void getfoF2();
|
||||
@ -82,17 +95,23 @@ private slots:
|
||||
void handleReply(QNetworkReply* reply);
|
||||
|
||||
signals:
|
||||
void indexUpdated(const QList<DataSet>& data);
|
||||
void dataUpdated(const GIROStationData& data); // Called when new data available.
|
||||
void mufUpdated(const QJsonDocument& doc);
|
||||
void foF2Updated(const QJsonDocument& doc);
|
||||
|
||||
private:
|
||||
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_mufTimer;
|
||||
QTimer m_foF2Timer;
|
||||
QNetworkAccessManager *m_networkManager;
|
||||
QNetworkDiskCache *m_cache;
|
||||
QList<DataSet> m_index;
|
||||
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user