From 7238b48e225f2f245ca22477e10739b12836cbcb Mon Sep 17 00:00:00 2001 From: srcejon Date: Wed, 28 Feb 2024 12:34:57 +0000 Subject: [PATCH] ADS-B: Add QT 6 support for map. --- plugins/channelrx/demodadsb/adsbdemodgui.cpp | 5 +- plugins/channelrx/demodadsb/map.qrc | 2 + .../demodadsb/map/ModifiedMapView.qml | 178 ++++++ plugins/channelrx/demodadsb/map/map_6.qml | 537 ++++++++++++++++++ 4 files changed, 721 insertions(+), 1 deletion(-) create mode 100644 plugins/channelrx/demodadsb/map/ModifiedMapView.qml create mode 100644 plugins/channelrx/demodadsb/map/map_6.qml 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 + } + } + } + } + } + } + } + } + } + } + +}