Map: Add Spy Server and Kiwi SDR. Add weather and satellite overlays.

pull/2000/head
srcejon 2024-02-27 15:40:06 +00:00
rodzic c9b7ffa3c2
commit e0839fce82
43 zmienionych plików z 3348 dodań i 271 usunięć

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 23 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 60 KiB

Wyświetl plik

@ -58,6 +58,7 @@ if(NOT SERVER_MODE)
mapmodel.cpp
mapitem.cpp
mapwebsocketserver.cpp
maptileserver.cpp
cesiuminterface.cpp
czml.cpp
map.qrc
@ -78,6 +79,7 @@ if(NOT SERVER_MODE)
mapmodel.h
mapitem.h
mapwebsocketserver.h
maptileserver.h
cesiuminterface.h
czml.h
)
@ -86,12 +88,12 @@ if(NOT SERVER_MODE)
set(TARGET_LIB_GUI "sdrgui")
set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR})
if(Qt${QT_DEFAULT_MAJOR_VERSION}WebEngineCore_FOUND)
set(TARGET_LIB "Qt::Widgets" Qt::Quick Qt::QuickWidgets Qt::Positioning Qt::Location Qt::WebEngineCore Qt::WebEngineWidgets)
elseif(Qt${QT_DEFAULT_MAJOR_VERSION}WebEngine_FOUND)
set(TARGET_LIB "Qt::Widgets" Qt::Quick Qt::QuickWidgets Qt::Positioning Qt::Location Qt::WebEngine Qt::WebEngineCore Qt::WebEngineWidgets)
if(Qt${QT_DEFAULT_MAJOR_VERSION}WebEngine_FOUND)
set(TARGET_LIB "Qt::Widgets" Qt::Quick Qt::QuickWidgets Qt::Svg Qt::Positioning Qt::Location Qt::WebEngine Qt::WebEngineCore Qt::WebEngineWidgets)
elseif(Qt${QT_DEFAULT_MAJOR_VERSION}WebEngineCore_FOUND)
set(TARGET_LIB "Qt::Widgets" Qt::Quick Qt::QuickWidgets Qt::Svg Qt::SvgWidgets Qt::Positioning Qt::Location Qt::WebEngineCore Qt::WebEngineWidgets)
else()
set(TARGET_LIB "Qt::Widgets" Qt::Quick Qt::QuickWidgets Qt::Positioning Qt::Location)
set(TARGET_LIB "Qt::Widgets" Qt::Quick Qt::QuickWidgets Qt::Svg Qt::Positioning Qt::Location)
endif()
else()
set(TARGET_NAME mapsrv)

Wyświetl plik

@ -177,6 +177,28 @@ void CesiumInterface::showfoF2(bool show)
send(obj);
}
void CesiumInterface::showLayer(const QString& layer, bool show)
{
QJsonObject obj {
{"command", "showLayer"},
{"layer", layer},
{"show", show}
};
send(obj);
}
void CesiumInterface::setLayerSettings(const QString& layer, const QStringList& settings, const QList<QVariant>& values)
{
QJsonObject obj {
{"command", "setLayerSettings"},
{"layer", layer},
};
for (int i = 0; i < settings.size(); i++) {
obj.insert(settings[i], QJsonValue::fromVariant(values[i]));
}
send(obj);
}
void CesiumInterface::updateImage(const QString &name, float east, float west, float north, float south, float altitude, const QString &data)
{
QJsonObject obj {

Wyświetl plik

@ -68,6 +68,8 @@ public:
void setAntiAliasing(const QString &antiAliasing);
void showMUF(bool show);
void showfoF2(bool show);
void showLayer(const QString& layer, bool show);
void setLayerSettings(const QString& layer, const QStringList& settings, const QList<QVariant>& values);
void updateImage(const QString &name, float east, float west, float north, float south, float altitude, const QString &data);
void removeImage(const QString &name);
void removeAllImages();

Wyświetl plik

@ -475,6 +475,7 @@ QJsonObject CZML::update(ObjectMapItem *mapItem, bool isTarget, bool isSelected)
if ((mapItem->m_group == "Beacons")
|| (mapItem->m_group == "AM") || (mapItem->m_group == "FM") || (mapItem->m_group == "DAB")
|| (mapItem->m_group == "NavAid")
|| (mapItem->m_group == "Waypoints")
) {
displayDistanceMax = 1000000;
} else if ((mapItem->m_group == "Station") || (mapItem->m_group == "Radar") || (mapItem->m_group == "Radio Time Transmitters")) {

Wyświetl plik

@ -1,11 +1,18 @@
<RCC>
<qresource prefix="/map/">
<file>icons/groundtracks.png</file>
<file>icons/clock.png</file>
<file>icons/ibp.png</file>
<file>icons/muf.png</file>
<file>icons/fof2.png</file>
<file>icons/controltower.png</file>
<file>icons/vor.png</file>
</qresource>
<qresource prefix="/map/">
<file>icons/groundtracks.png</file>
<file>icons/clock.png</file>
<file>icons/ibp.png</file>
<file>icons/muf.png</file>
<file>icons/fof2.png</file>
<file>icons/controltower.png</file>
<file>icons/vor.png</file>
<file>icons/precipitation.png</file>
<file>icons/anchor.png</file>
<file>icons/cloud.png</file>
<file>icons/layers.png</file>
<file>icons/railway.png</file>
<file>icons/waypoints.png</file>
<file>icons/earthsat.png</file>
</qresource>
</RCC>

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.7 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 389 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.9 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.1 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 420 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.7 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.7 KiB

Wyświetl plik

@ -1,5 +1,5 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021-2023 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2021-2024 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2021-2023 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// Copyright (C) 2022 Jiří Pinkava <jiri.pinkava@rossum.ai> //
// //
@ -47,6 +47,7 @@ const char* const Map::m_featureId = "Map";
Map::Map(WebAPIAdapterInterface *webAPIAdapterInterface) :
Feature(m_featureIdURI, webAPIAdapterInterface),
m_availableChannelOrFeatureHandler(MapSettings::m_pipeURIs, QStringList{"mapitems"}),
m_multiplier(0.0)
{
qDebug("Map::Map: webAPIAdapterInterface: %p", webAPIAdapterInterface);
@ -61,33 +62,33 @@ Map::Map(WebAPIAdapterInterface *webAPIAdapterInterface) :
&Map::networkManagerFinished
);
QObject::connect(
MainCore::instance(),
&MainCore::featureAdded,
&m_availableChannelOrFeatureHandler,
&AvailableChannelOrFeatureHandler::channelsOrFeaturesChanged,
this,
&Map::handleFeatureAdded
&Map::channelsOrFeaturesChanged
);
QObject::connect(
MainCore::instance(),
&MainCore::channelAdded,
&m_availableChannelOrFeatureHandler,
&AvailableChannelOrFeatureHandler::messageEnqueued,
this,
&Map::handleChannelAdded
&Map::handlePipeMessageQueue
);
QTimer::singleShot(2000, this, SLOT(scanAvailableChannelsAndFeatures()));
m_availableChannelOrFeatureHandler.scanAvailableChannelsAndFeatures();
}
Map::~Map()
{
QObject::disconnect(
MainCore::instance(),
&MainCore::featureAdded,
&m_availableChannelOrFeatureHandler,
&AvailableChannelOrFeatureHandler::channelsOrFeaturesChanged,
this,
&Map::handleFeatureAdded
&Map::channelsOrFeaturesChanged
);
QObject::disconnect(
MainCore::instance(),
&MainCore::channelAdded,
&m_availableChannelOrFeatureHandler,
&AvailableChannelOrFeatureHandler::messageEnqueued,
this,
&Map::handleChannelAdded
&Map::handlePipeMessageQueue
);
QObject::disconnect(
m_networkManager,
@ -435,156 +436,22 @@ QDateTime Map::getMapDateTime()
}
}
void Map::scanAvailableChannelsAndFeatures()
void Map::channelsOrFeaturesChanged(const QStringList& renameFrom, const QStringList& renameTo)
{
qDebug("Map::scanAvailableChannelsAndFeatures");
std::vector<FeatureSet*>& featureSets = MainCore::instance()->getFeatureeSets();
m_availableChannelOrFeatures.clear();
for (const auto& featureSet : featureSets)
{
for (int fei = 0; fei < featureSet->getNumberOfFeatures(); fei++)
{
Feature *feature = featureSet->getFeatureAt(fei);
if (MapSettings::m_pipeURIs.contains(feature->getURI()) && !m_availableChannelOrFeatures.contains(feature))
{
qDebug("Map::scanAvailableChannelsAndFeatures: store feature %d:%d %s (%p)",
featureSet->getIndex(), fei, qPrintable(feature->getURI()), feature);
registerPipe(feature);
MapSettings::AvailableChannelOrFeature availableItem =
MapSettings::AvailableChannelOrFeature{
"F",
featureSet->getIndex(),
fei,
feature->getIdentifier(),
feature
};
m_availableChannelOrFeatures[feature] = availableItem;
}
}
}
std::vector<DeviceSet*>& deviceSets = MainCore::instance()->getDeviceSets();
for (const auto& deviceSet : deviceSets)
{
DSPDeviceSourceEngine *deviceSourceEngine = deviceSet->m_deviceSourceEngine;
DSPDeviceMIMOEngine *deviceMimoEngine = deviceSet->m_deviceMIMOEngine;
if ((deviceSourceEngine) || (deviceMimoEngine))
{
for (int chi = 0; chi < deviceSet->getNumberOfChannels(); chi++)
{
ChannelAPI *channel = deviceSet->getChannelAt(chi);
if (MapSettings::m_pipeURIs.contains(channel->getURI()) && !m_availableChannelOrFeatures.contains(channel))
{
qDebug("Map::scanAvailableChannelsAndFeatures: store channel %d:%d %s (%p)",
deviceSet->getIndex(), chi, qPrintable(channel->getURI()), channel);
registerPipe(channel);
MapSettings::AvailableChannelOrFeature availableItem =
MapSettings::AvailableChannelOrFeature{
"R",
deviceSet->getIndex(),
chi,
channel->getIdentifier(),
channel};
m_availableChannelOrFeatures[channel] = availableItem;
}
}
}
}
notifyUpdate();
m_availableChannelOrFeatures = m_availableChannelOrFeatureHandler.getAvailableChannelOrFeatureList();
notifyUpdate(renameFrom, renameTo);
}
void Map::handleFeatureAdded(int featureSetIndex, Feature *feature)
{
FeatureSet *featureSet = MainCore::instance()->getFeatureeSets()[featureSetIndex];
if (MapSettings::m_pipeURIs.contains(feature->getURI()))
{
qDebug("Map::handleFeatureAdded: featureSetIndex: %d:%d feature: %s (%p)",
featureSetIndex, feature->getIndexInFeatureSet(), qPrintable(feature->getURI()), feature);
registerPipe(feature);
MapSettings::AvailableChannelOrFeature availableItem =
MapSettings::AvailableChannelOrFeature{
"F",
featureSet->getIndex(),
feature->getIndexInFeatureSet(),
feature->getIdentifier(),
feature
};
m_availableChannelOrFeatures[feature] = availableItem;
notifyUpdate();
}
}
void Map::handleChannelAdded(int deviceSetIndex, ChannelAPI *channel)
{
DeviceSet *deviceSet = MainCore::instance()->getDeviceSets()[deviceSetIndex];
DSPDeviceSourceEngine *deviceSourceEngine = deviceSet->m_deviceSourceEngine;
if (deviceSourceEngine && MapSettings::m_pipeURIs.contains(channel->getURI()))
{
qDebug("Map::handleChannelAdded: deviceSetIndex: %d:%d channel: %s (%p)",
deviceSetIndex, channel->getIndexInDeviceSet(), qPrintable(channel->getURI()), channel);
registerPipe(channel);
MapSettings::AvailableChannelOrFeature availableItem =
MapSettings::AvailableChannelOrFeature{
"R",
deviceSet->getIndex(),
channel->getIndexInDeviceSet(),
channel->getIdentifier(),
channel
};
m_availableChannelOrFeatures[channel] = availableItem;
notifyUpdate();
}
}
void Map::registerPipe(QObject *object)
{
qDebug("Map::registerPipe: register %s (%p)", qPrintable(object->objectName()), object);
MessagePipes& messagePipes = MainCore::instance()->getMessagePipes();
ObjectPipe *pipe = messagePipes.registerProducerToConsumer(object, this, "mapitems");
MessageQueue *messageQueue = qobject_cast<MessageQueue*>(pipe->m_element);
QObject::connect(
messageQueue,
&MessageQueue::messageEnqueued,
this,
[=](){ this->handlePipeMessageQueue(messageQueue); },
Qt::QueuedConnection
);
QObject::connect(
pipe,
&ObjectPipe::toBeDeleted,
this,
&Map::handleMessagePipeToBeDeleted
);
}
void Map::notifyUpdate()
void Map::notifyUpdate(const QStringList& renameFrom, const QStringList& renameTo)
{
if (getMessageQueueToGUI())
{
MsgReportAvailableChannelOrFeatures *msg = MsgReportAvailableChannelOrFeatures::create();
msg->getItems() = m_availableChannelOrFeatures.values();
MsgReportAvailableChannelOrFeatures *msg = MsgReportAvailableChannelOrFeatures::create(renameFrom, renameTo);
msg->getItems() = m_availableChannelOrFeatures;
getMessageQueueToGUI()->push(msg);
}
}
void Map::handleMessagePipeToBeDeleted(int reason, QObject* object)
{
if ((reason == 0) && m_availableChannelOrFeatures.contains(object)) // producer
{
qDebug("Map::handleMessagePipeToBeDeleted: removing channel or feature at (%p)", object);
m_availableChannelOrFeatures.remove(object);
notifyUpdate();
}
}
void Map::handlePipeMessageQueue(MessageQueue* messageQueue)
{
Message* message;

Wyświetl plik

@ -1,7 +1,7 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2020, 2022 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// Copyright (C) 2020 Kacper Michajłow <kasper93@gmail.com> //
// Copyright (C) 2021-2022 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2021-2024 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2022 Jiří Pinkava <jiri.pinkava@rossum.ai> //
// //
// This program is free software; you can redistribute it and/or modify //
@ -28,6 +28,8 @@
#include "feature/feature.h"
#include "util/message.h"
#include "availablechannelorfeaturehandler.h"
#include "maincore.h"
#include "mapsettings.h"
@ -110,17 +112,23 @@ public:
MESSAGE_CLASS_DECLARATION
public:
QList<MapSettings::AvailableChannelOrFeature>& getItems() { return m_availableChannelOrFeatures; }
AvailableChannelOrFeatureList& getItems() { return m_availableChannelOrFeatures; }
const QStringList& getRenameFrom() const { return m_renameFrom; }
const QStringList& getRenameTo() const { return m_renameTo; }
static MsgReportAvailableChannelOrFeatures* create() {
return new MsgReportAvailableChannelOrFeatures();
static MsgReportAvailableChannelOrFeatures* create(const QStringList& renameFrom, const QStringList& renameTo) {
return new MsgReportAvailableChannelOrFeatures(renameFrom, renameTo);
}
private:
QList<MapSettings::AvailableChannelOrFeature> m_availableChannelOrFeatures;
AvailableChannelOrFeatureList m_availableChannelOrFeatures;
QStringList m_renameFrom;
QStringList m_renameTo;
MsgReportAvailableChannelOrFeatures() :
Message()
MsgReportAvailableChannelOrFeatures(const QStringList& renameFrom, const QStringList& renameTo) :
Message(),
m_renameFrom(renameFrom),
m_renameTo(renameTo)
{}
};
@ -176,7 +184,8 @@ public:
private:
MapSettings m_settings;
QHash<QObject*, MapSettings::AvailableChannelOrFeature> m_availableChannelOrFeatures;
AvailableChannelOrFeatureList m_availableChannelOrFeatures;
AvailableChannelOrFeatureHandler m_availableChannelOrFeatureHandler;
QNetworkAccessManager *m_networkManager;
QNetworkRequest m_networkRequest;
@ -184,8 +193,7 @@ private:
void applySettings(const MapSettings& settings, const QList<QString>& settingsKeys, bool force = false);
void webapiFormatFeatureReport(SWGSDRangel::SWGFeatureReport& response);
void webapiReverseSendSettings(const QList<QString>& featureSettingsKeys, const MapSettings& settings, bool force);
void registerPipe(QObject *object);
void notifyUpdate();
void notifyUpdate(const QStringList& renameFrom, const QStringList& renameTo);
QDateTime m_mapDateTime;
QDateTime m_systemDateTime;
@ -194,10 +202,7 @@ private:
private slots:
void networkManagerFinished(QNetworkReply *reply);
void scanAvailableChannelsAndFeatures();
void handleFeatureAdded(int featureSetIndex, Feature *feature);
void handleChannelAdded(int deviceSetIndex, ChannelAPI *channel);
void handleMessagePipeToBeDeleted(int reason, QObject* object);
void channelsOrFeaturesChanged(const QStringList& renameFrom, const QStringList& renameTo);
void handlePipeMessageQueue(MessageQueue* messageQueue);
};

Wyświetl plik

@ -8,6 +8,8 @@
<file>map/antennadab.png</file>
<file>map/antennafm.png</file>
<file>map/antennaam.png</file>
<file>map/antennakiwi.png</file>
<file>map/antennaspyserver.png</file>
<file>map/ionosonde.png</file>
<file>map/VOR.png</file>
<file>map/VOR-DME.png</file>
@ -21,6 +23,7 @@
<file>map/airport_medium.png</file>
<file>map/airport_small.png</file>
<file>map/heliport.png</file>
<file>map/waypoint.png</file>
<file>map/map3d.html</file>
</qresource>
<qresource prefix="/">

Wyświetl plik

@ -1,5 +1,5 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021-2023 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2021-2024 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2022 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// //
// This program is free software; you can redistribute it and/or modify //
@ -22,6 +22,7 @@
#include <QTimer>
#include <QQuickItem>
#include <QQuickWidget>
#include <QTextEdit>
#include <QJsonObject>
#ifdef QT_WEBENGINE_FOUND
#include <QWebEngineFullScreenRequest>
@ -40,7 +41,13 @@
#include "util/azel.h"
#include "util/openaip.h"
#include "util/ourairportsdb.h"
#include "util/waypoints.h"
#include "util/rainviewer.h"
#include "util/nasaglobalimagery.h"
#include "util/kiwisdrlist.h"
#include "util/spyserverlist.h"
#include "settings/rollupstate.h"
#include "availablechannelorfeaturehandler.h"
#include "SWGMapItem.h"
@ -50,6 +57,7 @@
#include "mapradiotimedialog.h"
#include "cesiuminterface.h"
#include "osmtemplateserver.h"
#include "maptileserver.h"
#include "webserver.h"
#include "mapmodel.h"
@ -63,6 +71,7 @@ namespace Ui {
class MapGUI;
struct Beacon;
class QSvgWidget;
struct RadioTimeTransmitter {
QString m_callsign;
@ -165,8 +174,11 @@ public:
void addAirspace(const Airspace *airspace, const QString& group, int cnt);
void addAirspace();
void addAirports();
void addWaypoints();
void addNavtex();
void addVLF();
void addKiwiSDR();
void addSpyServer();
void find(const QString& target);
void track3D(const QString& target);
Q_INVOKABLE void supportedMapsChanged();
@ -181,7 +193,7 @@ private:
QList<QString> m_settingsKeys;
RollupState m_rollupState;
bool m_doApplySettings;
QList<MapSettings::AvailableChannelOrFeature> m_availableChannelOrFeatures;
AvailableChannelOrFeatureList m_availableChannelOrFeatures;
Map* m_map;
MessageQueue m_inputMessageQueue;
@ -201,17 +213,43 @@ private:
MapRadioTimeDialog m_radioTimeDialog;
quint16 m_osmPort;
OSMTemplateServer *m_templateServer;
quint16 m_mapTileServerPort;
MapTileServer *m_mapTileServer;
QTimer m_redrawMapTimer;
GIRO *m_giro;
QHash<QString, IonosondeStation *> m_ionosondeStations;
QSharedPointer<const QList<NavAid *>> m_navAids;
QSharedPointer<const QList<Airspace *>> m_airspaces;
QSharedPointer<const QHash<int, AirportInformation *>> m_airportInfo;
QSharedPointer<const QHash<QString, Waypoint *>> m_waypoints;
QGeoCoordinate m_lastFullUpdatePosition;
KiwiSDRList m_kiwiSDRList;
SpyServerList m_spyServerList;
CesiumInterface *m_cesium;
WebServer *m_webServer;
quint16 m_webPort;
RainViewer *m_rainViewer;
NASAGlobalImagery m_nasaGlobalImagery;
QList<NASAGlobalImagery::DataSet> m_nasaDataSets;
QHash<QString, NASAGlobalImagery::DataSet> m_nasaDataSetsHash;
NASAGlobalImagery::MetaData m_nasaMetaData;
QAction *m_displaySeaMarks;
QAction *m_displayRailways;
QAction *m_displayRain;
QAction *m_displayClouds;
QAction *m_displayNASAGlobalImagery;
QAction *m_displayMUF;
QAction *m_displayfoF2;
QString m_radarPath;
QString m_satellitePath;
NASAGlobalImagery::Legend *m_legend;
QWidget *m_nasaWidget;
QSvgWidget *m_legendWidget;
QTableWidget *m_overviewWidget;
QTextEdit *m_descriptionWidget;
explicit MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent = nullptr);
virtual ~MapGUI();
@ -232,11 +270,31 @@ private:
QString cesiumIonAPIKey() const;
void redrawMap();
void makeUIConnections();
void createLayersMenu();
void displayToolbar();
void setEnableOverlay();
void applyNASAGlobalImagerySettings();
void createNASAGlobalImageryView();
void displayNASAMetaData();
void openKiwiSDR(const QString& url);
void openSpyServer(const QString& url);
QString formatFrequency(qint64 frequency) const;
static QString getDataDir();
static const QList<RadioTimeTransmitter> m_radioTimeTransmitters;
static const QList<RadioTimeTransmitter> m_vlfTransmitters;
enum NASARow {
NASA_TITLE,
NASA_SUBTITLE,
NASA_DEFAULT_DATE,
NASA_START_DATE,
NASA_END_DATE,
NASA_PERIOD,
NASA_LAYER_GROUP,
NASA_ROWS
};
private slots:
void init3DMap();
void onMenuDialogCalled(const QPoint &p);
@ -245,8 +303,16 @@ private slots:
void on_displayNames_clicked(bool checked=false);
void on_displayAllGroundTracks_clicked(bool checked=false);
void on_displaySelectedGroundTracks_clicked(bool checked=false);
void on_displayRain_clicked(bool checked=false);
void on_displayClouds_clicked(bool checked=false);
void on_displaySeaMarks_clicked(bool checked=false);
void on_displayRailways_clicked(bool checked=false);
void on_displayMUF_clicked(bool checked=false);
void on_displayfoF2_clicked(bool checked=false);
void on_displayNASAGlobalImagery_clicked(bool checked=false);
void on_nasaGlobalImageryIdentifier_currentIndexChanged(int index);
void on_nasaGlobalImageryOpacity_valueChanged(int index);
void on_layersMenu_clicked();
void on_find_returnPressed();
void on_maidenhead_clicked();
void on_deleteAll_clicked();
@ -258,6 +324,7 @@ private slots:
void receivedCesiumEvent(const QJsonObject &obj);
virtual void showEvent(QShowEvent *event) override;
virtual bool eventFilter(QObject *obj, QEvent *event) override;
void orientationChanged(Qt::ScreenOrientation orientation);
#ifdef QT_WEBENGINE_FOUND
void fullScreenRequested(QWebEngineFullScreenRequest fullScreenRequest);
void renderProcessTerminated(QWebEnginePage::RenderProcessTerminationStatus terminationStatus, int exitCode);
@ -270,9 +337,18 @@ private slots:
void giroDataUpdated(const GIRO::GIROStationData& data);
void mufUpdated(const QJsonDocument& document);
void foF2Updated(const QJsonDocument& document);
void pathUpdated(const QString& radarPath, const QString& satellitePath);
void nasaGlobalImageryDataUpdated(const QList<NASAGlobalImagery::DataSet>& dataSets);
void nasaGlobalImageryMetaDataUpdated(const NASAGlobalImagery::MetaData& metaData);
void nasaGlobalImageryLegendAvailable(const QString& url, const QByteArray& data);
void nasaGlobalImageryHTMLAvailable(const QString& url, const QByteArray& data);
void navAidsUpdated();
void airspacesUpdated();
void airportsUpdated();
void waypointsUpdated();
void kiwiSDRUpdated(const QList<KiwiSDRList::KiwiSDR>& sdrs);
void spyServerUpdated(const QList<SpyServerList::SpyServer>& sdrs);
void linkClicked(const QString& url);
};

Wyświetl plik

@ -171,6 +171,86 @@
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displayRain">
<property name="toolTip">
<string>Display weather radar (rain/snow)</string>
</property>
<property name="text">
<string>^</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/map/icons/precipitation.png</normaloff>:/map/icons/precipitation.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displaySeaMarks">
<property name="toolTip">
<string>Display sea marks</string>
</property>
<property name="text">
<string>^</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/map/icons/anchor.png</normaloff>:/map/icons/anchor.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displayClouds">
<property name="toolTip">
<string>Display satellite infra-red (clouds)</string>
</property>
<property name="text">
<string>^</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/map/icons/cloud.png</normaloff>:/map/icons/cloud.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displayRailways">
<property name="toolTip">
<string>Display railways</string>
</property>
<property name="text">
<string>^</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/map/icons/railway.png</normaloff>:/map/icons/railway.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displayMUF">
<property name="toolTip">

Wyświetl plik

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>481</width>
<height>507</height>
<width>1282</width>
<height>293</height>
</rect>
</property>
<property name="sizePolicy">
@ -18,7 +18,7 @@
</property>
<property name="minimumSize">
<size>
<width>462</width>
<width>0</width>
<height>0</height>
</size>
</property>
@ -39,13 +39,13 @@
<rect>
<x>0</x>
<y>0</y>
<width>480</width>
<width>1221</width>
<height>41</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>480</width>
<width>300</width>
<height>0</height>
</size>
</property>
@ -165,10 +165,104 @@
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="layersMenu">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/map/icons/layers.png</normaloff>:/map/icons/layers.png</iconset>
</property>
<property name="popupMode">
<enum>QToolButton::InstantPopup</enum>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displayClouds">
<property name="toolTip">
<string>Display satellite infra-red (clouds)</string>
</property>
<property name="text">
<string>^</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/map/icons/cloud.png</normaloff>:/map/icons/cloud.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displayRain">
<property name="toolTip">
<string>Display weather radar (rain/snow)</string>
</property>
<property name="text">
<string>^</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/map/icons/precipitation.png</normaloff>:/map/icons/precipitation.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displaySeaMarks">
<property name="toolTip">
<string>Display sea marks</string>
</property>
<property name="text">
<string>^</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/map/icons/anchor.png</normaloff>:/map/icons/anchor.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displayRailways">
<property name="toolTip">
<string>Display railways</string>
</property>
<property name="text">
<string>^</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/map/icons/railway.png</normaloff>:/map/icons/railway.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displayMUF">
<property name="toolTip">
<string>Display MUF contours</string>
<string>Display MUF (Maximum Usable Frequency) contours (3D only)</string>
</property>
<property name="text">
<string>^</string>
@ -188,7 +282,7 @@
<item>
<widget class="ButtonSwitch" name="displayfoF2">
<property name="toolTip">
<string>Display foF2 contours</string>
<string>Display foF2 (F2 layer critical frequency) contours (3D only)</string>
</property>
<property name="text">
<string>^</string>
@ -205,6 +299,71 @@
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displayNASAGlobalImagery">
<property name="toolTip">
<string>Display NASA GIBS data</string>
</property>
<property name="text">
<string>^</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/map/icons/earthsat.png</normaloff>:/map/icons/earthsat.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="nasaGlobalImageryIdentifier">
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>NASA GIBS data</string>
</property>
</widget>
</item>
<item>
<widget class="QDial" name="nasaGlobalImageryOpacity">
<property name="maximumSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="toolTip">
<string>NASA GIBS image opacity (3D only)</string>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>100</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="nasaGlobalImageryOpacityText">
<property name="minimumSize">
<size>
<width>34</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>100%</string>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displayNames">
<property name="toolTip">
@ -392,12 +551,6 @@
<extends>QWidget</extends>
<header location="global">QtQuickWidgets/QQuickWidget</header>
</customwidget>
<customwidget>
<class>QWebEngineView</class>
<extends>QWidget</extends>
<header location="global">QtWebEngineWidgets/QWebEngineView</header>
<container>1</container>
</customwidget>
<customwidget>
<class>ButtonSwitch</class>
<extends>QToolButton</extends>
@ -409,6 +562,12 @@
<header>gui/rollupcontents.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>QWebEngineView</class>
<extends>QWidget</extends>
<header location="global">QtWebEngineWidgets/QWebEngineView</header>
<container>1</container>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>find</tabstop>

Wyświetl plik

@ -619,6 +619,11 @@ Q_INVOKABLE void ObjectMapModel::moveToBack(int oldRow)
}
}
Q_INVOKABLE void ObjectMapModel::link(const QString& url)
{
emit linkClicked(url);
}
QVariant ObjectMapModel::data(const QModelIndex &index, int role) const
{
int row = index.row();

Wyświetl plik

@ -238,6 +238,8 @@ public:
Q_INVOKABLE void moveToFront(int oldRow);
Q_INVOKABLE void moveToBack(int oldRow);
Q_INVOKABLE void link(const QString& link);
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant& value, int role = Qt::EditRole) override;
@ -248,7 +250,6 @@ public:
Q_INVOKABLE QStringList getDeviceSets() const;
Q_INVOKABLE void setFrequency(qint64 frequency, const QString& deviceSet);
Q_INVOKABLE void viewChanged(double bottomLeftLongitude, double bottomRightLongitude);
bool isSelected3D(const ObjectMapItem *item) const
@ -264,6 +265,9 @@ public:
//public slots:
// void update3DMap(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles = QVector<int>()) override;
signals:
void linkClicked(const QString& url);
protected:
void playAnimations(ObjectMapItem *item);
MapItem *newMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) override;

Wyświetl plik

@ -60,15 +60,21 @@ const QStringList MapSettings::m_pipeURIs = {
QStringLiteral("sdrangel.feature.vorlocalizer")
};
// GUI combo box should match ordering in this list
const QStringList MapSettings::m_mapProviders = {
QStringLiteral("osm"),
QStringLiteral("esri"),
QStringLiteral("mapbox"),
QStringLiteral("mapboxgl"),
QStringLiteral("maplibregl")
};
// Names as used in combo box in settings dialog
const QStringList MapSettings::m_mapProviderNames = {
QStringLiteral("OpenStreetMap"),
QStringLiteral("ESRI"),
QStringLiteral("MapboxGL"),
QStringLiteral("MapLibreGL")
};
MapSettings::MapSettings() :
m_rollupState(nullptr)
{
@ -144,6 +150,13 @@ MapSettings::MapSettings() :
m_itemSettings.insert("Airspace (Wave)", new MapItemSettings("Airspace (Wave)", false, QColor(255, 0, 0, 0x20), false, false, 11));
m_itemSettings.insert("Airspace (Airports)", new MapItemSettings("Airspace (Airports)", false, QColor(0, 0, 255, 0x20), false, false, 11));
MapItemSettings *waypointsSettings = new MapItemSettings("Waypoints", false, QColor(255, 0, 255), false, true, 11);
waypointsSettings->m_filterDistance = 500000;
m_itemSettings.insert("Waypoints", waypointsSettings);
m_itemSettings.insert("KiwiSDR", new MapItemSettings("KiwiSDR", true, QColor(0, 255, 0), false, true, 8));
m_itemSettings.insert("SpyServer", new MapItemSettings("SpyServer", true, QColor(0, 0, 255), false, true, 8));
resetToDefaults();
}
@ -184,6 +197,12 @@ void MapSettings::resetToDefaults()
m_antiAliasing = "None";
m_displayMUF = false;
m_displayfoF2 = false;
m_displayRain = false;
m_displayClouds = false;
m_displaySeaMarks = false;
m_displayNASAGlobalImagery = false;
m_nasaGlobalImageryIdentifier = "";
m_nasaGlobalImageryOpacity = 50;
m_workspaceIndex = 0;
m_checkWXAPIKey = "";
}
@ -229,6 +248,13 @@ QByteArray MapSettings::serialize() const
s.writeBool(35, m_displayMUF);
s.writeBool(36, m_displayfoF2);
s.writeBool(37, m_displayRain);
s.writeBool(38, m_displayClouds);
s.writeBool(39, m_displaySeaMarks);
s.writeBool(40, m_displayRailways);
s.writeBool(41, m_displayNASAGlobalImagery);
s.writeString(42, m_nasaGlobalImageryIdentifier);
s.writeS32(43, m_nasaGlobalImageryOpacity);
s.writeString(46, m_checkWXAPIKey);
@ -308,6 +334,13 @@ bool MapSettings::deserialize(const QByteArray& data)
d.readBool(35, &m_displayMUF, false);
d.readBool(36, &m_displayfoF2, false);
d.readBool(37, &m_displayRain, false);
d.readBool(38, &m_displayClouds, false);
d.readBool(39, &m_displaySeaMarks, false);
d.readBool(40, &m_displayRailways, false);
d.readBool(41, &m_displayNASAGlobalImagery, false);
d.readString(42, &m_nasaGlobalImageryIdentifier, "");
d.readS32(43, &m_nasaGlobalImageryOpacity, 50);
d.readString(46, &m_checkWXAPIKey, "");
@ -556,9 +589,30 @@ void MapSettings::applySettings(const QStringList& settingsKeys, const MapSettin
if (settingsKeys.contains("displayMUF")) {
m_displayMUF = settings.m_displayMUF;
}
if (settingsKeys.contains("misplayfoF2")) {
if (settingsKeys.contains("displayfoF2")) {
m_displayfoF2 = settings.m_displayfoF2;
}
if (settingsKeys.contains("displayRain")) {
m_displayRain = settings.m_displayRain;
}
if (settingsKeys.contains("displayClouds")) {
m_displayClouds = settings.m_displayClouds;
}
if (settingsKeys.contains("displaySeaMarks")) {
m_displaySeaMarks = settings.m_displaySeaMarks;
}
if (settingsKeys.contains("displayRailways")) {
m_displayRailways = settings.m_displayRailways;
}
if (settingsKeys.contains("displayNASAGlobalImagery")) {
m_displayNASAGlobalImagery = settings.m_displayNASAGlobalImagery;
}
if (settingsKeys.contains("nasaGlobalImageryIdentifier")) {
m_nasaGlobalImageryIdentifier = settings.m_nasaGlobalImageryIdentifier;
}
if (settingsKeys.contains("nasaGlobalImageryOpacity")) {
m_nasaGlobalImageryOpacity = settings.m_nasaGlobalImageryOpacity;
}
if (settingsKeys.contains("workspaceIndex")) {
m_workspaceIndex = settings.m_workspaceIndex;
}
@ -646,10 +700,30 @@ QString MapSettings::getDebugString(const QStringList& settingsKeys, bool force)
if (settingsKeys.contains("displayfoF2") || force) {
ostr << " m_displayfoF2: " << m_displayfoF2;
}
if (settingsKeys.contains("displayRain") || force) {
ostr << " m_displayRain: " << m_displayRain;
}
if (settingsKeys.contains("displayClouds") || force) {
ostr << " m_displayClouds: " << m_displayClouds;
}
if (settingsKeys.contains("displaySeaMarks") || force) {
ostr << " m_displaySeaMarks: " << m_displaySeaMarks;
}
if (settingsKeys.contains("displayRailways") || force) {
ostr << " m_displayRailways: " << m_displayRailways;
}
if (settingsKeys.contains("displayNASAGlobalImagery") || force) {
ostr << " m_displayNASAGlobalImagery: " << m_displayNASAGlobalImagery;
}
if (settingsKeys.contains("nasaGlobalImageryIdentifier") || force) {
ostr << " m_nasaGlobalImageryIdentifier: " << m_nasaGlobalImageryIdentifier.toStdString();
}
if (settingsKeys.contains("nasaGlobalImageryOpacity") || force) {
ostr << " m_nasaGlobalImageryOpacity: " << m_nasaGlobalImageryOpacity;
}
if (settingsKeys.contains("workspaceIndex") || force) {
ostr << " m_workspaceIndex: " << m_workspaceIndex;
}
return QString(ostr.str().c_str());
}

Wyświetl plik

@ -2,7 +2,7 @@
// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany //
// written by Christian Daniel //
// Copyright (C) 2015-2020, 2022 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// Copyright (C) 2020-2023 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2020-2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
@ -66,26 +66,6 @@ struct MapSettings
quint32 m_3DColor;
};
struct AvailableChannelOrFeature
{
QString m_kind; //!< "R" for channel, "F" for feature
int m_superIndex;
int m_index;
QString m_type;
QObject *m_source;
AvailableChannelOrFeature() = default;
AvailableChannelOrFeature(const AvailableChannelOrFeature&) = default;
AvailableChannelOrFeature& operator=(const AvailableChannelOrFeature&) = default;
bool operator==(const AvailableChannelOrFeature& a) const {
return (m_kind == a.m_kind)
&& (m_superIndex == a.m_superIndex)
&& (m_index == a.m_index)
&& (m_type == a.m_type)
&& (m_source == a.m_source);
}
};
bool m_displayNames;
QString m_mapProvider;
QString m_thunderforestAPIKey;
@ -121,6 +101,13 @@ struct MapSettings
bool m_displayMUF; // Plot MUF contours
bool m_displayfoF2; // Plot foF2 contours
bool m_displayRain;
bool m_displayClouds;
bool m_displaySeaMarks;
bool m_displayRailways;
bool m_displayNASAGlobalImagery;
QString m_nasaGlobalImageryIdentifier;
int m_nasaGlobalImageryOpacity;
QString m_checkWXAPIKey; //!< checkwxapi.com API key
@ -142,6 +129,7 @@ struct MapSettings
static const QStringList m_pipeURIs;
static const QStringList m_mapProviders;
static const QStringList m_mapProviderNames;
};
Q_DECLARE_METATYPE(MapSettings::MapItemSettings *);

Wyświetl plik

@ -120,8 +120,14 @@ MapSettingsDialog::MapSettingsDialog(MapSettings *settings, QWidget* parent) :
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
ui->mapProvider->clear();
ui->mapProvider->addItem("OpenStreetMap");
#else
#ifdef WIN32
ui->mapProvider->removeItem(ui->mapProvider->findText("MapboxGL")); // Not supported on Windows
#endif
ui->mapProvider->setCurrentIndex(MapSettings::m_mapProviders.indexOf(settings->m_mapProvider));
ui->mapProvider->removeItem(ui->mapProvider->findText("ESRI")); // Currently broken https://bugreports.qt.io/browse/QTBUG-121228
#endif
const QString mapProviderName = MapSettings::m_mapProviderNames[MapSettings::m_mapProviders.indexOf(settings->m_mapProvider)];
ui->mapProvider->setCurrentIndex(ui->mapProvider->findText(mapProviderName));
ui->thunderforestAPIKey->setText(settings->m_thunderforestAPIKey);
ui->maptilerAPIKey->setText(settings->m_maptilerAPIKey);
ui->mapBoxAPIKey->setText(settings->m_mapBoxAPIKey);
@ -198,6 +204,7 @@ MapSettingsDialog::MapSettingsDialog(MapSettings *settings, QWidget* parent) :
connect(&m_ourAirportsDB, &OurAirportsDB::downloadProgress, this, &MapSettingsDialog::downloadProgress);
connect(&m_ourAirportsDB, &OurAirportsDB::downloadError, this, &MapSettingsDialog::downloadError);
connect(&m_ourAirportsDB, &OurAirportsDB::downloadAirportInformationFinished, this, &MapSettingsDialog::downloadAirportInformationFinished);
connect(&m_waypoints, &Waypoints::downloadWaypointsFinished, this, &MapSettingsDialog::downloadWaypointsFinished);
#ifndef QT_WEBENGINE_FOUND
ui->map3DSettings->setVisible(false);
@ -219,7 +226,7 @@ MapSettingsDialog::~MapSettingsDialog()
void MapSettingsDialog::accept()
{
QString mapProvider = MapSettings::m_mapProviders[ui->mapProvider->currentIndex()];
QString mapProvider = MapSettings::m_mapProviders[MapSettings::m_mapProviderNames.indexOf(ui->mapProvider->currentText())];
QString osmURL = ui->osmURL->text();
QString mapBoxStyles = ui->mapBoxStyles->text();
QString mapBoxAPIKey = ui->mapBoxAPIKey->text();
@ -515,6 +522,19 @@ void MapSettingsDialog::on_getAirspacesDB_clicked()
}
}
void MapSettingsDialog::on_getWaypoints_clicked()
{
// Don't try to download while already in progress
if (m_progressDialog == nullptr)
{
m_progressDialog = new QProgressDialog(this);
m_progressDialog->setMaximum(1);
m_progressDialog->setCancelButton(nullptr);
m_progressDialog->setWindowFlag(Qt::WindowCloseButtonHint, false);
m_waypoints.downloadWaypoints();
}
}
void MapSettingsDialog::downloadingURL(const QString& url)
{
if (m_progressDialog)
@ -581,3 +601,17 @@ void MapSettingsDialog::downloadAirportInformationFinished()
}
}
void MapSettingsDialog::downloadWaypointsFinished()
{
if (m_progressDialog) {
m_progressDialog->setLabelText("Reading waypoints.");
}
emit waypointsUpdated();
if (m_progressDialog)
{
m_progressDialog->close();
delete m_progressDialog;
m_progressDialog = nullptr;
}
}

Wyświetl plik

@ -29,6 +29,7 @@
#include "gui/httpdownloadmanagergui.h"
#include "util/openaip.h"
#include "util/ourairportsdb.h"
#include "util/waypoints.h"
#include "ui_mapsettingsdialog.h"
#include "mapsettings.h"
@ -104,6 +105,7 @@ private:
QProgressDialog *m_progressDialog;
OpenAIP m_openAIP;
OurAirportsDB m_ourAirportsDB;
Waypoints m_waypoints;
void unzip(const QString &filename);
@ -114,6 +116,7 @@ private slots:
void on_downloadModels_clicked();
void on_getAirportDB_clicked();
void on_getAirspacesDB_clicked();
void on_getWaypoints_clicked();
void downloadComplete(const QString &filename, bool success, const QString &url, const QString &errorMessage);
void downloadingURL(const QString& url);
void downloadProgress(qint64 bytesRead, qint64 totalBytes);
@ -121,11 +124,13 @@ private slots:
void downloadAirspaceFinished();
void downloadNavAidsFinished();
void downloadAirportInformationFinished();
void downloadWaypointsFinished();
signals:
void navAidsUpdated();
void airspacesUpdated();
void airportsUpdated();
void waypointsUpdated();
private:
Ui::MapSettingsDialog* ui;

Wyświetl plik

@ -83,11 +83,6 @@
<string>ESRI</string>
</property>
</item>
<item>
<property name="text">
<string>Mapbox</string>
</property>
</item>
<item>
<property name="text">
<string>MapboxGL</string>
@ -337,6 +332,20 @@
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="getWaypoints">
<property name="toolTip">
<string>Download aviation waypoints (3MB)</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/map/icons/waypoints.png</normaloff>:/map/icons/waypoints.png</iconset>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">

Wyświetl plik

@ -0,0 +1,18 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "maptileserver.h"

Wyświetl plik

@ -0,0 +1,436 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021, 2023 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_MAPTILESERVER_H_
#define INCLUDE_MAPTILESERVER_H_
#include <QTcpServer>
#include <QTcpSocket>
#include <QRegularExpression>
#include <QDebug>
#include <QtNetwork>
#include <QImage>
#include <QPainter>
#include <QHash>
#include <QMutex>
#include <QMutexLocker>
#include <QNetworkDiskCache>
class MapTileServer : public QTcpServer
{
Q_OBJECT
private:
QString m_thunderforestAPIKey;
QString m_maptilerAPIKey;
QNetworkAccessManager m_manager;
QMutex m_mutex;
struct TileJob {
QTcpSocket* m_socket;
QList<QString> m_urls;
QHash<QString, QImage> m_images;
QString m_format;
};
QList<TileJob *> m_tileJobs;
QHash<QNetworkReply *, TileJob *> m_replies;
QNetworkDiskCache *m_cache;
QString m_radarPath;
QString m_satellitePath;
QString m_nasaGlobalImageryPath;
QString m_nasaGlobalImageryFormat;
bool m_displayRain;
bool m_displayClouds;
bool m_displaySeaMarks;
bool m_displayRailways;
bool m_displayNASAGlobalImagery;
public:
// port - port to listen on / is listening on. Use 0 for any free port.
MapTileServer(quint16 &port, QObject* parent = 0) :
QTcpServer(parent),
m_thunderforestAPIKey(""),
m_maptilerAPIKey(""),
m_radarPath(""),
m_satellitePath(""),
m_nasaGlobalImageryPath(""),
m_nasaGlobalImageryFormat(""),
m_displayRain(false),
m_displayClouds(false),
m_displaySeaMarks(false),
m_displayRailways(false),
m_displayNASAGlobalImagery(false)
{
connect(&m_manager, &QNetworkAccessManager::finished, this, &MapTileServer::downloadFinished);
listen(QHostAddress::Any, port);
port = serverPort();
QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
QDir writeableDir(locations[0]);
if (!writeableDir.mkpath(QStringLiteral("cache") + QDir::separator() + QStringLiteral("maptiles"))) {
qDebug() << "Failed to create cache/maptiles";
}
m_cache = new QNetworkDiskCache();
m_cache->setCacheDirectory(locations[0] + QDir::separator() + QStringLiteral("cache") + QDir::separator() + QStringLiteral("maptiles"));
m_cache->setMaximumCacheSize(1000000000);
m_manager.setCache(m_cache);
}
~MapTileServer()
{
disconnect(&m_manager, &QNetworkAccessManager::finished, this, &MapTileServer::downloadFinished);
delete m_cache;
}
void setThunderforestAPIKey(const QString& thunderforestAPIKey)
{
m_thunderforestAPIKey = thunderforestAPIKey;
}
void setMaptilerAPIKey(const QString& maptilerAPIKey)
{
m_maptilerAPIKey = maptilerAPIKey;
}
void setRadarPath(const QString& radarPath)
{
m_radarPath = radarPath;
}
void setSatellitePath(const QString& satellitePath)
{
m_satellitePath = satellitePath;
}
void setNASAGlobalImageryPath(const QString& nasaGlobalImageryPath)
{
m_nasaGlobalImageryPath = nasaGlobalImageryPath;
}
void setNASAGlobalImageryFormat(const QString& nasaGlobalImageryFormat)
{
m_nasaGlobalImageryFormat = nasaGlobalImageryFormat;
}
void setDisplaySeaMarks(bool displaySeaMarks)
{
m_displaySeaMarks = displaySeaMarks;
}
void setDisplayRailways(bool displayRailways)
{
m_displayRailways = displayRailways;
}
void setDisplayRain(bool displayRain)
{
m_displayRain = displayRain;
}
void setDisplayClouds(bool displayClouds)
{
m_displayClouds = displayClouds;
}
void setDisplayNASAGlobalImagery(bool displayNASAGlobalImagery)
{
m_displayNASAGlobalImagery = displayNASAGlobalImagery;
}
void incomingConnection(qintptr socket) override
{
QTcpSocket* s = new QTcpSocket(this);
connect(s, SIGNAL(readyRead()), this, SLOT(readClient()));
connect(s, SIGNAL(disconnected()), this, SLOT(discardClient()));
s->setSocketDescriptor(socket);
//addPendingConnection(socket);
}
bool isHttpRedirect(QNetworkReply *reply)
{
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
// 304 is file not changed, but maybe we did
return (status >= 301 && status <= 308);
}
QNetworkReply *download(const QUrl &url)
{
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
request.setRawHeader("User-Agent", "SDRangel"); // Required by a.tile.openstreetmap.org
// Don't cache rainviwer data as it's dynamic
if (!url.toString().contains("tilecache.rainviewer")) {
request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
}
QNetworkReply *reply = m_manager.get(request);
connect(reply, &QNetworkReply::sslErrors, this, &MapTileServer::sslErrors);
//qDebug() << "MapTileServer: Downloading from " << url;
return reply;
}
QImage combine(const TileJob *job)
{
// Don't use job->m_images[job->m_urls[0]].size() as not always valid (E.g. map tiler can return http 204 - no content)
// Do we need to support 512x512?
QImage image(QSize(256, 256), QImage::Format_ARGB32_Premultiplied);
image.fill(qPremultiply(QColor(0, 0, 0, 0).rgba()));
QPainter painter(&image);
for (int i = 0; i < job->m_images.size(); i++) {
const QImage &img = job->m_images[job->m_urls[i]];
//qDebug() << "Image format " << i << " is " << img.format() << img.size();
}
for (int i = 0; i < job->m_images.size(); i++) {
const QImage &img = job->m_images[job->m_urls[i]];
//img.save(QString("in%1.png").arg(i), "PNG");
if (img.format() != QImage::Format_Invalid) {
painter.drawImage(image.rect(), img);
}
}
return image;
}
void replyImage(QTcpSocket* socket, const QImage& image, const QString& format)
{
QByteArray ba;
QBuffer buffer(&ba);
buffer.open(QIODevice::WriteOnly);
image.save(&buffer, qPrintable(format));
//qDebug() << "socket: " << socket << "thread:" << QThread::currentThread();
socket->write("HTTP/1.0 200 Ok\r\n"
"Content-Type: image/png\r\n"
"\r\n");
socket->write(buffer.buffer());
socket->close();
if (socket->state() == QTcpSocket::UnconnectedState) {
delete socket;
}
}
void replyError(QTcpSocket* socket)
{
QTextStream os(socket);
os.setAutoDetectUnicode(true);
os << "HTTP/1.0 404 Not Found\r\n"
"Content-Type: text/html\r\n"
"\r\n"
"<html>Not found</html>\r\n";
socket->close();
if (socket->state() == QTcpSocket::UnconnectedState) {
delete socket;
}
}
private slots:
void readClient()
{
QMutexLocker locker(&m_mutex);
QTcpSocket* socket = (QTcpSocket*)sender();
if (socket->canReadLine())
{
QString line = socket->readLine();
qDebug() << "HTTP Request: " << line;
QStringList tokens = QString(line).split(QRegularExpression("[ \r\n][ \r\n]*"));
if (tokens[0] == "GET")
{
QString xml = "";
// Create multiple requests for each image
// https://wiki.openstreetmap.org/wiki/Raster_tile_providers
// rain radar: https://tilecache.rainviewer.com/v2/radar/{timestamp=1705359600}/{size=256}/z/x/y/{color=0}/{options=1_1}.png
// "GET /street/1/2/3.png HTTP/1.1\r\n"
const QRegularExpression re("\\/([A-Za-z0-9\\-_]+)\\/([0-9]+)\\/([0-9]+)\\/([0-9]+).(png|jpg)");
QRegularExpressionMatch match = re.match(tokens[1]);
if (match.hasMatch())
{
QString map, x, y, z, format;
map = match.captured(1);
z = match.captured(2);
x = match.captured(3);
y = match.captured(4);
format = match.captured(5);
TileJob *job = new TileJob;
//qDebug() << "Created job" << job << "socket:" << socket << "thread:" << QThread::currentThread() ;
job->m_socket = socket;
if (format == "png") {
job->m_format = "PNG";
} else {
job->m_format = "JPG";
}
// This should match code in OSMTemplateServer::readClient
QString baseMapURL;
if (map == "street") {
baseMapURL = QString("https://tile.openstreetmap.org/%3/%1/%2.png").arg(x).arg(y).arg(z);
} else if (map == "satellite") {
baseMapURL = QString("https://api.maptiler.com/tiles/satellite-v2/%3/%1/%2.jpg?key=%4").arg(x).arg(y).arg(z).arg(m_maptilerAPIKey);
} else if ((map == "dark_nolabels") || (map == "light_nolabels")) {
baseMapURL = QString("http://1.basemaps.cartocdn.com/%4/%3/%1/%2.png").arg(x).arg(y).arg(z).arg(map);
} else {
baseMapURL = QString("http://a.tile.thunderforest.com/%4/%3/%1/%2.png?apikey=%5").arg(x).arg(y).arg(z).arg(map).arg(m_thunderforestAPIKey);
}
job->m_urls.append(baseMapURL);
if (m_displaySeaMarks) {
job->m_urls.append(QString("https://tiles.openseamap.org/seamark/%3/%1/%2.png").arg(x).arg(y).arg(z));
}
if (m_displayRailways) {
job->m_urls.append(QString("https://a.tiles.openrailwaymap.org/standard/%3/%1/%2.png").arg(x).arg(y).arg(z));
}
if (m_displayNASAGlobalImagery && !m_nasaGlobalImageryPath.isEmpty()) {
job->m_urls.append(QString("https://gibs.earthdata.nasa.gov/wmts/epsg3857/best/%4/%3/%2/%1.%5").arg(x).arg(y).arg(z).arg(m_nasaGlobalImageryPath).arg(m_nasaGlobalImageryFormat)); // x,y reversed compared to others
}
if (m_displayClouds && !m_satellitePath.isEmpty()) {
job->m_urls.append(QString("https://tilecache.rainviewer.com%4/256/%3/%1/%2/0/0_0.png").arg(x).arg(y).arg(z).arg(m_satellitePath));
}
if (m_displayRain && !m_radarPath.isEmpty()) {
job->m_urls.append(QString("https://tilecache.rainviewer.com%4/256/%3/%1/%2/4/1_1.png").arg(x).arg(y).arg(z).arg(m_radarPath));
}
m_tileJobs.append(job);
for (const auto& url : job->m_urls)
{
QNetworkReply *reply = download(QUrl(url));
m_replies.insert(reply, job);
}
}
else
{
replyError(socket);
}
}
}
}
void discardClient()
{
QTcpSocket* socket = (QTcpSocket*)sender();
//qDebug() << "discardClient socket:" << socket;
socket->deleteLater();
for (auto job : m_tileJobs) {
if (job->m_socket == socket) {
//qDebug() << "Socket closed on active job. job: " << job << "socket" << socket;
job->m_socket = nullptr;
}
}
}
void downloadFinished(QNetworkReply *reply)
{
QMutexLocker locker(&m_mutex);
//QString url = reply->url().toEncoded().constData();
QString url = reply->request().url().toEncoded().constData(); // reply->url() may differ if redirection occured, so use requested
if (!isHttpRedirect(reply))
{
QByteArray data = reply->readAll();
QImage image;
if (!reply->error())
{
if (!image.loadFromData(data))
{
qDebug() << "MapTileServer::downloadFinished: Failed to load image: " << url;
}
else
{
bool cached = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
//qDebug() << "Downloaded " << url << "as" << image.size() << "cached:" << cached;
}
}
else
{
qDebug() << "MapTileServer::downloadFinished: Error: " << reply->error() << "for" << url;
}
bool found = false;
TileJob *job = m_replies[reply];
if (!m_tileJobs.contains(job)) {
qDebug() << "job has been deleted!";
}
for (const auto& jobURL : job->m_urls)
{
if (jobURL == url)
{
job->m_images.insert(url, image);
if (job->m_urls.size() == job->m_images.size())
{
// All images available
QImage combinedImage = combine(job);
if (job->m_socket)
{
replyImage(job->m_socket, combinedImage, job->m_format);
job->m_socket = nullptr;
m_tileJobs.removeAll(job);
delete job;
//qDebug() << "Delete job" << job;
}
else
{
qDebug() << "Socket was null. URL: " << url << "job:" << job;
}
}
found = true;
break;
}
}
if (!found) {
qDebug() << "MapTileServer::downloadFinished: Failed to match URL: " << url;
}
}
else
{
qDebug() << "MapTileServer::downloadFinished: Redirect";
}
reply->deleteLater();
m_replies.remove(reply);
}
void sslErrors(const QList<QSslError> &sslErrors)
{
for (const QSslError &error : sslErrors)
{
qCritical() << "MapTileServer: SSL error" << (int)error.error() << ": " << error.errorString();
#ifdef ANDROID
// On Android 6 (but not on 12), we always seem to get: "The issuer certificate of a locally looked up certificate could not be found"
// which causes downloads to fail, so ignore
if (error.error() == QSslError::UnableToGetLocalIssuerCertificate)
{
QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
QList<QSslError> errorsThatCanBeIgnored;
errorsThatCanBeIgnored << QSslError(QSslError::UnableToGetLocalIssuerCertificate, error.certificate());
reply->ignoreSslErrors(errorsThatCanBeIgnored);
}
#endif
}
}
};
#endif

Wyświetl plik

@ -29,13 +29,17 @@ class OSMTemplateServer : public QTcpServer
private:
QString m_thunderforestAPIKey;
QString m_maptilerAPIKey;
quint16 m_tileServerPort;
bool m_overlay;
public:
// port - port to listen on / is listening on. Use 0 for any free port.
OSMTemplateServer(const QString &thunderforestAPIKey, const QString &maptilerAPIKey, quint16 &port, QObject* parent = 0) :
OSMTemplateServer(const QString &thunderforestAPIKey, const QString &maptilerAPIKey, quint16 tileServerPort, quint16 &port, QObject* parent = 0) :
QTcpServer(parent),
m_thunderforestAPIKey(thunderforestAPIKey),
m_maptilerAPIKey(maptilerAPIKey)
m_maptilerAPIKey(maptilerAPIKey),
m_tileServerPort(tileServerPort),
m_overlay(false)
{
listen(QHostAddress::Any, port);
port = serverPort();
@ -50,13 +54,24 @@ public:
//addPendingConnection(socket);
}
void setThunderforestAPIKey(const QString& thunderforestAPIKey)
{
m_thunderforestAPIKey = thunderforestAPIKey;
}
void setMaptilerAPIKey(const QString& maptilerAPIKey)
{
m_maptilerAPIKey = maptilerAPIKey;
}
void setEnableOverlay(bool enableOverlay)
{
m_overlay = enableOverlay;
}
private slots:
void readClient()
{
QStringList map({"/cycle", "/cycle-hires", "/hiking", "/hiking-hires", "/night-transit", "/night-transit-hires", "/terrain", "/terrain-hires", "/transit", "/transit-hires"});
QStringList mapId({"thf-cycle", "thf-cycle-hires", "thf-hike", "thf-hike-hires", "thf-nighttransit", "thf-nighttransit-hires", "thf-landsc", "thf-landsc-hires", "thf-transit", "thf-transit-hires"});
QStringList mapUrl({"cycle", "cycle", "outdoors", "outdoors", "transport-dark", "transport-dark", "landscape", "landscape", "transport", "transport"});
QTcpSocket* socket = (QTcpSocket*)sender();
if (socket->canReadLine())
{
@ -67,33 +82,43 @@ private slots:
{
bool hires = tokens[1].contains("hires");
QString hiresURL = hires ? "@2x" : "";
QString xml;
QString xml, url;
if ((tokens[1] == "/street") || (tokens[1] == "/street-hires"))
{
if (m_overlay) {
url = QString("http://127.0.0.1:%1/street/%z/%x/%y.png").arg(m_tileServerPort);
} else {
url = "https://tile.openstreetmap.org/%z/%x/%y.png";
}
xml = QString("\
{\
\"UrlTemplate\" : \"https://tile.openstreetmap.org/%z/%x/%y.png\",\
\"ImageFormat\" : \"png\",\
\"QImageFormat\" : \"Indexed8\",\
\"ID\" : \"wmf-intl-1x\",\
\"MaximumZoomLevel\" : 19,\
\"MapCopyRight\" : \"<a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a>\",\
\"DataCopyRight\" : \"\"\
}");
{\
\"UrlTemplate\" : \"%1\",\
\"ImageFormat\" : \"png\",\
\"QImageFormat\" : \"Indexed8\",\
\"ID\" : \"wmf-intl-1x\",\
\"MaximumZoomLevel\" : 19,\
\"MapCopyRight\" : \"<a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a>\",\
\"DataCopyRight\" : \"\"\
}").arg(url);
}
else if (tokens[1] == "/satellite")
{
if (m_overlay) {
url = QString("http://127.0.0.1:%1/satellite/%z/%x/%y.jpg").arg(m_tileServerPort);
} else {
url = QString("https://api.maptiler.com/tiles/satellite-v2/%z/%x/%y%1.jpg?key=%2").arg(hiresURL).arg(m_maptilerAPIKey);
}
xml = QString("\
{\
\"Enabled\" : true,\
\"UrlTemplate\" : \"https://api.maptiler.com/tiles/satellite/%z/%x/%y%1.jpg?key=%2\",\
\"UrlTemplate\" : \"%1\",\
\"ImageFormat\" : \"jpg\",\
\"QImageFormat\" : \"RGB888\",\
\"ID\" : \"usgs-l7\",\
\"MaximumZoomLevel\" : 20,\
\"MaximumZoomLevel\" : 22,\
\"MapCopyRight\" : \"<a href='http://maptiler.com/'>Maptiler</a>\",\
\"DataCopyRight\" : \"\"\
}").arg(hiresURL).arg(m_maptilerAPIKey);
}").arg(url);
}
else if (tokens[1].contains("transit"))
{
@ -103,32 +128,46 @@ private slots:
// Use CartoDB maps without labels for aviation maps
int idx = map.indexOf(tokens[1]);
if (m_overlay) {
url = QString("http://127.0.0.1:%1/%2/%z/%x/%y.png").arg(m_tileServerPort).arg(mapUrl[idx]);
} else {
url = QString("http://1.basemaps.cartocdn.com/%2/%z/%x/%y.png%1").arg(hiresURL).arg(mapUrl[idx]);
}
xml = QString("\
{\
\"UrlTemplate\" : \"http://1.basemaps.cartocdn.com/%2/%z/%x/%y.png%1\",\
\"UrlTemplate\" : \"%1\",\
\"ImageFormat\" : \"png\",\
\"QImageFormat\" : \"Indexed8\",\
\"ID\" : \"%3\",\
\"ID\" : \"%2\",\
\"MaximumZoomLevel\" : 20,\
\"MapCopyRight\" : \"<a href='https://carto.com'>CartoDB</a>\",\
\"DataCopyRight\" : \"\"\
}").arg(hiresURL).arg(mapUrl[idx]).arg(mapId[idx]);
}").arg(url).arg(mapId[idx]);
}
else
{
QStringList map({"/cycle", "/cycle-hires", "/hiking", "/hiking-hires", "/night-transit", "/night-transit-hires", "/terrain", "/terrain-hires", "/transit", "/transit-hires"});
QStringList mapId({"thf-cycle", "thf-cycle-hires", "thf-hike", "thf-hike-hires", "thf-nighttransit", "thf-nighttransit-hires", "thf-landsc", "thf-landsc-hires", "thf-transit", "thf-transit-hires"});
QStringList mapUrl({"cycle", "cycle", "outdoors", "outdoors", "transport-dark", "transport-dark", "landscape", "landscape", "transport", "transport"});
int idx = map.indexOf(tokens[1]);
if (idx != -1)
{
if (m_overlay) {
url = QString("http://127.0.0.1:%1/%2/%z/%x/%y.png").arg(m_tileServerPort).arg(mapUrl[idx]);
} else {
url = QString("http://a.tile.thunderforest.com/%1/%z/%x/%y%3.png?apikey=%2").arg(mapUrl[idx]).arg(m_thunderforestAPIKey).arg(hiresURL);
}
xml = QString("\
{\
\"UrlTemplate\" : \"http://a.tile.thunderforest.com/%1/%z/%x/%y%4.png?apikey=%2\",\
\"UrlTemplate\" : \"%1\",\
\"ImageFormat\" : \"png\",\
\"QImageFormat\" : \"Indexed8\",\
\"ID\" : \"%3\",\
\"ID\" : \"%2\",\
\"MaximumZoomLevel\" : 20,\
\"MapCopyRight\" : \"<a href='http://www.thunderforest.com/'>Thunderforest</a>\",\
\"DataCopyRight\" : \"<a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a> contributors\"\
}").arg(mapUrl[idx]).arg(m_thunderforestAPIKey).arg(mapId[idx]).arg(hiresURL);
}").arg(url).arg(mapId[idx]);
}
}
QTextStream os(socket);

Wyświetl plik

@ -11,7 +11,7 @@ On top of this, it can plot data from other plugins, such as:
* Satellites from the Satellite Tracker,
* Weather imagery from APT Demodulator,
* The Sun, Moon and Stars from the Star Tracker,
* Weather balloons from the RadioSonde feature,
* 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.
@ -25,10 +25,15 @@ As well as internet data sources:
* Radio time transmitters,
* GRAVES radar,
* Ionosonde station data,
* Navtex transmitters.
* VLF transmitters.
* Navtex transmitters,
* VLF transmitters,
* KiwiSDRs,
* Weather radar,
* Satellite infra-red data (clouds),
* Sea marks,
* Satellite imagery from NASA GIBS (Global Imagery Browse Services).
It can also create tracks showing the path aircraft, ships and APRS objects have taken, as well as predicted paths for satellites.
It can also create tracks showing the path aircraft, ships, radiosondes and APRS objects have taken, as well as predicted paths for satellites.
![2D Map feature](../../../doc/img/Map_plugin_beacons.png)
@ -90,6 +95,34 @@ When clicked, opens the Radio Time Transmitters dialog.
![Radio Time transmitters dialog](../../../doc/img/Map_plugin_radiotime_dialog.png)
<h3>Display Satellite Infrared</h3>
When checked, satellite infrared measurements (10.3um) are downloaded from the internet and are overlaid on the maps.
This essentially shows cloud cover. The images are updated every 10 minutes.
The data is similar to that which can be received using the [APT Demodulator](../../channelrx/demodapt/readme.md) in the Thermal-infrared (10.3-11.3 um) channel.
<h3>Display Weather Radar</h3>
When checked, weather radar measurements are downloaded form the internet and are overlaid on the maps.
This shows rain and other forms of precipitation.
The images are updated every 10 minutes.
Green, yellow and red are rain, with red being the most intense.
Light blue through dark blue is snow, with dark blue being the most intense.
<h3>Display Sea Marks</h3>
When checked, sea marks are overlaid on the maps.
![Sea Marks Legend](../../../doc/img/Map_plugin_seamarks_legend.png)
<h3>Display Railways</h3>
When checked, railway routes are overlaid on the maps.
![Railway Legend](../../../doc/img/Map_plugin_railway_legend.png)
<h3>7: Display MUF Contours</h3>
When checked, contours will be downloaded and displayed on the 3D map, showing the MUF (Maximum Usable Frequency) for a 3000km path that reflects off the ionosphere.
@ -100,6 +133,23 @@ The contours will be updated every 15 minutes. The latest contour data will alwa
When checked, contours will be downloaded and displayed on the 3D map, showing coF2 (F2 layer critical frequency), the maximum frequency at which radio waves will be reflected vertically from the F2 region of the ionosphere.
The contours will be updated every 15 minutes. The latest contour data will always be displayed, irrespective of the time set on the 3D Map.
<h3>Display NASA GIBS Data</h3>
When checked, enables overlay of data from NASA GIBS (Global Imagery Browse Services). This includes a vast array of Earth observation satellite data,
such as land and sea temperatures, atmospheric conditions, flux measurements and the like.
Details of available data products can be found [here](https://nasa-gibs.github.io/gibs-api-docs/available-visualizations/#visualization-product-catalog).
For some data sets, GIBS has data spanning many decades. The data period may be hours, days or months. The 3D map will attemp to show data from the closest time set in the 3D map's timescale.
The 2D map will only show data from the default date (which is displayed in the table at the bottom).
<h3>NASA GIBS Data</h3>
Selects which data from NASA GIBS to overlay on the maps.
<h3>NASA GIBS Opacity</h3>
Sets the opacity used for the NASA GIBS overlay image overlay on the 3D map. Lower values make the image more transparent.
<h3>8: Display Names</h3>
When checked, names of objects are displayed in a bubble next to each object.
@ -205,8 +255,17 @@ Mapbox: https://www.mapbox.com/ Cesium: https://www.cesium.com Bing: https://www
Ionosonde data and MUF/coF2 contours from [KC2G](https://prop.kc2g.com/) with source data from [GIRO](https://giro.uml.edu/) and [NOAA NCEI](https://www.ngdc.noaa.gov/stp/iono/ionohome.html).
Icons made by Google from Flaticon https://www.flaticon.com
World icons created by turkkub from Flaticon https://www.flaticon.com
Sea Marks are from OpenSeaMap: https://www.openseamap.org/
Railways are from OpenRailwayMap: https://www.openrailwaymap.org/
Weather radar and satellite data is from RainViewer: https://www.rainviewer
Icons made by Google from Flaticon: https://www.flaticon.com
World icons created by turkkub from Flaticon: https://www.flaticon.com
Layers and Boat icons created by Freepik from Flaticon: https://www.flaticon.com
Railway icons created by Prosymbols Premium from Flaticon: https://www.flaticon.com
Satellite icons created by SyafriStudio from Flaticon: https://www.flaticon.com
3D models are by various artists under a variety of licenses. See: https://github.com/srcejon/sdrangel-3d-models
@ -218,8 +277,8 @@ If you wish to contribute a 3D model, see the https://github.com/srcejon/sdrange
Full details of the API can be found in the Swagger documentation. Here is a quick example of how to centre the map on an object from the command line:
curl -X POST "http://127.0.0.1:8091/sdrangel/featureset/0/feature/0/actions" -d '{"featureType": "Map", "MapActions": { "find": "M7RCE" }}'
curl -X POST "http://127.0.0.1:8091/sdrangel/featureset/feature/0/actions" -d '{"featureType": "Map", "MapActions": { "find": "M7RCE" }}'
And to centre the map at a particular latitude and longitude:
curl -X POST "http://127.0.0.1:8091/sdrangel/featureset/0/feature/0/actions" -d '{"featureType": "Map", "MapActions": { "find": "51.2 0.0" }}'
curl -X POST "http://127.0.0.1:8091/sdrangel/featureset/feature/0/actions" -d '{"featureType": "Map", "MapActions": { "find": "51.2 0.0" }}'

Wyświetl plik

@ -97,7 +97,7 @@ void WebServer::addFile(const QString &path, const QByteArray &data)
void WebServer::sendFile(QTcpSocket* socket, const QByteArray &data, MimeType *mimeType, const QString &path)
{
QString header = QString("HTTP/1.0 200 Ok\r\nContent-Type: %1\r\n\r\n").arg(mimeType->m_type);
QString header = QString("HTTP/1.0 200 Ok\r\nContent-Type: %1\r\nAccess-Control-Allow-Headers: *\r\nAccess-Control-Allow-Methods: *\r\nAccess-Control-Allow-Origin: *\r\n\r\n").arg(mimeType->m_type);
if (mimeType->m_binary)
{
// Send file as binary
@ -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")

Wyświetl plik

@ -0,0 +1,198 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "kiwisdrlist.h"
#include <QDebug>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkReply>
#include <QXmlStreamReader>
#include <QNetworkDiskCache>
#include <QRegularExpression>
KiwiSDRList::KiwiSDRList()
{
m_networkManager = new QNetworkAccessManager();
QObject::connect(m_networkManager, &QNetworkAccessManager::finished, this, &KiwiSDRList::handleReply);
QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
QDir writeableDir(locations[0]);
if (!writeableDir.mkpath(QStringLiteral("cache") + QDir::separator() + QStringLiteral("kiwisdr"))) {
qDebug() << "Failed to create cache/kiwisdr";
}
m_cache = new QNetworkDiskCache();
m_cache->setCacheDirectory(locations[0] + QDir::separator() + QStringLiteral("cache") + QDir::separator() + QStringLiteral("kiwisdr"));
m_cache->setMaximumCacheSize(100000000);
m_networkManager->setCache(m_cache);
connect(&m_timer, &QTimer::timeout, this, &KiwiSDRList::update);
}
KiwiSDRList::~KiwiSDRList()
{
QObject::disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &KiwiSDRList::handleReply);
delete m_networkManager;
}
void KiwiSDRList::getData()
{
QUrl url(QString("http://kiwisdr.com/public/"));
m_networkManager->get(QNetworkRequest(url));
}
void KiwiSDRList::getDataPeriodically(int periodInMins)
{
m_timer.setInterval(periodInMins*60*1000);
m_timer.start();
update();
}
void KiwiSDRList::update()
{
getData();
}
void KiwiSDRList::handleReply(QNetworkReply* reply)
{
if (reply)
{
if (!reply->error())
{
QString url = reply->url().toEncoded().constData();
QByteArray bytes = reply->readAll();
handleHTML(url, bytes);
}
else
{
qDebug() << "KiwiSDRList::handleReply: error: " << reply->error();
}
reply->deleteLater();
}
else
{
qDebug() << "KiwiSDRList::handleReply: reply is null";
}
}
void KiwiSDRList::handleHTML(const QString& url, const QByteArray& bytes)
{
QList<KiwiSDR> sdrs;
QString html(bytes);
QRegularExpression div("<div class='cl-info'>(.*?)<\\/div>", QRegularExpression::DotMatchesEverythingOption);
QRegularExpressionMatchIterator divItr = div.globalMatch(html);
if (divItr.hasNext())
{
while (divItr.hasNext())
{
QRegularExpressionMatch divMatch = divItr.next();
QString divText = divMatch.captured(1);
QRegularExpression urlRe("a href='(.*?)'");
QRegularExpressionMatch urlMatch = urlRe.match(divText);
if (urlMatch.hasMatch())
{
KiwiSDR sdr;
sdr.m_url = urlMatch.captured(1);
QRegularExpression element("<!-- (\\w+)=(.*?) -->");
QRegularExpressionMatchIterator elementItr = element.globalMatch(divText);
while(elementItr.hasNext())
{
QRegularExpressionMatch elementMatch = elementItr.next();
QString key = elementMatch.captured(1);
QString value = elementMatch.captured(2);
if (key == "name")
{
sdr.m_name = value;
}
else if (key == "sdr_hw")
{
sdr.m_sdrHW = value;
}
else if (key == "bands")
{
QRegularExpression freqRe("([\\d]+)-([\\d]+)");
QRegularExpressionMatch freqMatch = freqRe.match(value);
if (freqMatch.hasMatch())
{
sdr.m_lowFrequency = freqMatch.captured(1).toInt();
sdr.m_highFrequency = freqMatch.captured(2).toInt();
}
}
else if (key == "users")
{
sdr.m_users = value.toInt();
}
else if (key == "users_max")
{
sdr.m_usersMax = value.toInt();
}
else if (key == "gps")
{
QRegularExpression gpsRe("([\\d.+-]+), ([\\d.+-]+)");
QRegularExpressionMatch gpsMatch = gpsRe.match(value);
if (gpsMatch.hasMatch())
{
sdr.m_latitude = gpsMatch.captured(1).toFloat();
sdr.m_longitude = gpsMatch.captured(2).toFloat();
}
}
else if (key == "asl")
{
sdr.m_altitude = value.toInt();
}
else if (key == "loc")
{
sdr.m_location = value.toInt();
}
else if (key == "antenna")
{
sdr.m_antenna = value;
}
else if (key == "ant_connected")
{
sdr.m_antennaConnected = value == "1";
}
else if (key == "snr")
{
sdr.m_snr = value;
}
}
sdrs.append(sdr);
}
else
{
qDebug() << "KiwiSDRPublic::handleHTML: No URL found in:\n" << divText;
}
}
}
else
{
qDebug() << "KiwiSDRPublic::handleHTML: No cl-info found in:\n" << html;
}
emit dataUpdated(sdrs);
}

Wyświetl plik

@ -0,0 +1,78 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_KIWISDRLIST_H
#define INCLUDE_KIWISDRLIST_H
#include <QtCore>
#include <QTimer>
#include "export.h"
class QNetworkAccessManager;
class QNetworkReply;
class QNetworkDiskCache;
// Gets a list of public Kiwi SDRs from http://kiwisdr.com/public/
class SDRBASE_API KiwiSDRList : public QObject
{
Q_OBJECT
public:
struct KiwiSDR {
QString m_url;
QString m_status; // Only seems to be 'active'
QString m_offline; // Only seems to be 'no'
QString m_name;
QString m_sdrHW;
qint64 m_lowFrequency;
qint64 m_highFrequency;
int m_users;
int m_usersMax;
float m_latitude;
float m_longitude;
int m_altitude; // Above sea level (Not sure if ft or m)
QString m_location;
QString m_antenna;
bool m_antennaConnected;
QString m_snr;
};
KiwiSDRList();
~KiwiSDRList();
void getData();
void getDataPeriodically(int periodInMins=4);
public slots:
void handleReply(QNetworkReply* reply);
void update();
signals:
void dataUpdated(const QList<KiwiSDR>& sdrs); // Emitted when data are available.
private:
QNetworkAccessManager *m_networkManager;
QNetworkDiskCache *m_cache;
QTimer m_timer; // Timer for periodic updates
void handleHTML(const QString& url, const QByteArray& bytes);
};
#endif /* INCLUDE_KIWISDRLIST_H */

Wyświetl plik

@ -0,0 +1,326 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "nasaglobalimagery.h"
#include <QDebug>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkReply>
#include <QXmlStreamReader>
#include <QNetworkDiskCache>
#include <QJsonDocument>
#include <QJsonObject>
NASAGlobalImagery::NASAGlobalImagery()
{
m_networkManager = new QNetworkAccessManager();
QObject::connect(m_networkManager, &QNetworkAccessManager::finished, this, &NASAGlobalImagery::handleReply);
QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
QDir writeableDir(locations[0]);
if (!writeableDir.mkpath(QStringLiteral("cache") + QDir::separator() + QStringLiteral("nasaglobalimagery"))) {
qDebug() << "Failed to create cache/nasaglobalimagery";
}
m_cache = new QNetworkDiskCache();
m_cache->setCacheDirectory(locations[0] + QDir::separator() + QStringLiteral("cache") + QDir::separator() + QStringLiteral("nasaglobalimagery"));
m_cache->setMaximumCacheSize(100000000);
m_networkManager->setCache(m_cache);
}
NASAGlobalImagery::~NASAGlobalImagery()
{
QObject::disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &NASAGlobalImagery::handleReply);
delete m_networkManager;
}
void NASAGlobalImagery::getData()
{
QUrl url(QString("https://gibs.earthdata.nasa.gov/wmts/epsg3857/best/1.0.0/WMTSCapabilities.xml"));
m_networkManager->get(QNetworkRequest(url));
}
void NASAGlobalImagery::getMetaData()
{
QUrl url(QString("https://worldview.earthdata.nasa.gov/config/wv.json"));
m_networkManager->get(QNetworkRequest(url));
}
void NASAGlobalImagery::handleReply(QNetworkReply* reply)
{
if (reply)
{
if (!reply->error())
{
QString url = reply->url().toEncoded().constData();
QByteArray bytes = reply->readAll();
if (url.endsWith(".xml")) {
handleXML(bytes);
} else if (url.endsWith(".svg")) {
handleSVG(url, bytes);
} else if (url.endsWith(".json")) {
handleJSON(bytes);
} else if (url.endsWith(".html")) {
handleHTML(url, bytes);
} else {
qDebug() << "NASAGlobalImagery::handleReply: unexpected URL: " << url;
}
}
else
{
qDebug() << "NASAGlobalImagery::handleReply: error: " << reply->error();
}
reply->deleteLater();
}
else
{
qDebug() << "NASAGlobalImagery::handleReply: reply is null";
}
}
void NASAGlobalImagery::handleXML(const QByteArray& bytes)
{
QXmlStreamReader xmlReader(bytes);
QList<DataSet> dataSets;
while (!xmlReader.atEnd() && !xmlReader.hasError())
{
while (xmlReader.readNextStartElement())
{
if (xmlReader.name() == QLatin1String("Capabilities"))
{
while(xmlReader.readNextStartElement())
{
if (xmlReader.name() == QLatin1String("Contents"))
{
while(xmlReader.readNextStartElement())
{
if (xmlReader.name() == QLatin1String("Layer"))
{
QString identifier;
QString colorMap;
QList<Legend> legends;
QString tileMatrixSet;
QStringList urls;
QString format;
QString defaultDateTime;
QStringList dates;
while(xmlReader.readNextStartElement())
{
if (xmlReader.name() == QLatin1String("Identifier"))
{
identifier = xmlReader.readElementText();
}
else if (xmlReader.name() == QLatin1String("TileMatrixSetLink"))
{
while(xmlReader.readNextStartElement())
{
if (xmlReader.name() == QLatin1String("TileMatrixSet"))
{
tileMatrixSet = xmlReader.readElementText();
}
else
{
xmlReader.skipCurrentElement();
}
}
}
else if (xmlReader.name() == QLatin1String("Style"))
{
while(xmlReader.readNextStartElement())
{
if (xmlReader.name() == QLatin1String("LegendURL"))
{
Legend legend;
legend.m_url = xmlReader.attributes().value("xlink:href").toString();
legend.m_width = (int)xmlReader.attributes().value("width").toFloat();
legend.m_height = (int)xmlReader.attributes().value("height").toFloat();
//qDebug() << legend.m_url << legend.m_width << legend.m_height;
legends.append(legend);
xmlReader.skipCurrentElement();
}
else
{
xmlReader.skipCurrentElement();
}
}
}
else if (xmlReader.name() == QLatin1String("Metadata"))
{
colorMap = xmlReader.attributes().value("xlink:href").toString();
xmlReader.skipCurrentElement();
}
else if (xmlReader.name() == QLatin1String("Format"))
{
format = xmlReader.readElementText();
}
else if (xmlReader.name() == QLatin1String("ResourceURL"))
{
QString url = xmlReader.attributes().value("template").toString();
if (!url.isEmpty()) {
urls.append(url);
}
xmlReader.skipCurrentElement();
}
else if (xmlReader.name() == QLatin1String("Dimension"))
{
while(xmlReader.readNextStartElement())
{
if (xmlReader.name() == QLatin1String("Default"))
{
defaultDateTime = xmlReader.readElementText();
}
else if (xmlReader.name() == QLatin1String("Value"))
{
dates.append(xmlReader.readElementText());
}
else
{
xmlReader.skipCurrentElement();
}
}
}
else
{
xmlReader.skipCurrentElement();
}
}
// Some layers are <Format>application/vnd.mapbox-vector-tile</Format>
if (!identifier.isEmpty() && !tileMatrixSet.isEmpty() && ((format == "image/png") || (format == "image/jpeg")))
{
DataSet dataSet;
dataSet.m_identifier = identifier;
dataSet.m_legends = legends;
dataSet.m_tileMatrixSet = tileMatrixSet;
dataSet.m_format = format;
dataSet.m_defaultDateTime = defaultDateTime;
dataSet.m_dates = dates;
dataSets.append(dataSet);
//qDebug() << "Adding layer" << identifier << colorMap << legends << tileMatrixSet;
}
}
else
{
xmlReader.skipCurrentElement();
}
}
}
else if (xmlReader.name() == QLatin1String("ServiceMetadataURL"))
{
// Empty?
}
else
{
xmlReader.skipCurrentElement();
}
}
}
else
{
xmlReader.skipCurrentElement();
}
}
}
// Ignore "Premature end of document." here even if ok
if (!xmlReader.atEnd() && xmlReader.hasError())
{
qDebug() << "NASAGlobalImagery::handleReply: Error parsing XML: " << xmlReader.errorString();
}
emit dataUpdated(dataSets);
}
void NASAGlobalImagery::downloadLegend(const Legend& legend)
{
QUrl url(legend.m_url);
m_networkManager->get(QNetworkRequest(url));
}
void NASAGlobalImagery::downloadHTML(const QString& urlString)
{
QUrl url(urlString);
m_networkManager->get(QNetworkRequest(url));
}
void NASAGlobalImagery::handleSVG(const QString& url, const QByteArray& bytes)
{
emit legendAvailable(url, bytes);
}
void NASAGlobalImagery::handleJSON(const QByteArray& bytes)
{
MetaData metaData;
QJsonDocument document = QJsonDocument::fromJson(bytes);
if (document.isObject())
{
QJsonObject obj = document.object();
if (obj.contains(QStringLiteral("layers")))
{
for (const auto& oRef : obj.value(QStringLiteral("layers")).toObject())
{
Layer layer;
QJsonObject o = oRef.toObject();
if (o.contains(QStringLiteral("id"))) {
layer.m_identifier = o.value(QStringLiteral("id")).toString();
}
if (o.contains(QStringLiteral("title"))) {
layer.m_title = o.value(QStringLiteral("title")).toString();
}
if (o.contains(QStringLiteral("subtitle"))) {
layer.m_subtitle = o.value(QStringLiteral("subtitle")).toString();
}
if (o.contains(QStringLiteral("description"))) {
layer.m_descriptionURL = "https://worldview.earthdata.nasa.gov/config/metadata/layers/" + o.value(QStringLiteral("description")).toString() + ".html";
}
if (o.contains(QStringLiteral("startDate"))) {
layer.m_startDate = QDateTime::fromString(o.value(QStringLiteral("startDate")).toString(), Qt::ISODate);
}
if (o.contains(QStringLiteral("endDate"))) {
layer.m_endDate = QDateTime::fromString(o.value(QStringLiteral("endDate")).toString(), Qt::ISODate);
}
if (o.contains(QStringLiteral("ongoing"))) {
layer.m_ongoing = o.value(QStringLiteral("ongoing")).toBool();
}
if (o.contains(QStringLiteral("layerPeriod"))) {
layer.m_layerPeriod = o.value(QStringLiteral("layerPeriod")).toString();
}
if (o.contains(QStringLiteral("layergroup"))) {
layer.m_layerGroup = o.value(QStringLiteral("layergroup")).toString();
}
if (!layer.m_identifier.isEmpty()) {
metaData.m_layers.insert(layer.m_identifier, layer);
}
}
}
}
emit metaDataUpdated(metaData);
}
void NASAGlobalImagery::handleHTML(const QString& url, const QByteArray& bytes)
{
emit htmlAvailable(url, bytes);
}

Wyświetl plik

@ -0,0 +1,98 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_NASAGLOBALIMAGERY_H
#define INCLUDE_NASAGLOBALIMAGERY_H
#include <QtCore>
#include <QTimer>
#include "export.h"
class QNetworkAccessManager;
class QNetworkReply;
class QNetworkDiskCache;
// NASA GIBS (Global Imagery Browse Server) API (https://nasa-gibs.github.io/gibs-api-docs/)
// Gets details of available data sets for use on maps
// Also supports cached download of .svg legends
class SDRBASE_API NASAGlobalImagery : public QObject
{
Q_OBJECT
public:
struct Legend {
QString m_url; // Typically to .svg file
int m_width;
int m_height;
};
struct DataSet {
QString m_identifier;
QList<Legend> m_legends;
QString m_tileMatrixSet;
QString m_format;
QString m_defaultDateTime;
QStringList m_dates;
};
struct Layer {
QString m_identifier;
QString m_title;
QString m_subtitle;
QString m_descriptionURL;
QDateTime m_startDate;
QDateTime m_endDate;
bool m_ongoing;
QString m_layerPeriod;
QString m_layerGroup;
};
struct MetaData {
QHash<QString, Layer> m_layers;
};
NASAGlobalImagery();
~NASAGlobalImagery();
void getData();
void getMetaData();
void downloadLegend(const Legend& legend);
void downloadHTML(const QString& url);
public slots:
void handleReply(QNetworkReply* reply);
signals:
void dataUpdated(const QList<DataSet>& dataSets); // Emitted when paths to new data are available.
void metaDataUpdated(const MetaData& metaData);
void legendAvailable(const QString& url, const QByteArray data);
void htmlAvailable(const QString& url, const QByteArray data);
private:
QNetworkAccessManager *m_networkManager;
QNetworkDiskCache *m_cache;
void handleXML(const QByteArray& bytes);
void handleSVG(const QString& url, const QByteArray& bytes);
void handleJSON(const QByteArray& bytes);
void handleHTML(const QString& url, const QByteArray& bytes);
};
#endif /* INCLUDE_NASAGLOBALIMAGERY_H */

Wyświetl plik

@ -0,0 +1,130 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "rainviewer.h"
#include <QDebug>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
RainViewer::RainViewer()
{
connect(&m_timer, &QTimer::timeout, this, &RainViewer::update);
m_networkManager = new QNetworkAccessManager();
QObject::connect(m_networkManager, &QNetworkAccessManager::finished, this, &RainViewer::handleReply);
}
RainViewer::~RainViewer()
{
m_timer.stop();
QObject::disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &RainViewer::handleReply);
delete m_networkManager;
}
void RainViewer::getPathPeriodically(int periodInMins)
{
// Rain maps updated every 10mins
m_timer.setInterval(periodInMins*60*1000);
m_timer.start();
update();
}
void RainViewer::update()
{
getPath();
}
void RainViewer::getPath()
{
QUrl url(QString("https://api.rainviewer.com/public/weather-maps.json"));
m_networkManager->get(QNetworkRequest(url));
}
void RainViewer::handleReply(QNetworkReply* reply)
{
if (reply)
{
if (!reply->error())
{
QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
if (document.isObject())
{
QJsonObject obj = document.object();
QString radarPath = "";
QString satellitePath = "";
if (obj.contains(QStringLiteral("radar")))
{
QJsonValue val = obj.value(QStringLiteral("radar"));
QJsonObject mainObj = val.toObject();
if (mainObj.contains(QStringLiteral("past")))
{
QJsonArray past = mainObj.value(QStringLiteral("past")).toArray();
if (past.size() > 0)
{
QJsonObject mostRecent = past.last().toObject();
if (mostRecent.contains(QStringLiteral("path"))) {
radarPath = mostRecent.value(QStringLiteral("path")).toString();
}
}
}
}
else
{
qDebug() << "RainViewer::handleReply: Object doesn't contain a radar: " << obj;
}
if (obj.contains(QStringLiteral("satellite")))
{
QJsonValue val = obj.value(QStringLiteral("satellite"));
QJsonObject mainObj = val.toObject();
if (mainObj.contains(QStringLiteral("infrared")))
{
QJsonArray ir = mainObj.value(QStringLiteral("infrared")).toArray();
if (ir.size() > 0)
{
QJsonObject mostRecent = ir.last().toObject();
if (mostRecent.contains(QStringLiteral("path"))) {
satellitePath = mostRecent.value(QStringLiteral("path")).toString();
}
}
}
}
else
{
qDebug() << "RainViewer::handleReply: Object doesn't contain a satellite: " << obj;
}
emit pathUpdated(radarPath, satellitePath);
}
else
{
qDebug() << "RainViewer::handleReply: Document is not an object: " << document;
}
}
else
{
qDebug() << "RainViewer::handleReply: error: " << reply->error();
}
reply->deleteLater();
}
else
{
qDebug() << "RainViewer::handleReply: reply is null";
}
}

Wyświetl plik

@ -0,0 +1,55 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_RAINVIEWER_H
#define INCLUDE_RAINVIEWER_H
#include <QtCore>
#include <QTimer>
#include "export.h"
class QNetworkAccessManager;
class QNetworkReply;
// RainViewer API wrapper (https://www.rainviewer.com/)
// Gets details of currently available weather radar and satellite IR data
class SDRBASE_API RainViewer : public QObject
{
Q_OBJECT
public:
RainViewer();
~RainViewer();
void getPath();
void getPathPeriodically(int periodInMins=15);
public slots:
void update();
void handleReply(QNetworkReply* reply);
signals:
void pathUpdated(const QString& radarPath, const QString& satellitePath); // Emitted when paths to new data are available.
private:
QTimer m_timer; // Timer for periodic updates
QNetworkAccessManager *m_networkManager;
};
#endif /* INCLUDE_RAINVIEWER_H */

Wyświetl plik

@ -0,0 +1,164 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "spyserverlist.h"
#include <QDebug>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkReply>
#include <QXmlStreamReader>
#include <QNetworkDiskCache>
#include <QRegularExpression>
SpyServerList::SpyServerList()
{
m_networkManager = new QNetworkAccessManager();
QObject::connect(m_networkManager, &QNetworkAccessManager::finished, this, &SpyServerList::handleReply);
QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
QDir writeableDir(locations[0]);
if (!writeableDir.mkpath(QStringLiteral("cache") + QDir::separator() + QStringLiteral("spyserver"))) {
qDebug() << "Failed to create cache/spyserver";
}
m_cache = new QNetworkDiskCache();
m_cache->setCacheDirectory(locations[0] + QDir::separator() + QStringLiteral("cache") + QDir::separator() + QStringLiteral("spyserver"));
m_cache->setMaximumCacheSize(100000000);
m_networkManager->setCache(m_cache);
connect(&m_timer, &QTimer::timeout, this, &SpyServerList::update);
}
SpyServerList::~SpyServerList()
{
QObject::disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &SpyServerList::handleReply);
delete m_networkManager;
}
void SpyServerList::getData()
{
QUrl url(QString("https://airspy.com/directory/status.json"));
m_networkManager->get(QNetworkRequest(url));
}
void SpyServerList::getDataPeriodically(int periodInMins)
{
m_timer.setInterval(periodInMins*60*1000);
m_timer.start();
update();
}
void SpyServerList::update()
{
getData();
}
void SpyServerList::handleReply(QNetworkReply* reply)
{
if (reply)
{
if (!reply->error())
{
QString url = reply->url().toEncoded().constData();
QByteArray bytes = reply->readAll();
handleJSON(url, bytes);
}
else
{
qDebug() << "SpyServerList::handleReply: error: " << reply->error();
}
reply->deleteLater();
}
else
{
qDebug() << "SpyServerList::handleReply: reply is null";
}
}
void SpyServerList::handleJSON(const QString& url, const QByteArray& bytes)
{
QList<SpyServer> sdrs;
QJsonDocument document = QJsonDocument::fromJson(bytes);
if (document.isObject())
{
QJsonObject obj = document.object();
if (obj.contains(QStringLiteral("servers")))
{
QJsonArray servers = obj.value(QStringLiteral("servers")).toArray();
for (auto valRef : servers)
{
if (valRef.isObject())
{
QJsonObject serverObj = valRef.toObject();
SpyServer sdr;
if (serverObj.contains(QStringLiteral("generalDescription"))) {
sdr.m_generalDescription = serverObj.value(QStringLiteral("generalDescription")).toString();
}
if (serverObj.contains(QStringLiteral("deviceType"))) {
sdr.m_deviceType = serverObj.value(QStringLiteral("deviceType")).toString();
}
if (serverObj.contains(QStringLiteral("streamingHost"))) {
sdr.m_streamingHost = serverObj.value(QStringLiteral("streamingHost")).toString();
}
if (serverObj.contains(QStringLiteral("streamingPort"))) {
sdr.m_streamingPort = serverObj.value(QStringLiteral("streamingPort")).toInt();
}
if (serverObj.contains(QStringLiteral("currentClientCount"))) {
sdr.m_currentClientCount = serverObj.value(QStringLiteral("currentClientCount")).toInt();
}
if (serverObj.contains(QStringLiteral("maxClients"))) {
sdr.m_maxClients = serverObj.value(QStringLiteral("maxClients")).toInt();
}
if (serverObj.contains(QStringLiteral("antennaType"))) {
sdr.m_antennaType = serverObj.value(QStringLiteral("antennaType")).toString();
}
if (serverObj.contains(QStringLiteral("antennaLocation")))
{
QJsonObject location = serverObj.value(QStringLiteral("antennaLocation")).toObject();
sdr.m_latitude = location.value(QStringLiteral("lat")).toDouble();
sdr.m_longitude = location.value(QStringLiteral("long")).toDouble();
}
if (serverObj.contains(QStringLiteral("minimumFrequency"))) {
sdr.m_minimumFrequency = serverObj.value(QStringLiteral("minimumFrequency")).toInt();
}
if (serverObj.contains(QStringLiteral("maximumFrequency"))) {
sdr.m_maximumFrequency = serverObj.value(QStringLiteral("maximumFrequency")).toInt();
}
if (serverObj.contains(QStringLiteral("fullControlAllowed"))) {
sdr.m_fullControlAllowed = serverObj.value(QStringLiteral("fullControlAllowed")).toBool();
}
if (serverObj.contains(QStringLiteral("online"))) {
sdr.m_online = serverObj.value(QStringLiteral("online")).toBool();
}
sdrs.append(sdr);
}
}
}
}
else
{
qDebug() << "SpyServerList::handleJSON: Doc doesn't contain an object:\n" << document;
}
emit dataUpdated(sdrs);
}

Wyświetl plik

@ -0,0 +1,75 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_SPYSERVERLIST_H
#define INCLUDE_SPYSERVERLIST_H
#include <QtCore>
#include <QTimer>
#include "export.h"
class QNetworkAccessManager;
class QNetworkReply;
class QNetworkDiskCache;
// Gets a list of public SpyServers from https://airspy.com/directory/status.json
class SDRBASE_API SpyServerList : public QObject
{
Q_OBJECT
public:
struct SpyServer {
QString m_generalDescription;
QString m_deviceType;
QString m_streamingHost; // IP addrss
int m_streamingPort; // IP port
int m_currentClientCount;
int m_maxClients;
QString m_antennaType;
float m_latitude;
float m_longitude;
qint64 m_minimumFrequency;
qint64 m_maximumFrequency;
bool m_fullControlAllowed;
bool m_online;
};
SpyServerList();
~SpyServerList();
void getData();
void getDataPeriodically(int periodInMins=2);
public slots:
void handleReply(QNetworkReply* reply);
void update();
signals:
void dataUpdated(const QList<SpyServer>& sdrs); // Emitted when data are available.
private:
QNetworkAccessManager *m_networkManager;
QNetworkDiskCache *m_cache;
QTimer m_timer; // Timer for periodic updates
void handleJSON(const QString& url, const QByteArray& bytes);
};
#endif /* INCLUDE_SPYSERVERLIST_H */

Wyświetl plik

@ -0,0 +1,141 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QDebug>
#include "waypoints.h"
#include "csv.h"
QHash<QString, Waypoint *> *Waypoint::readCSV(const QString &filename)
{
QHash<QString, Waypoint *> *waypoints = new QHash<QString, Waypoint *>();
QFile file(filename);
if (file.open(QIODevice::ReadOnly | QIODevice::Text))
{
QTextStream in(&file);
QString error;
QStringList cols;
while(CSV::readRow(in, &cols))
{
Waypoint *waypoint = new Waypoint();
waypoint->m_name = cols[0];
waypoint->m_latitude = cols[1].toFloat();
waypoint->m_longitude = cols[2].toFloat();
waypoints->insert(waypoint->m_name, waypoint);
}
file.close();
}
else
{
qDebug() << "Waypoint::readCSV: Could not open " << filename << " for reading.";
}
return waypoints;
}
QSharedPointer<QHash<QString, Waypoint *>> Waypoints::m_waypoints;
QDateTime Waypoints::m_waypointsModifiedDateTime;
Waypoints::Waypoints(QObject *parent) :
QObject(parent)
{
connect(&m_dlm, &HttpDownloadManager::downloadComplete, this, &Waypoints::downloadFinished);
}
Waypoints::~Waypoints()
{
disconnect(&m_dlm, &HttpDownloadManager::downloadComplete, this, &Waypoints::downloadFinished);
}
QString Waypoints::getDataDir()
{
// Get directory to store app data in
QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
// First dir is writable
return locations[0];
}
QString Waypoints::getWaypointsFilename()
{
return getDataDir() + "/" + "waypoints.csv";
}
void Waypoints::downloadWaypoints()
{
QString filename = getWaypointsFilename();
QString urlString = WAYPOINTS_URL;
QUrl dbURL(urlString);
qDebug() << "Waypoints::downloadWaypoints: Downloading " << urlString;
emit downloadingURL(urlString);
m_dlm.download(dbURL, filename);
}
void Waypoints::downloadFinished(const QString& filename, bool success)
{
if (!success) {
qDebug() << "Waypoints::downloadFinished: Failed: " << filename;
}
if (filename == getWaypointsFilename())
{
emit downloadWaypointsFinished();
}
else
{
qDebug() << "Waypoints::downloadFinished: Unexpected filename: " << filename;
emit downloadError(QString("Unexpected filename: %1").arg(filename));
}
}
// Read waypoints
QHash<QString, Waypoint *> *Waypoints::readWaypoints()
{
return Waypoint::readCSV(getWaypointsFilename());
}
QSharedPointer<const QHash<QString, Waypoint *>> Waypoints::getWaypoints()
{
QDateTime filesDateTime = getWaypointsModifiedDateTime();
if (!m_waypoints || (filesDateTime > m_waypointsModifiedDateTime))
{
// Using shared pointer, so old object, if it exists, will be deleted, when no longer used
m_waypoints = QSharedPointer<QHash<QString, Waypoint *>>(readWaypoints());
m_waypointsModifiedDateTime = filesDateTime;
}
return m_waypoints;
}
// Gets the date and time the waypoint file was most recently modified
QDateTime Waypoints::getWaypointsModifiedDateTime()
{
QFileInfo fileInfo(getWaypointsFilename());
return fileInfo.lastModified();
}
// Find a waypoint by name
const Waypoint *Waypoints::findWayPoint(const QString& name)
{
if (m_waypoints->contains(name)) {
return m_waypoints->value(name);
} else {
return nullptr;
}
}

Wyświetl plik

@ -0,0 +1,80 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021-2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_WAYPOINTS_H
#define INCLUDE_WAYPOINTS_H
#include <QString>
#include <QFile>
#include <QHash>
#include <stdio.h>
#include <string.h>
#include "export.h"
#include "util/units.h"
#include "util/httpdownloadmanager.h"
#define WAYPOINTS_URL "https://github.com/srcejon/aviationwaypoints/waypoints.csv"
// Aviation waypoints
struct SDRBASE_API Waypoint {
QString m_name; // Typically 5 characters
float m_latitude;
float m_longitude;
static QHash<QString, Waypoint *> *readCSV(const QString &filename);
};
class SDRBASE_API Waypoints : public QObject {
Q_OBJECT
public:
Waypoints(QObject* parent = nullptr);
~Waypoints();
void downloadWaypoints();
static const Waypoint* findWayPoint(const QString& name);
static QSharedPointer<const QHash<QString, Waypoint *>> getWaypoints();
private:
HttpDownloadManager m_dlm;
static QSharedPointer<QHash<QString, Waypoint *>> m_waypoints;
static QDateTime m_waypointsModifiedDateTime;
static QHash<QString, Waypoint *> *readWaypoints();
static QString getDataDir();
static QString getWaypointsFilename();
static QDateTime getWaypointsModifiedDateTime();
public slots:
void downloadFinished(const QString& filename, bool success);
signals:
void downloadingURL(const QString& url);
void downloadError(const QString& error);
void downloadWaypointsFinished();
};
#endif // INCLUDE_WAYPOINTS_H