QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
FlyViewGeoMap.qml
Go to the documentation of this file.
1/****************************************************************************
2 *
3 * (c) 2009-2024 QGROUNDCONTROL PROJECT <http://www.qgroundcontrol.org>
4 *
5 * QGroundControl is licensed according to the terms in the file
6 * COPYING.md in the root of the source code directory.
7 *
8 ****************************************************************************/
9
10import QtQuick
11import QtQuick.Layouts
12import QtPositioning
13
14import QGroundControl
15import QGroundControl.Controls
16import QGroundControl.FlightMap
17import QGroundControl.GeoMap
18
19/// Fly-view 2D/3D map: the GeoMap-engine counterpart of FlyViewMap.
20/// Fly-view specific map content (vehicle items, trajectory, ...) lands here.
21GeoMap {
22 id: root
23
24 allowGCSLocationCenter: true
25 allowVehicleLocationCenter: true
26 keepVehicleCentered: QGroundControl.settingsManager.flyViewSettings.keepMapCenteredOnVehicle.rawValue
27
28 // Guided-action indicators for the map click drop panel (FlyViewMapClickDropPanel)
29 readonly property var gotoIndicator: gotoLocationItem
30 readonly property var orbitIndicator: orbitMapCircle
31
32 readonly property var _activeVehicle: QGroundControl.multiVehicleManager.activeVehicle
33 readonly property var _missionController: globals.planMasterControllerFlyView ? globals.planMasterControllerFlyView.missionController : null
34 readonly property var _rallyPointController: globals.planMasterControllerFlyView ? globals.planMasterControllerFlyView.rallyPointController : null
35 readonly property var _geoFenceController: globals.planMasterControllerFlyView ? globals.planMasterControllerFlyView.geoFenceController : null
36 readonly property var _flyViewSettings: QGroundControl.settingsManager.flyViewSettings
37
38 // DEM height minus vehicle-reported AMSL, sampled at home for the active
39 // vehicle. Computed once here and shared by GeoMapMissionPath and every
40 // GeoMapMissionItems marker instead of each re-sampling the same DEM
41 // lookup independently (see GeoMapVehicleItem for the same correction
42 // applied per-vehicle in the marker Repeater below, where it can't be
43 // shared since each vehicle has its own home position).
44 property real _activeVehicleHomeTerrainBias: 0
45
46 function _updateActiveVehicleHomeTerrainBias() {
47 if (root.surfaceModel && root._activeVehicle && root._activeVehicle.homePosition.isValid && !isNaN(root._activeVehicle.homePosition.altitude)) {
48 root._activeVehicleHomeTerrainBias = root.surfaceModel.terrainDataHeightAt(root._activeVehicle.homePosition) - root._activeVehicle.homePosition.altitude
49 } else {
50 root._activeVehicleHomeTerrainBias = 0
51 }
52 }
53
54 on_ActiveVehicleChanged: {
55 _updateActiveVehicleHomeTerrainBias()
56 _fuzzyVehicleDisplayAltitude = NaN
57 _updateFuzzyVehicleDisplayAltitude()
58 }
59 onSurfaceModelChanged: _updateActiveVehicleHomeTerrainBias()
60
61 // Deadbanded copy of the vehicle's bias-corrected display altitude so
62 // live-bound orbit visuals don't rebuild their ring geometry on every
63 // sub-meter telemetry altitude tick
64 property real _fuzzyVehicleDisplayAltitude: NaN
65
66 readonly property real _fuzzyAltitudeDeadbandMeters: 1
67
68 function _updateFuzzyVehicleDisplayAltitude() {
69 if (!root._activeVehicle || isNaN(root._activeVehicle.coordinate.altitude)) {
70 root._fuzzyVehicleDisplayAltitude = NaN
71 return
72 }
73 const displayAltitude = root._activeVehicle.coordinate.altitude + root._activeVehicleHomeTerrainBias
74 if (isNaN(root._fuzzyVehicleDisplayAltitude) || Math.abs(displayAltitude - root._fuzzyVehicleDisplayAltitude) > root._fuzzyAltitudeDeadbandMeters) {
75 root._fuzzyVehicleDisplayAltitude = displayAltitude
76 }
77 }
78
79 on_ActiveVehicleHomeTerrainBiasChanged: _updateFuzzyVehicleDisplayAltitude()
80
81 function _withFuzzyVehicleAltitude(coord) {
82 if (!isNaN(root._fuzzyVehicleDisplayAltitude)) {
83 return QtPositioning.coordinate(coord.latitude, coord.longitude, root._fuzzyVehicleDisplayAltitude)
84 }
85 return QtPositioning.coordinate(coord.latitude, coord.longitude)
86 }
87
88 // Guided commands hold the vehicle's current altitude; same bias-corrected
89 // AMSL as GeoMapVehicleItem so indicators line up with the rendered vehicle
90 function _withVehicleAltitude(coord) {
91 if (root._activeVehicle && !isNaN(root._activeVehicle.coordinate.altitude)) {
92 return QtPositioning.coordinate(coord.latitude, coord.longitude,
93 root._activeVehicle.coordinate.altitude + root._activeVehicleHomeTerrainBias)
94 }
95 return QtPositioning.coordinate(coord.latitude, coord.longitude)
96 }
97
98 property var _missionItemSelectionPanel: null
99
100 function _confirmSetWaypoint(sequenceNumber) {
101 globals.guidedControllerFlyView.confirmAction(globals.guidedControllerFlyView.actionSetWaypoint,
102 Math.max(sequenceNumber, 1))
103 }
104
105 function _closeMissionItemSelectionPanel() {
106 if (_missionItemSelectionPanel) {
107 _missionItemSelectionPanel.close()
108 }
109 }
110
111 Connections {
112 target: root.surfaceModel
113
114 function onTerrainDataChanged() {
115 root._updateActiveVehicleHomeTerrainBias()
116 }
117 }
118
119 Connections {
120 target: root._activeVehicle
121
122 function onHomePositionChanged() {
123 root._updateActiveVehicleHomeTerrainBias()
124 }
125
126 function onCoordinateChanged() {
127 root._updateFuzzyVehicleDisplayAltitude()
128 }
129 }
130
131 // Vehicle home (launch) location
132 GeoMapPin {
133 scene: root.scene
134 surfaceModel: root.surfaceModel
135 coordinate: root._activeVehicle ? root._activeVehicle.homePosition : QtPositioning.coordinate()
136 visible: root._activeVehicle && root._activeVehicle.homePosition.isValid
137 label: qsTr("L")
138 }
139
140 Repeater {
141 model: QGroundControl.multiVehicleManager.vehicles
142
143 GeoMapVehicleItem {
144 scene: root.scene
145 surfaceModel: root.surfaceModel
146 vehicle: object
147 }
148 }
149
150 GeoMapFlightPath {
151 scene: root.scene
152 surfaceModel: root.surfaceModel
153 vehicle: root._activeVehicle
154 }
155
156 GeoMapMissionPath {
157 scene: root.scene
158 surfaceModel: root.surfaceModel
159 missionController: root._missionController
160 vehicle: root._activeVehicle
161 homeTerrainBias: root._activeVehicleHomeTerrainBias
162 }
163
164 GeoMapMissionItems {
165 scene: root.scene
166 surfaceModel: root.surfaceModel
167 missionController: root._missionController
168 homeTerrainBias: root._activeVehicleHomeTerrainBias
169 // Same action as PlanMapItems in the QtLocation fly view: clicking a
170 // marker adjusts the vehicle's current mission item (after confirm)
171 onItemClicked: (item) => root._confirmSetWaypoint(item.sequenceNumber)
172 onGroupClicked: (items, marker) => {
173 root._closeMissionItemSelectionPanel()
174 const topLeft = marker.mapToItem(globals.parent, 0, 0)
175 root._missionItemSelectionPanel = missionItemSelectionPanelComponent.createObject(mainWindow, {
176 clickRect: Qt.rect(topLeft.x, topLeft.y, marker.width, marker.height),
177 groupItems: items
178 })
179 if (root._missionItemSelectionPanel) {
180 root._missionItemSelectionPanel.open()
181 }
182 }
183 onGroupsInvalidated: root._closeMissionItemSelectionPanel()
184 }
185
186 // The picker lists the old mission's items: close it when the mission is replaced
187 Connections {
188 target: root._missionController
189
190 function onVisualItemsReset() { root._closeMissionItemSelectionPanel() }
191 }
192
193 Component {
194 id: missionItemSelectionPanelComponent
195
196 MissionItemSelectionPanel {
197 id: missionItemSelectionPanel
198
199 onItemSelected: (sequenceNumber) => root._confirmSetWaypoint(sequenceNumber)
200 onClosed: {
201 if (root._missionItemSelectionPanel === missionItemSelectionPanel) {
202 root._missionItemSelectionPanel = null
203 }
204 destroy()
205 }
206 }
207 }
208
209 GeoMapMissionDirectionArrows {
210 scene: root.scene
211 surfaceModel: root.surfaceModel
212 missionController: root._missionController
213 homeTerrainBias: root._activeVehicleHomeTerrainBias
214 }
215
216 // Allow custom builds to add map items (QGCCorePlugin::customGeoMapItems)
217 Repeater {
218 model: QGroundControl.corePlugin.customGeoMapItems
219
220 Loader {
221 Component.onCompleted: setSource(object.url, {
222 customMapObject: object,
223 scene: root.scene,
224 surfaceModel: root.surfaceModel
225 })
226 }
227 }
228
229 GeoMapGeoFenceVisuals {
230 scene: root.scene
231 surfaceModel: root.surfaceModel
232 geoFenceController: root._geoFenceController
233 homePosition: root._activeVehicle && root._activeVehicle.homePosition.isValid ? root._activeVehicle.homePosition : QtPositioning.coordinate()
234 }
235
236 // Rally points
237 Repeater {
238 model: root._rallyPointController ? root._rallyPointController.points : undefined
239
240 GeoMapMissionLabel {
241 scene: root.scene
242 surfaceModel: root.surfaceModel
243 coordinate: object.coordinate
244 label: qsTr("R", "rally point map item label")
245 }
246 }
247
248 // Proximity sensor arcs per vehicle
249 Repeater {
250 model: QGroundControl.multiVehicleManager.vehicles
251
252 FlyViewGeoProximityRadar {
253 scene: root.scene
254 surfaceModel: root.surfaceModel
255 vehicle: object
256 }
257 }
258
259 // ADS-B traffic
260 Repeater {
261 model: QGroundControl.adsbVehicleManager.adsbVehicles
262
263 GeoMapADSBVehicleItem {
264 scene: root.scene
265 surfaceModel: root.surfaceModel
266 coordinate: object.coordinate
267 altitude: object.altitude
268 callsign: object.callsign
269 heading: object.heading
270 alert: object.alert
271 size: ScreenTools.defaultFontPixelHeight * 2.5
272 homeTerrainBias: root._activeVehicleHomeTerrainBias
273 }
274 }
275
276 // Camera trigger points: batched sprite layer (single draw call), since
277 // surveys can produce thousands of points
278 GeoMapSpriteLayer {
279 scene: root.scene
280 surfaceModel: root.surfaceModel
281 model: root._activeVehicle ? root._activeVehicle.cameraTriggerPoints : null
282 sprite: triggerIconSource
283 spriteSize: triggerIcon.width
284
285 CameraTriggerIcon { id: triggerIcon }
286 ShaderEffectSource {
287 id: triggerIconSource
288 sourceItem: triggerIcon
289 hideSource: true
290 // Render at physical resolution: the default (logical-size) texture
291 // is upscaled on hidpi displays and looks blurry
292 textureSize: Qt.size(triggerIcon.width * Screen.devicePixelRatio, triggerIcon.height * Screen.devicePixelRatio)
293 }
294 }
295
296 // GoTo Location forward flight circle visuals
297 GeoMapCircleVisuals {
298 id: fwdFlightGotoMapCircle
299 objectName: "flyViewGeoFwdFlightGotoCircle"
300 scene: root.scene
301 surfaceModel: root.surfaceModel
302 mapCircle: _fwdFlightGotoCircleModel
303 radiusLabelVisible: true
304 // Only the radius is editable, not the position
305 centerDragHandleVisible: false
306 // Loiter happens at the goto altitude (carried in the center coordinate)
307 altitudeMode: !isNaN(_fwdFlightGotoCircleModel.center.altitude) ? GeoMapItem.Absolute : GeoMapItem.ClampToGround
308 // PX4 ignores the commanded loiter radius (flies NAV_LOITER_RAD), so the circle size is unknown
309 visible: gotoLocationItem.visible && root._activeVehicle &&
310 !root._activeVehicle.px4Firmware &&
311 root._activeVehicle.inFwdFlight &&
312 !root._activeVehicle.orbitActive
313
314 property alias coordinate: _fwdFlightGotoCircleModel.center
315 property alias radius: _fwdFlightGotoCircleModel.radius
316 property alias clockwiseRotation: _fwdFlightGotoCircleModel.clockwiseRotation
317
318 Component.onCompleted: globals.guidedControllerFlyView.fwdFlightGotoMapCircle = this
319
320 Binding {
321 target: _fwdFlightGotoCircleModel
322 property: "center"
323 value: gotoLocationItem.coordinate
324 }
325
326 function startLoiterRadiusEdit() {
327 _fwdFlightGotoCircleModel.interactive = true
328 }
329
330 // Called when loiter edit is confirmed
331 function actionConfirmed() {
332 _fwdFlightGotoCircleModel.interactive = false
333 _fwdFlightGotoCircleModel._commitRadius()
334 }
335
336 // Called when loiter edit is cancelled
337 function actionCancelled() {
338 _fwdFlightGotoCircleModel.interactive = false
339 _fwdFlightGotoCircleModel._restoreRadius()
340 }
341
342 QGCMapCircle {
343 id: _fwdFlightGotoCircleModel
344 interactive: false
345 showRotation: true
346 clockwiseRotation: true
347
348 property real _defaultLoiterRadius: root._flyViewSettings.forwardFlightGoToLocationLoiterRad.rawValue
349 property real _committedRadius
350
351 onCenterChanged: {
352 // Don't commit the radius in case this operation is undone
353 radius.rawValue = _defaultLoiterRadius
354 }
355
356 Component.onCompleted: {
357 radius.rawValue = _defaultLoiterRadius
358 _commitRadius()
359 }
360
361 function _commitRadius() {
362 _committedRadius = radius.rawValue
363 }
364
365 function _restoreRadius() {
366 radius.rawValue = _committedRadius
367 }
368 }
369 }
370
371 // GoTo Location visuals
372 GeoMapMissionLabel {
373 id: gotoLocationItem
374 objectName: "flyViewGeoGotoLocation"
375 scene: root.scene
376 surfaceModel: root.surfaceModel
377 visible: false
378 checked: true
379 label: qsTr("Go here", "Go to location waypoint")
380 // Goto holds the vehicle's current altitude, so render there in 3D
381 altitudeMode: isNaN(coordinate.altitude) ? GeoMapItem.ClampToGround : GeoMapItem.Absolute
382
383 readonly property bool inGotoFlightMode: root._activeVehicle ? root._activeVehicle.flightMode === root._activeVehicle.gotoFlightMode : false
384
385 property var _committedCoordinate: null
386
387 onInGotoFlightModeChanged: {
388 if (!inGotoFlightMode && gotoLocationItem.visible) {
389 // Hide goto indicator when vehicle falls out of guided mode
390 hide()
391 }
392 }
393
394 function show(coord) {
395 gotoLocationItem.coordinate = root._withVehicleAltitude(coord)
396 gotoLocationItem.visible = true
397 }
398
399 function hide() {
400 gotoLocationItem.visible = false
401 }
402
403 function actionConfirmed() {
404 _commitCoordinate()
405
406 // Commit the new radius which possibly changed
407 fwdFlightGotoMapCircle.actionConfirmed()
408
409 // We leave the indicator visible. The handling for onInGuidedModeChanged will hide it.
410 }
411
412 function actionCancelled() {
413 _restoreCoordinate()
414
415 // Also restore the loiter radius
416 fwdFlightGotoMapCircle.actionCancelled()
417 }
418
419 function _commitCoordinate() {
420 // Must deep copy
421 _committedCoordinate = QtPositioning.coordinate(coordinate.latitude, coordinate.longitude, coordinate.altitude)
422 }
423
424 function _restoreCoordinate() {
425 if (_committedCoordinate) {
426 coordinate = _committedCoordinate
427 } else {
428 hide()
429 }
430 }
431 }
432
433 // Orbit editing visuals
434 GeoMapCircleVisuals {
435 id: orbitMapCircle
436 objectName: "flyViewGeoOrbitCircle"
437 scene: root.scene
438 surfaceModel: root.surfaceModel
439 mapCircle: _orbitCircleModel
440 altitudeMode: !isNaN(_orbitCircleModel.center.altitude) ? GeoMapItem.Absolute : GeoMapItem.ClampToGround
441 visible: false
442
443 property alias center: _orbitCircleModel.center
444 property alias clockwiseRotation: _orbitCircleModel.clockwiseRotation
445 readonly property real defaultRadius: 30
446
447 Connections {
448 target: QGroundControl.multiVehicleManager
449 function onActiveVehicleChanged(activeVehicle) {
450 if (!activeVehicle) {
451 orbitMapCircle.visible = false
452 }
453 }
454 }
455
456 function show(coord) {
457 _orbitCircleModel.radius.rawValue = defaultRadius
458 _orbitCircleModel.center = root._withVehicleAltitude(coord)
459 orbitMapCircle.visible = true
460 }
461
462 function hide() {
463 orbitMapCircle.visible = false
464 }
465
466 function actionConfirmed() {
467 // Live orbit status is handled by telemetry so we hide here and telemetry will show again.
468 hide()
469 }
470
471 function actionCancelled() {
472 hide()
473 }
474
475 function radius() {
476 return _orbitCircleModel.radius.rawValue
477 }
478
479 Component.onCompleted: globals.guidedControllerFlyView.orbitMapCircle = orbitMapCircle
480
481 QGCMapCircle {
482 id: _orbitCircleModel
483 interactive: true
484 radius.rawValue: 30
485 showRotation: true
486 clockwiseRotation: true
487 }
488 }
489
490 // ROI Location visuals
491 GeoMapMissionLabel {
492 id: roiLocationItem
493 scene: root.scene
494 surfaceModel: root.surfaceModel
495 visible: root._activeVehicle && root._activeVehicle.isROIEnabled
496 checked: true
497 label: qsTr("ROI here", "Make this a Region Of Interest")
498
499 // The vehicle reports the ROI center as lat/lon only; the commanded
500 // height lives in roiRelativeAltitudeMeters (relative to home).
501 // Render at that height, DEM bias-corrected like the mission markers.
502 property var _roiCenter: QtPositioning.coordinate()
503
504 // Picked coordinates carry altitude 0 (worldToGeo), so isNaN(altitude)
505 // cannot detect the missing-composition case; ground-clamp on this
506 readonly property bool _haveComposedAltitude: _roiCenter.isValid && root._activeVehicle
507 && root._activeVehicle.homePosition.isValid
508 && !isNaN(root._activeVehicle.homePosition.altitude)
509
510 // Re-check the vehicle here: _haveComposedAltitude can be stale-true
511 // during binding re-evaluation when the active vehicle goes null
512 coordinate: (_haveComposedAltitude && root._activeVehicle)
513 ? QtPositioning.coordinate(_roiCenter.latitude, _roiCenter.longitude,
514 root._activeVehicle.homePosition.altitude
515 + root._activeVehicle.roiRelativeAltitudeMeters
516 + root._activeVehicleHomeTerrainBias)
517 : _roiCenter
518 altitudeMode: _haveComposedAltitude ? GeoMapItem.Absolute : GeoMapItem.ClampToGround
519
520 Connections {
521 target: root._activeVehicle
522 function onRoiCoordChanged(centerCoord) {
523 roiLocationItem._roiCenter = centerCoord
524 }
525 }
526
527 MouseArea {
528 anchors.fill: parent
529 onClicked: (position) => {
530 const windowPosition = mapToItem(globals.parent, position.x, position.y)
531 const dropPanel = roiEditDropPanelComponent.createObject(mainWindow, { clickRect: Qt.rect(windowPosition.x, windowPosition.y, 0, 0) })
532 dropPanel.open()
533 }
534 }
535 }
536
537 // Orbit telemetry visuals. ORBIT_EXECUTION_STATUS carries no altitude, so
538 // render at the vehicle's altitude (the vehicle flies on this ring)
539 GeoMapCircle {
540 id: orbitTelemetryCircle
541 scene: root.scene
542 surfaceModel: root.surfaceModel
543 center: root._activeVehicle ? root._withFuzzyVehicleAltitude(root._activeVehicle.orbitMapCircle.center) : QtPositioning.coordinate()
544 radiusMeters: root._activeVehicle ? root._activeVehicle.orbitMapCircle.radius.rawValue : 0
545 altitudeMode: (center && !isNaN(center.altitude)) ? GeoMapItem.Absolute : GeoMapItem.ClampToGround
546 visible: root._activeVehicle ? root._activeVehicle.orbitActive : false
547 }
548
549 GeoMapMissionLabel {
550 id: orbitCenterIndicator
551 scene: root.scene
552 surfaceModel: root.surfaceModel
553 coordinate: root._activeVehicle ? root._withFuzzyVehicleAltitude(root._activeVehicle.orbitMapCircle.center) : QtPositioning.coordinate()
554 altitudeMode: isNaN(coordinate.altitude) ? GeoMapItem.ClampToGround : GeoMapItem.Absolute
555 visible: orbitTelemetryCircle.visible && !gotoLocationItem.visible
556 checked: true
557 label: qsTr("Orbit", "Orbit waypoint")
558 }
559
560 QGCPopupDialogFactory {
561 id: roiEditPositionDialogFactory
562
563 dialogComponent: roiEditPositionDialogComponent
564 }
565
566 Component {
567 id: roiEditPositionDialogComponent
568
569 EditPositionDialog {
570 // Edit the raw reported center, not the composed display coordinate:
571 // that binding recomputes on home/terrain-bias updates, which would
572 // fire onCoordinateChanged and re-command the ROI while editing
573 title: qsTr("Edit ROI Position")
574 coordinate: roiLocationItem._roiCenter
575
576 readonly property var _activeVehicle: QGroundControl.multiVehicleManager.activeVehicle
577 property var _openedForVehicle: null
578
579 Component.onCompleted: _openedForVehicle = _activeVehicle
580
581 // The ROI belongs to the vehicle the dialog was opened for; close
582 // if that vehicle goes away or the active vehicle changes
583 on_ActiveVehicleChanged: close()
584
585 onCoordinateChanged: {
586 // Guards the init-time change and the vehicle-switch race, where
587 // the rebound coordinate could command the new vehicle with the
588 // old vehicle's ROI before the close() handler runs
589 if (!_openedForVehicle || (_activeVehicle !== _openedForVehicle)) {
590 return
591 }
592 roiLocationItem._roiCenter = coordinate
593 _openedForVehicle.guidedModeROI(coordinate, _openedForVehicle.roiRelativeAltitudeMeters)
594 }
595 }
596 }
597
598 Component {
599 id: roiEditDropPanelComponent
600
601 DropPanel {
602 id: roiEditDropPanel
603
604 readonly property var _activeVehicle: QGroundControl.multiVehicleManager.activeVehicle
605
606 // The ROI belongs to the vehicle the panel was opened for; close
607 // if that vehicle goes away or the active vehicle changes
608 on_ActiveVehicleChanged: close()
609
610 onClosed: destroy()
611
612 sourceComponent: Component {
613 ColumnLayout {
614 spacing: ScreenTools.defaultFontPixelWidth / 2
615
616 QGCButton {
617 Layout.fillWidth: true
618 text: qsTr("Cancel ROI")
619 onClicked: {
620 root._activeVehicle.stopGuidedModeROI()
621 roiEditDropPanel.close()
622 }
623 }
624
625 QGCButton {
626 Layout.fillWidth: true
627 text: qsTr("Edit Position")
628 onClicked: {
629 roiEditPositionDialogFactory.open()
630 roiEditDropPanel.close()
631 }
632 }
633 }
634 }
635 }
636 }
637}