9import QGroundControl.Controls
10import QGroundControl.FlightMap
11import QGroundControl.PlanView
13/// QGCMapPolygon map visuals
17 property var mapControl ///< Map control to place item in
18 property var mapPolygon ///< QGCMapPolygon object
19 property bool interactive: mapPolygon.interactive
20 property color interiorColor: "transparent"
21 property color altColor: "transparent"
22 property real interiorOpacity: 1
23 property int borderWidth: 0
24 property color borderColor: "black"
26 property bool _circleMode: false
27 property real _circleRadius
28 property bool _circleRadiusDrag: false
29 property var _circleRadiusDragCoord: QtPositioning.coordinate()
30 property string _instructionText: _polygonToolsText
31 property var _savedVertices: [ ]
32 property bool _savedCircleMode
33 property bool _isVertexBeingDragged: false
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
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.")
42 function addCommonVisuals() {
43 if (_objMgrCommonVisuals.empty) {
44 _objMgrCommonVisuals.createObject(polygonComponent, mapControl, true)
48 function removeCommonVisuals() {
49 _objMgrCommonVisuals.destroyObjects()
52 function addEditingVisuals() {
53 if (_objMgrEditingVisuals.empty) {
54 _objMgrEditingVisuals.createObjects(
55 [ dragHandlesComponent, splitHandlesComponent, centerDragHandleComponent, edgeLengthHandlesComponent ],
61 function removeEditingVisuals() {
62 _objMgrEditingVisuals.destroyObjects()
65 function addToolbarVisuals() {
66 if (_objMgrToolVisuals.empty) {
67 var toolbar = _objMgrToolVisuals.createObject(toolbarComponent, mapControl)
68 toolbar.z = QGroundControl.zOrderWidgets
72 function removeToolVisuals() {
73 _objMgrToolVisuals.destroyObjects()
76 function addCircleVisuals() {
77 if (_objMgrCircleVisuals.empty) {
78 _objMgrCircleVisuals.createObject(radiusVisualsComponent, mapControl)
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
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 */)
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)
105 return [ topLeftCoord, topRightCoord, bottomRightCoord, bottomLeftCoord ]
108 /// Reset polygon back to initial default
109 function _resetPolygon() {
110 mapPolygon.beginReset()
112 mapPolygon.appendVertices(defaultPolygonVertices())
113 mapPolygon.endReset()
117 function _createCircularPolygon(center, radius) {
118 var unboundCenter = center.atDistanceAndAzimuth(0, 0)
120 var angleIncrement = 360 / segments
122 mapPolygon.beginReset()
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
130 mapPolygon.endReset()
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)
144 function _handleInteractiveChanged() {
149 mapPolygon.traceMode = false
150 removeEditingVisuals()
155 function _saveCurrentVertices() {
157 _savedCircleMode = _circleMode
158 for (var i=0; i<mapPolygon.count; i++) {
159 _savedVertices.push(mapPolygon.vertexCoordinate(i))
163 function _restorePreviousVertices() {
164 mapPolygon.beginReset()
166 for (var i=0; i<_savedVertices.length; i++) {
167 mapPolygon.appendVertex(_savedVertices[i])
169 mapPolygon.endReset()
170 _circleMode = _savedCircleMode
173 onInteractiveChanged: _handleInteractiveChanged()
175 on_CircleModeChanged: {
179 _objMgrCircleVisuals.destroyObjects()
185 function onTraceModeChanged(traceMode) {
187 _instructionText = _traceText
188 _objMgrTraceVisuals.createObject(traceMouseAreaComponent, mapControl, false)
190 _instructionText = _polygonToolsText
191 _objMgrTraceVisuals.destroyObjects()
196 Component.onCompleted: {
198 _handleInteractiveChanged()
200 Component.onDestruction: mapPolygon.traceMode = false
202 QGCDynamicObjectManager { id: _objMgrCommonVisuals }
203 QGCDynamicObjectManager { id: _objMgrToolVisuals }
204 QGCDynamicObjectManager { id: _objMgrEditingVisuals }
205 QGCDynamicObjectManager { id: _objMgrTraceVisuals }
206 QGCDynamicObjectManager { id: _objMgrCircleVisuals }
208 QGCPalette { id: qgcPal }
211 id: kmlOrSHPLoadDialog
212 title: qsTr("Select Polygon File")
214 onAcceptedForLoad: (file) => {
215 mapPolygon.loadKMLOrSHPFile(file)
216 mapFitFunctions.fitMapViewportToMissionItems()
224 property int _editingVertexIndex: -1
226 function popupVertex(curIndex) {
227 menu._editingVertexIndex = curIndex
228 removeVertexItem.visible = (mapPolygon.count > 3 && menu._editingVertexIndex >= 0)
232 function popupCenter() {
238 visible: !_circleMode
239 text: qsTr("Remove vertex")
241 if (menu._editingVertexIndex >= 0) {
242 mapPolygon.removeVertex(menu._editingVertexIndex)
248 visible: removeVertexItem.visible
252 text: qsTr("Set radius..." )
254 onTriggered: editCircleRadiusDialogFactory.open()
258 text: qsTr("Edit position..." )
260 onTriggered: editCenterPositionDialogFactory.open()
264 text: qsTr("Edit position..." )
265 visible: !_circleMode && menu._editingVertexIndex >= 0
266 onTriggered: editVertexPositionDialogFactory.open()
274 color: mapPolygon.showAltColor ? altColor : interiorColor
275 opacity: interiorOpacity
276 visible: _root.visible
277 border.color: mapPolygon.vertexDrag ? "orange" : borderColor
278 border.width: mapPolygon.vertexDrag ? 3 : borderWidth
279 path: mapPolygon.vertexDrag ? mapPolygon.dragPath : mapPolygon.path
284 id: edgeLengthHandleComponent
288 anchorPoint.x: sourceItem.width / 2
289 anchorPoint.y: sourceItem.height / 2
290 visible: !_circleMode
292 property int vertexIndex
293 property real distance
295 property var _unitsConversion: QGroundControl.unitsConversion
298 text: _unitsConversion.metersToAppSettingsHorizontalDistanceUnits(distance).toFixed(1) + " " +
299 _unitsConversion.appSettingsHorizontalDistanceUnitsString
306 id: edgeLengthHandlesComponent
309 model: _isVertexBeingDragged ? mapPolygon.path : undefined
312 property var _edgeLengthHandle
314 function _setHandlePosition() {
315 var vertices = mapPolygon.vertexDrag ? mapPolygon.dragPath : mapPolygon.path
316 var nextIndex = index + 1
317 if (nextIndex > vertices.length - 1) {
320 var distance = vertices[index].distanceTo(vertices[nextIndex])
321 var azimuth = vertices[index].azimuthTo(vertices[nextIndex])
322 _edgeLengthHandle.coordinate = vertices[index].atDistanceAndAzimuth(distance / 2, azimuth)
323 _edgeLengthHandle.distance = distance
328 function onDragPathChanged() { _setHandlePosition() }
331 Component.onCompleted: {
332 _edgeLengthHandle = edgeLengthHandleComponent.createObject(mapControl)
333 _edgeLengthHandle.vertexIndex = index
335 mapControl.addMapItem(_edgeLengthHandle)
338 Component.onDestruction: {
339 if (_edgeLengthHandle) {
340 _edgeLengthHandle.destroy()
348 id: splitHandleComponent
352 anchorPoint.x: sourceItem.width / 2
353 anchorPoint.y: sourceItem.height / 2
354 visible: !_circleMode && !_isVertexBeingDragged && !mapPolygon.centerDrag
356 property int vertexIndex
358 sourceItem: SplitIndicator {
359 z: _zorderSplitHandle
360 onClicked: if(_root.interactive) mapPolygon.splitPolygonSegment(mapQuickItem.vertexIndex)
366 id: splitHandlesComponent
369 model: mapPolygon.path
372 property var _splitHandle
373 property var _vertices: mapPolygon.path
375 function _setHandlePosition() {
376 var nextIndex = index + 1
377 if (nextIndex > _vertices.length - 1) {
380 var distance = _vertices[index].distanceTo(_vertices[nextIndex])
381 var azimuth = _vertices[index].azimuthTo(_vertices[nextIndex])
382 _splitHandle.coordinate = _vertices[index].atDistanceAndAzimuth(distance / 2, azimuth)
385 Component.onCompleted: {
386 _splitHandle = splitHandleComponent.createObject(mapControl)
387 _splitHandle.vertexIndex = index
389 mapControl.addMapItem(_splitHandle)
392 Component.onDestruction: {
394 _splitHandle.destroy()
401 // Control which is used to drag polygon vertices
403 id: dragAreaComponent
405 MissionItemIndicatorDrag {
407 mapControl: _root.mapControl
409 visible: !_circleMode
410 onDragStart: { _isVertexBeingDragged = true; mapPolygon.vertexDrag = true }
411 onDragStop: { _isVertexBeingDragged = false; mapPolygon.vertexDrag = false; mapPolygon.verifyClockwiseWinding() }
413 property int polygonVertex
415 property bool _creationComplete: false
417 Component.onCompleted: _creationComplete = true
419 onItemCoordinateChanged: {
420 if (_creationComplete) {
421 // During component creation some bad coordinate values got through which screws up draw
422 mapPolygon.adjustVertex(polygonVertex, itemCoordinate)
426 onClicked: if(_root.interactive) menu.popupVertex(polygonVertex)
434 anchorPoint.x: dragHandle.width * 0.5
435 anchorPoint.y: dragHandle.height * 0.5
437 visible: !_isVertexBeingDragged
438 sourceItem: Rectangle {
440 width: ScreenTools.defaultFontPixelHeight * 1.5
443 color: Qt.rgba(1,1,1,0.8)
444 border.color: Qt.rgba(0,0,0,0.25)
449 color: Qt.rgba(0,0,0,1)
451 fillMode: Image.PreserveAspectFit
452 source: "/qmlimages/MapCenter.svg"
453 sourceSize.height: height
454 anchors.centerIn: parent
461 id: dragHandleComponent
465 anchorPoint.x: dragHandle.width / 2
466 anchorPoint.y: dragHandle.height / 2
468 visible: !_circleMode && !mapPolygon.centerDrag
470 property int polygonVertex
472 sourceItem: Rectangle {
474 width: ScreenTools.defaultFontPixelHeight * 1.5
477 color: Qt.rgba(1,1,1,0.8)
478 border.color: Qt.rgba(0,0,0,0.25)
484 // Add all polygon vertex drag handles to the map
486 id: dragHandlesComponent
489 model: mapPolygon.pathModel
492 property var _visuals: [ ]
494 Component.onCompleted: {
495 var dragHandle = dragHandleComponent.createObject(mapControl)
496 dragHandle.coordinate = Qt.binding(function() { return object.coordinate })
497 dragHandle.polygonVertex = Qt.binding(function() { return index })
498 mapControl.addMapItem(dragHandle)
499 var dragArea = dragAreaComponent.createObject(mapControl, { "itemIndicator": dragHandle, "itemCoordinate": object.coordinate })
500 dragArea.polygonVertex = Qt.binding(function() { return index })
501 _visuals.push(dragHandle)
502 _visuals.push(dragArea)
505 Component.onDestruction: {
506 for (var i=0; i<_visuals.length; i++) {
507 _visuals[i].destroy()
515 QGCPopupDialogFactory {
516 id: editCircleRadiusDialogFactory
518 dialogComponent: editCircleRadiusDialog
522 id: editCircleRadiusDialog
526 title: qsTr("Set Radius")
527 buttons: Dialog.Save | Dialog.Cancel
530 const appRadius = Number(radiusField.text)
531 if (!isNaN(appRadius) && appRadius > 0) {
532 const radiusMeters = QGroundControl.unitsConversion.appSettingsHorizontalDistanceUnitsToMeters(appRadius)
533 _createCircularPolygon(mapPolygon.center, radiusMeters)
540 width: ScreenTools.defaultFontPixelWidth * 30
541 spacing: ScreenTools.defaultFontPixelHeight
544 Layout.fillWidth: true
545 text: qsTr("Enter circle radius.")
546 wrapMode: Text.WordWrap
551 Layout.fillWidth: true
552 text: QGroundControl.unitsConversion.metersToAppSettingsHorizontalDistanceUnits(_circleRadius).toFixed(1)
553 validator: DoubleValidator { bottom: 0.1; notation: DoubleValidator.StandardNotation }
554 inputMethodHints: Qt.ImhFormattedNumbersOnly
558 Layout.fillWidth: true
559 text: QGroundControl.unitsConversion.appSettingsHorizontalDistanceUnitsString
565 QGCPopupDialogFactory {
566 id: editCenterPositionDialogFactory
568 dialogComponent: editCenterPositionDialog
572 id: editCenterPositionDialog
575 title: qsTr("Edit Center Position")
576 coordinate: mapPolygon.center
577 onCoordinateChanged: {
578 // Prevent spamming signals on vertex changes by setting centerDrag = true when changing center position.
579 // This also fixes a bug where Qt gets confused by all the signalling and draws a bad visual.
580 mapPolygon.centerDrag = true
581 mapPolygon.center = coordinate
582 mapPolygon.centerDrag = false
587 QGCPopupDialogFactory {
588 id: editVertexPositionDialogFactory
590 dialogComponent: editVertexPositionDialog
594 id: editVertexPositionDialog
597 title: qsTr("Edit Vertex Position")
598 coordinate: mapPolygon.vertexCoordinate(menu._editingVertexIndex)
599 onCoordinateChanged: {
600 mapPolygon.adjustVertex(menu._editingVertexIndex, coordinate)
601 mapPolygon.verifyClockwiseWinding()
607 id: centerDragAreaComponent
609 MissionItemIndicatorDrag {
610 mapControl: _root.mapControl
611 z: _zorderCenterHandle
612 onItemCoordinateChanged: mapPolygon.center = itemCoordinate
613 onDragStart: { mapPolygon.centerDrag = true; mapPolygon.vertexDrag = true }
614 onDragStop: { mapPolygon.centerDrag = false; mapPolygon.vertexDrag = false }
615 onClicked: if(_root.interactive) menu.popupCenter()
620 id: centerDragHandleComponent
623 property var dragHandle
624 property var dragArea
626 Component.onCompleted: {
627 dragHandle = centerDragHandle.createObject(mapControl)
628 dragHandle.coordinate = Qt.binding(function() { return mapPolygon.centerDrag ? mapPolygon.dragCenter : mapPolygon.center })
629 mapControl.addMapItem(dragHandle)
630 dragArea = centerDragAreaComponent.createObject(mapControl, { "itemIndicator": dragHandle, "itemCoordinate": mapPolygon.center })
633 Component.onDestruction: {
644 anchors.horizontalCenter: mapControl.left
645 anchors.horizontalCenterOffset: mapControl.centerViewport.left + (mapControl.centerViewport.width / 2)
646 y: mapControl.centerViewport.top
647 availableWidth: mapControl.centerViewport.width
650 _horizontalPadding: 0
652 visible: !mapPolygon.traceMode
653 onClicked: _resetPolygon()
657 _horizontalPadding: 0
658 text: qsTr("Circular")
659 visible: !mapPolygon.traceMode
660 onClicked: _resetCircle()
664 _horizontalPadding: 0
665 text: mapPolygon.traceMode ? qsTr("Done Tracing") : qsTr("Trace")
667 if (mapPolygon.traceMode) {
668 if (mapPolygon.count < 3) {
669 _restorePreviousVertices()
671 mapPolygon.traceMode = false
673 _saveCurrentVertices()
675 mapPolygon.traceMode = true
682 _horizontalPadding: 0
683 text: qsTr("Load KML/SHP...")
684 onClicked: kmlOrSHPLoadDialog.openForLoad()
685 visible: !mapPolygon.traceMode
690 // Mouse area to capture clicks for tracing a polygon
692 id: traceMouseAreaComponent
695 anchors.fill: mapControl
696 preventStealing: true
697 z: QGroundControl.zOrderMapItems + 1 // Over item indicators
699 onClicked: (mouse) => {
700 if (mouse.button === Qt.LeftButton && _root.interactive) {
701 mapPolygon.appendVertex(mapControl.toCoordinate(Qt.point(mouse.x, mouse.y), false /* clipToViewPort */))
708 id: radiusDragHandleComponent
712 anchorPoint.x: dragHandle.width / 2
713 anchorPoint.y: dragHandle.height / 2
714 z: QGroundControl.zOrderMapItems + 2
715 visible: !mapPolygon.centerDrag
717 sourceItem: Rectangle {
719 width: ScreenTools.defaultFontPixelHeight * 1.5
723 opacity: interiorOpacity * .90
729 id: radiusDragDebounceTimer
733 property var pendingCoord: undefined
736 // re-build the circular polygon only once per event loop
738 var coord = pendingCoord
739 pendingCoord = undefined
740 var radius = mapPolygon.center.distanceTo(coord)
741 _createCircularPolygon(mapPolygon.center, radius)
747 id: radiusDragAreaComponent
749 MissionItemIndicatorDrag {
750 mapControl: _root.mapControl
752 _circleRadiusDrag = true
753 mapPolygon.vertexDrag = true
756 _circleRadiusDrag = false
757 mapPolygon.vertexDrag = false
760 onItemCoordinateChanged: {
761 // Keep the handle visually attached to the cursor while radius updates are de-bounced.
762 _circleRadiusDragCoord = itemCoordinate
764 var radius = mapPolygon.center.distanceTo(itemCoordinate)
766 if (Math.abs(radius - _circleRadius) > 0.1) {
767 // De-bounced circular polygon re-drawing
768 radiusDragDebounceTimer.pendingCoord = itemCoordinate
769 radiusDragDebounceTimer.start()
776 id: radiusVisualsComponent
779 property var circleCenterCoord: mapPolygon.center
781 function _calcRadiusDragCoord() {
782 _circleRadiusDragCoord = circleCenterCoord.atDistanceAndAzimuth(_circleRadius, 90)
785 onCircleCenterCoordChanged: {
786 if (!_circleRadiusDrag) {
787 _calcRadiusDragCoord()
791 QGCDynamicObjectManager {
795 Component.onCompleted: {
796 _calcRadiusDragCoord()
797 var radiusDragHandle = _objMgr.createObject(radiusDragHandleComponent, mapControl, true)
798 radiusDragHandle.coordinate = Qt.binding(function() { return _circleRadiusDragCoord })
799 var radiusDragIndicator = radiusDragAreaComponent.createObject(mapControl, { "itemIndicator": radiusDragHandle, "itemCoordinate": _circleRadiusDragCoord })
800 _objMgr.addObject(radiusDragIndicator)