QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
FWLandingPatternMapVisual.qml
Go to the documentation of this file.
1import QtQuick
2import QtQuick.Controls
3import QtLocation
4import QtPositioning
5import QtQuick.Layouts
6
7import QGroundControl
8import QGroundControl.Controls
9import QGroundControl.FlightMap
10
11/// Fixed Wing Landing Pattern map visuals
12Item {
13 id: _root
14
15 property var map ///< Map control to place item in
16 property var vehicle ///< Vehicle associated with this item
17 property bool interactive: true
18
19 signal clicked(int sequenceNumber)
20
21 readonly property real _landingWidthMeters: 15
22 readonly property real _landingLengthMeters: 100
23
24 property var _missionItem: object
25 property var _mouseArea
26 property var _dragAreas: [ ]
27 property var _flightPath
28 property var _loiterPointObject
29 property var _landingPointObject
30 property real _transitionAltitudeMeters
31 property real _midSlopeAltitudeMeters
32 property real _landingAltitudeMeters: _missionItem.landingAltitude.rawValue
33 property real _finalApproachAltitudeMeters: _missionItem.finalApproachAltitude.rawValue
34 property bool _useLoiterToAlt: _missionItem.useLoiterToAlt.rawValue
35 property real _landingAreaBearing: _missionItem.landingCoordinate.azimuthTo(_missionItem.slopeStartCoordinate)
36
37 function _calcGlideSlopeHeights() {
38 var adjacent
39 if (_useLoiterToAlt) {
40 adjacent = _missionItem.landingCoordinate.distanceTo(_missionItem.slopeStartCoordinate)
41 } else {
42 adjacent = _missionItem.landingCoordinate.distanceTo(_missionItem.finalApproachCoordinate)
43 }
44 var opposite = _finalApproachAltitudeMeters - _landingAltitudeMeters
45 var angleRadians = Math.atan(opposite / adjacent)
46 var transitionDistance = _landingLengthMeters / 2
47 var glideSlopeDistance = adjacent - transitionDistance
48
49 _transitionAltitudeMeters = Math.tan(angleRadians) * (transitionDistance)
50 _midSlopeAltitudeMeters = Math.tan(angleRadians) * (transitionDistance + (glideSlopeDistance / 2))
51 }
52
53 function hideItemVisuals() {
54 objMgr.destroyObjects()
55 }
56
57 function showItemVisuals() {
58 if (objMgr.rgDynamicObjects.length === 0) {
59 _loiterPointObject = objMgr.createObject(finalApproachComponent, map, true /* parentObjectIsMap */)
60 _landingPointObject = objMgr.createObject(landingPointComponent, map, true /* parentObjectIsMap */)
61
62 var rgComponents = [ flightPathComponent, loiterRadiusComponent, landingAreaComponent, landingAreaLabelComponent,
63 glideSlopeComponent, glideSlopeLabelComponent, transitionHeightComponent, midGlideSlopeHeightComponent,
64 approachHeightComponent ]
65 objMgr.createObjects(rgComponents, map, true /* parentObjectIsMap */)
66 }
67 }
68
69 function hideMouseArea() {
70 if (_mouseArea) {
71 _mouseArea.destroy()
72 _mouseArea = undefined
73 }
74 }
75
76 function showMouseArea() {
77 if (!_mouseArea) {
78 _mouseArea = mouseAreaComponent.createObject(map)
79 }
80 }
81
82 function hideDragAreas() {
83 for (var i=0; i<_dragAreas.length; i++) {
84 _dragAreas[i].destroy()
85 }
86 _dragAreas = [ ]
87 }
88
89 function showDragAreas() {
90 if (_dragAreas.length === 0) {
91 _dragAreas.push(finalApproachDragAreaComponent.createObject(map))
92 _dragAreas.push(landDragAreaComponent.createObject(map))
93 }
94 }
95
96 function _setFlightPath() {
97 if (_useLoiterToAlt) {
98 _flightPath = [ _missionItem.slopeStartCoordinate, _missionItem.landingCoordinate ]
99 } else {
100 _flightPath = [ _missionItem.finalApproachCoordinate, _missionItem.landingCoordinate ]
101 }
102 }
103
104 QGCDynamicObjectManager {
105 id: objMgr
106 }
107
108 Component.onCompleted: {
109 if (_missionItem.landingCoordSet) {
110 showItemVisuals()
111 if (!_missionItem.flyView && _missionItem.isCurrentItem) {
112 showDragAreas()
113 }
114 _setFlightPath()
115 } else if (!_missionItem.flyView && _missionItem.isCurrentItem) {
116 showMouseArea()
117 }
118 }
119
120 Component.onDestruction: {
121 hideDragAreas()
122 hideMouseArea()
123 hideItemVisuals()
124 }
125
126 on_LandingAltitudeMetersChanged: _calcGlideSlopeHeights()
127 on_FinalApproachAltitudeMetersChanged: _calcGlideSlopeHeights()
128 on_UseLoiterToAltChanged: { _calcGlideSlopeHeights(); _setFlightPath() }
129
130 Connections {
131 target: _missionItem
132
133 onIsCurrentItemChanged: {
134 if (_missionItem.flyView) {
135 return
136 }
137 if (_missionItem.isCurrentItem) {
138 if (_missionItem.landingCoordSet) {
139 showDragAreas()
140 } else {
141 showMouseArea()
142 }
143 } else {
144 hideMouseArea()
145 hideDragAreas()
146 }
147 }
148
149 onLandingCoordSetChanged: {
150 if (_missionItem.flyView) {
151 return
152 }
153 if (_missionItem.landingCoordSet) {
154 hideMouseArea()
155 showItemVisuals()
156 showDragAreas()
157 _setFlightPath()
158 } else if (_missionItem.isCurrentItem) {
159 hideDragAreas()
160 showMouseArea()
161 }
162 }
163
164 onLandingCoordinateChanged: {
165 _calcGlideSlopeHeights()
166 _setFlightPath()
167 }
168
169 onSlopeStartCoordinateChanged: {
170 _calcGlideSlopeHeights()
171 _setFlightPath()
172 }
173
174 onFinalApproachCoordinateChanged: {
175 _calcGlideSlopeHeights()
176 _setFlightPath()
177 }
178 }
179
180 // Mouse area to capture landing point coordindate
181 Component {
182 id: mouseAreaComponent
183
184 MouseArea {
185 anchors.fill: map
186 z: QGroundControl.zOrderMapItems + 1 // Over item indicators
187 visible: _root.interactive
188
189 readonly property int _decimalPlaces: 8
190
191 onClicked: (mouse) => {
192 var coordinate = map.toCoordinate(Qt.point(mouse.x, mouse.y), false /* clipToViewPort */)
193 coordinate.latitude = coordinate.latitude.toFixed(_decimalPlaces)
194 coordinate.longitude = coordinate.longitude.toFixed(_decimalPlaces)
195 coordinate.altitude = coordinate.altitude.toFixed(_decimalPlaces)
196 _missionItem.landingCoordinate = coordinate
197 _missionItem.setLandingHeadingToTakeoffHeading()
198 }
199 }
200 }
201
202 // Control which is used to drag the final approach point
203 Component {
204 id: finalApproachDragAreaComponent
205
206 MissionItemIndicatorDrag {
207 mapControl: _root.map
208 itemIndicator: _loiterPointObject
209 itemCoordinate: _missionItem.finalApproachCoordinate
210 visible: _root.interactive
211
212 property bool _preventReentrancy: false
213
214 onItemCoordinateChanged: {
215 if (!_preventReentrancy) {
216 if (Drag.active) {
217 _preventReentrancy = true
218 var angle = _missionItem.landingCoordinate.azimuthTo(itemCoordinate)
219 var distance = _missionItem.landingCoordinate.distanceTo(_missionItem.finalApproachCoordinate)
220 _missionItem.finalApproachCoordinate = _missionItem.landingCoordinate.atDistanceAndAzimuth(distance, angle)
221 _preventReentrancy = false
222 }
223 }
224 }
225 }
226 }
227
228 // Control which is used to drag the landing point
229 Component {
230 id: landDragAreaComponent
231
232 MissionItemIndicatorDrag {
233 mapControl: _root.map
234 itemIndicator: _landingPointObject
235 itemCoordinate: _missionItem.landingCoordinate
236 visible: _root.interactive
237
238 onItemCoordinateChanged: _missionItem.coordinate = itemCoordinate
239 }
240 }
241
242 // Flight path
243 Component {
244 id: flightPathComponent
245
246 MapPolyline {
247 z: QGroundControl.zOrderMapItems - 1 // Under item indicators
248 line.color: "#be781c"
249 line.width: 2
250 path: _flightPath
251 }
252 }
253
254 // Final approach point
255 Component {
256 id: finalApproachComponent
257
258 MapQuickItem {
259 anchorPoint.x: sourceItem.anchorPointX
260 anchorPoint.y: sourceItem.anchorPointY
261 z: QGroundControl.zOrderMapItems
262 coordinate: _missionItem.finalApproachCoordinate
263
264 sourceItem:
265 MissionItemIndexLabel {
266 index: _missionItem.sequenceNumber
267 label: _useLoiterToAlt ? qsTr("Loiter") : qsTr("Approach")
268 checked: _missionItem.isCurrentItem
269
270 onClicked: _root.clicked(_missionItem.sequenceNumber)
271 }
272 }
273 }
274
275 // Landing point
276 Component {
277 id: landingPointComponent
278
279 MapQuickItem {
280 anchorPoint.x: sourceItem.anchorPointX
281 anchorPoint.y: sourceItem.anchorPointY
282 z: QGroundControl.zOrderMapItems
283 coordinate: _missionItem.landingCoordinate
284
285 sourceItem:
286 MissionItemIndexLabel {
287 index: _missionItem.lastSequenceNumber
288 checked: _missionItem.isCurrentItem
289
290 onClicked: _root.clicked(_missionItem.sequenceNumber)
291 }
292 }
293 }
294
295 Component {
296 id: loiterRadiusComponent
297
298 MapCircle {
299 z: QGroundControl.zOrderMapItems
300 center: _missionItem.finalApproachCoordinate
301 radius: _missionItem.loiterRadius.rawValue
302 border.width: 2
303 border.color: "green"
304 color: "transparent"
305 visible: _useLoiterToAlt
306 }
307 }
308
309 Component {
310 id: landingAreaLabelComponent
311
312 MapQuickItem {
313 anchorPoint.x: sourceItem.contentWidth / 2
314 anchorPoint.y: sourceItem.contentHeight / 2
315 z: QGroundControl.zOrderMapItems
316 coordinate: _missionItem.landingCoordinate
317 visible: _missionItem.isCurrentItem
318
319 sourceItem: QGCLabel {
320 id: landingAreaLabel
321 text: qsTr("Landing Area")
322 color: "white"
323
324 property real _rawBearing: _landingAreaBearing
325 property real _adjustedBearing
326
327 on_RawBearingChanged: {
328 _adjustedBearing = _rawBearing
329 if (_adjustedBearing > 180) {
330 _adjustedBearing -= 180
331 }
332 _adjustedBearing -= 90
333 if (_adjustedBearing < 0) {
334 _adjustedBearing += 360
335 }
336 }
337
338 transform: Rotation {
339 origin.x: landingAreaLabel.width / 2
340 origin.y: landingAreaLabel.height / 2
341 angle: landingAreaLabel._adjustedBearing
342 }
343 }
344 }
345 }
346
347 Component {
348 id: glideSlopeLabelComponent
349
350 MapQuickItem {
351 anchorPoint.x: sourceItem._rawBearing > 180 ? sourceItem.contentWidth : 0
352 anchorPoint.y: sourceItem.contentHeight / 2
353 z: QGroundControl.zOrderMapItems
354 visible: _missionItem.isCurrentItem
355
356
357 sourceItem: QGCLabel {
358 id: glideSlopeLabel
359 text: qsTr("Glide Slope")
360 color: "white"
361
362 property real _rawBearing: _landingAreaBearing
363 property real _adjustedBearing
364
365 on_RawBearingChanged: {
366 _adjustedBearing = _rawBearing
367 if (_adjustedBearing > 180) {
368 _adjustedBearing -= 180
369 }
370 _adjustedBearing -= 90
371 if (_adjustedBearing < 0) {
372 _adjustedBearing += 360
373 }
374 }
375
376 transform: Rotation {
377 origin.x: sourceItem._rawBearing > 180 ? sourceItem.contentWidth : 0
378 origin.y: glideSlopeLabel.contentHeight / 2
379 angle: glideSlopeLabel._adjustedBearing
380 }
381 }
382
383 function recalc() {
384 coordinate = _missionItem.landingCoordinate.atDistanceAndAzimuth(_landingLengthMeters / 2 + 2, _landingAreaBearing)
385 }
386
387 Component.onCompleted: recalc()
388
389 Connections {
390 target: _missionItem
391 onLandingCoordinateChanged: recalc()
392 onSlopeStartCoordinateChanged: recalc()
393 onFinalApproachCoordinateChanged: recalc()
394 }
395 }
396 }
397
398 Component {
399 id: landingAreaComponent
400
401 MapPolygon {
402 z: QGroundControl.zOrderMapItems
403 border.width: 1
404 border.color: "black"
405 color: "green"
406 opacity: 0.5
407
408 readonly property real angleRadians: Math.atan((_landingWidthMeters / 2) / (_landingLengthMeters / 2))
409 readonly property real angleDegrees: (angleRadians * (180 / Math.PI))
410 readonly property real hypotenuse: (_landingWidthMeters / 2) / Math.sin(angleRadians)
411
412 function recalc() {
413 path = [ ]
414 addCoordinate(_missionItem.landingCoordinate.atDistanceAndAzimuth(hypotenuse, _landingAreaBearing - angleDegrees))
415 addCoordinate(_missionItem.landingCoordinate.atDistanceAndAzimuth(hypotenuse, _landingAreaBearing + angleDegrees))
416 addCoordinate(_missionItem.landingCoordinate.atDistanceAndAzimuth(hypotenuse, _landingAreaBearing + (180 - angleDegrees)))
417 addCoordinate(_missionItem.landingCoordinate.atDistanceAndAzimuth(hypotenuse, _landingAreaBearing - (180 - angleDegrees)))
418 }
419
420 Component.onCompleted: recalc()
421
422 Connections {
423 target: _missionItem
424 onLandingCoordinateChanged: recalc()
425 onSlopeStartCoordinateChanged: recalc()
426 onFinalApproachCoordinateChanged: recalc()
427 }
428 }
429 }
430
431 Component {
432 id: glideSlopeComponent
433
434 MapPolygon {
435 z: QGroundControl.zOrderMapItems
436 border.width: 1
437 border.color: "black"
438 color: _missionItem.terrainCollision ? "red" : "orange"
439 opacity: 0.5
440
441 readonly property real angleRadians: Math.atan((_landingWidthMeters / 2) / (_landingLengthMeters / 2))
442 readonly property real angleDegrees: (angleRadians * (180 / Math.PI))
443 readonly property real hypotenuse: (_landingWidthMeters / 2) / Math.sin(angleRadians)
444
445 function recalc() {
446 path = [ ]
447 addCoordinate(_missionItem.landingCoordinate.atDistanceAndAzimuth(hypotenuse, _landingAreaBearing - angleDegrees))
448 addCoordinate(_missionItem.landingCoordinate.atDistanceAndAzimuth(hypotenuse, _landingAreaBearing + angleDegrees))
449 addCoordinate(_useLoiterToAlt ? _missionItem.slopeStartCoordinate : _missionItem.finalApproachCoordinate)
450 }
451
452 Component.onCompleted: recalc()
453
454 Connections {
455 target: _missionItem
456 onLandingCoordinateChanged: recalc()
457 onSlopeStartCoordinateChanged: recalc()
458 onFinalApproachCoordinateChanged: recalc()
459 }
460
461 Connections {
462 target: _missionItem.useLoiterToAlt
463 onRawValueChanged: recalc()
464 }
465 }
466 }
467
468 Component {
469 id: transitionHeightComponent
470
471 MapQuickItem {
472 anchorPoint.x: sourceItem.width / 2
473 anchorPoint.y: 0
474 z: QGroundControl.zOrderMapItems
475 visible: _missionItem.isCurrentItem
476
477 sourceItem: HeightIndicator {
478 map: _root.map
479 heightText: Math.floor(QGroundControl.unitsConversion.metersToAppSettingsVerticalDistanceUnits(_transitionAltitudeMeters)) +
480 QGroundControl.unitsConversion.appSettingsVerticalDistanceUnitsString + "<sup>*</sup>"
481 }
482
483 function recalc() {
484 var centeredCoordinate = _missionItem.landingCoordinate.atDistanceAndAzimuth(_landingLengthMeters / 2, _landingAreaBearing)
485 var angleIncrement = _landingAreaBearing > 180 ? -90 : 90
486 coordinate = centeredCoordinate.atDistanceAndAzimuth(_landingWidthMeters, _landingAreaBearing + angleIncrement)
487 }
488
489 Component.onCompleted: recalc()
490
491 Connections {
492 target: _missionItem
493 onLandingCoordinateChanged: recalc()
494 onSlopeStartCoordinateChanged: recalc()
495 onFinalApproachCoordinateChanged: recalc()
496 }
497 }
498 }
499
500 Component {
501 id: midGlideSlopeHeightComponent
502
503 MapQuickItem {
504 anchorPoint.x: sourceItem.width / 2
505 anchorPoint.y: 0
506 z: QGroundControl.zOrderMapItems
507 visible: _missionItem.isCurrentItem
508
509 sourceItem: HeightIndicator {
510 map: _root.map
511 heightText: Math.floor(QGroundControl.unitsConversion.metersToAppSettingsVerticalDistanceUnits(_midSlopeAltitudeMeters)) +
512 QGroundControl.unitsConversion.appSettingsVerticalDistanceUnitsString + "<sup>*</sup>"
513 }
514
515 function recalc() {
516 var transitionCoordinate = _missionItem.landingCoordinate.atDistanceAndAzimuth(_landingLengthMeters / 2, _landingAreaBearing)
517 var halfDistance = transitionCoordinate.distanceTo(_useLoiterToAlt ? _missionItem.slopeStartCoordinate : _missionItem.finalApproachCoordinate) / 2
518 var centeredCoordinate = transitionCoordinate.atDistanceAndAzimuth(halfDistance, _landingAreaBearing)
519 var angleIncrement = _landingAreaBearing > 180 ? -90 : 90
520 coordinate = centeredCoordinate.atDistanceAndAzimuth(_landingWidthMeters / 2, _landingAreaBearing + angleIncrement)
521 }
522
523 Component.onCompleted: recalc()
524
525 Connections {
526 target: _missionItem
527 onLandingCoordinateChanged: recalc()
528 onSlopeStartCoordinateChanged: recalc()
529 onFinalApproachCoordinateChanged: recalc()
530 }
531
532 Connections {
533 target: _missionItem.useLoiterToAlt
534 onRawValueChanged: recalc()
535 }
536 }
537 }
538
539 Component {
540 id: approachHeightComponent
541
542 MapQuickItem {
543 anchorPoint.x: sourceItem.width / 2
544 anchorPoint.y: 0
545 z: QGroundControl.zOrderMapItems
546 visible: _missionItem.isCurrentItem
547 coordinate: _missionItem.slopeStartCoordinate
548
549 sourceItem: HeightIndicator {
550 map: _root.map
551 heightText: _missionItem.finalApproachAltitude.value.toFixed(1) + QGroundControl.unitsConversion.appSettingsHorizontalDistanceUnitsString
552 }
553 }
554 }
555}