QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
MissionController.cc
Go to the documentation of this file.
2#include "MissionController.h"
3#include "Vehicle.h"
4#include "VehicleSupports.h"
5#include "MissionManager.h"
6#include "FlightPathSegment.h"
7#include "FirmwarePlugin.h"
8#include "QGCApplication.h"
9#include "SimpleMissionItem.h"
10#include "SurveyComplexItem.h"
15#include "GeoJsonHelper.h"
16#include "JsonParsing.h"
18#include "SettingsManager.h"
19#include "AppSettings.h"
20#include "MissionSettingsItem.h"
22#include "KMLPlanDomDocument.h"
23#include "QGCCorePlugin.h"
24#include "TakeoffMissionItem.h"
25#include "PlanViewSettings.h"
26#include "MissionCommandTree.h"
27#include "QGCMath.h"
28#include "QGCLoggingCategory.h"
29
30#include <QtCore/QJsonArray>
31#include <QtCore/QJsonDocument>
32#include <QtMath>
33
34#define UPDATE_TIMEOUT 5000
35
36QGC_LOGGING_CATEGORY(MissionControllerLog, "PlanManager.MissionController")
37
38MissionController::MissionController(PlanMasterController* masterController, QObject *parent)
39 : PlanElementController (masterController, parent)
40 , _controllerVehicle (masterController->controllerVehicle())
41 , _managerVehicle (masterController->managerVehicle())
42 , _missionManager (masterController->managerVehicle()->missionManager())
43 , _visualItems (new QmlObjectListModel(this))
44 , _planViewSettings (SettingsManager::instance()->planViewSettings())
45 , _appSettings (SettingsManager::instance()->appSettings())
46{
47 _resetMissionFlightStatus();
48
49 _updateTimer.setSingleShot(true);
50
51 connect(&_updateTimer, &QTimer::timeout, this, &MissionController::_updateTimeout);
52 connect(_planViewSettings->takeoffItemNotRequired(), &Fact::rawValueChanged, this, &MissionController::_recalcPlanViewState);
53 connect(_planViewSettings->allowMultipleLandingPatterns(), &Fact::rawValueChanged, this, &MissionController::multipleLandPatternsAllowedChanged);
55 connect(this, &MissionController::multipleLandPatternsAllowedChanged, this, &MissionController::_recalcPlanViewState);
56 connect(this, &MissionController::homePositionSetChanged, this, &MissionController::_recalcPlanViewState);
58
59 // The follow is used to compress multiple recalc calls in a row to into a single call.
60 connect(this, &MissionController::_recalcMissionFlightStatusSignal, this, &MissionController::_recalcMissionFlightStatus, Qt::QueuedConnection);
61 connect(this, &MissionController::_recalcFlightPathSegmentsSignal, this, &MissionController::_recalcFlightPathSegments, Qt::QueuedConnection);
62 qgcApp()->addCompressedSignal(QMetaMethod::fromSignal(&MissionController::_recalcMissionFlightStatusSignal));
63 qgcApp()->addCompressedSignal(QMetaMethod::fromSignal(&MissionController::_recalcFlightPathSegmentsSignal));
64 qgcApp()->addCompressedSignal(QMetaMethod::fromSignal(&MissionController::recalcTerrainProfile));
65}
66
71
72void MissionController::_resetMissionFlightStatus(void)
73{
74 _flightStatusCalc.reset(_controllerVehicle, _managerVehicle, _missionStartsInVTOLMulticopterMode);
75 _missionFlightStatus = _flightStatusCalc.status();
76
77 emit missionPlannedDistanceChanged(_missionFlightStatus.plannedDistance);
78 emit missionTimeChanged();
79 emit missionHoverDistanceChanged(_missionFlightStatus.hoverDistance);
80 emit missionCruiseDistanceChanged(_missionFlightStatus.cruiseDistance);
83 emit missionMaxTelemetryChanged(_missionFlightStatus.maxTelemetryDistance);
84 emit batteryChangePointChanged(_missionFlightStatus.batteryChangePoint);
85 emit batteriesRequiredChanged(_missionFlightStatus.batteriesRequired);
86}
87
88void MissionController::start(bool flyView)
89{
90 qCDebug(MissionControllerLog) << "start flyView" << flyView;
91
92 _managerVehicleChanged(_managerVehicle);
93 connect(_masterController, &PlanMasterController::managerVehicleChanged, this, &MissionController::_managerVehicleChanged);
94
96 _init();
97}
98
99void MissionController::_init(void)
100{
101 // We start with an empty mission
102 _addMissionSettings(_visualItems);
103
104 // Set up the tree model structure once (groups + static children)
105 _setupTreeModel();
106
107 _initAllVisualItems();
108}
109
110// Called when new mission items have completed downloading from Vehicle
111void MissionController::_newMissionItemsAvailableFromVehicle(bool removeAllRequested)
112{
113 qCDebug(MissionControllerLog) << "_newMissionItemsAvailableFromVehicle flyView:count" << _flyView << _missionManager->missionItems().count();
114
115 // Fly view always reloads on _loadComplete
116 // Plan view only reloads if:
117 // - Load was specifically requested
118 // - There is no current Plan
119 if (_flyView || removeAllRequested || _itemsRequested || isEmpty()) {
120 // Fly Mode (accept if):
121 // - Always accepts new items from the vehicle so Fly view is kept up to date
122 // Edit Mode (accept if):
123 // - Remove all was requested from Fly view (clear mission on flight end)
124 // - A load from vehicle was manually requested
125 // - The initial automatic load from a vehicle completed and the current editor is empty
126
127 _setupNewVisualItems();
128
129 const QList<MissionItem*>& newMissionItems = _missionManager->missionItems();
130 qCDebug(MissionControllerLog) << "loading from vehicle: count"<< newMissionItems.count();
131
132 int i=0;
133 if (_controllerVehicle->firmwarePlugin()->sendHomePositionToVehicle() && newMissionItems.count() != 0) {
134 // First item is fake home position
135 MissionItem* fakeHomeItem = newMissionItems[0];
136 if (fakeHomeItem->coordinate().latitude() != 0 || fakeHomeItem->coordinate().longitude() != 0) {
137 _settingsItem->setCoordinate(fakeHomeItem->coordinate());
138 }
139 i = 1;
140 }
141
142 bool weHaveItemsFromVehicle = false;
143 for (; i < newMissionItems.count(); i++) {
144 weHaveItemsFromVehicle = true;
145 const MissionItem* missionItem = newMissionItems[i];
146 SimpleMissionItem* simpleItem = new SimpleMissionItem(_masterController, _flyView, *missionItem);
147 if (TakeoffMissionItem::isTakeoffCommand(static_cast<MAV_CMD>(simpleItem->command()))) {
148 // This needs to be a TakeoffMissionItem
149 _takeoffMissionItem = new TakeoffMissionItem(*missionItem, _masterController, _flyView, _settingsItem, false /* forLoad */);
150 _takeoffMissionItem->setWizardMode(false);
151 simpleItem->deleteLater();
152 simpleItem = _takeoffMissionItem;
153 }
154 _visualItems->append(simpleItem);
155 }
156
157 // We set Altitude frame to mixed, otherwise if we need a non relative altitude frame we won't be able to change it
158 setGlobalAltitudeFrame(weHaveItemsFromVehicle ? QGroundControlQmlGlobal::AltitudeFrameMixed : QGroundControlQmlGlobal::AltitudeFrameRelative);
159
160 MissionController::_scanForAdditionalSettings(_visualItems, _masterController);
161
162 _initAllVisualItems();
163
164 emit newItemsFromVehicle();
165
167 }
168 _itemsRequested = false;
169}
170
172{
173 if (_masterController->offline()) {
174 qCCritical(MissionControllerLog) << "MissionControllerLog::loadFromVehicle called while offline";
175 } else if (syncInProgress()) {
176 qCCritical(MissionControllerLog) << "MissionControllerLog::loadFromVehicle called while syncInProgress";
177 } else {
178 _itemsRequested = true;
179 _managerVehicle->missionManager()->loadFromVehicle();
180 }
181}
182
184{
185 if (_masterController->offline()) {
186 qCCritical(MissionControllerLog) << "MissionControllerLog::sendToVehicle called while offline";
187 } else if (syncInProgress()) {
188 qCCritical(MissionControllerLog) << "MissionControllerLog::sendToVehicle called while syncInProgress";
189 } else {
190 qCDebug(MissionControllerLog) << "MissionControllerLog::sendToVehicle";
191 if (_visualItems->count() == 1) {
192 // This prevents us from sending a possibly bogus home position to the vehicle
193 QmlObjectListModel emptyModel;
194 sendItemsToVehicle(_managerVehicle, &emptyModel);
195 } else {
196 sendItemsToVehicle(_managerVehicle, _visualItems);
197 }
198 setDirty(false);
199 }
200}
201
205bool MissionController::_convertToMissionItems(QmlObjectListModel* visualMissionItems, QList<MissionItem*>& rgMissionItems, QObject* missionItemParent)
206{
207 if (visualMissionItems->count() == 0) {
208 return false;
209 }
210
211 bool endActionSet = false;
212 int lastSeqNum = 0;
213
214 for (int i=0; i<visualMissionItems->count(); i++) {
215 VisualMissionItem* visualItem = qobject_cast<VisualMissionItem*>(visualMissionItems->get(i));
216
217 lastSeqNum = visualItem->lastSequenceNumber();
218 visualItem->appendMissionItems(rgMissionItems, missionItemParent);
219
220 qCDebug(MissionControllerLog) << "_convertToMissionItems seqNum:lastSeqNum:command"
221 << visualItem->sequenceNumber()
222 << lastSeqNum
223 << visualItem->commandName();
224 }
225
226 // Mission settings has a special case for end mission action
227 MissionSettingsItem* settingsItem = visualMissionItems->value<MissionSettingsItem*>(0);
228 if (settingsItem) {
229 endActionSet = settingsItem->addMissionEndAction(rgMissionItems, lastSeqNum + 1, missionItemParent);
230 }
231
232 return endActionSet;
233}
234
236{
237 QObject* deleteParent = new QObject();
238 QList<MissionItem*> rgMissionItems;
239
240 _convertToMissionItems(_visualItems, rgMissionItems, deleteParent);
241 planKML.addMission(_controllerVehicle, _visualItems, rgMissionItems);
242 deleteParent->deleteLater();
243}
244
246{
247 if (vehicle) {
248 QList<MissionItem*> rgMissionItems;
249
250 _convertToMissionItems(visualMissionItems, rgMissionItems, vehicle);
251
252 // PlanManager takes control of MissionItems so no need to delete
253 vehicle->missionManager()->writeMissionItems(rgMissionItems);
254 }
255}
256
257int MissionController::_nextSequenceNumber(void)
258{
259 if (_visualItems->count() == 0) {
260 qWarning() << "Internal error: Empty visual item list";
261 return 0;
262 } else {
263 VisualMissionItem* lastItem = _visualItems->value<VisualMissionItem*>(_visualItems->count() - 1);
264 return lastItem->lastSequenceNumber() + 1;
265 }
266}
267
268VisualMissionItem* MissionController::_insertSimpleMissionItemWorker(QGeoCoordinate coordinate, MAV_CMD command, int visualItemIndex, bool makeCurrentItem)
269{
270 int sequenceNumber = _nextSequenceNumber();
271 SimpleMissionItem * newItem = new SimpleMissionItem(_masterController, _flyView, false /* forLoad */);
272 newItem->setSequenceNumber(sequenceNumber);
273 newItem->setCoordinate(coordinate);
274 newItem->setCommand(command);
275 _initVisualItem(newItem);
276
277 if (newItem->specifiesAltitude()) {
278 if (!MissionCommandTree::instance()->isLandCommand(command)) {
279 double prevAltitude;
281
282 if (_findPreviousAltitude(visualItemIndex, &prevAltitude, &prevAltFrame)) {
283 newItem->altitude()->setRawValue(prevAltitude);
285 // We are in mixed altitude frames, so copy from previous. Otherwise altitude frame will be set from global setting.
286 newItem->setAltitudeFrame(static_cast<QGroundControlQmlGlobal::AltitudeFrame>(prevAltFrame));
287 }
288 }
289 }
290 }
291 if (visualItemIndex == -1) {
292 _visualItems->append(newItem);
293 } else {
294 _visualItems->insert(visualItemIndex, newItem);
295 }
296
297 // We send the click coordinate through here to be able to set the planned home position from the user click location if needed
298 _recalcAllWithCoordinate(coordinate);
299
300 if (makeCurrentItem) {
302 }
303
304 _firstItemAdded();
305
306 return newItem;
307}
308
309
310VisualMissionItem* MissionController::insertSimpleMissionItem(QGeoCoordinate coordinate, int visualItemIndex, bool makeCurrentItem)
311{
312 return _insertSimpleMissionItemWorker(coordinate, MAV_CMD_NAV_WAYPOINT, visualItemIndex, makeCurrentItem);
313}
314
315VisualMissionItem* MissionController::insertTakeoffItem(QGeoCoordinate /*coordinate*/, int visualItemIndex, bool makeCurrentItem)
316{
317 return _insertTakeoffItemWorker(
318 _controllerVehicle->vtol() ? MAV_CMD_NAV_VTOL_TAKEOFF : MAV_CMD_NAV_TAKEOFF,
319 visualItemIndex,
320 makeCurrentItem);
321}
322
324 QGeoCoordinate /*coordinate*/, int visualItemIndex, bool makeCurrentItem)
325{
326 if (!TakeoffMissionItem::isVTOLMulticopterTakeoff(_controllerVehicle, MAV_CMD_NAV_TAKEOFF)) {
327 qCWarning(MissionControllerLog) << "Multicopter takeoff requested for an unsupported vehicle";
328 return nullptr;
329 }
330
331 return _insertTakeoffItemWorker(MAV_CMD_NAV_TAKEOFF, visualItemIndex, makeCurrentItem);
332}
333
334VisualMissionItem* MissionController::_insertTakeoffItemWorker(MAV_CMD command, int visualItemIndex, bool makeCurrentItem)
335{
336 int sequenceNumber = _nextSequenceNumber();
337 _takeoffMissionItem = new TakeoffMissionItem(command, _masterController, _flyView, _settingsItem, false /* forLoad */);
338 _takeoffMissionItem->setSequenceNumber(sequenceNumber);
339 _initVisualItem(_takeoffMissionItem);
340
341 if (_takeoffMissionItem->specifiesAltitude()) {
342 double prevAltitude;
344
345 if (_findPreviousAltitude(visualItemIndex, &prevAltitude, &prevAltFrame)) {
346 _takeoffMissionItem->altitude()->setRawValue(prevAltitude);
347 _takeoffMissionItem->setAltitudeFrame(static_cast<QGroundControlQmlGlobal::AltitudeFrame>(prevAltFrame));
348 }
349 }
350 if (visualItemIndex == -1) {
351 _visualItems->append(_takeoffMissionItem);
352 } else {
353 _visualItems->insert(visualItemIndex, _takeoffMissionItem);
354 }
355
356 _recalcAll();
357
358 if (makeCurrentItem) {
359 setCurrentPlanViewSeqNum(_takeoffMissionItem->sequenceNumber(), true);
360 }
361
362 _firstItemAdded();
363
364 return _takeoffMissionItem;
365}
366
368 // Multiple landing sequences are a fixed-wing/VTOL landing pattern concept.
369 // Other vehicle types insert RTL, which only makes sense once per mission.
370 return (_controllerVehicle->fixedWing() || _controllerVehicle->vtol()) &&
371 _planViewSettings->allowMultipleLandingPatterns()
372 ->rawValue().toBool() &&
374}
375
376VisualMissionItem* MissionController::insertLandItem(QGeoCoordinate coordinate, int visualItemIndex, bool makeCurrentItem)
377{
378 if (_controllerVehicle->fixedWing()) {
379 FixedWingLandingComplexItem* fwLanding = qobject_cast<FixedWingLandingComplexItem*>(insertComplexMissionItem(FixedWingLandingComplexItem::canonicalName, coordinate, visualItemIndex, makeCurrentItem));
380 return fwLanding;
381 } else if (_controllerVehicle->vtol()) {
382 VTOLLandingComplexItem* vtolLanding = qobject_cast<VTOLLandingComplexItem*>(insertComplexMissionItem(VTOLLandingComplexItem::canonicalName, coordinate, visualItemIndex, makeCurrentItem));
383 return vtolLanding;
384 } else {
385 return _insertSimpleMissionItemWorker(coordinate, MAV_CMD_NAV_RETURN_TO_LAUNCH, visualItemIndex, makeCurrentItem);
386 }
387}
388
389VisualMissionItem* MissionController::insertROIMissionItem(QGeoCoordinate coordinate, int visualItemIndex, bool makeCurrentItem)
390{
391 SimpleMissionItem* simpleItem = qobject_cast<SimpleMissionItem*>(_insertSimpleMissionItemWorker(coordinate, MAV_CMD_DO_SET_ROI_LOCATION, visualItemIndex, makeCurrentItem));
392
393 if (!_controllerVehicle->firmwarePlugin()->supportedMissionCommands(QGCMAVLink::VehicleClassGeneric).contains(MAV_CMD_DO_SET_ROI_LOCATION)) {
394 simpleItem->setCommand(MAV_CMD_DO_SET_ROI) ;
395 simpleItem->missionItem().setParam1(MAV_ROI_LOCATION);
396 }
397 return simpleItem;
398}
399
400VisualMissionItem* MissionController::insertCancelROIMissionItem(int visualItemIndex, bool makeCurrentItem)
401{
402 SimpleMissionItem* simpleItem = qobject_cast<SimpleMissionItem*>(_insertSimpleMissionItemWorker(QGeoCoordinate(), MAV_CMD_DO_SET_ROI_NONE, visualItemIndex, makeCurrentItem));
403
404 if (!_controllerVehicle->firmwarePlugin()->supportedMissionCommands(QGCMAVLink::VehicleClassGeneric).contains(MAV_CMD_DO_SET_ROI_NONE)) {
405 simpleItem->setCommand(MAV_CMD_DO_SET_ROI) ;
406 simpleItem->missionItem().setParam1(MAV_ROI_NONE);
407 }
408 return simpleItem;
409}
410
411VisualMissionItem* MissionController::insertComplexMissionItem(QString itemName, QGeoCoordinate mapCenterCoordinate, int visualItemIndex, bool makeCurrentItem)
412{
414 if (!newItem) {
415 return nullptr;
416 }
417 newItem->setCoordinate(mapCenterCoordinate);
418
420 double prevAltitude;
422 if (_findPreviousAltitude(visualItemIndex, &prevAltitude, &prevAltFrame)) {
423 newItem->applyPreviousAltitudeFrame(prevAltFrame, prevAltitude);
424 }
425 }
426
427 _insertComplexMissionItemWorker(mapCenterCoordinate, newItem, visualItemIndex, makeCurrentItem);
428
429 return newItem;
430}
431
432VisualMissionItem* MissionController::insertComplexMissionItemFromKMLOrSHP(QString itemName, QString file, int visualItemIndex, bool makeCurrentItem)
433{
435 if (!newItem) {
436 return nullptr;
437 }
438
439 _insertComplexMissionItemWorker(QGeoCoordinate(), newItem, visualItemIndex, makeCurrentItem);
440
441 return newItem;
442}
443
444void MissionController::_insertComplexMissionItemWorker(const QGeoCoordinate& mapCenterCoordinate, ComplexMissionItem* complexItem, int visualItemIndex, bool makeCurrentItem)
445{
446 int sequenceNumber = _nextSequenceNumber();
447 bool surveyStyleItem = qobject_cast<SurveyComplexItem*>(complexItem) ||
448 qobject_cast<CorridorScanComplexItem*>(complexItem) ||
449 qobject_cast<StructureScanComplexItem*>(complexItem);
450
451 if (surveyStyleItem) {
452 bool rollSupported = false;
453 bool pitchSupported = false;
454 bool yawSupported = false;
455
456 // If the vehicle is known to have a gimbal then we automatically point the gimbal straight down if not already set
457
458 MissionSettingsItem* settingsItem = _visualItems->value<MissionSettingsItem*>(0);
459 CameraSection* cameraSection = settingsItem->cameraSection();
460
461 // Set camera to photo mode (leave alone if user already specified)
462 if (cameraSection->cameraModeSupported() && !cameraSection->specifyCameraMode()) {
463 cameraSection->setSpecifyCameraMode(true);
464 cameraSection->cameraMode()->setRawValue(CAMERA_MODE_IMAGE_SURVEY);
465 }
466
467 // Point gimbal straight down
468 if (_controllerVehicle->firmwarePlugin()->hasGimbal(_controllerVehicle, rollSupported, pitchSupported, yawSupported) && pitchSupported) {
469 // If the user already specified a gimbal angle leave it alone
470 if (!cameraSection->specifyGimbal()) {
471 cameraSection->setSpecifyGimbal(true);
472 cameraSection->gimbalPitch()->setRawValue(-90.0);
473 }
474 }
475 }
476
477 complexItem->setSequenceNumber(sequenceNumber);
478 if (!qobject_cast<VTOLLandingComplexItem*>(complexItem)) {
479 complexItem->setWizardMode(true);
480 }
481 _initVisualItem(complexItem);
482
483 if (visualItemIndex == -1) {
484 _visualItems->append(complexItem);
485 } else {
486 _visualItems->insert(visualItemIndex, complexItem);
487 }
488
489 //-- Keep track of bounding box changes in complex items
490 if(!complexItem->isSimpleItem()) {
491 connect(complexItem, &ComplexMissionItem::boundingCubeChanged, this, &MissionController::_complexBoundingBoxChanged);
492 }
493 _recalcAllWithCoordinate(mapCenterCoordinate);
494
495 if (makeCurrentItem) {
496 setCurrentPlanViewSeqNum(complexItem->sequenceNumber(), true);
497 }
498 _firstItemAdded();
499}
500
502{
503 return _visualItems ? _visualItems->indexOf(object) : -1;
504}
505
507{
508 if (viIndex <= 0 || viIndex >= _visualItems->count()) {
509 qWarning() << "MissionController::removeVisualItem called with bad index - count:index" << _visualItems->count() << viIndex;
510 return;
511 }
512
513 bool removeSurveyStyle = _visualItems->value<SurveyComplexItem*>(viIndex) || _visualItems->value<CorridorScanComplexItem*>(viIndex);
514 VisualMissionItem* item = qobject_cast<VisualMissionItem*>(_visualItems->removeAt(viIndex));
515
516 if (item == _takeoffMissionItem) {
517 _takeoffMissionItem = nullptr;
518 }
519
520 _deinitVisualItem(item);
521 item->deleteLater();
522
523 if (removeSurveyStyle) {
524 // Determine if the mission still has another survey style item in it
525 bool foundSurvey = false;
526 for (int i=1; i<_visualItems->count(); i++) {
527 if (_visualItems->value<SurveyComplexItem*>(i) || _visualItems->value<CorridorScanComplexItem*>(i)) {
528 foundSurvey = true;
529 break;
530 }
531 }
532
533 // If there is no longer a survey item in the mission remove added commands
534 if (!foundSurvey) {
535 bool rollSupported = false;
536 bool pitchSupported = false;
537 bool yawSupported = false;
538 CameraSection* cameraSection = _settingsItem->cameraSection();
539 if (_controllerVehicle->firmwarePlugin()->hasGimbal(_controllerVehicle, rollSupported, pitchSupported, yawSupported) && pitchSupported) {
540 if (cameraSection->specifyGimbal() && cameraSection->gimbalPitch()->rawValue().toDouble() == -90.0 && cameraSection->gimbalYaw()->rawValue().toDouble() == 0.0) {
541 cameraSection->setSpecifyGimbal(false);
542 }
543 }
544 if (cameraSection->cameraModeSupported() && cameraSection->specifyCameraMode() && cameraSection->cameraMode()->rawValue().toInt() == 0) {
545 cameraSection->setSpecifyCameraMode(false);
546 }
547 }
548 }
549
550 _recalcAll();
551
552 // Adjust current item
553 int newVIIndex;
554 if (viIndex >= _visualItems->count()) {
555 newVIIndex = _visualItems->count() - 1;
556 } else {
557 newVIIndex = viIndex;
558 }
559 setCurrentPlanViewSeqNum(_visualItems->value<VisualMissionItem*>(newVIIndex)->sequenceNumber(), true);
560
561 setDirty(true);
562
563 if (_visualItems->count() == 1) {
564 _allItemsRemoved();
565 }
566}
567
568void MissionController::_setupNewVisualItems(QmlObjectListModel* newItems)
569{
570 QmlObjectListModel* oldItems = _visualItems;
571
572 if (oldItems) {
573 _deinitAllVisualItems();
574
575 // Destroy old items after a delay — TreeView delegates are torn down
576 // asynchronously during a polish cycle and may still hold bindings.
577 QTimer::singleShot(1000, oldItems, [oldItems] {
578 oldItems->clearAndDeleteContents();
579 oldItems->deleteLater();
580 });
581 }
582
583 _settingsItem = nullptr;
584 _takeoffMissionItem = nullptr;
585
586 if (newItems) {
587 _visualItems = newItems;
588 if (_visualItems->count() == 0) {
589 _addMissionSettings(_visualItems);
590 } else {
591 _settingsItem = _visualItems->value<MissionSettingsItem*>(0);
592 }
593 } else {
594 _visualItems = new QmlObjectListModel(this);
595 _addMissionSettings(_visualItems);
596 }
597}
598
600{
601 _setupNewVisualItems();
602 _initAllVisualItems();
603 setDirty(true);
604 _resetMissionFlightStatus();
605 _allItemsRemoved();
606}
607
608bool MissionController::_loadJsonMissionFileV2(const QJsonObject& json, QmlObjectListModel* visualItems, QString& errorString)
609{
610 // Validate root object keys
611 QList<JsonParsing::KeyValidateInfo> rootKeyInfoList = {
612 { _jsonPlannedHomePositionKey, QJsonValue::Array, true },
613 { _jsonItemsKey, QJsonValue::Array, true },
614 { _jsonFirmwareTypeKey, QJsonValue::Double, true },
615 { _jsonVehicleTypeKey, QJsonValue::Double, false },
616 { _jsonCruiseSpeedKey, QJsonValue::Double, false },
617 { _jsonHoverSpeedKey, QJsonValue::Double, false },
618 { _jsonGlobalPlanAltitudeModeKey, QJsonValue::Double, false },
619 };
620 if (!JsonParsing::validateKeys(json, rootKeyInfoList, errorString)) {
621 return false;
622 }
623
625
626 qCDebug(MissionControllerLog) << "MissionController::_loadJsonMissionFileV2 itemCount:" << json[_jsonItemsKey].toArray().count();
627
629
630 // Get the firmware/vehicle type from the plan file
631 MAV_AUTOPILOT planFileFirmwareType = static_cast<MAV_AUTOPILOT>(json[_jsonFirmwareTypeKey].toInt());
632 MAV_TYPE planFileVehicleType = static_cast<MAV_TYPE> (QGCMAVLink::vehicleClassToMavType(appSettings->offlineEditingVehicleClass()->rawValue().toInt()));
633 if (json.contains(_jsonVehicleTypeKey)) {
634 planFileVehicleType = static_cast<MAV_TYPE>(json[_jsonVehicleTypeKey].toInt());
635 }
636
637 // Update firmware/vehicle offline settings if we aren't connect to a vehicle
638 if (_masterController->offline()) {
639 appSettings->offlineEditingFirmwareClass()->setRawValue(QGCMAVLink::firmwareClass(static_cast<MAV_AUTOPILOT>(json[_jsonFirmwareTypeKey].toInt())));
640 if (json.contains(_jsonVehicleTypeKey)) {
641 appSettings->offlineEditingVehicleClass()->setRawValue(QGCMAVLink::vehicleClass(planFileVehicleType));
642 }
643 }
644
645 // The controller vehicle always tracks the Plan file firmware/vehicle types so update it
646 _controllerVehicle->stopTrackingFirmwareVehicleTypeChanges();
647 _controllerVehicle->_offlineFirmwareTypeSettingChanged(planFileFirmwareType);
648 _controllerVehicle->_offlineVehicleTypeSettingChanged(planFileVehicleType);
649
650 if (json.contains(_jsonCruiseSpeedKey)) {
651 appSettings->offlineEditingCruiseSpeed()->setRawValue(json[_jsonCruiseSpeedKey].toDouble());
652 }
653 if (json.contains(_jsonHoverSpeedKey)) {
654 appSettings->offlineEditingHoverSpeed()->setRawValue(json[_jsonHoverSpeedKey].toDouble());
655 }
656 if (json.contains(_jsonGlobalPlanAltitudeModeKey)) {
657 setGlobalAltitudeFrame(json[_jsonGlobalPlanAltitudeModeKey].toVariant().value<QGroundControlQmlGlobal::AltitudeFrame>());
658 }
659
660 QGeoCoordinate homeCoordinate;
661 if (!GeoJsonHelper::loadGeoCoordinate(json[_jsonPlannedHomePositionKey], true /* altitudeRequired */, homeCoordinate, errorString)) {
662 return false;
663 }
665 settingsItem->setCoordinate(homeCoordinate);
666 visualItems->insert(0, settingsItem);
667 qCDebug(MissionControllerLog) << "plannedHomePosition" << homeCoordinate;
668
669 // Read mission items
670
671 int nextSequenceNumber = 1; // Start with 1 since home is in 0
672 const QJsonArray rgMissionItems(json[_jsonItemsKey].toArray());
673 for (int i=0; i<rgMissionItems.count(); i++) {
674 // Convert to QJsonObject
675 const QJsonValue& itemValue = rgMissionItems[i];
676 if (!itemValue.isObject()) {
677 errorString = tr("Mission item %1 is not an object").arg(i);
678 return false;
679 }
680 const QJsonObject itemObject = itemValue.toObject();
681
682 // Load item based on type
683
684 QList<JsonParsing::KeyValidateInfo> itemKeyInfoList = {
685 { VisualMissionItem::jsonTypeKey, QJsonValue::String, true },
686 };
687 if (!JsonParsing::validateKeys(itemObject, itemKeyInfoList, errorString)) {
688 return false;
689 }
690 QString itemType = itemObject[VisualMissionItem::jsonTypeKey].toString();
691
693 SimpleMissionItem* simpleItem = new SimpleMissionItem(_masterController, _flyView, true /* forLoad */);
694 if (simpleItem->load(itemObject, nextSequenceNumber, errorString)) {
695 if (TakeoffMissionItem::isTakeoffCommand(static_cast<MAV_CMD>(simpleItem->command()))) {
696 // This needs to be a TakeoffMissionItem
697 TakeoffMissionItem* takeoffItem = new TakeoffMissionItem(_masterController, _flyView, settingsItem, true /* forLoad */);
698 takeoffItem->load(itemObject, nextSequenceNumber, errorString);
699 simpleItem->deleteLater();
700 simpleItem = takeoffItem;
701 }
702 qCDebug(MissionControllerLog) << "Loading simple item: nextSequenceNumber:command" << nextSequenceNumber << simpleItem->command();
703 nextSequenceNumber = simpleItem->lastSequenceNumber() + 1;
704 visualItems->append(simpleItem);
705 } else {
706 return false;
707 }
708 } else if (itemType == VisualMissionItem::jsonTypeComplexItemValue) {
709 QList<JsonParsing::KeyValidateInfo> complexItemKeyInfoList = {
710 { ComplexMissionItem::jsonComplexItemTypeKey, QJsonValue::String, true },
711 };
712 if (!JsonParsing::validateKeys(itemObject, complexItemKeyInfoList, errorString)) {
713 return false;
714 }
715 QString complexItemType = itemObject[ComplexMissionItem::jsonComplexItemTypeKey].toString();
716
717 qCDebug(MissionControllerLog) << "Loading complex item type:" << complexItemType << "nextSequenceNumber:" << nextSequenceNumber;
719 if (!complexItem) {
720 errorString = tr("Unsupported complex item type: %1").arg(complexItemType);
721 return false;
722 }
723 if (!complexItem->load(itemObject, nextSequenceNumber++, errorString)) {
724 delete complexItem;
725 return false;
726 }
727 nextSequenceNumber = complexItem->lastSequenceNumber() + 1;
728 qCDebug(MissionControllerLog) << "Complex item load complete nextSequenceNumber:" << nextSequenceNumber;
729 visualItems->append(complexItem);
730 } else {
731 errorString = tr("Unknown item type: %1").arg(itemType);
732 return false;
733 }
734 }
735
736 // Fix up the DO_JUMP commands jump sequence number by finding the item with the matching doJumpId
737 for (int i=0; i<visualItems->count(); i++) {
740 if (doJumpItem->command() == MAV_CMD_DO_JUMP) {
741 bool found = false;
742 int findDoJumpId = static_cast<int>(doJumpItem->missionItem().param1());
743 for (int j=0; j<visualItems->count(); j++) {
746 if (targetItem->missionItem().doJumpId() == findDoJumpId) {
747 doJumpItem->missionItem().setParam1(targetItem->sequenceNumber());
748 found = true;
749 break;
750 }
751 }
752 }
753 if (!found) {
754 errorString = tr("Could not find doJumpId: %1").arg(findDoJumpId);
755 return false;
756 }
757 }
758 }
759 }
760
761 return true;
762}
763
764bool MissionController::_loadTextMissionFile(QTextStream& stream, QmlObjectListModel* visualItems, QString& errorString)
765{
766 bool firstItem = true;
767 bool plannedHomePositionInFile = false;
768
769 QString firstLine = stream.readLine();
770 const QStringList& version = firstLine.split(" ");
771
772 bool versionOk = false;
773 if (version.size() == 3 && version[0] == "QGC" && version[1] == "WPL") {
774 if (version[2] == "110") {
775 // ArduPilot file, planned home position is already in position 0
776 versionOk = true;
777 plannedHomePositionInFile = true;
778 } else if (version[2] == "120") {
779 // Old QGC file, no planned home position
780 versionOk = true;
781 plannedHomePositionInFile = false;
782 }
783 }
784
785 if (versionOk) {
786 MissionSettingsItem* settingsItem = _addMissionSettings(visualItems);
787
788 while (!stream.atEnd()) {
789 SimpleMissionItem* item = new SimpleMissionItem(_masterController, _flyView, true /* forLoad */);
790 if (item->load(stream)) {
791 if (firstItem && plannedHomePositionInFile) {
792 settingsItem->setCoordinate(item->coordinate());
793 } else {
794 if (TakeoffMissionItem::isTakeoffCommand(static_cast<MAV_CMD>(item->command()))) {
795 // This needs to be a TakeoffMissionItem
796 TakeoffMissionItem* takeoffItem = new TakeoffMissionItem(item->missionItem(), _masterController, _flyView, settingsItem, false /* forLoad */);
797 item->deleteLater();
798 item = takeoffItem;
799 }
800 visualItems->append(item);
801 }
802 firstItem = false;
803 } else {
804 errorString = tr("The mission file is corrupted.");
805 return false;
806 }
807 }
808 } else {
809 errorString = tr("The mission file is not compatible with this version of %1.").arg(QCoreApplication::applicationName());
810 return false;
811 }
812
813 if (!plannedHomePositionInFile) {
814 // Update sequence numbers in DO_JUMP commands to take into account added home position in index 0
815 for (int i=1; i<visualItems->count(); i++) {
816 SimpleMissionItem* item = qobject_cast<SimpleMissionItem*>(visualItems->get(i));
817 if (item && item->command() == MAV_CMD_DO_JUMP) {
818 item->missionItem().setParam1(static_cast<int>(item->missionItem().param1()) + 1);
819 }
820 }
821 }
822
823 return true;
824}
825
826void MissionController::_initLoadedVisualItems(QmlObjectListModel* loadedVisualItems)
827{
828 _setupNewVisualItems(loadedVisualItems);
829
830 MissionController::_scanForAdditionalSettings(_visualItems, _masterController);
831
832 _initAllVisualItems();
833
834 if (_visualItems->count() > 1) {
835 _firstItemAdded();
836 } else {
837 _allItemsRemoved();
838 }
839}
840
841bool MissionController::load(const QJsonObject& json, QString& errorString)
842{
843 QString errorStr;
844 QString errorMessage = tr("Mission: %1");
845 QmlObjectListModel* loadedVisualItems = new QmlObjectListModel(this);
846
847 if (!_loadJsonMissionFileV2(json, loadedVisualItems, errorStr)) {
848 errorString = errorMessage.arg(errorStr);
849 return false;
850 }
851 _initLoadedVisualItems(loadedVisualItems);
852
853 return true;
854}
855
857{
858 QString errorStr;
859 QString errorMessage = tr("Mission: %1");
860 QByteArray bytes = file.readAll();
861 QTextStream stream(bytes);
862
864
865 QmlObjectListModel* loadedVisualItems = new QmlObjectListModel(this);
866 if (!_loadTextMissionFile(stream, loadedVisualItems, errorStr)) {
867 errorString = errorMessage.arg(errorStr);
868 return false;
869 }
870
871 _initLoadedVisualItems(loadedVisualItems);
872
873 return true;
874}
875
877{
878 for (int i=0; i<_visualItems->count(); i++) {
879 VisualMissionItem* visualItem = qobject_cast<VisualMissionItem*>(_visualItems->get(i));
881 return visualItem->readyForSaveState();
882 }
883 }
884
886}
887
888void MissionController::save(QJsonObject& json)
889{
890 json[JsonParsing::jsonVersionKey] = _missionFileVersion;
891
892 // Mission settings
893
894 MissionSettingsItem* settingsItem = _visualItems->value<MissionSettingsItem*>(0);
895 if (!settingsItem) {
896 qWarning() << "First item is not MissionSettingsItem";
897 return;
898 }
899 QJsonValue coordinateValue;
900 GeoJsonHelper::saveGeoCoordinate(settingsItem->coordinate(), true /* writeAltitude */, coordinateValue);
901 json[_jsonPlannedHomePositionKey] = coordinateValue;
902 json[_jsonFirmwareTypeKey] = _controllerVehicle->firmwareType();
903 json[_jsonVehicleTypeKey] = _controllerVehicle->vehicleType();
904 json[_jsonCruiseSpeedKey] = _controllerVehicle->defaultCruiseSpeed();
905 json[_jsonHoverSpeedKey] = _controllerVehicle->defaultHoverSpeed();
906 json[_jsonGlobalPlanAltitudeModeKey] = _globalAltFrame;
907
908 // Save the visual items
909
910 QJsonArray rgJsonMissionItems;
911 for (int i=0; i<_visualItems->count(); i++) {
912 VisualMissionItem* visualItem = qobject_cast<VisualMissionItem*>(_visualItems->get(i));
913
914 visualItem->save(rgJsonMissionItems);
915 }
916
917 // Mission settings has a special case for end mission action
918 if (settingsItem) {
919 QList<MissionItem*> rgMissionItems;
920
921 if (_convertToMissionItems(_visualItems, rgMissionItems, this /* missionItemParent */)) {
922 QJsonObject saveObject;
923 MissionItem* missionItem = rgMissionItems[rgMissionItems.count() - 1];
924 missionItem->save(saveObject);
925 rgJsonMissionItems.append(saveObject);
926 }
927 for (int i=0; i<rgMissionItems.count(); i++) {
928 rgMissionItems[i]->deleteLater();
929 }
930 }
931
932 json[_jsonItemsKey] = rgJsonMissionItems;
933}
934
935static FlightPathSegment::SegmentType segmentTypeForPair(const VisualItemPair& pair, bool mavlinkTerrainFrame)
936{
937 if (pair.second->isTakeoffItem()) {
939 }
940 if (pair.second->isLandCommand()) {
942 }
944}
945
946FlightPathSegment* MissionController::_createFlightPathSegmentWorker(VisualItemPair& pair, bool mavlinkTerrainFrame)
947{
948 // The takeoff goes straight up from ground to alt and then over to specified position at same alt. Which means
949 // that coord 1 altitude is the same as coord altitude.
950 bool takeoffStraightUp = pair.second->isTakeoffItem() && !_controllerVehicle->fixedWing();
951
952 QGeoCoordinate coord1 = pair.first->exitCoordinate();
953 QGeoCoordinate coord2 = pair.second->entryCoordinate();
954 double coord2AMSLAlt = pair.second->amslEntryAlt();
955 double coord1AMSLAlt = takeoffStraightUp ? coord2AMSLAlt : pair.first->amslExitAlt();
956
957 FlightPathSegment* segment =
958 new FlightPathSegment(segmentTypeForPair(pair, mavlinkTerrainFrame), coord1, coord1AMSLAlt, coord2,
959 coord2AMSLAlt, !_flyView /* queryTerrainData */, this);
960
961 if (takeoffStraightUp) {
963 } else {
965 }
969
972
973 connect(segment, &FlightPathSegment::totalDistanceChanged, this, &MissionController::recalcTerrainProfile, Qt::QueuedConnection);
976 connect(segment, &FlightPathSegment::amslTerrainHeightsChanged, this, &MissionController::recalcTerrainProfile, Qt::QueuedConnection);
977 connect(segment, &FlightPathSegment::terrainCollisionChanged, this, &MissionController::recalcTerrainProfile, Qt::QueuedConnection);
978
979 return segment;
980}
981
982FlightPathSegment* MissionController::_addFlightPathSegment(FlightPathSegmentHashTable& prevItemPairHashTable, VisualItemPair& pair, bool mavlinkTerrainFrame)
983{
984 FlightPathSegment* segment = nullptr;
985
986 if (prevItemPairHashTable.contains(pair) &&
987 prevItemPairHashTable[pair]->segmentType() == segmentTypeForPair(pair, mavlinkTerrainFrame)) {
988 // Pair already exists and connected, just re-use
989 _flightPathSegmentHashTable[pair] = segment = prevItemPairHashTable.take(pair);
990 } else {
991 segment = _createFlightPathSegmentWorker(pair, mavlinkTerrainFrame);
992 _flightPathSegmentHashTable[pair] = segment;
993 }
994
995 _simpleFlightPathSegments.append(segment);
996
997 return segment;
998}
999
1000void MissionController::_recalcFlightPathSegments(void)
1001{
1002 VisualItemPair lastSegmentVisualItemPair;
1003 int segmentCount = 0;
1004 bool firstCoordinateNotFound = true;
1005 VisualMissionItem* lastFlyThroughVI = qobject_cast<VisualMissionItem*>(_visualItems->get(0));
1006 bool linkEndToHome = false;
1007 bool linkStartToHome = _controllerVehicle->rover() ? true : false;
1008 bool foundRTL = false;
1009 bool homePositionValid = _settingsItem->coordinate().isValid();
1010 bool roiActive = false;
1011 bool previousItemIsIncomplete = false;
1012
1013 FlightPathSegment* newSplitSegment = nullptr;
1014
1015 qCDebug(MissionControllerLog) << "_recalcFlightPathSegments homePositionValid" << homePositionValid;
1016
1017 FlightPathSegmentHashTable oldSegmentTable = _flightPathSegmentHashTable;
1018
1019 _missionStartsInVTOLMulticopterMode = false;
1020 _flightPathSegmentHashTable.clear();
1021
1022 _simpleFlightPathSegments.beginResetModel();
1023 _directionArrows.beginResetModel();
1024
1025 _simpleFlightPathSegments.clear();
1026 _directionArrows.clear();
1027
1028 // Mission Settings item needs to start with no segment
1029 lastFlyThroughVI->clearSimpleFlighPathSegment();
1030
1031 // We need to clear the simple flight path segments on all items since they are going to be rebuilt. We can't just do this in the main loop
1032 // below since that loop won't always process all items.
1033 for (int i=1; i<_visualItems->count(); i++) {
1034 qobject_cast<VisualMissionItem*>(_visualItems->get(i))->clearSimpleFlighPathSegment();
1035 }
1036
1037 // Grovel through the list of items keeping track of things needed to correctly draw waypoints lines
1038 for (int i=1; i<_visualItems->count(); i++) {
1039 VisualMissionItem* visualItem = qobject_cast<VisualMissionItem*>(_visualItems->get(i));
1040 SimpleMissionItem* simpleItem = qobject_cast<SimpleMissionItem*>(visualItem);
1041 ComplexMissionItem* complexItem = qobject_cast<ComplexMissionItem*>(visualItem);
1042
1043 visualItem->clearSimpleFlighPathSegment();
1044
1045 if (simpleItem) {
1046 if (roiActive) {
1047 if (_isROICancelItem(simpleItem)) {
1048 roiActive = false;
1049 }
1050 } else {
1051 if (_isROIBeginItem(simpleItem)) {
1052 roiActive = true;
1053 }
1054 }
1055
1056 MAV_CMD command = simpleItem->mavCommand();
1057 switch (command) {
1058 // A takeoff before any coordinate item means the mission starts from the ground: link it back to home
1059 case MAV_CMD_NAV_TAKEOFF:
1060 if (!linkEndToHome && firstCoordinateNotFound) {
1061 _missionStartsInVTOLMulticopterMode =
1062 TakeoffMissionItem::isVTOLMulticopterTakeoff(_controllerVehicle, command);
1063 linkStartToHome = true;
1064 }
1065 break;
1066 case MAV_CMD_NAV_VTOL_TAKEOFF:
1067 if (!linkEndToHome && firstCoordinateNotFound) {
1068 _missionStartsInVTOLMulticopterMode = true;
1069 linkStartToHome = true;
1070 }
1071 break;
1072 case MAV_CMD_NAV_RETURN_TO_LAUNCH:
1073 linkEndToHome = true;
1074 foundRTL = true;
1075 break;
1076 default:
1077 break;
1078 }
1079 }
1080
1081 // No need to add waypoint segments after an RTL.
1082 if (foundRTL) {
1083 break;
1084 }
1085
1086 // Don't draw segments immediately after a landing item
1087 if (lastFlyThroughVI->isLandCommand()) {
1088 lastFlyThroughVI = visualItem;
1089 continue;
1090 }
1091
1092 if (visualItem->specifiesCoordinate() && !visualItem->isStandaloneCoordinate()) {
1093 // Incomplete items are complex items which are waiting for the user to complete setup before there visuals can become valid.
1094 // They may not yet have valid entry/exit coordinates associated with them while in the incomplete state.
1095 // For examples a Survey item which has no polygon set yet.
1096 if (complexItem && complexItem->isIncomplete()) {
1097 // We don't link lines from a valid item to an incomplete item
1098 previousItemIsIncomplete = true;
1099 } else if (previousItemIsIncomplete) {
1100 // We also don't link lines from an incomplete item to a valid item.
1101 previousItemIsIncomplete = false;
1102 firstCoordinateNotFound = false;
1103 lastFlyThroughVI = visualItem;
1104 } else {
1105 if (lastFlyThroughVI != _settingsItem || (homePositionValid && linkStartToHome)) {
1106 bool addDirectionArrow = false;
1107 if (i != 1) {
1108 // Direction arrows are added to the second segment and every 5 segments thereafter.
1109 // The reason for start with second segment is to prevent an arrow being added in between the home position
1110 // and a takeoff item which may be right over each other. In that case the arrow points in a random direction.
1111 if (firstCoordinateNotFound || !lastFlyThroughVI->isSimpleItem() || !visualItem->isSimpleItem()) {
1112 addDirectionArrow = true;
1113 } else if (segmentCount > 5) {
1114 segmentCount = 0;
1115 addDirectionArrow = true;
1116 }
1117 segmentCount++;
1118 }
1119
1120 lastSegmentVisualItemPair = VisualItemPair(lastFlyThroughVI, visualItem);
1121 // A leg is flown in its destination item's frame (ArduPilot AC_WPNav)
1122 bool mavlinkTerrainFrame = simpleItem ? simpleItem->missionItem().frame() == MAV_FRAME_GLOBAL_TERRAIN_ALT : false;
1123 FlightPathSegment* segment = _addFlightPathSegment(oldSegmentTable, lastSegmentVisualItemPair, mavlinkTerrainFrame);
1124 segment->setSpecialVisual(roiActive);
1125 if (addDirectionArrow) {
1126 _directionArrows.append(segment);
1127 }
1128 // Same rule as setCurrentPlanViewSeqNum: the leg must start at a coordinate item other than home
1129 const bool legStartsAtCoordinateItem = lastFlyThroughVI != _settingsItem &&
1130 lastFlyThroughVI->specifiesCoordinate() &&
1131 !lastFlyThroughVI->isStandaloneCoordinate();
1132 if (!_flyView && visualItem->isCurrentItem() && legStartsAtCoordinateItem) {
1133 newSplitSegment = segment;
1134 }
1135 lastFlyThroughVI->setSimpleFlighPathSegment(segment);
1136 }
1137 firstCoordinateNotFound = false;
1138 lastFlyThroughVI = visualItem;
1139 }
1140 }
1141 }
1142
1143 if (linkEndToHome && lastFlyThroughVI != _settingsItem && homePositionValid) {
1144 lastSegmentVisualItemPair = VisualItemPair(lastFlyThroughVI, _settingsItem);
1145 FlightPathSegment* segment = _addFlightPathSegment(oldSegmentTable, lastSegmentVisualItemPair, false /* mavlinkTerrainFrame */);
1146 segment->setSpecialVisual(roiActive);
1147 lastFlyThroughVI->setSimpleFlighPathSegment(segment);
1148 }
1149
1150 // Add direction arrow to last segment
1151 if (lastSegmentVisualItemPair.first) {
1152 FlightPathSegment* coordVector = nullptr;
1153
1154 // The pair may not be in the hash, this can happen in the fly view where only segments with arrows on them are added to hash.
1155 // check for that first and add if needed
1156
1157 if (_flightPathSegmentHashTable.contains(lastSegmentVisualItemPair)) {
1158 // Pair exists in the new table already just reuse it
1159 coordVector = _flightPathSegmentHashTable[lastSegmentVisualItemPair];
1160 } else if (oldSegmentTable.contains(lastSegmentVisualItemPair)) {
1161 // Pair already exists in old table, pull from old to new and reuse
1162 _flightPathSegmentHashTable[lastSegmentVisualItemPair] = coordVector = oldSegmentTable.take(lastSegmentVisualItemPair);
1163 } else {
1164 // Create a new segment. Since this is the fly view there is no need to wire change signals or worry about correct SegmentType
1165 coordVector = new FlightPathSegment(
1167 lastSegmentVisualItemPair.first->isSimpleItem() ? lastSegmentVisualItemPair.first->entryCoordinate() : lastSegmentVisualItemPair.first->exitCoordinate(),
1168 lastSegmentVisualItemPair.first->isSimpleItem() ? lastSegmentVisualItemPair.first->amslEntryAlt() : lastSegmentVisualItemPair.first->amslExitAlt(),
1169 lastSegmentVisualItemPair.second->entryCoordinate(),
1170 lastSegmentVisualItemPair.second->amslEntryAlt(),
1171 !_flyView /* queryTerrainData */,
1172 this);
1173 _flightPathSegmentHashTable[lastSegmentVisualItemPair] = coordVector;
1174 }
1175
1176 _directionArrows.append(coordVector);
1177 }
1178
1179 _simpleFlightPathSegments.endResetModel();
1180 _directionArrows.endResetModel();
1181
1182 // Must be updated before the old segments are deleted so _splitSegment never dangles
1183 const bool signalSplitSegmentChanged = newSplitSegment != _splitSegment;
1184 _splitSegment = newSplitSegment;
1185
1186 // Anything left in the old table is an obsolete line object that can go
1187 qDeleteAll(oldSegmentTable);
1188
1190
1191 emit recalcTerrainProfile();
1192 if (signalSplitSegmentChanged) {
1193 qCDebug(MissionControllerLog) << "splitSegmentSet:" << (_splitSegment != nullptr);
1194 emit splitSegmentChanged();
1195 }
1196}
1197
1198void MissionController::_recalcMissionFlightStatus()
1199{
1200 if (!_visualItems->count()) {
1201 return;
1202 }
1203
1204 qCDebug(MissionControllerLog) << "_recalcMissionFlightStatus";
1205
1206 _flightStatusCalc.recalc(
1207 _visualItems, _settingsItem, _controllerVehicle, _managerVehicle, _appSettings, _planViewSettings,
1208 _missionStartsInVTOLMulticopterMode);
1209 _missionFlightStatus = _flightStatusCalc.status();
1210 _minAMSLAltitude = _flightStatusCalc.minAMSLAltitude();
1211 _maxAMSLAltitude = _flightStatusCalc.maxAMSLAltitude();
1212
1213 emit missionMaxTelemetryChanged (_missionFlightStatus.maxTelemetryDistance);
1214 emit missionTotalDistanceChanged (_missionFlightStatus.totalDistance);
1215 emit missionPlannedDistanceChanged (_missionFlightStatus.plannedDistance);
1216 emit missionHoverDistanceChanged (_missionFlightStatus.hoverDistance);
1217 emit missionCruiseDistanceChanged (_missionFlightStatus.cruiseDistance);
1218 emit missionTimeChanged ();
1221 emit batteryChangePointChanged (_missionFlightStatus.batteryChangePoint);
1222 emit batteriesRequiredChanged (_missionFlightStatus.batteriesRequired);
1223 emit minAMSLAltitudeChanged (_minAMSLAltitude);
1224 emit maxAMSLAltitudeChanged (_maxAMSLAltitude);
1225
1226 _updateTimer.start(UPDATE_TIMEOUT);
1227
1228 emit recalcTerrainProfile();
1229}
1230
1231// This will update the sequence numbers to be sequential starting from 0
1232void MissionController::_recalcSequence(void)
1233{
1234 if (_inRecalcSequence) {
1235 // Don't let this call recurse due to signalling
1236 return;
1237 }
1238
1239 // Setup ascending sequence numbers for all visual items
1240
1241 _inRecalcSequence = true;
1242 int sequenceNumber = 0;
1243 for (int i=0; i<_visualItems->count(); i++) {
1244 VisualMissionItem* item = qobject_cast<VisualMissionItem*>(_visualItems->get(i));
1245 item->setSequenceNumber(sequenceNumber);
1246 sequenceNumber = item->lastSequenceNumber() + 1;
1247 }
1248 _inRecalcSequence = false;
1249}
1250
1251// This will update the child item hierarchy
1252void MissionController::_recalcChildItems(void)
1253{
1254 VisualMissionItem* currentParentItem = qobject_cast<VisualMissionItem*>(_visualItems->get(0));
1255
1256 currentParentItem->childItems()->clear();
1257
1258 for (int i=1; i<_visualItems->count(); i++) {
1259 VisualMissionItem* item = _visualItems->value<VisualMissionItem*>(i);
1260
1261 item->setParentItem(nullptr);
1262 item->setHasCurrentChildItem(false);
1263
1264 // Set up non-coordinate item child hierarchy
1265 if (item->specifiesCoordinate()) {
1266 item->childItems()->clear();
1267 currentParentItem = item;
1268 } else if (item->isSimpleItem()) {
1269 item->setParentItem(currentParentItem);
1270 currentParentItem->childItems()->append(item);
1271 if (item->isCurrentItem()) {
1272 currentParentItem->setHasCurrentChildItem(true);
1273 }
1274 }
1275 }
1276}
1277
1278void MissionController::_setupTreeModel(void)
1279{
1280 _visualItemsTree.beginResetModel();
1281
1282 _visualItemsTree.clear();
1283
1284 // ── Plan File group ──
1285 _planFileGroupNode.setObjectName(tr("Plan Info"));
1286 _visualItemsTree.appendItem(&_planFileGroupNode, QModelIndex(), QStringLiteral("planFileGroup"));
1287
1288 _planFileInfoMarker.setObjectName(QStringLiteral("planFileInfo"));
1289 _visualItemsTree.appendItem(&_planFileInfoMarker,
1290 _visualItemsTree.indexForObject(&_planFileGroupNode), QStringLiteral("planFileInfo"));
1291
1292 // ── Defaults group ──
1293 _defaultsGroupNode.setObjectName(tr("Defaults"));
1294 _visualItemsTree.appendItem(&_defaultsGroupNode, QModelIndex(), QStringLiteral("defaultsGroup"));
1295
1296 _defaultsInfoMarker.setObjectName(QStringLiteral("defaultsInfo"));
1297 _visualItemsTree.appendItem(&_defaultsInfoMarker,
1298 _visualItemsTree.indexForObject(&_defaultsGroupNode), QStringLiteral("defaultsInfo"));
1299
1300 // ── Mission Items group ──
1301 _missionItemsGroupNode.setObjectName(tr("Mission Items"));
1302 _visualItemsTree.appendItem(&_missionItemsGroupNode, QModelIndex(), QStringLiteral("missionGroup"));
1303
1304 // ── GeoFence group ──
1305 _fenceGroupNode.setObjectName(tr("GeoFence"));
1306 _visualItemsTree.appendItem(&_fenceGroupNode, QModelIndex(), QStringLiteral("fenceGroup"));
1307
1308 // Single marker child — delegate loads the full GeoFenceEditor
1309 _fenceEditorMarker.setObjectName(QStringLiteral("fenceEditor"));
1310 _visualItemsTree.appendItem(&_fenceEditorMarker,
1311 _visualItemsTree.indexForObject(&_fenceGroupNode), QStringLiteral("fenceEditor"));
1312
1313 // ── Rally Points group ──
1314 _rallyGroupNode.setObjectName(tr("Rally Points"));
1315 _visualItemsTree.appendItem(&_rallyGroupNode, QModelIndex(), QStringLiteral("rallyGroup"));
1316
1317 // Marker child for the rally header / instructions
1318 _rallyHeaderMarker.setObjectName(QStringLiteral("rallyHeader"));
1319 _visualItemsTree.appendItem(&_rallyHeaderMarker,
1320 _visualItemsTree.indexForObject(&_rallyGroupNode), QStringLiteral("rallyHeader"));
1321
1322 // ── Transform group ──
1323 _transformGroupNode.setObjectName(tr("Transform"));
1324 _visualItemsTree.appendItem(&_transformGroupNode, QModelIndex(), QStringLiteral("transformGroup"));
1325
1326 // Single marker child — delegate loads the inline TransformEditor
1327 _transformEditorMarker.setObjectName(QStringLiteral("transformEditor"));
1328 _visualItemsTree.appendItem(&_transformEditorMarker,
1329 _visualItemsTree.indexForObject(&_transformGroupNode), QStringLiteral("transformEditor"));
1330
1331 _visualItemsTree.endResetModel();
1332
1333 // Capture persistent indexes after the reset so they aren't invalidated
1334 _planFileGroupIndex = QPersistentModelIndex(_visualItemsTree.indexForObject(&_planFileGroupNode));
1335 _defaultsGroupIndex = QPersistentModelIndex(_visualItemsTree.indexForObject(&_defaultsGroupNode));
1336 _missionGroupIndex = QPersistentModelIndex(_visualItemsTree.indexForObject(&_missionItemsGroupNode));
1337 _fenceGroupIndex = QPersistentModelIndex(_visualItemsTree.indexForObject(&_fenceGroupNode));
1338 _rallyGroupIndex = QPersistentModelIndex(_visualItemsTree.indexForObject(&_rallyGroupNode));
1339 _transformGroupIndex = QPersistentModelIndex(_visualItemsTree.indexForObject(&_transformGroupNode));
1340}
1341
1342//-----------------------------------------------------------------------------
1343// Incremental tree model sync — Mission Items
1344//-----------------------------------------------------------------------------
1345
1346void MissionController::_syncTreeMissionItemsInserted(const QModelIndex& parent, int first, int last)
1347{
1348 Q_UNUSED(parent);
1349 for (int i = first; i <= last; i++) {
1350 auto* item = _visualItems->value<VisualMissionItem*>(i);
1351 if (item) {
1352 _visualItemsTree.insertItem(i, item, _missionGroupIndex, QStringLiteral("missionItem"));
1353 }
1354 }
1355}
1356
1357void MissionController::_syncTreeMissionItemsAboutToBeRemoved(const QModelIndex& parent, int first, int last)
1358{
1359 Q_UNUSED(parent);
1360 // Remove in reverse order so indexes stay valid
1361 for (int i = last; i >= first; i--) {
1362 _visualItemsTree.removeAt(_missionGroupIndex, i);
1363 }
1364}
1365
1366void MissionController::_syncTreeMissionItemsReset(void)
1367{
1368 // removeChildren + appendItem each fire their own row-level signals scoped
1369 // to _missionGroupIndex, so persistent indexes for other groups survive.
1370 _visualItemsTree.removeChildren(_missionGroupIndex);
1371
1372 if (_visualItems) {
1373 for (int i = 0; i < _visualItems->count(); i++) {
1374 auto* item = _visualItems->value<VisualMissionItem*>(i);
1375 if (item) {
1376 _visualItemsTree.appendItem(item, _missionGroupIndex, QStringLiteral("missionItem"));
1377 }
1378 }
1379 }
1380}
1381
1382//-----------------------------------------------------------------------------
1383// Incremental tree model sync — Rally Points
1384//-----------------------------------------------------------------------------
1385
1386void MissionController::_syncTreeRallyPointsInserted(const QModelIndex& parent, int first, int last)
1387{
1388 Q_UNUSED(parent);
1389 auto* rallyController = _masterController->rallyPointController();
1390 if (!rallyController) return;
1391
1392 // If this is the first rally point, remove the header marker
1393 if (first == 0 && _visualItemsTree.rowCount(_rallyGroupIndex) == 1) {
1394 const QModelIndex child = _visualItemsTree.index(0, 0, _rallyGroupIndex);
1395 if (_visualItemsTree.data(child, QmlObjectTreeModel::NodeTypeRole).toString() == QStringLiteral("rallyHeader")) {
1396 _visualItemsTree.removeItem(child);
1397 }
1398 }
1399
1400 auto* pts = rallyController->points();
1401 for (int i = first; i <= last; i++) {
1402 _visualItemsTree.insertItem(i, (*pts)[i], _rallyGroupIndex, QStringLiteral("rallyItem"));
1403 }
1404}
1405
1406void MissionController::_syncTreeRallyPointsAboutToBeRemoved(const QModelIndex& parent, int first, int last)
1407{
1408 Q_UNUSED(parent);
1409 for (int i = last; i >= first; i--) {
1410 _visualItemsTree.removeAt(_rallyGroupIndex, i);
1411 }
1412}
1413
1414void MissionController::_syncTreeRallyPointsRemoved(const QModelIndex& parent, int first, int last)
1415{
1416 Q_UNUSED(parent);
1417 Q_UNUSED(first);
1418 Q_UNUSED(last);
1419
1420 // Re-add the header marker once the source model has finished removing rows
1421 auto* rallyController = _masterController->rallyPointController();
1422 if (rallyController && rallyController->points()->count() == 0) {
1423 _rallyHeaderMarker.setObjectName(QStringLiteral("rallyHeader"));
1424 _visualItemsTree.appendItem(&_rallyHeaderMarker, _rallyGroupIndex, QStringLiteral("rallyHeader"));
1425 }
1426}
1427
1428void MissionController::_syncTreeRallyPointsReset(void)
1429{
1430 // removeChildren + appendItem each fire their own row-level signals scoped
1431 // to _rallyGroupIndex, so persistent indexes for other groups survive.
1432 _visualItemsTree.removeChildren(_rallyGroupIndex);
1433
1434 // Repopulate — either rally items or the header marker
1435 auto* rallyController = _masterController->rallyPointController();
1436 if (rallyController && rallyController->points()->count() > 0) {
1437 auto* pts = rallyController->points();
1438 for (int i = 0; i < pts->count(); i++) {
1439 _visualItemsTree.appendItem((*pts)[i], _rallyGroupIndex, QStringLiteral("rallyItem"));
1440 }
1441 } else {
1442 _rallyHeaderMarker.setObjectName(QStringLiteral("rallyHeader"));
1443 _visualItemsTree.appendItem(&_rallyHeaderMarker, _rallyGroupIndex, QStringLiteral("rallyHeader"));
1444 }
1445}
1446
1447void MissionController::_setPlannedHomePositionFromFirstCoordinate(const QGeoCoordinate& clickCoordinate)
1448{
1449 bool foundFirstCoordinate = false;
1450 QGeoCoordinate firstCoordinate;
1451
1452 if (_settingsItem->coordinate().isValid()) {
1453 return;
1454 }
1455
1456 // Set the planned home position to be a delta from first coordinate
1457 for (int i=1; i<_visualItems->count(); i++) {
1458 VisualMissionItem* item = _visualItems->value<VisualMissionItem*>(i);
1459
1460 if (item->specifiesCoordinate() && item->entryCoordinate().isValid()) {
1461 foundFirstCoordinate = true;
1462 firstCoordinate = item->entryCoordinate();
1463 break;
1464 }
1465 }
1466
1467 // No item specifying a coordinate was found, in this case it we have a clickCoordinate use that
1468 if (!foundFirstCoordinate) {
1469 firstCoordinate = clickCoordinate;
1470 }
1471
1472 if (firstCoordinate.isValid()) {
1473 QGeoCoordinate plannedHomeCoord = firstCoordinate.atDistanceAndAzimuth(30, 0);
1474 plannedHomeCoord.setAltitude(0);
1475 _settingsItem->setCoordinate(plannedHomeCoord);
1476 }
1477}
1478
1479void MissionController::_recalcAllWithCoordinate(const QGeoCoordinate& coordinate)
1480{
1481 if (!_flyView) {
1482 _setPlannedHomePositionFromFirstCoordinate(coordinate);
1483 }
1484 _recalcSequence();
1485 _recalcChildItems();
1487 _updateTimer.start(UPDATE_TIMEOUT);
1488}
1489
1490void MissionController::_recalcAll(void)
1491{
1492 QGeoCoordinate emptyCoord;
1493 _recalcAllWithCoordinate(emptyCoord);
1494}
1495
1497void MissionController::_initAllVisualItems(void)
1498{
1499 // Setup home position at index 0
1500
1501 if (!_settingsItem) {
1502 _settingsItem = qobject_cast<MissionSettingsItem*>(_visualItems->get(0));
1503 if (!_settingsItem) {
1504 qWarning() << "First item not MissionSettingsItem";
1505 return;
1506 }
1507 }
1508
1509 connect(_settingsItem, &MissionSettingsItem::coordinateChanged, this, &MissionController::_recalcMissionFlightStatus);
1512
1513 for (int i=0; i<_visualItems->count(); i++) {
1514 VisualMissionItem* item = qobject_cast<VisualMissionItem*>(_visualItems->get(i));
1515 _initVisualItem(item);
1516
1517 TakeoffMissionItem* takeoffItem = qobject_cast<TakeoffMissionItem*>(item);
1518 if (takeoffItem) {
1519 _takeoffMissionItem = takeoffItem;
1520 }
1521 }
1522
1523 _recalcAll();
1524
1525 connect(_visualItems, &QmlObjectListModel::dirtyChanged, this, &MissionController::_visualItemsDirtyChanged);
1527
1528 // Connect for incremental tree model sync
1529 connect(_visualItems, &QAbstractItemModel::rowsInserted, this, &MissionController::_syncTreeMissionItemsInserted);
1530 connect(_visualItems, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MissionController::_syncTreeMissionItemsAboutToBeRemoved);
1531 connect(_visualItems, &QAbstractItemModel::modelReset, this, &MissionController::_syncTreeMissionItemsReset);
1532
1533 // Populate tree's mission group from current _visualItems
1534 _syncTreeMissionItemsReset();
1535
1536 // Connect rally controller for incremental tree model sync
1537 auto* rallyController = _masterController->rallyPointController();
1538 if (rallyController) {
1539 auto* pts = rallyController->points();
1540 connect(pts, &QAbstractItemModel::rowsInserted, this, &MissionController::_syncTreeRallyPointsInserted);
1541 connect(pts, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MissionController::_syncTreeRallyPointsAboutToBeRemoved);
1542 connect(pts, &QAbstractItemModel::rowsRemoved, this, &MissionController::_syncTreeRallyPointsRemoved);
1543 connect(pts, &QAbstractItemModel::modelReset, this, &MissionController::_syncTreeRallyPointsReset);
1544
1545 // Populate any existing rally points
1546 _syncTreeRallyPointsReset();
1547 }
1548
1549 emit visualItemsReset();
1550 emit containsItemsChanged();
1553
1554 if (!_flyView) {
1555 setCurrentPlanViewSeqNum(0, true);
1556 }
1557
1558 setDirty(false);
1559}
1560
1561void MissionController::_deinitAllVisualItems(void)
1562{
1563 disconnect(_settingsItem, &MissionSettingsItem::coordinateChanged, this, &MissionController::_recalcMissionFlightStatus);
1566
1567 for (int i=0; i<_visualItems->count(); i++) {
1568 _deinitVisualItem(qobject_cast<VisualMissionItem*>(_visualItems->get(i)));
1569 }
1570
1571 disconnect(_visualItems, &QmlObjectListModel::dirtyChanged, this, &MissionController::_visualItemsDirtyChanged);
1573
1574 // Disconnect incremental tree model sync
1575 disconnect(_visualItems, &QAbstractItemModel::rowsInserted, this, &MissionController::_syncTreeMissionItemsInserted);
1576 disconnect(_visualItems, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MissionController::_syncTreeMissionItemsAboutToBeRemoved);
1577 disconnect(_visualItems, &QAbstractItemModel::modelReset, this, &MissionController::_syncTreeMissionItemsReset);
1578
1579 // Disconnect rally controller tree model sync
1580 auto* rallyController = _masterController->rallyPointController();
1581 if (rallyController) {
1582 auto* pts = rallyController->points();
1583 disconnect(pts, &QAbstractItemModel::rowsInserted, this, &MissionController::_syncTreeRallyPointsInserted);
1584 disconnect(pts, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MissionController::_syncTreeRallyPointsAboutToBeRemoved);
1585 disconnect(pts, &QAbstractItemModel::rowsRemoved, this, &MissionController::_syncTreeRallyPointsRemoved);
1586 disconnect(pts, &QAbstractItemModel::modelReset, this, &MissionController::_syncTreeRallyPointsReset);
1587 }
1588}
1589
1590void MissionController::_initVisualItem(VisualMissionItem* visualItem)
1591{
1592 setDirty(false);
1593
1602 connect(visualItem, &VisualMissionItem::lastSequenceNumberChanged, this, &MissionController::_recalcSequence);
1603
1604 if (visualItem->isSimpleItem()) {
1605 // We need to track commandChanged on simple item since recalc has special handling for takeoff command
1606 SimpleMissionItem* simpleItem = qobject_cast<SimpleMissionItem*>(visualItem);
1607 if (simpleItem) {
1608 connect(&simpleItem->missionItem()._commandFact, &Fact::valueChanged, this, &MissionController::_itemCommandChanged);
1609 // The altitude frame drives segmentTypeForPair, and segment type is immutable once created
1610 connect(simpleItem, &SimpleMissionItem::altitudeFrameChanged, this,
1612 } else {
1613 qWarning() << "isSimpleItem == true, yet not SimpleMissionItem";
1614 }
1615 } else {
1616 ComplexMissionItem* complexItem = qobject_cast<ComplexMissionItem*>(visualItem);
1617 if (complexItem) {
1622 connect(complexItem, &ComplexMissionItem::isIncompleteChanged, this, &MissionController::_recalcFlightPathSegmentsSignal, Qt::QueuedConnection);
1623 } else {
1624 qWarning() << "ComplexMissionItem not found";
1625 }
1626 }
1627}
1628
1629void MissionController::_deinitVisualItem(VisualMissionItem* visualItem)
1630{
1631 // Disconnect only signals connected to this MissionController, not all receivers.
1632 // A full wildcard disconnect(obj, 0, 0, 0) tears out internal destroyed-signal
1633 // connections that Qt (and the tree/list models) rely on for cleanup.
1634 disconnect(visualItem, nullptr, this, nullptr);
1635}
1636
1637void MissionController::_itemCommandChanged(void)
1638{
1639 _recalcChildItems();
1641}
1642
1643void MissionController::_managerVehicleChanged(Vehicle* managerVehicle)
1644{
1645 if (_managerVehicle) {
1646 _missionManager->disconnect(this);
1647 _managerVehicle->disconnect(this);
1648 _managerVehicle = nullptr;
1649 _missionManager = nullptr;
1650 }
1651
1652 _managerVehicle = managerVehicle;
1653 if (!_managerVehicle) {
1654 qWarning() << "MissionController::managerVehicleChanged managerVehicle=NULL";
1655 return;
1656 }
1657
1658 _missionManager = _managerVehicle->missionManager();
1659 connect(_missionManager, &MissionManager::newMissionItemsAvailable, this, &MissionController::_newMissionItemsAvailableFromVehicle);
1660 connect(_missionManager, &MissionManager::sendComplete, this, &MissionController::_managerSendComplete);
1661 connect(_missionManager, &MissionManager::removeAllComplete, this, &MissionController::_managerRemoveAllComplete);
1662 connect(_missionManager, &MissionManager::inProgressChanged, this, &MissionController::_inProgressChanged);
1663 connect(_missionManager, &MissionManager::progressPctChanged, this, &MissionController::_progressPctChanged);
1664 connect(_missionManager, &MissionManager::currentIndexChanged, this, &MissionController::_currentMissionIndexChanged);
1668 connect(_managerVehicle, &Vehicle::defaultCruiseSpeedChanged, this, &MissionController::_recalcMissionFlightStatusSignal, Qt::QueuedConnection);
1669 connect(_managerVehicle, &Vehicle::defaultHoverSpeedChanged, this, &MissionController::_recalcMissionFlightStatusSignal, Qt::QueuedConnection);
1671
1674}
1675
1676void MissionController::_inProgressChanged(bool inProgress)
1677{
1678 emit syncInProgressChanged(inProgress);
1679}
1680
1681bool MissionController::_findPreviousAltitude(int newIndex, double* prevAltitude, QGroundControlQmlGlobal::AltitudeFrame* prevAltitudeMode)
1682{
1683 bool found = false;
1684 double foundAltitude = 0;
1686
1687 if (newIndex > _visualItems->count()) {
1688 return false;
1689 }
1690 newIndex--;
1691
1692 for (int i=newIndex; i>0; i--) {
1693 VisualMissionItem* visualItem = qobject_cast<VisualMissionItem*>(_visualItems->get(i));
1694
1695 if (visualItem->specifiesCoordinate() && !visualItem->isStandaloneCoordinate()) {
1696 if (visualItem->isSimpleItem()) {
1697 SimpleMissionItem* simpleItem = qobject_cast<SimpleMissionItem*>(visualItem);
1698 if (simpleItem->specifiesAltitude()) {
1699 foundAltitude = simpleItem->altitude()->rawValue().toDouble();
1700 foundAltFrame = simpleItem->altitudeFrame();
1701 found = true;
1702 break;
1703 }
1704 }
1705 }
1706 }
1707
1708 if (found) {
1709 *prevAltitude = foundAltitude;
1710 *prevAltitudeMode = foundAltFrame;
1711 }
1712
1713 return found;
1714}
1715
1716double MissionController::_normalizeLat(double lat)
1717{
1718 // Normalize latitude to range: 0 to 180, S to N
1719 return lat + 90.0;
1720}
1721
1722double MissionController::_normalizeLon(double lon)
1723{
1724 // Normalize longitude to range: 0 to 360, W to E
1725 return lon + 180.0;
1726}
1727
1729MissionSettingsItem* MissionController::_addMissionSettings(QmlObjectListModel* visualItems)
1730{
1731 qCDebug(MissionControllerLog) << "_addMissionSettings";
1732
1734 visualItems->insert(0, settingsItem);
1735
1736 if (visualItems == _visualItems) {
1737 _settingsItem = settingsItem;
1738 }
1739
1740 return settingsItem;
1741}
1742
1744{
1745
1746 int resumeIndex = 0;
1747
1748 if (_flyView) {
1749 resumeIndex = _missionManager->lastCurrentIndex() + (_controllerVehicle->firmwarePlugin()->sendHomePositionToVehicle() ? 0 : 1);
1750 if (resumeIndex > 1 && resumeIndex != _visualItems->value<VisualMissionItem*>(_visualItems->count() - 1)->sequenceNumber()) {
1751 // Resume at the item previous to the item we were heading towards
1752 resumeIndex--;
1753 } else {
1754 resumeIndex = 0;
1755 }
1756 }
1757
1758 return resumeIndex;
1759}
1760
1762{
1763 if (!_flyView) {
1764 return -1;
1765 } else {
1766 int currentIndex = _missionManager->currentIndex();
1767 if (!_controllerVehicle->firmwarePlugin()->sendHomePositionToVehicle()) {
1768 currentIndex++;
1769 }
1770 return currentIndex;
1771 }
1772}
1773
1774void MissionController::_currentMissionIndexChanged(int sequenceNumber)
1775{
1776 if (_flyView) {
1777 if (!_controllerVehicle->firmwarePlugin()->sendHomePositionToVehicle()) {
1778 sequenceNumber++;
1779 }
1780
1781 for (int i=0; i<_visualItems->count(); i++) {
1782 VisualMissionItem* item = qobject_cast<VisualMissionItem*>(_visualItems->get(i));
1783 item->setIsCurrentItem(item->sequenceNumber() == sequenceNumber);
1784 }
1786 }
1787}
1788
1790{
1791 return _missionManager->inProgress();
1792}
1793
1795{
1796 return _visualItems ? _visualItems->dirty() : false;
1797}
1798
1799
1801{
1802 if (_visualItems) {
1803 _visualItems->setDirty(dirty);
1804 }
1805}
1806
1807void MissionController::_scanForAdditionalSettings(QmlObjectListModel* visualItems, PlanMasterController* masterController)
1808{
1809 // First we look for Landing Patterns which are at the end
1812 }
1813
1814 int scanIndex = 0;
1815 while (scanIndex < visualItems->count()) {
1816 VisualMissionItem* visualItem = visualItems->value<VisualMissionItem*>(scanIndex);
1817
1818 qCDebug(MissionControllerLog) << "MissionController::_scanForAdditionalSettings count:scanIndex" << visualItems->count() << scanIndex;
1819
1820 if (!_flyView) {
1821 MissionSettingsItem* settingsItem = qobject_cast<MissionSettingsItem*>(visualItem);
1822 if (settingsItem) {
1823 scanIndex++;
1824 settingsItem->scanForMissionSettings(visualItems, scanIndex);
1825 continue;
1826 }
1827 }
1828
1829 SimpleMissionItem* simpleItem = qobject_cast<SimpleMissionItem*>(visualItem);
1830 if (simpleItem) {
1831 scanIndex++;
1832 simpleItem->scanForSections(visualItems, scanIndex, masterController);
1833 } else {
1834 // Complex item, can't have sections
1835 scanIndex++;
1836 }
1837 }
1838}
1839
1841{
1842 return _visualItems ? _visualItems->count() > 1 : false;
1843}
1844
1846{
1847 if (_masterController->offline()) {
1848 qCCritical(MissionControllerLog) << "MissionControllerLog::removeAllFromVehicle called while offline";
1849 } else if (syncInProgress()) {
1850 qCCritical(MissionControllerLog) << "MissionControllerLog::removeAllFromVehicle called while syncInProgress";
1851 } else {
1852 _itemsRequested = true;
1853 _missionManager->removeAll();
1854 }
1855}
1856
1858{
1859 return QGCCorePlugin::instance()->complexMissionItemNames(_controllerVehicle);
1860}
1861
1863{
1864 if (!_controllerVehicle->firmwarePlugin()->sendHomePositionToVehicle()) {
1865 resumeIndex--;
1866 }
1867 _missionManager->generateResumeMission(resumeIndex);
1868}
1869
1871{
1872 if (_settingsItem) {
1873 return _settingsItem->coordinate();
1874 } else {
1875 return QGeoCoordinate();
1876 }
1877}
1878
1880{
1881 return _settingsItem && _settingsItem->coordinate().isValid();
1882}
1883
1884void MissionController::setHomePosition(QGeoCoordinate coordinate)
1885{
1886 if (_settingsItem) {
1887 _settingsItem->setCoordinate(coordinate);
1888 }
1889}
1890
1892{
1893 double defaultAltitude = _appSettings->defaultMissionItemAltitude()->rawValue().toDouble();
1894
1895 for (int i=1; i<_visualItems->count(); i++) {
1896 VisualMissionItem* item = _visualItems->value<VisualMissionItem*>(i);
1897 item->applyNewAltitude(defaultAltitude);
1898 }
1899}
1900
1901void MissionController::_progressPctChanged(double progressPct)
1902{
1903 if (!QGC::fuzzyCompare(progressPct, _progressPct)) {
1904 _progressPct = progressPct;
1906 }
1907}
1908
1909void MissionController::_visualItemsDirtyChanged(bool dirty)
1910{
1911 // We could connect signal to signal and not need this but this is handy for setting a breakpoint on
1912 emit dirtyChanged(dirty);
1913}
1914
1916{
1917 qCDebug(MissionControllerLog) << "showPlanFromManagerVehicle _flyView" << _flyView;
1918 if (_masterController->offline()) {
1919 qCCritical(MissionControllerLog) << "MissionController::showPlanFromManagerVehicle called while offline";
1920 return true; // stops further propagation of showPlanFromManagerVehicle due to error
1921 } else {
1922 if (!_managerVehicle->initialPlanRequestComplete()) {
1923 // The vehicle hasn't completed initial load, we can just wait for newMissionItemsAvailable to be signalled automatically
1924 qCDebug(MissionControllerLog) << "showPlanFromManagerVehicle: !initialPlanRequestComplete, wait for signal";
1925 return true;
1926 } else if (syncInProgress()) {
1927 // If the sync is already in progress, newMissionItemsAvailable will be signalled automatically when it is done. So no need to do anything.
1928 qCDebug(MissionControllerLog) << "showPlanFromManagerVehicle: syncInProgress wait for signal";
1929 return true;
1930 } else {
1931 // Sync has already completed, fake a _newMissionItemsAvailable with the current items
1932 qCDebug(MissionControllerLog) << "showPlanFromManagerVehicle: sync complete simulate signal";
1933 _itemsRequested = true;
1934 _newMissionItemsAvailableFromVehicle(false /* removeAllRequested */);
1935 return false;
1936 }
1937 }
1938}
1939
1940void MissionController::_managerSendComplete(bool error)
1941{
1942 // Fly view always reloads on send complete
1943 if (!error && _flyView) {
1945 }
1946}
1947
1948void MissionController::_managerRemoveAllComplete(bool error)
1949{
1950 if (!error) {
1951 // Remove all from vehicle so we always update
1953 }
1954}
1955
1956bool MissionController::_isROIBeginItem(SimpleMissionItem* simpleItem)
1957{
1958 return simpleItem->mavCommand() == MAV_CMD_DO_SET_ROI_LOCATION ||
1959 simpleItem->mavCommand() == MAV_CMD_DO_SET_ROI_WPNEXT_OFFSET ||
1960 (simpleItem->mavCommand() == MAV_CMD_DO_SET_ROI &&
1961 static_cast<int>(simpleItem->missionItem().param1()) == MAV_ROI_LOCATION);
1962}
1963
1964bool MissionController::_isROICancelItem(SimpleMissionItem* simpleItem)
1965{
1966 return simpleItem->mavCommand() == MAV_CMD_DO_SET_ROI_NONE ||
1967 (simpleItem->mavCommand() == MAV_CMD_DO_SET_ROI &&
1968 static_cast<int>(simpleItem->missionItem().param1()) == MAV_ROI_NONE);
1969}
1970
1971void MissionController::setCurrentPlanViewSeqNum(int sequenceNumber, bool force)
1972{
1973 if (_visualItems && (force || sequenceNumber != _currentPlanViewSeqNum)) {
1974 qCDebug(MissionControllerLog) << "setCurrentPlanViewSeqNum";
1975 bool foundLand = false;
1976 bool onlyInsertTakeoffValid = false;
1977 int takeoffSeqNum = -1;
1978 int landSeqNum = -1;
1979 int lastFlyThroughSeqNum = -1;
1980
1981 _splitSegment = nullptr;
1982 _currentPlanViewItem = nullptr;
1983 _currentPlanViewSeqNum = -1;
1984 _currentPlanViewVIIndex = -1;
1985 _isInsertTakeoffValid = true;
1986 _isInsertLandValid = true;
1987 _isInsertROIValid = false;
1988 _isROIActive = false;
1989 _isROIBeginCurrentItem = false;
1990 _flyThroughCommandsAllowed = true;
1991 _previousCoordinate = QGeoCoordinate();
1992
1993 bool noItemsAddedYet = _visualItems->count() == 1;
1994 if (_masterController->controllerVehicle()->supports()->takeoffMissionCommand() && !_planViewSettings->takeoffItemNotRequired()->rawValue().toBool() && noItemsAddedYet) {
1995 onlyInsertTakeoffValid = true;
1996 }
1997
1998 for (int viIndex=0; viIndex<_visualItems->count(); viIndex++) {
1999 VisualMissionItem* pVI = qobject_cast<VisualMissionItem*>(_visualItems->get(viIndex));
2000 SimpleMissionItem* simpleItem = qobject_cast<SimpleMissionItem*>(pVI);
2001 int currentSeqNumber = pVI->sequenceNumber();
2002
2003 if (sequenceNumber != 0 && currentSeqNumber <= sequenceNumber) {
2004 if (pVI->specifiesCoordinate() && !pVI->isStandaloneCoordinate()) {
2005 // Coordinate based flight commands prior to where the takeoff would be inserted
2006 _isInsertTakeoffValid = false;
2007 }
2008 }
2009
2010 if (qobject_cast<TakeoffMissionItem*>(pVI)) {
2011 takeoffSeqNum = currentSeqNumber;
2012 _isInsertTakeoffValid = false;
2013 }
2014
2015 if (!foundLand) {
2016 if (simpleItem) {
2017 switch (simpleItem->mavCommand()) {
2018 case MAV_CMD_NAV_LAND:
2019 case MAV_CMD_NAV_VTOL_LAND:
2020 case MAV_CMD_DO_LAND_START:
2021 case MAV_CMD_NAV_RETURN_TO_LAUNCH:
2022 foundLand = true;
2023 landSeqNum = currentSeqNumber;
2024 break;
2025 default:
2026 break;
2027 }
2028 } else {
2029 FixedWingLandingComplexItem* fwLanding = qobject_cast<FixedWingLandingComplexItem*>(pVI);
2030 if (fwLanding) {
2031 foundLand = true;
2032 landSeqNum = currentSeqNumber;
2033 }
2034 }
2035 }
2036
2037 if (simpleItem) {
2038 // Remember previous coordinate
2039 if (currentSeqNumber < sequenceNumber && simpleItem->specifiesCoordinate() && !simpleItem->isStandaloneCoordinate()) {
2040 _previousCoordinate = simpleItem->coordinate();
2041 }
2042
2043 // ROI state handling
2044 if (currentSeqNumber <= sequenceNumber) {
2045 if (_isROIActive) {
2046 if (_isROICancelItem(simpleItem)) {
2047 _isROIActive = false;
2048 }
2049 } else {
2050 if (_isROIBeginItem(simpleItem)) {
2051 _isROIActive = true;
2052 }
2053 }
2054 }
2055 if (currentSeqNumber == sequenceNumber && _isROIBeginItem(simpleItem)) {
2056 _isROIBeginCurrentItem = true;
2057 }
2058 }
2059
2060 if (viIndex != 0) {
2061 // Complex items are assumed to be fly through
2062 if (!simpleItem || (simpleItem->specifiesCoordinate() && !simpleItem->isStandaloneCoordinate())) {
2063 lastFlyThroughSeqNum = currentSeqNumber;
2064 }
2065 }
2066
2067 if (currentSeqNumber == sequenceNumber) {
2068 pVI->setIsCurrentItem(true);
2069 pVI->setHasCurrentChildItem(false);
2070
2071 _currentPlanViewItem = pVI;
2072 _currentPlanViewSeqNum = sequenceNumber;
2073 _currentPlanViewVIIndex = viIndex;
2074
2075 if (pVI->specifiesCoordinate()) {
2076 if (!pVI->isStandaloneCoordinate()) {
2077 // Determine split segment used to display line split editing ui.
2078 for (int j=viIndex-1; j>0; j--) {
2079 VisualMissionItem* pPrev = qobject_cast<VisualMissionItem*>(_visualItems->get(j));
2080 if (pPrev->specifiesCoordinate() && !pPrev->isStandaloneCoordinate()) {
2081 // Missing while a compressed segment recalc is pending; that recalc sets it instead
2082 _splitSegment = _flightPathSegmentHashTable.value(VisualItemPair(pPrev, pVI), nullptr);
2083 break;
2084 }
2085 }
2086 }
2087 } else if (pVI->parentItem()) {
2088 pVI->parentItem()->setHasCurrentChildItem(true);
2089 }
2090 } else {
2091 pVI->setIsCurrentItem(false);
2092 // Child items will always be later in the mission sequence, so this should be correct, to be sure the HasCurrentChildItem is cleared
2093 pVI->setHasCurrentChildItem(false);
2094 }
2095 }
2096
2097 if (takeoffSeqNum != -1) {
2098 // Takeoff item was found which means mission starts from ground
2099 if (sequenceNumber < takeoffSeqNum) {
2100 // Land is only valid after the takeoff item.
2101 _isInsertLandValid = false;
2102 // Fly through commands are not allowed prior to the takeoff command
2103 _flyThroughCommandsAllowed = false;
2104 }
2105 }
2106
2107 if (lastFlyThroughSeqNum != -1) {
2108 // Land item must be after any fly through coordinates
2109 if (sequenceNumber < lastFlyThroughSeqNum) {
2110 _isInsertLandValid = false;
2111 }
2112 }
2113
2114 if (foundLand && !multipleLandPatternsAllowed()) {
2115 _isInsertLandValid = false;
2116 if (sequenceNumber >= landSeqNum) {
2117 // Can't have fly through commands after a land item
2118 _flyThroughCommandsAllowed = false;
2119 }
2120 }
2121
2122 if (_hasLandItem != foundLand) {
2123 // Update hasLandItem if the presence of a landing item has changed
2124 _hasLandItem = foundLand;
2125 emit hasLandItemChanged();
2126 }
2127
2128 // These are not valid when only takeoff is allowed
2129 _isInsertLandValid = _isInsertLandValid && !onlyInsertTakeoffValid;
2130 _flyThroughCommandsAllowed = _flyThroughCommandsAllowed && !onlyInsertTakeoffValid;
2131
2132 // Nothing can be inserted until the home position has been set
2133 const bool homePosSet = homePositionSet();
2134 _isInsertTakeoffValid = _isInsertTakeoffValid && homePosSet;
2135 _isInsertLandValid = _isInsertLandValid && homePosSet;
2136 _flyThroughCommandsAllowed = _flyThroughCommandsAllowed && homePosSet;
2137 _isInsertROIValid = homePosSet && !onlyInsertTakeoffValid;
2138
2139 // These properties are all recomputed together above, so a single signal is sufficient.
2140 // QML property bindings list planViewStateChanged as their NOTIFY signal which means
2141 // one emit re-evaluates all dependent bindings in one pass instead of many separate updates.
2142 // splitSegmentChanged is kept separate because PlanView.qml has an explicit onSplitSegmentChanged handler.
2143 emit planViewStateChanged();
2144 emit splitSegmentChanged();
2145 }
2146}
2147
2148void MissionController::repositionMission(const QGeoCoordinate& newHome,
2149 bool repositionTakeoffItems,
2150 bool repositionLandingItems)
2151{
2152 if (!newHome.isValid()) {
2153 qCWarning(MissionControllerLog) << "Cannot reposition mission to an invalid coordinate";
2154 return;
2155 }
2156
2157 if (!_settingsItem || !_settingsItem->coordinate().isValid()) {
2158 qCWarning(MissionControllerLog) << "Cannot reposition mission while home is invalid";
2159 return;
2160 }
2161
2162 const QGeoCoordinate oldHome = _settingsItem->coordinate();
2163
2164 for (int i = 0; i < _visualItems->count(); ++i) {
2165 auto* item = qobject_cast<VisualMissionItem*>(_visualItems->get(i));
2166 if (!item || !item->specifiesCoordinate() || item->isStandaloneCoordinate()) {
2167 continue;
2168 }
2169
2170 if ((!repositionTakeoffItems && item->isTakeoffItem()) ||
2171 (!repositionLandingItems && item->isLandCommand())) {
2172 continue;
2173 }
2174
2175 const QGeoCoordinate oldCoord = item->coordinate();
2176 if (!oldCoord.isValid()) {
2177 continue;
2178 }
2179
2180 const double distanceMeters = oldHome.distanceTo(oldCoord);
2181 const double azimuthDegrees = oldHome.azimuthTo(oldCoord);
2182
2183 QGeoCoordinate newCoord = newHome.atDistanceAndAzimuth(distanceMeters, azimuthDegrees);
2184 item->setCoordinate(newCoord);
2185 }
2186
2187 setDirty(true);
2188}
2189
2191 double northMeters,
2192 double upMeters,
2193 bool offsetTakeoffItems,
2194 bool offsetLandingItems)
2195{
2196 if (!qFuzzyIsNull(eastMeters) || !qFuzzyIsNull(northMeters)) {
2197 const double distanceMeters = qSqrt(eastMeters * eastMeters + northMeters * northMeters);
2198 double azimuthDegrees = 0.0;
2199 if (!qFuzzyIsNull(distanceMeters)) {
2200 const double azimuthRadians = qAtan2(eastMeters, northMeters);
2201 azimuthDegrees = qRadiansToDegrees(azimuthRadians);
2202 }
2203
2204 for (int i = 0; i < _visualItems->count(); ++i) {
2205 auto* item = qobject_cast<VisualMissionItem*>(_visualItems->get(i));
2206 if (!item || !item->specifiesCoordinate() || item->isStandaloneCoordinate()) {
2207 continue;
2208 }
2209
2210 if ((!offsetTakeoffItems && item->isTakeoffItem()) ||
2211 (!offsetLandingItems && item->isLandCommand())) {
2212 continue;
2213 }
2214
2215 const QGeoCoordinate oldCoord = item->coordinate();
2216 if (!oldCoord.isValid()) {
2217 continue;
2218 }
2219
2220 item->setCoordinate(oldCoord.atDistanceAndAzimuth(distanceMeters, azimuthDegrees));
2221 }
2222
2223 setDirty(true);
2224 }
2225
2226 if (!qFuzzyIsNull(upMeters)) {
2227 for (int i = 0; i < _visualItems->count(); ++i) {
2228 auto* item = qobject_cast<VisualMissionItem*>(_visualItems->get(i));
2229 if (!item || item == _settingsItem) {
2230 continue;
2231 }
2232
2233 if ((!offsetTakeoffItems && item->isTakeoffItem()) ||
2234 (!offsetLandingItems && item->isLandCommand())) {
2235 continue;
2236 }
2237
2238 if (!item->specifiesCoordinate() && !item->specifiesAltitudeOnly()) {
2239 continue;
2240 }
2241
2242 if (!qIsNaN(item->editableAlt())) {
2243 item->applyNewAltitude(item->editableAlt() + upMeters);
2244 }
2245 }
2246
2247 setDirty(true);
2248 }
2249}
2250
2252 bool rotateTakeoffItems,
2253 bool rotateLandingItems)
2254{
2255 if (qFuzzyIsNull(degreesCW)) {
2256 return;
2257 }
2258
2259 if (!_settingsItem || !_settingsItem->coordinate().isValid()) {
2260 qCWarning(MissionControllerLog) << "Cannot rotate mission while home is invalid";
2261 return;
2262 }
2263
2264 const QGeoCoordinate home = _settingsItem->coordinate();
2265
2266 for (int i = 0; i < _visualItems->count(); ++i) {
2267 auto* item = qobject_cast<VisualMissionItem*>(_visualItems->get(i));
2268 if (!item || !item->specifiesCoordinate() || item->isStandaloneCoordinate()) {
2269 continue;
2270 }
2271
2272 if ((!rotateTakeoffItems && item->isTakeoffItem()) ||
2273 (!rotateLandingItems && item->isLandCommand())) {
2274 continue;
2275 }
2276
2277 const QGeoCoordinate oldCoord = item->coordinate();
2278 if (!oldCoord.isValid()) {
2279 continue;
2280 }
2281
2282 const double distanceMeters = home.distanceTo(oldCoord);
2283 if (qFuzzyIsNull(distanceMeters)) {
2284 continue;
2285 }
2286 const double azimuthDegrees = home.azimuthTo(oldCoord);
2287
2288 QGeoCoordinate newCoord = home.atDistanceAndAzimuth(distanceMeters, azimuthDegrees + degreesCW);
2289 item->setCoordinate(newCoord);
2290 }
2291
2292 setDirty(true);
2293}
2294
2295void MissionController::_updateTimeout()
2296{
2297 QGeoCoordinate firstCoordinate;
2298 QGeoCoordinate takeoffCoordinate;
2299 QGCGeoBoundingCube boundingCube;
2300 double north = 0.0;
2301 double south = 180.0;
2302 double east = 0.0;
2303 double west = 360.0;
2304 double minAlt = QGCGeoBoundingCube::MaxAlt;
2305 double maxAlt = QGCGeoBoundingCube::MinAlt;
2306 for (int i = 1; i < _visualItems->count(); i++) {
2307 VisualMissionItem* item = qobject_cast<VisualMissionItem*>(_visualItems->get(i));
2308 if(item->isSimpleItem()) {
2309 SimpleMissionItem* pSimpleItem = qobject_cast<SimpleMissionItem*>(item);
2310 if(pSimpleItem) {
2311 switch(pSimpleItem->command()) {
2312 case MAV_CMD_NAV_TAKEOFF:
2313 case MAV_CMD_NAV_WAYPOINT:
2314 case MAV_CMD_NAV_LAND:
2315 if(pSimpleItem->coordinate().isValid()) {
2316 double alt = 0.0;
2317 if (!pSimpleItem->altitude()->rawValue().isNull() && !qIsNaN(pSimpleItem->altitude()->rawValue().toDouble())) {
2318 alt = pSimpleItem->altitude()->rawValue().toDouble();
2319 }
2320 if((MAV_CMD)pSimpleItem->command() == MAV_CMD_NAV_TAKEOFF) {
2321 takeoffCoordinate = pSimpleItem->coordinate();
2322 takeoffCoordinate.setAltitude(alt);
2323 minAlt = maxAlt = alt;
2324 } else if(!firstCoordinate.isValid()) {
2325 firstCoordinate = pSimpleItem->coordinate();
2326 }
2327 double lat = pSimpleItem->coordinate().latitude() + 90.0;
2328 double lon = pSimpleItem->coordinate().longitude() + 180.0;
2329 north = fmax(north, lat);
2330 south = fmin(south, lat);
2331 east = fmax(east, lon);
2332 west = fmin(west, lon);
2333 minAlt = fmin(minAlt, alt);
2334 maxAlt = fmax(maxAlt, alt);
2335 }
2336 break;
2337 default:
2338 break;
2339 }
2340 }
2341 } else {
2342 ComplexMissionItem* pComplexItem = qobject_cast<ComplexMissionItem*>(item);
2343 if(pComplexItem) {
2344 QGCGeoBoundingCube bc = pComplexItem->boundingCube();
2345 if(bc.isValid()) {
2346 if(!firstCoordinate.isValid() && pComplexItem->entryCoordinate().isValid()) {
2347 firstCoordinate = pComplexItem->entryCoordinate();
2348 }
2349 north = fmax(north, bc.pointNW.latitude() + 90.0);
2350 south = fmin(south, bc.pointSE.latitude() + 90.0);
2351 east = fmax(east, bc.pointNW.longitude() + 180.0);
2352 west = fmin(west, bc.pointSE.longitude() + 180.0);
2353 minAlt = fmin(minAlt, bc.pointNW.altitude());
2354 maxAlt = fmax(maxAlt, bc.pointSE.altitude());
2355 }
2356 }
2357 }
2358 }
2359 //-- Figure out where this thing is taking off from
2360 if(!takeoffCoordinate.isValid()) {
2361 if(firstCoordinate.isValid()) {
2362 takeoffCoordinate = firstCoordinate;
2363 } else {
2365 }
2366 }
2367 //-- Build bounding "cube"
2368 boundingCube = QGCGeoBoundingCube(
2369 QGeoCoordinate(north - 90.0, west - 180.0, minAlt),
2370 QGeoCoordinate(south - 90.0, east - 180.0, maxAlt));
2371 if(_travelBoundingCube != boundingCube || _takeoffCoordinate != takeoffCoordinate) {
2372 _takeoffCoordinate = takeoffCoordinate;
2373 _travelBoundingCube = boundingCube;
2375 qCDebug(MissionControllerLog) << "Bounding cube:" << _travelBoundingCube.pointNW << _travelBoundingCube.pointSE;
2376 }
2377}
2378
2379void MissionController::_complexBoundingBoxChanged()
2380{
2381 _updateTimer.start(UPDATE_TIMEOUT);
2382}
2383
2385 const int count = _visualItems->count();
2386
2387 for (int i = 0; i < count; i++) {
2388 QObject *obj = _visualItems->get(i);
2389
2390 LandingComplexItem *landingItem = qobject_cast<LandingComplexItem *>(obj);
2391 if (landingItem) {
2392 return landingItem == item;
2393 }
2394 }
2395
2396 return false;
2397}
2398
2400{
2401 return _visualItems->count() <= 1;
2402}
2403
2404void MissionController::_recalcPlanViewState(void)
2405{
2406 setCurrentPlanViewSeqNum(_currentPlanViewSeqNum, true /* force */);
2407}
2408
2409void MissionController::_allItemsRemoved(void)
2410{
2411 // When there are no mission items we track changes to firmware/vehicle type. This allows a vehicle connection
2412 // to adjust these items.
2413 _controllerVehicle->trackFirmwareVehicleTypeChanges();
2414}
2415
2416void MissionController::_firstItemAdded(void)
2417{
2418 // As soon as the first item is added we lock the firmware/vehicle type to current values. So if you then connect a vehicle
2419 // it will not affect these values.
2420 _controllerVehicle->stopTrackingFirmwareVehicleTypeChanges();
2421}
2422
2424{
2425 if (_managerVehicle->isOfflineEditingVehicle()) {
2427 }
2428 if (_managerVehicle->armed() && _managerVehicle->flightMode() == _managerVehicle->missionFlightMode()) {
2430 }
2431 if (_controllerVehicle->firmwareType() != _managerVehicle->firmwareType() || QGCMAVLink::vehicleClass(_controllerVehicle->vehicleType()) != QGCMAVLink::vehicleClass(_managerVehicle->vehicleType())) {
2433 }
2435}
2436
2441
2450
2452{
2453 if (_globalAltFrame != altFrame) {
2454 _globalAltFrame = altFrame;
2456 }
2457}
#define UPDATE_TIMEOUT
How often we check for bounding box changes.
static FlightPathSegment::SegmentType segmentTypeForPair(const VisualItemPair &pair, bool mavlinkTerrainFrame)
QPair< VisualMissionItem *, VisualMissionItem * > VisualItemPair
QHash< VisualItemPair, FlightPathSegment * > FlightPathSegmentHashTable
#define qgcApp()
QString errorString
Error error
#define QGC_LOGGING_CATEGORY(name, categoryStr)
Application Settings.
Definition AppSettings.h:10
void setSpecifyCameraMode(bool specifyCameraMode)
void setSpecifyGimbal(bool specifyGimbal)
bool specifyCameraMode(void) const
Fact * gimbalPitch(void)
Fact * cameraMode(void)
bool cameraModeSupported(void) const
Fact * gimbalYaw(void)
bool specifyGimbal(void) const
void isIncompleteChanged(void)
void boundingCubeChanged(void)
static constexpr const char * jsonComplexItemTypeKey
This mission item attribute specifies the type of the complex item.
void greatestDistanceToChanged(void)
bool isIncomplete(void) const
virtual void applyPreviousAltitudeFrame(QGroundControlQmlGlobal::AltitudeFrame prevAltFrame, double prevAltitude)
void maxAMSLAltitudeChanged(void)
virtual bool load(const QJsonObject &complexObject, int sequenceNumber, QString &errorString)=0
void minAMSLAltitudeChanged(void)
void complexDistanceChanged(void)
void rawValueChanged(const QVariant &value)
void setRawValue(const QVariant &value)
Definition Fact.cc:135
QVariant rawValue() const
Definition Fact.h:87
void valueChanged(const QVariant &value)
This signal is only meant for use by the QT property system. It should not be connected to by client ...
virtual QList< MAV_CMD > supportedMissionCommands(QGCMAVLinkTypes::VehicleClass_t) const
List of supported mission commands. Empty list for all commands supported.
virtual bool sendHomePositionToVehicle() const
virtual bool hasGimbal(Vehicle *vehicle, bool &rollSupported, bool &pitchSupported, bool &yawSupported) const
static bool scanForItems(QmlObjectListModel *visualItems, bool flyView, PlanMasterController *masterController)
Scans the loaded items for a landing pattern complex item.
static constexpr const char * canonicalName
void terrainCollisionChanged(bool terrainCollision)
void totalDistanceChanged(double totalDistance)
void setCoordinate2(const QGeoCoordinate &coordinate)
void coord2AMSLAltChanged(void)
void coord1AMSLAltChanged(void)
void setSpecialVisual(bool specialVisual)
void amslTerrainHeightsChanged(void)
void setCoord2AMSLAlt(double alt)
void setCoord1AMSLAlt(double alt)
void setCoordinate1(const QGeoCoordinate &coordinate)
Used to convert a Plan to a KML document.
void addMission(Vehicle *vehicle, QmlObjectListModel *visualItems, QList< MissionItem * > rgMissionItems)
static MissionCommandTree * instance()
void minAMSLAltitudeChanged(double minAMSLAltitude)
Q_INVOKABLE void resumeMission(int resumeIndex)
double progressPct(void) const
Q_INVOKABLE int visualItemIndexForObject(QObject *object) const
Returns the visual item index for the given VisualMissionItem object, or -1 if not found.
void complexMissionItemsChanged(void)
Q_INVOKABLE void setHomePosition(QGeoCoordinate coordinate)
Set the planned home position from a map click.
bool showPlanFromManagerVehicle(void) final
Q_INVOKABLE SendToVehiclePreCheckState sendToVehiclePreCheck(void)
bool dirty(void) const final
Q_INVOKABLE VisualMissionItem * insertComplexMissionItem(QString itemName, QGeoCoordinate mapCenterCoordinate, int visualItemIndex, bool makeCurrentItem=false)
void batteriesRequiredChanged(int batteriesRequired)
bool loadTextFile(QFile &file, QString &errorString)
void multipleLandPatternsAllowedChanged(void)
bool multipleLandPatternsAllowed(void) const
Q_INVOKABLE VisualMissionItem * insertTakeoffItem(QGeoCoordinate coordinate, int visualItemIndex, bool makeCurrentItem=false)
QGeoCoordinate takeoffCoordinate()
Q_INVOKABLE VisualMissionItem * insertLandItem(QGeoCoordinate coordinate, int visualItemIndex, bool makeCurrentItem=false)
Q_INVOKABLE void offsetMission(double eastMeters, double northMeters, double upMeters=0.0, bool offsetTakeoffItems=false, bool offsetLandingItems=false)
Q_INVOKABLE void setCurrentPlanViewSeqNum(int sequenceNumber, bool force)
void maxAMSLAltitudeChanged(double maxAMSLAltitude)
void removeAllFromVehicle(void) final
void homePositionSetChanged(void)
void recalcTerrainProfile(void)
void missionPlannedDistanceChanged(double missionPlannedDistance)
void missionTotalDistanceChanged(double missionTotalDistance)
void hasLandItemChanged(void)
void _recalcFlightPathSegmentsSignal(void)
int currentMissionIndex(void) const
void missionMaxTelemetryChanged(double missionMaxTelemetry)
Q_INVOKABLE VisualMissionItem * insertVTOLMulticopterTakeoffItem(QGeoCoordinate coordinate, int visualItemIndex, bool makeCurrentItem=false)
static void sendItemsToVehicle(Vehicle *vehicle, QmlObjectListModel *visualMissionItems)
Sends the mission items to the specified vehicle.
bool load(const QJsonObject &json, QString &errorString) final
void splitSegmentChanged(void)
Q_INVOKABLE void repositionMission(const QGeoCoordinate &newHome, bool repositionTakeoffItems=true, bool repositionLandingItems=true)
void resumeMissionReady(void)
@ SendToVehiclePreCheckStateFirwmareVehicleMismatch
void batteryChangePointChanged(int batteryChangePoint)
Q_INVOKABLE void rotateMission(double degreesCW, bool rotateTakeoffItems=false, bool rotateLandingItems=false)
void addMissionToKML(KMLPlanDomDocument &planKML)
void _recalcMissionFlightStatusSignal(void)
void missionHoverDistanceChanged(double missionHoverDistance)
bool syncInProgress(void) const final
void sendToVehicle(void) final
void resumeMissionIndexChanged(void)
void currentMissionIndexChanged(int currentMissionIndex)
Q_INVOKABLE VisualMissionItem * insertROIMissionItem(QGeoCoordinate coordinate, int visualItemIndex, bool makeCurrentItem=false)
void plannedHomePositionChanged(QGeoCoordinate plannedHomePosition)
Q_INVOKABLE void removeVisualItem(int viIndex)
bool isEmpty(void) const
void newItemsFromVehicle(void)
void setGlobalAltitudeFrame(QGroundControlQmlGlobal::AltitudeFrame altFrame)
void save(QJsonObject &json) final
Q_INVOKABLE VisualMissionItem * insertSimpleMissionItem(QGeoCoordinate coordinate, int visualItemIndex, bool makeCurrentItem=false)
void missionHoverTimeChanged(void)
bool isFirstLandingComplexItem(const LandingComplexItem *item) const
QGeoCoordinate plannedHomePosition(void) const
bool homePositionSet(void) const
bool containsItems(void) const final
void progressPctChanged(double progressPct)
void missionCruiseTimeChanged(void)
QGroundControlQmlGlobal::AltitudeFrame globalAltitudeFrame(void)
int readyForSaveState(void) const
QGroundControlQmlGlobal::AltitudeFrame globalAltitudeFrameDefault(void)
void removeAll(void) final
Removes all from controller only.
void visualItemsReset(void)
void missionBoundingCubeChanged(void)
QVariantList complexMissionItems(void) const
void missionTimeChanged(void)
Q_INVOKABLE void applyDefaultMissionAltitude(void)
Updates the altitudes of the items in the current mission to the new default altitude.
void setDirty(bool dirty) final
void start(bool flyView) final
Should be called immediately upon Component.onCompleted.
void planViewStateChanged(void)
All plan-view properties are recomputed together in setCurrentPlanViewSeqNum, so one signal covers th...
void globalAltitudeFrameChanged(void)
void missionCruiseDistanceChanged(double missionCruiseDistance)
void resumeMissionUploadFail(void)
void loadFromVehicle(void) final
Q_INVOKABLE VisualMissionItem * insertCancelROIMissionItem(int visualItemIndex, bool makeCurrentItem=false)
int resumeMissionIndex(void) const
Q_INVOKABLE VisualMissionItem * insertComplexMissionItemFromKMLOrSHP(QString itemName, QString file, int visualItemIndex, bool makeCurrentItem=false)
QmlObjectListModel * visualItems(void)
const MissionFlightStatus_t & status() const
void recalc(QmlObjectListModel *visualItems, MissionSettingsItem *settingsItem, Vehicle *controllerVehicle, Vehicle *managerVehicle, AppSettings *appSettings, PlanViewSettings *planViewSettings, bool missionStartsInVTOLMulticopterMode)
void reset(Vehicle *controllerVehicle, Vehicle *managerVehicle, bool missionStartsInVTOLMulticopterMode)
Resets the flight status fields to defaults based on vehicle properties.
void setParam1(double param1)
MAV_FRAME frame(void) const
Definition MissionItem.h:52
void save(QJsonObject &json) const
double param1(void) const
Definition MissionItem.h:54
QGeoCoordinate coordinate(void) const
int doJumpId(void) const
Definition MissionItem.h:62
int lastCurrentIndex(void) const
Last current mission item reported while in Mission flight mode.
void generateResumeMission(int resumeIndex)
int currentIndex(void) const
Current mission item as reported by MISSION_CURRENT.
bool scanForMissionSettings(QmlObjectListModel *visualItems, int scanIndex)
Scans the loaded items for settings items.
QGeoCoordinate coordinate(void) const final
void setCoordinate(const QGeoCoordinate &coordinate) final
bool addMissionEndAction(QList< MissionItem * > &items, int seqNum, QObject *missionItemParent)
CameraSection * cameraSection(void)
void endResetModel()
Depth-counted endResetModel — only the outermost call has effect.
void beginResetModel()
Depth-counted beginResetModel — only the outermost call has effect.
void dirtyChanged(bool dirty)
void countChanged(int count)
This is the abstract base clas for Plan Element controllers.
void syncInProgressChanged(bool syncInProgress)
PlanMasterController * masterController(void)
PlanMasterController * _masterController
virtual void start(bool flyView)
Should be called immediately upon Component.onCompleted.
void dirtyChanged(bool dirty)
void resumeMissionUploadFail(void)
void currentIndexChanged(int currentIndex)
void lastCurrentIndexChanged(int lastCurrentIndex)
void sendComplete(bool error)
void resumeMissionReady(void)
void removeAll(void)
void newMissionItemsAvailable(bool removeAllRequested)
void loadFromVehicle(void)
void removeAllComplete(bool error)
void writeMissionItems(const QList< MissionItem * > &missionItems)
bool inProgress(void) const
void inProgressChanged(bool inProgress)
void progressPctChanged(double progressPercentPct)
const QList< MissionItem * > & missionItems(void)
Definition PlanManager.h:22
Master controller for mission, fence, rally.
Vehicle * controllerVehicle(void)
void managerVehicleChanged(Vehicle *managerVehicle)
RallyPointController * rallyPointController(void)
virtual ComplexMissionItem * createComplexMissionItem(const QString &complexItemType, PlanMasterController *masterController, bool flyView, const QString &kmlOrShpFile=QString())
virtual QVariantList complexMissionItemNames(Vehicle *vehicle)
static QGCCorePlugin * instance()
Q_INVOKABLE bool isValid() const
void append(QObject *object)
Caller maintains responsibility for object ownership and deletion.
T value(int index) const
void setDirty(bool dirty) override final
Q_INVOKABLE QObject * get(int index)
QObject * removeAt(int index)
int count() const override final
void clear() override final
void clearAndDeleteContents() override final
Clears the list and calls deleteLater on each entry.
void insert(int index, QObject *object)
int indexOf(const QObject *object)
int rowCount(const QModelIndex &parent=QModelIndex()) const override
static constexpr int NodeTypeRole
QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const override
void removeChildren(const QModelIndex &parentIndex)
Removes all children of parentIndex without removing the parent itself.
Q_INVOKABLE QObject * removeItem(const QModelIndex &index)
QObject * removeAt(const QModelIndex &parentIndex, int row)
QModelIndex index(int row, int column, const QModelIndex &parent=QModelIndex()) const override
Q_INVOKABLE QModelIndex indexForObject(QObject *object) const
Searches the entire tree for object and returns its QModelIndex (invalid if not found)
Q_INVOKABLE QModelIndex insertItem(int row, QObject *object, const QModelIndex &parentIndex=QModelIndex())
Inserts object at row under parentIndex. Returns the new item's index.
Q_INVOKABLE QModelIndex appendItem(QObject *object, const QModelIndex &parentIndex=QModelIndex())
Appends object as the last child of parentIndex (root if invalid). Returns the new item's index.
QmlObjectListModel * points(void)
Provides access to all app settings.
static SettingsManager * instance()
AppSettings * appSettings() const
A SimpleMissionItem is used to represent a single MissionItem to the ui.
bool specifiesCoordinate(void) const final
void setSequenceNumber(int sequenceNumber) final
bool isStandaloneCoordinate(void) const final
int sequenceNumber(void) const final
bool scanForSections(QmlObjectListModel *visualItems, int scanIndex, PlanMasterController *masterController)
void altitudeFrameChanged(void)
int lastSequenceNumber(void) const final
MAV_CMD mavCommand(void) const
bool specifiesAltitude(void) const
QGeoCoordinate coordinate(void) const final
virtual bool load(QTextStream &loadStream)
void setCoordinate(const QGeoCoordinate &coordinate) override
void setCommand(int command)
int command(void) const
void setAltitudeFrame(QGroundControlQmlGlobal::AltitudeFrame altitudeFrame)
MissionItem & missionItem(void)
QGroundControlQmlGlobal::AltitudeFrame altitudeFrame(void) const
Takeoff mission item is a special case of a SimpleMissionItem which supports Launch Location display/...
static bool isTakeoffCommand(MAV_CMD command)
static bool isVTOLMulticopterTakeoff(Vehicle *vehicle, MAV_CMD command)
bool load(QTextStream &loadStream) final
static constexpr const char * canonicalName
static bool scanForItems(QmlObjectListModel *visualItems, bool flyView, PlanMasterController *masterController)
Scans the loaded items for a landing pattern complex item.
bool takeoffMissionCommand() const
bool px4Firmware() const
Definition Vehicle.h:512
void defaultHoverSpeedChanged(double hoverSpeed)
void defaultCruiseSpeedChanged(double cruiseSpeed)
QString missionFlightMode() const
Definition Vehicle.cc:2600
void _offlineVehicleTypeSettingChanged(QVariant varVehicleType)
Definition Vehicle.cc:498
QString flightMode() const
Definition Vehicle.cc:1485
bool vtol() const
Definition Vehicle.cc:1780
bool rover() const
Definition Vehicle.cc:1760
MAV_TYPE vehicleType() const
Definition Vehicle.h:446
FirmwarePlugin * firmwarePlugin()
Provides access to the Firmware Plugin for this Vehicle.
Definition Vehicle.h:462
bool isOfflineEditingVehicle() const
Definition Vehicle.h:532
MAV_AUTOPILOT firmwareType() const
Definition Vehicle.h:445
void _offlineFirmwareTypeSettingChanged(QVariant varFirmwareType)
Definition Vehicle.cc:485
void trackFirmwareVehicleTypeChanges(void)
Definition Vehicle.cc:213
double defaultHoverSpeed() const
Definition Vehicle.h:547
void stopTrackingFirmwareVehicleTypeChanges(void)
Definition Vehicle.cc:222
bool initialPlanRequestComplete() const
Definition Vehicle.h:733
bool fixedWing() const
Definition Vehicle.cc:1755
double defaultCruiseSpeed() const
Definition Vehicle.h:546
bool armed() const
Definition Vehicle.h:467
VehicleSupports * supports()
Definition Vehicle.h:420
MissionManager * missionManager()
Definition Vehicle.h:592
void vehicleTypeChanged()
virtual int lastSequenceNumber(void) const =0
void setIsCurrentItem(bool isCurrentItem)
void setHasCurrentChildItem(bool hasCurrentChildItem)
void specifiedVehicleYawChanged(void)
virtual void setCoordinate(const QGeoCoordinate &coordinate)=0
virtual int sequenceNumber(void) const =0
virtual void applyNewAltitude(double newAltitude)=0
Adjust the altitude of the item if appropriate to the new altitude.
static constexpr const char * jsonTypeComplexItemValue
Item type is Complex Item.
virtual bool isTakeoffItem(void) const
virtual bool isStandaloneCoordinate(void) const =0
void amslExitAltChanged(double alt)
void setParentItem(VisualMissionItem *parentItem)
void terrainAltitudeChanged(double terrainAltitude)
void setWizardMode(bool wizardMode)
bool isCurrentItem(void) const
void lastSequenceNumberChanged(int sequenceNumber)
void entryCoordinateChanged(const QGeoCoordinate &entryCoordinate)
void specifiesCoordinateChanged(void)
void coordinateChanged(const QGeoCoordinate &coordinate)
void setSimpleFlighPathSegment(FlightPathSegment *segment)
void specifiedFlightSpeedChanged(void)
void clearSimpleFlighPathSegment(void)
void currentVTOLModeChanged(void)
Signals that this item has changed the VTOL mode (MAV_CMD_DO_VTOL_TRANSITION)
virtual void setSequenceNumber(int sequenceNumber)=0
virtual ReadyForSaveState readyForSaveState(void) const
void specifiedGimbalYawChanged(void)
void amslEntryAltChanged(double alt)
void specifiedGimbalPitchChanged(void)
static constexpr const char * jsonTypeKey
Json file attribute which specifies the item type.
virtual QGeoCoordinate coordinate(void) const =0
virtual bool specifiesAltitudeOnly(void) const =0
virtual void appendMissionItems(QList< MissionItem * > &items, QObject *missionItemParent)=0
virtual QString commandName(void) const =0
virtual bool isLandCommand(void) const
static constexpr const char * jsonTypeSimpleItemValue
Item type is MISSION_ITEM.
virtual QGCGeoBoundingCube * boundingCube(void)
virtual bool isSimpleItem(void) const =0
QmlObjectListModel * childItems(void)
virtual bool specifiesCoordinate(void) const =0
void exitCoordinateChanged(const QGeoCoordinate &exitCoordinate)
void additionalTimeDelayChanged(void)
virtual void save(QJsonArray &missionItems)=0
VisualMissionItem * parentItem(void)
virtual QGeoCoordinate entryCoordinate(void) const =0
virtual double editableAlt(void) const =0
bool loadGeoCoordinate(const QJsonValue &jsonValue, bool altitudeRequired, QGeoCoordinate &coordinate, QString &errorString)
void saveGeoCoordinate(const QGeoCoordinate &coordinate, bool writeAltitude, QJsonValue &jsonValue)
Saves a QGeoCoordinate as [lat, lon, alt] array (QGC plan format).
bool validateKeys(const QJsonObject &jsonObject, const QList< KeyValidateInfo > &keyInfo, QString &errorString)
Validates that all required keys are present and that listed keys have the expected type.
constexpr const char * jsonVersionKey
Definition JsonParsing.h:14
bool fuzzyCompare(double value1, double value2)
Returns true if the two values are equal or close. Correctly handles 0 and NaN values.
Definition QGCMath.cc:71
GPSCommandSequence::Command command(QByteArrayView body, GPSTextMatcher reply, std::chrono::milliseconds timeout)
body labelled by its text, framed as a sentence, and completed by reply within timeout.
int batteriesRequired
-1 for not supported
int batteryChangePoint
-1 for not supported, 0 for not needed
static constexpr VehicleClass_t VehicleClassGeneric