You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
659 lines
23 KiB
659 lines
23 KiB
/**************************************************************************** |
|
* |
|
* (c) 2009-2020 QGROUNDCONTROL PROJECT <http://www.qgroundcontrol.org> |
|
* |
|
* QGroundControl is licensed according to the terms in the file |
|
* COPYING.md in the root of the source code directory. |
|
* |
|
****************************************************************************/ |
|
|
|
import QtQuick 2.11 |
|
import QtQuick.Controls 2.4 |
|
import QtLocation 5.3 |
|
import QtPositioning 5.3 |
|
import QtQuick.Dialogs 1.2 |
|
import QtQuick.Layouts 1.11 |
|
|
|
import QGroundControl 1.0 |
|
import QGroundControl.ScreenTools 1.0 |
|
import QGroundControl.Palette 1.0 |
|
import QGroundControl.Controls 1.0 |
|
import QGroundControl.FlightMap 1.0 |
|
import QGroundControl.ShapeFileHelper 1.0 |
|
|
|
/// QGCMapPolygon map visuals |
|
Item { |
|
id: _root |
|
|
|
property var mapControl ///< Map control to place item in |
|
property var mapPolygon ///< QGCMapPolygon object |
|
property bool interactive: mapPolygon.interactive |
|
property color interiorColor: "transparent" |
|
property real interiorOpacity: 1 |
|
property int borderWidth: 0 |
|
property color borderColor: "black" |
|
|
|
property bool _circleMode: false |
|
property real _circleRadius |
|
property bool _circleRadiusDrag: false |
|
property var _circleRadiusDragCoord: QtPositioning.coordinate() |
|
property bool _editCircleRadius: false |
|
property string _instructionText: _polygonToolsText |
|
property var _savedVertices: [ ] |
|
property bool _savedCircleMode |
|
|
|
property real _zorderDragHandle: QGroundControl.zOrderMapItems + 3 // Highest to prevent splitting when items overlap |
|
property real _zorderSplitHandle: QGroundControl.zOrderMapItems + 2 |
|
property real _zorderCenterHandle: QGroundControl.zOrderMapItems + 1 // Lowest such that drag or split takes precedence |
|
|
|
readonly property string _polygonToolsText: qsTr("Polygon Tools") |
|
readonly property string _traceText: qsTr("Click in the map to add vertices. Click 'Done Tracing' when finished.") |
|
|
|
function addCommonVisuals() { |
|
if (_objMgrCommonVisuals.empty) { |
|
_objMgrCommonVisuals.createObject(polygonComponent, mapControl, true) |
|
} |
|
} |
|
|
|
function removeCommonVisuals() { |
|
_objMgrCommonVisuals.destroyObjects() |
|
} |
|
|
|
function addEditingVisuals() { |
|
if (_objMgrEditingVisuals.empty) { |
|
_objMgrEditingVisuals.createObjects([ dragHandlesComponent, splitHandlesComponent, centerDragHandleComponent ], mapControl, false /* addToMap */) |
|
} |
|
} |
|
|
|
function removeEditingVisuals() { |
|
_objMgrEditingVisuals.destroyObjects() |
|
} |
|
|
|
|
|
function addToolbarVisuals() { |
|
if (_objMgrToolVisuals.empty) { |
|
_objMgrToolVisuals.createObject(toolbarComponent, mapControl) |
|
} |
|
} |
|
|
|
function removeToolVisuals() { |
|
_objMgrToolVisuals.destroyObjects() |
|
} |
|
|
|
function addCircleVisuals() { |
|
if (_objMgrCircleVisuals.empty) { |
|
_objMgrCircleVisuals.createObject(radiusVisualsComponent, mapControl) |
|
} |
|
} |
|
|
|
/// Calculate the default/initial 4 sided polygon |
|
function defaultPolygonVertices() { |
|
// Initial polygon is inset to take 2/3rds space |
|
var rect = Qt.rect(mapControl.centerViewport.x, mapControl.centerViewport.y, mapControl.centerViewport.width, mapControl.centerViewport.height) |
|
rect.x += (rect.width * 0.25) / 2 |
|
rect.y += (rect.height * 0.25) / 2 |
|
rect.width *= 0.75 |
|
rect.height *= 0.75 |
|
|
|
var centerCoord = mapControl.toCoordinate(Qt.point(rect.x + (rect.width / 2), rect.y + (rect.height / 2)), false /* clipToViewPort */) |
|
var topLeftCoord = mapControl.toCoordinate(Qt.point(rect.x, rect.y), false /* clipToViewPort */) |
|
var topRightCoord = mapControl.toCoordinate(Qt.point(rect.x + rect.width, rect.y), false /* clipToViewPort */) |
|
var bottomLeftCoord = mapControl.toCoordinate(Qt.point(rect.x, rect.y + rect.height), false /* clipToViewPort */) |
|
var bottomRightCoord = mapControl.toCoordinate(Qt.point(rect.x + rect.width, rect.y + rect.height), false /* clipToViewPort */) |
|
|
|
// Initial polygon has max width and height of 3000 meters |
|
var halfWidthMeters = Math.min(topLeftCoord.distanceTo(topRightCoord), 3000) / 2 |
|
var halfHeightMeters = Math.min(topLeftCoord.distanceTo(bottomLeftCoord), 3000) / 2 |
|
topLeftCoord = centerCoord.atDistanceAndAzimuth(halfWidthMeters, -90).atDistanceAndAzimuth(halfHeightMeters, 0) |
|
topRightCoord = centerCoord.atDistanceAndAzimuth(halfWidthMeters, 90).atDistanceAndAzimuth(halfHeightMeters, 0) |
|
bottomLeftCoord = centerCoord.atDistanceAndAzimuth(halfWidthMeters, -90).atDistanceAndAzimuth(halfHeightMeters, 180) |
|
bottomRightCoord = centerCoord.atDistanceAndAzimuth(halfWidthMeters, 90).atDistanceAndAzimuth(halfHeightMeters, 180) |
|
|
|
return [ topLeftCoord, topRightCoord, bottomRightCoord, bottomLeftCoord, centerCoord ] |
|
} |
|
|
|
/// Reset polygon back to initial default |
|
function _resetPolygon() { |
|
mapPolygon.beginReset() |
|
mapPolygon.clear() |
|
var initialVertices = defaultPolygonVertices() |
|
for (var i=0; i<4; i++) { |
|
mapPolygon.appendVertex(initialVertices[i]) |
|
} |
|
mapPolygon.endReset() |
|
_circleMode = false |
|
} |
|
|
|
function _createCircularPolygon(center, radius) { |
|
var unboundCenter = center.atDistanceAndAzimuth(0, 0) |
|
var segments = 16 |
|
var angleIncrement = 360 / segments |
|
var angle = 0 |
|
mapPolygon.beginReset() |
|
mapPolygon.clear() |
|
_circleRadius = radius |
|
for (var i=0; i<segments; i++) { |
|
var vertex = unboundCenter.atDistanceAndAzimuth(radius, angle) |
|
mapPolygon.appendVertex(vertex) |
|
angle += angleIncrement |
|
} |
|
mapPolygon.endReset() |
|
_circleMode = true |
|
} |
|
|
|
/// Reset polygon to a circle which fits within initial polygon |
|
function _resetCircle() { |
|
var initialVertices = defaultPolygonVertices() |
|
var width = initialVertices[0].distanceTo(initialVertices[1]) |
|
var height = initialVertices[1].distanceTo(initialVertices[2]) |
|
var radius = Math.min(width, height) / 2 |
|
var center = initialVertices[4] |
|
_createCircularPolygon(center, radius) |
|
} |
|
|
|
function _handleInteractiveChanged() { |
|
if (interactive) { |
|
addEditingVisuals() |
|
addToolbarVisuals() |
|
} else { |
|
mapPolygon.traceMode = false |
|
removeEditingVisuals() |
|
removeToolVisuals() |
|
} |
|
} |
|
|
|
function _saveCurrentVertices() { |
|
_savedVertices = [ ] |
|
_savedCircleMode = _circleMode |
|
for (var i=0; i<mapPolygon.count; i++) { |
|
_savedVertices.push(mapPolygon.vertexCoordinate(i)) |
|
} |
|
} |
|
|
|
function _restorePreviousVertices() { |
|
mapPolygon.beginReset() |
|
mapPolygon.clear() |
|
for (var i=0; i<_savedVertices.length; i++) { |
|
mapPolygon.appendVertex(_savedVertices[i]) |
|
} |
|
mapPolygon.endReset() |
|
_circleMode = _savedCircleMode |
|
} |
|
|
|
onInteractiveChanged: _handleInteractiveChanged() |
|
|
|
on_CircleModeChanged: { |
|
if (_circleMode) { |
|
addCircleVisuals() |
|
} else { |
|
_objMgrCircleVisuals.destroyObjects() |
|
} |
|
} |
|
|
|
Connections { |
|
target: mapPolygon |
|
onTraceModeChanged: { |
|
if (mapPolygon.traceMode) { |
|
_instructionText = _traceText |
|
_objMgrTraceVisuals.createObject(traceMouseAreaComponent, mapControl, false) |
|
} else { |
|
_instructionText = _polygonToolsText |
|
_objMgrTraceVisuals.destroyObjects() |
|
} |
|
} |
|
} |
|
|
|
Component.onCompleted: { |
|
addCommonVisuals() |
|
_handleInteractiveChanged() |
|
} |
|
Component.onDestruction: mapPolygon.traceMode = false |
|
|
|
QGCDynamicObjectManager { id: _objMgrCommonVisuals } |
|
QGCDynamicObjectManager { id: _objMgrToolVisuals } |
|
QGCDynamicObjectManager { id: _objMgrEditingVisuals } |
|
QGCDynamicObjectManager { id: _objMgrTraceVisuals } |
|
QGCDynamicObjectManager { id: _objMgrCircleVisuals } |
|
|
|
QGCPalette { id: qgcPal } |
|
|
|
KMLOrSHPFileDialog { |
|
id: kmlOrSHPLoadDialog |
|
title: qsTr("Select Polygon File") |
|
selectExisting: true |
|
|
|
onAcceptedForLoad: { |
|
mapPolygon.loadKMLOrSHPFile(file) |
|
mapFitFunctions.fitMapViewportToMissionItems() |
|
close() |
|
} |
|
} |
|
|
|
QGCMenu { |
|
id: menu |
|
|
|
property int _editingVertexIndex: -1 |
|
|
|
function popupVertex(curIndex) { |
|
menu._editingVertexIndex = curIndex |
|
removeVertexItem.visible = (mapPolygon.count > 3 && menu._editingVertexIndex >= 0) |
|
menu.popup() |
|
} |
|
|
|
function popupCenter() { |
|
menu.popup() |
|
} |
|
|
|
QGCMenuItem { |
|
id: removeVertexItem |
|
visible: !_circleMode |
|
text: qsTr("Remove vertex") |
|
onTriggered: { |
|
if (menu._editingVertexIndex >= 0) { |
|
mapPolygon.removeVertex(menu._editingVertexIndex) |
|
} |
|
} |
|
} |
|
|
|
QGCMenuSeparator { |
|
visible: removeVertexItem.visible |
|
} |
|
|
|
QGCMenuItem { |
|
text: qsTr("Set radius..." ) |
|
visible: _circleMode |
|
onTriggered: _editCircleRadius = true |
|
} |
|
|
|
QGCMenuItem { |
|
text: qsTr("Edit position..." ) |
|
visible: _circleMode |
|
onTriggered: mainWindow.showComponentDialog(editCenterPositionDialog, qsTr("Edit Center Position"), mainWindow.showDialogDefaultWidth, StandardButton.Close) |
|
} |
|
|
|
QGCMenuItem { |
|
text: qsTr("Edit position..." ) |
|
visible: !_circleMode && menu._editingVertexIndex >= 0 |
|
onTriggered: mainWindow.showComponentDialog(editVertexPositionDialog, qsTr("Edit Vertex Position"), mainWindow.showDialogDefaultWidth, StandardButton.Close) |
|
} |
|
} |
|
|
|
Component { |
|
id: polygonComponent |
|
|
|
MapPolygon { |
|
color: interiorColor |
|
opacity: interiorOpacity |
|
border.color: borderColor |
|
border.width: borderWidth |
|
path: mapPolygon.path |
|
} |
|
} |
|
|
|
Component { |
|
id: splitHandleComponent |
|
|
|
MapQuickItem { |
|
id: mapQuickItem |
|
anchorPoint.x: sourceItem.width / 2 |
|
anchorPoint.y: sourceItem.height / 2 |
|
visible: !_circleMode |
|
|
|
property int vertexIndex |
|
|
|
sourceItem: SplitIndicator { |
|
z: _zorderSplitHandle |
|
onClicked: if(_root.interactive) mapPolygon.splitPolygonSegment(mapQuickItem.vertexIndex) |
|
} |
|
} |
|
} |
|
|
|
Component { |
|
id: splitHandlesComponent |
|
|
|
Repeater { |
|
model: mapPolygon.path |
|
|
|
delegate: Item { |
|
property var _splitHandle |
|
property var _vertices: mapPolygon.path |
|
|
|
function _setHandlePosition() { |
|
var nextIndex = index + 1 |
|
if (nextIndex > _vertices.length - 1) { |
|
nextIndex = 0 |
|
} |
|
var distance = _vertices[index].distanceTo(_vertices[nextIndex]) |
|
var azimuth = _vertices[index].azimuthTo(_vertices[nextIndex]) |
|
_splitHandle.coordinate = _vertices[index].atDistanceAndAzimuth(distance / 2, azimuth) |
|
} |
|
|
|
Component.onCompleted: { |
|
_splitHandle = splitHandleComponent.createObject(mapControl) |
|
_splitHandle.vertexIndex = index |
|
_setHandlePosition() |
|
mapControl.addMapItem(_splitHandle) |
|
} |
|
|
|
Component.onDestruction: { |
|
if (_splitHandle) { |
|
_splitHandle.destroy() |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
// Control which is used to drag polygon vertices |
|
Component { |
|
id: dragAreaComponent |
|
|
|
MissionItemIndicatorDrag { |
|
id: dragArea |
|
mapControl: _root.mapControl |
|
z: _zorderDragHandle |
|
visible: !_circleMode |
|
onDragStop: mapPolygon.verifyClockwiseWinding() |
|
|
|
property int polygonVertex |
|
|
|
property bool _creationComplete: false |
|
|
|
Component.onCompleted: _creationComplete = true |
|
|
|
onItemCoordinateChanged: { |
|
if (_creationComplete) { |
|
// During component creation some bad coordinate values got through which screws up draw |
|
mapPolygon.adjustVertex(polygonVertex, itemCoordinate) |
|
} |
|
} |
|
|
|
onClicked: if(_root.interactive) menu.popupVertex(polygonVertex) |
|
} |
|
} |
|
|
|
Component { |
|
id: centerDragHandle |
|
MapQuickItem { |
|
id: mapQuickItem |
|
anchorPoint.x: dragHandle.width * 0.5 |
|
anchorPoint.y: dragHandle.height * 0.5 |
|
z: _zorderDragHandle |
|
sourceItem: Rectangle { |
|
id: dragHandle |
|
width: ScreenTools.defaultFontPixelHeight * 1.5 |
|
height: width |
|
radius: width * 0.5 |
|
color: Qt.rgba(1,1,1,0.8) |
|
border.color: Qt.rgba(0,0,0,0.25) |
|
border.width: 1 |
|
QGCColoredImage { |
|
width: parent.width |
|
height: width |
|
color: Qt.rgba(0,0,0,1) |
|
mipmap: true |
|
fillMode: Image.PreserveAspectFit |
|
source: "/qmlimages/MapCenter.svg" |
|
sourceSize.height: height |
|
anchors.centerIn: parent |
|
} |
|
} |
|
} |
|
} |
|
|
|
Component { |
|
id: dragHandleComponent |
|
|
|
MapQuickItem { |
|
id: mapQuickItem |
|
anchorPoint.x: dragHandle.width / 2 |
|
anchorPoint.y: dragHandle.height / 2 |
|
z: _zorderDragHandle |
|
visible: !_circleMode |
|
|
|
property int polygonVertex |
|
|
|
sourceItem: Rectangle { |
|
id: dragHandle |
|
width: ScreenTools.defaultFontPixelHeight * 1.5 |
|
height: width |
|
radius: width * 0.5 |
|
color: Qt.rgba(1,1,1,0.8) |
|
border.color: Qt.rgba(0,0,0,0.25) |
|
border.width: 1 |
|
} |
|
} |
|
} |
|
|
|
// Add all polygon vertex drag handles to the map |
|
Component { |
|
id: dragHandlesComponent |
|
|
|
Repeater { |
|
model: mapPolygon.pathModel |
|
|
|
delegate: Item { |
|
property var _visuals: [ ] |
|
|
|
Component.onCompleted: { |
|
var dragHandle = dragHandleComponent.createObject(mapControl) |
|
dragHandle.coordinate = Qt.binding(function() { return object.coordinate }) |
|
dragHandle.polygonVertex = Qt.binding(function() { return index }) |
|
mapControl.addMapItem(dragHandle) |
|
var dragArea = dragAreaComponent.createObject(mapControl, { "itemIndicator": dragHandle, "itemCoordinate": object.coordinate }) |
|
dragArea.polygonVertex = Qt.binding(function() { return index }) |
|
_visuals.push(dragHandle) |
|
_visuals.push(dragArea) |
|
} |
|
|
|
Component.onDestruction: { |
|
for (var i=0; i<_visuals.length; i++) { |
|
_visuals[i].destroy() |
|
} |
|
_visuals = [ ] |
|
} |
|
} |
|
} |
|
} |
|
|
|
Component { |
|
id: editCenterPositionDialog |
|
|
|
EditPositionDialog { |
|
coordinate: mapPolygon.center |
|
onCoordinateChanged: { |
|
// Prevent spamming signals on vertex changes by setting centerDrag = true when changing center position. |
|
// This also fixes a bug where Qt gets confused by all the signalling and draws a bad visual. |
|
mapPolygon.centerDrag = true |
|
mapPolygon.center = coordinate |
|
mapPolygon.centerDrag = false |
|
} |
|
} |
|
} |
|
|
|
Component { |
|
id: editVertexPositionDialog |
|
|
|
EditPositionDialog { |
|
coordinate: mapPolygon.vertexCoordinate(menu._editingVertexIndex) |
|
onCoordinateChanged: { |
|
mapPolygon.adjustVertex(menu._editingVertexIndex, coordinate) |
|
mapPolygon.verifyClockwiseWinding() |
|
} |
|
} |
|
} |
|
|
|
Component { |
|
id: centerDragAreaComponent |
|
|
|
MissionItemIndicatorDrag { |
|
mapControl: _root.mapControl |
|
z: _zorderCenterHandle |
|
onItemCoordinateChanged: mapPolygon.center = itemCoordinate |
|
onDragStart: mapPolygon.centerDrag = true |
|
onDragStop: mapPolygon.centerDrag = false |
|
} |
|
} |
|
|
|
Component { |
|
id: centerDragHandleComponent |
|
|
|
Item { |
|
property var dragHandle |
|
property var dragArea |
|
|
|
Component.onCompleted: { |
|
dragHandle = centerDragHandle.createObject(mapControl) |
|
dragHandle.coordinate = Qt.binding(function() { return mapPolygon.center }) |
|
mapControl.addMapItem(dragHandle) |
|
dragArea = centerDragAreaComponent.createObject(mapControl, { "itemIndicator": dragHandle, "itemCoordinate": mapPolygon.center }) |
|
} |
|
|
|
Component.onDestruction: { |
|
dragHandle.destroy() |
|
dragArea.destroy() |
|
} |
|
} |
|
} |
|
|
|
Component { |
|
id: toolbarComponent |
|
|
|
PlanEditToolbar { |
|
anchors.horizontalCenter: mapControl.left |
|
anchors.horizontalCenterOffset: mapControl.centerViewport.left + (mapControl.centerViewport.width / 2) |
|
y: mapControl.centerViewport.top |
|
z: QGroundControl.zOrderMapItems + 2 |
|
availableWidth: mapControl.centerViewport.width |
|
|
|
QGCButton { |
|
_horizontalPadding: 0 |
|
text: qsTr("Basic") |
|
visible: !mapPolygon.traceMode |
|
onClicked: _resetPolygon() |
|
} |
|
|
|
QGCButton { |
|
_horizontalPadding: 0 |
|
text: qsTr("Circular") |
|
visible: !mapPolygon.traceMode |
|
onClicked: _resetCircle() |
|
} |
|
|
|
QGCButton { |
|
_horizontalPadding: 0 |
|
text: mapPolygon.traceMode ? qsTr("Done Tracing") : qsTr("Trace") |
|
onClicked: { |
|
if (mapPolygon.traceMode) { |
|
if (mapPolygon.count < 3) { |
|
_restorePreviousVertices() |
|
} |
|
mapPolygon.traceMode = false |
|
} else { |
|
_saveCurrentVertices() |
|
_circleMode = false |
|
mapPolygon.traceMode = true |
|
mapPolygon.clear(); |
|
} |
|
} |
|
} |
|
|
|
QGCButton { |
|
_horizontalPadding: 0 |
|
text: qsTr("Load KML/SHP...") |
|
onClicked: kmlOrSHPLoadDialog.openForLoad() |
|
visible: !mapPolygon.traceMode |
|
} |
|
} |
|
} |
|
|
|
// Mouse area to capture clicks for tracing a polygon |
|
Component { |
|
id: traceMouseAreaComponent |
|
|
|
MouseArea { |
|
anchors.fill: mapControl |
|
preventStealing: true |
|
z: QGroundControl.zOrderMapItems + 1 // Over item indicators |
|
|
|
onClicked: { |
|
if (mouse.button === Qt.LeftButton && _root.interactive) { |
|
mapPolygon.appendVertex(mapControl.toCoordinate(Qt.point(mouse.x, mouse.y), false /* clipToViewPort */)) |
|
} |
|
} |
|
} |
|
} |
|
|
|
Component { |
|
id: radiusDragHandleComponent |
|
|
|
MapQuickItem { |
|
id: mapQuickItem |
|
anchorPoint.x: dragHandle.width / 2 |
|
anchorPoint.y: dragHandle.height / 2 |
|
z: QGroundControl.zOrderMapItems + 2 |
|
|
|
sourceItem: Rectangle { |
|
id: dragHandle |
|
width: ScreenTools.defaultFontPixelHeight * 1.5 |
|
height: width |
|
radius: width / 2 |
|
color: "white" |
|
opacity: interiorOpacity * .90 |
|
} |
|
} |
|
} |
|
|
|
Component { |
|
id: radiusDragAreaComponent |
|
|
|
MissionItemIndicatorDrag { |
|
mapControl: _root.mapControl |
|
|
|
property real _lastRadius |
|
|
|
onItemCoordinateChanged: { |
|
var radius = mapPolygon.center.distanceTo(itemCoordinate) |
|
// Prevent signalling re-entrancy |
|
if (!_circleRadiusDrag && Math.abs(radius - _lastRadius) > 0.1) { |
|
_circleRadiusDrag = true |
|
_createCircularPolygon(mapPolygon.center, radius) |
|
_circleRadiusDragCoord = itemCoordinate |
|
_circleRadiusDrag = false |
|
_lastRadius = radius |
|
} |
|
} |
|
} |
|
} |
|
|
|
Component { |
|
id: radiusVisualsComponent |
|
|
|
Item { |
|
property var circleCenterCoord: mapPolygon.center |
|
|
|
function _calcRadiusDragCoord() { |
|
_circleRadiusDragCoord = circleCenterCoord.atDistanceAndAzimuth(_circleRadius, 90) |
|
} |
|
|
|
onCircleCenterCoordChanged: { |
|
if (!_circleRadiusDrag) { |
|
_calcRadiusDragCoord() |
|
} |
|
} |
|
|
|
QGCDynamicObjectManager { |
|
id: _objMgr |
|
} |
|
|
|
Component.onCompleted: { |
|
_calcRadiusDragCoord() |
|
var radiusDragHandle = _objMgr.createObject(radiusDragHandleComponent, mapControl, true) |
|
radiusDragHandle.coordinate = Qt.binding(function() { return _circleRadiusDragCoord }) |
|
var radiusDragIndicator = radiusDragAreaComponent.createObject(mapControl, { "itemIndicator": radiusDragHandle, "itemCoordinate": _circleRadiusDragCoord }) |
|
_objMgr.addObject(radiusDragIndicator) |
|
} |
|
} |
|
} |
|
} |
|
|
|
|