10import QGroundControl.FlightMap
11import QGroundControl.Controls
12import QGroundControl.FactControls
13import QGroundControl.FlyView
18 readonly property int _decimalPlaces: 8
19 readonly property real _margin: ScreenTools.defaultFontPixelHeight * 0.5
20 readonly property real _toolsMargin: ScreenTools.defaultFontPixelWidth * 0.75
21 readonly property real _rightPanelWidth: Math.min(width / 3, ScreenTools.defaultFontPixelWidth * 30)
23 property var _planMasterController: planMasterController
24 property var _missionController: _planMasterController.missionController
25 property var _geoFenceController: _planMasterController.geoFenceController
26 property var _rallyPointController: _planMasterController.rallyPointController
27 property var _visualItems: _missionController.visualItems
28 property bool _singleComplexItem: _missionController.complexMissionItemNames.length === 1
29 property int _editingLayer: _layerMission
30 property var _appSettings: QGroundControl.settingsManager.appSettings
31 property var _planViewSettings: QGroundControl.settingsManager.planViewSettings
32 property bool _promptForPlanUsageShowing: false
33 property bool _addROIOnClick: false
34 property bool _addWaypointOnClick: false
36 readonly property int _layerMission: 1
37 readonly property int _layerFence: 2
38 readonly property int _layerRally: 3
42 editorMap.zoomLevel = QGroundControl.flightMapZoom
43 editorMap.center = QGroundControl.flightMapPosition
47 function mapCenter() {
48 var coordinate = editorMap.center
49 coordinate.latitude = coordinate.latitude.toFixed(_decimalPlaces)
50 coordinate.longitude = coordinate.longitude.toFixed(_decimalPlaces)
51 coordinate.altitude = coordinate.altitude.toFixed(_decimalPlaces)
56 id: mapFitFunctions // The name for this id cannot be changed without breaking references outside of this code. Beware!
58 usePlannedHomePosition: true
59 planMasterController: _planMasterController
62 PlanMasterController {
63 id: planMasterController
66 Component.onCompleted: {
67 _planMasterController.start()
68 _missionController.setCurrentPlanViewSeqNum(0, true)
71 onPromptForPlanUsageOnVehicleChange: {
72 if (!_promptForPlanUsageShowing) {
73 _promptForPlanUsageShowing = true
74 promptForPlanUsageOnVehicleChangePopupFactory.open()
78 function waitingOnIncompleteDataMessage(save) {
79 var saveOrUpload = save ? qsTr("Save") : qsTr("Upload")
80 QGroundControl.showMessageDialog(_root, qsTr("Unable to %1").arg(saveOrUpload), qsTr("Plan has incomplete items. Complete all items and %1 again.").arg(saveOrUpload))
83 function waitingOnTerrainDataMessage(save) {
84 var saveOrUpload = save ? qsTr("Save") : qsTr("Upload")
85 QGroundControl.showMessageDialog(_root, qsTr("Unable to %1").arg(saveOrUpload), qsTr("Plan is waiting on terrain data from server for correct altitude values."))
88 function checkReadyForSaveUpload(save) {
89 if (readyForSaveState() == VisualMissionItem.NotReadyForSaveData) {
90 waitingOnIncompleteDataMessage(save)
92 } else if (readyForSaveState() == VisualMissionItem.NotReadyForSaveTerrain) {
93 waitingOnTerrainDataMessage(save)
100 if (!checkReadyForSaveUpload(false /* save */)) {
103 switch (_missionController.sendToVehiclePreCheck()) {
104 case MissionController.SendToVehiclePreCheckStateOk: sendToVehicle()
106 case MissionController.SendToVehiclePreCheckStateActiveMission: QGroundControl.showMessageDialog(_root, qsTr("Send To Vehicle"), qsTr("Current mission must be paused prior to uploading a new Plan"))
108 case MissionController.SendToVehiclePreCheckStateFirwmareVehicleMismatch: QGroundControl.showMessageDialog(_root, qsTr("Plan Upload"),
109 qsTr("This Plan was created for a different firmware or vehicle type than the firmware/vehicle type of vehicle you are uploading to. " +
110 "This can lead to errors or incorrect behavior. " +
111 "It is recommended to recreate the Plan for the correct firmware/vehicle type.\n\n" +
112 "Click 'Ok' to upload the Plan anyway."),
113 Dialog.Ok | Dialog.Cancel,
114 function() { _planMasterController.sendToVehicle() })
119 function loadFromSelectedFile() {
120 fileDialog.title = qsTr("Select Plan File")
121 fileDialog.planFiles = true
122 fileDialog.nameFilters = _planMasterController.loadNameFilters
123 fileDialog.openForLoad()
126 function saveToSelectedFile() {
127 if (!checkReadyForSaveUpload(true /* save */)) {
130 fileDialog.title = qsTr("Save Plan")
131 fileDialog.planFiles = true
132 fileDialog.nameFilters = _planMasterController.saveNameFilters
133 fileDialog.openForSave()
136 function fitViewportToItems() {
137 mapFitFunctions.fitMapViewportToMissionItems()
140 function saveKmlToSelectedFile() {
141 if (!checkReadyForSaveUpload(true /* save */)) {
144 fileDialog.title = qsTr("Save KML")
145 fileDialog.planFiles = false
146 fileDialog.nameFilters = ShapeFileHelper.fileDialogKMLFilters
147 fileDialog.openForSave()
152 target: _missionController
154 function onNewItemsFromVehicle() {
155 if (_visualItems && _visualItems.count !== 1) {
156 mapFitFunctions.fitMapViewportToMissionItems()
158 _missionController.setCurrentPlanViewSeqNum(0, true)
162 function insertSimpleItemAfterCurrent(coordinate) {
163 var nextIndex = _missionController.currentPlanViewVIIndex + 1
164 _missionController.insertSimpleMissionItem(coordinate, nextIndex, true /* makeCurrentItem */)
167 function insertROIAfterCurrent(coordinate) {
168 var nextIndex = _missionController.currentPlanViewVIIndex + 1
169 _missionController.insertROIMissionItem(coordinate, nextIndex, true /* makeCurrentItem */)
172 function insertCancelROIAfterCurrent() {
173 var nextIndex = _missionController.currentPlanViewVIIndex + 1
174 _missionController.insertCancelROIMissionItem(nextIndex, true /* makeCurrentItem */)
177 function insertComplexItemAfterCurrent(complexItemName) {
178 var nextIndex = _missionController.currentPlanViewVIIndex + 1
179 _missionController.insertComplexMissionItem(complexItemName, mapCenter(), nextIndex, true /* makeCurrentItem */)
182 function insertTakeoffItemAfterCurrent() {
183 var nextIndex = _missionController.currentPlanViewVIIndex + 1
184 _missionController.insertTakeoffItem(mapCenter(), nextIndex, true /* makeCurrentItem */)
187 function insertLandItemAfterCurrent() {
188 var nextIndex = _missionController.currentPlanViewVIIndex + 1
189 _missionController.insertLandItem(mapCenter(), nextIndex, true /* makeCurrentItem */)
194 folder: _appSettings ? _appSettings.missionSavePath : ""
196 property bool planFiles: true ///< true: working with plan files, false: working with kml file
198 onAcceptedForSave: (file) => {
200 if (_planMasterController.saveToFile(file)) {
204 _planMasterController.saveToKml(file)
209 onAcceptedForLoad: (file) => {
210 _planMasterController.loadFromFile(file)
211 _planMasterController.fitViewportToItems()
212 _missionController.setCurrentPlanViewSeqNum(0, true)
219 planMasterController: _planMasterController
220 showRallyPointsHelp: _editingLayer === _layerRally
225 anchors.left: parent.left
226 anchors.right: parent.right
227 anchors.top: planToolBar.bottom
228 anchors.bottom: parent.bottom
233 mapName: "MissionEditor"
234 allowGCSLocationCenter: true
235 allowVehicleLocationCenter: true
238 zoomLevel: QGroundControl.flightMapZoom
239 center: QGroundControl.flightMapPosition
241 // This is the center rectangle of the map which is not obscured by tools
242 property rect centerViewport: Qt.rect(_leftToolWidth + _margin, _margin, editorMap.width - _leftToolWidth - _rightToolWidth - (_margin * 2), (missionStatus.visible ? missionStatus.y : height - _margin) - _margin)
244 property real _leftToolWidth: toolStrip.x + toolStrip.width
245 property real _rightToolWidth: rightPanel.width + rightPanel.anchors.rightMargin
246 property real _nonInteractiveOpacity: 0.5
248 // Initial map position duplicates Fly view position
249 Component.onCompleted: editorMap.center = QGroundControl.flightMapPosition
251 onZoomLevelChanged: {
252 QGroundControl.flightMapZoom = editorMap.zoomLevel
255 QGroundControl.flightMapPosition = editorMap.center
258 onMapClicked: (mouse) => {
259 // Take focus to close any previous editing
260 editorMap.focus = true
262 // Collapse layer switcher on any map click
263 layerSwitcher.expanded = false
266 if (!mainWindow.allowViewSwitch()) {
269 var coordinate = editorMap.toCoordinate(Qt.point(mouse.x, mouse.y), false /* clipToViewPort */)
270 coordinate.latitude = coordinate.latitude.toFixed(_decimalPlaces)
271 coordinate.longitude = coordinate.longitude.toFixed(_decimalPlaces)
272 coordinate.altitude = coordinate.altitude.toFixed(_decimalPlaces)
274 switch (_editingLayer) {
276 if (_addROIOnClick) {
277 _addROIOnClick = false
278 if (_missionController.isROIActive) {
279 var pos = Qt.point(mouse.x, mouse.y)
280 // For some strange reason using mainWindow in mapToItem doesn't work, so we use globals.parent instead which also gets us mainWindow
281 pos = editorMap.mapToItem(globals.parent, pos)
282 var dropPanel = insertOrCancelROIDropPanelComponent.createObject(mainWindow, { mapClickCoord: coordinate, clickRect: Qt.rect(pos.x, pos.y, 0, 0) })
285 insertROIAfterCurrent(coordinate)
287 } else if (_addWaypointOnClick) {
288 insertSimpleItemAfterCurrent(coordinate)
292 if (_rallyPointController.supported) {
293 _rallyPointController.addPoint(coordinate)
299 // Add the mission item visuals to the map
301 model: _missionController.visualItems
302 delegate: MissionItemMapVisual {
304 opacity: _editingLayer == _layerMission ? 1 : editorMap._nonInteractiveOpacity
305 interactive: _editingLayer == _layerMission
306 vehicle: _planMasterController.controllerVehicle
307 onClicked: (sequenceNumber) => { _missionController.setCurrentPlanViewSeqNum(sequenceNumber, false) }
311 // Add lines between waypoints
313 showSpecialVisual: _missionController.isROIBeginCurrentItem
314 model: _missionController.simpleFlightPathSegments
315 opacity: _editingLayer == _layerMission ? 1 : editorMap._nonInteractiveOpacity
318 // Direction arrows in waypoint lines
320 model: _editingLayer == _layerMission ? _missionController.directionArrows : undefined
322 delegate: MapLineArrow {
323 fromCoord: object ? object.coordinate1 : undefined
324 toCoord: object ? object.coordinate2 : undefined
326 z: QGroundControl.zOrderWaypointLines + 1
330 // UI for splitting the current segment
333 anchorPoint.x: sourceItem.width / 2
334 anchorPoint.y: sourceItem.height / 2
335 z: QGroundControl.zOrderWaypointLines + 1
336 visible: _editingLayer == _layerMission
338 sourceItem: SplitIndicator {
339 onClicked: _missionController.insertSimpleMissionItem(splitSegmentItem.coordinate,
340 _missionController.currentPlanViewVIIndex,
341 true /* makeCurrentItem */)
344 function _updateSplitCoord() {
345 if (_missionController.splitSegment) {
346 var distance = _missionController.splitSegment.coordinate1.distanceTo(_missionController.splitSegment.coordinate2)
347 var azimuth = _missionController.splitSegment.coordinate1.azimuthTo(_missionController.splitSegment.coordinate2)
348 splitSegmentItem.coordinate = _missionController.splitSegment.coordinate1.atDistanceAndAzimuth(distance / 2, azimuth)
350 coordinate = QtPositioning.coordinate()
355 target: _missionController
356 function onSplitSegmentChanged() { splitSegmentItem._updateSplitCoord() }
360 target: _missionController.splitSegment
361 function onCoordinate1Changed() { splitSegmentItem._updateSplitCoord() }
362 function onCoordinate2Changed() { splitSegmentItem._updateSplitCoord() }
366 // Add the vehicles to the map
368 model: QGroundControl.multiVehicleManager.vehicles
369 delegate: VehicleMapItem {
371 coordinate: object.coordinate
373 size: ScreenTools.defaultFontPixelHeight * 3
374 z: QGroundControl.zOrderMapItems - 1
380 myGeoFenceController: _geoFenceController
381 interactive: _editingLayer == _layerFence
382 homePosition: _missionController.plannedHomePosition
384 opacity: _editingLayer != _layerFence ? editorMap._nonInteractiveOpacity : 1
387 RallyPointMapVisuals {
389 myRallyPointController: _rallyPointController
390 interactive: _editingLayer == _layerRally
392 opacity: _editingLayer != _layerRally ? editorMap._nonInteractiveOpacity : 1
397 //-----------------------------------------------------------
401 anchors.margins: _toolsMargin
402 anchors.left: parent.left
403 anchors.top: parent.top
404 z: QGroundControl.zOrderWidgets
405 maxHeight: parent.height - toolStrip.y
406 visible: _editingLayer == _layerMission
408 property bool _isMissionLayer: _editingLayer == _layerMission
411 target: waypointButton
413 value: _addWaypointOnClick
419 value: _addROIOnClick
422 ToolStripActionList {
423 id: toolStripActionList
426 text: qsTr("Takeoff")
427 iconSource: "/res/takeoff.svg"
428 enabled: _missionController.isInsertTakeoffValid
429 visible: toolStrip._isMissionLayer && !_planMasterController.controllerVehicle.rover
431 insertTakeoffItemAfterCurrent()
435 text: _singleComplexItem ? _missionController.complexMissionItemNames[0] : qsTr("Pattern")
436 iconSource: "/qmlimages/MapDrawShape.svg"
437 enabled: _missionController.flyThroughCommandsAllowed
438 visible: toolStrip._isMissionLayer
439 dropPanelComponent: _singleComplexItem ? undefined : patternDropPanel
441 if (_singleComplexItem) {
442 insertComplexItemAfterCurrent(_missionController.complexMissionItemNames[0])
448 text: qsTr("Waypoint")
449 iconSource: "/res/waypoint.svg"
450 visible: toolStrip._isMissionLayer
452 onTriggered: { _addWaypointOnClick = !_addWaypointOnClick; if (_addWaypointOnClick) _addROIOnClick = false }
457 iconSource: "/qmlimages/roi.svg"
458 visible: toolStrip._isMissionLayer && _planMasterController.controllerVehicle.supports.roiMode
460 onTriggered: { _addROIOnClick = !_addROIOnClick; if (_addROIOnClick) _addWaypointOnClick = false }
463 text: _planMasterController.controllerVehicle.multiRotor
465 : _missionController.isInsertLandValid && _missionController.hasLandItem
468 iconSource: "/res/rtl.svg"
469 enabled: _missionController.isInsertLandValid
470 visible: toolStrip._isMissionLayer
472 insertLandItemAfterCurrent()
477 iconSource: "/res/chevron-double-right.svg"
478 visible: missionStatus.hidden && QGroundControl.corePlugin.options.showMissionStatus
479 onTriggered: missionStatus.showMissionStatus()
484 model: toolStripActionList.model
488 anchors.margins: _toolsMargin
489 anchors.left: toolStrip.right
490 anchors.top: parent.top
491 mapControl: editorMap
497 anchors.top: parent.top
498 anchors.bottom: parent.bottom
499 anchors.right: parent.right
500 width: _rightPanelWidth
501 planMasterController: _planMasterController
503 onEditingLayerChangeRequested: (layer) => _editingLayer = layer
506 // Layer switching icons — only active icon visible; click to expand choices leftward
509 anchors.right: rightPanel.left
510 anchors.rightMargin: _toolsMargin
511 anchors.top: parent.top
512 anchors.topMargin: _toolsMargin
513 width: layerRow.width
514 height: _layerButtonSize
515 z: QGroundControl.zOrderWidgets
517 property bool expanded: false
518 property real _layerButtonSize: ScreenTools.defaultFontPixelHeight * 2.0
519 property real _spacing: ScreenTools.defaultFontPixelHeight * 0.25
521 readonly property var _layers: [
522 { layer: _layerMission, icon: "/res/waypoint.svg", nodeType: "missionGroup" },
523 { layer: _layerFence, icon: "/res/GeoFence.svg", nodeType: "fenceGroup" },
524 { layer: _layerRally, icon: "/res/RallyPoint.svg", nodeType: "rallyGroup" }
530 onTriggered: layerSwitcher.expanded = false
536 collapseTimer.restart()
542 function choose(nodeType) {
545 rightPanel.selectLayer(nodeType)
548 // Row laid out right-to-left: active icon on the right, choices expand left
551 anchors.right: parent.right
552 spacing: layerSwitcher._spacing
553 layoutDirection: Qt.RightToLeft
555 // Active layer button (always visible)
557 width: layerSwitcher._layerButtonSize
559 radius: ScreenTools.defaultBorderRadius
560 color: QGroundControl.globalPalette.buttonHighlight
563 anchors.centerIn: parent
564 width: parent.width * 0.6
566 source: layerSwitcher._layers.find(l => l.layer === _editingLayer)?.icon ?? "/res/waypoint.svg"
567 color: QGroundControl.globalPalette.buttonHighlightText
572 onClicked: layerSwitcher.toggle()
576 // Choice buttons (only layers that are NOT the current one)
578 model: layerSwitcher._layers.filter(l => l.layer !== _editingLayer)
581 required property var modelData
582 width: layerSwitcher._layerButtonSize
584 radius: ScreenTools.defaultBorderRadius
585 color: QGroundControl.globalPalette.button
587 opacity: layerSwitcher.expanded ? 1 : 0
589 Behavior on opacity { NumberAnimation { duration: 150 } }
592 anchors.centerIn: parent
593 width: parent.width * 0.6
595 source: modelData.icon
596 color: QGroundControl.globalPalette.buttonText
601 onClicked: layerSwitcher.choose(modelData.nodeType)
610 anchors.margins: _toolsMargin
611 anchors.left: _calcLeftAnchor()
612 anchors.right: rightPanel.left
613 anchors.bottom: parent.bottom
615 visible: !hidden && _editingLayer == _layerMission && QGroundControl.corePlugin.options.showMissionStatus
617 readonly property bool hidden: _planViewSettings.showMissionItemStatus.rawValue ? false : true
619 function showMissionStatus() {
620 _planViewSettings.showMissionItemStatus.rawValue = true
623 function _calcLeftAnchor() {
624 let bottomOfToolStrip = toolStrip.y + toolStrip.height
625 let largestStatsHeight = Math.max(terrainStatus.height, missionStats.height)
626 if (bottomOfToolStrip + largestStatsHeight > parent.height - missionStatus.anchors.margins) {
627 return toolStrip.right
632 function _toggleMissionStatusVisibility() {
633 _planViewSettings.showMissionItemStatus.rawValue = _planViewSettings.showMissionItemStatus.rawValue ? false : true
637 id: missionStatsButtonLayout
638 Layout.alignment: Qt.AlignBottom
641 property real _buttonImplicitWidth: ScreenTools.defaultFontPixelHeight * 1.5
642 property real _buttonImageMargins: _buttonImplicitWidth * 0.15
646 implicitWidth: missionStatsButtonLayout._buttonImplicitWidth
647 implicitHeight: implicitWidth
648 color: checked ? QGroundControl.globalPalette.buttonHighlight : QGroundControl.globalPalette.button
650 property bool checked: true
653 anchors.margins: missionStatsButtonLayout._buttonImageMargins
655 source: "/res/terrain.svg"
656 color: parent.checked ? QGroundControl.globalPalette.buttonHighlightText : QGroundControl.globalPalette.buttonText
662 terrainButton.checked = true
663 missionStatsButton.checked = false
669 id: missionStatsButton
670 implicitWidth: missionStatsButtonLayout._buttonImplicitWidth
671 implicitHeight: implicitWidth
672 color: checked ? QGroundControl.globalPalette.buttonHighlight : QGroundControl.globalPalette.button
674 property bool checked: false
677 anchors.margins: missionStatsButtonLayout._buttonImageMargins
679 source: "/res/sliders.svg"
680 color: parent.checked ? QGroundControl.globalPalette.buttonHighlightText : QGroundControl.globalPalette.buttonText
686 missionStatsButton.checked = true
687 terrainButton.checked = false
693 id: bottomStatusOpenCloseButton
694 implicitWidth: missionStatsButtonLayout._buttonImplicitWidth
695 implicitHeight: implicitWidth
696 color: QGroundControl.globalPalette.button
699 anchors.margins: missionStatsButtonLayout._buttonImageMargins
701 source: "/res/chevron-double-left.svg"
702 color: QGroundControl.globalPalette.buttonText
707 onClicked: missionStatus._toggleMissionStatusVisibility()
714 Layout.alignment: Qt.AlignBottom
715 Layout.fillWidth: true
716 height: ScreenTools.defaultFontPixelHeight * 7
717 missionController: _missionController
718 visible: terrainButton.checked
719 onSetCurrentSeqNum: _missionController.setCurrentPlanViewSeqNum(seqNum, true)
724 Layout.alignment: Qt.AlignBottom
725 Layout.fillWidth: true
726 visible: missionStatsButton.checked
727 planMasterController: _root._planMasterController
732 //- ToolStrip ToolStripDropPanel Components
738 spacing: ScreenTools.defaultFontPixelWidth * 0.5
740 QGCLabel { text: qsTr("Create complex pattern:") }
743 model: _missionController.complexMissionItemNames
747 Layout.fillWidth: true
750 insertComplexItemAfterCurrent(modelData)
758 QGCPopupDialogFactory {
759 id: promptForPlanUsageOnVehicleChangePopupFactory
761 dialogComponent: promptForPlanUsageOnVehicleChangePopupComponent
765 id: promptForPlanUsageOnVehicleChangePopupComponent
767 title: _planMasterController.managerVehicle.isOfflineEditingVehicle ? qsTr("Plan View - Vehicle Disconnected") : qsTr("Plan View - Vehicle Changed")
768 buttons: Dialog.NoButton
772 Layout.maximumWidth: parent.width
773 wrapMode: QGCLabel.WordWrap
774 text: _planMasterController.managerVehicle.isOfflineEditingVehicle ?
775 qsTr("The vehicle associated with the plan in the Plan View is no longer available. What would you like to do with that plan?") : qsTr("The plan being worked on in the Plan View is not from the current vehicle. What would you like to do with that plan?")
779 Layout.fillWidth: true
780 text: (_planMasterController.dirtyForSave) ?
781 (_planMasterController.managerVehicle.isOfflineEditingVehicle ?
782 qsTr("Discard Unsaved Changes") : qsTr("Discard Unsaved Changes, Load New Plan From Vehicle")) : qsTr("Load New Plan From Vehicle")
784 _planMasterController.showPlanFromManagerVehicle()
785 _promptForPlanUsageShowing = false
791 Layout.fillWidth: true
792 text: _planMasterController.managerVehicle.isOfflineEditingVehicle ?
793 qsTr("Keep Current Plan") : qsTr("Keep Current Plan, Don't Update From Vehicle")
795 _promptForPlanUsageShowing = false
804 id: insertOrCancelROIDropPanelComponent
807 id: insertOrCancelROIDropPanel
810 property var mapClickCoord
812 sourceComponent: Component {
814 spacing: ScreenTools.defaultFontPixelWidth / 2
817 Layout.fillWidth: true
818 text: qsTr("Insert ROI")
821 insertOrCancelROIDropPanel.close()
822 insertROIAfterCurrent(mapClickCoord)
827 Layout.fillWidth: true
828 text: qsTr("Insert Cancel ROI")
831 insertOrCancelROIDropPanel.close()
832 insertCancelROIAfterCurrent()