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 ****************************************************************************/
12import QtQuick3D.Helpers
16import QGroundControl.Controls
17import QGroundControl.GeoMap
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.
25 // Keep map item overlays (GeoMapItem) inside the viewport, matching
26 // QtLocation Map behavior
29 property alias camera: geoCamera
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
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
45 function startPerfCapture() {
46 patchModel.startCapture()
49 // Returns the CSV file path (empty on failure)
50 function stopPerfCapture() {
51 return patchModel.stopCapture()
54 readonly property int cameraAnimationMs: 500
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)
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
70 function _updateHomeTerrainBias() {
71 if (_activeVehicle && _activeVehicle.homePosition.isValid && !isNaN(_activeVehicle.homePosition.altitude)) {
72 _homeTerrainBias = patchModel.terrainHeightAt(_activeVehicle.homePosition) - _activeVehicle.homePosition.altitude
78 on_ActiveVehicleChanged: _updateHomeTerrainBias()
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
84 // Terrain displacement factor (see GeoScene.terrainScale)
85 property alias terrainScale: geoScene.terrainScale
87 // Scene wiring for map items (GeoMapItem consumers)
88 readonly property GeoScene scene: geoScene
89 readonly property SurfacePatchModel surfaceModel: patchModel
91 function analyzeSurface() {
92 patchModel.analyzeSurface()
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()
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)
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()
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
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)
145 objectName: "geoMapScene"
147 content3D: mapContent3D
152 objectName: "geoMapPatchModel"
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
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
170 onCenterMap: (coordinate, firstPosition) => {
171 recenterAnimation.stop()
172 geoCamera.center = coordinate
174 geoCamera.distance = geoCamera.distanceForZoomLevel(QGroundControl.flightMapInitialZoom)
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()
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
196 running: root.visible
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), [])
208 objectName: "geoMapViewport"
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
220 depthNear: 10 * geoCamera.distance
221 depthFar: (patchModel.maxRangeMultiplier + 1) * geoCamera.distance
227 ambientColor: Qt.rgba(0.4, 0.4, 0.4, 1.0)
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.
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))
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
255 scale: Qt.vector3d(1, 1, Math.max(0.0001, geoScene.terrainScale * geoScene.verticalScale))
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
272 // A covered pending patch stays hidden: its empty flat grid
273 // would z-fight the retained retiring cover
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
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
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
305 : (patchModel.mapType !== ""
307 : root._lodColor(patchDelegate.centerX, patchDelegate.centerY,
308 patchDelegate.span, patchDelegate.zoomLevel))
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.
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.
329 acceptedButtons: Qt.LeftButton
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)
341 geoCamera.panTo(centroid.position)
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.
354 acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
355 acceptedButtons: Qt.RightButton
358 root.completeCameraAnimations()
359 geoCamera.beginOrbit(centroid.pressPosition)
364 geoCamera.orbitTo(centroid.position)
371 onWheel: (event) => {
372 root.completeCameraAnimations()
373 geoCamera.zoom(event.angleDelta.y, point.position)
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)
385 root.completeCameraAnimations()
388 onScaleChanged: (delta) => geoCamera.zoomBy(1 / delta, centroid.position)
389 onRotationChanged: (delta) => geoCamera.rotateBy(delta, centroid.position)
393 /// Ground station location (parity with the FlightMap marker)
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
405 readonly property var _gcsPosition: QGroundControl.qgcPositionManger.gcsPosition
406 readonly property real _gcsHeading: QGroundControl.qgcPositionManger.gcsHeading
410 source: isNaN(gcsIndicator._gcsHeading) ? "/res/QGCLogoFull.svg" : "/res/QGCLogoArrow.svg"
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
426 objectName: "geoMapRenderStats"
427 anchors.left: parent.left
428 anchors.top: parent.top
430 visible: root.renderStats
431 resourceDetailsVisible: true
438 duration: root.cameraAnimationMs
439 easing.type: Easing.InOutQuad
446 duration: root.cameraAnimationMs
447 easing.type: Easing.InOutQuad
453 property: "terrainScale"
454 duration: root.cameraAnimationMs
455 easing.type: Easing.InOutQuad
458 CoordinateAnimation {
459 id: recenterAnimation
463 easing.type: Easing.InOutQuad
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
471 function onModeChanged() {
472 const to3D = geoCamera.mode === GeoMapCamera.Mode3D
474 tiltAnimation.to = to3D ? geoCamera.default3DTilt : 0
475 tiltAnimation.start()
476 terrainAnimation.stop()
477 terrainAnimation.to = to3D ? 1 : 0
478 terrainAnimation.start()
480 function onCenterChanged() {
481 root._updateCenterElevation()
487 function onTerrainHeightsChanged() {
488 root._updateCenterElevation()
489 root._updateHomeTerrainBias()
495 function onTerrainScaleChanged() {
496 root._updateCenterElevation()
498 // verticalScale depends on the scene origin
499 function onSceneOriginChanged() {
500 root._updateCenterElevation()
505 target: root._activeVehicle
506 function onHomePositionChanged() {
507 root._updateHomeTerrainBias()