1/****************************************************************************
3 * (c) 2009-2024 QGROUNDCONTROL PROJECT <http://www.qgroundcontrol.org>
5 * QGroundControl is licensed according to the terms in the file
6 * COPYING.md in the root of the source code directory.
8 ****************************************************************************/
13import QGroundControl.GeoMap
15/// Mission-item overlay for the GeoMap: instantiates the GeoMap-native visual
16/// declared by each item's VisualMissionItem.geoMapVisualQML (mirrors the
17/// existing mapVisualQML dispatch consumed by src/PlanView/MissionItemMapVisual.qml
18/// — every item type states its own visual, simple items included, rather
19/// than special-casing GeoMapWaypointItem outside the dispatch). Items with
20/// no GeoMap-native visual render nothing: the home item (already shown via
21/// its own GeoMapPin) and the Survey/Corridor/Structure complex items, which
22/// never appear in the fly-view controller (vehicle downloads reconstruct
23/// only simple items and landing patterns).
24/// Overlapping waypoint markers are grouped (MissionItemIndicatorGroup parity): a
25/// click reports the whole group in either mode, and settled top-down 2D also
26/// collapses each group into one badged marker.
30 property var missionController
32 property var surfaceModel
33 property real homeTerrainBias: 0 ///< See GeoMapWaypointItem.homeTerrainBias
35 /// A marker was clicked; item is its VisualMissionItem (mirrors
36 /// MissionItemMapVisual.clicked as consumed by PlanMapItems)
37 signal itemClicked(var item)
39 /// A marker overlapping others was clicked; items are the group's
40 /// VisualMissionItems by sequence number, marker the clicked GeoMapWaypointItem
41 signal groupClicked(var items, var marker)
43 /// Markers moved on screen (zoom, resize, 2D/3D), so an open group picker is stale.
44 /// Center moves don't count: vehicle follow recenters on every position update.
45 signal groupsInvalidated()
47 readonly property var _camera: scene ? scene.camera : null
48 // Top-down 2D only pans, rotates and zooms, so groups change only with the zoom
49 readonly property bool _collapse: _camera !== null && _camera.mode === GeoMapCamera.Mode2D && _camera.isTopDown
50 && scene.terrainScale === 0
52 // Re-evaluates when a marker moves or the current item changes
53 readonly property var _groupingState: {
55 const items = root.missionController ? root.missionController.visualItems : null
56 for (let i = 0; items && i < items.count; ++i) {
57 const item = items.get(i)
58 if (item.isSimpleItem && item.specifiesCoordinate) {
59 state.push([item.coordinate, item.isCurrentItem])
65 function _waypointMarkers() {
67 for (let i = 0; i < repeater.count; ++i) {
68 const loader = repeater.itemAt(i)
69 if (loader && loader.item && loader.item.hasOwnProperty("collapsed")) {
70 markers.push(loader.item)
76 // Groups of on-screen waypoint markers, each led by its representative: the
77 // current item, else the lowest sequence number
78 function _markerGroups() {
79 const markers = root._waypointMarkers().filter(marker => marker.hasMarker && marker.projected)
80 markers.sort((first, second) => {
81 const firstCurrent = first.item.isCurrentItem || first.item.hasCurrentChildItem
82 const secondCurrent = second.item.isCurrentItem || second.item.hasCurrentChildItem
83 if (firstCurrent !== secondCurrent) {
84 return firstCurrent ? -1 : 1
86 return first.item.sequenceNumber - second.item.sequenceNumber
88 if (markers.length === 0) {
91 const points = markers.map(marker => marker.mapToItem(root, marker.anchorPoint.x, marker.anchorPoint.y))
92 // A small marker's radius, as MissionItemIndicatorGroup
93 const representatives = MapMarkerGrouping.representatives(points, markers[0].smallIndicatorSize / 2)
95 const groupsByRepresentative = {}
96 for (let i = 0; i < markers.length; ++i) {
97 const representative = representatives[i]
98 if (!groupsByRepresentative[representative]) {
99 groupsByRepresentative[representative] = []
100 groups.push(groupsByRepresentative[representative])
102 groupsByRepresentative[representative].push(markers[i])
107 function _regroup() {
108 const markers = root._waypointMarkers()
109 for (const marker of markers) {
110 marker.collapsed = false
111 marker.grouped = false
113 if (!root._collapse) {
116 for (const group of root._markerGroups()) {
117 if (group.length > 1) {
118 group[0].grouped = true
119 for (let i = 1; i < group.length; ++i) {
120 group[i].collapsed = true
126 function _scheduleRegroup() {
127 Qt.callLater(root._regroup)
130 function _markerClicked(marker) {
131 const group = root._markerGroups().find(members => members.includes(marker))
132 if (group && group.length > 1) {
133 const items = group.map(member => member.item)
134 items.sort((first, second) => first.sequenceNumber - second.sequenceNumber)
135 root.groupClicked(items, marker)
137 root.itemClicked(marker.item)
141 on_CollapseChanged: {
145 on_GroupingStateChanged: _scheduleRegroup()
150 function onDistanceChanged() { root.groupsInvalidated() }
151 function onViewportSizeChanged() { root.groupsInvalidated() }
155 target: root._collapse ? root._camera : null
157 function onDistanceChanged() { root._scheduleRegroup() }
158 function onCenterElevationChanged() { root._scheduleRegroup() }
159 function onViewportSizeChanged() { root._scheduleRegroup() }
165 model: root.missionController ? root.missionController.visualItems : 0
169 active: object.geoMapVisualQML !== ""
171 Component.onCompleted: {
173 setSource(object.geoMapVisualQML, {
176 surfaceModel: root.surfaceModel
182 // Declared by GeoMapWaypointItem and the landing pattern
183 // visuals; wire it up without hardcoding filenames here
184 if (item.hasOwnProperty("homeTerrainBias")) {
185 item.homeTerrainBias = Qt.binding(() => root.homeTerrainBias)
188 if (visual.hasOwnProperty("collapsed")) {
189 visual.clicked.connect(() => root._markerClicked(visual))
190 root._scheduleRegroup()
191 } else if (visual.clicked !== undefined) {
192 visual.clicked.connect(() => root.itemClicked(object))