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.GeoMap
18/// Mission path for the GeoMap: a 3D ribbon connecting every mission item in
19/// sequence order (simple items at their coordinate; complex items via their
20/// generic entryCoordinate/exitCoordinate, so the ribbon routes straight
21/// through a complex item's own footprint rather than skipping it), at true
22/// (AMSL) altitude. A leg is flown in its destination item's frame (ArduPilot
23/// AC_WPNav): legs arriving at a terrain-frame item are draped over the DEM
24/// with a linear AGL ramp, all other legs are straight lines. Straight legs
25/// that pass below the terrain draw in collisionColor, as Plan view marks
26/// them. Stays visible
27/// in 2D mode (crossfade3D off), same as GeoMapFlightPath. Rebuilt wholesale
28/// on any mission change (missions are edited, not streamed, so the flight
29/// path's incremental append API is not needed here).
33 enum Leg { Generic, Terrain, Takeoff, Land, Interior }
35 property var missionController
36 property var vehicle ///< Active vehicle, for the RTL-to-home segment
37 // DEM-vs-home-AMSL bias (see GeoMapVehicleItem), computed once by the
38 // consumer for the active vehicle and shared with GeoMapMissionItems
39 // instead of being re-sampled here independently
40 property real homeTerrainBias: 0
41 property real lineWidth: ScreenTools.defaultFontPixelHeight / 5
42 // Matches MissionLineView.qml's existing flight-path line color
43 property color lineColor: QGroundControl.globalPalette.mapMissionTrajectory
44 property color collisionColor: "red"
46 altitudeMode: GeoMapItem.Absolute
49 if (!_geometry || !_geometry.anchorCoordinate.isValid) {
50 return QtPositioning.coordinate()
52 const anchor = _geometry.anchorCoordinate
53 return QtPositioning.coordinate(anchor.latitude, anchor.longitude, anchor.altitude + root.homeTerrainBias)
55 visible: !!missionController
57 // The geometry lives inside the 3D delegate (created/destroyed with the
58 // scene); this bridges its anchor out for the coordinate binding above
59 property var _geometry: null
61 readonly property var _camera: scene ? scene.camera : null
63 // Pixel-to-scene-unit factor at unit distance; the shader scales it by
64 // each vertex's own camera distance
65 readonly property real _screenFactor: _camera ? _camera.unitsPerPixelAtUnitDistance : 0
67 // Ordered route points ({coordinate, leg}) of every item the vehicle
68 // actually routes through, mirroring MissionController::_recalcFlightPathSegments:
69 // items must specifiesCoordinate && !isStandaloneCoordinate to get a
70 // segment (excludes RTL, which has no mission-encoded coordinate, and
71 // standalone items like ROI); a qualifying item contributes its
72 // entryCoordinate, plus its exitCoordinate too when that differs
73 // (exitCoordinateSameAsEntry false) — for a simple item these are always
74 // the same point, for a complex item this routes the ribbon straight
75 // through its footprint edge-to-edge. Hitting RTL stops the walk and
76 // links the last point to home instead (linkEndToHome). A land item ends
77 // the walk at its entry: its interior path (loiter exit -> touchdown) is
78 // drawn by the landing pattern visual from the slope start, not straight
79 // from the loiter center, and _recalcFlightPathSegments draws no segment
80 // after a landing item either. The walk starts at item 1 (item 0 is the
81 // MissionSettingsItem, i.e. home); home is prepended only when the mission
82 // starts from the ground — rover, or a takeoff command before the first
83 // coordinate item — mirroring linkStartToHome. leg is the type of the leg
84 // ARRIVING at a point, mirroring segmentTypeForPair: Takeoff / Land
85 // destinations first, then Terrain for a terrain-frame destination
86 // (MAV_FRAME_GLOBAL_TERRAIN_ALT), else Generic; Interior is a complex
87 // item's own entry -> exit span, which has no flight path segment. Recomputes automatically on any
88 // structural or per-item change since the loop below reads every
89 // NOTIFY-backed property it depends on.
90 readonly property var _routePoints: {
92 if (root.missionController) {
93 const items = root.missionController.visualItems
94 let linkStartToHome = root.vehicle ? root.vehicle.rover : false
95 for (let i = 1; i < items.count; ++i) {
96 const visualItem = items.get(i)
97 if (visualItem.isSimpleItem && visualItem.command === MAVLinkEnums.MAV_CMD_NAV_RETURN_TO_LAUNCH) {
98 if (points.length > 0 && root.vehicle && root.vehicle.homePosition.isValid) {
99 points.push({ coordinate: root.vehicle.homePosition, leg: GeoMapMissionPath.Generic })
103 if (points.length === 0 && visualItem.isTakeoffItem) {
104 linkStartToHome = true
106 if (visualItem.specifiesCoordinate && !visualItem.isStandaloneCoordinate) {
108 coordinate: QtPositioning.coordinate(visualItem.entryCoordinate.latitude,
109 visualItem.entryCoordinate.longitude,
110 visualItem.amslEntryAlt),
111 leg: visualItem.isTakeoffItem ? GeoMapMissionPath.Takeoff
112 : visualItem.isLandCommand ? GeoMapMissionPath.Land
113 : (visualItem.isSimpleItem && visualItem.altitudeFrame === QGroundControl.AltitudeFrameTerrain) ? GeoMapMissionPath.Terrain
114 : GeoMapMissionPath.Generic
116 if (visualItem.isLandCommand) {
119 if (!visualItem.exitCoordinateSameAsEntry) {
121 coordinate: QtPositioning.coordinate(visualItem.exitCoordinate.latitude,
122 visualItem.exitCoordinate.longitude,
123 visualItem.amslExitAlt),
124 leg: GeoMapMissionPath.Interior
129 if (linkStartToHome && points.length > 0 && items.count > 0) {
130 const home = items.get(0)
131 if (home.coordinate.isValid) {
133 coordinate: QtPositioning.coordinate(home.coordinate.latitude,
134 home.coordinate.longitude,
136 leg: GeoMapMissionPath.Generic
144 // Straight legs longer than this are subdivided along the great circle
145 // (matching MissionLineView.qml): the geometry joins points linearly in
146 // Web Mercator, which diverges materially from the actual route over long
148 readonly property real _maxSegmentLengthM: 50000
150 // Terrain-frame legs sample the DEM at this spacing (same idea as
151 // FenceWallGeometry), capped so a degenerate long leg can't explode the
153 readonly property real _terrainSampleSpacingM: 50
154 readonly property int _maxTerrainSamplesPerLeg: 200
156 // Terrain sampling can't live in the _routePoints binding: terrainHeightAt
157 // is not NOTIFY-reactive, so draping is re-run explicitly here on route
158 // changes and on terrainHeightsChanged as DEM patches stream in.
159 // Returns the ribbon points and a highlight flag per point.
160 function _expandedPath() {
161 const points = root._routePoints
162 const collisions = root._legCollisions
163 const coordinates = []
164 const highlights = []
165 for (let i = 0; i < points.length; ++i) {
166 const coord = points[i].coordinate
168 const prev = points[i - 1].coordinate
169 const collision = collisions[i]
170 // Restart a recolored leg at a copy of its start point so the color
171 // changes at the waypoint instead of blending along the leg
172 if (collision !== highlights[highlights.length - 1]) {
173 coordinates.push(prev)
174 highlights.push(collision)
176 if (points[i].leg === GeoMapMissionPath.Terrain && root.surfaceModel) {
177 root._appendTerrainLeg(coordinates, prev, coord)
179 coordinates.push(...root._longLegSubdivision(prev, coord))
181 coordinates.push(coord)
182 while (highlights.length < coordinates.length) {
183 highlights.push(collision)
186 coordinates.push(coord)
187 highlights.push(points.length > 1 && collisions[1])
190 return { coordinates: coordinates, highlights: highlights }
193 // Interior points of a straight leg (none when it is short enough)
194 function _longLegSubdivision(prev, coord) {
196 const distance = prev.distanceTo(coord)
197 if (distance <= root._maxSegmentLengthM) {
200 const segments = Math.ceil(distance / root._maxSegmentLengthM)
201 const azimuth = prev.azimuthTo(coord)
202 for (let j = 1; j < segments; ++j) {
203 const point = prev.atDistanceAndAzimuth((j * distance) / segments, azimuth)
204 result.push(QtPositioning.coordinate(point.latitude, point.longitude,
205 prev.altitude + ((coord.altitude - prev.altitude) * j) / segments))
210 // ArduPilot flies a terrain-frame leg at a height above terrain that
211 // ramps from the origin's AGL to the destination's while riding the
212 // terrain profile (AC_WPNav terrain offset); approximate that by draping
213 // the leg over the DEM with a linear AGL ramp. AGLs are computed against
214 // the same DEM the surface renders from, so the ribbon meets both
215 // endpoint markers exactly (the DEM-vs-home AMSL bias cancels between
216 // the endpoint and sample terms).
217 function _appendTerrainLeg(result, prev, coord) {
218 const distance = prev.distanceTo(coord)
219 const steps = Math.min(Math.ceil(distance / root._terrainSampleSpacingM), root._maxTerrainSamplesPerLeg)
223 const azimuth = prev.azimuthTo(coord)
224 const prevAgl = prev.altitude - root.surfaceModel.terrainHeightAt(prev)
225 const destAgl = coord.altitude - root.surfaceModel.terrainHeightAt(coord)
226 for (let j = 1; j < steps; ++j) {
228 const point = prev.atDistanceAndAzimuth(distance * s, azimuth)
229 result.push(QtPositioning.coordinate(point.latitude, point.longitude,
230 root.surfaceModel.terrainHeightAt(point) + prevAgl + ((destAgl - prevAgl) * s)))
234 // Per route point: the leg arriving there passes below the terrain
235 // (FlightPathSegment::terrainCollision rules). Terrain-frame legs ride the
236 // terrain and never collide.
237 property var _legCollisions: []
239 // FlightPathSegment::_collisionIgnoreMeters: takeoff and land legs meet the ground at one end
240 readonly property real _collisionIgnoreMeters: 10
242 function _computeLegCollisions() {
243 const points = root._routePoints
244 const collisions = points.map(() => false)
245 // A rover follows the ground: its constant-altitude legs aren't a flight path
246 if (!root.surfaceModel || (root.vehicle && root.vehicle.rover)) {
249 const straightUpTakeoff = !(root.vehicle && root.vehicle.fixedWing)
250 for (let i = 1; i < points.length; ++i) {
251 const leg = points[i].leg
252 if (leg === GeoMapMissionPath.Terrain || leg === GeoMapMissionPath.Interior) {
255 const coord = points[i].coordinate
256 let prev = points[i - 1].coordinate
257 // MissionController::_createFlightPathSegmentWorker: a non-fixed-wing
258 // climbs vertically, then flies over at the takeoff altitude
259 if (leg === GeoMapMissionPath.Takeoff && straightUpTakeoff) {
260 prev = QtPositioning.coordinate(prev.latitude, prev.longitude, coord.altitude)
262 // Check the same great-circle pieces _expandedPath draws
263 const chain = [prev, ...root._longLegSubdivision(prev, coord), coord]
264 for (let j = 1; j < chain.length && !collisions[i]; ++j) {
265 collisions[i] = root.surfaceModel.segmentBelowTerrain(
266 root._demFrame(chain[j - 1]), root._demFrame(chain[j]),
267 (leg === GeoMapMissionPath.Takeoff && j === 1) ? root._collisionIgnoreMeters : 0,
268 (leg === GeoMapMissionPath.Land && j === chain.length - 1) ? root._collisionIgnoreMeters : 0)
274 // Route altitudes are vehicle AMSL; the rendered DEM sits homeTerrainBias above that frame
275 function _demFrame(coord) {
276 return QtPositioning.coordinate(coord.latitude, coord.longitude, coord.altitude + root.homeTerrainBias)
279 function _setGeometryPath() {
280 const path = root._expandedPath()
281 _geometry.setPath(path.coordinates, path.highlights)
284 function _reloadPath() {
286 root._legCollisions = root._computeLegCollisions()
287 root._setGeometryPath()
291 function _refreshCollisions() {
295 const collisions = root._computeLegCollisions()
296 const changed = collisions.some((collision, i) => collision !== root._legCollisions[i])
297 root._legCollisions = collisions
299 root._setGeometryPath()
303 function _redrapePath() {
305 root._setGeometryPath()
309 // Qt.callLater coalesces rebuild bursts (per-patch terrainHeightsChanged
310 // during tile streaming, route + terrain changes in one pass) into one
311 // re-drape, same idea as FenceWallGeometry::_scheduleRebuild
312 function _scheduleReloadPath() {
313 Qt.callLater(root._reloadPath)
316 function _scheduleCollisionRefresh() {
317 Qt.callLater(root._refreshCollisions)
320 on_RoutePointsChanged: _scheduleReloadPath()
321 onHomeTerrainBiasChanged: _scheduleCollisionRefresh()
323 readonly property bool _hasTerrainLeg: _routePoints.some(p => p.leg === GeoMapMissionPath.Terrain)
326 target: root.surfaceModel
328 // Draped legs follow the drawn mesh, which also changes on patch churn
329 function onTerrainHeightsChanged() {
330 if (root._hasTerrainLeg) {
331 Qt.callLater(root._redrapePath)
335 function onTerrainDataChanged() {
336 root._scheduleCollisionRefresh()
340 delegate3D: Component {
341 // z offsets are relative altitudes; flatten them with the terrain
342 // during the 2D<->3D transition (anchor z already tracks terrainScale
343 // through GeoMapItem)
345 scale: Qt.vector3d(1, 1, root.scene ? root.scene.terrainScale : 1)
348 geometry: FlightPathGeometry {
354 materials: CustomMaterial {
355 shadingMode: CustomMaterial.Unshaded
356 cullMode: Material.NoCulling
357 vertexShader: "shaders/flightpath.vert"
358 fragmentShader: "shaders/flightpath.frag"
360 property real lineWidth: root.lineWidth
361 property real screenFactor: root._screenFactor
362 property color pathColor: root.lineColor
363 property color highlightColor: root.collisionColor
364 // Ramps in as the terrain flattens, effectively disabling
365 // depth testing in 2D mode (see flightpath.vert)
366 property real depthPull: root.scene ? 0.5 * (1.0 - root.scene.terrainScale) : 0.0
369 Component.onCompleted: {
370 root._geometry = pathGeometry
373 Component.onDestruction: root._geometry = null