QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
QGCMapPolygonVisuals.qml
Go to the documentation of this file.
1import QtQuick
2import QtQuick.Controls
3import QtLocation
4import QtPositioning
5import QtQuick.Dialogs
6import QtQuick.Layouts
7
8import QGroundControl
9import QGroundControl.Controls
10import QGroundControl.FlightMap
11
12/// QGCMapPolygon map visuals
13Item {
14 id: _root
15
16 property var mapControl ///< Map control to place item in
17 property var mapPolygon ///< QGCMapPolygon object
18 property bool interactive: mapPolygon.interactive
19 property color interiorColor: "transparent"
20 property color altColor: "transparent"
21 property real interiorOpacity: 1
22 property int borderWidth: 0
23 property color borderColor: "black"
24
25 property bool _circleMode: false
26 property real _circleRadius
27 property bool _circleRadiusDrag: false
28 property var _circleRadiusDragCoord: QtPositioning.coordinate()
29 property bool _editCircleRadius: false
30 property string _instructionText: _polygonToolsText
31 property var _savedVertices: [ ]
32 property bool _savedCircleMode
33 property bool _isVertexBeingDragged: false
34
35 property real _zorderDragHandle: QGroundControl.zOrderMapItems + 3 // Highest to prevent splitting when items overlap
36 property real _zorderSplitHandle: QGroundControl.zOrderMapItems + 2
37 property real _zorderCenterHandle: QGroundControl.zOrderMapItems + 1 // Lowest such that drag or split takes precedence
38
39 readonly property string _polygonToolsText: qsTr("Polygon Tools")
40 readonly property string _traceText: qsTr("Click in the map to add vertices. Click 'Done Tracing' when finished.")
41
42 function addCommonVisuals() {
43 if (_objMgrCommonVisuals.empty) {
44 _objMgrCommonVisuals.createObject(polygonComponent, mapControl, true)
45 }
46 }
47
48 function removeCommonVisuals() {
49 _objMgrCommonVisuals.destroyObjects()
50 }
51
52 function addEditingVisuals() {
53 if (_objMgrEditingVisuals.empty) {
54 _objMgrEditingVisuals.createObjects(
55 [ dragHandlesComponent, splitHandlesComponent, centerDragHandleComponent, edgeLengthHandlesComponent ],
56 mapControl,
57 false /* addToMap */)
58 }
59 }
60
61 function removeEditingVisuals() {
62 _objMgrEditingVisuals.destroyObjects()
63 }
64
65 function addToolbarVisuals() {
66 if (_objMgrToolVisuals.empty) {
67 var toolbar = _objMgrToolVisuals.createObject(toolbarComponent, mapControl)
68 toolbar.z = QGroundControl.zOrderWidgets
69 }
70 }
71
72 function removeToolVisuals() {
73 _objMgrToolVisuals.destroyObjects()
74 }
75
76 function addCircleVisuals() {
77 if (_objMgrCircleVisuals.empty) {
78 _objMgrCircleVisuals.createObject(radiusVisualsComponent, mapControl)
79 }
80 }
81
82 /// Calculate the default/initial 4 sided polygon
83 function defaultPolygonVertices() {
84 // Initial polygon is inset to take 2/3rds space
85 var rect = Qt.rect(mapControl.centerViewport.x, mapControl.centerViewport.y, mapControl.centerViewport.width, mapControl.centerViewport.height)
86 rect.x += (rect.width * 0.25) / 2
87 rect.y += (rect.height * 0.25) / 2
88 rect.width *= 0.75
89 rect.height *= 0.75
90
91 var centerCoord = mapControl.toCoordinate(Qt.point(rect.x + (rect.width / 2), rect.y + (rect.height / 2)), false /* clipToViewPort */)
92 var topLeftCoord = mapControl.toCoordinate(Qt.point(rect.x, rect.y), false /* clipToViewPort */)
93 var topRightCoord = mapControl.toCoordinate(Qt.point(rect.x + rect.width, rect.y), false /* clipToViewPort */)
94 var bottomLeftCoord = mapControl.toCoordinate(Qt.point(rect.x, rect.y + rect.height), false /* clipToViewPort */)
95 var bottomRightCoord = mapControl.toCoordinate(Qt.point(rect.x + rect.width, rect.y + rect.height), false /* clipToViewPort */)
96
97 // Initial polygon has max width and height of 3000 meters
98 var halfWidthMeters = Math.min(topLeftCoord.distanceTo(topRightCoord), 3000) / 2
99 var halfHeightMeters = Math.min(topLeftCoord.distanceTo(bottomLeftCoord), 3000) / 2
100 topLeftCoord = centerCoord.atDistanceAndAzimuth(halfWidthMeters, -90).atDistanceAndAzimuth(halfHeightMeters, 0)
101 topRightCoord = centerCoord.atDistanceAndAzimuth(halfWidthMeters, 90).atDistanceAndAzimuth(halfHeightMeters, 0)
102 bottomLeftCoord = centerCoord.atDistanceAndAzimuth(halfWidthMeters, -90).atDistanceAndAzimuth(halfHeightMeters, 180)
103 bottomRightCoord = centerCoord.atDistanceAndAzimuth(halfWidthMeters, 90).atDistanceAndAzimuth(halfHeightMeters, 180)
104
105 return [ topLeftCoord, topRightCoord, bottomRightCoord, bottomLeftCoord ]
106 }
107
108 /// Reset polygon back to initial default
109 function _resetPolygon() {
110 mapPolygon.beginReset()
111 mapPolygon.clear()
112 mapPolygon.appendVertices(defaultPolygonVertices())
113 mapPolygon.endReset()
114 _circleMode = false
115 }
116
117 function _createCircularPolygon(center, radius) {
118 var unboundCenter = center.atDistanceAndAzimuth(0, 0)
119 var segments = 16
120 var angleIncrement = 360 / segments
121 var angle = 0
122 mapPolygon.beginReset()
123 mapPolygon.clear()
124 _circleRadius = radius
125 for (var i=0; i<segments; i++) {
126 var vertex = unboundCenter.atDistanceAndAzimuth(radius, angle)
127 mapPolygon.appendVertex(vertex)
128 angle += angleIncrement
129 }
130 mapPolygon.endReset()
131 _circleMode = true
132 }
133
134 /// Reset polygon to a circle which fits within initial polygon
135 function _resetCircle() {
136 var initialVertices = defaultPolygonVertices()
137 var width = initialVertices[0].distanceTo(initialVertices[1])
138 var height = initialVertices[1].distanceTo(initialVertices[2])
139 var radius = Math.min(width, height) / 2
140 var center = initialVertices[0].atDistanceAndAzimuth(width / 2, 90).atDistanceAndAzimuth(height / 2, 180)
141 _createCircularPolygon(center, radius)
142 }
143
144 function _handleInteractiveChanged() {
145 if (interactive) {
146 addEditingVisuals()
147 addToolbarVisuals()
148 } else {
149 mapPolygon.traceMode = false
150 removeEditingVisuals()
151 removeToolVisuals()
152 }
153 }
154
155 function _saveCurrentVertices() {
156 _savedVertices = [ ]
157 _savedCircleMode = _circleMode
158 for (var i=0; i<mapPolygon.count; i++) {
159 _savedVertices.push(mapPolygon.vertexCoordinate(i))
160 }
161 }
162
163 function _restorePreviousVertices() {
164 mapPolygon.beginReset()
165 mapPolygon.clear()
166 for (var i=0; i<_savedVertices.length; i++) {
167 mapPolygon.appendVertex(_savedVertices[i])
168 }
169 mapPolygon.endReset()
170 _circleMode = _savedCircleMode
171 }
172
173 onInteractiveChanged: _handleInteractiveChanged()
174
175 on_CircleModeChanged: {
176 if (_circleMode) {
177 addCircleVisuals()
178 } else {
179 _objMgrCircleVisuals.destroyObjects()
180 }
181 }
182
183 Connections {
184 target: mapPolygon
185 function onTraceModeChanged(traceMode) {
186 if (traceMode) {
187 _instructionText = _traceText
188 _objMgrTraceVisuals.createObject(traceMouseAreaComponent, mapControl, false)
189 } else {
190 _instructionText = _polygonToolsText
191 _objMgrTraceVisuals.destroyObjects()
192 }
193 }
194 }
195
196 Component.onCompleted: {
197 addCommonVisuals()
198 _handleInteractiveChanged()
199 }
200 Component.onDestruction: mapPolygon.traceMode = false
201
202 QGCDynamicObjectManager { id: _objMgrCommonVisuals }
203 QGCDynamicObjectManager { id: _objMgrToolVisuals }
204 QGCDynamicObjectManager { id: _objMgrEditingVisuals }
205 QGCDynamicObjectManager { id: _objMgrTraceVisuals }
206 QGCDynamicObjectManager { id: _objMgrCircleVisuals }
207
208 QGCPalette { id: qgcPal }
209
210 KMLOrSHPFileDialog {
211 id: kmlOrSHPLoadDialog
212 title: qsTr("Select Polygon File")
213
214 onAcceptedForLoad: (file) => {
215 mapPolygon.loadKMLOrSHPFile(file)
216 mapFitFunctions.fitMapViewportToMissionItems()
217 close()
218 }
219 }
220
221 QGCMenu {
222 id: menu
223
224 property int _editingVertexIndex: -1
225
226 function popupVertex(curIndex) {
227 menu._editingVertexIndex = curIndex
228 removeVertexItem.visible = (mapPolygon.count > 3 && menu._editingVertexIndex >= 0)
229 menu.popup()
230 }
231
232 function popupCenter() {
233 menu.popup()
234 }
235
236 QGCMenuItem {
237 id: removeVertexItem
238 visible: !_circleMode
239 text: qsTr("Remove vertex")
240 onTriggered: {
241 if (menu._editingVertexIndex >= 0) {
242 mapPolygon.removeVertex(menu._editingVertexIndex)
243 }
244 }
245 }
246
247 QGCMenuSeparator {
248 visible: removeVertexItem.visible
249 }
250
251 QGCMenuItem {
252 text: qsTr("Set radius..." )
253 visible: _circleMode
254 onTriggered: _editCircleRadius = true
255 }
256
257 QGCMenuItem {
258 text: qsTr("Edit position..." )
259 visible: _circleMode
260 onTriggered: editCenterPositionDialogFactory.open()
261 }
262
263 QGCMenuItem {
264 text: qsTr("Edit position..." )
265 visible: !_circleMode && menu._editingVertexIndex >= 0
266 onTriggered: editVertexPositionDialogFactory.open()
267 }
268 }
269
270 Component {
271 id: polygonComponent
272
273 MapPolygon {
274 color: mapPolygon.showAltColor ? altColor : interiorColor
275 opacity: interiorOpacity
276 border.color: borderColor
277 border.width: borderWidth
278 path: mapPolygon.path
279 }
280 }
281
282 Component {
283 id: edgeLengthHandleComponent
284
285 MapQuickItem {
286 id: mapQuickItem
287 anchorPoint.x: sourceItem.width / 2
288 anchorPoint.y: sourceItem.height / 2
289 visible: !_circleMode
290
291 property int vertexIndex
292 property real distance
293
294 property var _unitsConversion: QGroundControl.unitsConversion
295
296 sourceItem: Text {
297 text: _unitsConversion.metersToAppSettingsHorizontalDistanceUnits(distance).toFixed(1) + " " +
298 _unitsConversion.appSettingsHorizontalDistanceUnitsString
299 color: "white"
300 }
301 }
302 }
303
304 Component {
305 id: edgeLengthHandlesComponent
306
307 Repeater {
308 model: _isVertexBeingDragged ? mapPolygon.path : undefined
309
310 delegate: Item {
311 property var _edgeLengthHandle
312 property var _vertices: mapPolygon.path
313
314 function _setHandlePosition() {
315 var nextIndex = index + 1
316 if (nextIndex > _vertices.length - 1) {
317 nextIndex = 0
318 }
319 var distance = _vertices[index].distanceTo(_vertices[nextIndex])
320 var azimuth = _vertices[index].azimuthTo(_vertices[nextIndex])
321 _edgeLengthHandle.coordinate =_vertices[index].atDistanceAndAzimuth(distance / 3, azimuth)
322 _edgeLengthHandle.distance = distance
323 }
324
325 Component.onCompleted: {
326 _edgeLengthHandle = edgeLengthHandleComponent.createObject(mapControl)
327 _edgeLengthHandle.vertexIndex = index
328 _setHandlePosition()
329 mapControl.addMapItem(_edgeLengthHandle)
330 }
331
332 Component.onDestruction: {
333 if (_edgeLengthHandle) {
334 _edgeLengthHandle.destroy()
335 }
336 }
337 }
338 }
339 }
340
341 Component {
342 id: splitHandleComponent
343
344 MapQuickItem {
345 id: mapQuickItem
346 anchorPoint.x: sourceItem.width / 2
347 anchorPoint.y: sourceItem.height / 2
348 visible: !_circleMode
349
350 property int vertexIndex
351
352 sourceItem: SplitIndicator {
353 z: _zorderSplitHandle
354 onClicked: if(_root.interactive) mapPolygon.splitPolygonSegment(mapQuickItem.vertexIndex)
355 }
356 }
357 }
358
359 Component {
360 id: splitHandlesComponent
361
362 Repeater {
363 model: mapPolygon.path
364
365 delegate: Item {
366 property var _splitHandle
367 property var _vertices: mapPolygon.path
368
369 function _setHandlePosition() {
370 var nextIndex = index + 1
371 if (nextIndex > _vertices.length - 1) {
372 nextIndex = 0
373 }
374 var distance = _vertices[index].distanceTo(_vertices[nextIndex])
375 var azimuth = _vertices[index].azimuthTo(_vertices[nextIndex])
376 _splitHandle.coordinate = _vertices[index].atDistanceAndAzimuth(distance / 2, azimuth)
377 }
378
379 Component.onCompleted: {
380 _splitHandle = splitHandleComponent.createObject(mapControl)
381 _splitHandle.vertexIndex = index
382 _setHandlePosition()
383 mapControl.addMapItem(_splitHandle)
384 }
385
386 Component.onDestruction: {
387 if (_splitHandle) {
388 _splitHandle.destroy()
389 }
390 }
391 }
392 }
393 }
394
395 // Control which is used to drag polygon vertices
396 Component {
397 id: dragAreaComponent
398
399 MissionItemIndicatorDrag {
400 id: dragArea
401 mapControl: _root.mapControl
402 z: _zorderDragHandle
403 visible: !_circleMode
404 onDragStart: _isVertexBeingDragged = true
405 onDragStop: { _isVertexBeingDragged = false; mapPolygon.verifyClockwiseWinding() }
406
407 property int polygonVertex
408
409 property bool _creationComplete: false
410
411 Component.onCompleted: _creationComplete = true
412
413 onItemCoordinateChanged: {
414 if (_creationComplete) {
415 // During component creation some bad coordinate values got through which screws up draw
416 mapPolygon.adjustVertex(polygonVertex, itemCoordinate)
417 }
418 }
419
420 onClicked: if(_root.interactive) menu.popupVertex(polygonVertex)
421 }
422 }
423
424 Component {
425 id: centerDragHandle
426 MapQuickItem {
427 id: mapQuickItem
428 anchorPoint.x: dragHandle.width * 0.5
429 anchorPoint.y: dragHandle.height * 0.5
430 z: _zorderDragHandle
431 sourceItem: Rectangle {
432 id: dragHandle
433 width: ScreenTools.defaultFontPixelHeight * 1.5
434 height: width
435 radius: width * 0.5
436 color: Qt.rgba(1,1,1,0.8)
437 border.color: Qt.rgba(0,0,0,0.25)
438 border.width: 1
439 QGCColoredImage {
440 width: parent.width
441 height: width
442 color: Qt.rgba(0,0,0,1)
443 mipmap: true
444 fillMode: Image.PreserveAspectFit
445 source: "/qmlimages/MapCenter.svg"
446 sourceSize.height: height
447 anchors.centerIn: parent
448 }
449 }
450 }
451 }
452
453 Component {
454 id: dragHandleComponent
455
456 MapQuickItem {
457 id: mapQuickItem
458 anchorPoint.x: dragHandle.width / 2
459 anchorPoint.y: dragHandle.height / 2
460 z: _zorderDragHandle
461 visible: !_circleMode
462
463 property int polygonVertex
464
465 sourceItem: Rectangle {
466 id: dragHandle
467 width: ScreenTools.defaultFontPixelHeight * 1.5
468 height: width
469 radius: width * 0.5
470 color: Qt.rgba(1,1,1,0.8)
471 border.color: Qt.rgba(0,0,0,0.25)
472 border.width: 1
473 }
474 }
475 }
476
477 // Add all polygon vertex drag handles to the map
478 Component {
479 id: dragHandlesComponent
480
481 Repeater {
482 model: mapPolygon.pathModel
483
484 delegate: Item {
485 property var _visuals: [ ]
486
487 Component.onCompleted: {
488 var dragHandle = dragHandleComponent.createObject(mapControl)
489 dragHandle.coordinate = Qt.binding(function() { return object.coordinate })
490 dragHandle.polygonVertex = Qt.binding(function() { return index })
491 mapControl.addMapItem(dragHandle)
492 var dragArea = dragAreaComponent.createObject(mapControl, { "itemIndicator": dragHandle, "itemCoordinate": object.coordinate })
493 dragArea.polygonVertex = Qt.binding(function() { return index })
494 _visuals.push(dragHandle)
495 _visuals.push(dragArea)
496 }
497
498 Component.onDestruction: {
499 for (var i=0; i<_visuals.length; i++) {
500 _visuals[i].destroy()
501 }
502 _visuals = [ ]
503 }
504 }
505 }
506 }
507
508 QGCPopupDialogFactory {
509 id: editCenterPositionDialogFactory
510
511 dialogComponent: editCenterPositionDialog
512 }
513
514 Component {
515 id: editCenterPositionDialog
516
517 EditPositionDialog {
518 title: qsTr("Edit Center Position")
519 coordinate: mapPolygon.center
520 onCoordinateChanged: {
521 // Prevent spamming signals on vertex changes by setting centerDrag = true when changing center position.
522 // This also fixes a bug where Qt gets confused by all the signalling and draws a bad visual.
523 mapPolygon.centerDrag = true
524 mapPolygon.center = coordinate
525 mapPolygon.centerDrag = false
526 }
527 }
528 }
529
530 QGCPopupDialogFactory {
531 id: editVertexPositionDialogFactory
532
533 dialogComponent: editVertexPositionDialog
534 }
535
536 Component {
537 id: editVertexPositionDialog
538
539 EditPositionDialog {
540 title: qsTr("Edit Vertex Position")
541 coordinate: mapPolygon.vertexCoordinate(menu._editingVertexIndex)
542 onCoordinateChanged: {
543 mapPolygon.adjustVertex(menu._editingVertexIndex, coordinate)
544 mapPolygon.verifyClockwiseWinding()
545 }
546 }
547 }
548
549 Component {
550 id: centerDragAreaComponent
551
552 MissionItemIndicatorDrag {
553 mapControl: _root.mapControl
554 z: _zorderCenterHandle
555 onItemCoordinateChanged: mapPolygon.center = itemCoordinate
556 onDragStart: mapPolygon.centerDrag = true
557 onDragStop: mapPolygon.centerDrag = false
558 }
559 }
560
561 Component {
562 id: centerDragHandleComponent
563
564 Item {
565 property var dragHandle
566 property var dragArea
567
568 Component.onCompleted: {
569 dragHandle = centerDragHandle.createObject(mapControl)
570 dragHandle.coordinate = Qt.binding(function() { return mapPolygon.center })
571 mapControl.addMapItem(dragHandle)
572 dragArea = centerDragAreaComponent.createObject(mapControl, { "itemIndicator": dragHandle, "itemCoordinate": mapPolygon.center })
573 }
574
575 Component.onDestruction: {
576 dragHandle.destroy()
577 dragArea.destroy()
578 }
579 }
580 }
581
582 Component {
583 id: toolbarComponent
584
585 PlanEditToolbar {
586 anchors.horizontalCenter: mapControl.left
587 anchors.horizontalCenterOffset: mapControl.centerViewport.left + (mapControl.centerViewport.width / 2)
588 y: mapControl.centerViewport.top
589 availableWidth: mapControl.centerViewport.width
590
591 QGCButton {
592 _horizontalPadding: 0
593 text: qsTr("Basic")
594 visible: !mapPolygon.traceMode
595 onClicked: _resetPolygon()
596 }
597
598 QGCButton {
599 _horizontalPadding: 0
600 text: qsTr("Circular")
601 visible: !mapPolygon.traceMode
602 onClicked: _resetCircle()
603 }
604
605 QGCButton {
606 _horizontalPadding: 0
607 text: mapPolygon.traceMode ? qsTr("Done Tracing") : qsTr("Trace")
608 onClicked: {
609 if (mapPolygon.traceMode) {
610 if (mapPolygon.count < 3) {
611 _restorePreviousVertices()
612 }
613 mapPolygon.traceMode = false
614 } else {
615 _saveCurrentVertices()
616 _circleMode = false
617 mapPolygon.traceMode = true
618 mapPolygon.clear();
619 }
620 }
621 }
622
623 QGCButton {
624 _horizontalPadding: 0
625 text: qsTr("Load KML/SHP...")
626 onClicked: kmlOrSHPLoadDialog.openForLoad()
627 visible: !mapPolygon.traceMode
628 }
629 }
630 }
631
632 // Mouse area to capture clicks for tracing a polygon
633 Component {
634 id: traceMouseAreaComponent
635
636 MouseArea {
637 anchors.fill: mapControl
638 preventStealing: true
639 z: QGroundControl.zOrderMapItems + 1 // Over item indicators
640
641 onClicked: (mouse) => {
642 if (mouse.button === Qt.LeftButton && _root.interactive) {
643 mapPolygon.appendVertex(mapControl.toCoordinate(Qt.point(mouse.x, mouse.y), false /* clipToViewPort */))
644 }
645 }
646 }
647 }
648
649 Component {
650 id: radiusDragHandleComponent
651
652 MapQuickItem {
653 id: mapQuickItem
654 anchorPoint.x: dragHandle.width / 2
655 anchorPoint.y: dragHandle.height / 2
656 z: QGroundControl.zOrderMapItems + 2
657
658 sourceItem: Rectangle {
659 id: dragHandle
660 width: ScreenTools.defaultFontPixelHeight * 1.5
661 height: width
662 radius: width / 2
663 color: "white"
664 opacity: interiorOpacity * .90
665 }
666 }
667 }
668
669 Timer {
670 id: radiusDragDebounceTimer
671 interval: 0
672 repeat: false
673
674 property var pendingCoord: undefined
675
676 onTriggered: {
677 // re-build the circular polygon only once per event loop
678 if (pendingCoord) {
679 var coord = pendingCoord
680 pendingCoord = undefined
681 var radius = mapPolygon.center.distanceTo(coord)
682 _createCircularPolygon(mapPolygon.center, radius)
683 }
684 }
685 }
686
687 Component {
688 id: radiusDragAreaComponent
689
690 MissionItemIndicatorDrag {
691 mapControl: _root.mapControl
692
693 onItemCoordinateChanged: {
694 var radius = mapPolygon.center.distanceTo(itemCoordinate)
695
696 if (Math.abs(radius - _circleRadius) > 0.1) {
697 // De-bounced circular polygon re-drawing
698 radiusDragDebounceTimer.pendingCoord = itemCoordinate
699 radiusDragDebounceTimer.start()
700 }
701 }
702 }
703 }
704
705 Component {
706 id: radiusVisualsComponent
707
708 Item {
709 property var circleCenterCoord: mapPolygon.center
710
711 function _calcRadiusDragCoord() {
712 _circleRadiusDragCoord = circleCenterCoord.atDistanceAndAzimuth(_circleRadius, 90)
713 }
714
715 onCircleCenterCoordChanged: {
716 if (!_circleRadiusDrag) {
717 _calcRadiusDragCoord()
718 }
719 }
720
721 QGCDynamicObjectManager {
722 id: _objMgr
723 }
724
725 Component.onCompleted: {
726 _calcRadiusDragCoord()
727 var radiusDragHandle = _objMgr.createObject(radiusDragHandleComponent, mapControl, true)
728 radiusDragHandle.coordinate = Qt.binding(function() { return _circleRadiusDragCoord })
729 var radiusDragIndicator = radiusDragAreaComponent.createObject(mapControl, { "itemIndicator": radiusDragHandle, "itemCoordinate": _circleRadiusDragCoord })
730 _objMgr.addObject(radiusDragIndicator)
731 }
732 }
733 }
734}