diff --git a/.appveyor.yml b/.appveyor.yml index b297bebcc..cb9b4d1ed 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -84,7 +84,7 @@ for: qml-module-qtlocation qml-module-qtpositioning qml-module-qtquick-window2 qml-module-qtquick-dialogs \ qml-module-qtquick-controls qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qtgraphicaleffects \ libqt5serialport5-dev qtdeclarative5-dev qtpositioning5-dev qtlocation5-dev \ - libqt5charts5-dev libqt5texttospeech5-dev libqt5gamepad5-dev libfaad-dev zlib1g-dev \ + libqt5charts5-dev libqt5texttospeech5-dev libqt5gamepad5-dev libqt5svg5-dev libfaad-dev zlib1g-dev \ libusb-1.0-0-dev libhidapi-dev libboost-all-dev libasound2-dev libopencv-dev libopencv-imgcodecs-dev \ libxml2-dev bison flex ffmpeg libpostproc-dev libavcodec-dev libavformat-dev \ libopus-dev libcodec2-dev libairspy-dev libhackrf-dev \ diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d4ef2174..22d05a8ce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -396,6 +396,10 @@ if(VS2019) set(LIBSIGMF_LIBRARIES_DEBUG "${EXTERNAL_LIBRARY_FOLDER}/libsigmf/lib/libsigmfd.lib" DbgHelp CACHE INTERNAL "") set(LIBSIGMF_DLL_DIR "${EXTERNAL_LIBRARY_FOLDER}/libsigmf/lib/" CACHE INTERNAL "") + set(LIBACARS_FOUND ON CACHE INTERNAL "") + set(LIBACARS_INCLUDE_DIR "${EXTERNAL_LIBRARY_FOLDER}/libacars/include/libacars-2" CACHE INTERNAL "") + set(LIBACARS_LIBRARIES "${EXTERNAL_LIBRARY_FOLDER}/libacars/lib/acars-2.lib" CACHE INTERNAL "") + # used on fixup_bundle phase set(WINDOWS_FIXUP_BUNDLE_LIB_DIRS "${EXTERNAL_LIBRARY_FOLDER}/fftw-3" @@ -654,7 +658,8 @@ if (BUILD_GUI) Quick QuickWidgets TextToSpeech - Svg) + Svg + SvgWidgets) else() find_package(Qt5 REQUIRED COMPONENTS @@ -677,6 +682,8 @@ if (BUILD_GUI) OpenGLWidgets Quick QuickWidgets + Svg + SvgWidgets OPTIONAL_COMPONENTS WebEngineQuick WebEngineCore @@ -691,6 +698,7 @@ if (BUILD_GUI) QuickWidgets Location TextToSpeech + Svg OPTIONAL_COMPONENTS WebEngine WebEngineCore diff --git a/debian/control b/debian/control index dde22244a..510bc75f2 100644 --- a/debian/control +++ b/debian/control @@ -17,6 +17,7 @@ Build-Depends: debhelper (>= 9), libqt5quick5, libqt5texttospeech5-dev, libqt5gamepad5-dev, + libqt5svg5-dev, qml-module-qtlocation, qml-module-qtpositioning, qml-module-qtquick-window2, @@ -64,6 +65,7 @@ Depends: ${shlibs:Depends}, libgl1-mesa-glx, libqt5multimedia5-plugins, libqt5gamepad5, + libqt5svg5, qtspeech5-speechd-plugin, pulseaudio, ffmpeg, diff --git a/doc/img/Map_plugin_GIBS.png b/doc/img/Map_plugin_GIBS.png new file mode 100644 index 000000000..065282733 Binary files /dev/null and b/doc/img/Map_plugin_GIBS.png differ diff --git a/doc/img/Map_plugin_SDRs.png b/doc/img/Map_plugin_SDRs.png new file mode 100644 index 000000000..98e690f57 Binary files /dev/null and b/doc/img/Map_plugin_SDRs.png differ diff --git a/doc/img/Map_plugin_clouds.png b/doc/img/Map_plugin_clouds.png new file mode 100644 index 000000000..34a22b40a Binary files /dev/null and b/doc/img/Map_plugin_clouds.png differ diff --git a/doc/img/Map_plugin_display_settings.png b/doc/img/Map_plugin_display_settings.png index 718f8bd1d..a8671b337 100644 Binary files a/doc/img/Map_plugin_display_settings.png and b/doc/img/Map_plugin_display_settings.png differ diff --git a/doc/img/Map_plugin_display_settings_apikeys.png b/doc/img/Map_plugin_display_settings_apikeys.png new file mode 100644 index 000000000..6fca61ea4 Binary files /dev/null and b/doc/img/Map_plugin_display_settings_apikeys.png differ diff --git a/doc/img/Map_plugin_display_settings_items.png b/doc/img/Map_plugin_display_settings_items.png new file mode 100644 index 000000000..32c6c3474 Binary files /dev/null and b/doc/img/Map_plugin_display_settings_items.png differ diff --git a/doc/img/Map_plugin_railway_legend.png b/doc/img/Map_plugin_railway_legend.png new file mode 100644 index 000000000..e72664cdb Binary files /dev/null and b/doc/img/Map_plugin_railway_legend.png differ diff --git a/doc/img/Map_plugin_railways.png b/doc/img/Map_plugin_railways.png new file mode 100644 index 000000000..825b8d58b Binary files /dev/null and b/doc/img/Map_plugin_railways.png differ diff --git a/doc/img/Map_plugin_seamarks.png b/doc/img/Map_plugin_seamarks.png new file mode 100644 index 000000000..3eba544a7 Binary files /dev/null and b/doc/img/Map_plugin_seamarks.png differ diff --git a/doc/img/Map_plugin_seamarks_legend.png b/doc/img/Map_plugin_seamarks_legend.png new file mode 100644 index 000000000..c988e3f63 Binary files /dev/null and b/doc/img/Map_plugin_seamarks_legend.png differ diff --git a/doc/img/Map_plugin_weather_radar.png b/doc/img/Map_plugin_weather_radar.png new file mode 100644 index 000000000..8cbcce79b Binary files /dev/null and b/doc/img/Map_plugin_weather_radar.png differ diff --git a/plugins/channelrx/demodadsb/adsbdemodgui.cpp b/plugins/channelrx/demodadsb/adsbdemodgui.cpp index 0388c84f8..c77b38f5e 100644 --- a/plugins/channelrx/demodadsb/adsbdemodgui.cpp +++ b/plugins/channelrx/demodadsb/adsbdemodgui.cpp @@ -4875,8 +4875,11 @@ ADSBDemodGUI::ADSBDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseb ui->map->rootContext()->setContextProperty("airportModel", &m_airportModel); ui->map->rootContext()->setContextProperty("airspaceModel", &m_airspaceModel); ui->map->rootContext()->setContextProperty("navAidModel", &m_navAidModel); +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) ui->map->setSource(QUrl(QStringLiteral("qrc:/map/map.qml"))); - +#else + ui->map->setSource(QUrl(QStringLiteral("qrc:/map/map_6.qml"))); +#endif connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); m_adsbDemod = reinterpret_cast(rxChannel); //new ADSBDemod(m_deviceUISet->m_deviceSourceAPI); diff --git a/plugins/channelrx/demodadsb/map.qrc b/plugins/channelrx/demodadsb/map.qrc index ca4e46714..26efab54d 100644 --- a/plugins/channelrx/demodadsb/map.qrc +++ b/plugins/channelrx/demodadsb/map.qrc @@ -1,6 +1,8 @@ map/map.qml + map/map_6.qml + map/ModifiedMapView.qml map/MapStation.qml map/aircraft_2engine.png map/aircraft_2enginesmall.png diff --git a/plugins/channelrx/demodadsb/map/ModifiedMapView.qml b/plugins/channelrx/demodadsb/map/ModifiedMapView.qml new file mode 100644 index 000000000..2aa266940 --- /dev/null +++ b/plugins/channelrx/demodadsb/map/ModifiedMapView.qml @@ -0,0 +1,178 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import QtQuick +import QtLocation as QL +import QtPositioning as QP +import Qt.labs.animation +/*! + \qmltype MapView + \inqmlmodule QtLocation + \brief An interactive map viewer component. + + MapView wraps a Map and adds the typical interactive features: + changing the zoom level, panning and tilting the map. + + The implementation is a QML assembly of smaller building blocks that are + available separately. In case you want to make changes in your own version + of this component, you can copy the QML, which is installed into the + \c qml/QtLocation module directory, and modify it as needed. + + \sa Map +*/ +Item { + /*! + \qmlproperty Map MapView::map + + This property provides access to the underlying Map instance. + */ + property alias map: map + + /*! + \qmlproperty real minimumZoomLevel + + The minimum zoom level according to the size of the view. + + \sa Map::minimumZoomLevel + */ + property real minimumZoomLevel: map.minimumZoomLevel + + /*! + \qmlproperty real maximumZoomLevel + + The maximum valid zoom level for the map. + + \sa Map::maximumZoomLevel + */ + property real maximumZoomLevel: map.maximumZoomLevel + + // -------------------------------- + // implementation + id: root + Component.onCompleted: map.resetPinchMinMax() + + QL.Map { + id: map + width: parent.width + height: parent.height + tilt: tiltHandler.persistentTranslation.y / -5 + property bool pinchAdjustingZoom: false + + BoundaryRule on zoomLevel { + id: br + minimum: map.minimumZoomLevel + maximum: map.maximumZoomLevel + } + + onZoomLevelChanged: { + br.returnToBounds(); + if (!pinchAdjustingZoom) resetPinchMinMax() + } + + function resetPinchMinMax() { + pinch.persistentScale = 1 + pinch.scaleAxis.minimum = Math.pow(2, root.minimumZoomLevel - map.zoomLevel + 1) + pinch.scaleAxis.maximum = Math.pow(2, root.maximumZoomLevel - map.zoomLevel - 1) + } + + PinchHandler { + id: pinch + target: null + property real rawBearing: 0 + property QP.geoCoordinate startCentroid + onActiveChanged: if (active) { + flickAnimation.stop() + pinch.startCentroid = map.toCoordinate(pinch.centroid.position, false) + } else { + flickAnimation.restart(centroid.velocity) + map.resetPinchMinMax() + } + onScaleChanged: (delta) => { + map.pinchAdjustingZoom = true + map.zoomLevel += Math.log2(delta) + map.alignCoordinateToPoint(pinch.startCentroid, pinch.centroid.position) + map.pinchAdjustingZoom = false + } + onRotationChanged: (delta) => { + pinch.rawBearing -= delta + // snap to 0° if we're close enough + map.bearing = (Math.abs(pinch.rawBearing) < 5) ? 0 : pinch.rawBearing + map.alignCoordinateToPoint(pinch.startCentroid, pinch.centroid.position) + } + grabPermissions: PointerHandler.TakeOverForbidden + } + WheelHandler { + id: wheel + // workaround for QTBUG-87646 / QTBUG-112394 / QTBUG-112432: + // Magic Mouse pretends to be a trackpad but doesn't work with PinchHandler + // and we don't yet distinguish mice and trackpads on Wayland either + acceptedDevices: Qt.platform.pluginName === "cocoa" || Qt.platform.pluginName === "wayland" + ? PointerDevice.Mouse | PointerDevice.TouchPad + : PointerDevice.Mouse + onWheel: (event) => { + const loc = map.toCoordinate(wheel.point.position) + switch (event.modifiers) { + case Qt.NoModifier: + // jonb - Changed to make more like Qt5 + //map.zoomLevel += event.angleDelta.y / 120 + map.zoomLevel += event.angleDelta.y / 1000 + break + case Qt.ShiftModifier: + map.bearing += event.angleDelta.y / 15 + break + case Qt.ControlModifier: + map.tilt += event.angleDelta.y / 15 + break + } + map.alignCoordinateToPoint(loc, wheel.point.position) + } + } + DragHandler { + id: drag + signal flickStarted // for autotests only + signal flickEnded + target: null + onTranslationChanged: (delta) => map.pan(-delta.x, -delta.y) + onActiveChanged: if (active) { + flickAnimation.stop() + } else { + flickAnimation.restart(centroid.velocity) + } + } + + property vector3d animDest + onAnimDestChanged: if (flickAnimation.running) { + const delta = Qt.vector2d(animDest.x - flickAnimation.animDestLast.x, animDest.y - flickAnimation.animDestLast.y) + map.pan(-delta.x, -delta.y) + flickAnimation.animDestLast = animDest + } + + Vector3dAnimation on animDest { + id: flickAnimation + property vector3d animDestLast + from: Qt.vector3d(0, 0, 0) + duration: 500 + easing.type: Easing.OutQuad + onStarted: drag.flickStarted() + onStopped: drag.flickEnded() + + function restart(vel) { + stop() + map.animDest = Qt.vector3d(0, 0, 0) + animDestLast = Qt.vector3d(0, 0, 0) + to = Qt.vector3d(vel.x / duration * 100, vel.y / duration * 100, 0) + start() + } + } + + DragHandler { + id: tiltHandler + minimumPointCount: 2 + maximumPointCount: 2 + target: null + xAxis.enabled: false + grabPermissions: PointerHandler.TakeOverForbidden + onActiveChanged: if (active) flickAnimation.stop() + } + } +} diff --git a/plugins/channelrx/demodadsb/map/map_6.qml b/plugins/channelrx/demodadsb/map/map_6.qml new file mode 100644 index 000000000..259eeb05b --- /dev/null +++ b/plugins/channelrx/demodadsb/map/map_6.qml @@ -0,0 +1,537 @@ +import QtQuick 2.14 +import QtQuick.Window 2.14 +import QtQuick.Controls 2.14 +import QtPositioning 6.5 +import QtLocation 6.5 +import Qt5Compat.GraphicalEffects + +Item { + id: qmlMap + property int aircraftZoomLevel: 11 + property int aircraftMinZoomLevel: 11 + property int airportZoomLevel: 11 + property string mapProvider: "osm" + property variant mapPtr + property string requestedMapType + property bool lightIcons + property variant guiPtr + property bool smoothing + + function createMap(pluginParameters, requestedMap, gui) { + requestedMapType = requestedMap + guiPtr = gui + + var paramString = "" + for (var prop in pluginParameters) { + var parameter = 'PluginParameter { name: "' + prop + '"; value: "' + pluginParameters[prop] + '"}' + paramString = paramString + parameter + } + var pluginString = 'import QtLocation 6.5; Plugin{ name:"' + mapProvider + '"; ' + paramString + '}' + var plugin = Qt.createQmlObject (pluginString, qmlMap) + + if (mapPtr) { + // Objects aren't destroyed immediately, so don't call findChild("map") + mapPtr.destroy() + mapPtr = null + } + mapPtr = actualMapComponent.createObject(page) + mapPtr.map.plugin = plugin + mapPtr.map.forceActiveFocus() + return mapPtr + } + + Item { + id: page + anchors.fill: parent + } + + Component { + id: actualMapComponent + + ModifiedMapView { + id: mapView + objectName: "mapView" + anchors.fill: parent + map.center: QtPositioning.coordinate(51.5, 0.125) // London + map.zoomLevel: 10 + map.objectName: "map" + + // not in 6 + //gesture.enabled: true + //gesture.acceptedGestures: MapGestureArea.PinchGesture | MapGestureArea.PanGesture + + MouseArea { + anchors.fill: parent + propagateComposedEvents: true + onClicked: { + // Unhighlight current aircraft + guiPtr.clearHighlighted() + mouse.accepted = false + } + } + + MapStation { + id: station + objectName: "station" + stationName: "Home" + } + + MapItemView { + model: airspaceModel + delegate: airspaceComponent + parent: mapView.map + } + + MapItemView { + model: navAidModel + delegate: navAidComponent + parent: mapView.map + } + + MapItemView { + model: airspaceModel + delegate: airspaceNameComponent + parent: mapView.map + } + + MapItemView { + model: airportModel + delegate: airportComponent + parent: mapView.map + } + + // This needs to be before aircraftComponent MapItemView, so it's drawn underneath + MapItemView { + model: aircraftModel + delegate: aircraftPathComponent + parent: mapView.map + } + + MapItemView { + model: aircraftModel + delegate: aircraftComponent + parent: mapView.map + } + + map.onZoomLevelChanged: { + if (map.zoomLevel > aircraftMinZoomLevel) { + aircraftZoomLevel = map.zoomLevel + } else { + aircraftZoomLevel = aircraftMinZoomLevel + } + if (map.zoomLevel > 11) { + station.zoomLevel = map.zoomLevel + airportZoomLevel = map.zoomLevel + } else { + station.zoomLevel = 11 + airportZoomLevel = 11 + } + } + + map.onSupportedMapTypesChanged : { + for (var i = 0; i < map.supportedMapTypes.length; i++) { + if (requestedMapType == map.supportedMapTypes[i].name) { + map.activeMapType = map.supportedMapTypes[i] + } + } + lightIcons = (requestedMapType == "Night Transit Map") || (requestedMapType == "mapbox://styles/mapbox/dark-v9") + } + + } + } + + Component { + id: navAidComponent + MapQuickItem { + id: navAid + anchorPoint.x: image.width/2 + anchorPoint.y: image.height/2 + coordinate: position + zoomLevel: airportZoomLevel + + sourceItem: Grid { + columns: 1 + Grid { + horizontalItemAlignment: Grid.AlignHCenter + columnSpacing: 5 + layer.enabled: smoothing + layer.smooth: smoothing + Image { + id: image + source: navAidImage + visible: !lightIcons + MouseArea { + anchors.fill: parent + onClicked: (mouse) => { + selected = !selected + } + } + } + ColorOverlay { + cached: true + width: image.width + height: image.height + source: image + color: "#c0ffffff" + visible: lightIcons + } + Rectangle { + id: bubble + color: bubbleColour + border.width: 1 + width: text.width + 5 + height: text.height + 5 + radius: 5 + Text { + id: text + anchors.centerIn: parent + text: navAidData + } + MouseArea { + anchors.fill: parent + hoverEnabled: true + onClicked: (mouse) => { + selected = !selected + } + } + } + } + } + } + } + + Component { + id: airspaceComponent + MapPolygon { + border.width: 1 + border.color: airspaceBorderColor + color: airspaceFillColor + path: airspacePolygon + } + } + + Component { + id: airspaceNameComponent + MapQuickItem { + coordinate: position + anchorPoint.x: airspaceText.width/2 + anchorPoint.y: airspaceText.height/2 + zoomLevel: airportZoomLevel + sourceItem: Grid { + columns: 1 + Grid { + layer.enabled: smoothing + layer.smooth: smoothing + horizontalItemAlignment: Grid.AlignHCenter + Text { + id: airspaceText + text: details + } + } + } + } + } + + Component { + id: aircraftPathComponent + MapPolyline { + line.width: 2 + line.color: 'gray' + path: aircraftPath + } + } + + Component { + id: aircraftComponent + MapQuickItem { + id: aircraft + anchorPoint.x: image.width/2 + anchorPoint.y: image.height/2 + coordinate: position + zoomLevel: aircraftZoomLevel + + sourceItem: Grid { + columns: 1 + Grid { + layer.enabled: smoothing + layer.smooth: smoothing + horizontalItemAlignment: Grid.AlignHCenter + Image { + id: image + rotation: heading + source: aircraftImage + visible: !lightIcons + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + onClicked: { + if (mouse.button === Qt.LeftButton) { + highlighted = true + console.log("z=" + aircraft.sourceItem.z) + aircraft.sourceItem.z = aircraft.sourceItem.z + 1 + } else if (mouse.button === Qt.RightButton) { + contextMenu.popup() + } + } + onDoubleClicked: { + target = true + } + } + } + ColorOverlay { + cached: true + width: image.width + height: image.height + rotation: heading + source: image + color: "#c0ffffff" + visible: lightIcons + MouseArea { + anchors.fill: parent + onClicked: { + highlighted = true + } + onDoubleClicked: { + target = true + } + } + } + Rectangle { + id: bubble + color: bubbleColour + border.width: 1 + width: text.width * 1.1 + height: text.height * 1.1 + radius: 5 + Text { + id: text + anchors.centerIn: parent + text: adsbData + textFormat: TextEdit.RichText + } + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + onClicked: { + if (mouse.button === Qt.LeftButton) { + showAll = !showAll + } else if (mouse.button === Qt.RightButton) { + contextMenu.popup() + } + } + Menu { + id: contextMenu + MenuItem { + text: "Set as target" + onTriggered: target = true + } + MenuItem { + text: "Find on feature map" + onTriggered: aircraftModel.findOnMap(index) + } + } + } + } + } + } + } + } + + Component { + id: airportComponent + MapItemGroup { + MapItemGroup { + property var groupVisible: false + id: rangeGroup + MapCircle { + id: circle5nm + center: position + color: "transparent" + border.color: "gray" + radius: 9260 // 5nm in metres + visible: rangeGroup.groupVisible + } + MapCircle { + id: circle10nm + center: position + color: "transparent" + border.color: "gray" + radius: 18520 + visible: rangeGroup.groupVisible + } + MapCircle { + id: circle15nm + center: airport.coordinate + color: "transparent" + border.color: "gray" + radius: 27780 + visible: rangeGroup.groupVisible + } + MapQuickItem { + id: text5nm + coordinate { + latitude: position.latitude + longitude: position.longitude + (5/60)/Math.cos(Math.abs(position.latitude)*Math.PI/180) + } + anchorPoint.x: 0 + anchorPoint.y: height/2 + sourceItem: Text { + color: "grey" + text: "5nm" + } + visible: rangeGroup.groupVisible + } + MapQuickItem { + id: text10nm + coordinate { + latitude: position.latitude + longitude: position.longitude + (10/60)/Math.cos(Math.abs(position.latitude)*Math.PI/180) + } + anchorPoint.x: 0 + anchorPoint.y: height/2 + sourceItem: Text { + color: "grey" + text: "10nm" + } + visible: rangeGroup.groupVisible + } + MapQuickItem { + id: text15nm + coordinate { + latitude: position.latitude + longitude: position.longitude + (15/60)/Math.cos(Math.abs(position.latitude)*Math.PI/180) + } + anchorPoint.x: 0 + anchorPoint.y: height/2 + sourceItem: Text { + color: "grey" + text: "15nm" + } + visible: rangeGroup.groupVisible + } + } + + MapQuickItem { + id: airport + anchorPoint.x: image.width/2 + anchorPoint.y: image.height/2 + coordinate: position + zoomLevel: airportZoomLevel + sourceItem: Grid { + columns: 1 + Grid { + horizontalItemAlignment: Grid.AlignHCenter + layer.enabled: smoothing + layer.smooth: smoothing + Image { + id: image + source: airportImage + visible: !lightIcons + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + onClicked: (mouse) => { + console.log("AIRPORT CLICKED ************************* "); + if (mouse.button === Qt.RightButton) { + showRangeItem.visible = !rangeGroup.groupVisible + hideRangeItem.visible = rangeGroup.groupVisible + menuItems.clear() + var scanners = airportModel.getFreqScanners() + for (var i = 0; i < scanners.length; i++) { + menuItems.append({ + text: "Send to Frequency Scanner " + scanners[i], + airport: index, + scanner: scanners[i] + }) + } + contextMenu.popup() + } + } + onDoubleClicked: (mouse) => { + rangeGroup.groupVisible = !rangeGroup.groupVisible + } + + ListModel { + id: menuItems + } + + Menu { + id: contextMenu + MenuItem { + id: showRangeItem + text: "Show range rings" + onTriggered: rangeGroup.groupVisible = true + height: visible ? implicitHeight : 0 + } + MenuItem { + id: hideRangeItem + text: "Hide range rings" + onTriggered: rangeGroup.groupVisible = false + height: visible ? implicitHeight : 0 + } + Instantiator { + model: menuItems + MenuItem { + text: model.text + onTriggered: airportModel.sendToFreqScanner(model.airport, model.scanner) + } + onObjectAdded: function(index, object) { + contextMenu.insertItem(index, object) + } + onObjectRemoved: function(index, object) { + contextMenu.removeItem(object) + } + } + } + } + } + ColorOverlay { + cached: true + width: image.width + height: image.height + source: image + color: "#c0ffffff" + visible: lightIcons + } + Rectangle { + id: bubble + color: bubbleColour + border.width: 1 + width: text.width + 5 + height: text.height + 5 + radius: 5 + Text { + id: text + anchors.centerIn: parent + text: airportData + } + MouseArea { + anchors.fill: parent + onClicked: (mouse) => { + console.log("AIRPORT 2 CLICKED ************************* "); + if (showFreq) { + var freqIdx = Math.floor((mouse.y-5)/((height-10)/airportDataRows)) + if (freqIdx == 0) { + showFreq = false + } + } else { + showFreq = true + } + } + onDoubleClicked: (mouse) => { + if (showFreq) { + var freqIdx = Math.floor((mouse.y-5)/((height-10)/airportDataRows)) + if (freqIdx != 0) { + selectedFreq = freqIdx - 1 + } + } + } + } + } + } + } + } + } + } + +} diff --git a/plugins/feature/map/CMakeLists.txt b/plugins/feature/map/CMakeLists.txt index a4ffd64c8..975546db9 100644 --- a/plugins/feature/map/CMakeLists.txt +++ b/plugins/feature/map/CMakeLists.txt @@ -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) diff --git a/plugins/feature/map/cesiuminterface.cpp b/plugins/feature/map/cesiuminterface.cpp index 097a3266e..e9a3e39a2 100644 --- a/plugins/feature/map/cesiuminterface.cpp +++ b/plugins/feature/map/cesiuminterface.cpp @@ -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& 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 { diff --git a/plugins/feature/map/cesiuminterface.h b/plugins/feature/map/cesiuminterface.h index 359929ed3..418d0bf07 100644 --- a/plugins/feature/map/cesiuminterface.h +++ b/plugins/feature/map/cesiuminterface.h @@ -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& 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(); diff --git a/plugins/feature/map/czml.cpp b/plugins/feature/map/czml.cpp index 1ba0f1bb0..002e1e5d1 100644 --- a/plugins/feature/map/czml.cpp +++ b/plugins/feature/map/czml.cpp @@ -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")) { diff --git a/plugins/feature/map/icons.qrc b/plugins/feature/map/icons.qrc index 46b171f4c..cd287def8 100644 --- a/plugins/feature/map/icons.qrc +++ b/plugins/feature/map/icons.qrc @@ -1,11 +1,18 @@ - - icons/groundtracks.png - icons/clock.png - icons/ibp.png - icons/muf.png - icons/fof2.png - icons/controltower.png - icons/vor.png - + + icons/groundtracks.png + icons/clock.png + icons/ibp.png + icons/muf.png + icons/fof2.png + icons/controltower.png + icons/vor.png + icons/precipitation.png + icons/anchor.png + icons/cloud.png + icons/layers.png + icons/railway.png + icons/waypoints.png + icons/earthsat.png + diff --git a/plugins/feature/map/icons/anchor.png b/plugins/feature/map/icons/anchor.png new file mode 100644 index 000000000..39400706b Binary files /dev/null and b/plugins/feature/map/icons/anchor.png differ diff --git a/plugins/feature/map/icons/cloud.png b/plugins/feature/map/icons/cloud.png new file mode 100644 index 000000000..7a69d8095 Binary files /dev/null and b/plugins/feature/map/icons/cloud.png differ diff --git a/plugins/feature/map/icons/earthsat.png b/plugins/feature/map/icons/earthsat.png new file mode 100644 index 000000000..88163ed13 Binary files /dev/null and b/plugins/feature/map/icons/earthsat.png differ diff --git a/plugins/feature/map/icons/layers.png b/plugins/feature/map/icons/layers.png new file mode 100644 index 000000000..89e904527 Binary files /dev/null and b/plugins/feature/map/icons/layers.png differ diff --git a/plugins/feature/map/icons/precipitation.png b/plugins/feature/map/icons/precipitation.png new file mode 100644 index 000000000..0f2a8a6a0 Binary files /dev/null and b/plugins/feature/map/icons/precipitation.png differ diff --git a/plugins/feature/map/icons/railway.png b/plugins/feature/map/icons/railway.png new file mode 100644 index 000000000..3fc8f419b Binary files /dev/null and b/plugins/feature/map/icons/railway.png differ diff --git a/plugins/feature/map/icons/waypoints.png b/plugins/feature/map/icons/waypoints.png new file mode 100644 index 000000000..b8aafaccc Binary files /dev/null and b/plugins/feature/map/icons/waypoints.png differ diff --git a/plugins/feature/map/map.cpp b/plugins/feature/map/map.cpp index 76c4abaf5..ed12fe3d4 100644 --- a/plugins/feature/map/map.cpp +++ b/plugins/feature/map/map.cpp @@ -1,5 +1,5 @@ /////////////////////////////////////////////////////////////////////////////////// -// Copyright (C) 2021-2023 Jon Beniston, M7RCE // +// Copyright (C) 2021-2024 Jon Beniston, M7RCE // // Copyright (C) 2021-2023 Edouard Griffiths, F4EXB // // Copyright (C) 2022 Jiří Pinkava // // // @@ -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& 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& 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(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; diff --git a/plugins/feature/map/map.h b/plugins/feature/map/map.h index a2867ee13..6f68f557a 100644 --- a/plugins/feature/map/map.h +++ b/plugins/feature/map/map.h @@ -1,7 +1,7 @@ /////////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2020, 2022 Edouard Griffiths, F4EXB // // Copyright (C) 2020 Kacper Michajłow // -// Copyright (C) 2021-2022 Jon Beniston, M7RCE // +// Copyright (C) 2021-2024 Jon Beniston, M7RCE // // Copyright (C) 2022 Jiří Pinkava // // // // 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& 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 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 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& settingsKeys, bool force = false); void webapiFormatFeatureReport(SWGSDRangel::SWGFeatureReport& response); void webapiReverseSendSettings(const QList& 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); }; diff --git a/plugins/feature/map/map.qrc b/plugins/feature/map/map.qrc index ef95138e4..d2a1e2807 100644 --- a/plugins/feature/map/map.qrc +++ b/plugins/feature/map/map.qrc @@ -8,6 +8,8 @@ map/antennadab.png map/antennafm.png map/antennaam.png + map/antennakiwi.png + map/antennaspyserver.png map/ionosonde.png map/VOR.png map/VOR-DME.png @@ -21,6 +23,7 @@ map/airport_medium.png map/airport_small.png map/heliport.png + map/waypoint.png map/map3d.html diff --git a/plugins/feature/map/map/antennakiwi.png b/plugins/feature/map/map/antennakiwi.png new file mode 100644 index 000000000..0ba063180 Binary files /dev/null and b/plugins/feature/map/map/antennakiwi.png differ diff --git a/plugins/feature/map/map/antennaspyserver.png b/plugins/feature/map/map/antennaspyserver.png new file mode 100644 index 000000000..85a4dd5dc Binary files /dev/null and b/plugins/feature/map/map/antennaspyserver.png differ diff --git a/plugins/feature/map/map/map.qml b/plugins/feature/map/map/map.qml index 2501a74e3..ba6b824cf 100644 --- a/plugins/feature/map/map/map.qml +++ b/plugins/feature/map/map/map.qml @@ -303,12 +303,6 @@ Item { height: text.height + 5 radius: 5 visible: mapTextVisible - Text { - id: text - anchors.centerIn: parent - text: mapText - textFormat: TextEdit.RichText - } MouseArea { anchors.fill: parent hoverEnabled: true @@ -406,6 +400,17 @@ Item { } } } + // Have Text after MouseArea, so links can be clicked + Text { + id: text + anchors.centerIn: parent + text: mapText + textFormat: TextEdit.RichText + onLinkActivated: { + console.log("Link", link); + mapModel.link(link); + } + } } } } diff --git a/plugins/feature/map/map/map3d.html b/plugins/feature/map/map/map3d.html index 62ff61adc..7c529afee 100644 --- a/plugins/feature/map/map/map3d.html +++ b/plugins/feature/map/map/map3d.html @@ -1,623 +1,776 @@ - - - - - - - - - -
- - - - - + + + + + + + + + +
+ + + + + diff --git a/plugins/feature/map/map/map_6.qml b/plugins/feature/map/map/map_6.qml index a774e695c..08145323d 100644 --- a/plugins/feature/map/map/map_6.qml +++ b/plugins/feature/map/map/map_6.qml @@ -316,12 +316,6 @@ Item { height: text.height + 5 radius: 5 visible: mapTextVisible - Text { - id: text - anchors.centerIn: parent - text: mapText - textFormat: TextEdit.RichText - } MouseArea { anchors.fill: parent hoverEnabled: true @@ -417,6 +411,17 @@ Item { object.menu.removeItem(object) } } + } + } + // Have Text after MouseArea, so links can be clicked + Text { + id: text + anchors.centerIn: parent + text: mapText + textFormat: TextEdit.RichText + onLinkActivated: { + console.log("Link", link); + mapModel.link(link); } } } diff --git a/plugins/feature/map/map/waypoint.png b/plugins/feature/map/map/waypoint.png new file mode 100644 index 000000000..8ff6fdc6b Binary files /dev/null and b/plugins/feature/map/map/waypoint.png differ diff --git a/plugins/feature/map/mapgui.cpp b/plugins/feature/map/mapgui.cpp index f1c9df910..e192af0b5 100644 --- a/plugins/feature/map/mapgui.cpp +++ b/plugins/feature/map/mapgui.cpp @@ -1,5 +1,5 @@ /////////////////////////////////////////////////////////////////////////////////// -// Copyright (C) 2021-2023 Jon Beniston, M7RCE // +// Copyright (C) 2021-2024 Jon Beniston, M7RCE // // Copyright (C) 2021-2022 Edouard Griffiths, F4EXB // // // // This program is free software; you can redistribute it and/or modify // @@ -24,6 +24,10 @@ #include #include #include +#include +#include +#include +#include #ifdef QT_WEBENGINE_FOUND #include @@ -35,7 +39,11 @@ #include "gui/basicfeaturesettingsdialog.h" #include "gui/dialogpositioner.h" #include "mainwindow.h" +#include "device/deviceset.h" +#include "device/deviceapi.h" +#include "dsp/devicesamplesource.h" #include "device/deviceuiset.h" +#include "device/deviceenumerator.h" #include "util/units.h" #include "util/maidenhead.h" #include "util/morse.h" @@ -50,6 +58,9 @@ #include "mapgui.h" #include "SWGMapItem.h" #include "SWGTargetAzimuthElevation.h" +#include "SWGDeviceSettings.h" +#include "SWGKiwiSDRSettings.h" +#include "SWGRemoteTCPInputSettings.h" MapGUI* MapGUI::create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature) { @@ -140,7 +151,7 @@ bool MapGUI::handleMessage(const Message& message) for (int i = 0; i < m_availableChannelOrFeatures.size(); i++) { - if (m_availableChannelOrFeatures[i].m_source == msgMapItem.getPipeSource()) + if (m_availableChannelOrFeatures[i].m_object == msgMapItem.getPipeSource()) { for (int j = 0; j < MapSettings::m_pipeTypes.size(); j++) { @@ -192,7 +203,12 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur m_beaconDialog(this), m_ibpBeaconDialog(this), m_radioTimeDialog(this), - m_cesium(nullptr) + m_cesium(nullptr), + m_legend(nullptr), + m_nasaWidget(nullptr), + m_legendWidget(nullptr), + m_overviewWidget(nullptr), + m_descriptionWidget(nullptr) { m_feature = feature; setAttribute(Qt::WA_DeleteOnClose, true); @@ -214,10 +230,36 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur ui->map->setFormat(format); } + createNASAGlobalImageryView(); + connect(&m_nasaGlobalImagery, &NASAGlobalImagery::dataUpdated, this, &MapGUI::nasaGlobalImageryDataUpdated); + connect(&m_nasaGlobalImagery, &NASAGlobalImagery::metaDataUpdated, this, &MapGUI::nasaGlobalImageryMetaDataUpdated); + connect(&m_nasaGlobalImagery, &NASAGlobalImagery::legendAvailable, this, &MapGUI::nasaGlobalImageryLegendAvailable); + connect(&m_nasaGlobalImagery, &NASAGlobalImagery::htmlAvailable, this, &MapGUI::nasaGlobalImageryHTMLAvailable); + m_nasaGlobalImagery.getData(); + m_nasaGlobalImagery.getMetaData(); + + createLayersMenu(); + displayToolbar(); + connect(screen(), &QScreen::orientationChanged, this, &MapGUI::orientationChanged); +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + screen()->setOrientationUpdateMask(Qt::PortraitOrientation + | Qt::LandscapeOrientation + | Qt::InvertedPortraitOrientation + | Qt::InvertedLandscapeOrientation); +#endif + clearWikiMediaOSMCache(); + m_rainViewer = new RainViewer(); + connect(m_rainViewer, &RainViewer::pathUpdated, this, &MapGUI::pathUpdated); + m_rainViewer->getPathPeriodically(); + + m_mapTileServerPort = 60602; + m_mapTileServer = new MapTileServer(m_mapTileServerPort); + m_mapTileServer->setThunderforestAPIKey(thunderforestAPIKey()); + m_mapTileServer->setMaptilerAPIKey(maptilerAPIKey()); m_osmPort = 0; - m_templateServer = new OSMTemplateServer(thunderforestAPIKey(), maptilerAPIKey(), m_osmPort); + m_templateServer = new OSMTemplateServer(thunderforestAPIKey(), maptilerAPIKey(), m_mapTileServerPort, m_osmPort); // Web server to serve dynamic files from QResources m_webPort = 0; @@ -253,6 +295,9 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + connect(&m_kiwiSDRList, &KiwiSDRList::dataUpdated, this, &MapGUI::kiwiSDRUpdated); + connect(&m_spyServerList, &SpyServerList::dataUpdated, this, &MapGUI::spyServerUpdated); + #ifdef QT_WEBENGINE_FOUND QWebEngineSettings *settings = ui->web->settings(); settings->setAttribute(QWebEngineSettings::FullScreenSupportEnabled, true); @@ -320,12 +365,17 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur addNavAids(); addAirspace(); addAirports(); + addWaypoints(); addNavtex(); addVLF(); + addKiwiSDR(); + addSpyServer(); displaySettings(); applySettings(true); + connect(&m_objectMapModel, &ObjectMapModel::linkClicked, this, &MapGUI::linkClicked); + connect(&m_redrawMapTimer, &QTimer::timeout, this, &MapGUI::redrawMap); m_redrawMapTimer.setSingleShot(true); ui->map->installEventFilter(this); @@ -342,12 +392,18 @@ MapGUI::~MapGUI() disconnect(&m_redrawMapTimer, &QTimer::timeout, this, &MapGUI::redrawMap); m_redrawMapTimer.stop(); //m_cesium->deleteLater(); + delete m_rainViewer; delete m_cesium; if (m_templateServer) { m_templateServer->close(); delete m_templateServer; } + if (m_mapTileServer) + { + m_mapTileServer->close(); + delete m_mapTileServer; + } if (m_webServer) { m_webServer->close(); @@ -542,6 +598,124 @@ void MapGUI::addRadar() update(m_map, &radarMapItem, "Radar"); } +QString MapGUI::formatFrequency(qint64 frequency) const +{ + QString s = QString::number(frequency); + if (s.endsWith("000000000")) { + return s.left(s.size() - 9) + " GHz"; + } else if (s.endsWith("000000")) { + return s.left(s.size() - 6) + " MHz"; + } else if (s.endsWith("000")) { + return s.left(s.size() - 3) + " kHz"; + } + return s + " Hz"; +} + +void MapGUI::addKiwiSDR() +{ + m_kiwiSDRList.getDataPeriodically(); +} + +void MapGUI::kiwiSDRUpdated(const QList& sdrs) +{ + for (const auto& sdr : sdrs) + { + SWGSDRangel::SWGMapItem kiwiMapItem; + kiwiMapItem.setName(new QString(sdr.m_name)); + kiwiMapItem.setLatitude(sdr.m_latitude); + kiwiMapItem.setLongitude(sdr.m_longitude); + kiwiMapItem.setAltitude(sdr.m_altitude); + kiwiMapItem.setImage(new QString("antennakiwi.png")); + kiwiMapItem.setImageRotation(0); + QString url = QString("sdrangel-kiwisdr://%1").arg(sdr.m_url); + QString antenna = sdr.m_antenna; + if (!sdr.m_antennaConnected) { + antenna.append(" (Not connected)"); + } + QString text = QString("KiwiSDR\nName: %1\nHW: %2\nUsers: %3/%4\nFrequency: %5 - %6\nAntenna: %7\nSNR: %8 dB\nURL: %9") + .arg(sdr.m_name) + .arg(sdr.m_sdrHW) + .arg(sdr.m_users) + .arg(sdr.m_usersMax) + .arg(formatFrequency(sdr.m_lowFrequency)) + .arg(formatFrequency(sdr.m_highFrequency)) + .arg(antenna) + .arg(sdr.m_snr) + .arg(QString("%2").arg(url).arg(sdr.m_url)) + ; + kiwiMapItem.setText(new QString(text)); + kiwiMapItem.setModel(new QString("antenna.glb")); + kiwiMapItem.setFixedPosition(true); + kiwiMapItem.setOrientation(0); + QString band = "HF"; + if (sdr.m_highFrequency > 300000000) { + band = "UHF"; + } else if (sdr.m_highFrequency > 320000000) { // Technically 30MHz, but many HF Kiwis list up to 32MHz + band = "VHF"; + } + QString label = QString("Kiwi %1").arg(band); + kiwiMapItem.setLabel(new QString(label)); + kiwiMapItem.setLabelAltitudeOffset(4.5); + kiwiMapItem.setAltitudeReference(1); + update(m_map, &kiwiMapItem, "KiwiSDR"); + } +} + +void MapGUI::addSpyServer() +{ + m_spyServerList.getDataPeriodically(); +} + +void MapGUI::spyServerUpdated(const QList& sdrs) +{ + for (const auto& sdr : sdrs) + { + SWGSDRangel::SWGMapItem spyServerMapItem; + + QString address = QString("%1:%2").arg(sdr.m_streamingHost).arg(sdr.m_streamingPort); + spyServerMapItem.setName(new QString(address)); + spyServerMapItem.setLatitude(sdr.m_latitude); + spyServerMapItem.setLongitude(sdr.m_longitude); + spyServerMapItem.setAltitude(0); + spyServerMapItem.setImage(new QString("antennaspyserver.png")); + spyServerMapItem.setImageRotation(0); + QString url = QString("sdrangel-spyserver://%1").arg(address); + QString text = QString("SpyServer\nDescription: %1\nHW: %2\nUsers: %3/%4\nFrequency: %5 - %6\nAntenna: %7\nOnline: %8\nURL: %9") + .arg(sdr.m_generalDescription) + .arg(sdr.m_deviceType) + .arg(sdr.m_currentClientCount) + .arg(sdr.m_maxClients) + .arg(formatFrequency(sdr.m_minimumFrequency)) + .arg(formatFrequency(sdr.m_maximumFrequency)) + .arg(sdr.m_antennaType) + .arg(sdr.m_online ? "Yes" : "No") + .arg(QString("%2").arg(url).arg(address)) + ; + spyServerMapItem.setText(new QString(text)); + spyServerMapItem.setModel(new QString("antenna.glb")); + spyServerMapItem.setFixedPosition(true); + spyServerMapItem.setOrientation(0); + QStringList bands; + if (sdr.m_minimumFrequency < 30000000) { + bands.append("HF"); + } + if ((sdr.m_minimumFrequency < 300000000) && (sdr.m_maximumFrequency > 30000000)) { + bands.append("VHF"); + } + if ((sdr.m_minimumFrequency < 3000000000) && (sdr.m_maximumFrequency > 300000000)) { + bands.append("UHF"); + } + if (sdr.m_maximumFrequency > 3000000000) { + bands.append("SHF"); + } + QString label = QString("SpyServer %1").arg(bands.join(" ")); + spyServerMapItem.setLabel(new QString(label)); + spyServerMapItem.setLabelAltitudeOffset(4.5); + spyServerMapItem.setAltitudeReference(1); + update(m_map, &spyServerMapItem, "SpyServer"); + } +} + // Ionosonde stations void MapGUI::addIonosonde() { @@ -612,6 +786,24 @@ void MapGUI::foF2Updated(const QJsonDocument& document) } } +void MapGUI::pathUpdated(const QString& radarPath, const QString& satellitePath) +{ + m_radarPath = radarPath; + m_satellitePath = satellitePath; + m_mapTileServer->setRadarPath(radarPath); + m_mapTileServer->setSatellitePath(satellitePath); + if (m_settings.m_displayRain || m_settings.m_displayClouds) + { + clearOSMCache(); + applyMap2DSettings(true); + } + if (m_cesium) + { + m_cesium->setLayerSettings("rain", {"path", "show"}, {radarPath, m_settings.m_displayRain}); + m_cesium->setLayerSettings("clouds", {"path", "show"}, {satellitePath, m_settings.m_displayClouds}); + } +} + void MapGUI::addBroadcast() { QFile file(":/map/data/transmitters.csv"); @@ -1033,6 +1225,39 @@ void MapGUI::addAirports() } } +void MapGUI::addWaypoints() +{ + m_waypoints = Waypoints::getWaypoints(); + if (m_waypoints) + { + QHashIterator i(*m_waypoints); + while (i.hasNext()) + { + i.next(); + const Waypoint *waypoint = i.value(); + + SWGSDRangel::SWGMapItem waypointMapItem; + waypointMapItem.setName(new QString(waypoint->m_name)); + waypointMapItem.setLatitude(waypoint->m_latitude); + waypointMapItem.setLongitude(waypoint->m_longitude); + waypointMapItem.setAltitude(0); + waypointMapItem.setImage(new QString("waypoint.png")); + waypointMapItem.setImageRotation(0); + QStringList list; + list.append(QString("Waypoint: %1").arg(waypoint->m_name)); + waypointMapItem.setText(new QString(list.join("\n"))); + //waypointMapItem.setModel(new QString("waypoint.glb")); // No such model currently + waypointMapItem.setFixedPosition(true); + waypointMapItem.setOrientation(0); + waypointMapItem.setLabel(new QString(waypoint->m_name)); + waypointMapItem.setLabelAltitudeOffset(4.5); + waypointMapItem.setAltitude(Units::feetToMetres(25000)); + waypointMapItem.setAltitudeReference(1); + update(m_map, &waypointMapItem, "Waypoints"); + } + } +} + void MapGUI::navAidsUpdated() { addNavAids(); @@ -1048,6 +1273,10 @@ void MapGUI::airportsUpdated() addAirports(); } +void MapGUI::waypointsUpdated() +{ + addWaypoints(); +} void MapGUI::addNavtex() { @@ -1097,6 +1326,139 @@ void MapGUI::blockApplySettings(bool block) m_doApplySettings = !block; } +void MapGUI::nasaGlobalImageryDataUpdated(const QList& dataSets) +{ + m_nasaDataSets = dataSets; + m_nasaDataSetsHash.clear(); + ui->nasaGlobalImageryIdentifier->blockSignals(true); + ui->nasaGlobalImageryIdentifier->clear(); + for (const auto& dataSet : m_nasaDataSets) + { + ui->nasaGlobalImageryIdentifier->addItem(dataSet.m_identifier); + m_nasaDataSetsHash.insert(dataSet.m_identifier, dataSet); + } + ui->nasaGlobalImageryIdentifier->blockSignals(false); + ui->nasaGlobalImageryIdentifier->setCurrentIndex(ui->nasaGlobalImageryIdentifier->findText(m_settings.m_nasaGlobalImageryIdentifier)); +} + +void MapGUI::createNASAGlobalImageryView() +{ + m_nasaWidget = new QWidget(); + m_nasaWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); + + m_legendWidget = new QSvgWidget(); + + QPalette pal = QPalette(); + pal.setColor(QPalette::Window, Qt::white); + m_legendWidget->setAutoFillBackground(true); + m_legendWidget->setPalette(pal); + m_nasaWidget->setAutoFillBackground(true); + m_nasaWidget->setPalette(pal); + + m_descriptionWidget = new QTextEdit(); + m_descriptionWidget->setReadOnly(true); + m_descriptionWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); + + m_overviewWidget = new QTableWidget(NASA_ROWS, 2); + m_overviewWidget->setItem(NASA_TITLE, 0, new QTableWidgetItem("Title")); + m_overviewWidget->setItem(NASA_TITLE, 1, new QTableWidgetItem("")); + m_overviewWidget->setItem(NASA_SUBTITLE, 0, new QTableWidgetItem("Subtitle")); + m_overviewWidget->setItem(NASA_SUBTITLE, 1, new QTableWidgetItem("")); + m_overviewWidget->setItem(NASA_DEFAULT_DATE, 0, new QTableWidgetItem("Default Date")); + m_overviewWidget->setItem(NASA_DEFAULT_DATE, 1, new QTableWidgetItem("")); + m_overviewWidget->setItem(NASA_START_DATE, 0, new QTableWidgetItem("Start Date")); + m_overviewWidget->setItem(NASA_START_DATE, 1, new QTableWidgetItem("")); + m_overviewWidget->setItem(NASA_END_DATE, 0, new QTableWidgetItem("End Date")); + m_overviewWidget->setItem(NASA_END_DATE, 1, new QTableWidgetItem("")); + m_overviewWidget->setItem(NASA_PERIOD, 0, new QTableWidgetItem("Period")); + m_overviewWidget->setItem(NASA_PERIOD, 1, new QTableWidgetItem("")); + m_overviewWidget->setItem(NASA_LAYER_GROUP, 0, new QTableWidgetItem("Group")); + m_overviewWidget->setItem(NASA_LAYER_GROUP, 1, new QTableWidgetItem("")); + m_overviewWidget->horizontalHeader()->setStretchLastSection(true); + m_overviewWidget->horizontalHeader()->hide(); + m_overviewWidget->verticalHeader()->hide(); + m_overviewWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); + m_overviewWidget->setSelectionMode(QAbstractItemView::NoSelection); + + QHBoxLayout *hbox = new QHBoxLayout(); + hbox->addWidget(m_overviewWidget); + hbox->addWidget(m_legendWidget, 0, Qt::AlignHCenter | Qt::AlignTop); + hbox->addWidget(m_descriptionWidget); + hbox->setContentsMargins(0, 0, 0, 0); + + m_nasaWidget->setLayout(hbox); + ui->splitter->addWidget(m_nasaWidget); + + // Limit size of widget, otherwise the splitter makes it bigger than the maps for some unknown reason + m_nasaWidget->setMaximumHeight(m_overviewWidget->sizeHint().height()); + + m_nasaWidget->show(); +} + +void MapGUI::displayNASAMetaData() +{ + if (m_nasaMetaData.m_layers.contains(m_settings.m_nasaGlobalImageryIdentifier)) + { + const NASAGlobalImagery::Layer& layer = m_nasaMetaData.m_layers.value(m_settings.m_nasaGlobalImageryIdentifier); + const NASAGlobalImagery::DataSet& dataSet = m_nasaDataSetsHash.value(m_settings.m_nasaGlobalImageryIdentifier); + + m_overviewWidget->item(NASA_TITLE, 1)->setText(layer.m_title); + m_overviewWidget->item(NASA_SUBTITLE, 1)->setText(layer.m_subtitle); + m_overviewWidget->item(NASA_DEFAULT_DATE, 1)->setText(dataSet.m_defaultDateTime); + m_overviewWidget->item(NASA_START_DATE, 1)->setText(layer.m_startDate.date().toString("yyyy-MM-dd")); + if (layer.m_endDate.isValid()) { + m_overviewWidget->item(NASA_END_DATE, 1)->setText(layer.m_endDate.date().toString("yyyy-MM-dd")); + } else if (layer.m_ongoing) { + m_overviewWidget->item(NASA_END_DATE, 1)->setText("Ongoing"); + } else { + m_overviewWidget->item(NASA_END_DATE, 1)->setText(""); + } + m_overviewWidget->item(NASA_PERIOD, 1)->setText(layer.m_layerPeriod); + m_overviewWidget->item(NASA_LAYER_GROUP, 1)->setText(layer.m_layerGroup); + } + else + { + qDebug() << "MapGUI::displayNASAMetaData: No metadata for " << m_settings.m_nasaGlobalImageryIdentifier; + m_overviewWidget->item(NASA_TITLE, 1)->setText(""); + m_overviewWidget->item(NASA_SUBTITLE, 1)->setText(""); + m_overviewWidget->item(NASA_DEFAULT_DATE, 1)->setText(""); + m_overviewWidget->item(NASA_START_DATE, 1)->setText(""); + m_overviewWidget->item(NASA_END_DATE, 1)->setText(""); + m_overviewWidget->item(NASA_PERIOD, 1)->setText(""); + m_overviewWidget->item(NASA_LAYER_GROUP, 1)->setText(""); + } +} + +void MapGUI::nasaGlobalImageryMetaDataUpdated(const NASAGlobalImagery::MetaData& metaData) +{ + m_nasaMetaData = metaData; + displayNASAMetaData(); +} + +void MapGUI::nasaGlobalImageryLegendAvailable(const QString& url, const QByteArray& data) +{ + (void) url; + + if (m_legendWidget) + { + m_legendWidget->load(data); + if (m_legend && (m_legend->m_height > 0)) + { + m_legendWidget->setFixedSize(m_legend->m_width, m_legend->m_height); + m_nasaWidget->updateGeometry(); + } + } +} + +void MapGUI::nasaGlobalImageryHTMLAvailable(const QString& url, const QByteArray& data) +{ + (void) url; + + if (m_descriptionWidget) { + m_descriptionWidget->setHtml(data); + } +} + QString MapGUI::osmCachePath() { return QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + "/QtLocation/5.8/tiles/osm/sdrangel_map"; @@ -1105,10 +1467,13 @@ QString MapGUI::osmCachePath() void MapGUI::clearOSMCache() { // Delete all cached custom tiles when user changes the URL. Is there a better way to do this? + // Now deletes all cached tiles, to remove overlays QDir dir(osmCachePath()); if (dir.exists()) { - QStringList filenames = dir.entryList({"osm_100-l-8-*.png"}); + // FIXME: Restore this - and use custom URL for overlays!!! + //QStringList filenames = dir.entryList({"osm_100-l-8-*.png"}); // 8=custom URL + QStringList filenames = dir.entryList({"osm_100-l-*"}); for (const auto& filename : filenames) { QFile file(dir.filePath(filename)); @@ -1174,6 +1539,12 @@ void MapGUI::applyMap2DSettings(bool reloadMap) zoom = 10.0; } + // Update API keys in servers + m_templateServer->setThunderforestAPIKey(thunderforestAPIKey()); + m_templateServer->setMaptilerAPIKey(maptilerAPIKey()); + m_mapTileServer->setThunderforestAPIKey(thunderforestAPIKey()); + m_mapTileServer->setMaptilerAPIKey(maptilerAPIKey()); + // Create the map using the specified provider QQmlProperty::write(item, "smoothing", MainCore::instance()->getSettings().getMapSmoothing()); QQmlProperty::write(item, "mapProvider", m_settings.m_mapProvider); @@ -1288,6 +1659,38 @@ bool MapGUI::eventFilter(QObject *obj, QEvent *event) return FeatureGUI::eventFilter(obj, event); } +void MapGUI::orientationChanged(Qt::ScreenOrientation orientation) +{ + (void) orientation; + + // Need a delay before geometry() reflects new orientation + // https://bugreports.qt.io/browse/QTBUG-109127 + QTimer::singleShot(200, [this]() { + displayToolbar(); + }); +} + +void MapGUI::displayToolbar() +{ + // Replace buttons with menu when window gets narrow + bool narrow = this->screen()->availableGeometry().width() < 400; + ui->layersMenu->setVisible(narrow); + bool overlayButtons = !narrow && ((m_settings.m_mapProvider == "osm") || m_settings.m_map3DEnabled); + ui->displayRain->setVisible(overlayButtons); + ui->displayClouds->setVisible(overlayButtons); + ui->displaySeaMarks->setVisible(overlayButtons); + ui->displayRailways->setVisible(overlayButtons); + ui->displayNASAGlobalImagery->setVisible(overlayButtons); + ui->displayMUF->setVisible(!narrow && m_settings.m_map3DEnabled); + ui->displayfoF2->setVisible(!narrow && m_settings.m_map3DEnabled); +} + +void MapGUI::setEnableOverlay() +{ + bool enable = m_settings.m_displayClouds || m_settings.m_displayRain || m_settings.m_displaySeaMarks || m_settings.m_displayRailways || m_settings.m_displayNASAGlobalImagery; + m_templateServer->setEnableOverlay(enable); +} + MapSettings::MapItemSettings *MapGUI::getItemSettings(const QString &group) { if (m_settings.m_itemSettings.contains(group)) { @@ -1388,6 +1791,12 @@ void MapGUI::applyMap3DSettings(bool reloadMap) m_cesium->getDateTime(); m_cesium->showMUF(m_settings.m_displayMUF); m_cesium->showfoF2(m_settings.m_displayfoF2); + m_cesium->showLayer("rain", m_settings.m_displayRain); + m_cesium->showLayer("clouds", m_settings.m_displayClouds); + m_cesium->showLayer("seaMarks", m_settings.m_displaySeaMarks); + m_cesium->showLayer("railways", m_settings.m_displayRailways); + m_cesium->showLayer("nasaGlobalImagery", m_settings.m_displayNASAGlobalImagery); + applyNASAGlobalImagerySettings(); m_objectMapModel.allUpdated(); m_imageMapModel.allUpdated(); m_polygonMapModel.allUpdated(); @@ -1471,6 +1880,19 @@ void MapGUI::init3DMap() m_cesium->showMUF(m_settings.m_displayMUF); m_cesium->showfoF2(m_settings.m_displayfoF2); + + m_cesium->showLayer("rain", m_settings.m_displayRain); + m_cesium->showLayer("clouds", m_settings.m_displayClouds); + m_cesium->showLayer("seaMarks", m_settings.m_displaySeaMarks); + m_cesium->showLayer("railways", m_settings.m_displayRailways); + applyNASAGlobalImagerySettings(); + + if (!m_radarPath.isEmpty()) { + m_cesium->setLayerSettings("rain", {"path", "show"}, {m_radarPath, m_settings.m_displayRain}); + } + if (!m_satellitePath.isEmpty()) { + m_cesium->setLayerSettings("clouds", {"path", "show"}, {m_satellitePath, m_settings.m_displayClouds}); + } #endif } @@ -1483,8 +1905,32 @@ void MapGUI::displaySettings() ui->displayNames->setChecked(m_settings.m_displayNames); ui->displaySelectedGroundTracks->setChecked(m_settings.m_displaySelectedGroundTracks); ui->displayAllGroundTracks->setChecked(m_settings.m_displayAllGroundTracks); + ui->displayRain->setChecked(m_settings.m_displayRain); + m_displayRain->setChecked(m_settings.m_displayRain); + m_mapTileServer->setDisplayRain(m_settings.m_displayRain); + ui->displayClouds->setChecked(m_settings.m_displayClouds); + m_displayClouds->setChecked(m_settings.m_displayClouds); + m_mapTileServer->setDisplayClouds(m_settings.m_displayClouds); + ui->displaySeaMarks->setChecked(m_settings.m_displaySeaMarks); + m_displaySeaMarks->setChecked(m_settings.m_displaySeaMarks); + m_mapTileServer->setDisplaySeaMarks(m_settings.m_displaySeaMarks); + ui->displayRailways->setChecked(m_settings.m_displayRailways); + m_displayRailways->setChecked(m_settings.m_displayRailways); + m_mapTileServer->setDisplayRailways(m_settings.m_displayRailways); + ui->displayNASAGlobalImagery->setChecked(m_settings.m_displayNASAGlobalImagery); + m_displayNASAGlobalImagery->setChecked(m_settings.m_displayNASAGlobalImagery); + ui->nasaGlobalImageryIdentifier->setVisible(m_settings.m_displayNASAGlobalImagery); + ui->nasaGlobalImageryOpacity->setVisible(m_settings.m_displayNASAGlobalImagery); + ui->nasaGlobalImageryOpacityText->setVisible(m_settings.m_displayNASAGlobalImagery); + ui->nasaGlobalImageryOpacity->setValue(m_settings.m_nasaGlobalImageryOpacity); + if (m_nasaWidget) { + m_nasaWidget->setVisible(m_settings.m_displayNASAGlobalImagery); + } + m_mapTileServer->setDisplayNASAGlobalImagery(m_settings.m_displayNASAGlobalImagery); ui->displayMUF->setChecked(m_settings.m_displayMUF); + m_displayMUF->setChecked(m_settings.m_displayMUF); ui->displayfoF2->setChecked(m_settings.m_displayfoF2); + m_displayfoF2->setChecked(m_settings.m_displayfoF2); m_objectMapModel.setDisplayNames(m_settings.m_displayNames); m_objectMapModel.setDisplaySelectedGroundTracks(m_settings.m_displaySelectedGroundTracks); m_objectMapModel.setDisplayAllGroundTracks(m_settings.m_displayAllGroundTracks); @@ -1492,6 +1938,8 @@ void MapGUI::displaySettings() m_imageMapModel.updateItemSettings(m_settings.m_itemSettings); m_polygonMapModel.updateItemSettings(m_settings.m_itemSettings); m_polylineMapModel.updateItemSettings(m_settings.m_itemSettings); + setEnableOverlay(); + displayToolbar(); applyMap2DSettings(true); applyMap3DSettings(true); getRollupContents()->restoreState(m_rollupState); @@ -1575,12 +2023,195 @@ void MapGUI::on_displayAllGroundTracks_clicked(bool checked) m_objectMapModel.setDisplayAllGroundTracks(checked); } +void MapGUI::on_displayRain_clicked(bool checked) +{ + if (this->sender() != ui->displayRain) { + ui->displayRain->setChecked(checked); + } + if (this->sender() != m_displayRain) { + m_displayRain->setChecked(checked); + } + m_settings.m_displayRain = checked; + m_mapTileServer->setDisplayRain(m_settings.m_displayRain); + setEnableOverlay(); + clearOSMCache(); + applyMap2DSettings(true); + if (m_cesium) { + m_cesium->showLayer("rain", m_settings.m_displayRain); + } +} + +void MapGUI::on_displayClouds_clicked(bool checked) +{ + if (this->sender() != ui->displayClouds) { + ui->displayClouds->setChecked(checked); + } + if (this->sender() != m_displayClouds) { + m_displayClouds->setChecked(checked); + } + m_settings.m_displayClouds = checked; + m_mapTileServer->setDisplayClouds(m_settings.m_displayClouds); + setEnableOverlay(); + clearOSMCache(); + applyMap2DSettings(true); + if (m_cesium) { + m_cesium->showLayer("clouds", m_settings.m_displayClouds); + } +} + +void MapGUI::on_displaySeaMarks_clicked(bool checked) +{ + if (this->sender() != ui->displaySeaMarks) { + ui->displaySeaMarks->setChecked(checked); + } + if (this->sender() != m_displaySeaMarks) { + m_displaySeaMarks->setChecked(checked); + } + m_settings.m_displaySeaMarks = checked; + m_mapTileServer->setDisplaySeaMarks(m_settings.m_displaySeaMarks); + setEnableOverlay(); + clearOSMCache(); + applyMap2DSettings(true); + if (m_cesium) { + m_cesium->showLayer("seaMarks", m_settings.m_displaySeaMarks); + } +} + +void MapGUI::on_displayRailways_clicked(bool checked) +{ + if (this->sender() != ui->displayRailways) { + ui->displayRailways->setChecked(checked); + } + if (this->sender() != m_displayRailways) { + m_displayRailways->setChecked(checked); + } + m_settings.m_displayRailways = checked; + m_mapTileServer->setDisplayRailways(m_settings.m_displayRailways); + setEnableOverlay(); + clearOSMCache(); + applyMap2DSettings(true); + if (m_cesium) { + m_cesium->showLayer("railways", m_settings.m_displayRailways); + } +} + +void MapGUI::on_displayNASAGlobalImagery_clicked(bool checked) +{ + if (this->sender() != ui->displayNASAGlobalImagery) { + ui->displayNASAGlobalImagery->setChecked(checked); + } + if (this->sender() != m_displayNASAGlobalImagery) { + m_displayNASAGlobalImagery->setChecked(checked); + } + m_settings.m_displayNASAGlobalImagery = checked; + ui->nasaGlobalImageryIdentifier->setVisible(m_settings.m_displayNASAGlobalImagery); + ui->nasaGlobalImageryOpacity->setVisible(m_settings.m_displayNASAGlobalImagery); + ui->nasaGlobalImageryOpacityText->setVisible(m_settings.m_displayNASAGlobalImagery); + if (m_nasaWidget) { + m_nasaWidget->setVisible(m_settings.m_displayNASAGlobalImagery); + } + m_mapTileServer->setDisplayNASAGlobalImagery(m_settings.m_displayNASAGlobalImagery); + setEnableOverlay(); + clearOSMCache(); + applyMap2DSettings(true); + if (m_cesium) { + m_cesium->showLayer("NASAGlobalImagery", m_settings.m_displayNASAGlobalImagery); + } +} + +void MapGUI::on_nasaGlobalImageryIdentifier_currentIndexChanged(int index) +{ + if ((index >= 0) && (index < m_nasaDataSets.size())) + { + m_settings.m_nasaGlobalImageryIdentifier = m_nasaDataSets[index].m_identifier; + // MODIS_Terra_Aerosol/default/2014-04-09/GoogleMapsCompatible_Level6 + QString date = "default"; // FIXME: Get from 3D map + QString path = QString("%1/default/%2/%3").arg(m_settings.m_nasaGlobalImageryIdentifier).arg(date).arg(m_nasaDataSets[index].m_tileMatrixSet); + m_mapTileServer->setNASAGlobalImageryPath(path); + QString format = m_nasaDataSets[index].m_format; + if (format == "image/jpeg") { + m_mapTileServer->setNASAGlobalImageryFormat("jpeg"); + } else { + m_mapTileServer->setNASAGlobalImageryFormat("png"); + } + setEnableOverlay(); + clearOSMCache(); + applyMap2DSettings(true); + applyNASAGlobalImagerySettings(); + } +} + +void MapGUI::applyNASAGlobalImagerySettings() +{ + int index = ui->nasaGlobalImageryIdentifier->currentIndex(); + + // Update 3D map + if (m_cesium) + { + if ((index >= 0) && (index < m_nasaDataSets.size())) + { + QString format = m_nasaDataSets[index].m_format; + QString extension = (format == "image/jpeg") ? "jpg" : "png"; + // Does cesium want epsg3857 or epsg4326 + //QString url = QString("https://gibs.earthdata.nasa.gov/wmts/epsg4326/best/%1/default/{Time}/{TileMatrixSet}/{TileMatrix}/{TileRow}/{TileCol}.%2").arg(m_settings.m_nasaGlobalImageryIdentifier).arg(extension); + //QString url = QString("https://gibs.earthdata.nasa.gov/wmts/epsg3857/best/%1/default/default/{TileMatrixSet}/{TileMatrix}/{TileRow}/{TileCol}.%2").arg(m_settings.m_nasaGlobalImageryIdentifier).arg(extension); + QString url = QString("https://gibs.earthdata.nasa.gov/wmts/epsg3857/best/%1/default/{Time}/{TileMatrixSet}/{TileMatrix}/{TileRow}/{TileCol}.%2").arg(m_settings.m_nasaGlobalImageryIdentifier).arg(extension); + QString show = m_settings.m_displayNASAGlobalImagery ? "true" : "false"; + + m_cesium->setLayerSettings("NASAGlobalImagery", + {"url", "tileMatrixSet", "format", "show", "opacity", "dates"}, + {url, m_nasaDataSets[index].m_tileMatrixSet, format, m_settings.m_displayNASAGlobalImagery, m_settings.m_nasaGlobalImageryOpacity, m_nasaDataSets[index].m_dates}); + } + } + + // Update NASA table / legend / description + if ((index >= 0) && (m_nasaDataSets[index].m_legends.size() > 0)) + { + m_legend = &m_nasaDataSets[index].m_legends[0]; + m_nasaGlobalImagery.downloadLegend(*m_legend); + m_descriptionWidget->setHtml(""); + if (m_nasaMetaData.m_layers.contains(m_settings.m_nasaGlobalImageryIdentifier)) + { + QString url = m_nasaMetaData.m_layers.value(m_settings.m_nasaGlobalImageryIdentifier).m_descriptionURL; + m_nasaGlobalImagery.downloadHTML(url); + } + displayNASAMetaData(); + m_nasaWidget->setVisible(m_settings.m_displayNASAGlobalImagery); + } + else + { + if (m_nasaWidget) { + m_nasaWidget->setVisible(false); + } + } +} + +void MapGUI::on_nasaGlobalImageryOpacity_valueChanged(int value) +{ + m_settings.m_nasaGlobalImageryOpacity = value; + ui->nasaGlobalImageryOpacityText->setText(QString("%1%").arg(m_settings.m_nasaGlobalImageryOpacity)); + + if (m_cesium) + { + m_cesium->setLayerSettings("NASAGlobalImagery", + {"opacity"}, + {m_settings.m_nasaGlobalImageryOpacity} + ); + } +} + void MapGUI::on_displayMUF_clicked(bool checked) { + if (this->sender() != ui->displayMUF) { + ui->displayMUF->setChecked(checked); + } + if (this->sender() != m_displayMUF) { + m_displayMUF->setChecked(checked); + } m_settings.m_displayMUF = checked; // Only call show if disabling, so we don't get two updates // (as getMUFPeriodically results in a call to showMUF when the data is available) - m_giro->getMUFPeriodically(m_settings.m_displayMUF ? 15 : 0); + m_giro->getMUFPeriodically(m_settings.m_displayMUF ? 15 : 0); if (m_cesium && !m_settings.m_displayMUF) { m_cesium->showMUF(m_settings.m_displayMUF); } @@ -1588,6 +2219,12 @@ void MapGUI::on_displayMUF_clicked(bool checked) void MapGUI::on_displayfoF2_clicked(bool checked) { + if (this->sender() != ui->displayfoF2) { + ui->displayfoF2->setChecked(checked); + } + if (this->sender() != m_displayfoF2) { + m_displayfoF2->setChecked(checked); + } m_settings.m_displayfoF2 = checked; m_giro->getfoF2Periodically(m_settings.m_displayfoF2 ? 15 : 0); if (m_cesium && !m_settings.m_displayfoF2) { @@ -1595,6 +2232,55 @@ void MapGUI::on_displayfoF2_clicked(bool checked) } } +void MapGUI::createLayersMenu() +{ + QMenu *menu = new QMenu(); + + m_displayRain = menu->addAction("Weather radar"); + m_displayRain->setCheckable(true); + m_displayRain->setToolTip("Display weather radar (rain/snow)"); + connect(m_displayRain, &QAction::triggered, this, &MapGUI::on_displayRain_clicked); + + m_displayClouds = menu->addAction("Satellite IR"); + m_displayClouds->setCheckable(true); + m_displayClouds->setToolTip("Display satellite infra-red (clouds)"); + connect(m_displayClouds, &QAction::triggered, this, &MapGUI::on_displayClouds_clicked); + + m_displaySeaMarks = menu->addAction("Sea marks"); + m_displaySeaMarks->setCheckable(true); + m_displaySeaMarks->setToolTip("Display sea marks"); + //QIcon seaMarksIcon; + //seaMarksIcon.addPixmap(QPixmap("://map/icons/anchor.png"), QIcon::Normal, QIcon::On); + //displaySeaMarks->setIcon(seaMarksIcon); + connect(m_displaySeaMarks, &QAction::triggered, this, &MapGUI::on_displaySeaMarks_clicked); + + m_displayRailways = menu->addAction("Railways"); + m_displayRailways->setCheckable(true); + m_displayRailways->setToolTip("Display railways"); + connect(m_displayRailways, &QAction::triggered, this, &MapGUI::on_displayRailways_clicked); + + m_displayNASAGlobalImagery = menu->addAction("NASA Global Imagery"); + m_displayNASAGlobalImagery->setCheckable(true); + m_displayNASAGlobalImagery->setToolTip("Display NASA Global Imagery"); + connect(m_displayNASAGlobalImagery, &QAction::triggered, this, &MapGUI::on_displayNASAGlobalImagery_clicked); + + m_displayMUF = menu->addAction("MUF"); + m_displayMUF->setCheckable(true); + m_displayMUF->setToolTip("Display Maximum Usable Frequency contours"); + connect(m_displayMUF, &QAction::triggered, this, &MapGUI::on_displayMUF_clicked); + + m_displayfoF2 = menu->addAction("foF2"); + m_displayfoF2->setCheckable(true); + m_displayfoF2->setToolTip("Display F2 layer critical frequency contours"); + connect(m_displayfoF2, &QAction::triggered, this, &MapGUI::on_displayfoF2_clicked); + + ui->layersMenu->setMenu(menu); +} + +void MapGUI::on_layersMenu_clicked() +{ +} + void MapGUI::on_find_returnPressed() { find(ui->find->text().trimmed()); @@ -1781,6 +2467,7 @@ void MapGUI::on_displaySettings_clicked() if (dialog.m_osmURLChanged) { clearOSMCache(); } + displayToolbar(); applyMap2DSettings(dialog.m_map2DSettingsChanged); applyMap3DSettings(dialog.m_map3DSettingsChanged); m_settingsKeys.append(dialog.m_settingsKeysChanged); @@ -1858,6 +2545,11 @@ void MapGUI::receivedCesiumEvent(const QJsonObject &obj) m_map->setMapDateTime(mapDateTime, systemDateTime, canAnimate && shouldAnimate ? multiplier : 0.0); } } + else if (event == "link") + { + QString url = obj.value("url").toString(); + linkClicked(url); + } } else { @@ -1865,6 +2557,150 @@ void MapGUI::receivedCesiumEvent(const QJsonObject &obj) } } +// Handle link clicked in 2D Map Text box or 3D map infobox. +void MapGUI::linkClicked(const QString& url) +{ + if (url.startsWith("sdrangel-kiwisdr://")) + { + QString kiwiURL = url.mid(19); + openKiwiSDR(kiwiURL); + } + else if (url.startsWith("sdrangel-spyserver://")) + { + QString spyServerURL = url.mid(21); + openSpyServer(spyServerURL); + } +} + +// Open a KiwiSDR RX device +void MapGUI::openKiwiSDR(const QString& url) +{ + // Create DeviceSet + MainCore *mainCore = MainCore::instance(); + unsigned int deviceSetIndex = mainCore->getDeviceSets().size(); + MainCore::MsgAddDeviceSet *msg = MainCore::MsgAddDeviceSet::create(0); + mainCore->getMainMessageQueue()->push(msg); + + // Switch to KiwiSDR + int nbSamplingDevices = DeviceEnumerator::instance()->getNbRxSamplingDevices(); + bool found = false; + QString hwType = "KiwiSDR"; + for (int i = 0; i < nbSamplingDevices; i++) + { + const PluginInterface::SamplingDevice *samplingDevice; + + samplingDevice = DeviceEnumerator::instance()->getRxSamplingDevice(i); + + if (!hwType.isEmpty() && (hwType != samplingDevice->hardwareId)) { + continue; + } + + int direction = 0; + MainCore::MsgSetDevice *msg = MainCore::MsgSetDevice::create(deviceSetIndex, i, direction); + mainCore->getMainMessageQueue()->push(msg); + found = true; + break; + } + if (!found) + { + qCritical() << "MapGUI::openKiwiSDR: Failed to find KiwiSDR"; + return; + } + + // Wait until device is created - is there a better way? + DeviceSet *deviceSet = nullptr; + do + { + QTime dieTime = QTime::currentTime().addMSecs(100); + while (QTime::currentTime() < dieTime) { + QCoreApplication::processEvents(QEventLoop::AllEvents, 100); + } + if (mainCore->getDeviceSets().size() > deviceSetIndex) + { + deviceSet = mainCore->getDeviceSets()[deviceSetIndex]; + } + } + while (!deviceSet); + + // Move to same workspace + //getWorkspaceIndex(); + + // Set address setting + QStringList deviceSettingsKeys = {"serverAddress"}; + SWGSDRangel::SWGDeviceSettings response; + response.init(); + SWGSDRangel::SWGKiwiSDRSettings *deviceSettings = response.getKiwiSdrSettings(); + deviceSettings->setServerAddress(new QString(url)); + QString errorMessage; + deviceSet->m_deviceAPI->getSampleSource()->webapiSettingsPutPatch(false, deviceSettingsKeys, response, errorMessage); +} + +// Open a RemoteTCPInput device to use for SpyServer +void MapGUI::openSpyServer(const QString& url) +{ + // Create DeviceSet + MainCore *mainCore = MainCore::instance(); + unsigned int deviceSetIndex = mainCore->getDeviceSets().size(); + MainCore::MsgAddDeviceSet *msg = MainCore::MsgAddDeviceSet::create(0); + mainCore->getMainMessageQueue()->push(msg); + + // Switch to RemoteTCPInput + int nbSamplingDevices = DeviceEnumerator::instance()->getNbRxSamplingDevices(); + bool found = false; + QString hwType = "RemoteTCPInput"; + for (int i = 0; i < nbSamplingDevices; i++) + { + const PluginInterface::SamplingDevice *samplingDevice; + + samplingDevice = DeviceEnumerator::instance()->getRxSamplingDevice(i); + + if (!hwType.isEmpty() && (hwType != samplingDevice->hardwareId)) { + continue; + } + + int direction = 0; + MainCore::MsgSetDevice *msg = MainCore::MsgSetDevice::create(deviceSetIndex, i, direction); + mainCore->getMainMessageQueue()->push(msg); + found = true; + break; + } + if (!found) + { + qCritical() << "MapGUI::openSpyServer: Failed to find RemoteTCPInput"; + return; + } + + // Wait until device is created - is there a better way? + DeviceSet *deviceSet = nullptr; + do + { + QTime dieTime = QTime::currentTime().addMSecs(100); + while (QTime::currentTime() < dieTime) { + QCoreApplication::processEvents(QEventLoop::AllEvents, 100); + } + if (mainCore->getDeviceSets().size() > deviceSetIndex) + { + deviceSet = mainCore->getDeviceSets()[deviceSetIndex]; + } + } + while (!deviceSet); + + // Move to same workspace + //getWorkspaceIndex(); + + // Set address/port setting + QStringList address = url.split(":"); + QStringList deviceSettingsKeys = {"dataAddress", "dataPort", "protocol"}; + SWGSDRangel::SWGDeviceSettings response; + response.init(); + SWGSDRangel::SWGRemoteTCPInputSettings *deviceSettings = response.getRemoteTcpInputSettings(); + deviceSettings->setDataAddress(new QString(address[0])); + deviceSettings->setDataPort(address[1].toInt()); + deviceSettings->setProtocol(new QString("Spy Server")); + QString errorMessage; + deviceSet->m_deviceAPI->getSampleSource()->webapiSettingsPutPatch(false, deviceSettingsKeys, response, errorMessage); +} + #ifdef QT_WEBENGINE_FOUND void MapGUI::fullScreenRequested(QWebEngineFullScreenRequest fullScreenRequest) { @@ -1940,6 +2776,13 @@ void MapGUI::makeUIConnections() QObject::connect(ui->displayNames, &ButtonSwitch::clicked, this, &MapGUI::on_displayNames_clicked); QObject::connect(ui->displayAllGroundTracks, &ButtonSwitch::clicked, this, &MapGUI::on_displayAllGroundTracks_clicked); QObject::connect(ui->displaySelectedGroundTracks, &ButtonSwitch::clicked, this, &MapGUI::on_displaySelectedGroundTracks_clicked); + QObject::connect(ui->displayRain, &ButtonSwitch::clicked, this, &MapGUI::on_displayRain_clicked); + QObject::connect(ui->displayClouds, &ButtonSwitch::clicked, this, &MapGUI::on_displayClouds_clicked); + QObject::connect(ui->displaySeaMarks, &ButtonSwitch::clicked, this, &MapGUI::on_displaySeaMarks_clicked); + QObject::connect(ui->displayRailways, &ButtonSwitch::clicked, this, &MapGUI::on_displayRailways_clicked); + QObject::connect(ui->displayNASAGlobalImagery, &ButtonSwitch::clicked, this, &MapGUI::on_displayNASAGlobalImagery_clicked); + QObject::connect(ui->nasaGlobalImageryIdentifier, qOverload(&QComboBox::currentIndexChanged), this, &MapGUI::on_nasaGlobalImageryIdentifier_currentIndexChanged); + QObject::connect(ui->nasaGlobalImageryOpacity, qOverload(&QDial::valueChanged), this, &MapGUI::on_nasaGlobalImageryOpacity_valueChanged); QObject::connect(ui->displayMUF, &ButtonSwitch::clicked, this, &MapGUI::on_displayMUF_clicked); QObject::connect(ui->displayfoF2, &ButtonSwitch::clicked, this, &MapGUI::on_displayfoF2_clicked); QObject::connect(ui->find, &QLineEdit::returnPressed, this, &MapGUI::on_find_returnPressed); diff --git a/plugins/feature/map/mapgui.h b/plugins/feature/map/mapgui.h index 145c4621b..b541f4318 100644 --- a/plugins/feature/map/mapgui.h +++ b/plugins/feature/map/mapgui.h @@ -1,5 +1,5 @@ /////////////////////////////////////////////////////////////////////////////////// -// Copyright (C) 2021-2023 Jon Beniston, M7RCE // +// Copyright (C) 2021-2024 Jon Beniston, M7RCE // // Copyright (C) 2022 Edouard Griffiths, F4EXB // // // // This program is free software; you can redistribute it and/or modify // @@ -22,6 +22,7 @@ #include #include #include +#include #include #ifdef QT_WEBENGINE_FOUND #include @@ -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 m_settingsKeys; RollupState m_rollupState; bool m_doApplySettings; - QList 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 m_ionosondeStations; QSharedPointer> m_navAids; QSharedPointer> m_airspaces; QSharedPointer> m_airportInfo; + QSharedPointer> 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 m_nasaDataSets; + QHash 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 m_radioTimeTransmitters; static const QList 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& 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& sdrs); + void spyServerUpdated(const QList& sdrs); + void linkClicked(const QString& url); }; diff --git a/plugins/feature/map/mapguinowebengine.ui b/plugins/feature/map/mapguinowebengine.ui index 93377861c..dc455fc85 100644 --- a/plugins/feature/map/mapguinowebengine.ui +++ b/plugins/feature/map/mapguinowebengine.ui @@ -171,6 +171,86 @@ + + + + Display weather radar (rain/snow) + + + ^ + + + + :/map/icons/precipitation.png:/map/icons/precipitation.png + + + true + + + true + + + + + + + Display sea marks + + + ^ + + + + :/map/icons/anchor.png:/map/icons/anchor.png + + + true + + + true + + + + + + + Display satellite infra-red (clouds) + + + ^ + + + + :/map/icons/cloud.png:/map/icons/cloud.png + + + true + + + true + + + + + + + Display railways + + + ^ + + + + :/map/icons/railway.png:/map/icons/railway.png + + + true + + + true + + + diff --git a/plugins/feature/map/mapguiwebengine.ui b/plugins/feature/map/mapguiwebengine.ui index b9a3d4898..3bfb11781 100644 --- a/plugins/feature/map/mapguiwebengine.ui +++ b/plugins/feature/map/mapguiwebengine.ui @@ -6,8 +6,8 @@ 0 0 - 481 - 507 + 1282 + 293 @@ -18,7 +18,7 @@ - 462 + 0 0 @@ -39,13 +39,13 @@ 0 0 - 480 + 1221 41 - 480 + 300 0 @@ -165,10 +165,104 @@ + + + + + + + + :/map/icons/layers.png:/map/icons/layers.png + + + QToolButton::InstantPopup + + + + + + + Display satellite infra-red (clouds) + + + ^ + + + + :/map/icons/cloud.png:/map/icons/cloud.png + + + true + + + true + + + + + + + Display weather radar (rain/snow) + + + ^ + + + + :/map/icons/precipitation.png:/map/icons/precipitation.png + + + true + + + true + + + + + + + Display sea marks + + + ^ + + + + :/map/icons/anchor.png:/map/icons/anchor.png + + + true + + + true + + + + + + + Display railways + + + ^ + + + + :/map/icons/railway.png:/map/icons/railway.png + + + true + + + true + + + - Display MUF contours + Display MUF (Maximum Usable Frequency) contours (3D only) ^ @@ -188,7 +282,7 @@ - Display foF2 contours + Display foF2 (F2 layer critical frequency) contours (3D only) ^ @@ -205,6 +299,71 @@ + + + + Display NASA GIBS data + + + ^ + + + + :/map/icons/earthsat.png:/map/icons/earthsat.png + + + true + + + true + + + + + + + + 200 + 0 + + + + NASA GIBS data + + + + + + + + 24 + 24 + + + + NASA GIBS image opacity (3D only) + + + 100 + + + 100 + + + + + + + + 34 + 0 + + + + 100% + + + @@ -392,12 +551,6 @@ QWidget
QtQuickWidgets/QQuickWidget
- - QWebEngineView - QWidget -
QtWebEngineWidgets/QWebEngineView
- 1 -
ButtonSwitch QToolButton @@ -409,6 +562,12 @@
gui/rollupcontents.h
1
+ + QWebEngineView + QWidget +
QtWebEngineWidgets/QWebEngineView
+ 1 +
find diff --git a/plugins/feature/map/mapmodel.cpp b/plugins/feature/map/mapmodel.cpp index 58f107546..5d56b978e 100644 --- a/plugins/feature/map/mapmodel.cpp +++ b/plugins/feature/map/mapmodel.cpp @@ -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(); diff --git a/plugins/feature/map/mapmodel.h b/plugins/feature/map/mapmodel.h index 2a038f39c..46ad4e570 100644 --- a/plugins/feature/map/mapmodel.h +++ b/plugins/feature/map/mapmodel.h @@ -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 &roles = QVector()) 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; diff --git a/plugins/feature/map/mapsettings.cpp b/plugins/feature/map/mapsettings.cpp index c280d95c7..577d7e769 100644 --- a/plugins/feature/map/mapsettings.cpp +++ b/plugins/feature/map/mapsettings.cpp @@ -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()); } - diff --git a/plugins/feature/map/mapsettings.h b/plugins/feature/map/mapsettings.h index 36a2e2067..07bad8e1d 100644 --- a/plugins/feature/map/mapsettings.h +++ b/plugins/feature/map/mapsettings.h @@ -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 // -// Copyright (C) 2020-2023 Jon Beniston, M7RCE // +// Copyright (C) 2020-2024 Jon Beniston, M7RCE // // // // This program is free software; you can redistribute it and/or modify // // it under the terms of the GNU General Public License as published by // @@ -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 *); diff --git a/plugins/feature/map/mapsettingsdialog.cpp b/plugins/feature/map/mapsettingsdialog.cpp index 2aec4c43b..062e27db3 100644 --- a/plugins/feature/map/mapsettingsdialog.cpp +++ b/plugins/feature/map/mapsettingsdialog.cpp @@ -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; + } +} + diff --git a/plugins/feature/map/mapsettingsdialog.h b/plugins/feature/map/mapsettingsdialog.h index b966a5586..f6fe3af64 100644 --- a/plugins/feature/map/mapsettingsdialog.h +++ b/plugins/feature/map/mapsettingsdialog.h @@ -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; diff --git a/plugins/feature/map/mapsettingsdialog.ui b/plugins/feature/map/mapsettingsdialog.ui index 53f2c962c..472c22f7f 100644 --- a/plugins/feature/map/mapsettingsdialog.ui +++ b/plugins/feature/map/mapsettingsdialog.ui @@ -83,11 +83,6 @@ ESRI
- - - Mapbox - - MapboxGL @@ -337,6 +332,20 @@
+ + + + Download aviation waypoints (3MB) + + + + + + + :/map/icons/waypoints.png:/map/icons/waypoints.png + + + diff --git a/plugins/feature/map/maptileserver.cpp b/plugins/feature/map/maptileserver.cpp new file mode 100644 index 000000000..932442f77 --- /dev/null +++ b/plugins/feature/map/maptileserver.cpp @@ -0,0 +1,18 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "maptileserver.h" diff --git a/plugins/feature/map/maptileserver.h b/plugins/feature/map/maptileserver.h new file mode 100644 index 000000000..736f0bd89 --- /dev/null +++ b/plugins/feature/map/maptileserver.h @@ -0,0 +1,431 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021, 2023 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_MAPTILESERVER_H_ +#define INCLUDE_MAPTILESERVER_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 m_urls; + QHash m_images; + QString m_format; + }; + QList m_tileJobs; + QHash 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" + "Not found\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 + { + 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 &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(sender()); + QList errorsThatCanBeIgnored; + errorsThatCanBeIgnored << QSslError(QSslError::UnableToGetLocalIssuerCertificate, error.certificate()); + reply->ignoreSslErrors(errorsThatCanBeIgnored); + } + #endif + } + } + +}; + +#endif diff --git a/plugins/feature/map/osmtemplateserver.h b/plugins/feature/map/osmtemplateserver.h index 73430acb5..a871002d0 100644 --- a/plugins/feature/map/osmtemplateserver.h +++ b/plugins/feature/map/osmtemplateserver.h @@ -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\" : \"OpenStreetMap\",\ - \"DataCopyRight\" : \"\"\ - }"); + {\ + \"UrlTemplate\" : \"%1\",\ + \"ImageFormat\" : \"png\",\ + \"QImageFormat\" : \"Indexed8\",\ + \"ID\" : \"wmf-intl-1x\",\ + \"MaximumZoomLevel\" : 19,\ + \"MapCopyRight\" : \"OpenStreetMap\",\ + \"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\" : \"Maptiler\",\ \"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\" : \"CartoDB\",\ \"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\" : \"Thunderforest\",\ \"DataCopyRight\" : \"OpenStreetMap contributors\"\ - }").arg(mapUrl[idx]).arg(m_thunderforestAPIKey).arg(mapId[idx]).arg(hiresURL); + }").arg(url).arg(mapId[idx]); } } QTextStream os(socket); diff --git a/plugins/feature/map/readme.md b/plugins/feature/map/readme.md index 584223cc8..3969ec24f 100644 --- a/plugins/feature/map/readme.md +++ b/plugins/feature/map/readme.md @@ -9,9 +9,9 @@ On top of this, it can plot data from other plugins, such as: * Aircraft from the ADS-B Demodulator, * Ships from the AIS Demodulator, * Satellites from the Satellite Tracker, -* Weather imagery from APT Demodulator, +* Satellite 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,16 +25,22 @@ As well as internet data sources: * Radio time transmitters, * GRAVES radar, * Ionosonde station data, -* Navtex transmitters. -* VLF transmitters. +* Navtex transmitters, +* VLF transmitters, +* KiwiSDRs, +* Spy Servers, +* 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) ![3D Map feature](../../../doc/img/Map_plugin_apt.png) -3D Models are not included with SDRangel. They must be downloaded by pressing the Download 3D Models button in the Display Settings dialog (13). +3D Models are not included with SDRangel. They must be downloaded by pressing the Download 3D Models button in the Display Settings dialog (20).

Interface

@@ -90,45 +96,104 @@ When clicked, opens the Radio Time Transmitters dialog. ![Radio Time transmitters dialog](../../../doc/img/Map_plugin_radiotime_dialog.png) -

7: Display MUF Contours

+

7: Display Satellite Infrared

+ +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. + +![Satellite IR](../../../doc/img/Map_plugin_clouds.png) + +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. + +This is only supported on 2D raster maps and the 3D map. + +

8: Display Weather Radar

+ +When checked, weather radar measurements are downloaded from 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. + +![Weather Radar](../../../doc/img/Map_plugin_weather_radar.png) + +This is only supported on 2D raster maps and the 3D map. + +

9: Display Sea Marks

+ +When checked, sea marks are overlaid on the maps. + +![Sea Marks](../../../doc/img/Map_plugin_seamarks.png) + +![Sea Marks Legend](../../../doc/img/Map_plugin_seamarks_legend.png) + +This is only supported on 2D raster maps and the 3D map. + +

10: Display Railways

+ +When checked, railway routes are overlaid on the maps. + +![Railways](../../../doc/img/Map_plugin_railways.png) + +![Railway Legend](../../../doc/img/Map_plugin_railway_legend.png) + +This is only supported on 2D raster maps and the 3D map. + +

11: Display MUF Contours

When checked, contours will be downloaded and displayed on the 3D map, showing the MUF (Maximum Usable Frequency) for a 3000km path that reflects off the ionosphere. The contours will be updated every 15 minutes. The latest contour data will always be displayed, irrespective of the time set on the 3D Map. -

8: Display coF2 Contours

+![MUF contours](../../../doc/img/Map_plugin_muf.png) + +

12: Display coF2 Contours

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

8: Display Names

+

13: Display NASA GIBS Data

+ +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). + +![NASA GIBS](../../../doc/img/Map_plugin_GIBS.png) + +This is only supported on 2D raster maps and the 3D map. + +

14: NASA GIBS Data

+ +Selects which data from NASA GIBS to overlay on the maps. + +

15: NASA GIBS Opacity

+ +Sets the opacity used for the NASA GIBS overlay image overlay on the 3D map. Lower values make the image more transparent. + +

16: Display Names

When checked, names of objects are displayed in a bubble next to each object. -

9: Display tracks for selected object

+

17: Display tracks for selected object

When checked, displays the track (taken or predicted) for the selected object. -

10: Display tracks for all objects

+

18: Display tracks for all objects

When checked, displays the track (taken or predicted) for the all objects. -

11: Delete

+

19: Delete

When clicked, all items will be deleted from the map. -

12: Display settings

+

20: Display settings

When clicked, opens the Map Display Settings dialog: -![Map Display Settings Dialog](../../../doc/img/Map_plugin_display_settings.png) - -The top half of the dialog allows customization of how objects from different SDRangel -plugins are displayed on the 2D and 3D maps. This includes: - -* Whether images are displayed on the 2D map and whether 3D models are displayed on the 2D map. -* Whether labels are displayed giving the name of the object. -* Whether taken and predicted tracks are displayed and in which colour. -* How the image or 3D model is scaled as the zoom level changes. +![Map Display Settings Dialog Maps Tab](../../../doc/img/Map_plugin_display_settings.png) For the 2D map, the settings include: @@ -139,13 +204,28 @@ For the 2D map, the settings include: For the 3D map, the settings include: +* Whether the 3D map is displayed. * The terrain provider, which provides elevation data. For a "flat" globe, terrain can be set to Ellipsoid for the WGS-84 ellipsoid. * The buildings provider, which provides 3D building models. This can be set to None if no buildings are desired. * Whether the globe and models are lit from the direction of the Sun or the camera. * The camera reference frame. For ECEF (Earth Centered Earth Fixed), the camera rotates with the globe. For ECI (Earth Centred Inertial) the camera is fixed in space and the globe will rotate under it. -* API keys, required to access maps from different providers. +The "Download 3D Models" button will download the 3D models of aircraft, ships and satellites that are required for the 3D map. +These are not included with the SDRangel distribution, so must be downloaded. It is recommeded to restart SDRangel after downloading the models. + +![Map Display Settings Dialog Items Tab](../../../doc/img/Map_plugin_display_settings_items.png) + +The Map Items tab customization of how objects from different SDRangel plugins and the Internet are displayed on the 2D and 3D maps. This includes: + +* Whether images are displayed on the 2D map and whether 3D models are displayed on the 3D map. +* Whether labels are displayed giving the name of the object. +* Whether taken and predicted tracks are displayed and in which colour. +* How the image or 3D model is scaled as the zoom level changes. + +![Map Display Settings Dialog API Keys Tab](../../../doc/img/Map_plugin_display_settings_apikeys.png) + +API keys are required to access maps from different providers. Free API keys are available by signing up for an accounts with: @@ -156,9 +236,6 @@ Free API keys are available by signing up for an accounts with: If API keys are not specified, a default key will be used, but this may not work if too many users use it. -The "Download 3D Models" button will download the 3D models of aircraft, ships and satellites that are required for the 3D map. -These are not included with the SDRangel distribution, so must be downloaded. -

Map

The map feature displays a 2D and a 3D map overlaid with objects reported by other SDRangel channels and features, as well as beacon locations. @@ -177,6 +254,14 @@ The 2D map will only display the last reported positions for objects. The 3D map, however, has a timeline that allows replaying how objects have moved over time. To the right of the timeline is the fullscreen toggle button, which allows the 3D map to be displayed fullscreen. +

SDRs

+ +The map can display KiwiSDRs and Spy Servers that are publically accessible via the internet. A URL is displayed in the info box. +Clicking on the URL will open a new KiwiSDR or RemoteTCPInput device which will connect to the corresponding SDR. +Before connecting, you should check the whether the number of users is below the maximum. Server data is updated every 2 minutes. + +![SDRs](../../../doc/img/Map_plugin_SDRs.png) +

Ionosonde Stations

When Ionosonde Stations are displayed, data is downloaded and displayed every 2 minutes. The data includes: @@ -205,8 +290,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 +312,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" }}' diff --git a/plugins/feature/map/webserver.cpp b/plugins/feature/map/webserver.cpp index 474056fff..546697e36 100644 --- a/plugins/feature/map/webserver.cpp +++ b/plugins/feature/map/webserver.cpp @@ -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") diff --git a/plugins/feature/skymap/CMakeLists.txt b/plugins/feature/skymap/CMakeLists.txt index 5d83d879f..2466ff69a 100644 --- a/plugins/feature/skymap/CMakeLists.txt +++ b/plugins/feature/skymap/CMakeLists.txt @@ -80,12 +80,6 @@ target_link_libraries(${TARGET_NAME} install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER}) -if(WIN32) - # Run deployqt for QtQuick etc - include(DeployQt) - windeployqt(${TARGET_NAME} ${SDRANGEL_BINARY_BIN_DIR} ${PROJECT_SOURCE_DIR}/skymap) -endif() - # Install debug symbols if (WIN32) install(FILES $ CONFIGURATIONS Debug RelWithDebInfo DESTINATION ${INSTALL_FOLDER} ) diff --git a/plugins/feature/skymap/webserver.cpp b/plugins/feature/skymap/webserver.cpp index 1adb1b966..23dc85e7b 100644 --- a/plugins/feature/skymap/webserver.cpp +++ b/plugins/feature/skymap/webserver.cpp @@ -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\nAccess-Control-Allow-Origin \"*\"\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 diff --git a/plugins/feature/skymap/wtml.cpp b/plugins/feature/skymap/wtml.cpp index c7210e2bb..d0c940903 100644 --- a/plugins/feature/skymap/wtml.cpp +++ b/plugins/feature/skymap/wtml.cpp @@ -21,11 +21,23 @@ #include #include #include +#include WTML::WTML() { m_networkManager = new QNetworkAccessManager(); QObject::connect(m_networkManager, &QNetworkAccessManager::finished, this, &WTML::handleReply); + + QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation); + QDir writeableDir(locations[0]); + if (!writeableDir.mkpath(QStringLiteral("cache") + QDir::separator() + QStringLiteral("wtml"))) { + qDebug() << "Failed to create cache/wtml"; + } + + m_cache = new QNetworkDiskCache(); + m_cache->setCacheDirectory(locations[0] + QDir::separator() + QStringLiteral("cache") + QDir::separator() + QStringLiteral("wtml")); + m_cache->setMaximumCacheSize(100000000); + m_networkManager->setCache(m_cache); } WTML::~WTML() diff --git a/plugins/feature/skymap/wtml.h b/plugins/feature/skymap/wtml.h index 6e3db15fb..ba06c0596 100644 --- a/plugins/feature/skymap/wtml.h +++ b/plugins/feature/skymap/wtml.h @@ -22,6 +22,7 @@ class QNetworkAccessManager; class QNetworkReply; +class QNetworkDiskCache; // World Wide Telescope WTML files containing imageset catalogs class WTML : public QObject @@ -48,6 +49,7 @@ signals: private: QNetworkAccessManager *m_networkManager; + QNetworkDiskCache *m_cache; }; diff --git a/plugins/samplesource/remotetcpinput/CMakeLists.txt b/plugins/samplesource/remotetcpinput/CMakeLists.txt index 39bd1d6f6..9603cb95a 100644 --- a/plugins/samplesource/remotetcpinput/CMakeLists.txt +++ b/plugins/samplesource/remotetcpinput/CMakeLists.txt @@ -14,6 +14,7 @@ set(remotetcpinput_HEADERS remotetcpinputsettings.h remotetcpinputwebapiadapter.h remotetcpinputplugin.h + spyserver.h ) include_directories( diff --git a/plugins/samplesource/remotetcpinput/readme.md b/plugins/samplesource/remotetcpinput/readme.md index c8c013428..9158dfd40 100644 --- a/plugins/samplesource/remotetcpinput/readme.md +++ b/plugins/samplesource/remotetcpinput/readme.md @@ -2,7 +2,8 @@

Introduction

-This input sample source plugin gets its I/Q samples over the network via a TCP/IP connection from a server such as rtl_tcp, rsp_tcp or SDRangel's [Remote TCP Channel Sink](../../channelrx/remotetcpsink/readme.md) plugin. +This input sample source plugin gets its I/Q samples over the network via a TCP/IP connection from a server such as +rtl_tcp, rsp_tcp, Spy Server or SDRangel's [Remote TCP Channel Sink](../../channelrx/remotetcpsink/readme.md) plugin.

Interface

@@ -92,7 +93,7 @@ When unchecked, the channel sample rate can be set to any value. Specifies number of bits per I/Q sample transmitted via TCP/IP. -When the protocol is RTL0, only 8-bits are supported. SDRA protocol supports 8, 16, 24 and 32-bit samples. +When the protocol is RTL0, only 8-bits are supported. SDRA and Spy Server protocol supports 8, 16, 24 and 32-bit samples.

19: Server IP address

@@ -102,7 +103,11 @@ IP address or hostname of the server that is running SDRangel's Remote TCP Sink TCP port on the server to connect to. -

21: Connection settings

+

21: Protocol

+ +Selects protocol to use. Set to SDRangel for rtl_tcp, rsp_tcp or SDRangel's own protocol. Alternative, Spy Server can be selected to connect to Spy Servers. + +

23: Connection settings

Determines which settings are used when connecting. @@ -110,31 +115,32 @@ When checked, settings in the RemoteTCPInput GUI are written to the remote devic When unchecked, if the remote server is using the SDRA protocol, the RemoteTCPInput GUI will be updated with the current settings from the remote device. If the remote server is using the RTL0 protocol, the GUI will not be updated, which may mean the two are inconsistent. -

22: Pre-fill

+

24: Pre-fill

Determines how many seconds of I/Q samples are buffered locally from the remote device, before being processed in SDRangel. More buffering can handle more network congestion and other network problems, without gaps in the output, but increases the latency in changes to remote device settings. -

23: Input buffer gauge

+

25: Input buffer gauge

Shows how much data is in the input buffer. Typically this will be just under the pre-fill setting. If it becomes empty, the plugin will pause outputting of data until the buffer is refilled to the pre-fill level. If the buffer repeatedly runs empty, this suggests you do not have enough network bandwidth for the current combination of channel sample rate and sample bit depth. Reducing these to lower values may be required for uninterrupted data. -

24: Output buffer gauge

+

26: Output buffer gauge

Shows how much data is in the output buffer. This should typically be empty. If not empty, this suggests your CPU can't keep up with the amount of data being received. -

25: Device status

+

27: Device status

Shows the type of remote device that has been connected to. -

26: Protocol status

+

28: Protocol status

-Shows the protocol being used by the remote server. This will be RTL0 or SDRA. +Shows the protocol being used by the remote server. This will be RTL0, SDRA or Spy Server. rtl_tcp and rsp_tcp always use the RTL0 protocol. SDRangel's Remote TCP Sink plugin can use RTL0 or SDRA. RTL0 is limited to sending 8-bit data, doesn't support decimation and does not send the current device settings on connection. +Spy Server supports decimation and gain, but no other settings. diff --git a/plugins/samplesource/remotetcpinput/remotetcpinput.cpp b/plugins/samplesource/remotetcpinput/remotetcpinput.cpp index 0cae3da6a..730313a09 100644 --- a/plugins/samplesource/remotetcpinput/remotetcpinput.cpp +++ b/plugins/samplesource/remotetcpinput/remotetcpinput.cpp @@ -394,6 +394,9 @@ void RemoteTCPInput::webapiUpdateDeviceSettings( if (deviceSettingsKeys.contains("preFill")) { settings.m_preFill = response.getRemoteTcpInputSettings()->getPreFill() != 0; } + if (deviceSettingsKeys.contains("protocol")) { + settings.m_protocol = *response.getRemoteTcpInputSettings()->getProtocol(); + } if (deviceSettingsKeys.contains("useReverseAPI")) { settings.m_useReverseAPI = response.getRemoteTcpInputSettings()->getUseReverseApi() != 0; } @@ -430,6 +433,7 @@ void RemoteTCPInput::webapiFormatDeviceSettings(SWGSDRangel::SWGDeviceSettings& response.getRemoteTcpInputSettings()->setDataPort(settings.m_dataPort); response.getRemoteTcpInputSettings()->setOverrideRemoteSettings(settings.m_overrideRemoteSettings ? 1 : 0); response.getRemoteTcpInputSettings()->setPreFill(settings.m_preFill ? 1 : 0); + response.getRemoteTcpInputSettings()->setProtocol(new QString(settings.m_protocol)); response.getRemoteTcpInputSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); diff --git a/plugins/samplesource/remotetcpinput/remotetcpinputgui.cpp b/plugins/samplesource/remotetcpinput/remotetcpinputgui.cpp index 2880c362a..3d1623cc9 100644 --- a/plugins/samplesource/remotetcpinput/remotetcpinputgui.cpp +++ b/plugins/samplesource/remotetcpinput/remotetcpinputgui.cpp @@ -1,5 +1,5 @@ /////////////////////////////////////////////////////////////////////////////////// -// Copyright (C) 2022-2023 Jon Beniston, M7RCE // +// Copyright (C) 2022-2024 Jon Beniston, M7RCE // // Copyright (C) 2022 Edouard Griffiths, F4EXB // // // // This program is free software; you can redistribute it and/or modify // @@ -49,7 +49,9 @@ RemoteTCPInputGui::RemoteTCPInputGui(DeviceUISet *deviceUISet, QWidget* parent) m_forceSettings(true), m_deviceGains(nullptr), m_remoteDevice(RemoteTCPProtocol::RTLSDR_R820T), - m_connectionError(false) + m_connectionError(false), + m_spyServerGainRange("Gain", 0, 41, 1, ""), + m_spyServerGains({m_spyServerGainRange}, false, false) { m_deviceUISet = deviceUISet; setAttribute(Qt::WA_DeleteOnClose, true); @@ -223,23 +225,27 @@ bool RemoteTCPInputGui::handleMessage(const Message& message) device = devices.value(m_remoteDevice); } ui->device->setText(QString("Device: %1").arg(device)); - ui->protocol->setText(QString("Protocol: %1").arg(report.getProtocol())); + ui->detectedProtocol->setText(QString("Protocol: %1").arg(report.getProtocol())); // Update GUI so we only show widgets available for the protocol in use bool sdra = report.getProtocol() == "SDRA"; - if (sdra && (ui->sampleBits->count() != 4)) + bool spyServer = report.getProtocol() == "Spy Server"; + if (spyServer) { + m_spyServerGains.m_gains[0].m_max = report.getMaxGain(); + } + if ((sdra || spyServer) && (ui->sampleBits->count() < 4)) { ui->sampleBits->addItem("16"); ui->sampleBits->addItem("24"); ui->sampleBits->addItem("32"); } - else if (!sdra && (ui->sampleBits->count() != 1)) + else if (!(sdra || spyServer) && (ui->sampleBits->count() != 1)) { while (ui->sampleBits->count() > 1) { ui->sampleBits->removeItem(ui->sampleBits->count() - 1); } } - if (sdra && (ui->decim->count() != 7)) + if ((sdra || spyServer) && (ui->decim->count() != 7)) { ui->decim->addItem("2"); ui->decim->addItem("4"); @@ -248,19 +254,24 @@ bool RemoteTCPInputGui::handleMessage(const Message& message) ui->decim->addItem("32"); ui->decim->addItem("64"); } - else if (!sdra && (ui->decim->count() != 1)) + else if (!(sdra || spyServer) && (ui->decim->count() != 1)) { while (ui->decim->count() > 1) { ui->decim->removeItem(ui->decim->count() - 1); } } - if (!sdra) { + if (!sdra) + { ui->deltaFrequency->setValue(0); ui->channelGain->setValue(0); ui->decimation->setChecked(true); } + ui->deltaFrequencyLabel->setEnabled(sdra); ui->deltaFrequency->setEnabled(sdra); + ui->deltaUnits->setEnabled(sdra); + ui->channelGainLabel->setEnabled(sdra); ui->channelGain->setEnabled(sdra); + ui->channelGainText->setEnabled(sdra); ui->decimation->setEnabled(sdra); if (sdra) { ui->centerFrequency->setValueRange(9, 0, 999999999); // Should add transverter control to protocol in the future @@ -271,7 +282,7 @@ bool RemoteTCPInputGui::handleMessage(const Message& message) // Set sample rate range if (m_sampleRateRanges.contains(m_remoteDevice)) { - const SampleRateRange *range = m_sampleRateRanges.value(m_remoteDevice); + const SampleRateRange *range = m_sampleRateRanges.value(m_remoteDevice); ui->devSampleRate->setValueRange(8, range->m_min, range->m_max); } else if (m_sampleRateLists.contains(m_remoteDevice)) @@ -284,6 +295,18 @@ bool RemoteTCPInputGui::handleMessage(const Message& message) { ui->devSampleRate->setValueRange(8, 0, 99999999); } + ui->devSampleRateLabel->setEnabled(!spyServer); + ui->devSampleRate->setEnabled(!spyServer); + ui->devSampleRateUnits->setEnabled(!spyServer); + ui->agc->setEnabled(!spyServer); + ui->rfBWLabel->setEnabled(!spyServer); + ui->rfBW->setEnabled(!spyServer); + ui->rfBWUnits->setEnabled(!spyServer); + ui->dcOffset->setEnabled(!spyServer); + ui->iqImbalance->setEnabled(!spyServer); + ui->ppm->setEnabled(!spyServer); + ui->ppmLabel->setEnabled(!spyServer); + ui->ppmText->setEnabled(!spyServer); displayGains(); return true; @@ -368,7 +391,9 @@ void RemoteTCPInputGui::displaySettings() ui->deviceRateText->setText(tr("%1k").arg(m_settings.m_channelSampleRate / 1000.0)); ui->decimation->setChecked(!m_settings.m_channelDecimation); ui->channelSampleRate->setEnabled(m_settings.m_channelDecimation); - ui->sampleBits->setCurrentIndex(m_settings.m_sampleBits/8-1); + ui->channelSampleRateLabel->setEnabled(m_settings.m_channelDecimation); + ui->channelSampleRateUnit->setEnabled(m_settings.m_channelDecimation); + ui->sampleBits->setCurrentText(QString::number(m_settings.m_sampleBits)); ui->dataPort->setText(tr("%1").arg(m_settings.m_dataPort)); ui->dataAddress->blockSignals(true); @@ -385,6 +410,10 @@ void RemoteTCPInputGui::displaySettings() ui->preFill->setValue((int)(m_settings.m_preFill * 10.0)); ui->preFillText->setText(QString("%1s").arg(m_settings.m_preFill, 0, 'f', 2)); + int idx = ui->protocol->findText(m_settings.m_protocol); + if (idx > 0) { + ui->protocol->setCurrentIndex(idx); + } displayGains(); blockApplySettings(false); @@ -526,7 +555,11 @@ void RemoteTCPInputGui::displayGains() QLabel *gainTexts[3] = {ui->gain1Text, ui->gain2Text, ui->gain3Text}; QWidget *gainLine[2] = {ui->gainLine1, ui->gainLine2}; - m_deviceGains = m_gains.value(m_remoteDevice); + if (m_settings.m_protocol == "Spy Server") { + m_deviceGains = &m_spyServerGains; + } else { + m_deviceGains = m_gains.value(m_remoteDevice); + } if (m_deviceGains) { ui->agc->setVisible(m_deviceGains->m_agc); @@ -746,12 +779,16 @@ void RemoteTCPInputGui::on_decimation_toggled(bool checked) ui->channelSampleRate->setValue(m_settings.m_channelSampleRate); } ui->channelSampleRate->setEnabled(!checked); + ui->channelSampleRateLabel->setEnabled(!checked); + ui->channelSampleRateUnit->setEnabled(!checked); sendSettings(); } void RemoteTCPInputGui::on_sampleBits_currentIndexChanged(int index) { - m_settings.m_sampleBits = 8 * (index + 1); + (void) index; + + m_settings.m_sampleBits = ui->sampleBits->currentText().toInt(); m_settingsKeys.append("sampleBits"); sendSettings(); } @@ -808,6 +845,16 @@ void RemoteTCPInputGui::on_preFill_valueChanged(int value) sendSettings(); } +void RemoteTCPInputGui::on_protocol_currentIndexChanged(int index) +{ + (void) index; + + m_settings.m_protocol = ui->protocol->currentText(); + m_settingsKeys.append("protocol"); + sendSettings(); + displayGains(); +} + void RemoteTCPInputGui::updateHardware() { if (m_doApplySettings) @@ -902,4 +949,5 @@ void RemoteTCPInputGui::makeUIConnections() QObject::connect(ui->dataPort, &QLineEdit::editingFinished, this, &RemoteTCPInputGui::on_dataPort_editingFinished); QObject::connect(ui->overrideRemoteSettings, &ButtonSwitch::toggled, this, &RemoteTCPInputGui::on_overrideRemoteSettings_toggled); QObject::connect(ui->preFill, &QDial::valueChanged, this, &RemoteTCPInputGui::on_preFill_valueChanged); + QObject::connect(ui->protocol, QOverload::of(&QComboBox::currentIndexChanged), this, &RemoteTCPInputGui::on_protocol_currentIndexChanged); } diff --git a/plugins/samplesource/remotetcpinput/remotetcpinputgui.h b/plugins/samplesource/remotetcpinput/remotetcpinputgui.h index 3de89c95d..9afcec66a 100644 --- a/plugins/samplesource/remotetcpinput/remotetcpinputgui.h +++ b/plugins/samplesource/remotetcpinput/remotetcpinputgui.h @@ -1,5 +1,5 @@ /////////////////////////////////////////////////////////////////////////////////// -// Copyright (C) 2022-2023 Jon Beniston, M7RCE // +// Copyright (C) 2022-2024 Jon Beniston, M7RCE // // Copyright (C) 2022 Edouard Griffiths, F4EXB // // // // This program is free software; you can redistribute it and/or modify // @@ -122,6 +122,9 @@ private: RemoteTCPProtocol::Device m_remoteDevice; // Remote device reported when connecting bool m_connectionError; + DeviceGains::GainRange m_spyServerGainRange; + DeviceGains m_spyServerGains; + static const DeviceGains::GainRange m_rtlSDR34kGainRange; static const DeviceGains m_rtlSDRe4kGains; static const DeviceGains::GainRange m_rtlSDRR820GainRange; @@ -208,6 +211,7 @@ private slots: void on_dataPort_editingFinished(); void on_overrideRemoteSettings_toggled(bool checked); void on_preFill_valueChanged(int value); + void on_protocol_currentIndexChanged(int index); void updateHardware(); void updateStatus(); void openDeviceSettingsDialog(const QPoint& p); diff --git a/plugins/samplesource/remotetcpinput/remotetcpinputgui.ui b/plugins/samplesource/remotetcpinput/remotetcpinputgui.ui index 6d8d881fd..ec526650d 100644 --- a/plugins/samplesource/remotetcpinput/remotetcpinputgui.ui +++ b/plugins/samplesource/remotetcpinput/remotetcpinputgui.ui @@ -742,7 +742,7 @@ Use to ensure full dynamic range of 8-bit data is used. - + Ch SR @@ -777,7 +777,7 @@ Use to ensure full dynamic range of 8-bit data is used. - + S/s @@ -957,6 +957,29 @@ Use to ensure full dynamic range of 8-bit data is used.
+ + + + + 75 + 0 + + + + Protocol to use. SDRangel (inc. rtl_tcp) or Spy Server + + + + SDRangel + + + + + Spy Server + + + + @@ -1163,7 +1186,7 @@ This should typically be empty. If full, your CPU cannot keep up and data will b - + @@ -1174,6 +1197,11 @@ This should typically be empty. If full, your CPU cannot keep up and data will b + + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
ValueDialZ QWidget @@ -1186,11 +1214,6 @@ This should typically be empty. If full, your CPU cannot keep up and data will b
gui/valuedial.h
1
- - ButtonSwitch - QToolButton -
gui/buttonswitch.h
-
startStop diff --git a/plugins/samplesource/remotetcpinput/remotetcpinputsettings.cpp b/plugins/samplesource/remotetcpinput/remotetcpinputsettings.cpp index 2b693ac8e..ad5291794 100644 --- a/plugins/samplesource/remotetcpinput/remotetcpinputsettings.cpp +++ b/plugins/samplesource/remotetcpinput/remotetcpinputsettings.cpp @@ -1,5 +1,5 @@ /////////////////////////////////////////////////////////////////////////////////// -// Copyright (C) 2022-2023 Jon Beniston, M7RCE // +// Copyright (C) 2022-2024 Jon Beniston, M7RCE // // Copyright (C) 2022 Edouard Griffiths, F4EXB // // // // This program is free software; you can redistribute it and/or modify // @@ -48,6 +48,7 @@ void RemoteTCPInputSettings::resetToDefaults() m_dataPort = 1234; m_overrideRemoteSettings = true; m_preFill = 1.0f; + m_protocol = "SDRangel"; m_useReverseAPI = false; m_reverseAPIAddress = "127.0.0.1"; m_reverseAPIPort = 8888; @@ -81,6 +82,7 @@ QByteArray RemoteTCPInputSettings::serialize() const s.writeU32(22, m_reverseAPIPort); s.writeU32(23, m_reverseAPIDeviceIndex); s.writeList(24, m_addressList); + s.writeString(25, m_protocol); for (int i = 0; i < m_maxGains; i++) { s.writeS32(30+i, m_gain[i]); @@ -136,6 +138,7 @@ bool RemoteTCPInputSettings::deserialize(const QByteArray& data) m_reverseAPIDeviceIndex = uintval > 99 ? 99 : uintval; d.readList(24, &m_addressList); + d.readString(25, &m_protocol, "SDRangel"); for (int i = 0; i < m_maxGains; i++) { d.readS32(30+i, &m_gain[i], 0); @@ -224,6 +227,9 @@ void RemoteTCPInputSettings::applySettings(const QStringList& settingsKeys, cons if (settingsKeys.contains("addressList")) { m_addressList = settings.m_addressList; } + if (settingsKeys.contains("protocol")) { + m_protocol = settings.m_protocol; + } for (int i = 0; i < m_maxGains; i++) { @@ -309,6 +315,9 @@ QString RemoteTCPInputSettings::getDebugString(const QStringList& settingsKeys, if (settingsKeys.contains("addressList") || force) { ostr << " m_addressList: " << m_addressList.join(",").toStdString(); } + if (settingsKeys.contains("protocol") || force) { + ostr << " m_protocol: " << m_protocol.toStdString(); + } for (int i = 0; i < m_maxGains; i++) { diff --git a/plugins/samplesource/remotetcpinput/remotetcpinputsettings.h b/plugins/samplesource/remotetcpinput/remotetcpinputsettings.h index 1694d9b31..a95dc9773 100644 --- a/plugins/samplesource/remotetcpinput/remotetcpinputsettings.h +++ b/plugins/samplesource/remotetcpinput/remotetcpinputsettings.h @@ -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 // -// Copyright (C) 2020, 2022-2023 Jon Beniston, M7RCE // +// Copyright (C) 2020, 2022-2024 Jon Beniston, M7RCE // // // // This program is free software; you can redistribute it and/or modify // // it under the terms of the GNU General Public License as published by // @@ -53,6 +53,7 @@ struct RemoteTCPInputSettings uint16_t m_reverseAPIPort; uint16_t m_reverseAPIDeviceIndex; QStringList m_addressList; // List of dataAddresses that have been used in the past + QString m_protocol; // "SDRangel" or "Spy Server" RemoteTCPInputSettings(); void resetToDefaults(); diff --git a/plugins/samplesource/remotetcpinput/remotetcpinputtcphandler.cpp b/plugins/samplesource/remotetcpinput/remotetcpinputtcphandler.cpp index c44118d2d..906d62bdf 100644 --- a/plugins/samplesource/remotetcpinput/remotetcpinputtcphandler.cpp +++ b/plugins/samplesource/remotetcpinput/remotetcpinputtcphandler.cpp @@ -1,5 +1,5 @@ /////////////////////////////////////////////////////////////////////////////////// -// Copyright (C) 2022-2023 Jon Beniston, M7RCE // +// Copyright (C) 2022-2024 Jon Beniston, M7RCE // // Copyright (C) 2022 Edouard Griffiths, F4EXB // // Copyright (C) 2022 Jiří Pinkava // // // @@ -49,7 +49,7 @@ RemoteTCPInputTCPHandler::RemoteTCPInputTCPHandler(SampleSinkFifo *sampleFifo, D { m_sampleFifo->setSize(5000000); // Start with large FIFO, to avoid having to resize m_tcpBuf = new char[m_sampleFifo->size()*2*4]; - m_timer.setInterval(125); + m_timer.setInterval(50); // Previously 125, but this results in an obviously slow spectrum refresh rate connect(&m_reconnectTimer, SIGNAL(timeout()), this, SLOT(reconnect())); m_reconnectTimer.setSingleShot(true); } @@ -165,11 +165,21 @@ void RemoteTCPInputTCPHandler::cleanup() // E.g. sample rate or bit depth void RemoteTCPInputTCPHandler::clearBuffer() { - if (m_dataSocket) + if (m_dataSocket && m_readMetaData) { - m_dataSocket->flush(); - m_dataSocket->readAll(); - m_fillBuffer = true; + if (m_spyServer) + { + // Can't just flush buffer, otherwise we'll lose header sync + // Read and throw away any available data + processSpyServerData(m_dataSocket->bytesAvailable(), true); + m_fillBuffer = true; + } + else + { + m_dataSocket->flush(); + m_dataSocket->readAll(); + m_fillBuffer = true; + } } } @@ -389,6 +399,60 @@ void RemoteTCPInputTCPHandler::setSampleBitDepth(int sampleBits) } } +void RemoteTCPInputTCPHandler::spyServerConnect() +{ + QMutexLocker mutexLocker(&m_mutex); + + quint8 request[8+4+9]; + SpyServerProtocol::encodeUInt32(&request[0], 0); + SpyServerProtocol::encodeUInt32(&request[4], 4+9); + SpyServerProtocol::encodeUInt32(&request[8], SpyServerProtocol::ProtocolID); + memcpy(&request[8+4], "SDRangel", 9); + if (m_dataSocket) { + m_dataSocket->write((char*)request, sizeof(request)); + } +} + +void RemoteTCPInputTCPHandler::spyServerSet(int setting, int value) +{ + QMutexLocker mutexLocker(&m_mutex); + + quint8 request[8+8]; + SpyServerProtocol::encodeUInt32(&request[0], 2); + SpyServerProtocol::encodeUInt32(&request[4], 8); + SpyServerProtocol::encodeUInt32(&request[8], setting); + SpyServerProtocol::encodeUInt32(&request[12], value); + if (m_dataSocket) { + m_dataSocket->write((char*)request, sizeof(request)); + } +} + +void RemoteTCPInputTCPHandler::spyServerSetIQFormat(int sampleBits) +{ + quint32 format; + + if (sampleBits == 8) { + format = 1; + } else if (sampleBits == 16) { + format = 2; + } else if (sampleBits == 24) { + format = 3; + } else if (sampleBits == 32) { + format = 4; // This is float + } else { + qDebug() << "RemoteTCPInputTCPHandler::spyServerSetIQFormat: Unsupported value" << sampleBits; + format = 1; + } + spyServerSet(SpyServerProtocol::setIQFormat, format); +} + +void RemoteTCPInputTCPHandler::spyServerSetStreamIQ() +{ + spyServerSetIQFormat(m_settings.m_sampleBits); + spyServerSet(SpyServerProtocol::setStreamingMode, 1); // Stream IQ only + spyServerSet(SpyServerProtocol::setStreamingEnabled, 1); // Enable streaming +} + void RemoteTCPInputTCPHandler::applySettings(const RemoteTCPInputSettings& settings, const QList& settingsKeys, bool force) { qDebug() << "RemoteTCPInputTCPHandler::applySettings: " @@ -396,86 +460,124 @@ void RemoteTCPInputTCPHandler::applySettings(const RemoteTCPInputSettings& setti << settings.getDebugString(settingsKeys, force); QMutexLocker mutexLocker(&m_mutex); - if (settingsKeys.contains("centerFrequency") || force) { - setCenterFrequency(settings.m_centerFrequency); - } - if (settingsKeys.contains("loPpmCorrection") || force) { - setFreqCorrection(settings.m_loPpmCorrection); - } - if (settingsKeys.contains("dcBlock") || force) { - if (m_sdra) { - setDCOffsetRemoval(settings.m_dcBlock); - } - } - if (settingsKeys.contains("iqCorrection") || force) { - if (m_sdra) { - setIQCorrection(settings.m_iqCorrection); - } - } - if (settingsKeys.contains("biasTee") || force) { - setBiasTee(settings.m_biasTee); - } - if (settingsKeys.contains("directSampling") || force) { - setDirectSampling(settings.m_directSampling); - } - if (settingsKeys.contains("log2Decim") || force) { - if (m_sdra) { - setDecimation(settings.m_log2Decim); - } - } - if (settingsKeys.contains("devSampleRate") || force) { - setSampleRate(settings.m_devSampleRate); - } - if (settingsKeys.contains("agc") || force) { - setAGC(settings.m_agc); - } - if (force) { - setTunerAGC(1); // The SDRangel RTLSDR driver always has tuner gain as manual - } - if (settingsKeys.contains("gain[0]") || force) { - setTunerGain(settings.m_gain[0]); - } - for (int i = 1; i < 3; i++) + if (m_spyServer) { - if (settingsKeys.contains(QString("gain[%1]").arg(i)) || force) { - setIFGain(i, settings.m_gain[i]); + if (settingsKeys.contains("centerFrequency") || force) { + spyServerSet(SpyServerProtocol::setCenterFrequency, settings.m_centerFrequency); } - } - if (settingsKeys.contains("rfBW") || force) { - setBandwidth(settings.m_rfBW); - } - if (settingsKeys.contains("inputFrequencyOffset") || force) { - if (m_sdra) { - setChannelFreqOffset(settings.m_inputFrequencyOffset); - } - } - if (settingsKeys.contains("channelGain") || force) { - if (m_sdra) { - setChannelGain(settings.m_channelGain); - } - } - if ((settings.m_channelSampleRate != m_settings.m_channelSampleRate) || force) - { - // Resize FIFO to give us 1 second - if ((settingsKeys.contains("channelSampleRate") || force) && (settings.m_channelSampleRate > (qint32)m_sampleFifo->size())) + if ((settings.m_channelSampleRate != m_settings.m_channelSampleRate) || force) { - qDebug() << "RemoteTCPInputTCPHandler::applySettings: Resizing sample FIFO from " << m_sampleFifo->size() << "to" << settings.m_channelSampleRate; - m_sampleFifo->setSize(settings.m_channelSampleRate); - delete[] m_tcpBuf; - m_tcpBuf = new char[m_sampleFifo->size()*2*4]; - m_fillBuffer = true; // So we reprime FIFO + // Resize FIFO to give us 1 second + if ((settingsKeys.contains("channelSampleRate") || force) && (settings.m_channelSampleRate > (qint32)m_sampleFifo->size())) + { + qDebug() << "RemoteTCPInputTCPHandler::applySettings: Resizing sample FIFO from " << m_sampleFifo->size() << "to" << settings.m_channelSampleRate; + m_sampleFifo->setSize(settings.m_channelSampleRate); + delete[] m_tcpBuf; + m_tcpBuf = new char[m_sampleFifo->size()*2*4]; + m_fillBuffer = true; // So we reprime FIFO + } + // Protocol only seems to allow changing decimation + //spyServerSet(SpyServerProtocol::???, settings.m_channelSampleRate); + clearBuffer(); } - if (m_sdra) { - setChannelSampleRate(settings.m_channelSampleRate); + if (settingsKeys.contains("sampleBits") || force) + { + spyServerSetIQFormat(settings.m_sampleBits); + clearBuffer(); + } + if (settingsKeys.contains("log2Decim") || force) + { + spyServerSet(SpyServerProtocol::setIQDecimation, settings.m_log2Decim); + clearBuffer(); + } + if (settingsKeys.contains("gain[0]") || force) + { + spyServerSet(SpyServerProtocol::setGain, settings.m_gain[0] / 10); // Convert 10ths dB to index } - clearBuffer(); } - if (settingsKeys.contains("sampleBits") || force) + else { - if (m_sdra) { - setSampleBitDepth(settings.m_sampleBits); + if (settingsKeys.contains("centerFrequency") || force) { + setCenterFrequency(settings.m_centerFrequency); + } + if (settingsKeys.contains("loPpmCorrection") || force) { + setFreqCorrection(settings.m_loPpmCorrection); + } + if (settingsKeys.contains("dcBlock") || force) { + if (m_sdra) { + setDCOffsetRemoval(settings.m_dcBlock); + } + } + if (settingsKeys.contains("iqCorrection") || force) { + if (m_sdra) { + setIQCorrection(settings.m_iqCorrection); + } + } + if (settingsKeys.contains("biasTee") || force) { + setBiasTee(settings.m_biasTee); + } + if (settingsKeys.contains("directSampling") || force) { + setDirectSampling(settings.m_directSampling); + } + if (settingsKeys.contains("log2Decim") || force) { + if (m_sdra) { + setDecimation(settings.m_log2Decim); + } + } + if (settingsKeys.contains("devSampleRate") || force) { + setSampleRate(settings.m_devSampleRate); + } + if (settingsKeys.contains("agc") || force) { + setAGC(settings.m_agc); + } + if (force) { + setTunerAGC(1); // The SDRangel RTLSDR driver always has tuner gain as manual + } + if (settingsKeys.contains("gain[0]") || force) { + setTunerGain(settings.m_gain[0]); + } + for (int i = 1; i < 3; i++) + { + if (settingsKeys.contains(QString("gain[%1]").arg(i)) || force) { + setIFGain(i, settings.m_gain[i]); + } + } + if (settingsKeys.contains("rfBW") || force) { + setBandwidth(settings.m_rfBW); + } + if (settingsKeys.contains("inputFrequencyOffset") || force) { + if (m_sdra) { + setChannelFreqOffset(settings.m_inputFrequencyOffset); + } + } + if (settingsKeys.contains("channelGain") || force) { + if (m_sdra) { + setChannelGain(settings.m_channelGain); + } + } + if ((settings.m_channelSampleRate != m_settings.m_channelSampleRate) || force) + { + // Resize FIFO to give us 1 second + if ((settingsKeys.contains("channelSampleRate") || force) && (settings.m_channelSampleRate > (qint32)m_sampleFifo->size())) + { + qDebug() << "RemoteTCPInputTCPHandler::applySettings: Resizing sample FIFO from " << m_sampleFifo->size() << "to" << settings.m_channelSampleRate; + m_sampleFifo->setSize(settings.m_channelSampleRate); + delete[] m_tcpBuf; + m_tcpBuf = new char[m_sampleFifo->size()*2*4]; + m_fillBuffer = true; // So we reprime FIFO + } + if (m_sdra) { + setChannelSampleRate(settings.m_channelSampleRate); + } + clearBuffer(); + } + if (settingsKeys.contains("sampleBits") || force) + { + if (m_sdra) { + setSampleBitDepth(settings.m_sampleBits); + } + clearBuffer(); } - clearBuffer(); } // Don't use force, as disconnect can cause rtl_tcp to quit @@ -501,6 +603,12 @@ void RemoteTCPInputTCPHandler::connected() MsgReportConnection *msg = MsgReportConnection::create(true); m_messageQueueToGUI->push(msg); } + m_spyServer = m_settings.m_protocol == "Spy Server"; + m_state = HEADER; + m_sdra = false; + if (m_spyServer) { + spyServerConnect(); + } } void RemoteTCPInputTCPHandler::reconnect() @@ -542,116 +650,366 @@ void RemoteTCPInputTCPHandler::dataReadyRead() { QMutexLocker mutexLocker(&m_mutex); - if (!m_readMetaData) + if (!m_readMetaData && !m_spyServer) { - quint8 metaData[RemoteTCPProtocol::m_sdraMetaDataSize]; - if (m_dataSocket->bytesAvailable() >= (qint64)sizeof(metaData)) + processMetaData(); + } + else if (!m_readMetaData && m_spyServer) + { + processSpyServerMetaData(); + } +} + +void RemoteTCPInputTCPHandler::processMetaData() +{ + quint8 metaData[RemoteTCPProtocol::m_sdraMetaDataSize]; + if (m_dataSocket->bytesAvailable() >= (qint64)sizeof(metaData)) + { + qint64 bytesRead = m_dataSocket->read((char *)&metaData[0], 4); + if (bytesRead == 4) { - qint64 bytesRead = m_dataSocket->read((char *)&metaData[0], 4); - if (bytesRead == 4) + // Read first 4 bytes which indicate which protocol is in use + // RTL0 or SDRA + char protochars[5]; + memcpy(protochars, metaData, 4); + protochars[4] = '\0'; + QString protocol(protochars); + + if (protocol == "RTL0") { - // Read first 4 bytes which indicate which protocol is in use - // RTL0 or SDRA - char protochars[5]; - memcpy(protochars, metaData, 4); - protochars[4] = '\0'; - QString protocol(protochars); + m_sdra = false; + m_spyServer = false; + bytesRead = m_dataSocket->read((char *)&metaData[4], RemoteTCPProtocol::m_rtl0MetaDataSize-4); - if (protocol == "RTL0") + m_device = (RemoteTCPProtocol::Device)RemoteTCPProtocol::extractUInt32(&metaData[4]); + if (m_messageQueueToGUI) { + m_messageQueueToGUI->push(MsgReportRemoteDevice::create(m_device, protocol)); + } + if (m_settings.m_sampleBits != 8) { - m_sdra = false; - bytesRead = m_dataSocket->read((char *)&metaData[4], RemoteTCPProtocol::m_rtl0MetaDataSize-4); - - RemoteTCPProtocol::Device tuner = (RemoteTCPProtocol::Device)RemoteTCPProtocol::extractUInt32(&metaData[4]); - if (m_messageQueueToGUI) { - m_messageQueueToGUI->push(MsgReportRemoteDevice::create(tuner, protocol)); + RemoteTCPInputSettings& settings = m_settings; + settings.m_sampleBits = 8; + QList settingsKeys{"sampleBits"}; + if (m_messageQueueToInput) { + m_messageQueueToInput->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys)); } - if (m_settings.m_sampleBits != 8) - { - RemoteTCPInputSettings& settings = m_settings; - settings.m_sampleBits = 8; - QList settingsKeys{"sampleBits"}; - if (m_messageQueueToInput) { - m_messageQueueToInput->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys)); - } - if (m_messageQueueToGUI) { - m_messageQueueToGUI->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys)); - } + if (m_messageQueueToGUI) { + m_messageQueueToGUI->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys)); } } - else if (protocol == "SDRA") - { - m_sdra = true; - bytesRead = m_dataSocket->read((char *)&metaData[4], RemoteTCPProtocol::m_sdraMetaDataSize-4); + } + else if (protocol == "SDRA") + { + m_sdra = true; + m_spyServer = false; + bytesRead = m_dataSocket->read((char *)&metaData[4], RemoteTCPProtocol::m_sdraMetaDataSize-4); - RemoteTCPProtocol::Device device = (RemoteTCPProtocol::Device)RemoteTCPProtocol::extractUInt32(&metaData[4]); - if (m_messageQueueToGUI) { - m_messageQueueToGUI->push(MsgReportRemoteDevice::create(device, protocol)); - } - if (!m_settings.m_overrideRemoteSettings) + m_device = (RemoteTCPProtocol::Device)RemoteTCPProtocol::extractUInt32(&metaData[4]); + if (m_messageQueueToGUI) { + m_messageQueueToGUI->push(MsgReportRemoteDevice::create(m_device, protocol)); + } + if (!m_settings.m_overrideRemoteSettings) + { + // Update local settings to match remote + RemoteTCPInputSettings& settings = m_settings; + QList settingsKeys; + settings.m_centerFrequency = RemoteTCPProtocol::extractUInt64(&metaData[8]); + settingsKeys.append("centerFrequency"); + settings.m_loPpmCorrection = RemoteTCPProtocol::extractUInt32(&metaData[16]); + settingsKeys.append("loPpmCorrection"); + quint32 flags = RemoteTCPProtocol::extractUInt32(&metaData[20]); + settings.m_biasTee = flags & 1; + settingsKeys.append("biasTee"); + settings.m_directSampling = (flags >> 1) & 1; + settingsKeys.append("directSampling"); + settings.m_agc = (flags >> 2) & 1; + settingsKeys.append("agc"); + settings.m_dcBlock = (flags >> 3) & 1; + settingsKeys.append("dcBlock"); + settings.m_iqCorrection = (flags >> 4) & 1; + settingsKeys.append("iqCorrection"); + settings.m_devSampleRate = RemoteTCPProtocol::extractUInt32(&metaData[24]); + settingsKeys.append("devSampleRate"); + settings.m_log2Decim = RemoteTCPProtocol::extractUInt32(&metaData[28]); + settingsKeys.append("log2Decim"); + settings.m_gain[0] = RemoteTCPProtocol::extractInt16(&metaData[32]); + settings.m_gain[1] = RemoteTCPProtocol::extractInt16(&metaData[34]); + settings.m_gain[2] = RemoteTCPProtocol::extractInt16(&metaData[36]); + settingsKeys.append("gain[0]"); + settingsKeys.append("gain[1]"); + settingsKeys.append("gain[2]"); + settings.m_rfBW = RemoteTCPProtocol::extractUInt32(&metaData[40]); + settingsKeys.append("rfBW"); + settings.m_inputFrequencyOffset = RemoteTCPProtocol::extractUInt32(&metaData[44]); + settingsKeys.append("inputFrequencyOffset"); + settings.m_channelGain = RemoteTCPProtocol::extractUInt32(&metaData[48]); + settingsKeys.append("channelGain"); + settings.m_channelSampleRate = RemoteTCPProtocol::extractUInt32(&metaData[52]); + settingsKeys.append("channelSampleRate"); + settings.m_sampleBits = RemoteTCPProtocol::extractUInt32(&metaData[56]); + settingsKeys.append("sampleBits"); + if (settings.m_channelSampleRate != (settings.m_devSampleRate >> settings.m_log2Decim)) { - // Update local settings to match remote - RemoteTCPInputSettings& settings = m_settings; - QList settingsKeys; - settings.m_centerFrequency = RemoteTCPProtocol::extractUInt64(&metaData[8]); - settingsKeys.append("centerFrequency"); - settings.m_loPpmCorrection = RemoteTCPProtocol::extractUInt32(&metaData[16]); - settingsKeys.append("loPpmCorrection"); - quint32 flags = RemoteTCPProtocol::extractUInt32(&metaData[20]); - settings.m_biasTee = flags & 1; - settingsKeys.append("biasTee"); - settings.m_directSampling = (flags >> 1) & 1; - settingsKeys.append("directSampling"); - settings.m_agc = (flags >> 2) & 1; - settingsKeys.append("agc"); - settings.m_dcBlock = (flags >> 3) & 1; - settingsKeys.append("dcBlock"); - settings.m_iqCorrection = (flags >> 4) & 1; - settingsKeys.append("iqCorrection"); - settings.m_devSampleRate = RemoteTCPProtocol::extractUInt32(&metaData[24]); - settingsKeys.append("devSampleRate"); - settings.m_log2Decim = RemoteTCPProtocol::extractUInt32(&metaData[28]); - settingsKeys.append("log2Decim"); - settings.m_gain[0] = RemoteTCPProtocol::extractInt16(&metaData[32]); - settings.m_gain[1] = RemoteTCPProtocol::extractInt16(&metaData[34]); - settings.m_gain[2] = RemoteTCPProtocol::extractInt16(&metaData[36]); - settingsKeys.append("gain[0]"); - settingsKeys.append("gain[1]"); - settingsKeys.append("gain[2]"); - settings.m_rfBW = RemoteTCPProtocol::extractUInt32(&metaData[40]); - settingsKeys.append("rfBW"); - settings.m_inputFrequencyOffset = RemoteTCPProtocol::extractUInt32(&metaData[44]); - settingsKeys.append("inputFrequencyOffset"); - settings.m_channelGain = RemoteTCPProtocol::extractUInt32(&metaData[48]); - settingsKeys.append("channelGain"); - settings.m_channelSampleRate = RemoteTCPProtocol::extractUInt32(&metaData[52]); - settingsKeys.append("channelSampleRate"); - settings.m_sampleBits = RemoteTCPProtocol::extractUInt32(&metaData[56]); - settingsKeys.append("sampleBits"); - if (settings.m_channelSampleRate != (settings.m_devSampleRate >> settings.m_log2Decim)) - { - settings.m_channelDecimation = true; - settingsKeys.append("channelDecimation"); - } - if (m_messageQueueToInput) { - m_messageQueueToInput->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys)); - } - if (m_messageQueueToGUI) { - m_messageQueueToGUI->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys)); - } + settings.m_channelDecimation = true; + settingsKeys.append("channelDecimation"); + } + if (m_messageQueueToInput) { + m_messageQueueToInput->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys)); + } + if (m_messageQueueToGUI) { + m_messageQueueToGUI->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys)); + } + } + } + else + { + qDebug() << "RemoteTCPInputTCPHandler::dataReadyRead: Unknown protocol: " << protocol; + } + if (m_settings.m_overrideRemoteSettings) + { + // Force settings to be sent to remote device (this needs to be after m_sdra is determined above) + applySettings(m_settings, QList(), true); + } + } + m_readMetaData = true; + } +} + +void RemoteTCPInputTCPHandler::processSpyServerMetaData() +{ + bool done = false; + + while (!done) + { + if (m_state == HEADER) + { + if (m_dataSocket->bytesAvailable() >= (qint64)sizeof(SpyServerProtocol::Header)) + { + qint64 bytesRead = m_dataSocket->read((char *)&m_spyServerHeader, sizeof(SpyServerProtocol::Header)); + if (bytesRead == sizeof(SpyServerProtocol::Header)) { + m_state = DATA; + } else { + qDebug() << "RemoteTCPInputTCPHandler::processSpyServerMetaData: Failed to read:" << bytesRead << "/" << sizeof(SpyServerProtocol::Header); + } + } + else + { + done = true; + } + } + else if (m_state == DATA) + { + if (m_dataSocket->bytesAvailable() >= m_spyServerHeader.m_size) + { + qint64 bytesRead = m_dataSocket->read(&m_tcpBuf[0], m_spyServerHeader.m_size); + if (bytesRead == m_spyServerHeader.m_size) + { + if (m_spyServerHeader.m_message == SpyServerProtocol::DeviceMessage) + { + processSpyServerDevice((SpyServerProtocol::Device *) &m_tcpBuf[0]); + m_state = HEADER; + } + else if (m_spyServerHeader.m_message == SpyServerProtocol::StateMessage) + { + // This call can result in applySettings() calling clearBuffer() then processSpyServerData() + processSpyServerState((SpyServerProtocol::State *) &m_tcpBuf[0], true); + spyServerSetStreamIQ(); + + m_state = HEADER; + m_readMetaData = true; + done = true; + } + else + { + qDebug() << "RemoteTCPInputTCPHandler::processSpyServerMetaData: Unexpected message type" << m_spyServerHeader.m_message; + m_state = HEADER; } } else { - qDebug() << "RemoteTCPInputTCPHandler::dataReadyRead: Unknown protocol: " << protocol; - } - if (m_settings.m_overrideRemoteSettings) - { - // Force settings to be sent to remote device (this needs to be after m_sdra is determined above) - applySettings(m_settings, QList(), true); + qDebug() << "RemoteTCPInputTCPHandler::processSpyServerMetaData: Failed to read:" << bytesRead << "/" << m_spyServerHeader.m_size; } } - m_readMetaData = true; + else + { + done = true; + } + } + } +} + +void RemoteTCPInputTCPHandler::processSpyServerDevice(const SpyServerProtocol::Device* ssDevice) +{ + qDebug() << "RemoteTCPInputTCPHandler::processSpyServerDevice:" + << "device:" << ssDevice->m_device + << "serial:" << ssDevice->m_serial + << "sampleRate:" << ssDevice->m_sampleRate + << "decimationStages:" << ssDevice->m_decimationStages + << "maxGainIndex:" << ssDevice->m_maxGainIndex + << "minFrequency:" << ssDevice->m_minFrequency + << "maxFrequency:" << ssDevice->m_maxFrequency + << "sampleBits:" << ssDevice->m_sampleBits + << "minDecimation:" << ssDevice->m_minDecimation; + + switch (ssDevice->m_device) + { + case 1: + m_device = RemoteTCPProtocol::AIRSPY; + break; + case 2: + m_device = RemoteTCPProtocol::AIRSPY_HF; + break; + case 3: + m_device = ssDevice->m_maxGainIndex == 14 + ? RemoteTCPProtocol::RTLSDR_E4000 + : RemoteTCPProtocol::RTLSDR_R820T; + break; + default: + m_device = RemoteTCPProtocol::UNKNOWN; + break; + } + if (m_messageQueueToGUI) { + m_messageQueueToGUI->push(MsgReportRemoteDevice::create(m_device, "Spy Server", ssDevice->m_maxGainIndex)); + } + + RemoteTCPInputSettings& settings = m_settings; + QList settingsKeys{}; + // We can't change sample rate, so always have to update local setting to match + m_settings.m_devSampleRate = settings.m_devSampleRate = ssDevice->m_sampleRate; + settingsKeys.append("devSampleRate"); + // Make sure decimation setting is at least the minimum + if (!m_settings.m_overrideRemoteSettings || (settings.m_log2Decim < (int) ssDevice->m_minDecimation)) + { + m_settings.m_log2Decim = settings.m_log2Decim = ssDevice->m_minDecimation; + settingsKeys.append("log2Decim"); + } + if (m_messageQueueToInput) { + m_messageQueueToInput->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys)); + } + if (m_messageQueueToGUI) { + m_messageQueueToGUI->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys)); + } +} + +void RemoteTCPInputTCPHandler::processSpyServerState(const SpyServerProtocol::State* ssState, bool initial) +{ + qDebug() << "RemoteTCPInputTCPHandler::processSpyServerState: " + << "initial:" << initial + << "controllable:" << ssState->m_controllable + << "gain:" << ssState->m_gain + << "deviceCenterFrequency:" << ssState->m_deviceCenterFrequency + << "iqCenterFrequency:" << ssState->m_iqCenterFrequency; + + if (initial && ssState->m_controllable && m_settings.m_overrideRemoteSettings) + { + // Force client settings to be sent to server + applySettings(m_settings, QList(), true); + } + else + { + // Update client settings with that from server + RemoteTCPInputSettings& settings = m_settings; + QList settingsKeys; + + if (m_settings.m_centerFrequency != ssState->m_iqCenterFrequency) + { + settings.m_centerFrequency = ssState->m_iqCenterFrequency; + settingsKeys.append("centerFrequency"); + } + if (m_settings.m_gain[0] != (qint32) ssState->m_gain) + { + settings.m_gain[0] = ssState->m_gain; + settingsKeys.append("gain[0]"); + } + if (settingsKeys.size() > 0) + { + if (m_messageQueueToInput) { + m_messageQueueToInput->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys)); + } + if (m_messageQueueToGUI) { + m_messageQueueToGUI->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys)); + } + } + } +} + +void RemoteTCPInputTCPHandler::processSpyServerData(int requiredBytes, bool clear) +{ + if (!m_readMetaData) { + return; + } + + bool done = false; + + while (!done) + { + if (m_state == HEADER) + { + if (m_dataSocket->bytesAvailable() >= (qint64) sizeof(SpyServerProtocol::Header)) + { + qint64 bytesRead = m_dataSocket->read((char *) &m_spyServerHeader, sizeof(SpyServerProtocol::Header)); + if (bytesRead == sizeof(SpyServerProtocol::Header)) { + m_state = DATA; + } else { + qDebug() << "RemoteTCPInputTCPHandler::processSpyServerData: Failed to read:" << bytesRead << "/" << sizeof(SpyServerProtocol::Header); + } + } + else + { + done = true; + } + } + else if (m_state == DATA) + { + int bytes; + + if ((m_spyServerHeader.m_message >= SpyServerProtocol::IQ8MMessage) && (m_spyServerHeader.m_message <= SpyServerProtocol::IQ32Message)) { + bytes = std::min(requiredBytes, (int) m_spyServerHeader.m_size); + } else { + bytes = m_spyServerHeader.m_size; + } + + if (m_dataSocket->bytesAvailable() >= bytes) + { + qint64 bytesRead = m_dataSocket->read(&m_tcpBuf[0], bytes); + if (bytesRead == bytes) + { + if ((m_spyServerHeader.m_message >= SpyServerProtocol::IQ8MMessage) && (m_spyServerHeader.m_message <= SpyServerProtocol::IQ32Message)) + { + if (!clear) + { + const int bytesPerIQPair = 2 * m_settings.m_sampleBits / 8; + convert(bytesRead / bytesPerIQPair); + } + m_spyServerHeader.m_size -= bytesRead; + requiredBytes -= bytesRead; + if (m_spyServerHeader.m_size == 0) { + m_state = HEADER; + } + if (requiredBytes <= 0) { + done = true; + } + } + else if (m_spyServerHeader.m_message == SpyServerProtocol::StateMessage) + { + processSpyServerState((SpyServerProtocol::State *) &m_tcpBuf[0], false); + m_state = HEADER; + } + else + { + qDebug() << "RemoteTCPInputTCPHandler::processSpyServerData: Skipping unsupported message"; + m_state = HEADER; + } + } + else + { + qDebug() << "RemoteTCPInputTCPHandler::processSpyServerData: Failed to read:" << bytesRead << "/" << bytes; + } + } + else + { + done = true; + } } } } @@ -663,12 +1021,12 @@ void RemoteTCPInputTCPHandler::processData() if (m_dataSocket && (m_dataSocket->state() == QAbstractSocket::ConnectedState)) { int sampleRate = m_settings.m_channelSampleRate; - int bytesPerSample = m_settings.m_sampleBits / 8; - int bytesPerSecond = sampleRate * 2 * bytesPerSample; + int bytesPerIQPair = 2 * m_settings.m_sampleBits / 8; + int bytesPerSecond = sampleRate * bytesPerIQPair; if (m_dataSocket->bytesAvailable() < (0.1f * m_settings.m_preFill * bytesPerSecond)) { - qDebug() << "RemoteTCPInputTCPHandler::processData: Buffering!"; + qDebug() << "RemoteTCPInputTCPHandler::processData: Buffering - bytesAvailable:" << m_dataSocket->bytesAvailable(); m_fillBuffer = true; } @@ -690,7 +1048,7 @@ void RemoteTCPInputTCPHandler::processData() { if (m_dataSocket->bytesAvailable() >= m_settings.m_preFill * bytesPerSecond) { - qDebug() << "Buffer primed bytesAvailable:" << m_dataSocket->bytesAvailable(); + qDebug() << "RemoteTCPInputTCPHandler::processData: Buffer primed - bytesAvailable:" << m_dataSocket->bytesAvailable(); m_fillBuffer = false; m_prevDateTime = QDateTime::currentDateTime(); factor = 1.0f / 4.0f; // If this is too high, samples can just be dropped downstream @@ -708,10 +1066,20 @@ void RemoteTCPInputTCPHandler::processData() if (!m_fillBuffer) { - if (m_dataSocket->bytesAvailable() >= requiredSamples*2*bytesPerSample) + if (!m_spyServer) { - m_dataSocket->read(&m_tcpBuf[0], requiredSamples*2*bytesPerSample); - convert(requiredSamples); + // rtl_tcp/SDRA stream is just IQ samples + if (m_dataSocket->bytesAvailable() >= requiredSamples*bytesPerIQPair) + { + m_dataSocket->read(&m_tcpBuf[0], requiredSamples*bytesPerIQPair); + convert(requiredSamples); + } + } + else + { + // SpyServer stream is packetized, into a header and body, with multiple packet types + int requiredBytes = requiredSamples*bytesPerIQPair; + processSpyServerData(requiredBytes, false); } } } @@ -728,10 +1096,32 @@ void RemoteTCPInputTCPHandler::convert(int nbSamples) m_converterBuffer = new int32_t[nbSamples*2]; } - if ((m_settings.m_sampleBits == 32) && (SDR_RX_SAMP_SZ == 24)) + if ((m_settings.m_sampleBits == 32) && (SDR_RX_SAMP_SZ == 24) && !m_spyServer) { m_sampleFifo->write(reinterpret_cast(m_tcpBuf), nbSamples*sizeof(Sample)); } + else if ((m_settings.m_sampleBits == 32) && (SDR_RX_SAMP_SZ == 24) && m_spyServer) + { + float *in = (float *)m_tcpBuf; + qint32 *out = (qint32 *)m_converterBuffer; + + for (int is = 0; is < nbSamples*2; is++) { + out[is] = (qint32)(in[is] * SDR_RX_SCALEF); + } + + m_sampleFifo->write(reinterpret_cast(out), nbSamples*sizeof(Sample)); + } + else if ((m_settings.m_sampleBits == 32) && (SDR_RX_SAMP_SZ == 16) && m_spyServer) + { + float *in = (float *)m_tcpBuf; + qint16 *out = (qint16 *)m_converterBuffer; + + for (int is = 0; is < nbSamples*2; is++) { + out[is] = (qint16)(in[is] * SDR_RX_SCALEF); + } + + m_sampleFifo->write(reinterpret_cast(out), nbSamples*sizeof(Sample)); + } else if ((m_settings.m_sampleBits == 8) && (SDR_RX_SAMP_SZ == 16)) { quint8 *in = (quint8 *)m_tcpBuf; diff --git a/plugins/samplesource/remotetcpinput/remotetcpinputtcphandler.h b/plugins/samplesource/remotetcpinput/remotetcpinputtcphandler.h index 6f6cf18b6..a829c9370 100644 --- a/plugins/samplesource/remotetcpinput/remotetcpinputtcphandler.h +++ b/plugins/samplesource/remotetcpinput/remotetcpinputtcphandler.h @@ -1,5 +1,5 @@ /////////////////////////////////////////////////////////////////////////////////// -// Copyright (C) 2022 Jon Beniston, M7RCE // +// Copyright (C) 2022-2024 Jon Beniston, M7RCE // // Copyright (C) 2022 Edouard Griffiths, F4EXB // // Copyright (C) 2022 Jiří Pinkava // // // @@ -29,6 +29,7 @@ #include "util/messagequeue.h" #include "remotetcpinputsettings.h" #include "../../channelrx/remotetcpsink/remotetcpprotocol.h" +#include "spyserver.h" class SampleSinkFifo; class MessageQueue; @@ -70,20 +71,23 @@ public: public: RemoteTCPProtocol::Device getDevice() const { return m_device; } QString getProtocol() const { return m_protocol; } + int getMaxGain() const { return m_maxGain; } - static MsgReportRemoteDevice* create(RemoteTCPProtocol::Device device, const QString& protocol) + static MsgReportRemoteDevice* create(RemoteTCPProtocol::Device device, const QString& protocol, int maxGain = 0) { - return new MsgReportRemoteDevice(device, protocol); + return new MsgReportRemoteDevice(device, protocol, maxGain); } protected: RemoteTCPProtocol::Device m_device; QString m_protocol; + int m_maxGain; - MsgReportRemoteDevice(RemoteTCPProtocol::Device device, const QString& protocol) : + MsgReportRemoteDevice(RemoteTCPProtocol::Device device, const QString& protocol, int maxGain) : Message(), m_device(device), - m_protocol(protocol) + m_protocol(protocol), + m_maxGain(maxGain) { } }; @@ -139,6 +143,11 @@ private: QTimer m_reconnectTimer; QDateTime m_prevDateTime; bool m_sdra; + bool m_spyServer; + RemoteTCPProtocol::Device m_device; + SpyServerProtocol::Header m_spyServerHeader; + enum {HEADER, DATA} m_state; //!< FSM for reading Spy Server packets + int32_t *m_converterBuffer; uint32_t m_converterBufferNbSamples; @@ -172,6 +181,15 @@ private: void setChannelGain(int gain); void setSampleBitDepth(int sampleBits); void applySettings(const RemoteTCPInputSettings& settings, const QList& settingsKeys, bool force = false); + void processMetaData(); + void spyServerConnect(); + void spyServerSet(int setting, int value); + void spyServerSetIQFormat(int sampleBits); + void spyServerSetStreamIQ(); + void processSpyServerMetaData(); + void processSpyServerDevice(const SpyServerProtocol::Device* ssDevice); + void processSpyServerState(const SpyServerProtocol::State* ssState, bool initial); + void processSpyServerData(int requiredBytes, bool clear); private slots: void started(); diff --git a/plugins/samplesource/remotetcpinput/spyserver.h b/plugins/samplesource/remotetcpinput/spyserver.h new file mode 100644 index 000000000..fc7cda019 --- /dev/null +++ b/plugins/samplesource/remotetcpinput/spyserver.h @@ -0,0 +1,85 @@ +#ifndef SPY_SERVER_H +#define SPY_SERVER_H + +#include + +class SpyServerProtocol { + +public: + + static constexpr int ProtocolID = (2<<24) | 1700; + + enum Command { + setStreamingMode = 0, + setStreamingEnabled = 1, + setGain = 2, + setIQFormat = 100, + setCenterFrequency = 101, + setIQDecimation = 102, + }; + + enum Message { + DeviceMessage = 0, + StateMessage = 1, + IQ8MMessage = 100, + IQ16Message = 101, + IQ24Message = 102, + IQ32Message = 103 + }; + + struct Header { + quint32 m_id; + quint32 m_message; + quint32 m_unused1; + quint32 m_unused2; + quint32 m_size; + }; + + struct Device { + quint32 m_device; + quint32 m_serial; + quint32 m_sampleRate; + quint32 m_unused1; + quint32 m_decimationStages; // 8 for Airspy HF, 11 for Airspy, 9 for E4000/R828D/R820 + quint32 m_unused2; + quint32 m_maxGainIndex; // 8 for Airspy HF, 21 for Airspy, 14 for E4000, 29 for R828D/R820 + quint32 m_minFrequency; + quint32 m_maxFrequency; + quint32 m_sampleBits; + quint32 m_minDecimation; // Set when maximum_bandwidth is set in spyserver.config + quint32 m_unused3; + }; + + struct State { + quint32 m_controllable; + quint32 m_gain; + quint32 m_deviceCenterFrequency; + quint32 m_iqCenterFrequency; + quint32 m_unused1; + quint32 m_unused2; + quint32 m_unused3; + quint32 m_unused4; + quint32 m_unused5; + }; + + static void encodeUInt32(quint8 *p, quint32 data) + { + p[3] = (data >> 24) & 0xff; + p[2] = (data >> 16) & 0xff; + p[1] = (data >> 8) & 0xff; + p[0] = data & 0xff; + } + + static quint32 extractUInt32(quint8 *p) + { + quint32 data; + data = (p[0] & 0xff) + | ((p[1] & 0xff) << 8) + | ((p[2] & 0xff) << 16) + | ((p[3] & 0xff) << 24); + return data; + } + +}; + +#endif /* SPY_SERVER_H */ diff --git a/plugins/samplesource/sdrplayv3/sdrplayv3gui.cpp b/plugins/samplesource/sdrplayv3/sdrplayv3gui.cpp index 8f62cc751..9bb681ce9 100644 --- a/plugins/samplesource/sdrplayv3/sdrplayv3gui.cpp +++ b/plugins/samplesource/sdrplayv3/sdrplayv3gui.cpp @@ -320,23 +320,30 @@ void SDRPlayV3Gui::updateLNAValues() bool found = false; const int *attenuations = SDRPlayV3LNA::getAttenuations(m_sdrPlayV3Input->getDeviceId(), m_settings.m_centerFrequency); - int len = attenuations[0]; ui->gainLNA->blockSignals(true); ui->gainLNA->clear(); - for (int i = 1; i <= len; i++) + if (attenuations) { - if (attenuations[i] == 0) - ui->gainLNA->addItem("0"); - else - ui->gainLNA->addItem(QString("-%1").arg(attenuations[i])); - - // Find closest match - if ((attenuations[i] == -currentValue) || (!found && (attenuations[i] > -currentValue))) + int len = attenuations[0]; + for (int i = 1; i <= len; i++) { - ui->gainLNA->setCurrentIndex(i - 1); - found = true; + if (attenuations[i] == 0) + ui->gainLNA->addItem("0"); + else + ui->gainLNA->addItem(QString("-%1").arg(attenuations[i])); + + // Find closest match + if ((attenuations[i] == -currentValue) || (!found && (attenuations[i] > -currentValue))) + { + ui->gainLNA->setCurrentIndex(i - 1); + found = true; + } } } + else + { + qDebug() << "SDRPlayV3Gui::updateLNAValues: No attenuations for deviceID: " << m_sdrPlayV3Input->getDeviceId(); + } ui->gainLNA->blockSignals(false); } diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt index 987eda401..8c1ddb3b3 100644 --- a/sdrbase/CMakeLists.txt +++ b/sdrbase/CMakeLists.txt @@ -239,12 +239,14 @@ set(sdrbase_SOURCES util/golay2312.cpp util/httpdownloadmanager.cpp util/interpolation.cpp + util/kiwisdrlist.cpp util/lfsr.cpp util/maidenhead.cpp util/message.cpp util/messagequeue.cpp util/mmsi.cpp util/morse.cpp + util/nasaglobalimagery.cpp util/navtex.cpp util/openaip.cpp util/osndb.cpp @@ -256,17 +258,20 @@ set(sdrbase_SOURCES util/profiler.cpp util/psk31.cpp util/radiosonde.cpp + util/rainviewer.cpp util/rtpsink.cpp util/syncmessenger.cpp util/samplesourceserializer.cpp util/simpleserializer.cpp util/serialutil.cpp #util/spinlock.cpp + util/spyserverlist.cpp util/rtty.cpp util/uid.cpp util/units.cpp util/timeutil.cpp util/visa.cpp + util/waypoints.cpp util/weather.cpp util/iot/device.cpp util/iot/homeassistant.cpp @@ -482,6 +487,7 @@ set(sdrbase_HEADERS util/incrementalarray.h util/incrementalvector.h util/interpolation.h + util/kiwisdrlist.h util/lfsr.h util/maidenhead.h util/message.h @@ -490,6 +496,7 @@ set(sdrbase_HEADERS util/morse.h util/movingaverage.h util/movingmaximum.h + util/nasaglobalimagery.h util/navtex.h util/openaip.h util/osndb.h @@ -501,6 +508,7 @@ set(sdrbase_HEADERS util/profiler.h util/psk31.h util/radiosonde.h + util/rainviewer.h util/rtpsink.h util/rtty.h util/syncmessenger.h @@ -508,10 +516,12 @@ set(sdrbase_HEADERS util/simpleserializer.h util/serialutil.h #util/spinlock.h + util/spyserverlist.h util/uid.h util/units.h util/timeutil.h util/visa.h + util/waypoints.h util/weather.h util/iot/device.h util/iot/homeassistant.h diff --git a/sdrbase/resources/webapi/doc/html2/index.html b/sdrbase/resources/webapi/doc/html2/index.html index ba5d19681..794221a9f 100644 --- a/sdrbase/resources/webapi/doc/html2/index.html +++ b/sdrbase/resources/webapi/doc/html2/index.html @@ -13104,6 +13104,10 @@ margin-bottom: 20px; "preFill" : { "type" : "integer" }, + "protocol" : { + "type" : "string", + "description" : "(SDRangel or Spy Server)" + }, "useReverseAPI" : { "type" : "integer", "description" : "Synchronize with reverse API (1 for yes, 0 for no)" @@ -58668,7 +58672,7 @@ except ApiException as e:
- Generated 2024-02-12T10:33:45.606+01:00 + Generated 2024-02-27T15:07:08.970+01:00
diff --git a/sdrbase/resources/webapi/doc/swagger/include/RemoteTCPInput.yaml b/sdrbase/resources/webapi/doc/swagger/include/RemoteTCPInput.yaml index df1bf0f8f..09cc073e6 100644 --- a/sdrbase/resources/webapi/doc/swagger/include/RemoteTCPInput.yaml +++ b/sdrbase/resources/webapi/doc/swagger/include/RemoteTCPInput.yaml @@ -43,6 +43,9 @@ RemoteTCPInputSettings: type: integer preFill: type: integer + protocol: + description: (SDRangel or Spy Server) + type: string useReverseAPI: description: Synchronize with reverse API (1 for yes, 0 for no) type: integer diff --git a/sdrbase/util/httpdownloadmanager.cpp b/sdrbase/util/httpdownloadmanager.cpp index 71537381b..fa88b9454 100644 --- a/sdrbase/util/httpdownloadmanager.cpp +++ b/sdrbase/util/httpdownloadmanager.cpp @@ -149,7 +149,7 @@ void HttpDownloadManager::downloadFinished(QNetworkReply *reply) QString action = match.captured(1); action = action.replace("&", "&"); - qDebug() << "HttpDownloadManager: Skipping Go ogle drive warning - downloading " << action; + qDebug() << "HttpDownloadManager: Skipping Google drive warning - downloading " << action; QUrl newUrl(action); QNetworkReply *newReply = download(newUrl, filename); @@ -160,7 +160,51 @@ void HttpDownloadManager::downloadFinished(QNetworkReply *reply) } else { - qDebug() << "HttpDownloadManager: Can't find action URL in Google Drive page " << data; + qDebug() << "HttpDownloadManager: Can't find action URL in Google Drive page\nURL: " << url << "\nData:\n" << data; + } + } + else if (url.startsWith("https://drive.usercontent.google.com/download") + && data.startsWith("") + && !filename.endsWith(".html") + ) + { + QRegularExpression regexpAction("action=\\\"(.*?)\\\""); + QRegularExpressionMatch matchAction = regexpAction.match(data); + QRegularExpression regexpId("name=\"id\" value=\"([\\w-]+)\""); + QRegularExpressionMatch matchId = regexpId.match(data); + QRegularExpression regexpUuid("name=\"uuid\" value=\"([\\w-]+)\""); + QRegularExpressionMatch matchUuid = regexpUuid.match(data); + QRegularExpression regexpAt("name=\"at\" value=\"([\\w-]+\\:)\""); + QRegularExpressionMatch matchAt = regexpAt.match(data); + + if (matchAction.hasMatch() && matchId.hasMatch() && matchUuid.hasMatch()) + { + m_downloads.removeAll(reply); + m_filenames.remove(idx); + + QString newURLString = matchAction.captured(1) + + "?id=" + matchId.captured(1) + + "&export=download" + + "&authuser=0" + + "&confirm=t" + + "&uuid=" + matchUuid.captured(1) + ; + if (matchAt.hasMatch()) { + newURLString = newURLString + "at=" + matchAt.captured(1); + } + + qDebug() << "HttpDownloadManager: Skipping Google drive warning - downloading " << newURLString; + QUrl newUrl(newURLString); + QNetworkReply *newReply = download(newUrl, filename); + + // Indicate that we are retrying, so progress dialogs can be updated + emit retryDownload(filename, reply, newReply); + + retry = true; + } + else + { + qDebug() << "HttpDownloadManager: Can't find action URL in Google Drive page\nURL: " << url << "\nData:\n" << data; } } else if (writeToFile(filename, data)) diff --git a/sdrbase/util/kiwisdrlist.cpp b/sdrbase/util/kiwisdrlist.cpp new file mode 100644 index 000000000..d3989b9a0 --- /dev/null +++ b/sdrbase/util/kiwisdrlist.cpp @@ -0,0 +1,198 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "kiwisdrlist.h" + +#include +#include +#include +#include +#include +#include +#include + +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 sdrs; + QString html(bytes); + QRegularExpression div("
(.*?)<\\/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(""); + 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; + } + 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); +} diff --git a/sdrbase/util/kiwisdrlist.h b/sdrbase/util/kiwisdrlist.h new file mode 100644 index 000000000..a6da7f731 --- /dev/null +++ b/sdrbase/util/kiwisdrlist.h @@ -0,0 +1,78 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_KIWISDRLIST_H +#define INCLUDE_KIWISDRLIST_H + +#include +#include + +#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& 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 */ diff --git a/sdrbase/util/nasaglobalimagery.cpp b/sdrbase/util/nasaglobalimagery.cpp new file mode 100644 index 000000000..93851516f --- /dev/null +++ b/sdrbase/util/nasaglobalimagery.cpp @@ -0,0 +1,326 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "nasaglobalimagery.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +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 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 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 application/vnd.mapbox-vector-tile + 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); +} diff --git a/sdrbase/util/nasaglobalimagery.h b/sdrbase/util/nasaglobalimagery.h new file mode 100644 index 000000000..9408c5bcc --- /dev/null +++ b/sdrbase/util/nasaglobalimagery.h @@ -0,0 +1,98 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_NASAGLOBALIMAGERY_H +#define INCLUDE_NASAGLOBALIMAGERY_H + +#include +#include + +#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 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 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& 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 */ diff --git a/sdrbase/util/rainviewer.cpp b/sdrbase/util/rainviewer.cpp new file mode 100644 index 000000000..8d761e323 --- /dev/null +++ b/sdrbase/util/rainviewer.cpp @@ -0,0 +1,130 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "rainviewer.h" + +#include +#include +#include +#include +#include +#include + +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"; + } +} diff --git a/sdrbase/util/rainviewer.h b/sdrbase/util/rainviewer.h new file mode 100644 index 000000000..4a5acb983 --- /dev/null +++ b/sdrbase/util/rainviewer.h @@ -0,0 +1,55 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_RAINVIEWER_H +#define INCLUDE_RAINVIEWER_H + +#include +#include + +#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 */ diff --git a/sdrbase/util/spyserverlist.cpp b/sdrbase/util/spyserverlist.cpp new file mode 100644 index 000000000..bd8143c5a --- /dev/null +++ b/sdrbase/util/spyserverlist.cpp @@ -0,0 +1,164 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "spyserverlist.h" + +#include +#include +#include +#include +#include +#include +#include + +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 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); +} diff --git a/sdrbase/util/spyserverlist.h b/sdrbase/util/spyserverlist.h new file mode 100644 index 000000000..53c92dac0 --- /dev/null +++ b/sdrbase/util/spyserverlist.h @@ -0,0 +1,75 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_SPYSERVERLIST_H +#define INCLUDE_SPYSERVERLIST_H + +#include +#include + +#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& 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 */ diff --git a/sdrbase/util/waypoints.cpp b/sdrbase/util/waypoints.cpp new file mode 100644 index 000000000..00835e86c --- /dev/null +++ b/sdrbase/util/waypoints.cpp @@ -0,0 +1,141 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "waypoints.h" +#include "csv.h" + +QHash *Waypoint::readCSV(const QString &filename) +{ + QHash *waypoints = new QHash(); + 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> 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 *Waypoints::readWaypoints() +{ + return Waypoint::readCSV(getWaypointsFilename()); +} + +QSharedPointer> 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>(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; + } +} diff --git a/sdrbase/util/waypoints.h b/sdrbase/util/waypoints.h new file mode 100644 index 000000000..3df2c6eb3 --- /dev/null +++ b/sdrbase/util/waypoints.h @@ -0,0 +1,80 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021-2024 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_WAYPOINTS_H +#define INCLUDE_WAYPOINTS_H + +#include +#include +#include + +#include +#include + +#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 *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> getWaypoints(); + +private: + HttpDownloadManager m_dlm; + + static QSharedPointer> m_waypoints; + + static QDateTime m_waypointsModifiedDateTime; + + static QHash *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 diff --git a/swagger/sdrangel/api/swagger/include/RemoteTCPInput.yaml b/swagger/sdrangel/api/swagger/include/RemoteTCPInput.yaml index df1bf0f8f..09cc073e6 100644 --- a/swagger/sdrangel/api/swagger/include/RemoteTCPInput.yaml +++ b/swagger/sdrangel/api/swagger/include/RemoteTCPInput.yaml @@ -43,6 +43,9 @@ RemoteTCPInputSettings: type: integer preFill: type: integer + protocol: + description: (SDRangel or Spy Server) + type: string useReverseAPI: description: Synchronize with reverse API (1 for yes, 0 for no) type: integer diff --git a/swagger/sdrangel/code/html2/index.html b/swagger/sdrangel/code/html2/index.html index ba5d19681..794221a9f 100644 --- a/swagger/sdrangel/code/html2/index.html +++ b/swagger/sdrangel/code/html2/index.html @@ -13104,6 +13104,10 @@ margin-bottom: 20px; "preFill" : { "type" : "integer" }, + "protocol" : { + "type" : "string", + "description" : "(SDRangel or Spy Server)" + }, "useReverseAPI" : { "type" : "integer", "description" : "Synchronize with reverse API (1 for yes, 0 for no)" @@ -58668,7 +58672,7 @@ except ApiException as e:
- Generated 2024-02-12T10:33:45.606+01:00 + Generated 2024-02-27T15:07:08.970+01:00
diff --git a/swagger/sdrangel/code/qt5/client/SWGRemoteTCPInputSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGRemoteTCPInputSettings.cpp index cb50977b5..1f86e8091 100644 --- a/swagger/sdrangel/code/qt5/client/SWGRemoteTCPInputSettings.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGRemoteTCPInputSettings.cpp @@ -68,6 +68,8 @@ SWGRemoteTCPInputSettings::SWGRemoteTCPInputSettings() { m_override_remote_settings_isSet = false; pre_fill = 0; m_pre_fill_isSet = false; + protocol = nullptr; + m_protocol_isSet = false; use_reverse_api = 0; m_use_reverse_api_isSet = false; reverse_api_address = nullptr; @@ -124,6 +126,8 @@ SWGRemoteTCPInputSettings::init() { m_override_remote_settings_isSet = false; pre_fill = 0; m_pre_fill_isSet = false; + protocol = new QString(""); + m_protocol_isSet = false; use_reverse_api = 0; m_use_reverse_api_isSet = false; reverse_api_address = new QString(""); @@ -158,6 +162,9 @@ SWGRemoteTCPInputSettings::cleanup() { + if(protocol != nullptr) { + delete protocol; + } if(reverse_api_address != nullptr) { delete reverse_api_address; @@ -217,6 +224,8 @@ SWGRemoteTCPInputSettings::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&pre_fill, pJson["preFill"], "qint32", ""); + ::SWGSDRangel::setValue(&protocol, pJson["protocol"], "QString", "QString"); + ::SWGSDRangel::setValue(&use_reverse_api, pJson["useReverseAPI"], "qint32", ""); ::SWGSDRangel::setValue(&reverse_api_address, pJson["reverseAPIAddress"], "QString", "QString"); @@ -301,6 +310,9 @@ SWGRemoteTCPInputSettings::asJsonObject() { if(m_pre_fill_isSet){ obj->insert("preFill", QJsonValue(pre_fill)); } + if(protocol != nullptr && *protocol != QString("")){ + toJsonValue(QString("protocol"), protocol, obj, QString("QString")); + } if(m_use_reverse_api_isSet){ obj->insert("useReverseAPI", QJsonValue(use_reverse_api)); } @@ -517,6 +529,16 @@ SWGRemoteTCPInputSettings::setPreFill(qint32 pre_fill) { this->m_pre_fill_isSet = true; } +QString* +SWGRemoteTCPInputSettings::getProtocol() { + return protocol; +} +void +SWGRemoteTCPInputSettings::setProtocol(QString* protocol) { + this->protocol = protocol; + this->m_protocol_isSet = true; +} + qint32 SWGRemoteTCPInputSettings::getUseReverseApi() { return use_reverse_api; @@ -622,6 +644,9 @@ SWGRemoteTCPInputSettings::isSet(){ if(m_pre_fill_isSet){ isObjectUpdated = true; break; } + if(protocol && *protocol != QString("")){ + isObjectUpdated = true; break; + } if(m_use_reverse_api_isSet){ isObjectUpdated = true; break; } diff --git a/swagger/sdrangel/code/qt5/client/SWGRemoteTCPInputSettings.h b/swagger/sdrangel/code/qt5/client/SWGRemoteTCPInputSettings.h index 80ee77496..5189b7c32 100644 --- a/swagger/sdrangel/code/qt5/client/SWGRemoteTCPInputSettings.h +++ b/swagger/sdrangel/code/qt5/client/SWGRemoteTCPInputSettings.h @@ -102,6 +102,9 @@ public: qint32 getPreFill(); void setPreFill(qint32 pre_fill); + QString* getProtocol(); + void setProtocol(QString* protocol); + qint32 getUseReverseApi(); void setUseReverseApi(qint32 use_reverse_api); @@ -178,6 +181,9 @@ private: qint32 pre_fill; bool m_pre_fill_isSet; + QString* protocol; + bool m_protocol_isSet; + qint32 use_reverse_api; bool m_use_reverse_api_isSet;