QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
GeoMap.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 QtQuick3D
12import QtQuick3D.Helpers
13import QtPositioning
14
15import QGroundControl
16import QGroundControl.Controls
17import QGroundControl.GeoMap
18
19/// Experimental 2D/3D map control (preview feature): the GeoMap-engine
20/// counterpart of FlightMap. Renders the LOD surface patch quadtree with full
21/// camera gestures (pan, orbit, zoom, twist) and no view chrome.
22Item {
23 id: root
24
25 // Keep map item overlays (GeoMapItem) inside the viewport, matching
26 // QtLocation Map behavior
27 clip: true
28
29 property alias camera: geoCamera
30
31 // Auto-centering (parity with FlightMap): one-shot GCS/vehicle centering
32 // plus vehicle following, all decided by the shared MapPositionTracker
33 property alias allowGCSLocationCenter: _positionTracker.allowGCSLocationCenter
34 property alias allowVehicleLocationCenter: _positionTracker.allowVehicleLocationCenter
35 property alias keepVehicleCentered: _positionTracker.keepVehicleCentered
36 property alias positionTracker: _positionTracker
37
38 // Live SurfaceModel stats for debug overlays
39 property alias patchCount: patchModel.patchCount
40 property alias pendingCount: patchModel.pendingCount
41 property alias maxZoomLevel: patchModel.maxZoomLevel
42 property alias modelStats: patchModel.statsText
43 property alias perfCapturing: patchModel.capturing
44
45 function startPerfCapture() {
46 patchModel.startCapture()
47 }
48
49 // Returns the CSV file path (empty on failure)
50 function stopPerfCapture() {
51 return patchModel.stopCapture()
52 }
53
54 readonly property int cameraAnimationMs: 500
55
56 readonly property var _activeVehicle: QGroundControl.multiVehicleManager.activeVehicle
57 readonly property var _activeVehicleCoordinate: _activeVehicle ? _activeVehicle.coordinate : QtPositioning.coordinate()
58 // Vehicle items render at coordinate.altitude + home terrain bias (see
59 // GeoMapVehicleItem): inset-follow must track that same rendered point or
60 // the recenter target is vertically off by the bias under a tilted camera
61 readonly property var _trackedVehicleCoordinate: QtPositioning.coordinate(_activeVehicleCoordinate.latitude,
62 _activeVehicleCoordinate.longitude,
63 _activeVehicleCoordinate.altitude + _homeTerrainBias)
64
65 // DEM height minus vehicle-reported AMSL, sampled at home (same math as
66 // GeoMapVehicleItem). terrainHeightAt is not a binding dependency, so
67 // re-sampled explicitly below as terrain tiles arrive and when home moves.
68 property real _homeTerrainBias: 0
69
70 function _updateHomeTerrainBias() {
71 if (_activeVehicle && _activeVehicle.homePosition.isValid && !isNaN(_activeVehicle.homePosition.altitude)) {
72 _homeTerrainBias = patchModel.terrainHeightAt(_activeVehicle.homePosition) - _activeVehicle.homePosition.altitude
73 } else {
74 _homeTerrainBias = 0
75 }
76 }
77
78 on_ActiveVehicleChanged: _updateHomeTerrainBias()
79
80 // Render-statistics overlay (FPS, frame timing, draw calls, texture/mesh
81 // memory) for perf work; owned here because it needs the internal View3D
82 property bool renderStats: false
83
84 // Terrain displacement factor (see GeoScene.terrainScale)
85 property alias terrainScale: geoScene.terrainScale
86
87 // Scene wiring for map items (GeoMapItem consumers)
88 readonly property GeoScene scene: geoScene
89 readonly property SurfacePatchModel surfaceModel: patchModel
90
91 function analyzeSurface() {
92 patchModel.analyzeSurface()
93 }
94
95 // Compass reset: animate heading back to north
96 function animateHeadingToNorth() {
97 headingAnimation.stop()
98 // Wrap through 360 for the short way home; the setter normalizes the
99 // final value back to 0
100 headingAnimation.to = (geoCamera.heading > 180) ? 360 : 0
101 headingAnimation.start()
102 }
103
104 // LOD checker colors for debug mode (imagery off)
105 function _lodColor(centerX, centerY, span, zoomLevel) {
106 const parity = (Math.round(centerX / span) + Math.round(centerY / span)) & 1
107 return Qt.hsla((zoomLevel * 0.13) % 1.0, 0.5, parity ? 0.45 : 0.6, 1.0)
108 }
109
110 // Gestures jump a running mode transition to its end state rather
111 // than stranding the pose mid-transition (2D mode locks tilt, so a
112 // half-finished 3D->2D tilt could never recover by gesture)
113 function completeCameraAnimations() {
114 tiltAnimation.complete()
115 headingAnimation.complete()
116 terrainAnimation.complete()
117 // A recenter jumped to its end would fight the starting gesture; drop it
118 recenterAnimation.stop()
119 }
120
121 // Keep the camera's look-at point riding the rendered surface: without
122 // this the orbit center stays at z=0 and a close-zoom 2D->3D switch over
123 // high terrain puts the camera under the mesh (blank view)
124 function _updateCenterElevation() {
125 geoCamera.centerElevation = patchModel.terrainHeightAt(geoCamera.center)
126 * geoScene.verticalScale * geoScene.terrainScale
127 }
128
129 GeoMapCamera {
130 id: geoCamera
131 objectName: "geoMapCamera"
132 viewportSize: Qt.size(root.width, root.height)
133 sceneOrigin: geoScene.sceneOrigin
134 // Startup pose: overhead view of a land area with terrain relief so
135 // imagery and elevation are visually verifiable (home-position
136 // integration comes later). Until this center applies, the camera is
137 // unpositioned and SurfaceModel builds nothing (see
138 // GeoMapCamera::isPositioned), so the null-island default pose never
139 // triggers doomed terrain fetches at (0,0).
140 center: QtPositioning.coordinate(47.6329078, -122.0876875)
141 }
142
143 GeoScene {
144 id: geoScene
145 objectName: "geoMapScene"
146 camera: geoCamera
147 content3D: mapContent3D
148 }
149
150 SurfacePatchModel {
151 id: patchModel
152 objectName: "geoMapPatchModel"
153 scene: geoScene
154 terrain: true
155 statsEnabled: root.renderStats
156 // Drape the map imagery the rest of QGC uses (empty disables imagery)
157 mapType: QGroundControl.settingsManager.flightMapSettings.mapProvider.rawValue
158 + " " + QGroundControl.settingsManager.flightMapSettings.mapType.rawValue
159 }
160
161 MapPositionTracker {
162 id: _positionTracker
163
164 gcsPosition: QGroundControl.qgcPositionManger.gcsPosition
165 vehicleCoordinate: root._activeVehicleCoordinate
166 centerGCSWhenVehicleValid: QGroundControl.settingsManager.flyViewSettings.keepMapCenteredOnVehicle.rawValue
167 userInteracting: panHandler.active || orbitHandler.active || pinchHandler.active
168 animating: recenterAnimation.running
169
170 onCenterMap: (coordinate, firstPosition) => {
171 recenterAnimation.stop()
172 geoCamera.center = coordinate
173 if (firstPosition) {
174 geoCamera.distance = geoCamera.distanceForZoomLevel(QGroundControl.flightMapInitialZoom)
175 }
176 }
177
178 onRecenterVehicleTo: (screenPoint) => {
179 recenterAnimation.stop()
180 recenterAnimation.from = geoCamera.center
181 // The solve assumes the camera pivot elevation stays put, but the
182 // pivot rides the terrain (_updateCenterElevation): re-solve at
183 // the elevation the pivot will settle to at the destination
184 let target = geoScene.centerForCoordinateAtScreenPoint(root._trackedVehicleCoordinate, screenPoint)
185 target = geoScene.centerForCoordinateAtScreenPoint(root._trackedVehicleCoordinate, screenPoint,
186 patchModel.terrainHeightAt(target))
187 recenterAnimation.to = target
188 recenterAnimation.start()
189 }
190 }
191
192 // Inset-follow evaluation: no view chrome here yet, so the unobstructed
193 // center rect is the full viewport and there are no corner rects
194 Timer {
195 interval: 500
196 running: root.visible
197 repeat: true
198 onTriggered: {
199 const screenPos = geoScene.screenPositionFor(root._trackedVehicleCoordinate)
200 // Unprojectable (behind camera) counts as off-screen: recenter
201 const vehiclePoint = (screenPos === undefined) ? Qt.point(-1, -1) : screenPos
202 _positionTracker.evaluateInsetFollow(vehiclePoint, Qt.rect(0, 0, root.width, root.height), [])
203 }
204 }
205
206 View3D {
207 id: viewport
208 objectName: "geoMapViewport"
209 anchors.fill: parent
210
211 environment: SceneEnvironment {
212 clearColor: "#1a2028"
213 backgroundMode: SceneEnvironment.Color
214 // Distance haze toward the background color hides the far-field
215 // LOD rings at the patch range edge
216 fog: Fog {
217 enabled: true
218 color: "#1a2028"
219 depthEnabled: true
220 depthNear: 10 * geoCamera.distance
221 depthFar: (patchModel.maxRangeMultiplier + 1) * geoCamera.distance
222 }
223 }
224
225 DirectionalLight {
226 eulerRotation.x: -60
227 ambientColor: Qt.rgba(0.4, 0.4, 0.4, 1.0)
228 }
229
230 // Scene camera fully driven by the GeoMapCamera pose. Clip planes
231 // scale with distance so the surface stays visible from close-up
232 // through whole-earth views.
233 PerspectiveCamera {
234 id: sceneCamera
235 objectName: "geoMapSceneCamera"
236 position: geoCamera.scenePosition
237 rotation: geoCamera.sceneRotation
238 fieldOfView: geoCamera.fieldOfView
239 clipNear: Math.max(1, geoCamera.distance / 1000)
240 // Ray length to the farthest retained ground point is at most
241 // maxRange + camera height <= (maxRangeMultiplier + 1) * distance
242 clipFar: Math.max(100000, geoCamera.distance * (patchModel.maxRangeMultiplier + 1))
243 }
244
245 // One surface patch per active SurfaceModel entry: draped tile
246 // imagery once delivered, LOD-colored fallback while loading.
247 // The shared z-scale converts true-meter heights into mercator scene
248 // units (verticalScale) and animates terrain flat<->full during the
249 // 2D/3D mode transition (terrainScale) without geometry rebuilds:
250 // one binding here instead of one per delegate (delegate creation
251 // cost dominates patch churn, and transitions re-evaluated it N
252 // times per frame). Never exactly 0: a singular scale breaks the
253 // normal matrix.
254 Node {
255 scale: Qt.vector3d(1, 1, Math.max(0.0001, geoScene.terrainScale * geoScene.verticalScale))
256
257 Repeater3D {
258 model: patchModel
259 delegate: Model {
260 id: patchDelegate
261 objectName: "geoMapPatchDelegate"
262 required property real centerX
263 required property real centerY
264 required property real span
265 required property int zoomLevel
266 required property var heights
267 required property bool covered
268 required property var edgeLodDeltas
269 required property var tileImage
270 required property bool hasTileImage
271
272 // A covered pending patch stays hidden: its empty flat grid
273 // would z-fight the retained retiring cover
274 visible: !covered
275 position: Qt.vector3d(centerX, centerY, 0)
276 geometry: PatchGeometry {
277 gridSize: patchModel.gridSize
278 span: patchDelegate.span
279 heights: patchDelegate.heights
280 edgeLodDeltas: patchDelegate.edgeLodDeltas
281 }
282
283 Texture {
284 id: patchTexture
285 // Clamp: default Repeat wrap bleeds opposite-edge texels
286 // at UV 0/1, drawing 1-px seams on every patch border
287 tilingModeHorizontal: Texture.ClampToEdge
288 tilingModeVertical: Texture.ClampToEdge
289 textureData: PatchTextureData {
290 image: patchDelegate.tileImage
291 }
292 }
293
294 materials: [
295 DefaultMaterial {
296 cullMode: Material.NoCulling
297 // Unlit: tiles carry their own shading, and lighting
298 // would show the per-patch normal seams
299 lighting: DefaultMaterial.NoLighting
300 diffuseMap: patchDelegate.hasTileImage ? patchTexture : null
301 // Loading fallback: quiet neutral when imagery is on
302 // (LOD flash on zoom); LOD checker colors in debug mode
303 diffuseColor: patchDelegate.hasTileImage
304 ? "white"
305 : (patchModel.mapType !== ""
306 ? "#3a4048"
307 : root._lodColor(patchDelegate.centerX, patchDelegate.centerY,
308 patchDelegate.span, patchDelegate.zoomLevel))
309 }
310 ]
311 }
312 }
313 }
314
315 // Map item 3D content (GeoMapItem delegate3D instances). Sibling of
316 // the terrain node: items pre-scale their own z, and the terrain
317 // z-scale would squash models during the 2D/3D transition.
318 Node {
319 id: mapContent3D
320 }
321
322 // Gestures (Viewer3D semantics): the ground point under the cursor
323 // at gesture start stays under the cursor throughout. Any gesture
324 // completes a running mode/compass animation to its end state so
325 // the two never fight over the same pose properties.
326 DragHandler {
327 id: panHandler
328 target: null
329 acceptedButtons: Qt.LeftButton
330 onActiveChanged: {
331 if (active) {
332 root.completeCameraAnimations()
333 // Anchor at the current position, not pressPosition: after a
334 // two-finger pinch drops to one finger this handler re-activates
335 // with a stale pressPosition, and anchoring there jumps the map.
336 geoCamera.beginPan(centroid.position)
337 }
338 }
339 onCentroidChanged: {
340 if (active) {
341 geoCamera.panTo(centroid.position)
342 }
343 }
344 }
345
346 // Right-drag orbits: full width = 360 deg heading, full height = 180 deg tilt.
347 // Mouse/touchpad only: acceptedButtons doesn't filter touch points, so
348 // without acceptedDevices this handler steals single-finger drags from
349 // panHandler on touchscreens (touch orbits via PinchHandler twist instead).
350 // TouchPad keeps trackpad secondary-click drag working.
351 DragHandler {
352 id: orbitHandler
353 target: null
354 acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
355 acceptedButtons: Qt.RightButton
356 onActiveChanged: {
357 if (active) {
358 root.completeCameraAnimations()
359 geoCamera.beginOrbit(centroid.pressPosition)
360 }
361 }
362 onCentroidChanged: {
363 if (active) {
364 geoCamera.orbitTo(centroid.position)
365 }
366 }
367 }
368
369 WheelHandler {
370 target: null
371 onWheel: (event) => {
372 root.completeCameraAnimations()
373 geoCamera.zoom(event.angleDelta.y, point.position)
374 }
375 }
376
377 // Pinch zooms about the centroid; two-finger twist rotates around it
378 // (handler rotation is positive clockwise on screen; heading follows
379 // so the world tracks the fingers)
380 PinchHandler {
381 id: pinchHandler
382 target: null
383 onActiveChanged: {
384 if (active) {
385 root.completeCameraAnimations()
386 }
387 }
388 onScaleChanged: (delta) => geoCamera.zoomBy(1 / delta, centroid.position)
389 onRotationChanged: (delta) => geoCamera.rotateBy(delta, centroid.position)
390 }
391 }
392
393 /// Ground station location (parity with the FlightMap marker)
394 GeoMapItem {
395 id: gcsIndicator
396
397 scene: geoScene
398 surfaceModel: patchModel
399 coordinate: gcsIndicator._gcsPosition
400 anchorPoint: Qt.point(gcsImage.width / 2, gcsImage.height / 2)
401 width: gcsImage.width
402 height: gcsImage.height
403 visible: gcsIndicator._gcsPosition.isValid
404
405 readonly property var _gcsPosition: QGroundControl.qgcPositionManger.gcsPosition
406 readonly property real _gcsHeading: QGroundControl.qgcPositionManger.gcsHeading
407
408 Image {
409 id: gcsImage
410 source: isNaN(gcsIndicator._gcsHeading) ? "/res/QGCLogoFull.svg" : "/res/QGCLogoArrow.svg"
411 mipmap: true
412 antialiasing: true
413 fillMode: Image.PreserveAspectFit
414 height: ScreenTools.defaultFontPixelHeight * (isNaN(gcsIndicator._gcsHeading) ? 1.75 : 2.5)
415 sourceSize.height: height
416 transform: Rotation {
417 origin.x: gcsImage.width / 2
418 origin.y: gcsImage.height / 2
419 // Camera heading rotates map north on screen; the arrow follows
420 angle: isNaN(gcsIndicator._gcsHeading) ? 0 : gcsIndicator._gcsHeading + geoCamera.heading
421 }
422 }
423 }
424
425 DebugView {
426 objectName: "geoMapRenderStats"
427 anchors.left: parent.left
428 anchors.top: parent.top
429 source: viewport
430 visible: root.renderStats
431 resourceDetailsVisible: true
432 }
433
434 NumberAnimation {
435 id: tiltAnimation
436 target: geoCamera
437 property: "tilt"
438 duration: root.cameraAnimationMs
439 easing.type: Easing.InOutQuad
440 }
441
442 NumberAnimation {
443 id: headingAnimation
444 target: geoCamera
445 property: "heading"
446 duration: root.cameraAnimationMs
447 easing.type: Easing.InOutQuad
448 }
449
450 NumberAnimation {
451 id: terrainAnimation
452 target: geoScene
453 property: "terrainScale"
454 duration: root.cameraAnimationMs
455 easing.type: Easing.InOutQuad
456 }
457
458 CoordinateAnimation {
459 id: recenterAnimation
460 target: geoCamera
461 property: "center"
462 duration: 1000
463 easing.type: Easing.InOutQuad
464 }
465
466 // Single transition path for every mode writer (button, goTo2D/3D,
467 // C++): mode changes always animate tilt and terrain to the mode's
468 // targets, so camera state and terrain displacement cannot desync
469 Connections {
470 target: geoCamera
471 function onModeChanged() {
472 const to3D = geoCamera.mode === GeoMapCamera.Mode3D
473 tiltAnimation.stop()
474 tiltAnimation.to = to3D ? geoCamera.default3DTilt : 0
475 tiltAnimation.start()
476 terrainAnimation.stop()
477 terrainAnimation.to = to3D ? 1 : 0
478 terrainAnimation.start()
479 }
480 function onCenterChanged() {
481 root._updateCenterElevation()
482 }
483 }
484
485 Connections {
486 target: patchModel
487 function onTerrainHeightsChanged() {
488 root._updateCenterElevation()
489 root._updateHomeTerrainBias()
490 }
491 }
492
493 Connections {
494 target: geoScene
495 function onTerrainScaleChanged() {
496 root._updateCenterElevation()
497 }
498 // verticalScale depends on the scene origin
499 function onSceneOriginChanged() {
500 root._updateCenterElevation()
501 }
502 }
503
504 Connections {
505 target: root._activeVehicle
506 function onHomePositionChanged() {
507 root._updateHomeTerrainBias()
508 }
509 }
510}