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 ****************************************************************************/
15import QGroundControl.Controls
16import QGroundControl.FlightMap
17import QGroundControl.GeoMap
19/// Fly-view 2D/3D map: the GeoMap-engine counterpart of FlyViewMap.
20/// Fly-view specific map content (vehicle items, trajectory, ...) lands here.
24 allowGCSLocationCenter: true
25 allowVehicleLocationCenter: true
26 keepVehicleCentered: QGroundControl.settingsManager.flyViewSettings.keepMapCenteredOnVehicle.rawValue
28 // Guided-action indicators for the map click drop panel (FlyViewMapClickDropPanel)
29 readonly property var gotoIndicator: gotoLocationItem
30 readonly property var orbitIndicator: orbitMapCircle
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
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
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
50 root._activeVehicleHomeTerrainBias = 0
54 on_ActiveVehicleChanged: {
55 _updateActiveVehicleHomeTerrainBias()
56 _fuzzyVehicleDisplayAltitude = NaN
57 _updateFuzzyVehicleDisplayAltitude()
59 onSurfaceModelChanged: _updateActiveVehicleHomeTerrainBias()
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
66 readonly property real _fuzzyAltitudeDeadbandMeters: 1
68 function _updateFuzzyVehicleDisplayAltitude() {
69 if (!root._activeVehicle || isNaN(root._activeVehicle.coordinate.altitude)) {
70 root._fuzzyVehicleDisplayAltitude = NaN
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
79 on_ActiveVehicleHomeTerrainBiasChanged: _updateFuzzyVehicleDisplayAltitude()
81 function _withFuzzyVehicleAltitude(coord) {
82 if (!isNaN(root._fuzzyVehicleDisplayAltitude)) {
83 return QtPositioning.coordinate(coord.latitude, coord.longitude, root._fuzzyVehicleDisplayAltitude)
85 return QtPositioning.coordinate(coord.latitude, coord.longitude)
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)
95 return QtPositioning.coordinate(coord.latitude, coord.longitude)
98 property var _missionItemSelectionPanel: null
100 function _confirmSetWaypoint(sequenceNumber) {
101 globals.guidedControllerFlyView.confirmAction(globals.guidedControllerFlyView.actionSetWaypoint,
102 Math.max(sequenceNumber, 1))
105 function _closeMissionItemSelectionPanel() {
106 if (_missionItemSelectionPanel) {
107 _missionItemSelectionPanel.close()
112 target: root.surfaceModel
114 function onTerrainDataChanged() {
115 root._updateActiveVehicleHomeTerrainBias()
120 target: root._activeVehicle
122 function onHomePositionChanged() {
123 root._updateActiveVehicleHomeTerrainBias()
126 function onCoordinateChanged() {
127 root._updateFuzzyVehicleDisplayAltitude()
131 // Vehicle home (launch) location
134 surfaceModel: root.surfaceModel
135 coordinate: root._activeVehicle ? root._activeVehicle.homePosition : QtPositioning.coordinate()
136 visible: root._activeVehicle && root._activeVehicle.homePosition.isValid
141 model: QGroundControl.multiVehicleManager.vehicles
145 surfaceModel: root.surfaceModel
152 surfaceModel: root.surfaceModel
153 vehicle: root._activeVehicle
158 surfaceModel: root.surfaceModel
159 missionController: root._missionController
160 vehicle: root._activeVehicle
161 homeTerrainBias: root._activeVehicleHomeTerrainBias
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),
179 if (root._missionItemSelectionPanel) {
180 root._missionItemSelectionPanel.open()
183 onGroupsInvalidated: root._closeMissionItemSelectionPanel()
186 // The picker lists the old mission's items: close it when the mission is replaced
188 target: root._missionController
190 function onVisualItemsReset() { root._closeMissionItemSelectionPanel() }
194 id: missionItemSelectionPanelComponent
196 MissionItemSelectionPanel {
197 id: missionItemSelectionPanel
199 onItemSelected: (sequenceNumber) => root._confirmSetWaypoint(sequenceNumber)
201 if (root._missionItemSelectionPanel === missionItemSelectionPanel) {
202 root._missionItemSelectionPanel = null
209 GeoMapMissionDirectionArrows {
211 surfaceModel: root.surfaceModel
212 missionController: root._missionController
213 homeTerrainBias: root._activeVehicleHomeTerrainBias
216 // Allow custom builds to add map items (QGCCorePlugin::customGeoMapItems)
218 model: QGroundControl.corePlugin.customGeoMapItems
221 Component.onCompleted: setSource(object.url, {
222 customMapObject: object,
224 surfaceModel: root.surfaceModel
229 GeoMapGeoFenceVisuals {
231 surfaceModel: root.surfaceModel
232 geoFenceController: root._geoFenceController
233 homePosition: root._activeVehicle && root._activeVehicle.homePosition.isValid ? root._activeVehicle.homePosition : QtPositioning.coordinate()
238 model: root._rallyPointController ? root._rallyPointController.points : undefined
242 surfaceModel: root.surfaceModel
243 coordinate: object.coordinate
244 label: qsTr("R", "rally point map item label")
248 // Proximity sensor arcs per vehicle
250 model: QGroundControl.multiVehicleManager.vehicles
252 FlyViewGeoProximityRadar {
254 surfaceModel: root.surfaceModel
261 model: QGroundControl.adsbVehicleManager.adsbVehicles
263 GeoMapADSBVehicleItem {
265 surfaceModel: root.surfaceModel
266 coordinate: object.coordinate
267 altitude: object.altitude
268 callsign: object.callsign
269 heading: object.heading
271 size: ScreenTools.defaultFontPixelHeight * 2.5
272 homeTerrainBias: root._activeVehicleHomeTerrainBias
276 // Camera trigger points: batched sprite layer (single draw call), since
277 // surveys can produce thousands of points
280 surfaceModel: root.surfaceModel
281 model: root._activeVehicle ? root._activeVehicle.cameraTriggerPoints : null
282 sprite: triggerIconSource
283 spriteSize: triggerIcon.width
285 CameraTriggerIcon { id: triggerIcon }
287 id: triggerIconSource
288 sourceItem: triggerIcon
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)
296 // GoTo Location forward flight circle visuals
297 GeoMapCircleVisuals {
298 id: fwdFlightGotoMapCircle
299 objectName: "flyViewGeoFwdFlightGotoCircle"
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
314 property alias coordinate: _fwdFlightGotoCircleModel.center
315 property alias radius: _fwdFlightGotoCircleModel.radius
316 property alias clockwiseRotation: _fwdFlightGotoCircleModel.clockwiseRotation
318 Component.onCompleted: globals.guidedControllerFlyView.fwdFlightGotoMapCircle = this
321 target: _fwdFlightGotoCircleModel
323 value: gotoLocationItem.coordinate
326 function startLoiterRadiusEdit() {
327 _fwdFlightGotoCircleModel.interactive = true
330 // Called when loiter edit is confirmed
331 function actionConfirmed() {
332 _fwdFlightGotoCircleModel.interactive = false
333 _fwdFlightGotoCircleModel._commitRadius()
336 // Called when loiter edit is cancelled
337 function actionCancelled() {
338 _fwdFlightGotoCircleModel.interactive = false
339 _fwdFlightGotoCircleModel._restoreRadius()
343 id: _fwdFlightGotoCircleModel
346 clockwiseRotation: true
348 property real _defaultLoiterRadius: root._flyViewSettings.forwardFlightGoToLocationLoiterRad.rawValue
349 property real _committedRadius
352 // Don't commit the radius in case this operation is undone
353 radius.rawValue = _defaultLoiterRadius
356 Component.onCompleted: {
357 radius.rawValue = _defaultLoiterRadius
361 function _commitRadius() {
362 _committedRadius = radius.rawValue
365 function _restoreRadius() {
366 radius.rawValue = _committedRadius
371 // GoTo Location visuals
374 objectName: "flyViewGeoGotoLocation"
376 surfaceModel: root.surfaceModel
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
383 readonly property bool inGotoFlightMode: root._activeVehicle ? root._activeVehicle.flightMode === root._activeVehicle.gotoFlightMode : false
385 property var _committedCoordinate: null
387 onInGotoFlightModeChanged: {
388 if (!inGotoFlightMode && gotoLocationItem.visible) {
389 // Hide goto indicator when vehicle falls out of guided mode
394 function show(coord) {
395 gotoLocationItem.coordinate = root._withVehicleAltitude(coord)
396 gotoLocationItem.visible = true
400 gotoLocationItem.visible = false
403 function actionConfirmed() {
406 // Commit the new radius which possibly changed
407 fwdFlightGotoMapCircle.actionConfirmed()
409 // We leave the indicator visible. The handling for onInGuidedModeChanged will hide it.
412 function actionCancelled() {
415 // Also restore the loiter radius
416 fwdFlightGotoMapCircle.actionCancelled()
419 function _commitCoordinate() {
421 _committedCoordinate = QtPositioning.coordinate(coordinate.latitude, coordinate.longitude, coordinate.altitude)
424 function _restoreCoordinate() {
425 if (_committedCoordinate) {
426 coordinate = _committedCoordinate
433 // Orbit editing visuals
434 GeoMapCircleVisuals {
436 objectName: "flyViewGeoOrbitCircle"
438 surfaceModel: root.surfaceModel
439 mapCircle: _orbitCircleModel
440 altitudeMode: !isNaN(_orbitCircleModel.center.altitude) ? GeoMapItem.Absolute : GeoMapItem.ClampToGround
443 property alias center: _orbitCircleModel.center
444 property alias clockwiseRotation: _orbitCircleModel.clockwiseRotation
445 readonly property real defaultRadius: 30
448 target: QGroundControl.multiVehicleManager
449 function onActiveVehicleChanged(activeVehicle) {
450 if (!activeVehicle) {
451 orbitMapCircle.visible = false
456 function show(coord) {
457 _orbitCircleModel.radius.rawValue = defaultRadius
458 _orbitCircleModel.center = root._withVehicleAltitude(coord)
459 orbitMapCircle.visible = true
463 orbitMapCircle.visible = false
466 function actionConfirmed() {
467 // Live orbit status is handled by telemetry so we hide here and telemetry will show again.
471 function actionCancelled() {
476 return _orbitCircleModel.radius.rawValue
479 Component.onCompleted: globals.guidedControllerFlyView.orbitMapCircle = orbitMapCircle
482 id: _orbitCircleModel
486 clockwiseRotation: true
490 // ROI Location visuals
494 surfaceModel: root.surfaceModel
495 visible: root._activeVehicle && root._activeVehicle.isROIEnabled
497 label: qsTr("ROI here", "Make this a Region Of Interest")
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()
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)
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)
518 altitudeMode: _haveComposedAltitude ? GeoMapItem.Absolute : GeoMapItem.ClampToGround
521 target: root._activeVehicle
522 function onRoiCoordChanged(centerCoord) {
523 roiLocationItem._roiCenter = centerCoord
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) })
537 // Orbit telemetry visuals. ORBIT_EXECUTION_STATUS carries no altitude, so
538 // render at the vehicle's altitude (the vehicle flies on this ring)
540 id: orbitTelemetryCircle
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
550 id: orbitCenterIndicator
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
557 label: qsTr("Orbit", "Orbit waypoint")
560 QGCPopupDialogFactory {
561 id: roiEditPositionDialogFactory
563 dialogComponent: roiEditPositionDialogComponent
567 id: roiEditPositionDialogComponent
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
576 readonly property var _activeVehicle: QGroundControl.multiVehicleManager.activeVehicle
577 property var _openedForVehicle: null
579 Component.onCompleted: _openedForVehicle = _activeVehicle
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()
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)) {
592 roiLocationItem._roiCenter = coordinate
593 _openedForVehicle.guidedModeROI(coordinate, _openedForVehicle.roiRelativeAltitudeMeters)
599 id: roiEditDropPanelComponent
604 readonly property var _activeVehicle: QGroundControl.multiVehicleManager.activeVehicle
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()
612 sourceComponent: Component {
614 spacing: ScreenTools.defaultFontPixelWidth / 2
617 Layout.fillWidth: true
618 text: qsTr("Cancel ROI")
620 root._activeVehicle.stopGuidedModeROI()
621 roiEditDropPanel.close()
626 Layout.fillWidth: true
627 text: qsTr("Edit Position")
629 roiEditPositionDialogFactory.open()
630 roiEditDropPanel.close()