10import QGroundControl.FlightMap
11import QGroundControl.Controls
12import QGroundControl.FactControls
13import QGroundControl.FlyView
14import QGroundControl.Geo
15import QGroundControl.Toolbar
20 readonly property int _decimalPlaces: 8
21 readonly property real _margin: ScreenTools.defaultFontPixelHeight * 0.5
22 readonly property real _toolsMargin: ScreenTools.defaultFontPixelWidth * 0.75
23 readonly property real _rightPanelWidth: Math.min(width / 3, ScreenTools.defaultFontPixelWidth * 30)
25 property var _planMasterController: planMasterController
26 property var _missionController: _planMasterController.missionController
27 property var _geoFenceController: _planMasterController.geoFenceController
28 property var _rallyPointController: _planMasterController.rallyPointController
29 property var _visualItems: _missionController.visualItems
30 property bool _singleComplexItem: _missionController.complexMissionItems.length === 1
31 property int _editingLayer: _layerMission
32 property var _appSettings: QGroundControl.settingsManager.appSettings
33 property var _planViewSettings: QGroundControl.settingsManager.planViewSettings
34 property bool _promptForPlanUsageShowing: false
35 property bool _addROIOnClick: false
36 property bool _addWaypointOnClick: false
37 property bool _homePositionSet: _missionController.homePositionSet
39 readonly property int _layerMission: 1
40 readonly property int _layerFence: 2
41 readonly property int _layerRally: 3
45 editorMap.zoomLevel = QGroundControl.flightMapZoom
46 editorMap.center = QGroundControl.flightMapPosition
52 function onToolbarButtonClicked() {
53 _addWaypointOnClick = false
54 _addROIOnClick = false
58 function mapCenter() {
59 var coordinate = editorMap.center
60 coordinate.latitude = coordinate.latitude.toFixed(_decimalPlaces)
61 coordinate.longitude = coordinate.longitude.toFixed(_decimalPlaces)
62 coordinate.altitude = coordinate.altitude.toFixed(_decimalPlaces)
67 id: mapFitFunctions // The name for this id cannot be changed without breaking references outside of this code. Beware!
69 usePlannedHomePosition: true
70 planMasterController: _planMasterController
73 PlanMasterController {
74 id: planMasterController
77 Component.onCompleted: {
78 _planMasterController.start()
79 _missionController.setCurrentPlanViewSeqNum(0, true)
82 onPromptForPlanUsageOnVehicleChange: {
83 if (!_promptForPlanUsageShowing) {
84 _promptForPlanUsageShowing = true
85 promptForPlanUsageOnVehicleChangePopupFactory.open()
89 function waitingOnIncompleteDataMessage(save) {
90 var saveOrUpload = save ? qsTr("Save") : qsTr("Upload")
91 QGroundControl.showMessageDialog(_root, qsTr("Unable to %1").arg(saveOrUpload), qsTr("Plan has incomplete items. Complete all items and %1 again.").arg(saveOrUpload))
94 function waitingOnTerrainDataMessage(save) {
95 var saveOrUpload = save ? qsTr("Save") : qsTr("Upload")
96 QGroundControl.showMessageDialog(_root, qsTr("Unable to %1").arg(saveOrUpload), qsTr("Plan is waiting on terrain data from server for correct altitude values."))
99 function checkReadyForSaveUpload(save) {
100 if (readyForSaveState() == VisualMissionItem.NotReadyForSaveData) {
101 waitingOnIncompleteDataMessage(save)
103 } else if (readyForSaveState() == VisualMissionItem.NotReadyForSaveTerrain) {
104 waitingOnTerrainDataMessage(save)
111 if (!checkReadyForSaveUpload(false /* save */)) {
114 switch (_missionController.sendToVehiclePreCheck()) {
115 case MissionController.SendToVehiclePreCheckStateOk: sendToVehicle()
117 case MissionController.SendToVehiclePreCheckStateActiveMission: QGroundControl.showMessageDialog(_root, qsTr("Send To Vehicle"), qsTr("Current mission must be paused prior to uploading a new Plan"))
119 case MissionController.SendToVehiclePreCheckStateFirwmareVehicleMismatch: QGroundControl.showMessageDialog(_root, qsTr("Plan Upload"),
120 qsTr("This Plan was created for a different firmware or vehicle type than the firmware/vehicle type of vehicle you are uploading to. " +
121 "This can lead to errors or incorrect behavior. " +
122 "It is recommended to recreate the Plan for the correct firmware/vehicle type.\n\n" +
123 "Click 'Ok' to upload the Plan anyway."),
124 Dialog.Ok | Dialog.Cancel,
125 function() { _planMasterController.sendToVehicle() })
130 function loadFromSelectedFile() {
131 fileDialog.title = qsTr("Select Plan File")
132 fileDialog.planFiles = true
133 fileDialog.nameFilters = _planMasterController.loadNameFilters
134 fileDialog.openForLoad()
137 function saveToSelectedFile() {
138 if (!checkReadyForSaveUpload(true /* save */)) {
141 fileDialog.title = qsTr("Save Plan")
142 fileDialog.planFiles = true
143 fileDialog.nameFilters = _planMasterController.saveNameFilters
144 fileDialog.openForSave()
147 function fitViewportToItems() {
148 mapFitFunctions.fitMapViewportToMissionItems()
151 function saveKmlToSelectedFile() {
152 if (!checkReadyForSaveUpload(true /* save */)) {
155 fileDialog.title = qsTr("Save KML")
156 fileDialog.planFiles = false
157 fileDialog.nameFilters = ShapeFileHelper.fileDialogKMLFilters
158 fileDialog.openForSave()
163 target: _missionController
165 function onNewItemsFromVehicle() {
166 if (_visualItems && _visualItems.count !== 1) {
167 mapFitFunctions.fitMapViewportToMissionItems()
169 _missionController.setCurrentPlanViewSeqNum(0, true)
173 function insertSimpleItemAfterCurrent(coordinate) {
174 var nextIndex = _missionController.currentPlanViewVIIndex + 1
175 _missionController.insertSimpleMissionItem(coordinate, nextIndex, true /* makeCurrentItem */)
178 function insertROIAfterCurrent(coordinate) {
179 var nextIndex = _missionController.currentPlanViewVIIndex + 1
180 _missionController.insertROIMissionItem(coordinate, nextIndex, true /* makeCurrentItem */)
183 function insertCancelROIAfterCurrent() {
184 var nextIndex = _missionController.currentPlanViewVIIndex + 1
185 _missionController.insertCancelROIMissionItem(nextIndex, true /* makeCurrentItem */)
188 function insertComplexItemAfterCurrent(complexItemName) {
189 var nextIndex = _missionController.currentPlanViewVIIndex + 1
190 _missionController.insertComplexMissionItem(complexItemName, mapCenter(), nextIndex, true /* makeCurrentItem */)
193 function insertTakeoffItemAfterCurrent() {
194 var nextIndex = _missionController.currentPlanViewVIIndex + 1
195 _missionController.insertTakeoffItem(mapCenter(), nextIndex, true /* makeCurrentItem */)
198 function insertLandItemAfterCurrent() {
199 var nextIndex = _missionController.currentPlanViewVIIndex + 1
200 _missionController.insertLandItem(mapCenter(), nextIndex, true /* makeCurrentItem */)
205 folder: _appSettings ? _appSettings.missionSavePath : ""
207 property bool planFiles: true ///< true: working with plan files, false: working with kml file
209 onAcceptedForSave: (file) => {
211 if (_planMasterController.saveToFile(file)) {
215 _planMasterController.saveToKml(file)
220 onAcceptedForLoad: (file) => {
221 _planMasterController.loadFromFile(file)
222 _planMasterController.fitViewportToItems()
223 _missionController.setCurrentPlanViewSeqNum(0, true)
230 planMasterController: _planMasterController
231 showRallyPointsHelp: _editingLayer === _layerRally
236 anchors.left: parent.left
237 anchors.right: parent.right
238 anchors.top: planToolBar.bottom
239 anchors.bottom: parent.bottom
244 mapName: "MissionEditor"
245 allowGCSLocationCenter: true
246 allowVehicleLocationCenter: true
249 zoomLevel: QGroundControl.flightMapZoom
250 center: QGroundControl.flightMapPosition
252 // This is the center rectangle of the map which is not obscured by tools
253 property rect centerViewport: Qt.rect(_leftToolWidth + _margin, _margin, editorMap.width - _leftToolWidth - _rightToolWidth - (_margin * 2), (missionStatus.visible ? missionStatus.y : height - _margin) - _margin)
255 property real _leftToolWidth: toolStrip.x + toolStrip.width
256 property real _rightToolWidth: rightPanel.width + rightPanel.anchors.rightMargin
257 property real _nonInteractiveOpacity: 0.5
259 // Initial map position duplicates Fly view position
260 Component.onCompleted: editorMap.center = QGroundControl.flightMapPosition
262 onZoomLevelChanged: {
263 QGroundControl.flightMapZoom = editorMap.zoomLevel
266 QGroundControl.flightMapPosition = editorMap.center
269 onMapClicked: (mouse) => {
270 // Take focus to close any previous editing
271 editorMap.focus = true
273 // Collapse layer switcher on any map click
274 layerSwitcher.expanded = false
277 if (!mainWindow.allowViewSwitch()) {
280 var coordinate = editorMap.toCoordinate(Qt.point(mouse.x, mouse.y), false /* clipToViewPort */)
281 coordinate.latitude = coordinate.latitude.toFixed(_decimalPlaces)
282 coordinate.longitude = coordinate.longitude.toFixed(_decimalPlaces)
283 coordinate.altitude = coordinate.altitude.toFixed(_decimalPlaces)
285 switch (_editingLayer) {
287 if (_planMasterController.showCreateFromTemplate) {
288 _missionController.setHomePosition(coordinate)
289 } else if (_addROIOnClick) {
290 _addROIOnClick = false
291 if (_missionController.isROIActive) {
292 var pos = Qt.point(mouse.x, mouse.y)
293 // For some strange reason using mainWindow in mapToItem doesn't work, so we use globals.parent instead which also gets us mainWindow
294 pos = editorMap.mapToItem(globals.parent, pos)
295 var dropPanel = insertOrCancelROIDropPanelComponent.createObject(mainWindow, { mapClickCoord: coordinate, clickRect: Qt.rect(pos.x, pos.y, 0, 0) })
298 insertROIAfterCurrent(coordinate)
300 } else if (_addWaypointOnClick) {
301 insertSimpleItemAfterCurrent(coordinate)
305 if (_rallyPointController.supported) {
306 _rallyPointController.addPoint(coordinate)
312 // Add the mission item visuals to the map
314 model: _missionController.visualItems
315 delegate: MissionItemMapVisual {
317 opacity: _editingLayer == _layerMission ? 1 : editorMap._nonInteractiveOpacity
318 interactive: _editingLayer == _layerMission
319 vehicle: _planMasterController.controllerVehicle
320 onClicked: (sequenceNumber) => { _missionController.setCurrentPlanViewSeqNum(sequenceNumber, false) }
324 // Add lines between waypoints
326 showSpecialVisual: _missionController.isROIBeginCurrentItem
327 model: _missionController.simpleFlightPathSegments
328 opacity: _editingLayer == _layerMission ? 1 : editorMap._nonInteractiveOpacity
331 // Direction arrows in waypoint lines
333 model: _editingLayer == _layerMission ? _missionController.directionArrows : undefined
335 delegate: MapLineArrow {
336 fromCoord: object ? object.coordinate1 : undefined
337 toCoord: object ? object.coordinate2 : undefined
339 z: QGroundControl.zOrderWaypointLines + 1
343 // UI for splitting the current segment
346 anchorPoint.x: sourceItem.width / 2
347 anchorPoint.y: sourceItem.height / 2
348 z: QGroundControl.zOrderWaypointLines + 1
349 visible: _editingLayer == _layerMission
351 sourceItem: SplitIndicator {
352 onClicked: _missionController.insertSimpleMissionItem(splitSegmentItem.coordinate,
353 _missionController.currentPlanViewVIIndex,
354 true /* makeCurrentItem */)
357 function _updateSplitCoord() {
358 if (_missionController.splitSegment) {
359 var distance = _missionController.splitSegment.coordinate1.distanceTo(_missionController.splitSegment.coordinate2)
360 var azimuth = _missionController.splitSegment.coordinate1.azimuthTo(_missionController.splitSegment.coordinate2)
361 splitSegmentItem.coordinate = _missionController.splitSegment.coordinate1.atDistanceAndAzimuth(distance / 2, azimuth)
363 coordinate = QtPositioning.coordinate()
368 target: _missionController
369 function onSplitSegmentChanged() { splitSegmentItem._updateSplitCoord() }
373 target: _missionController.splitSegment
374 function onCoordinate1Changed() { splitSegmentItem._updateSplitCoord() }
375 function onCoordinate2Changed() { splitSegmentItem._updateSplitCoord() }
379 // Add the vehicles to the map
381 model: QGroundControl.multiVehicleManager.vehicles
382 delegate: VehicleMapItem {
384 coordinate: object.coordinate
386 size: ScreenTools.defaultFontPixelHeight * 3
387 z: QGroundControl.zOrderMapItems - 1
393 myGeoFenceController: _geoFenceController
394 interactive: _editingLayer == _layerFence
395 homePosition: _missionController.plannedHomePosition
397 opacity: _editingLayer != _layerFence ? editorMap._nonInteractiveOpacity : 1
400 RallyPointMapVisuals {
402 myRallyPointController: _rallyPointController
403 interactive: _editingLayer == _layerRally
405 opacity: _editingLayer != _layerRally ? editorMap._nonInteractiveOpacity : 1
410 //-----------------------------------------------------------
414 anchors.margins: _toolsMargin
415 anchors.left: parent.left
416 anchors.top: parent.top
417 z: QGroundControl.zOrderWidgets
418 maxHeight: parent.height - toolStrip.y
419 visible: _editingLayer == _layerMission
421 property bool _isMissionLayer: _editingLayer == _layerMission
424 target: waypointButton
426 value: _addWaypointOnClick
432 value: _addROIOnClick
435 ToolStripActionList {
436 id: toolStripActionList
439 text: qsTr("Takeoff")
440 iconSource: "/res/takeoff.svg"
441 enabled: _homePositionSet && _missionController.isInsertTakeoffValid
442 visible: toolStrip._isMissionLayer && !_planMasterController.controllerVehicle.rover
444 insertTakeoffItemAfterCurrent()
448 text: _singleComplexItem ? _missionController.complexMissionItems[0].translatedName : qsTr("Pattern")
449 iconSource: "/qmlimages/MapDrawShape.svg"
450 enabled: _homePositionSet && _missionController.flyThroughCommandsAllowed
451 visible: toolStrip._isMissionLayer
452 dropPanelComponent: _singleComplexItem ? undefined : patternDropPanel
454 if (_singleComplexItem) {
455 insertComplexItemAfterCurrent(_missionController.complexMissionItems[0].canonicalName)
461 text: qsTr("Waypoint")
462 iconSource: "/res/waypoint.svg"
463 enabled: _homePositionSet
464 visible: toolStrip._isMissionLayer
466 onTriggered: { _addWaypointOnClick = !_addWaypointOnClick; if (_addWaypointOnClick) _addROIOnClick = false }
471 iconSource: "/qmlimages/roi.svg"
472 enabled: _homePositionSet
473 visible: toolStrip._isMissionLayer && _planMasterController.controllerVehicle.supports.roiMode
475 onTriggered: { _addROIOnClick = !_addROIOnClick; if (_addROIOnClick) _addWaypointOnClick = false }
478 text: _planMasterController.controllerVehicle.multiRotor
480 : _missionController.isInsertLandValid && _missionController.hasLandItem
483 iconSource: "/res/rtl.svg"
484 enabled: _homePositionSet && _missionController.isInsertLandValid
485 visible: toolStrip._isMissionLayer
487 insertLandItemAfterCurrent()
492 iconSource: "/res/chevron-double-right.svg"
493 visible: missionStatus.hidden && QGroundControl.corePlugin.options.showMissionStatus
494 onTriggered: missionStatus.showMissionStatus()
499 model: toolStripActionList.model
503 anchors.margins: _toolsMargin
504 anchors.left: toolStrip.right
505 anchors.top: parent.top
506 mapControl: editorMap
512 anchors.top: parent.top
513 anchors.bottom: parent.bottom
514 anchors.right: parent.right
515 width: _rightPanelWidth
516 planMasterController: _planMasterController
518 onEditingLayerChangeRequested: (layer) => _editingLayer = layer
521 // Layer switching icons — only active icon visible; click to expand choices leftward
524 anchors.right: rightPanel.left
525 anchors.rightMargin: _toolsMargin
526 anchors.top: parent.top
527 anchors.topMargin: _toolsMargin
528 width: layerRow.width
529 height: _layerButtonSize
530 z: QGroundControl.zOrderWidgets
532 property bool expanded: false
533 property real _layerButtonSize: ScreenTools.defaultFontPixelHeight * 2.0
534 property real _spacing: ScreenTools.defaultFontPixelHeight * 0.25
536 readonly property var _layers: [
537 { layer: _layerMission, icon: "/res/waypoint.svg", nodeType: "missionGroup" },
538 { layer: _layerFence, icon: "/res/GeoFence.svg", nodeType: "fenceGroup" },
539 { layer: _layerRally, icon: "/res/RallyPoint.svg", nodeType: "rallyGroup" }
545 onTriggered: layerSwitcher.expanded = false
551 collapseTimer.restart()
557 function choose(nodeType) {
560 rightPanel.selectLayer(nodeType)
563 // Row laid out right-to-left: active icon on the right, choices expand left
566 anchors.right: parent.right
567 spacing: layerSwitcher._spacing
568 layoutDirection: Qt.RightToLeft
570 // Active layer button (always visible)
572 width: layerSwitcher._layerButtonSize
574 radius: ScreenTools.defaultBorderRadius
575 color: QGroundControl.globalPalette.buttonHighlight
578 anchors.centerIn: parent
579 width: parent.width * 0.6
581 source: layerSwitcher._layers.find(l => l.layer === _editingLayer)?.icon ?? "/res/waypoint.svg"
582 color: QGroundControl.globalPalette.buttonHighlightText
587 onClicked: layerSwitcher.toggle()
591 // Choice buttons (only layers that are NOT the current one)
593 model: layerSwitcher._layers.filter(l => l.layer !== _editingLayer)
596 required property var modelData
597 width: layerSwitcher._layerButtonSize
599 radius: ScreenTools.defaultBorderRadius
600 color: QGroundControl.globalPalette.button
602 opacity: layerSwitcher.expanded ? 1 : 0
604 Behavior on opacity { NumberAnimation { duration: 150 } }
607 anchors.centerIn: parent
608 width: parent.width * 0.6
610 source: modelData.icon
611 color: QGroundControl.globalPalette.buttonText
616 onClicked: layerSwitcher.choose(modelData.nodeType)
625 anchors.margins: _toolsMargin
626 anchors.left: _calcLeftAnchor()
627 anchors.right: rightPanel.left
628 anchors.bottom: parent.bottom
630 visible: !hidden && _editingLayer == _layerMission && QGroundControl.corePlugin.options.showMissionStatus
632 readonly property bool hidden: _planViewSettings.showMissionItemStatus.rawValue ? false : true
634 function showMissionStatus() {
635 _planViewSettings.showMissionItemStatus.rawValue = true
638 function _calcLeftAnchor() {
639 let bottomOfToolStrip = toolStrip.y + toolStrip.height
640 let largestStatsHeight = Math.max(terrainStatus.height, missionStats.height)
641 if (bottomOfToolStrip + largestStatsHeight > parent.height - missionStatus.anchors.margins) {
642 return toolStrip.right
647 function _toggleMissionStatusVisibility() {
648 _planViewSettings.showMissionItemStatus.rawValue = _planViewSettings.showMissionItemStatus.rawValue ? false : true
652 id: missionStatsButtonLayout
653 Layout.alignment: Qt.AlignBottom
656 property real _buttonImplicitWidth: ScreenTools.defaultFontPixelHeight * 1.5
657 property real _buttonImageMargins: _buttonImplicitWidth * 0.15
661 implicitWidth: missionStatsButtonLayout._buttonImplicitWidth
662 implicitHeight: implicitWidth
663 color: checked ? QGroundControl.globalPalette.buttonHighlight : QGroundControl.globalPalette.button
665 property bool checked: true
668 anchors.margins: missionStatsButtonLayout._buttonImageMargins
670 source: "/res/terrain.svg"
671 color: parent.checked ? QGroundControl.globalPalette.buttonHighlightText : QGroundControl.globalPalette.buttonText
677 terrainButton.checked = true
678 missionStatsButton.checked = false
684 id: missionStatsButton
685 implicitWidth: missionStatsButtonLayout._buttonImplicitWidth
686 implicitHeight: implicitWidth
687 color: checked ? QGroundControl.globalPalette.buttonHighlight : QGroundControl.globalPalette.button
689 property bool checked: false
692 anchors.margins: missionStatsButtonLayout._buttonImageMargins
694 source: "/res/sliders.svg"
695 color: parent.checked ? QGroundControl.globalPalette.buttonHighlightText : QGroundControl.globalPalette.buttonText
701 missionStatsButton.checked = true
702 terrainButton.checked = false
708 id: bottomStatusOpenCloseButton
709 implicitWidth: missionStatsButtonLayout._buttonImplicitWidth
710 implicitHeight: implicitWidth
711 color: QGroundControl.globalPalette.button
714 anchors.margins: missionStatsButtonLayout._buttonImageMargins
716 source: "/res/chevron-double-left.svg"
717 color: QGroundControl.globalPalette.buttonText
722 onClicked: missionStatus._toggleMissionStatusVisibility()
729 Layout.alignment: Qt.AlignBottom
730 Layout.fillWidth: true
731 height: ScreenTools.defaultFontPixelHeight * 7
732 missionController: _missionController
733 visible: terrainButton.checked
734 onSetCurrentSeqNum: _missionController.setCurrentPlanViewSeqNum(seqNum, true)
739 Layout.alignment: Qt.AlignBottom
740 Layout.fillWidth: true
741 visible: missionStatsButton.checked
742 planMasterController: _root._planMasterController
747 //- ToolStrip ToolStripDropPanel Components
753 spacing: ScreenTools.defaultFontPixelWidth * 0.5
755 QGCLabel { text: qsTr("Create complex pattern:") }
758 model: _missionController.complexMissionItems
761 text: modelData.translatedName
762 Layout.fillWidth: true
765 insertComplexItemAfterCurrent(modelData.canonicalName)
773 QGCPopupDialogFactory {
774 id: promptForPlanUsageOnVehicleChangePopupFactory
776 dialogComponent: promptForPlanUsageOnVehicleChangePopupComponent
780 id: promptForPlanUsageOnVehicleChangePopupComponent
782 title: _planMasterController.managerVehicle.isOfflineEditingVehicle ? qsTr("Plan View - Vehicle Disconnected") : qsTr("Plan View - Vehicle Changed")
783 buttons: Dialog.NoButton
787 Layout.maximumWidth: parent.width
788 wrapMode: QGCLabel.WordWrap
789 text: _planMasterController.managerVehicle.isOfflineEditingVehicle ?
790 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?")
794 Layout.fillWidth: true
795 text: (_planMasterController.dirtyForSave) ?
796 (_planMasterController.managerVehicle.isOfflineEditingVehicle ?
797 qsTr("Discard Unsaved Changes") : qsTr("Discard Unsaved Changes, Load New Plan From Vehicle")) : qsTr("Load New Plan From Vehicle")
799 _planMasterController.showPlanFromManagerVehicle()
800 _promptForPlanUsageShowing = false
806 Layout.fillWidth: true
807 text: _planMasterController.managerVehicle.isOfflineEditingVehicle ?
808 qsTr("Keep Current Plan") : qsTr("Keep Current Plan, Don't Update From Vehicle")
810 _promptForPlanUsageShowing = false
819 id: insertOrCancelROIDropPanelComponent
822 id: insertOrCancelROIDropPanel
825 property var mapClickCoord
827 sourceComponent: Component {
829 spacing: ScreenTools.defaultFontPixelWidth / 2
832 Layout.fillWidth: true
833 text: qsTr("Insert ROI")
836 insertOrCancelROIDropPanel.close()
837 insertROIAfterCurrent(mapClickCoord)
842 Layout.fillWidth: true
843 text: qsTr("Insert Cancel ROI")
846 insertOrCancelROIDropPanel.close()
847 insertCancelROIAfterCurrent()