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/// Google Earth-style camera gestures (pan, orbit, zoom, twist, first-person
22/// look) and no view chrome.
23Item {
24 id: root
25
26 // Keep map item overlays (GeoMapItem) inside the viewport, matching
27 // QtLocation Map behavior
28 clip: true
29
30 property alias camera: geoCamera
31
32 // Same contract as FlightMap: block pinch-to-zoom while virtual joystick
33 // thumbs are down (issue #13450)
34 property bool pinchZoomDisabledByVirtualJoysticks: false
35
36 // Same contract as FlightMap: plain left-click/tap (no drag, no modifier)
37 // in viewport coordinates; consumers convert via surfaceCoordinateAt
38 signal mapClicked(var position)
39
40 // Geographic coordinate of the rendered terrain surface under a screen
41 // point (viewport pixels): the pick lands on the visible front surface,
42 // so ridges occlude the ground behind them. Works at any tilt, unlike a
43 // ground-plane pick which can land kilometers past elevated terrain.
44 // Invalid coordinate on a sky pick.
45 function surfaceCoordinateAt(screenPos) {
46 return patchModel.surfaceCoordinateAtScreenPoint(geoCamera, screenPos,
47 geoScene.verticalScale * geoScene.terrainScale)
48 }
49
50 // Auto-centering (parity with FlightMap): one-shot GCS/vehicle centering
51 // plus vehicle following, all decided by the shared MapPositionTracker
52 property alias allowGCSLocationCenter: _positionTracker.allowGCSLocationCenter
53 property alias allowVehicleLocationCenter: _positionTracker.allowVehicleLocationCenter
54 property alias keepVehicleCentered: _positionTracker.keepVehicleCentered
55 property alias positionTracker: _positionTracker
56 property rect followViewportRect: Qt.rect(0, 0, width, height) ///< Map area the followed vehicle is kept within
57 property var followOccluders: [] ///< Rects within the viewport covered by host UI
58
59 // Live SurfaceModel stats for debug overlays
60 property alias patchCount: patchModel.patchCount
61 property alias pendingCount: patchModel.pendingCount
62 property alias maxZoomLevel: patchModel.maxZoomLevel
63 property alias modelStats: patchModel.statsText
64 property alias perfCapturing: patchModel.capturing
65
66 function startPerfCapture() {
67 patchModel.startCapture()
68 }
69
70 // Returns the CSV file path (empty on failure)
71 function stopPerfCapture() {
72 return patchModel.stopCapture()
73 }
74
75 readonly property int cameraAnimationMs: 500
76
77 readonly property var _activeVehicle: QGroundControl.multiVehicleManager.activeVehicle
78 readonly property var _activeVehicleCoordinate: _activeVehicle ? _activeVehicle.coordinate : QtPositioning.coordinate()
79 // Vehicle items render at coordinate.altitude + home terrain bias (see
80 // GeoMapVehicleItem): occluder-follow must track that same rendered point or
81 // the recenter target is vertically off by the bias under a tilted camera
82 readonly property var _trackedVehicleCoordinate: QtPositioning.coordinate(_activeVehicleCoordinate.latitude,
83 _activeVehicleCoordinate.longitude,
84 _activeVehicleCoordinate.altitude + _homeTerrainBias)
85
86 // DEM height minus vehicle-reported AMSL, sampled at home (same math as
87 // GeoMapVehicleItem). terrainHeightAt is not a binding dependency, so
88 // re-sampled explicitly below as terrain tiles arrive and when home moves.
89 property real _homeTerrainBias: 0
90
91 function _updateHomeTerrainBias() {
92 if (_activeVehicle && _activeVehicle.homePosition.isValid && !isNaN(_activeVehicle.homePosition.altitude)) {
93 _homeTerrainBias = patchModel.terrainDataHeightAt(_activeVehicle.homePosition) - _activeVehicle.homePosition.altitude
94 } else {
95 _homeTerrainBias = 0
96 }
97 }
98
99 on_ActiveVehicleChanged: _updateHomeTerrainBias()
100
101 // Render-statistics overlay (FPS, frame timing, draw calls, texture/mesh
102 // memory) for perf work; owned here because it needs the internal View3D
103 property bool renderStats: false
104
105 // Terrain displacement factor (see GeoScene.terrainScale)
106 property alias terrainScale: geoScene.terrainScale
107
108 // Scene wiring for map items (GeoMapItem consumers)
109 readonly property GeoScene scene: geoScene
110 readonly property SurfacePatchModel surfaceModel: patchModel
111
112 function analyzeSurface() {
113 patchModel.analyzeSurface()
114 }
115
116 // Compass reset: animate heading back to north
117 function animateHeadingToNorth() {
118 headingAnimation.stop()
119 // Wrap through 360 for the short way home; the setter normalizes the
120 // final value back to 0
121 headingAnimation.to = (geoCamera.heading > 180) ? 360 : 0
122 headingAnimation.start()
123 }
124
125 // Show the whole QGeoRectangle at the current heading/tilt
126 function fitToRegion(region) {
127 completeCameraAnimations()
128 geoCamera.fitToRegion(region)
129 }
130
131 // LOD checker colors for debug mode (imagery off)
132 function _lodColor(centerX, centerY, span, zoomLevel) {
133 const parity = (Math.round(centerX / span) + Math.round(centerY / span)) & 1
134 return Qt.hsla((zoomLevel * 0.13) % 1.0, 0.5, parity ? 0.45 : 0.6, 1.0)
135 }
136
137 // Gestures jump a running mode transition to its end state rather
138 // than stranding the pose mid-transition (2D mode locks tilt, so a
139 // half-finished 3D->2D tilt could never recover by gesture)
140 function completeCameraAnimations() {
141 tiltAnimation.complete()
142 headingAnimation.complete()
143 terrainAnimation.complete()
144 // A recenter jumped to its end would fight the starting gesture; drop it
145 recenterAnimation.stop()
146 }
147
148 // Keep the camera's look-at point riding the terrain data, not the drawn
149 // mesh: the pivot feeds LOD selection, so following patch churn would loop.
150 // Without this the orbit center stays at z=0 and a close-zoom 2D->3D switch
151 // over high terrain puts the camera under the mesh (blank view)
152 function _updateCenterElevation() {
153 geoCamera.centerElevation = patchModel.terrainDataHeightAt(geoCamera.center)
154 * geoScene.verticalScale * geoScene.terrainScale
155 }
156
157 // Scene-space z of the rendered surface under a screen point, so gestures
158 // anchor to the terrain the user actually clicked instead of the z=0
159 // plane far beneath it. Sample at the ground-plane hit, then refine once
160 // at that elevation (same two-step solve as onRecenterVehicleTo).
161 function _surfaceZAt(screenPos) {
162 const coord = geoCamera.coordinateAtScreenPoint(screenPos)
163 if (!coord.isValid) {
164 return 0
165 }
166 const zScale = geoScene.verticalScale * geoScene.terrainScale
167 const z = patchModel.terrainHeightAt(coord) * zScale
168 const refined = geoCamera.coordinateAtScreenPoint(screenPos, z)
169 return refined.isValid ? (patchModel.terrainHeightAt(refined) * zScale) : z
170 }
171
172 GeoMapCamera {
173 id: geoCamera
174 objectName: "geoMapCamera"
175 viewportSize: Qt.size(root.width, root.height)
176 sceneOrigin: geoScene.sceneOrigin
177 // Startup pose: overhead view of a land area with terrain relief so
178 // imagery and elevation are visually verifiable (home-position
179 // integration comes later). Until this center applies, the camera is
180 // unpositioned and SurfaceModel builds nothing (see
181 // GeoMapCamera::isPositioned), so the null-island default pose never
182 // triggers doomed terrain fetches at (0,0).
183 center: QtPositioning.coordinate(47.6329078, -122.0876875)
184 }
185
186 GeoScene {
187 id: geoScene
188 objectName: "geoMapScene"
189 camera: geoCamera
190 content3D: mapContent3D
191 }
192
193 SurfacePatchModel {
194 id: patchModel
195 objectName: "geoMapPatchModel"
196 scene: geoScene
197 terrain: true
198 statsEnabled: root.renderStats
199 // Drape the map imagery the rest of QGC uses (empty disables imagery)
200 mapType: QGroundControl.settingsManager.flightMapSettings.mapProvider.rawValue
201 + " " + QGroundControl.settingsManager.flightMapSettings.mapType.rawValue
202 }
203
204 MapPositionTracker {
205 id: _positionTracker
206
207 gcsPosition: QGroundControl.positionManager.gcsPosition
208 vehicleCoordinate: root._activeVehicleCoordinate
209 centerGCSWhenVehicleValid: QGroundControl.settingsManager.flyViewSettings.keepMapCenteredOnVehicle.rawValue
210 userInteracting: panHandler.active || orbitHandler.active || shiftOrbitHandler.active
211 || lookHandler.active || metaLookHandler.active || pinchHandler.active
212 animating: recenterAnimation.running
213
214 onCenterMap: (coordinate, firstPosition) => {
215 recenterAnimation.stop()
216 geoCamera.center = coordinate
217 if (firstPosition) {
218 geoCamera.distance = geoCamera.distanceForZoomLevel(QGroundControl.flightMapInitialZoom)
219 }
220 }
221
222 onRecenterVehicleTo: (screenPoint) => {
223 recenterAnimation.stop()
224 recenterAnimation.from = geoCamera.center
225 // The solve assumes the camera pivot elevation stays put, but the
226 // pivot rides the terrain (_updateCenterElevation): re-solve at
227 // the elevation the pivot will settle to at the destination
228 let target = geoScene.centerForCoordinateAtScreenPoint(root._trackedVehicleCoordinate, screenPoint)
229 target = geoScene.centerForCoordinateAtScreenPoint(root._trackedVehicleCoordinate, screenPoint,
230 patchModel.terrainDataHeightAt(target))
231 recenterAnimation.to = target
232 recenterAnimation.start()
233 }
234 }
235
236 Timer {
237 interval: 500
238 running: root.visible
239 repeat: true
240 onTriggered: {
241 const screenPos = geoScene.screenPositionFor(root._trackedVehicleCoordinate)
242 // Unprojectable (behind camera) counts as off-screen: recenter
243 const vehiclePoint = (screenPos === undefined) ? Qt.point(-1, -1) : screenPos
244 _positionTracker.evaluateOccluderFollow(vehiclePoint, root.followViewportRect, root.followOccluders)
245 }
246 }
247
248 View3D {
249 id: viewport
250 objectName: "geoMapViewport"
251 anchors.fill: parent
252
253 environment: SceneEnvironment {
254 clearColor: "#1a2028"
255 backgroundMode: SceneEnvironment.Color
256 // Distance haze toward the background color hides the far-field
257 // LOD rings at the patch range edge
258 fog: Fog {
259 enabled: true
260 color: "#1a2028"
261 depthEnabled: true
262 depthNear: 10 * geoCamera.distance
263 depthFar: (patchModel.maxRangeMultiplier + 1) * geoCamera.distance
264 }
265 }
266
267 DirectionalLight {
268 eulerRotation.x: -60
269 ambientColor: Qt.rgba(0.4, 0.4, 0.4, 1.0)
270 }
271
272 // Scene camera fully driven by the GeoMapCamera pose. Clip planes
273 // scale with distance so the surface stays visible from close-up
274 // through whole-earth views.
275 PerspectiveCamera {
276 id: sceneCamera
277 objectName: "geoMapSceneCamera"
278 position: geoCamera.scenePosition
279 rotation: geoCamera.sceneRotation
280 fieldOfView: geoCamera.verticalFieldOfView
281 clipNear: Math.max(1, geoCamera.distance / 1000)
282 // Ray length to the farthest retained ground point is at most
283 // maxRange + camera height <= (maxRangeMultiplier + 1) * distance
284 clipFar: Math.max(100000, geoCamera.distance * (patchModel.maxRangeMultiplier + 1))
285 }
286
287 // One surface patch per active SurfaceModel entry: draped tile
288 // imagery once delivered, LOD-colored fallback while loading.
289 // The shared z-scale converts true-meter heights into mercator scene
290 // units (verticalScale) and animates terrain flat<->full during the
291 // 2D/3D mode transition (terrainScale) without geometry rebuilds:
292 // one binding here instead of one per delegate (delegate creation
293 // cost dominates patch churn, and transitions re-evaluated it N
294 // times per frame). Never exactly 0: a singular scale breaks the
295 // normal matrix.
296 Node {
297 scale: Qt.vector3d(1, 1, Math.max(0.0001, geoScene.terrainScale * geoScene.verticalScale))
298
299 Repeater3D {
300 model: patchModel
301 delegate: Model {
302 id: patchDelegate
303 objectName: "geoMapPatchDelegate"
304 required property real centerX
305 required property real centerY
306 required property real span
307 required property int zoomLevel
308 required property var heights
309 required property bool covered
310 required property var edgeLodDeltas
311 required property var tileImage
312 required property bool hasTileImage
313
314 // A covered pending patch stays hidden: its empty flat grid
315 // would z-fight the retained retiring cover
316 visible: !covered
317 position: Qt.vector3d(centerX, centerY, 0)
318 geometry: PatchGeometry {
319 gridSize: patchModel.gridSize
320 span: patchDelegate.span
321 heights: patchDelegate.heights
322 edgeLodDeltas: patchDelegate.edgeLodDeltas
323 }
324
325 Texture {
326 id: patchTexture
327 // Clamp: default Repeat wrap bleeds opposite-edge texels
328 // at UV 0/1, drawing 1-px seams on every patch border
329 tilingModeHorizontal: Texture.ClampToEdge
330 tilingModeVertical: Texture.ClampToEdge
331 textureData: PatchTextureData {
332 image: patchDelegate.tileImage
333 }
334 }
335
336 materials: [
337 DefaultMaterial {
338 cullMode: Material.NoCulling
339 // Unlit: tiles carry their own shading, and lighting
340 // would show the per-patch normal seams
341 lighting: DefaultMaterial.NoLighting
342 diffuseMap: patchDelegate.hasTileImage ? patchTexture : null
343 // Loading fallback: quiet neutral when imagery is on
344 // (LOD flash on zoom); LOD checker colors in debug mode
345 diffuseColor: patchDelegate.hasTileImage
346 ? "white"
347 : (patchModel.mapType !== ""
348 ? "#3a4048"
349 : root._lodColor(patchDelegate.centerX, patchDelegate.centerY,
350 patchDelegate.span, patchDelegate.zoomLevel))
351 }
352 ]
353 }
354 }
355 }
356
357 // Map item 3D content (GeoMapItem delegate3D instances). Sibling of
358 // the terrain node: items pre-scale their own z, and the terrain
359 // z-scale would squash models during the 2D/3D transition.
360 Node {
361 id: mapContent3D
362 }
363
364 // Gestures (Google Earth semantics): the ground point under the cursor
365 // at gesture start stays under the cursor throughout. Any gesture
366 // completes a running mode/compass animation to its end state so
367 // the two never fight over the same pose properties.
368 DragHandler {
369 id: panHandler
370 target: null
371 acceptedButtons: Qt.LeftButton
372 // Plain drag only: Shift/Ctrl+left-drag are the orbit/look gestures below
373 acceptedModifiers: Qt.NoModifier
374 onActiveChanged: {
375 if (active) {
376 root.completeCameraAnimations()
377 // Anchor at the current position, not pressPosition: after a
378 // two-finger pinch drops to one finger this handler re-activates
379 // with a stale pressPosition, and anchoring there jumps the map.
380 geoCamera.beginPan(centroid.position, root._surfaceZAt(centroid.position))
381 }
382 }
383 onCentroidChanged: {
384 if (active) {
385 geoCamera.panTo(centroid.position)
386 }
387 }
388 }
389
390 // Right/middle-drag orbits about the pressed ground point: full width =
391 // 360 deg heading, full height = 180 deg tilt.
392 // Mouse/touchpad only: acceptedButtons doesn't filter touch points, so
393 // without acceptedDevices this handler steals single-finger drags from
394 // panHandler on touchscreens (touch orbits via PinchHandler twist instead).
395 // TouchPad keeps trackpad secondary-click drag working.
396 DragHandler {
397 id: orbitHandler
398 target: null
399 acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
400 acceptedButtons: Qt.RightButton | Qt.MiddleButton
401 // Unmodified only: macOS synthesizes Ctrl+left-click as a right-click
402 // (carrying MetaModifier), which must fall through to metaLookHandler
403 acceptedModifiers: Qt.NoModifier
404 onActiveChanged: {
405 if (active) {
406 root.completeCameraAnimations()
407 geoCamera.beginOrbit(centroid.pressPosition, root._surfaceZAt(centroid.pressPosition))
408 // Apply motion accumulated before activation: a short drag
409 // can activate and release with no further centroid change
410 geoCamera.orbitTo(centroid.position)
411 }
412 }
413 onCentroidChanged: {
414 if (active) {
415 geoCamera.orbitTo(centroid.position)
416 }
417 }
418 }
419
420 WheelHandler {
421 target: null
422 // Default acceptedDevices=Mouse drops trackpad scroll (and mouse
423 // wheel on Wayland/xcb, which misreport as TouchPad) — see the
424 // FlightMap WheelHandler comment for the full platform rundown
425 acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
426 onWheel: (event) => {
427 root.completeCameraAnimations()
428 geoCamera.zoom(event.angleDelta.y, point.position)
429 }
430 }
431
432 // Shift+left-drag orbits about the pressed ground point, same as
433 // right/middle-drag (keyboard-modifier alternative for one-button mice)
434 DragHandler {
435 id: shiftOrbitHandler
436 target: null
437 acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
438 acceptedButtons: Qt.LeftButton
439 acceptedModifiers: Qt.ShiftModifier
440 onActiveChanged: {
441 if (active) {
442 root.completeCameraAnimations()
443 geoCamera.beginOrbit(centroid.pressPosition, root._surfaceZAt(centroid.pressPosition))
444 // Apply motion accumulated before activation (see orbitHandler)
445 geoCamera.orbitTo(centroid.position)
446 }
447 }
448 onCentroidChanged: {
449 if (active) {
450 geoCamera.orbitTo(centroid.position)
451 }
452 }
453 }
454
455 // Ctrl+left-drag is first-person look: the camera stays put and the
456 // view rotates, like turning your head (Google Earth Ctrl+drag).
457 // Drag toward where you want to look.
458 DragHandler {
459 id: lookHandler
460 target: null
461 acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
462 acceptedButtons: Qt.LeftButton
463 acceptedModifiers: Qt.ControlModifier
464 onActiveChanged: {
465 if (active) {
466 root.completeCameraAnimations()
467 geoCamera.beginLook(centroid.pressPosition)
468 // Apply motion accumulated before activation (see orbitHandler)
469 geoCamera.lookTo(centroid.position)
470 }
471 }
472 onCentroidChanged: {
473 if (active) {
474 geoCamera.lookTo(centroid.position)
475 }
476 }
477 }
478
479 // macOS delivers the physical Ctrl key as Qt.MetaModifier (Qt swaps
480 // Ctrl/Cmd), so accept it too: Ctrl+drag looks on every platform
481 // (and Cmd+drag still works via lookHandler, matching Google Earth).
482 // RightButton included because macOS synthesizes Ctrl+left-click as a
483 // right-click, so that's the button this drag actually arrives on.
484 DragHandler {
485 id: metaLookHandler
486 target: null
487 enabled: Qt.platform.os === "osx"
488 acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
489 acceptedButtons: Qt.LeftButton | Qt.RightButton
490 acceptedModifiers: Qt.MetaModifier
491 onActiveChanged: {
492 if (active) {
493 root.completeCameraAnimations()
494 geoCamera.beginLook(centroid.pressPosition)
495 // Apply motion accumulated before activation (see orbitHandler)
496 geoCamera.lookTo(centroid.position)
497 }
498 }
499 onCentroidChanged: {
500 if (active) {
501 geoCamera.lookTo(centroid.position)
502 }
503 }
504 }
505
506 // Pinch zooms about the centroid; two-finger twist rotates around it
507 // (handler rotation is positive clockwise on screen; heading follows
508 // so the world tracks the fingers)
509 PinchHandler {
510 id: pinchHandler
511 target: null
512 enabled: !root.pinchZoomDisabledByVirtualJoysticks
513 onActiveChanged: {
514 if (active) {
515 root.completeCameraAnimations()
516 }
517 }
518 onScaleChanged: (delta) => geoCamera.zoomBy(1 / delta, centroid.position)
519 onRotationChanged: (delta) => geoCamera.rotateBy(delta, centroid.position)
520 }
521
522 // Plain click/tap: the default DragThreshold gesture policy means any
523 // pan drag cancels the tap, so this never fires during camera gestures
524 TapHandler {
525 acceptedButtons: Qt.LeftButton
526 acceptedModifiers: Qt.NoModifier
527 onTapped: (eventPoint, button) => root.mapClicked(eventPoint.position)
528 }
529 }
530
531 // Pivot ring (Google Earth-style): marks the ground point an orbit drag
532 // rotates about, visible only while the drag is active. The pivot stays
533 // pinned to its press position on screen, so the ring never moves.
534 Rectangle {
535 id: orbitPivotIndicator
536 objectName: "geoMapOrbitPivotIndicator"
537
538 readonly property point _pivot: orbitHandler.active ? orbitHandler.centroid.pressPosition
539 : shiftOrbitHandler.centroid.pressPosition
540
541 visible: orbitHandler.active || shiftOrbitHandler.active
542 x: _pivot.x - (width / 2)
543 y: _pivot.y - (height / 2)
544 width: ScreenTools.defaultFontPixelHeight * 1.5
545 height: width
546 radius: width / 2
547 color: "transparent"
548 // Dark halo keeps the white ring visible over light imagery
549 border.color: Qt.rgba(0, 0, 0, 0.4)
550 border.width: (ScreenTools.defaultFontPixelHeight / 8) + 2
551
552 Rectangle {
553 anchors.fill: parent
554 anchors.margins: 1
555 radius: width / 2
556 color: "transparent"
557 border.color: "white"
558 border.width: ScreenTools.defaultFontPixelHeight / 8
559 }
560
561 Rectangle {
562 anchors.centerIn: parent
563 width: ScreenTools.defaultFontPixelHeight / 4
564 height: width
565 radius: width / 2
566 color: "white"
567 }
568 }
569
570 /// Ground station location (parity with the FlightMap marker)
571 GeoMapItem {
572 id: gcsIndicator
573
574 scene: geoScene
575 surfaceModel: patchModel
576 coordinate: gcsIndicator._gcsPosition
577 anchorPoint: Qt.point(gcsImage.width / 2, gcsImage.height / 2)
578 width: gcsImage.width
579 height: gcsImage.height
580 visible: gcsIndicator._gcsPosition.isValid
581
582 readonly property var _gcsPosition: QGroundControl.positionManager.gcsPosition
583 readonly property real _gcsHeading: QGroundControl.positionManager.gcsHeading
584
585 Image {
586 id: gcsImage
587 source: isNaN(gcsIndicator._gcsHeading) ? "/res/QGCLogoFull.svg" : "/res/QGCLogoArrow.svg"
588 mipmap: true
589 antialiasing: true
590 fillMode: Image.PreserveAspectFit
591 height: ScreenTools.defaultFontPixelHeight * (isNaN(gcsIndicator._gcsHeading) ? 1.75 : 2.5)
592 sourceSize.height: height
593 transform: Rotation {
594 origin.x: gcsImage.width / 2
595 origin.y: gcsImage.height / 2
596 // Camera heading rotates map north on screen; the arrow follows
597 angle: isNaN(gcsIndicator._gcsHeading) ? 0 : gcsIndicator._gcsHeading + geoCamera.heading
598 }
599 }
600 }
601
602 DebugView {
603 objectName: "geoMapRenderStats"
604 anchors.left: parent.left
605 anchors.top: parent.top
606 source: viewport
607 visible: root.renderStats
608 resourceDetailsVisible: true
609 }
610
611 NumberAnimation {
612 id: tiltAnimation
613 target: geoCamera
614 property: "tilt"
615 duration: root.cameraAnimationMs
616 easing.type: Easing.InOutQuad
617 }
618
619 NumberAnimation {
620 id: headingAnimation
621 target: geoCamera
622 property: "heading"
623 duration: root.cameraAnimationMs
624 easing.type: Easing.InOutQuad
625 }
626
627 NumberAnimation {
628 id: terrainAnimation
629 target: geoScene
630 property: "terrainScale"
631 duration: root.cameraAnimationMs
632 easing.type: Easing.InOutQuad
633 }
634
635 CoordinateAnimation {
636 id: recenterAnimation
637 target: geoCamera
638 property: "center"
639 duration: 1000
640 easing.type: Easing.InOutQuad
641 }
642
643 // Single transition path for every mode writer (button, goTo2D/3D,
644 // C++): mode changes always animate tilt and terrain to the mode's
645 // targets, so camera state and terrain displacement cannot desync
646 Connections {
647 target: geoCamera
648 function onModeChanged() {
649 const to3D = geoCamera.mode === GeoMapCamera.Mode3D
650 tiltAnimation.stop()
651 tiltAnimation.to = to3D ? geoCamera.default3DTilt : 0
652 tiltAnimation.start()
653 terrainAnimation.stop()
654 terrainAnimation.to = to3D ? 1 : 0
655 terrainAnimation.start()
656 }
657 function onCenterChanged() {
658 root._updateCenterElevation()
659 }
660 }
661
662 Connections {
663 target: patchModel
664 function onTerrainDataChanged() {
665 root._updateCenterElevation()
666 root._updateHomeTerrainBias()
667 }
668 }
669
670 Connections {
671 target: geoScene
672 function onTerrainScaleChanged() {
673 root._updateCenterElevation()
674 }
675 // verticalScale depends on the scene origin
676 function onSceneOriginChanged() {
677 root._updateCenterElevation()
678 }
679 }
680
681 Connections {
682 target: root._activeVehicle
683 function onHomePositionChanged() {
684 root._updateHomeTerrainBias()
685 }
686 }
687}