From 2cb9ec4df3c447b986861f714d5f52c97f2968f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20P=C3=A9rez=20Alonso?= Date: Thu, 16 Jul 2026 10:35:32 +0200 Subject: [PATCH 1/5] fix(FlightMap): show current item above others When multiple simple mission items shared the same position, later items could partially cover the current target, including its number. The current item is now drawn above other mission item indicators, ensuring the active navigation target can always be identified during flight. --- src/FlightMap/MapItems/MissionItemIndicator.qml | 7 ++++--- src/PlanView/SimpleItemMapVisual.qml | 2 +- src/PlanView/TakeoffItemMapVisual.qml | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/FlightMap/MapItems/MissionItemIndicator.qml b/src/FlightMap/MapItems/MissionItemIndicator.qml index 76e53fa1220d..1b1b90fca987 100644 --- a/src/FlightMap/MapItems/MissionItemIndicator.qml +++ b/src/FlightMap/MapItems/MissionItemIndicator.qml @@ -12,15 +12,18 @@ MapQuickItem { property var missionItem property int sequenceNumber + readonly property bool _isCurrentItem: missionItem ? missionItem.isCurrentItem || missionItem.hasCurrentChildItem : false + signal clicked anchorPoint.x: sourceItem.anchorPointX anchorPoint.y: sourceItem.anchorPointY + z: QGroundControl.zOrderMapItems + (_isCurrentItem ? 0.5 : 0) // Show current item above other indicators, but below controls sourceItem: MissionItemIndexLabel { id: _label - checked: _isCurrentItem + checked: _item._isCurrentItem label: missionItem.abbreviation index: missionItem.abbreviation.charAt(0) > 'A' && missionItem.abbreviation.charAt(0) < 'z' ? -1 : missionItem.sequenceNumber gimbalYaw: missionItem.missionGimbalYaw @@ -29,7 +32,5 @@ MapQuickItem { highlightSelected: true onClicked: _item.clicked() opacity: _item.opacity - - property bool _isCurrentItem: missionItem ? missionItem.isCurrentItem || missionItem.hasCurrentChildItem : false } } diff --git a/src/PlanView/SimpleItemMapVisual.qml b/src/PlanView/SimpleItemMapVisual.qml index c441e75cb182..972583a3612c 100644 --- a/src/PlanView/SimpleItemMapVisual.qml +++ b/src/PlanView/SimpleItemMapVisual.qml @@ -65,7 +65,7 @@ MissionItemMapVisualBase { MissionItemIndicator { coordinate: _missionItem.coordinate visible: _missionItem.specifiesCoordinate - z: QGroundControl.zOrderMapItems + z: QGroundControl.zOrderMapItems + (_missionItem.isCurrentItem || _missionItem.hasCurrentChildItem ? 1 : 0) missionItem: _missionItem sequenceNumber: _missionItem.sequenceNumber onClicked: if(_root.interactive) _root.clicked(_missionItem.sequenceNumber) diff --git a/src/PlanView/TakeoffItemMapVisual.qml b/src/PlanView/TakeoffItemMapVisual.qml index 20985a3758a7..a67ffb0690c3 100644 --- a/src/PlanView/TakeoffItemMapVisual.qml +++ b/src/PlanView/TakeoffItemMapVisual.qml @@ -98,7 +98,7 @@ Item { MissionItemIndicator { coordinate: _missionItem.specifiesCoordinate ? _missionItem.coordinate : _missionItem.launchCoordinate - z: QGroundControl.zOrderMapItems + z: QGroundControl.zOrderMapItems + (_missionItem.isCurrentItem || _missionItem.hasCurrentChildItem ? 1 : 0) missionItem: _missionItem sequenceNumber: _missionItem.sequenceNumber onClicked: _root.clicked(_missionItem.sequenceNumber) From 1aea08f68e94345c3e144f1c6f5565bec0e131ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20P=C3=A9rez=20Alonso?= Date: Fri, 17 Jul 2026 02:42:15 +0200 Subject: [PATCH 2/5] fix(FlightMap): hide arrows on short legs Direction arrows could become larger on screen than their underlying mission legs, obscuring rather than clarifying the route. They now hide whenever a leg's projected length is shorter than the 30-pixel width of the arrow canvas. --- src/FlightMap/MapItems/MapLineArrow.qml | 27 +++++++++++++++++++++++- src/FlightMap/MapItems/PlanMapItems.qml | 1 + src/PlanView/PlanView.qml | 1 + src/PlanView/TransectStyleMapVisuals.qml | 4 ++++ 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/FlightMap/MapItems/MapLineArrow.qml b/src/FlightMap/MapItems/MapLineArrow.qml index 3f0957429709..270a75fc5fd6 100644 --- a/src/FlightMap/MapItems/MapLineArrow.qml +++ b/src/FlightMap/MapItems/MapLineArrow.qml @@ -8,14 +8,18 @@ import QGroundControl.Controls import QGroundControl.FlightMap MapQuickItem { + id: root + + property FlightMap mapControl + property color arrowColor: "white" property var fromCoord: QtPositioning.coordinate() property var toCoord: QtPositioning.coordinate() property int arrowPosition: 1 ///< 1: first quarter, 2: halfway, 3: last quarter - property var _map: parent property real _arrowSize: 15 property real _arrowHeading: 0 + property real _screenLegLength: 0 function _updateArrowDetails() { if (fromCoord && fromCoord.isValid && toCoord && toCoord.isValid) { @@ -26,16 +30,37 @@ MapQuickItem { coordinate = QtPositioning.coordinate() _arrowHeading = 0 } + _updateScreenLegLength() + } + + function _updateScreenLegLength() { + if (mapControl && fromCoord && fromCoord.isValid && toCoord && toCoord.isValid) { + const fromPoint = mapControl.fromCoordinate(fromCoord, false) + const toPoint = mapControl.fromCoordinate(toCoord, false) + _screenLegLength = Math.hypot(toPoint.x - fromPoint.x, toPoint.y - fromPoint.y) + } else { + _screenLegLength = 0 + } } onFromCoordChanged: _updateArrowDetails() onToCoordChanged: _updateArrowDetails() + onMapControlChanged: _updateScreenLegLength() + + Component.onCompleted: _updateArrowDetails() + + Connections { + target: root.mapControl + + function onZoomLevelChanged() { root._updateScreenLegLength() } + } sourceItem: Canvas { x: -_arrowSize y: 0 width: _arrowSize * 2 height: _arrowSize + visible: root._screenLegLength >= width onPaint: { var ctx = getContext("2d"); diff --git a/src/FlightMap/MapItems/PlanMapItems.qml b/src/FlightMap/MapItems/PlanMapItems.qml index 8a37f7030ca3..2c194497f6eb 100644 --- a/src/FlightMap/MapItems/PlanMapItems.qml +++ b/src/FlightMap/MapItems/PlanMapItems.qml @@ -68,6 +68,7 @@ Item { fromCoord: object ? object.coordinate1 : undefined toCoord: object ? object.coordinate2 : undefined arrowPosition: 3 + mapControl: _root.map z: QGroundControl.zOrderWaypointLines + 1 } } diff --git a/src/PlanView/PlanView.qml b/src/PlanView/PlanView.qml index 257a99cf3027..4622e6ff862d 100644 --- a/src/PlanView/PlanView.qml +++ b/src/PlanView/PlanView.qml @@ -347,6 +347,7 @@ Item { fromCoord: object ? object.coordinate1 : undefined toCoord: object ? object.coordinate2 : undefined arrowPosition: 3 + mapControl: editorMap z: QGroundControl.zOrderWaypointLines + 1 } } diff --git a/src/PlanView/TransectStyleMapVisuals.qml b/src/PlanView/TransectStyleMapVisuals.qml index ce6ddf17da54..6bbf3ffb3f28 100644 --- a/src/PlanView/TransectStyleMapVisuals.qml +++ b/src/PlanView/TransectStyleMapVisuals.qml @@ -136,6 +136,7 @@ Item { fromCoord: _transectPoints[_firstTrueTransectIndex] toCoord: _transectPoints[_firstTrueTransectIndex + 1] arrowPosition: 1 + mapControl: _root.map visible: _currentItem && !_vertexDrag opacity: _root.opacity } @@ -148,6 +149,7 @@ Item { fromCoord: _transectPoints[nextTrueTransectIndex] toCoord: _transectPoints[nextTrueTransectIndex + 1] arrowPosition: 1 + mapControl: _root.map visible: _currentItem && _transectCount > 3 && !_vertexDrag opacity: _root.opacity @@ -162,6 +164,7 @@ Item { fromCoord: _transectPoints[_lastTrueTransectIndex - 1] toCoord: _transectPoints[_lastTrueTransectIndex] arrowPosition: 3 + mapControl: _root.map visible: _currentItem && !_vertexDrag opacity: _root.opacity } @@ -174,6 +177,7 @@ Item { fromCoord: _transectPoints[prevTrueTransectIndex - 1] toCoord: _transectPoints[prevTrueTransectIndex] arrowPosition: 13 + mapControl: _root.map visible: _currentItem && _transectCount > 3 && !_vertexDrag opacity: _root.opacity From e4ecd6be8ac1355a304d84f54591622c723749e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20P=C3=A9rez=20Alonso?= Date: Thu, 16 Jul 2026 12:27:55 +0200 Subject: [PATCH 3/5] feat(FlightMap): group overlapping mission items Simple mission items can share coordinates or become close enough at the current map scale for their indicators to overlap. Only the topmost indicator could then be reliably identified or selected, particularly on touchscreens. They are now combined into a single group indicator with direct access to every grouped item. Key changes: - Groups items in screen space and updates the groups when the map scale or item positions change. Indicators remain separate while they can still be selected individually. - Shows the current item's normal marker label when the group contains it, or that of the lowest-sequence item otherwise. A new intermediate marker size and an ellipsis below the label distinguish groups from individual items. - Opens a non-modal horizontal selector when a group is selected in Plan or Fly view. The selector grows with its contents up to a viewport-relative limit while keeping every member directly accessible. Command-specific labels preserve their normal abbreviations and include sequence numbers to distinguish repeated items. - Keeps each simple mission item independently loaded. This preserves mission legs, loiter circles, dragging, and other associated map visuals. Complex items do not interact with the grouping system. --- src/FlightMap/CMakeLists.txt | 1 + .../MapItems/MissionItemIndicator.qml | 35 +- .../MapItems/MissionItemIndicatorGroup.qml | 328 ++++++++++++++++++ src/FlightMap/MapItems/PlanMapItems.qml | 17 +- src/PlanView/MissionItemIndexLabel.qml | 41 ++- src/PlanView/MissionItemMapVisual.qml | 10 +- src/PlanView/MissionItemMapVisualBase.qml | 9 + src/PlanView/PlanView.qml | 15 +- src/PlanView/SimpleItemMapVisual.qml | 15 +- src/PlanView/TakeoffItemMapVisual.qml | 4 +- 10 files changed, 449 insertions(+), 26 deletions(-) create mode 100644 src/FlightMap/MapItems/MissionItemIndicatorGroup.qml diff --git a/src/FlightMap/CMakeLists.txt b/src/FlightMap/CMakeLists.txt index 9d537719772a..1b09c9db1428 100644 --- a/src/FlightMap/CMakeLists.txt +++ b/src/FlightMap/CMakeLists.txt @@ -16,6 +16,7 @@ qt_add_qml_module(FlightMapModule MapItems/MapLineArrow.qml MapItems/MissionItemIndicator.qml MapItems/MissionItemIndicatorDrag.qml + MapItems/MissionItemIndicatorGroup.qml MapItems/MissionLineView.qml MapItems/PlanMapItems.qml MapItems/ProximityRadarMapView.qml diff --git a/src/FlightMap/MapItems/MissionItemIndicator.qml b/src/FlightMap/MapItems/MissionItemIndicator.qml index 1b1b90fca987..5042ad9d710e 100644 --- a/src/FlightMap/MapItems/MissionItemIndicator.qml +++ b/src/FlightMap/MapItems/MissionItemIndicator.qml @@ -11,26 +11,53 @@ MapQuickItem { property var missionItem property int sequenceNumber + property MissionItemIndicatorGroup indicatorGroup + property bool indicatorVisible: true + property bool interactive: true readonly property bool _isCurrentItem: missionItem ? missionItem.isCurrentItem || missionItem.hasCurrentChildItem : false + readonly property bool _usesAbbreviation: missionItem + ? missionItem.abbreviation.charAt(0) > 'A' + && missionItem.abbreviation.charAt(0) < 'z' + : false + readonly property var _group: indicatorGroup ? indicatorGroup.groupForItem(missionItem) : null + readonly property bool _isGrouped: _group ? _group.items.length > 1 : false + readonly property bool _isGroupRepresentative: !_group || _group.representative === missionItem signal clicked + function activate() { + if (_isGrouped) { + const topLeft = _label.mapToItem(globals.parent, Qt.point(0, 0)) + const bottomRight = _label.mapToItem(globals.parent, Qt.point(_label.width, _label.height)) + const clickRect = Qt.rect(topLeft.x, topLeft.y, + bottomRight.x - topLeft.x, bottomRight.y - topLeft.y) + indicatorGroup.showGroup(missionItem, clickRect) + } else { + clicked() + } + } + anchorPoint.x: sourceItem.anchorPointX anchorPoint.y: sourceItem.anchorPointY z: QGroundControl.zOrderMapItems + (_isCurrentItem ? 0.5 : 0) // Show current item above other indicators, but below controls + visible: indicatorVisible && _isGroupRepresentative sourceItem: MissionItemIndexLabel { id: _label checked: _item._isCurrentItem - label: missionItem.abbreviation - index: missionItem.abbreviation.charAt(0) > 'A' && missionItem.abbreviation.charAt(0) < 'z' ? -1 : missionItem.sequenceNumber + label: _item.missionItem.abbreviation + index: _item._usesAbbreviation ? -1 : _item.missionItem.sequenceNumber + indicatorSubText: _item._isGrouped ? "…" : "" + small: !_item._isGrouped && !_item._isCurrentItem + medium: _item._isGrouped && !_item._isCurrentItem gimbalYaw: missionItem.missionGimbalYaw vehicleYaw: missionItem.missionVehicleYaw - showGimbalYaw: !isNaN(missionItem.missionGimbalYaw) + showGimbalYaw: !_item._isGrouped && !isNaN(_item.missionItem.missionGimbalYaw) highlightSelected: true - onClicked: _item.clicked() + enabled: _item.interactive + onClicked: _item.activate() opacity: _item.opacity } } diff --git a/src/FlightMap/MapItems/MissionItemIndicatorGroup.qml b/src/FlightMap/MapItems/MissionItemIndicatorGroup.qml new file mode 100644 index 000000000000..e596db7bd2dc --- /dev/null +++ b/src/FlightMap/MapItems/MissionItemIndicatorGroup.qml @@ -0,0 +1,328 @@ +pragma ComponentBehavior: Bound + +import QtQuick + +import QGroundControl +import QGroundControl.Controls +import QGroundControl.FlightMap +import QGroundControl.PlanView + +/// Groups mission item indicators which are too close to select individually. +Item { + id: root + + required property FlightMap map + required property QmlObjectListModel missionItems + + readonly property real _smallIndicatorRadius: _oddCeil((ScreenTools.defaultFontPixelHeight * ScreenTools.smallFontPointRatio) / 2) + readonly property real _largeIndicatorRadius: _oddCeil(ScreenTools.defaultFontPixelHeight * 0.66) + readonly property real _mediumIndicatorRadius: _oddCeil(((_smallIndicatorRadius * 2) + _largeIndicatorRadius) / 3) + readonly property real _groupingDistance: _mediumIndicatorRadius + readonly property var _groupingState: { + const state = [] + const count = missionItems ? missionItems.count : 0 + + for (let i = 0; i < count; i++) { + const item = missionItems.get(i) + if (!item || !item.isSimpleItem || (!item.specifiesCoordinate && !item.isTakeoffItem)) { + continue + } + + const coordinate = _coordinateForItem(item) + state.push({ + sequenceNumber: item.sequenceNumber, + coordinateValid: coordinate && coordinate.isValid, + latitude: coordinate && coordinate.isValid ? coordinate.latitude : NaN, + longitude: coordinate && coordinate.isValid ? coordinate.longitude : NaN, + current: item.isCurrentItem || item.hasCurrentChildItem + }) + } + + return state + } + + property var _groupsBySequenceNumber: ({}) + property var _selectionPanel + + signal itemSelected(int sequenceNumber) + + function groupForItem(item) { + return item ? _groupsBySequenceNumber[item.sequenceNumber] || null : null + } + + function showGroup(item, clickRect) { + const group = groupForItem(item) + if (!group || group.items.length < 2) { + itemSelected(item.sequenceNumber) + return + } + + _closeSelectionPanel() + const panel = selectionPanelComponent.createObject(mainWindow, { + clickRect: clickRect, + groupItems: group.items + }) + if (!panel) { + return + } + + _selectionPanel = panel + panel.open() + } + + function _oddCeil(value) { + const rounded = Math.ceil(value) + return rounded + (rounded % 2 === 0 ? 1 : 0) + } + + function _scheduleRegroup() { + regroupTimer.restart() + } + + function _coordinateForItem(item) { + return item.isTakeoffItem && !item.specifiesCoordinate ? item.launchCoordinate : item.coordinate + } + + function _closeSelectionPanel() { + if (_selectionPanel) { + _selectionPanel.close() + _selectionPanel = null + } + } + + function _regroup() { + _closeSelectionPanel() + + if (!map || !map.mapReady) { + _groupsBySequenceNumber = {} + return + } + + const entries = [] + const count = missionItems ? missionItems.count : 0 + for (let i = 0; i < count; i++) { + const item = missionItems.get(i) + if (!item || !item.isSimpleItem || (!item.specifiesCoordinate && !item.isTakeoffItem)) { + continue + } + + const coordinate = _coordinateForItem(item) + if (!coordinate || !coordinate.isValid) { + continue + } + + const point = map.fromCoordinate(coordinate, false /* clipToViewPort */) + if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) { + continue + } + + entries.push({ + item: item, + point: point, + current: item.isCurrentItem || item.hasCurrentChildItem + }) + } + + entries.sort((first, second) => { + if (first.current !== second.current) { + return first.current ? -1 : 1 + } + return first.item.sequenceNumber - second.item.sequenceNumber + }) + + const cells = {} + const groups = [] + for (const entry of entries) { + const cellX = Math.floor(entry.point.x / _groupingDistance) + const cellY = Math.floor(entry.point.y / _groupingDistance) + let closestGroup = null + let closestDistanceSquared = Infinity + + for (let x = cellX - 1; x <= cellX + 1; x++) { + for (let y = cellY - 1; y <= cellY + 1; y++) { + const nearbyGroups = cells[`${x},${y}`] || [] + for (const group of nearbyGroups) { + const deltaX = entry.point.x - group.point.x + const deltaY = entry.point.y - group.point.y + const distanceSquared = (deltaX * deltaX) + (deltaY * deltaY) + if (distanceSquared <= _groupingDistance * _groupingDistance + && (distanceSquared < closestDistanceSquared + || (distanceSquared === closestDistanceSquared + && group.representative.sequenceNumber < closestGroup.representative.sequenceNumber))) { + closestGroup = group + closestDistanceSquared = distanceSquared + } + } + } + } + + if (closestGroup) { + closestGroup.items.push(entry.item) + continue + } + + const group = { + items: [entry.item], + point: entry.point, + representative: entry.item + } + groups.push(group) + + const cellKey = `${cellX},${cellY}` + if (!cells[cellKey]) { + cells[cellKey] = [] + } + cells[cellKey].push(group) + } + + const groupsBySequenceNumber = {} + for (const group of groups) { + group.items.sort((first, second) => first.sequenceNumber - second.sequenceNumber) + for (const item of group.items) { + groupsBySequenceNumber[item.sequenceNumber] = group + } + } + _groupsBySequenceNumber = groupsBySequenceNumber + } + + Timer { + id: regroupTimer + + interval: 0 + repeat: false + onTriggered: root._regroup() + } + + Component { + id: selectionPanelComponent + + DropPanel { + id: selectionPanel + + modal: false + + required property var groupItems + + sourceComponent: Component { + Item { + implicitWidth: itemListView.width + implicitHeight: itemListView.height + + QGCListView { + id: itemListView + + width: Math.min(Math.max(contentItem.childrenRect.width, _itemExtent), _maxWidth) + height: _itemExtent + orientation: ListView.Horizontal + model: selectionPanel.groupItems + cacheBuffer: width * 2 + reuseItems: true + currentIndex: -1 + + readonly property real _itemExtent: Math.max(ScreenTools.minTouchPixels, ScreenTools.defaultFontPixelHeight * 2.5) + readonly property real _maxWidth: selectionPanel.dropViewPort.width * 0.4 + + function _scrollBy(delta) { + const currentTarget = wheelScrollAnimation.running ? wheelScrollAnimation.to : contentX + const target = Math.max(0, Math.min(currentTarget - delta, Math.max(0, contentWidth - width))) + wheelScrollAnimation.stop() + wheelScrollAnimation.from = contentX + wheelScrollAnimation.to = target + wheelScrollAnimation.start() + } + + delegate: Item { + id: itemDelegate + + width: Math.max(itemListView._itemExtent, itemLabel.width + ScreenTools.defaultFontPixelWidth) + height: itemListView._itemExtent + + required property var modelData + + readonly property bool _usesAbbreviation: modelData.abbreviation.charAt(0) > 'A' && modelData.abbreviation.charAt(0) < 'z' + readonly property string _supplementaryLabel: !_usesAbbreviation ? "" : `${modelData.abbreviation} (${modelData.sequenceNumber})` + + MissionItemIndexLabel { + id: itemLabel + anchors.centerIn: parent + checked: itemDelegate.modelData.isCurrentItem || itemDelegate.modelData.hasCurrentChildItem + label: itemDelegate.modelData.abbreviation + index: itemDelegate._usesAbbreviation ? -1 : itemDelegate.modelData.sequenceNumber + small: false + supplementaryLabel: itemDelegate._supplementaryLabel + } + + MouseArea { + anchors.fill: parent + onClicked: { + selectionPanel.close() + root.itemSelected(itemDelegate.modelData.sequenceNumber) + } + } + } + + NumberAnimation { + id: wheelScrollAnimation + + target: itemListView + property: "contentX" + duration: 120 + easing.type: Easing.OutCubic + } + + WheelHandler { + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + orientation: Qt.Vertical + target: null + onWheel: event => { + itemListView._scrollBy(event.pixelDelta.y || event.angleDelta.y) + event.accepted = true + } + } + + WheelHandler { + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + orientation: Qt.Horizontal + target: null + onWheel: event => { + itemListView._scrollBy(event.pixelDelta.x || event.angleDelta.x) + event.accepted = true + } + } + + onMovementStarted: wheelScrollAnimation.stop() + } + } + } + + onClosed: { + if (root._selectionPanel === selectionPanel) { + root._selectionPanel = null + } + destroy() + } + } + } + + Connections { + target: root.map + + function onMapPanStop() { root._scheduleRegroup() } + function onMapReadyChanged() { root._scheduleRegroup() } + function onZoomLevelChanged() { root._scheduleRegroup() } + } + + Connections { + target: root.missionItems + + function onDataChanged() { root._scheduleRegroup() } + } + + on_GroupingStateChanged: _scheduleRegroup() + on_GroupingDistanceChanged: _scheduleRegroup() + onMapChanged: _scheduleRegroup() + onMissionItemsChanged: _scheduleRegroup() + + Component.onCompleted: _scheduleRegroup() + Component.onDestruction: _closeSelectionPanel() +} diff --git a/src/FlightMap/MapItems/PlanMapItems.qml b/src/FlightMap/MapItems/PlanMapItems.qml index 2c194497f6eb..2eea5fa9b94e 100644 --- a/src/FlightMap/MapItems/PlanMapItems.qml +++ b/src/FlightMap/MapItems/PlanMapItems.qml @@ -27,14 +27,25 @@ Item { property string fmode: vehicle.flightMode + MissionItemIndicatorGroup { + id: _missionItemIndicatorGroup + + map: _root._map + missionItems: _root.largeMapView ? _root._missionController.visualItems : null + onItemSelected: (sequenceNumber) => { + _root._guidedController.confirmAction(_root._guidedController.actionSetWaypoint, Math.max(sequenceNumber, 1)) + } + } + // Add the mission item visuals to the map Repeater { model: largeMapView ? _missionController.visualItems : 0 delegate: MissionItemMapVisual { - map: _map - vehicle: _vehicle - onClicked: _guidedController.confirmAction(_guidedController.actionSetWaypoint, Math.max(object.sequenceNumber, 1)) + map: _map + vehicle: _vehicle + indicatorGroup: _missionItemIndicatorGroup + onClicked: _guidedController.confirmAction(_guidedController.actionSetWaypoint, Math.max(object.sequenceNumber, 1)) } } diff --git a/src/PlanView/MissionItemIndexLabel.qml b/src/PlanView/MissionItemIndexLabel.qml index 332838b9b115..8b24f4e384df 100644 --- a/src/PlanView/MissionItemIndexLabel.qml +++ b/src/PlanView/MissionItemIndexLabel.qml @@ -16,6 +16,7 @@ Canvas { property int index: 0 ///< Index to show in the indicator, 0 will show single char label instead, -1 first char of label in indicator full label to the side property bool checked: false property bool small: !checked + property bool medium: false property bool child: false property bool highlightSelected: false property var color: checked ? "green" : (child ? qgcPal.mapIndicatorChild : qgcPal.mapIndicator) @@ -26,20 +27,21 @@ Canvas { property real vehicleYaw property bool showGimbalYaw: false property bool showSequenceNumbers: true + property string indicatorSubText + property string supplementaryLabel property real _width: showGimbalYaw ? Math.max(_gimbalYawWidth, labelControl.visible ? labelControl.width : indicator.width) : (labelControl.visible ? labelControl.width : indicator.width) property real _height: showGimbalYaw ? _gimbalYawWidth : (labelControl.visible ? labelControl.height : indicator.height) property real _gimbalYawRadius: ScreenTools.defaultFontPixelHeight property real _gimbalYawWidth: _gimbalYawRadius * 2 - property real _smallRadiusRaw: Math.ceil((ScreenTools.defaultFontPixelHeight * ScreenTools.smallFontPointRatio) / 2) - property real _smallRadius: _smallRadiusRaw + ((_smallRadiusRaw % 2 == 0) ? 1 : 0) // odd number for better centering - property real _normalRadiusRaw: Math.ceil(ScreenTools.defaultFontPixelHeight * 0.66) - property real _normalRadius: _normalRadiusRaw + ((_normalRadiusRaw % 2 == 0) ? 1 : 0) - property real _indicatorRadius: small ? _smallRadius : _normalRadius + property real _smallRadius: _oddCeil((ScreenTools.defaultFontPixelHeight * ScreenTools.smallFontPointRatio) / 2) + property real _largeRadius: _oddCeil(ScreenTools.defaultFontPixelHeight * 0.66) + property real _mediumRadius: _oddCeil(((_smallRadius * 2) + _largeRadius) / 3) + property real _indicatorRadius: small ? _smallRadius : (medium ? _mediumRadius : _largeRadius) property real _gimbalRadians: degreesToRadians(vehicleYaw + gimbalYaw - 90) property real _labelMargin: 2 property real _labelRadius: _indicatorRadius + _labelMargin - property string _label: label.length > 1 ? label : "" + property string _label: supplementaryLabel !== "" ? supplementaryLabel : (label.length > 1 ? label : "") property string _index: index === 0 || index === -1 ? label.charAt(0) : (showSequenceNumbers ? index : "") onColorChanged: requestPaint() @@ -49,6 +51,11 @@ Canvas { QGCPalette { id: qgcPal } + function _oddCeil(value) { + const rounded = Math.ceil(value) + return rounded + (rounded % 2 === 0 ? 1 : 0) + } + function degreesToRadians(degrees) { return (Math.PI/180)*degrees } @@ -110,6 +117,7 @@ Canvas { radius: _indicatorRadius QGCLabel { + id: indexLabel anchors.fill: parent horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter @@ -118,6 +126,23 @@ Canvas { fontSizeMode: Text.Fit text: _index } + + QGCLabel { + anchors { + horizontalCenter: indexLabel.horizontalCenter + baseline: indexLabel.baseline + baselineOffset: indexLabel.contentHeight / 5 + } + width: indicator.width + height: indicator.height * 0.4 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: "white" + font.pointSize: ScreenTools.smallFontPointSize + fontSizeMode: Text.Fit + text: root.indicatorSubText + visible: text !== "" + } } // Extra circle to indicate selection @@ -132,10 +157,10 @@ Canvas { anchors.centerIn: indicator } - // The mouse click area is always the size of a normal indicator + // The mouse click area is always the size of a large indicator Item { id: mouseAreaFill - anchors.margins: small ? -(_normalRadius - _smallRadius) : 0 + anchors.margins: -(root._largeRadius - root._indicatorRadius) anchors.fill: indicator } diff --git a/src/PlanView/MissionItemMapVisual.qml b/src/PlanView/MissionItemMapVisual.qml index b71a2d950367..7fe04aa331d0 100644 --- a/src/PlanView/MissionItemMapVisual.qml +++ b/src/PlanView/MissionItemMapVisual.qml @@ -5,6 +5,7 @@ import QtPositioning import QGroundControl import QGroundControl.Controls +import QGroundControl.FlightMap /// Mission item map visual Item { @@ -13,6 +14,7 @@ Item { property var map ///< Map control to place item in property var vehicle ///< Vehicle associated with this item property bool interactive: true ///< Vehicle associated with this item + property MissionItemIndicatorGroup indicatorGroup signal clicked(int sequenceNumber) @@ -22,12 +24,16 @@ Item { asynchronous: true Component.onCompleted: { - mapVisualLoader.setSource(object.mapVisualQML, { + const properties = { map: _root.map, vehicle: _root.vehicle, opacity: Qt.binding(() => _root.opacity), interactive: Qt.binding(() => _root.interactive) - }) + } + if (object.isSimpleItem) { + properties.indicatorGroup = _root.indicatorGroup + } + mapVisualLoader.setSource(object.mapVisualQML, properties) } onLoaded: { diff --git a/src/PlanView/MissionItemMapVisualBase.qml b/src/PlanView/MissionItemMapVisualBase.qml index f5a1055c95bf..14d044f43917 100644 --- a/src/PlanView/MissionItemMapVisualBase.qml +++ b/src/PlanView/MissionItemMapVisualBase.qml @@ -15,6 +15,7 @@ Item { property var map ///< Map control to place item in property var vehicle ///< Vehicle associated with this item property bool interactive: true + property MissionItemIndicatorGroup indicatorGroup /// Subclasses must set this to their indicator Component property Component indicatorComponent @@ -122,6 +123,14 @@ Item { itemIndicator: itemVisualLoader.item itemCoordinate: _missionItem.coordinate visible: control.interactive + onClicked: { + const indicator = itemVisualLoader.item + if (indicator && indicator.activate) { + indicator.activate() + } else { + control.clicked(control._missionItem.sequenceNumber) + } + } onItemCoordinateChanged: _missionItem.coordinate = itemCoordinate } } diff --git a/src/PlanView/PlanView.qml b/src/PlanView/PlanView.qml index 4622e6ff862d..1e7a566db84f 100644 --- a/src/PlanView/PlanView.qml +++ b/src/PlanView/PlanView.qml @@ -320,6 +320,16 @@ Item { } } + MissionItemIndicatorGroup { + id: _missionItemIndicatorGroup + + map: editorMap + missionItems: _root._missionController.visualItems + onItemSelected: (sequenceNumber) => { + _root._missionController.setCurrentPlanViewSeqNum(sequenceNumber, false) + } + } + // Add the mission item visuals to the map Repeater { model: _missionController.visualItems @@ -328,7 +338,10 @@ Item { opacity: _editingLayer == _layerMission ? 1 : editorMap._nonInteractiveOpacity interactive: _editingLayer == _layerMission vehicle: _planMasterController.controllerVehicle - onClicked: (sequenceNumber) => { _missionController.setCurrentPlanViewSeqNum(sequenceNumber, false) } + indicatorGroup: _missionItemIndicatorGroup + onClicked: (sequenceNumber) => { + _root._missionController.setCurrentPlanViewSeqNum(sequenceNumber, false) + } } } diff --git a/src/PlanView/SimpleItemMapVisual.qml b/src/PlanView/SimpleItemMapVisual.qml index 972583a3612c..e8aaa8a12dea 100644 --- a/src/PlanView/SimpleItemMapVisual.qml +++ b/src/PlanView/SimpleItemMapVisual.qml @@ -63,13 +63,14 @@ MissionItemMapVisualBase { id: indicatorComponent MissionItemIndicator { - coordinate: _missionItem.coordinate - visible: _missionItem.specifiesCoordinate - z: QGroundControl.zOrderMapItems + (_missionItem.isCurrentItem || _missionItem.hasCurrentChildItem ? 1 : 0) - missionItem: _missionItem - sequenceNumber: _missionItem.sequenceNumber - onClicked: if(_root.interactive) _root.clicked(_missionItem.sequenceNumber) - opacity: _root.opacity + coordinate: _missionItem.coordinate + indicatorGroup: _root.indicatorGroup + indicatorVisible: _missionItem.specifiesCoordinate + interactive: _root.interactive + missionItem: _missionItem + sequenceNumber: _missionItem.sequenceNumber + onClicked: _root.clicked(_missionItem.sequenceNumber) + opacity: _root.opacity } } diff --git a/src/PlanView/TakeoffItemMapVisual.qml b/src/PlanView/TakeoffItemMapVisual.qml index a67ffb0690c3..1bc99f58fc6d 100644 --- a/src/PlanView/TakeoffItemMapVisual.qml +++ b/src/PlanView/TakeoffItemMapVisual.qml @@ -14,6 +14,7 @@ Item { property var map ///< Map control to place item in property var vehicle ///< Vehicle associated with this item property bool interactive: true + property MissionItemIndicatorGroup indicatorGroup property var _missionItem: object property var _takeoffIndicatorItem @@ -98,7 +99,8 @@ Item { MissionItemIndicator { coordinate: _missionItem.specifiesCoordinate ? _missionItem.coordinate : _missionItem.launchCoordinate - z: QGroundControl.zOrderMapItems + (_missionItem.isCurrentItem || _missionItem.hasCurrentChildItem ? 1 : 0) + indicatorGroup: _root.indicatorGroup + interactive: _root.interactive missionItem: _missionItem sequenceNumber: _missionItem.sequenceNumber onClicked: _root.clicked(_missionItem.sequenceNumber) From d396616b76b2bc21bf1c81c9c8ba33fbce232426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20P=C3=A9rez=20Alonso?= Date: Thu, 16 Jul 2026 16:53:27 +0200 Subject: [PATCH 4/5] fix(FlightMap): keep items visible at all zooms MapQuickItem's default auto-fade hides simple mission indicators at low zoom levels, removing mission context when several items occupy a small area. Grouped indicators manage that density without losing access to any item. Automatic fading is therefore disabled, allowing simple mission markers to remain visible and selectable at every zoom level. --- src/FlightMap/MapItems/MissionItemIndicator.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/FlightMap/MapItems/MissionItemIndicator.qml b/src/FlightMap/MapItems/MissionItemIndicator.qml index 5042ad9d710e..b4405ee1e9a8 100644 --- a/src/FlightMap/MapItems/MissionItemIndicator.qml +++ b/src/FlightMap/MapItems/MissionItemIndicator.qml @@ -40,6 +40,7 @@ MapQuickItem { anchorPoint.x: sourceItem.anchorPointX anchorPoint.y: sourceItem.anchorPointY + autoFadeIn: false z: QGroundControl.zOrderMapItems + (_isCurrentItem ? 0.5 : 0) // Show current item above other indicators, but below controls visible: indicatorVisible && _isGroupRepresentative From d834e78d090a817c349153dc955495c4ca201e4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20P=C3=A9rez=20Alonso?= Date: Fri, 17 Jul 2026 17:26:41 +0200 Subject: [PATCH 5/5] fix(PlanView): hide split handles on short legs The split handle remained visible on legs whose midpoint was too close to either endpoint for an inserted item to remain separately selectable. It now hides whenever the resulting item would join the same indicator group. --- .../MapItems/MissionItemIndicatorGroup.qml | 11 +++++----- src/PlanView/PlanView.qml | 22 +++++++++++++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/FlightMap/MapItems/MissionItemIndicatorGroup.qml b/src/FlightMap/MapItems/MissionItemIndicatorGroup.qml index e596db7bd2dc..312adbd1036d 100644 --- a/src/FlightMap/MapItems/MissionItemIndicatorGroup.qml +++ b/src/FlightMap/MapItems/MissionItemIndicatorGroup.qml @@ -14,10 +14,11 @@ Item { required property FlightMap map required property QmlObjectListModel missionItems + readonly property real groupingDistance: _mediumIndicatorRadius + readonly property real _smallIndicatorRadius: _oddCeil((ScreenTools.defaultFontPixelHeight * ScreenTools.smallFontPointRatio) / 2) readonly property real _largeIndicatorRadius: _oddCeil(ScreenTools.defaultFontPixelHeight * 0.66) readonly property real _mediumIndicatorRadius: _oddCeil(((_smallIndicatorRadius * 2) + _largeIndicatorRadius) / 3) - readonly property real _groupingDistance: _mediumIndicatorRadius readonly property var _groupingState: { const state = [] const count = missionItems ? missionItems.count : 0 @@ -133,8 +134,8 @@ Item { const cells = {} const groups = [] for (const entry of entries) { - const cellX = Math.floor(entry.point.x / _groupingDistance) - const cellY = Math.floor(entry.point.y / _groupingDistance) + const cellX = Math.floor(entry.point.x / groupingDistance) + const cellY = Math.floor(entry.point.y / groupingDistance) let closestGroup = null let closestDistanceSquared = Infinity @@ -145,7 +146,7 @@ Item { const deltaX = entry.point.x - group.point.x const deltaY = entry.point.y - group.point.y const distanceSquared = (deltaX * deltaX) + (deltaY * deltaY) - if (distanceSquared <= _groupingDistance * _groupingDistance + if (distanceSquared <= groupingDistance * groupingDistance && (distanceSquared < closestDistanceSquared || (distanceSquared === closestDistanceSquared && group.representative.sequenceNumber < closestGroup.representative.sequenceNumber))) { @@ -319,7 +320,7 @@ Item { } on_GroupingStateChanged: _scheduleRegroup() - on_GroupingDistanceChanged: _scheduleRegroup() + onGroupingDistanceChanged: _scheduleRegroup() onMapChanged: _scheduleRegroup() onMissionItemsChanged: _scheduleRegroup() diff --git a/src/PlanView/PlanView.qml b/src/PlanView/PlanView.qml index 1e7a566db84f..76ec66be5697 100644 --- a/src/PlanView/PlanView.qml +++ b/src/PlanView/PlanView.qml @@ -368,10 +368,14 @@ Item { // UI for splitting the current segment MapQuickItem { id: splitSegmentItem + + property real _screenLegLength: 0 + anchorPoint.x: sourceItem.width / 2 anchorPoint.y: sourceItem.height / 2 z: QGroundControl.zOrderWaypointLines + 1 visible: _editingLayer == _layerMission + && _screenLegLength > _missionItemIndicatorGroup.groupingDistance * 2 sourceItem: SplitIndicator { onClicked: _missionController.insertSimpleMissionItem(splitSegmentItem.coordinate, @@ -379,6 +383,17 @@ Item { true /* makeCurrentItem */) } + function _updateScreenLegLength() { + const segment = _root._missionController.splitSegment + if (segment && segment.coordinate1.isValid && segment.coordinate2.isValid) { + const fromPoint = editorMap.fromCoordinate(segment.coordinate1, false /* clipToViewPort */) + const toPoint = editorMap.fromCoordinate(segment.coordinate2, false /* clipToViewPort */) + _screenLegLength = Math.hypot(toPoint.x - fromPoint.x, toPoint.y - fromPoint.y) + } else { + _screenLegLength = 0 + } + } + function _updateSplitCoord() { if (_missionController.splitSegment) { var distance = _missionController.splitSegment.coordinate1.distanceTo(_missionController.splitSegment.coordinate2) @@ -387,6 +402,7 @@ Item { } else { coordinate = QtPositioning.coordinate() } + _updateScreenLegLength() } Connections { @@ -399,6 +415,12 @@ Item { function onCoordinate1Changed() { splitSegmentItem._updateSplitCoord() } function onCoordinate2Changed() { splitSegmentItem._updateSplitCoord() } } + + Connections { + target: editorMap + function onCenterChanged() { splitSegmentItem._updateScreenLegLength() } + function onZoomLevelChanged() { splitSegmentItem._updateScreenLegLength() } + } } // Add the vehicles to the map