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, _missionContainsVTOLTakeoff);
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 int sequenceNumber = _nextSequenceNumber();
318 _takeoffMissionItem = new TakeoffMissionItem(_controllerVehicle->vtol() ? MAV_CMD_NAV_VTOL_TAKEOFF : MAV_CMD_NAV_TAKEOFF, _masterController, _flyView, _settingsItem, false /* forLoad */);
319 _takeoffMissionItem->setSequenceNumber(sequenceNumber);
320 _initVisualItem(_takeoffMissionItem);
321
322 if (_takeoffMissionItem->specifiesAltitude()) {
323 double prevAltitude;
325
326 if (_findPreviousAltitude(visualItemIndex, &prevAltitude, &prevAltFrame)) {
327 _takeoffMissionItem->altitude()->setRawValue(prevAltitude);
328 _takeoffMissionItem->setAltitudeFrame(static_cast<QGroundControlQmlGlobal::AltitudeFrame>(prevAltFrame));
329 }
330 }
331 if (visualItemIndex == -1) {
332 _visualItems->append(_takeoffMissionItem);
333 } else {
334 _visualItems->insert(visualItemIndex, _takeoffMissionItem);
335 }
336
337 _recalcAll();
338
339 if (makeCurrentItem) {
340 setCurrentPlanViewSeqNum(_takeoffMissionItem->sequenceNumber(), true);
341 }
342
343 _firstItemAdded();
344
345 return _takeoffMissionItem;
346}
347
349 // Can't have more than one land sequence unless allowed in settings and
350 // supported by the firmware
351 return _planViewSettings->allowMultipleLandingPatterns()
352 ->rawValue().toBool() &&
354}
355
356VisualMissionItem* MissionController::insertLandItem(QGeoCoordinate coordinate, int visualItemIndex, bool makeCurrentItem)
357{
358 if (_controllerVehicle->fixedWing()) {
359 FixedWingLandingComplexItem* fwLanding = qobject_cast<FixedWingLandingComplexItem*>(insertComplexMissionItem(FixedWingLandingComplexItem::canonicalName, coordinate, visualItemIndex, makeCurrentItem));
360 return fwLanding;
361 } else if (_controllerVehicle->vtol()) {
362 VTOLLandingComplexItem* vtolLanding = qobject_cast<VTOLLandingComplexItem*>(insertComplexMissionItem(VTOLLandingComplexItem::canonicalName, coordinate, visualItemIndex, makeCurrentItem));
363 return vtolLanding;
364 } else {
365 return _insertSimpleMissionItemWorker(coordinate, _controllerVehicle->vtol() ? MAV_CMD_NAV_VTOL_LAND : MAV_CMD_NAV_RETURN_TO_LAUNCH, visualItemIndex, makeCurrentItem);
366 }
367}
368
369VisualMissionItem* MissionController::insertROIMissionItem(QGeoCoordinate coordinate, int visualItemIndex, bool makeCurrentItem)
370{
371 SimpleMissionItem* simpleItem = qobject_cast<SimpleMissionItem*>(_insertSimpleMissionItemWorker(coordinate, MAV_CMD_DO_SET_ROI_LOCATION, visualItemIndex, makeCurrentItem));
372
373 if (!_controllerVehicle->firmwarePlugin()->supportedMissionCommands(QGCMAVLink::VehicleClassGeneric).contains(MAV_CMD_DO_SET_ROI_LOCATION)) {
374 simpleItem->setCommand(MAV_CMD_DO_SET_ROI) ;
375 simpleItem->missionItem().setParam1(MAV_ROI_LOCATION);
376 }
377 return simpleItem;
378}
379
380VisualMissionItem* MissionController::insertCancelROIMissionItem(int visualItemIndex, bool makeCurrentItem)
381{
382 SimpleMissionItem* simpleItem = qobject_cast<SimpleMissionItem*>(_insertSimpleMissionItemWorker(QGeoCoordinate(), MAV_CMD_DO_SET_ROI_NONE, visualItemIndex, makeCurrentItem));
383
384 if (!_controllerVehicle->firmwarePlugin()->supportedMissionCommands(QGCMAVLink::VehicleClassGeneric).contains(MAV_CMD_DO_SET_ROI_NONE)) {
385 simpleItem->setCommand(MAV_CMD_DO_SET_ROI) ;
386 simpleItem->missionItem().setParam1(MAV_ROI_NONE);
387 }
388 return simpleItem;
389}
390
391VisualMissionItem* MissionController::insertComplexMissionItem(QString itemName, QGeoCoordinate mapCenterCoordinate, int visualItemIndex, bool makeCurrentItem)
392{
394 if (!newItem) {
395 return nullptr;
396 }
397 newItem->setCoordinate(mapCenterCoordinate);
398
400 double prevAltitude;
402 if (_findPreviousAltitude(visualItemIndex, &prevAltitude, &prevAltFrame)) {
403 newItem->applyPreviousAltitudeFrame(prevAltFrame, prevAltitude);
404 }
405 }
406
407 _insertComplexMissionItemWorker(mapCenterCoordinate, newItem, visualItemIndex, makeCurrentItem);
408
409 return newItem;
410}
411
412VisualMissionItem* MissionController::insertComplexMissionItemFromKMLOrSHP(QString itemName, QString file, int visualItemIndex, bool makeCurrentItem)
413{
415 if (!newItem) {
416 return nullptr;
417 }
418
419 _insertComplexMissionItemWorker(QGeoCoordinate(), newItem, visualItemIndex, makeCurrentItem);
420
421 return newItem;
422}
423
424void MissionController::_insertComplexMissionItemWorker(const QGeoCoordinate& mapCenterCoordinate, ComplexMissionItem* complexItem, int visualItemIndex, bool makeCurrentItem)
425{
426 int sequenceNumber = _nextSequenceNumber();
427 bool surveyStyleItem = qobject_cast<SurveyComplexItem*>(complexItem) ||
428 qobject_cast<CorridorScanComplexItem*>(complexItem) ||
429 qobject_cast<StructureScanComplexItem*>(complexItem);
430
431 if (surveyStyleItem) {
432 bool rollSupported = false;
433 bool pitchSupported = false;
434 bool yawSupported = false;
435
436 // If the vehicle is known to have a gimbal then we automatically point the gimbal straight down if not already set
437
438 MissionSettingsItem* settingsItem = _visualItems->value<MissionSettingsItem*>(0);
439 CameraSection* cameraSection = settingsItem->cameraSection();
440
441 // Set camera to photo mode (leave alone if user already specified)
442 if (cameraSection->cameraModeSupported() && !cameraSection->specifyCameraMode()) {
443 cameraSection->setSpecifyCameraMode(true);
444 cameraSection->cameraMode()->setRawValue(CAMERA_MODE_IMAGE_SURVEY);
445 }
446
447 // Point gimbal straight down
448 if (_controllerVehicle->firmwarePlugin()->hasGimbal(_controllerVehicle, rollSupported, pitchSupported, yawSupported) && pitchSupported) {
449 // If the user already specified a gimbal angle leave it alone
450 if (!cameraSection->specifyGimbal()) {
451 cameraSection->setSpecifyGimbal(true);
452 cameraSection->gimbalPitch()->setRawValue(-90.0);
453 }
454 }
455 }
456
457 complexItem->setSequenceNumber(sequenceNumber);
458 if (!qobject_cast<VTOLLandingComplexItem*>(complexItem)) {
459 complexItem->setWizardMode(true);
460 }
461 _initVisualItem(complexItem);
462
463 if (visualItemIndex == -1) {
464 _visualItems->append(complexItem);
465 } else {
466 _visualItems->insert(visualItemIndex, complexItem);
467 }
468
469 //-- Keep track of bounding box changes in complex items
470 if(!complexItem->isSimpleItem()) {
471 connect(complexItem, &ComplexMissionItem::boundingCubeChanged, this, &MissionController::_complexBoundingBoxChanged);
472 }
473 _recalcAllWithCoordinate(mapCenterCoordinate);
474
475 if (makeCurrentItem) {
476 setCurrentPlanViewSeqNum(complexItem->sequenceNumber(), true);
477 }
478 _firstItemAdded();
479}
480
482{
483 return _visualItems ? _visualItems->indexOf(object) : -1;
484}
485
487{
488 if (viIndex <= 0 || viIndex >= _visualItems->count()) {
489 qWarning() << "MissionController::removeVisualItem called with bad index - count:index" << _visualItems->count() << viIndex;
490 return;
491 }
492
493 bool removeSurveyStyle = _visualItems->value<SurveyComplexItem*>(viIndex) || _visualItems->value<CorridorScanComplexItem*>(viIndex);
494 VisualMissionItem* item = qobject_cast<VisualMissionItem*>(_visualItems->removeAt(viIndex));
495
496 if (item == _takeoffMissionItem) {
497 _takeoffMissionItem = nullptr;
498 }
499
500 _deinitVisualItem(item);
501 item->deleteLater();
502
503 if (removeSurveyStyle) {
504 // Determine if the mission still has another survey style item in it
505 bool foundSurvey = false;
506 for (int i=1; i<_visualItems->count(); i++) {
507 if (_visualItems->value<SurveyComplexItem*>(i) || _visualItems->value<CorridorScanComplexItem*>(i)) {
508 foundSurvey = true;
509 break;
510 }
511 }
512
513 // If there is no longer a survey item in the mission remove added commands
514 if (!foundSurvey) {
515 bool rollSupported = false;
516 bool pitchSupported = false;
517 bool yawSupported = false;
518 CameraSection* cameraSection = _settingsItem->cameraSection();
519 if (_controllerVehicle->firmwarePlugin()->hasGimbal(_controllerVehicle, rollSupported, pitchSupported, yawSupported) && pitchSupported) {
520 if (cameraSection->specifyGimbal() && cameraSection->gimbalPitch()->rawValue().toDouble() == -90.0 && cameraSection->gimbalYaw()->rawValue().toDouble() == 0.0) {
521 cameraSection->setSpecifyGimbal(false);
522 }
523 }
524 if (cameraSection->cameraModeSupported() && cameraSection->specifyCameraMode() && cameraSection->cameraMode()->rawValue().toInt() == 0) {
525 cameraSection->setSpecifyCameraMode(false);
526 }
527 }
528 }
529
530 _recalcAll();
531
532 // Adjust current item
533 int newVIIndex;
534 if (viIndex >= _visualItems->count()) {
535 newVIIndex = _visualItems->count() - 1;
536 } else {
537 newVIIndex = viIndex;
538 }
539 setCurrentPlanViewSeqNum(_visualItems->value<VisualMissionItem*>(newVIIndex)->sequenceNumber(), true);
540
541 setDirty(true);
542
543 if (_visualItems->count() == 1) {
544 _allItemsRemoved();
545 }
546}
547
548void MissionController::_setupNewVisualItems(QmlObjectListModel* newItems)
549{
550 QmlObjectListModel* oldItems = _visualItems;
551
552 if (oldItems) {
553 _deinitAllVisualItems();
554
555 // Destroy old items after a delay — TreeView delegates are torn down
556 // asynchronously during a polish cycle and may still hold bindings.
557 QTimer::singleShot(1000, oldItems, [oldItems] {
558 oldItems->clearAndDeleteContents();
559 oldItems->deleteLater();
560 });
561 }
562
563 _settingsItem = nullptr;
564 _takeoffMissionItem = nullptr;
565
566 if (newItems) {
567 _visualItems = newItems;
568 if (_visualItems->count() == 0) {
569 _addMissionSettings(_visualItems);
570 } else {
571 _settingsItem = _visualItems->value<MissionSettingsItem*>(0);
572 }
573 } else {
574 _visualItems = new QmlObjectListModel(this);
575 _addMissionSettings(_visualItems);
576 }
577}
578
580{
581 _setupNewVisualItems();
582 _initAllVisualItems();
583 setDirty(true);
584 _resetMissionFlightStatus();
585 _allItemsRemoved();
586}
587
588bool MissionController::_loadJsonMissionFileV2(const QJsonObject& json, QmlObjectListModel* visualItems, QString& errorString)
589{
590 // Validate root object keys
591 QList<JsonParsing::KeyValidateInfo> rootKeyInfoList = {
592 { _jsonPlannedHomePositionKey, QJsonValue::Array, true },
593 { _jsonItemsKey, QJsonValue::Array, true },
594 { _jsonFirmwareTypeKey, QJsonValue::Double, true },
595 { _jsonVehicleTypeKey, QJsonValue::Double, false },
596 { _jsonCruiseSpeedKey, QJsonValue::Double, false },
597 { _jsonHoverSpeedKey, QJsonValue::Double, false },
598 { _jsonGlobalPlanAltitudeModeKey, QJsonValue::Double, false },
599 };
600 if (!JsonParsing::validateKeys(json, rootKeyInfoList, errorString)) {
601 return false;
602 }
603
605
606 qCDebug(MissionControllerLog) << "MissionController::_loadJsonMissionFileV2 itemCount:" << json[_jsonItemsKey].toArray().count();
607
609
610 // Get the firmware/vehicle type from the plan file
611 MAV_AUTOPILOT planFileFirmwareType = static_cast<MAV_AUTOPILOT>(json[_jsonFirmwareTypeKey].toInt());
612 MAV_TYPE planFileVehicleType = static_cast<MAV_TYPE> (QGCMAVLink::vehicleClassToMavType(appSettings->offlineEditingVehicleClass()->rawValue().toInt()));
613 if (json.contains(_jsonVehicleTypeKey)) {
614 planFileVehicleType = static_cast<MAV_TYPE>(json[_jsonVehicleTypeKey].toInt());
615 }
616
617 // Update firmware/vehicle offline settings if we aren't connect to a vehicle
618 if (_masterController->offline()) {
619 appSettings->offlineEditingFirmwareClass()->setRawValue(QGCMAVLink::firmwareClass(static_cast<MAV_AUTOPILOT>(json[_jsonFirmwareTypeKey].toInt())));
620 if (json.contains(_jsonVehicleTypeKey)) {
621 appSettings->offlineEditingVehicleClass()->setRawValue(QGCMAVLink::vehicleClass(planFileVehicleType));
622 }
623 }
624
625 // The controller vehicle always tracks the Plan file firmware/vehicle types so update it
626 _controllerVehicle->stopTrackingFirmwareVehicleTypeChanges();
627 _controllerVehicle->_offlineFirmwareTypeSettingChanged(planFileFirmwareType);
628 _controllerVehicle->_offlineVehicleTypeSettingChanged(planFileVehicleType);
629
630 if (json.contains(_jsonCruiseSpeedKey)) {
631 appSettings->offlineEditingCruiseSpeed()->setRawValue(json[_jsonCruiseSpeedKey].toDouble());
632 }
633 if (json.contains(_jsonHoverSpeedKey)) {
634 appSettings->offlineEditingHoverSpeed()->setRawValue(json[_jsonHoverSpeedKey].toDouble());
635 }
636 if (json.contains(_jsonGlobalPlanAltitudeModeKey)) {
637 setGlobalAltitudeFrame(json[_jsonGlobalPlanAltitudeModeKey].toVariant().value<QGroundControlQmlGlobal::AltitudeFrame>());
638 }
639
640 QGeoCoordinate homeCoordinate;
641 if (!GeoJsonHelper::loadGeoCoordinate(json[_jsonPlannedHomePositionKey], true /* altitudeRequired */, homeCoordinate, errorString)) {
642 return false;
643 }
645 settingsItem->setCoordinate(homeCoordinate);
646 visualItems->insert(0, settingsItem);
647 qCDebug(MissionControllerLog) << "plannedHomePosition" << homeCoordinate;
648
649 // Read mission items
650
651 int nextSequenceNumber = 1; // Start with 1 since home is in 0
652 const QJsonArray rgMissionItems(json[_jsonItemsKey].toArray());
653 for (int i=0; i<rgMissionItems.count(); i++) {
654 // Convert to QJsonObject
655 const QJsonValue& itemValue = rgMissionItems[i];
656 if (!itemValue.isObject()) {
657 errorString = tr("Mission item %1 is not an object").arg(i);
658 return false;
659 }
660 const QJsonObject itemObject = itemValue.toObject();
661
662 // Load item based on type
663
664 QList<JsonParsing::KeyValidateInfo> itemKeyInfoList = {
665 { VisualMissionItem::jsonTypeKey, QJsonValue::String, true },
666 };
667 if (!JsonParsing::validateKeys(itemObject, itemKeyInfoList, errorString)) {
668 return false;
669 }
670 QString itemType = itemObject[VisualMissionItem::jsonTypeKey].toString();
671
673 SimpleMissionItem* simpleItem = new SimpleMissionItem(_masterController, _flyView, true /* forLoad */);
674 if (simpleItem->load(itemObject, nextSequenceNumber, errorString)) {
675 if (TakeoffMissionItem::isTakeoffCommand(static_cast<MAV_CMD>(simpleItem->command()))) {
676 // This needs to be a TakeoffMissionItem
677 TakeoffMissionItem* takeoffItem = new TakeoffMissionItem(_masterController, _flyView, settingsItem, true /* forLoad */);
678 takeoffItem->load(itemObject, nextSequenceNumber, errorString);
679 simpleItem->deleteLater();
680 simpleItem = takeoffItem;
681 }
682 qCDebug(MissionControllerLog) << "Loading simple item: nextSequenceNumber:command" << nextSequenceNumber << simpleItem->command();
683 nextSequenceNumber = simpleItem->lastSequenceNumber() + 1;
684 visualItems->append(simpleItem);
685 } else {
686 return false;
687 }
688 } else if (itemType == VisualMissionItem::jsonTypeComplexItemValue) {
689 QList<JsonParsing::KeyValidateInfo> complexItemKeyInfoList = {
690 { ComplexMissionItem::jsonComplexItemTypeKey, QJsonValue::String, true },
691 };
692 if (!JsonParsing::validateKeys(itemObject, complexItemKeyInfoList, errorString)) {
693 return false;
694 }
695 QString complexItemType = itemObject[ComplexMissionItem::jsonComplexItemTypeKey].toString();
696
697 qCDebug(MissionControllerLog) << "Loading complex item type:" << complexItemType << "nextSequenceNumber:" << nextSequenceNumber;
699 if (!complexItem) {
700 errorString = tr("Unsupported complex item type: %1").arg(complexItemType);
701 return false;
702 }
703 if (!complexItem->load(itemObject, nextSequenceNumber++, errorString)) {
704 delete complexItem;
705 return false;
706 }
707 nextSequenceNumber = complexItem->lastSequenceNumber() + 1;
708 qCDebug(MissionControllerLog) << "Complex item load complete nextSequenceNumber:" << nextSequenceNumber;
709 visualItems->append(complexItem);
710 } else {
711 errorString = tr("Unknown item type: %1").arg(itemType);
712 return false;
713 }
714 }
715
716 // Fix up the DO_JUMP commands jump sequence number by finding the item with the matching doJumpId
717 for (int i=0; i<visualItems->count(); i++) {
720 if (doJumpItem->command() == MAV_CMD_DO_JUMP) {
721 bool found = false;
722 int findDoJumpId = static_cast<int>(doJumpItem->missionItem().param1());
723 for (int j=0; j<visualItems->count(); j++) {
726 if (targetItem->missionItem().doJumpId() == findDoJumpId) {
727 doJumpItem->missionItem().setParam1(targetItem->sequenceNumber());
728 found = true;
729 break;
730 }
731 }
732 }
733 if (!found) {
734 errorString = tr("Could not find doJumpId: %1").arg(findDoJumpId);
735 return false;
736 }
737 }
738 }
739 }
740
741 return true;
742}
743
744bool MissionController::_loadTextMissionFile(QTextStream& stream, QmlObjectListModel* visualItems, QString& errorString)
745{
746 bool firstItem = true;
747 bool plannedHomePositionInFile = false;
748
749 QString firstLine = stream.readLine();
750 const QStringList& version = firstLine.split(" ");
751
752 bool versionOk = false;
753 if (version.size() == 3 && version[0] == "QGC" && version[1] == "WPL") {
754 if (version[2] == "110") {
755 // ArduPilot file, planned home position is already in position 0
756 versionOk = true;
757 plannedHomePositionInFile = true;
758 } else if (version[2] == "120") {
759 // Old QGC file, no planned home position
760 versionOk = true;
761 plannedHomePositionInFile = false;
762 }
763 }
764
765 if (versionOk) {
766 MissionSettingsItem* settingsItem = _addMissionSettings(visualItems);
767
768 while (!stream.atEnd()) {
769 SimpleMissionItem* item = new SimpleMissionItem(_masterController, _flyView, true /* forLoad */);
770 if (item->load(stream)) {
771 if (firstItem && plannedHomePositionInFile) {
772 settingsItem->setCoordinate(item->coordinate());
773 } else {
774 if (TakeoffMissionItem::isTakeoffCommand(static_cast<MAV_CMD>(item->command()))) {
775 // This needs to be a TakeoffMissionItem
776 TakeoffMissionItem* takeoffItem = new TakeoffMissionItem(item->missionItem(), _masterController, _flyView, settingsItem, false /* forLoad */);
777 item->deleteLater();
778 item = takeoffItem;
779 }
780 visualItems->append(item);
781 }
782 firstItem = false;
783 } else {
784 errorString = tr("The mission file is corrupted.");
785 return false;
786 }
787 }
788 } else {
789 errorString = tr("The mission file is not compatible with this version of %1.").arg(QCoreApplication::applicationName());
790 return false;
791 }
792
793 if (!plannedHomePositionInFile) {
794 // Update sequence numbers in DO_JUMP commands to take into account added home position in index 0
795 for (int i=1; i<visualItems->count(); i++) {
796 SimpleMissionItem* item = qobject_cast<SimpleMissionItem*>(visualItems->get(i));
797 if (item && item->command() == MAV_CMD_DO_JUMP) {
798 item->missionItem().setParam1(static_cast<int>(item->missionItem().param1()) + 1);
799 }
800 }
801 }
802
803 return true;
804}
805
806void MissionController::_initLoadedVisualItems(QmlObjectListModel* loadedVisualItems)
807{
808 _setupNewVisualItems(loadedVisualItems);
809
810 MissionController::_scanForAdditionalSettings(_visualItems, _masterController);
811
812 _initAllVisualItems();
813
814 if (_visualItems->count() > 1) {
815 _firstItemAdded();
816 } else {
817 _allItemsRemoved();
818 }
819}
820
821bool MissionController::load(const QJsonObject& json, QString& errorString)
822{
823 QString errorStr;
824 QString errorMessage = tr("Mission: %1");
825 QmlObjectListModel* loadedVisualItems = new QmlObjectListModel(this);
826
827 if (!_loadJsonMissionFileV2(json, loadedVisualItems, errorStr)) {
828 errorString = errorMessage.arg(errorStr);
829 return false;
830 }
831 _initLoadedVisualItems(loadedVisualItems);
832
833 return true;
834}
835
837{
838 QString errorStr;
839 QString errorMessage = tr("Mission: %1");
840 QByteArray bytes = file.readAll();
841 QTextStream stream(bytes);
842
844
845 QmlObjectListModel* loadedVisualItems = new QmlObjectListModel(this);
846 if (!_loadTextMissionFile(stream, loadedVisualItems, errorStr)) {
847 errorString = errorMessage.arg(errorStr);
848 return false;
849 }
850
851 _initLoadedVisualItems(loadedVisualItems);
852
853 return true;
854}
855
857{
858 for (int i=0; i<_visualItems->count(); i++) {
859 VisualMissionItem* visualItem = qobject_cast<VisualMissionItem*>(_visualItems->get(i));
861 return visualItem->readyForSaveState();
862 }
863 }
864
866}
867
868void MissionController::save(QJsonObject& json)
869{
870 json[JsonParsing::jsonVersionKey] = _missionFileVersion;
871
872 // Mission settings
873
874 MissionSettingsItem* settingsItem = _visualItems->value<MissionSettingsItem*>(0);
875 if (!settingsItem) {
876 qWarning() << "First item is not MissionSettingsItem";
877 return;
878 }
879 QJsonValue coordinateValue;
880 GeoJsonHelper::saveGeoCoordinate(settingsItem->coordinate(), true /* writeAltitude */, coordinateValue);
881 json[_jsonPlannedHomePositionKey] = coordinateValue;
882 json[_jsonFirmwareTypeKey] = _controllerVehicle->firmwareType();
883 json[_jsonVehicleTypeKey] = _controllerVehicle->vehicleType();
884 json[_jsonCruiseSpeedKey] = _controllerVehicle->defaultCruiseSpeed();
885 json[_jsonHoverSpeedKey] = _controllerVehicle->defaultHoverSpeed();
886 json[_jsonGlobalPlanAltitudeModeKey] = _globalAltFrame;
887
888 // Save the visual items
889
890 QJsonArray rgJsonMissionItems;
891 for (int i=0; i<_visualItems->count(); i++) {
892 VisualMissionItem* visualItem = qobject_cast<VisualMissionItem*>(_visualItems->get(i));
893
894 visualItem->save(rgJsonMissionItems);
895 }
896
897 // Mission settings has a special case for end mission action
898 if (settingsItem) {
899 QList<MissionItem*> rgMissionItems;
900
901 if (_convertToMissionItems(_visualItems, rgMissionItems, this /* missionItemParent */)) {
902 QJsonObject saveObject;
903 MissionItem* missionItem = rgMissionItems[rgMissionItems.count() - 1];
904 missionItem->save(saveObject);
905 rgJsonMissionItems.append(saveObject);
906 }
907 for (int i=0; i<rgMissionItems.count(); i++) {
908 rgMissionItems[i]->deleteLater();
909 }
910 }
911
912 json[_jsonItemsKey] = rgJsonMissionItems;
913}
914
915FlightPathSegment* MissionController::_createFlightPathSegmentWorker(VisualItemPair& pair, bool mavlinkTerrainFrame)
916{
917 // The takeoff goes straight up from ground to alt and then over to specified position at same alt. Which means
918 // that coord 1 altitude is the same as coord altitude.
919 bool takeoffStraightUp = pair.second->isTakeoffItem() && !_controllerVehicle->fixedWing();
920
921 QGeoCoordinate coord1 = pair.first->exitCoordinate();
922 QGeoCoordinate coord2 = pair.second->entryCoordinate();
923 double coord2AMSLAlt = pair.second->amslEntryAlt();
924 double coord1AMSLAlt = takeoffStraightUp ? coord2AMSLAlt : pair.first->amslExitAlt();
925
927 if (pair.second->isTakeoffItem()) {
929 } else if (pair.second->isLandCommand()) {
931 }
932
933 FlightPathSegment* segment = new FlightPathSegment(segmentType, coord1, coord1AMSLAlt, coord2, coord2AMSLAlt, !_flyView /* queryTerrainData */, this);
934
935 if (takeoffStraightUp) {
937 } else {
939 }
943
946
947 connect(segment, &FlightPathSegment::totalDistanceChanged, this, &MissionController::recalcTerrainProfile, Qt::QueuedConnection);
950 connect(segment, &FlightPathSegment::amslTerrainHeightsChanged, this, &MissionController::recalcTerrainProfile, Qt::QueuedConnection);
951 connect(segment, &FlightPathSegment::terrainCollisionChanged, this, &MissionController::recalcTerrainProfile, Qt::QueuedConnection);
952
953 return segment;
954}
955
956FlightPathSegment* MissionController::_addFlightPathSegment(FlightPathSegmentHashTable& prevItemPairHashTable, VisualItemPair& pair, bool mavlinkTerrainFrame)
957{
958 FlightPathSegment* segment = nullptr;
959
960 if (prevItemPairHashTable.contains(pair) && (prevItemPairHashTable[pair]->segmentType() == FlightPathSegment::SegmentTypeTerrainFrame) != mavlinkTerrainFrame) {
961 // Pair already exists and connected, just re-use
962 _flightPathSegmentHashTable[pair] = segment = prevItemPairHashTable.take(pair);
963 } else {
964 segment = _createFlightPathSegmentWorker(pair, mavlinkTerrainFrame);
965 _flightPathSegmentHashTable[pair] = segment;
966 }
967
968 _simpleFlightPathSegments.append(segment);
969
970 return segment;
971}
972
973void MissionController::_recalcFlightPathSegments(void)
974{
975 VisualItemPair lastSegmentVisualItemPair;
976 int segmentCount = 0;
977 bool firstCoordinateNotFound = true;
978 VisualMissionItem* lastFlyThroughVI = qobject_cast<VisualMissionItem*>(_visualItems->get(0));
979 bool linkEndToHome = false;
980 bool linkStartToHome = _controllerVehicle->rover() ? true : false;
981 bool foundRTL = false;
982 bool homePositionValid = _settingsItem->coordinate().isValid();
983 bool roiActive = false;
984 bool previousItemIsIncomplete = false;
985 bool signalSplitSegmentChanged = false;
986
987 qCDebug(MissionControllerLog) << "_recalcFlightPathSegments homePositionValid" << homePositionValid;
988
989 FlightPathSegmentHashTable oldSegmentTable = _flightPathSegmentHashTable;
990
991 _missionContainsVTOLTakeoff = false;
992 _flightPathSegmentHashTable.clear();
993
994 _simpleFlightPathSegments.beginResetModel();
995 _directionArrows.beginResetModel();
996
997 _simpleFlightPathSegments.clear();
998 _directionArrows.clear();
999
1000 // Mission Settings item needs to start with no segment
1001 lastFlyThroughVI->clearSimpleFlighPathSegment();
1002
1003 // 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
1004 // below since that loop won't always process all items.
1005 for (int i=1; i<_visualItems->count(); i++) {
1006 qobject_cast<VisualMissionItem*>(_visualItems->get(i))->clearSimpleFlighPathSegment();
1007 }
1008
1009 // Grovel through the list of items keeping track of things needed to correctly draw waypoints lines
1010 for (int i=1; i<_visualItems->count(); i++) {
1011 VisualMissionItem* visualItem = qobject_cast<VisualMissionItem*>(_visualItems->get(i));
1012 SimpleMissionItem* simpleItem = qobject_cast<SimpleMissionItem*>(visualItem);
1013 ComplexMissionItem* complexItem = qobject_cast<ComplexMissionItem*>(visualItem);
1014
1015 visualItem->clearSimpleFlighPathSegment();
1016
1017 if (simpleItem) {
1018 if (roiActive) {
1019 if (_isROICancelItem(simpleItem)) {
1020 roiActive = false;
1021 }
1022 } else {
1023 if (_isROIBeginItem(simpleItem)) {
1024 roiActive = true;
1025 }
1026 }
1027
1028 MAV_CMD command = simpleItem->mavCommand();
1029 switch (command) {
1030 case MAV_CMD_NAV_TAKEOFF:
1031 case MAV_CMD_NAV_VTOL_TAKEOFF:
1032 _missionContainsVTOLTakeoff = command == MAV_CMD_NAV_VTOL_TAKEOFF;
1033 if (!linkEndToHome) {
1034 // If we still haven't found the first coordinate item and we hit a takeoff command this means the mission starts from the ground.
1035 // Link the first item back to home to show that.
1036 if (firstCoordinateNotFound) {
1037 linkStartToHome = true;
1038 }
1039 }
1040 break;
1041 case MAV_CMD_NAV_RETURN_TO_LAUNCH:
1042 linkEndToHome = true;
1043 foundRTL = true;
1044 break;
1045 default:
1046 break;
1047 }
1048 }
1049
1050 // No need to add waypoint segments after an RTL.
1051 if (foundRTL) {
1052 break;
1053 }
1054
1055 // Don't draw segments immediately after a landing item
1056 if (lastFlyThroughVI->isLandCommand()) {
1057 lastFlyThroughVI = visualItem;
1058 continue;
1059 }
1060
1061 if (visualItem->specifiesCoordinate() && !visualItem->isStandaloneCoordinate()) {
1062 // Incomplete items are complex items which are waiting for the user to complete setup before there visuals can become valid.
1063 // They may not yet have valid entry/exit coordinates associated with them while in the incomplete state.
1064 // For examples a Survey item which has no polygon set yet.
1065 if (complexItem && complexItem->isIncomplete()) {
1066 // We don't link lines from a valid item to an incomplete item
1067 previousItemIsIncomplete = true;
1068 } else if (previousItemIsIncomplete) {
1069 // We also don't link lines from an incomplete item to a valid item.
1070 previousItemIsIncomplete = false;
1071 firstCoordinateNotFound = false;
1072 lastFlyThroughVI = visualItem;
1073 } else {
1074 if (lastFlyThroughVI != _settingsItem || (homePositionValid && linkStartToHome)) {
1075 bool addDirectionArrow = false;
1076 if (i != 1) {
1077 // Direction arrows are added to the second segment and every 5 segments thereafter.
1078 // The reason for start with second segment is to prevent an arrow being added in between the home position
1079 // and a takeoff item which may be right over each other. In that case the arrow points in a random direction.
1080 if (firstCoordinateNotFound || !lastFlyThroughVI->isSimpleItem() || !visualItem->isSimpleItem()) {
1081 addDirectionArrow = true;
1082 } else if (segmentCount > 5) {
1083 segmentCount = 0;
1084 addDirectionArrow = true;
1085 }
1086 segmentCount++;
1087 }
1088
1089 lastSegmentVisualItemPair = VisualItemPair(lastFlyThroughVI, visualItem);
1090 SimpleMissionItem* lastSimpleItem = qobject_cast<SimpleMissionItem*>(lastFlyThroughVI);
1091 bool mavlinkTerrainFrame = lastSimpleItem ? lastSimpleItem->missionItem().frame() == MAV_FRAME_GLOBAL_TERRAIN_ALT : false;
1092 FlightPathSegment* segment = _addFlightPathSegment(oldSegmentTable, lastSegmentVisualItemPair, mavlinkTerrainFrame);
1093 segment->setSpecialVisual(roiActive);
1094 if (addDirectionArrow) {
1095 _directionArrows.append(segment);
1096 }
1097 if (visualItem->isCurrentItem() && _delayedSplitSegmentUpdate) {
1098 _splitSegment = segment;
1099 _delayedSplitSegmentUpdate = false;
1100 signalSplitSegmentChanged = true;
1101 }
1102 lastFlyThroughVI->setSimpleFlighPathSegment(segment);
1103 }
1104 firstCoordinateNotFound = false;
1105 lastFlyThroughVI = visualItem;
1106 }
1107 }
1108 }
1109
1110 if (linkEndToHome && lastFlyThroughVI != _settingsItem && homePositionValid) {
1111 lastSegmentVisualItemPair = VisualItemPair(lastFlyThroughVI, _settingsItem);
1112 FlightPathSegment* segment = _addFlightPathSegment(oldSegmentTable, lastSegmentVisualItemPair, false /* mavlinkTerrainFrame */);
1113 segment->setSpecialVisual(roiActive);
1114 lastFlyThroughVI->setSimpleFlighPathSegment(segment);
1115 }
1116
1117 // Add direction arrow to last segment
1118 if (lastSegmentVisualItemPair.first) {
1119 FlightPathSegment* coordVector = nullptr;
1120
1121 // 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.
1122 // check for that first and add if needed
1123
1124 if (_flightPathSegmentHashTable.contains(lastSegmentVisualItemPair)) {
1125 // Pair exists in the new table already just reuse it
1126 coordVector = _flightPathSegmentHashTable[lastSegmentVisualItemPair];
1127 } else if (oldSegmentTable.contains(lastSegmentVisualItemPair)) {
1128 // Pair already exists in old table, pull from old to new and reuse
1129 _flightPathSegmentHashTable[lastSegmentVisualItemPair] = coordVector = oldSegmentTable.take(lastSegmentVisualItemPair);
1130 } else {
1131 // Create a new segment. Since this is the fly view there is no need to wire change signals or worry about correct SegmentType
1132 coordVector = new FlightPathSegment(
1134 lastSegmentVisualItemPair.first->isSimpleItem() ? lastSegmentVisualItemPair.first->entryCoordinate() : lastSegmentVisualItemPair.first->exitCoordinate(),
1135 lastSegmentVisualItemPair.first->isSimpleItem() ? lastSegmentVisualItemPair.first->amslEntryAlt() : lastSegmentVisualItemPair.first->amslExitAlt(),
1136 lastSegmentVisualItemPair.second->entryCoordinate(),
1137 lastSegmentVisualItemPair.second->amslEntryAlt(),
1138 !_flyView /* queryTerrainData */,
1139 this);
1140 _flightPathSegmentHashTable[lastSegmentVisualItemPair] = coordVector;
1141 }
1142
1143 _directionArrows.append(coordVector);
1144 }
1145
1146 _simpleFlightPathSegments.endResetModel();
1147 _directionArrows.endResetModel();
1148
1149 // Anything left in the old table is an obsolete line object that can go
1150 qDeleteAll(oldSegmentTable);
1151
1153
1154 emit recalcTerrainProfile();
1155 if (signalSplitSegmentChanged) {
1156 emit splitSegmentChanged();
1157 }
1158}
1159
1160void MissionController::_recalcMissionFlightStatus()
1161{
1162 if (!_visualItems->count()) {
1163 return;
1164 }
1165
1166 qCDebug(MissionControllerLog) << "_recalcMissionFlightStatus";
1167
1168 _flightStatusCalc.recalc(_visualItems, _settingsItem, _controllerVehicle, _managerVehicle, _appSettings, _planViewSettings, _missionContainsVTOLTakeoff);
1169 _missionFlightStatus = _flightStatusCalc.status();
1170 _minAMSLAltitude = _flightStatusCalc.minAMSLAltitude();
1171 _maxAMSLAltitude = _flightStatusCalc.maxAMSLAltitude();
1172
1173 emit missionMaxTelemetryChanged (_missionFlightStatus.maxTelemetryDistance);
1174 emit missionTotalDistanceChanged (_missionFlightStatus.totalDistance);
1175 emit missionPlannedDistanceChanged (_missionFlightStatus.plannedDistance);
1176 emit missionHoverDistanceChanged (_missionFlightStatus.hoverDistance);
1177 emit missionCruiseDistanceChanged (_missionFlightStatus.cruiseDistance);
1178 emit missionTimeChanged ();
1181 emit batteryChangePointChanged (_missionFlightStatus.batteryChangePoint);
1182 emit batteriesRequiredChanged (_missionFlightStatus.batteriesRequired);
1183 emit minAMSLAltitudeChanged (_minAMSLAltitude);
1184 emit maxAMSLAltitudeChanged (_maxAMSLAltitude);
1185
1186 _updateTimer.start(UPDATE_TIMEOUT);
1187
1188 emit recalcTerrainProfile();
1189}
1190
1191// This will update the sequence numbers to be sequential starting from 0
1192void MissionController::_recalcSequence(void)
1193{
1194 if (_inRecalcSequence) {
1195 // Don't let this call recurse due to signalling
1196 return;
1197 }
1198
1199 // Setup ascending sequence numbers for all visual items
1200
1201 _inRecalcSequence = true;
1202 int sequenceNumber = 0;
1203 for (int i=0; i<_visualItems->count(); i++) {
1204 VisualMissionItem* item = qobject_cast<VisualMissionItem*>(_visualItems->get(i));
1205 item->setSequenceNumber(sequenceNumber);
1206 sequenceNumber = item->lastSequenceNumber() + 1;
1207 }
1208 _inRecalcSequence = false;
1209}
1210
1211// This will update the child item hierarchy
1212void MissionController::_recalcChildItems(void)
1213{
1214 VisualMissionItem* currentParentItem = qobject_cast<VisualMissionItem*>(_visualItems->get(0));
1215
1216 currentParentItem->childItems()->clear();
1217
1218 for (int i=1; i<_visualItems->count(); i++) {
1219 VisualMissionItem* item = _visualItems->value<VisualMissionItem*>(i);
1220
1221 item->setParentItem(nullptr);
1222 item->setHasCurrentChildItem(false);
1223
1224 // Set up non-coordinate item child hierarchy
1225 if (item->specifiesCoordinate()) {
1226 item->childItems()->clear();
1227 currentParentItem = item;
1228 } else if (item->isSimpleItem()) {
1229 item->setParentItem(currentParentItem);
1230 currentParentItem->childItems()->append(item);
1231 if (item->isCurrentItem()) {
1232 currentParentItem->setHasCurrentChildItem(true);
1233 }
1234 }
1235 }
1236}
1237
1238void MissionController::_setupTreeModel(void)
1239{
1240 _visualItemsTree.beginResetModel();
1241
1242 _visualItemsTree.clear();
1243
1244 // ── Plan File group ──
1245 _planFileGroupNode.setObjectName(tr("Plan Info"));
1246 _visualItemsTree.appendItem(&_planFileGroupNode, QModelIndex(), QStringLiteral("planFileGroup"));
1247
1248 _planFileInfoMarker.setObjectName(QStringLiteral("planFileInfo"));
1249 _visualItemsTree.appendItem(&_planFileInfoMarker,
1250 _visualItemsTree.indexForObject(&_planFileGroupNode), QStringLiteral("planFileInfo"));
1251
1252 // ── Defaults group ──
1253 _defaultsGroupNode.setObjectName(tr("Defaults"));
1254 _visualItemsTree.appendItem(&_defaultsGroupNode, QModelIndex(), QStringLiteral("defaultsGroup"));
1255
1256 _defaultsInfoMarker.setObjectName(QStringLiteral("defaultsInfo"));
1257 _visualItemsTree.appendItem(&_defaultsInfoMarker,
1258 _visualItemsTree.indexForObject(&_defaultsGroupNode), QStringLiteral("defaultsInfo"));
1259
1260 // ── Mission Items group ──
1261 _missionItemsGroupNode.setObjectName(tr("Mission Items"));
1262 _visualItemsTree.appendItem(&_missionItemsGroupNode, QModelIndex(), QStringLiteral("missionGroup"));
1263
1264 // ── GeoFence group ──
1265 _fenceGroupNode.setObjectName(tr("GeoFence"));
1266 _visualItemsTree.appendItem(&_fenceGroupNode, QModelIndex(), QStringLiteral("fenceGroup"));
1267
1268 // Single marker child — delegate loads the full GeoFenceEditor
1269 _fenceEditorMarker.setObjectName(QStringLiteral("fenceEditor"));
1270 _visualItemsTree.appendItem(&_fenceEditorMarker,
1271 _visualItemsTree.indexForObject(&_fenceGroupNode), QStringLiteral("fenceEditor"));
1272
1273 // ── Rally Points group ──
1274 _rallyGroupNode.setObjectName(tr("Rally Points"));
1275 _visualItemsTree.appendItem(&_rallyGroupNode, QModelIndex(), QStringLiteral("rallyGroup"));
1276
1277 // Marker child for the rally header / instructions
1278 _rallyHeaderMarker.setObjectName(QStringLiteral("rallyHeader"));
1279 _visualItemsTree.appendItem(&_rallyHeaderMarker,
1280 _visualItemsTree.indexForObject(&_rallyGroupNode), QStringLiteral("rallyHeader"));
1281
1282 // ── Transform group ──
1283 _transformGroupNode.setObjectName(tr("Transform"));
1284 _visualItemsTree.appendItem(&_transformGroupNode, QModelIndex(), QStringLiteral("transformGroup"));
1285
1286 // Single marker child — delegate loads the inline TransformEditor
1287 _transformEditorMarker.setObjectName(QStringLiteral("transformEditor"));
1288 _visualItemsTree.appendItem(&_transformEditorMarker,
1289 _visualItemsTree.indexForObject(&_transformGroupNode), QStringLiteral("transformEditor"));
1290
1291 _visualItemsTree.endResetModel();
1292
1293 // Capture persistent indexes after the reset so they aren't invalidated
1294 _planFileGroupIndex = QPersistentModelIndex(_visualItemsTree.indexForObject(&_planFileGroupNode));
1295 _defaultsGroupIndex = QPersistentModelIndex(_visualItemsTree.indexForObject(&_defaultsGroupNode));
1296 _missionGroupIndex = QPersistentModelIndex(_visualItemsTree.indexForObject(&_missionItemsGroupNode));
1297 _fenceGroupIndex = QPersistentModelIndex(_visualItemsTree.indexForObject(&_fenceGroupNode));
1298 _rallyGroupIndex = QPersistentModelIndex(_visualItemsTree.indexForObject(&_rallyGroupNode));
1299 _transformGroupIndex = QPersistentModelIndex(_visualItemsTree.indexForObject(&_transformGroupNode));
1300}
1301
1302//-----------------------------------------------------------------------------
1303// Incremental tree model sync — Mission Items
1304//-----------------------------------------------------------------------------
1305
1306void MissionController::_syncTreeMissionItemsInserted(const QModelIndex& parent, int first, int last)
1307{
1308 Q_UNUSED(parent);
1309 for (int i = first; i <= last; i++) {
1310 auto* item = _visualItems->value<VisualMissionItem*>(i);
1311 if (item) {
1312 _visualItemsTree.insertItem(i, item, _missionGroupIndex, QStringLiteral("missionItem"));
1313 }
1314 }
1315}
1316
1317void MissionController::_syncTreeMissionItemsAboutToBeRemoved(const QModelIndex& parent, int first, int last)
1318{
1319 Q_UNUSED(parent);
1320 // Remove in reverse order so indexes stay valid
1321 for (int i = last; i >= first; i--) {
1322 _visualItemsTree.removeAt(_missionGroupIndex, i);
1323 }
1324}
1325
1326void MissionController::_syncTreeMissionItemsReset(void)
1327{
1328 // removeChildren + appendItem each fire their own row-level signals scoped
1329 // to _missionGroupIndex, so persistent indexes for other groups survive.
1330 _visualItemsTree.removeChildren(_missionGroupIndex);
1331
1332 if (_visualItems) {
1333 for (int i = 0; i < _visualItems->count(); i++) {
1334 auto* item = _visualItems->value<VisualMissionItem*>(i);
1335 if (item) {
1336 _visualItemsTree.appendItem(item, _missionGroupIndex, QStringLiteral("missionItem"));
1337 }
1338 }
1339 }
1340}
1341
1342//-----------------------------------------------------------------------------
1343// Incremental tree model sync — Rally Points
1344//-----------------------------------------------------------------------------
1345
1346void MissionController::_syncTreeRallyPointsInserted(const QModelIndex& parent, int first, int last)
1347{
1348 Q_UNUSED(parent);
1349 auto* rallyController = _masterController->rallyPointController();
1350 if (!rallyController) return;
1351
1352 // If this is the first rally point, remove the header marker
1353 if (first == 0 && _visualItemsTree.rowCount(_rallyGroupIndex) == 1) {
1354 const QModelIndex child = _visualItemsTree.index(0, 0, _rallyGroupIndex);
1355 if (_visualItemsTree.data(child, QmlObjectTreeModel::NodeTypeRole).toString() == QStringLiteral("rallyHeader")) {
1356 _visualItemsTree.removeItem(child);
1357 }
1358 }
1359
1360 auto* pts = rallyController->points();
1361 for (int i = first; i <= last; i++) {
1362 _visualItemsTree.insertItem(i, (*pts)[i], _rallyGroupIndex, QStringLiteral("rallyItem"));
1363 }
1364}
1365
1366void MissionController::_syncTreeRallyPointsAboutToBeRemoved(const QModelIndex& parent, int first, int last)
1367{
1368 Q_UNUSED(parent);
1369 for (int i = last; i >= first; i--) {
1370 _visualItemsTree.removeAt(_rallyGroupIndex, i);
1371 }
1372}
1373
1374void MissionController::_syncTreeRallyPointsRemoved(const QModelIndex& parent, int first, int last)
1375{
1376 Q_UNUSED(parent);
1377 Q_UNUSED(first);
1378 Q_UNUSED(last);
1379
1380 // Re-add the header marker once the source model has finished removing rows
1381 auto* rallyController = _masterController->rallyPointController();
1382 if (rallyController && rallyController->points()->count() == 0) {
1383 _rallyHeaderMarker.setObjectName(QStringLiteral("rallyHeader"));
1384 _visualItemsTree.appendItem(&_rallyHeaderMarker, _rallyGroupIndex, QStringLiteral("rallyHeader"));
1385 }
1386}
1387
1388void MissionController::_syncTreeRallyPointsReset(void)
1389{
1390 // removeChildren + appendItem each fire their own row-level signals scoped
1391 // to _rallyGroupIndex, so persistent indexes for other groups survive.
1392 _visualItemsTree.removeChildren(_rallyGroupIndex);
1393
1394 // Repopulate — either rally items or the header marker
1395 auto* rallyController = _masterController->rallyPointController();
1396 if (rallyController && rallyController->points()->count() > 0) {
1397 auto* pts = rallyController->points();
1398 for (int i = 0; i < pts->count(); i++) {
1399 _visualItemsTree.appendItem((*pts)[i], _rallyGroupIndex, QStringLiteral("rallyItem"));
1400 }
1401 } else {
1402 _rallyHeaderMarker.setObjectName(QStringLiteral("rallyHeader"));
1403 _visualItemsTree.appendItem(&_rallyHeaderMarker, _rallyGroupIndex, QStringLiteral("rallyHeader"));
1404 }
1405}
1406
1407void MissionController::_setPlannedHomePositionFromFirstCoordinate(const QGeoCoordinate& clickCoordinate)
1408{
1409 bool foundFirstCoordinate = false;
1410 QGeoCoordinate firstCoordinate;
1411
1412 if (_settingsItem->coordinate().isValid()) {
1413 return;
1414 }
1415
1416 // Set the planned home position to be a delta from first coordinate
1417 for (int i=1; i<_visualItems->count(); i++) {
1418 VisualMissionItem* item = _visualItems->value<VisualMissionItem*>(i);
1419
1420 if (item->specifiesCoordinate() && item->entryCoordinate().isValid()) {
1421 foundFirstCoordinate = true;
1422 firstCoordinate = item->entryCoordinate();
1423 break;
1424 }
1425 }
1426
1427 // No item specifying a coordinate was found, in this case it we have a clickCoordinate use that
1428 if (!foundFirstCoordinate) {
1429 firstCoordinate = clickCoordinate;
1430 }
1431
1432 if (firstCoordinate.isValid()) {
1433 QGeoCoordinate plannedHomeCoord = firstCoordinate.atDistanceAndAzimuth(30, 0);
1434 plannedHomeCoord.setAltitude(0);
1435 _settingsItem->setCoordinate(plannedHomeCoord);
1436 }
1437}
1438
1439void MissionController::_recalcAllWithCoordinate(const QGeoCoordinate& coordinate)
1440{
1441 if (!_flyView) {
1442 _setPlannedHomePositionFromFirstCoordinate(coordinate);
1443 }
1444 _recalcSequence();
1445 _recalcChildItems();
1447 _updateTimer.start(UPDATE_TIMEOUT);
1448}
1449
1450void MissionController::_recalcAll(void)
1451{
1452 QGeoCoordinate emptyCoord;
1453 _recalcAllWithCoordinate(emptyCoord);
1454}
1455
1457void MissionController::_initAllVisualItems(void)
1458{
1459 // Setup home position at index 0
1460
1461 if (!_settingsItem) {
1462 _settingsItem = qobject_cast<MissionSettingsItem*>(_visualItems->get(0));
1463 if (!_settingsItem) {
1464 qWarning() << "First item not MissionSettingsItem";
1465 return;
1466 }
1467 }
1468
1469 connect(_settingsItem, &MissionSettingsItem::coordinateChanged, this, &MissionController::_recalcMissionFlightStatus);
1472
1473 for (int i=0; i<_visualItems->count(); i++) {
1474 VisualMissionItem* item = qobject_cast<VisualMissionItem*>(_visualItems->get(i));
1475 _initVisualItem(item);
1476
1477 TakeoffMissionItem* takeoffItem = qobject_cast<TakeoffMissionItem*>(item);
1478 if (takeoffItem) {
1479 _takeoffMissionItem = takeoffItem;
1480 }
1481 }
1482
1483 _recalcAll();
1484
1485 connect(_visualItems, &QmlObjectListModel::dirtyChanged, this, &MissionController::_visualItemsDirtyChanged);
1487
1488 // Connect for incremental tree model sync
1489 connect(_visualItems, &QAbstractItemModel::rowsInserted, this, &MissionController::_syncTreeMissionItemsInserted);
1490 connect(_visualItems, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MissionController::_syncTreeMissionItemsAboutToBeRemoved);
1491 connect(_visualItems, &QAbstractItemModel::modelReset, this, &MissionController::_syncTreeMissionItemsReset);
1492
1493 // Populate tree's mission group from current _visualItems
1494 _syncTreeMissionItemsReset();
1495
1496 // Connect rally controller for incremental tree model sync
1497 auto* rallyController = _masterController->rallyPointController();
1498 if (rallyController) {
1499 auto* pts = rallyController->points();
1500 connect(pts, &QAbstractItemModel::rowsInserted, this, &MissionController::_syncTreeRallyPointsInserted);
1501 connect(pts, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MissionController::_syncTreeRallyPointsAboutToBeRemoved);
1502 connect(pts, &QAbstractItemModel::rowsRemoved, this, &MissionController::_syncTreeRallyPointsRemoved);
1503 connect(pts, &QAbstractItemModel::modelReset, this, &MissionController::_syncTreeRallyPointsReset);
1504
1505 // Populate any existing rally points
1506 _syncTreeRallyPointsReset();
1507 }
1508
1509 emit visualItemsReset();
1510 emit containsItemsChanged();
1513
1514 if (!_flyView) {
1515 setCurrentPlanViewSeqNum(0, true);
1516 }
1517
1518 setDirty(false);
1519}
1520
1521void MissionController::_deinitAllVisualItems(void)
1522{
1523 disconnect(_settingsItem, &MissionSettingsItem::coordinateChanged, this, &MissionController::_recalcMissionFlightStatus);
1526
1527 for (int i=0; i<_visualItems->count(); i++) {
1528 _deinitVisualItem(qobject_cast<VisualMissionItem*>(_visualItems->get(i)));
1529 }
1530
1531 disconnect(_visualItems, &QmlObjectListModel::dirtyChanged, this, &MissionController::_visualItemsDirtyChanged);
1533
1534 // Disconnect incremental tree model sync
1535 disconnect(_visualItems, &QAbstractItemModel::rowsInserted, this, &MissionController::_syncTreeMissionItemsInserted);
1536 disconnect(_visualItems, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MissionController::_syncTreeMissionItemsAboutToBeRemoved);
1537 disconnect(_visualItems, &QAbstractItemModel::modelReset, this, &MissionController::_syncTreeMissionItemsReset);
1538
1539 // Disconnect rally controller tree model sync
1540 auto* rallyController = _masterController->rallyPointController();
1541 if (rallyController) {
1542 auto* pts = rallyController->points();
1543 disconnect(pts, &QAbstractItemModel::rowsInserted, this, &MissionController::_syncTreeRallyPointsInserted);
1544 disconnect(pts, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MissionController::_syncTreeRallyPointsAboutToBeRemoved);
1545 disconnect(pts, &QAbstractItemModel::rowsRemoved, this, &MissionController::_syncTreeRallyPointsRemoved);
1546 disconnect(pts, &QAbstractItemModel::modelReset, this, &MissionController::_syncTreeRallyPointsReset);
1547 }
1548}
1549
1550void MissionController::_initVisualItem(VisualMissionItem* visualItem)
1551{
1552 setDirty(false);
1553
1562 connect(visualItem, &VisualMissionItem::lastSequenceNumberChanged, this, &MissionController::_recalcSequence);
1563
1564 if (visualItem->isSimpleItem()) {
1565 // We need to track commandChanged on simple item since recalc has special handling for takeoff command
1566 SimpleMissionItem* simpleItem = qobject_cast<SimpleMissionItem*>(visualItem);
1567 if (simpleItem) {
1568 connect(&simpleItem->missionItem()._commandFact, &Fact::valueChanged, this, &MissionController::_itemCommandChanged);
1569 } else {
1570 qWarning() << "isSimpleItem == true, yet not SimpleMissionItem";
1571 }
1572 } else {
1573 ComplexMissionItem* complexItem = qobject_cast<ComplexMissionItem*>(visualItem);
1574 if (complexItem) {
1579 connect(complexItem, &ComplexMissionItem::isIncompleteChanged, this, &MissionController::_recalcFlightPathSegmentsSignal, Qt::QueuedConnection);
1580 } else {
1581 qWarning() << "ComplexMissionItem not found";
1582 }
1583 }
1584}
1585
1586void MissionController::_deinitVisualItem(VisualMissionItem* visualItem)
1587{
1588 // Disconnect only signals connected to this MissionController, not all receivers.
1589 // A full wildcard disconnect(obj, 0, 0, 0) tears out internal destroyed-signal
1590 // connections that Qt (and the tree/list models) rely on for cleanup.
1591 disconnect(visualItem, nullptr, this, nullptr);
1592}
1593
1594void MissionController::_itemCommandChanged(void)
1595{
1596 _recalcChildItems();
1598}
1599
1600void MissionController::_managerVehicleChanged(Vehicle* managerVehicle)
1601{
1602 if (_managerVehicle) {
1603 _missionManager->disconnect(this);
1604 _managerVehicle->disconnect(this);
1605 _managerVehicle = nullptr;
1606 _missionManager = nullptr;
1607 }
1608
1609 _managerVehicle = managerVehicle;
1610 if (!_managerVehicle) {
1611 qWarning() << "MissionController::managerVehicleChanged managerVehicle=NULL";
1612 return;
1613 }
1614
1615 _missionManager = _managerVehicle->missionManager();
1616 connect(_missionManager, &MissionManager::newMissionItemsAvailable, this, &MissionController::_newMissionItemsAvailableFromVehicle);
1617 connect(_missionManager, &MissionManager::sendComplete, this, &MissionController::_managerSendComplete);
1618 connect(_missionManager, &MissionManager::removeAllComplete, this, &MissionController::_managerRemoveAllComplete);
1619 connect(_missionManager, &MissionManager::inProgressChanged, this, &MissionController::_inProgressChanged);
1620 connect(_missionManager, &MissionManager::progressPctChanged, this, &MissionController::_progressPctChanged);
1621 connect(_missionManager, &MissionManager::currentIndexChanged, this, &MissionController::_currentMissionIndexChanged);
1625 connect(_managerVehicle, &Vehicle::defaultCruiseSpeedChanged, this, &MissionController::_recalcMissionFlightStatusSignal, Qt::QueuedConnection);
1626 connect(_managerVehicle, &Vehicle::defaultHoverSpeedChanged, this, &MissionController::_recalcMissionFlightStatusSignal, Qt::QueuedConnection);
1628
1631}
1632
1633void MissionController::_inProgressChanged(bool inProgress)
1634{
1635 emit syncInProgressChanged(inProgress);
1636}
1637
1638bool MissionController::_findPreviousAltitude(int newIndex, double* prevAltitude, QGroundControlQmlGlobal::AltitudeFrame* prevAltitudeMode)
1639{
1640 bool found = false;
1641 double foundAltitude = 0;
1643
1644 if (newIndex > _visualItems->count()) {
1645 return false;
1646 }
1647 newIndex--;
1648
1649 for (int i=newIndex; i>0; i--) {
1650 VisualMissionItem* visualItem = qobject_cast<VisualMissionItem*>(_visualItems->get(i));
1651
1652 if (visualItem->specifiesCoordinate() && !visualItem->isStandaloneCoordinate()) {
1653 if (visualItem->isSimpleItem()) {
1654 SimpleMissionItem* simpleItem = qobject_cast<SimpleMissionItem*>(visualItem);
1655 if (simpleItem->specifiesAltitude()) {
1656 foundAltitude = simpleItem->altitude()->rawValue().toDouble();
1657 foundAltFrame = simpleItem->altitudeFrame();
1658 found = true;
1659 break;
1660 }
1661 }
1662 }
1663 }
1664
1665 if (found) {
1666 *prevAltitude = foundAltitude;
1667 *prevAltitudeMode = foundAltFrame;
1668 }
1669
1670 return found;
1671}
1672
1673double MissionController::_normalizeLat(double lat)
1674{
1675 // Normalize latitude to range: 0 to 180, S to N
1676 return lat + 90.0;
1677}
1678
1679double MissionController::_normalizeLon(double lon)
1680{
1681 // Normalize longitude to range: 0 to 360, W to E
1682 return lon + 180.0;
1683}
1684
1686MissionSettingsItem* MissionController::_addMissionSettings(QmlObjectListModel* visualItems)
1687{
1688 qCDebug(MissionControllerLog) << "_addMissionSettings";
1689
1691 visualItems->insert(0, settingsItem);
1692
1693 if (visualItems == _visualItems) {
1694 _settingsItem = settingsItem;
1695 }
1696
1697 return settingsItem;
1698}
1699
1701{
1702
1703 int resumeIndex = 0;
1704
1705 if (_flyView) {
1706 resumeIndex = _missionManager->lastCurrentIndex() + (_controllerVehicle->firmwarePlugin()->sendHomePositionToVehicle() ? 0 : 1);
1707 if (resumeIndex > 1 && resumeIndex != _visualItems->value<VisualMissionItem*>(_visualItems->count() - 1)->sequenceNumber()) {
1708 // Resume at the item previous to the item we were heading towards
1709 resumeIndex--;
1710 } else {
1711 resumeIndex = 0;
1712 }
1713 }
1714
1715 return resumeIndex;
1716}
1717
1719{
1720 if (!_flyView) {
1721 return -1;
1722 } else {
1723 int currentIndex = _missionManager->currentIndex();
1724 if (!_controllerVehicle->firmwarePlugin()->sendHomePositionToVehicle()) {
1725 currentIndex++;
1726 }
1727 return currentIndex;
1728 }
1729}
1730
1731void MissionController::_currentMissionIndexChanged(int sequenceNumber)
1732{
1733 if (_flyView) {
1734 if (!_controllerVehicle->firmwarePlugin()->sendHomePositionToVehicle()) {
1735 sequenceNumber++;
1736 }
1737
1738 for (int i=0; i<_visualItems->count(); i++) {
1739 VisualMissionItem* item = qobject_cast<VisualMissionItem*>(_visualItems->get(i));
1740 item->setIsCurrentItem(item->sequenceNumber() == sequenceNumber);
1741 }
1743 }
1744}
1745
1747{
1748 return _missionManager->inProgress();
1749}
1750
1752{
1753 return _visualItems ? _visualItems->dirty() : false;
1754}
1755
1756
1758{
1759 if (_visualItems) {
1760 _visualItems->setDirty(dirty);
1761 }
1762}
1763
1764void MissionController::_scanForAdditionalSettings(QmlObjectListModel* visualItems, PlanMasterController* masterController)
1765{
1766 // First we look for Landing Patterns which are at the end
1769 }
1770
1771 int scanIndex = 0;
1772 while (scanIndex < visualItems->count()) {
1773 VisualMissionItem* visualItem = visualItems->value<VisualMissionItem*>(scanIndex);
1774
1775 qCDebug(MissionControllerLog) << "MissionController::_scanForAdditionalSettings count:scanIndex" << visualItems->count() << scanIndex;
1776
1777 if (!_flyView) {
1778 MissionSettingsItem* settingsItem = qobject_cast<MissionSettingsItem*>(visualItem);
1779 if (settingsItem) {
1780 scanIndex++;
1781 settingsItem->scanForMissionSettings(visualItems, scanIndex);
1782 continue;
1783 }
1784 }
1785
1786 SimpleMissionItem* simpleItem = qobject_cast<SimpleMissionItem*>(visualItem);
1787 if (simpleItem) {
1788 scanIndex++;
1789 simpleItem->scanForSections(visualItems, scanIndex, masterController);
1790 } else {
1791 // Complex item, can't have sections
1792 scanIndex++;
1793 }
1794 }
1795}
1796
1798{
1799 return _visualItems ? _visualItems->count() > 1 : false;
1800}
1801
1803{
1804 if (_masterController->offline()) {
1805 qCCritical(MissionControllerLog) << "MissionControllerLog::removeAllFromVehicle called while offline";
1806 } else if (syncInProgress()) {
1807 qCCritical(MissionControllerLog) << "MissionControllerLog::removeAllFromVehicle called while syncInProgress";
1808 } else {
1809 _itemsRequested = true;
1810 _missionManager->removeAll();
1811 }
1812}
1813
1815{
1816 return QGCCorePlugin::instance()->complexMissionItemNames(_controllerVehicle);
1817}
1818
1820{
1821 if (!_controllerVehicle->firmwarePlugin()->sendHomePositionToVehicle()) {
1822 resumeIndex--;
1823 }
1824 _missionManager->generateResumeMission(resumeIndex);
1825}
1826
1828{
1829 if (_settingsItem) {
1830 return _settingsItem->coordinate();
1831 } else {
1832 return QGeoCoordinate();
1833 }
1834}
1835
1837{
1838 return _settingsItem && _settingsItem->coordinate().isValid();
1839}
1840
1841void MissionController::setHomePosition(QGeoCoordinate coordinate)
1842{
1843 if (_settingsItem) {
1844 _settingsItem->setCoordinate(coordinate);
1845 }
1846}
1847
1849{
1850 double defaultAltitude = _appSettings->defaultMissionItemAltitude()->rawValue().toDouble();
1851
1852 for (int i=1; i<_visualItems->count(); i++) {
1853 VisualMissionItem* item = _visualItems->value<VisualMissionItem*>(i);
1854 item->applyNewAltitude(defaultAltitude);
1855 }
1856}
1857
1858void MissionController::_progressPctChanged(double progressPct)
1859{
1860 if (!QGC::fuzzyCompare(progressPct, _progressPct)) {
1861 _progressPct = progressPct;
1863 }
1864}
1865
1866void MissionController::_visualItemsDirtyChanged(bool dirty)
1867{
1868 // We could connect signal to signal and not need this but this is handy for setting a breakpoint on
1869 emit dirtyChanged(dirty);
1870}
1871
1873{
1874 qCDebug(MissionControllerLog) << "showPlanFromManagerVehicle _flyView" << _flyView;
1875 if (_masterController->offline()) {
1876 qCCritical(MissionControllerLog) << "MissionController::showPlanFromManagerVehicle called while offline";
1877 return true; // stops further propagation of showPlanFromManagerVehicle due to error
1878 } else {
1879 if (!_managerVehicle->initialPlanRequestComplete()) {
1880 // The vehicle hasn't completed initial load, we can just wait for newMissionItemsAvailable to be signalled automatically
1881 qCDebug(MissionControllerLog) << "showPlanFromManagerVehicle: !initialPlanRequestComplete, wait for signal";
1882 return true;
1883 } else if (syncInProgress()) {
1884 // If the sync is already in progress, newMissionItemsAvailable will be signalled automatically when it is done. So no need to do anything.
1885 qCDebug(MissionControllerLog) << "showPlanFromManagerVehicle: syncInProgress wait for signal";
1886 return true;
1887 } else {
1888 // Sync has already completed, fake a _newMissionItemsAvailable with the current items
1889 qCDebug(MissionControllerLog) << "showPlanFromManagerVehicle: sync complete simulate signal";
1890 _itemsRequested = true;
1891 _newMissionItemsAvailableFromVehicle(false /* removeAllRequested */);
1892 return false;
1893 }
1894 }
1895}
1896
1897void MissionController::_managerSendComplete(bool error)
1898{
1899 // Fly view always reloads on send complete
1900 if (!error && _flyView) {
1902 }
1903}
1904
1905void MissionController::_managerRemoveAllComplete(bool error)
1906{
1907 if (!error) {
1908 // Remove all from vehicle so we always update
1910 }
1911}
1912
1913bool MissionController::_isROIBeginItem(SimpleMissionItem* simpleItem)
1914{
1915 return simpleItem->mavCommand() == MAV_CMD_DO_SET_ROI_LOCATION ||
1916 simpleItem->mavCommand() == MAV_CMD_DO_SET_ROI_WPNEXT_OFFSET ||
1917 (simpleItem->mavCommand() == MAV_CMD_DO_SET_ROI &&
1918 static_cast<int>(simpleItem->missionItem().param1()) == MAV_ROI_LOCATION);
1919}
1920
1921bool MissionController::_isROICancelItem(SimpleMissionItem* simpleItem)
1922{
1923 return simpleItem->mavCommand() == MAV_CMD_DO_SET_ROI_NONE ||
1924 (simpleItem->mavCommand() == MAV_CMD_DO_SET_ROI &&
1925 static_cast<int>(simpleItem->missionItem().param1()) == MAV_ROI_NONE);
1926}
1927
1928void MissionController::setCurrentPlanViewSeqNum(int sequenceNumber, bool force)
1929{
1930 if (_visualItems && (force || sequenceNumber != _currentPlanViewSeqNum)) {
1931 qCDebug(MissionControllerLog) << "setCurrentPlanViewSeqNum";
1932 bool foundLand = false;
1933 bool onlyInsertTakeoffValid = false;
1934 int takeoffSeqNum = -1;
1935 int landSeqNum = -1;
1936 int lastFlyThroughSeqNum = -1;
1937
1938 _splitSegment = nullptr;
1939 _currentPlanViewItem = nullptr;
1940 _currentPlanViewSeqNum = -1;
1941 _currentPlanViewVIIndex = -1;
1942 _isInsertTakeoffValid = true;
1943 _isInsertLandValid = true;
1944 _isInsertROIValid = false;
1945 _isROIActive = false;
1946 _isROIBeginCurrentItem = false;
1947 _flyThroughCommandsAllowed = true;
1948 _previousCoordinate = QGeoCoordinate();
1949
1950 bool noItemsAddedYet = _visualItems->count() == 1;
1951 if (_masterController->controllerVehicle()->supports()->takeoffMissionCommand() && !_planViewSettings->takeoffItemNotRequired()->rawValue().toBool() && noItemsAddedYet) {
1952 onlyInsertTakeoffValid = true;
1953 }
1954
1955 for (int viIndex=0; viIndex<_visualItems->count(); viIndex++) {
1956 VisualMissionItem* pVI = qobject_cast<VisualMissionItem*>(_visualItems->get(viIndex));
1957 SimpleMissionItem* simpleItem = qobject_cast<SimpleMissionItem*>(pVI);
1958 int currentSeqNumber = pVI->sequenceNumber();
1959
1960 if (sequenceNumber != 0 && currentSeqNumber <= sequenceNumber) {
1961 if (pVI->specifiesCoordinate() && !pVI->isStandaloneCoordinate()) {
1962 // Coordinate based flight commands prior to where the takeoff would be inserted
1963 _isInsertTakeoffValid = false;
1964 }
1965 }
1966
1967 if (qobject_cast<TakeoffMissionItem*>(pVI)) {
1968 takeoffSeqNum = currentSeqNumber;
1969 _isInsertTakeoffValid = false;
1970 }
1971
1972 if (!foundLand) {
1973 if (simpleItem) {
1974 switch (simpleItem->mavCommand()) {
1975 case MAV_CMD_NAV_LAND:
1976 case MAV_CMD_NAV_VTOL_LAND:
1977 case MAV_CMD_DO_LAND_START:
1978 case MAV_CMD_NAV_RETURN_TO_LAUNCH:
1979 foundLand = true;
1980 landSeqNum = currentSeqNumber;
1981 break;
1982 default:
1983 break;
1984 }
1985 } else {
1986 FixedWingLandingComplexItem* fwLanding = qobject_cast<FixedWingLandingComplexItem*>(pVI);
1987 if (fwLanding) {
1988 foundLand = true;
1989 landSeqNum = currentSeqNumber;
1990 }
1991 }
1992 }
1993
1994 if (simpleItem) {
1995 // Remember previous coordinate
1996 if (currentSeqNumber < sequenceNumber && simpleItem->specifiesCoordinate() && !simpleItem->isStandaloneCoordinate()) {
1997 _previousCoordinate = simpleItem->coordinate();
1998 }
1999
2000 // ROI state handling
2001 if (currentSeqNumber <= sequenceNumber) {
2002 if (_isROIActive) {
2003 if (_isROICancelItem(simpleItem)) {
2004 _isROIActive = false;
2005 }
2006 } else {
2007 if (_isROIBeginItem(simpleItem)) {
2008 _isROIActive = true;
2009 }
2010 }
2011 }
2012 if (currentSeqNumber == sequenceNumber && _isROIBeginItem(simpleItem)) {
2013 _isROIBeginCurrentItem = true;
2014 }
2015 }
2016
2017 if (viIndex != 0) {
2018 // Complex items are assumed to be fly through
2019 if (!simpleItem || (simpleItem->specifiesCoordinate() && !simpleItem->isStandaloneCoordinate())) {
2020 lastFlyThroughSeqNum = currentSeqNumber;
2021 }
2022 }
2023
2024 if (currentSeqNumber == sequenceNumber) {
2025 pVI->setIsCurrentItem(true);
2026 pVI->setHasCurrentChildItem(false);
2027
2028 _currentPlanViewItem = pVI;
2029 _currentPlanViewSeqNum = sequenceNumber;
2030 _currentPlanViewVIIndex = viIndex;
2031
2032 if (pVI->specifiesCoordinate()) {
2033 if (!pVI->isStandaloneCoordinate()) {
2034 // Determine split segment used to display line split editing ui.
2035 for (int j=viIndex-1; j>0; j--) {
2036 VisualMissionItem* pPrev = qobject_cast<VisualMissionItem*>(_visualItems->get(j));
2037 if (pPrev->specifiesCoordinate() && !pPrev->isStandaloneCoordinate()) {
2038 VisualItemPair splitPair(pPrev, pVI);
2039 if (_flightPathSegmentHashTable.contains(splitPair)) {
2040 _splitSegment = _flightPathSegmentHashTable[splitPair];
2041 } else {
2042 // The recalc of flight path segments hasn't happened yet since it is delayed and compressed.
2043 // So we need to register the fact that we need a split segment update and it will happen in the recalc instead.
2044 _delayedSplitSegmentUpdate = true;
2045 }
2046 break;
2047 }
2048 }
2049 }
2050 } else if (pVI->parentItem()) {
2051 pVI->parentItem()->setHasCurrentChildItem(true);
2052 }
2053 } else {
2054 pVI->setIsCurrentItem(false);
2055 // Child items will always be later in the mission sequence, so this should be correct, to be sure the HasCurrentChildItem is cleared
2056 pVI->setHasCurrentChildItem(false);
2057 }
2058 }
2059
2060 if (takeoffSeqNum != -1) {
2061 // Takeoff item was found which means mission starts from ground
2062 if (sequenceNumber < takeoffSeqNum) {
2063 // Land is only valid after the takeoff item.
2064 _isInsertLandValid = false;
2065 // Fly through commands are not allowed prior to the takeoff command
2066 _flyThroughCommandsAllowed = false;
2067 }
2068 }
2069
2070 if (lastFlyThroughSeqNum != -1) {
2071 // Land item must be after any fly through coordinates
2072 if (sequenceNumber < lastFlyThroughSeqNum) {
2073 _isInsertLandValid = false;
2074 }
2075 }
2076
2077 if (foundLand && !multipleLandPatternsAllowed()) {
2078 _isInsertLandValid = false;
2079 if (sequenceNumber >= landSeqNum) {
2080 // Can't have fly through commands after a land item
2081 _flyThroughCommandsAllowed = false;
2082 }
2083 }
2084
2085 if (_hasLandItem != foundLand) {
2086 // Update hasLandItem if the presence of a landing item has changed
2087 _hasLandItem = foundLand;
2088 emit hasLandItemChanged();
2089 }
2090
2091 // These are not valid when only takeoff is allowed
2092 _isInsertLandValid = _isInsertLandValid && !onlyInsertTakeoffValid;
2093 _flyThroughCommandsAllowed = _flyThroughCommandsAllowed && !onlyInsertTakeoffValid;
2094
2095 // Nothing can be inserted until the home position has been set
2096 const bool homePosSet = homePositionSet();
2097 _isInsertTakeoffValid = _isInsertTakeoffValid && homePosSet;
2098 _isInsertLandValid = _isInsertLandValid && homePosSet;
2099 _flyThroughCommandsAllowed = _flyThroughCommandsAllowed && homePosSet;
2100 _isInsertROIValid = homePosSet && !onlyInsertTakeoffValid;
2101
2102 // These properties are all recomputed together above, so a single signal is sufficient.
2103 // QML property bindings list planViewStateChanged as their NOTIFY signal which means
2104 // one emit re-evaluates all dependent bindings in one pass instead of many separate updates.
2105 // splitSegmentChanged is kept separate because PlanView.qml has an explicit onSplitSegmentChanged handler.
2106 emit planViewStateChanged();
2107 emit splitSegmentChanged();
2108 }
2109}
2110
2111void MissionController::repositionMission(const QGeoCoordinate& newHome,
2112 bool repositionTakeoffItems,
2113 bool repositionLandingItems)
2114{
2115 if (!newHome.isValid()) {
2116 qCWarning(MissionControllerLog) << "Cannot reposition mission to an invalid coordinate";
2117 return;
2118 }
2119
2120 if (!_settingsItem || !_settingsItem->coordinate().isValid()) {
2121 qCWarning(MissionControllerLog) << "Cannot reposition mission while home is invalid";
2122 return;
2123 }
2124
2125 const QGeoCoordinate oldHome = _settingsItem->coordinate();
2126
2127 for (int i = 0; i < _visualItems->count(); ++i) {
2128 auto* item = qobject_cast<VisualMissionItem*>(_visualItems->get(i));
2129 if (!item || !item->specifiesCoordinate() || item->isStandaloneCoordinate()) {
2130 continue;
2131 }
2132
2133 if ((!repositionTakeoffItems && item->isTakeoffItem()) ||
2134 (!repositionLandingItems && item->isLandCommand())) {
2135 continue;
2136 }
2137
2138 const QGeoCoordinate oldCoord = item->coordinate();
2139 if (!oldCoord.isValid()) {
2140 continue;
2141 }
2142
2143 const double distanceMeters = oldHome.distanceTo(oldCoord);
2144 const double azimuthDegrees = oldHome.azimuthTo(oldCoord);
2145
2146 QGeoCoordinate newCoord = newHome.atDistanceAndAzimuth(distanceMeters, azimuthDegrees);
2147 item->setCoordinate(newCoord);
2148 }
2149
2150 setDirty(true);
2151}
2152
2154 double northMeters,
2155 double upMeters,
2156 bool offsetTakeoffItems,
2157 bool offsetLandingItems)
2158{
2159 if (!qFuzzyIsNull(eastMeters) || !qFuzzyIsNull(northMeters)) {
2160 const double distanceMeters = qSqrt(eastMeters * eastMeters + northMeters * northMeters);
2161 double azimuthDegrees = 0.0;
2162 if (!qFuzzyIsNull(distanceMeters)) {
2163 const double azimuthRadians = qAtan2(eastMeters, northMeters);
2164 azimuthDegrees = qRadiansToDegrees(azimuthRadians);
2165 }
2166
2167 for (int i = 0; i < _visualItems->count(); ++i) {
2168 auto* item = qobject_cast<VisualMissionItem*>(_visualItems->get(i));
2169 if (!item || !item->specifiesCoordinate() || item->isStandaloneCoordinate()) {
2170 continue;
2171 }
2172
2173 if ((!offsetTakeoffItems && item->isTakeoffItem()) ||
2174 (!offsetLandingItems && item->isLandCommand())) {
2175 continue;
2176 }
2177
2178 const QGeoCoordinate oldCoord = item->coordinate();
2179 if (!oldCoord.isValid()) {
2180 continue;
2181 }
2182
2183 item->setCoordinate(oldCoord.atDistanceAndAzimuth(distanceMeters, azimuthDegrees));
2184 }
2185
2186 setDirty(true);
2187 }
2188
2189 if (!qFuzzyIsNull(upMeters)) {
2190 for (int i = 0; i < _visualItems->count(); ++i) {
2191 auto* item = qobject_cast<VisualMissionItem*>(_visualItems->get(i));
2192 if (!item || item == _settingsItem) {
2193 continue;
2194 }
2195
2196 if ((!offsetTakeoffItems && item->isTakeoffItem()) ||
2197 (!offsetLandingItems && item->isLandCommand())) {
2198 continue;
2199 }
2200
2201 if (!item->specifiesCoordinate() && !item->specifiesAltitudeOnly()) {
2202 continue;
2203 }
2204
2205 if (!qIsNaN(item->editableAlt())) {
2206 item->applyNewAltitude(item->editableAlt() + upMeters);
2207 }
2208 }
2209
2210 setDirty(true);
2211 }
2212}
2213
2215 bool rotateTakeoffItems,
2216 bool rotateLandingItems)
2217{
2218 if (qFuzzyIsNull(degreesCW)) {
2219 return;
2220 }
2221
2222 if (!_settingsItem || !_settingsItem->coordinate().isValid()) {
2223 qCWarning(MissionControllerLog) << "Cannot rotate mission while home is invalid";
2224 return;
2225 }
2226
2227 const QGeoCoordinate home = _settingsItem->coordinate();
2228
2229 for (int i = 0; i < _visualItems->count(); ++i) {
2230 auto* item = qobject_cast<VisualMissionItem*>(_visualItems->get(i));
2231 if (!item || !item->specifiesCoordinate() || item->isStandaloneCoordinate()) {
2232 continue;
2233 }
2234
2235 if ((!rotateTakeoffItems && item->isTakeoffItem()) ||
2236 (!rotateLandingItems && item->isLandCommand())) {
2237 continue;
2238 }
2239
2240 const QGeoCoordinate oldCoord = item->coordinate();
2241 if (!oldCoord.isValid()) {
2242 continue;
2243 }
2244
2245 const double distanceMeters = home.distanceTo(oldCoord);
2246 if (qFuzzyIsNull(distanceMeters)) {
2247 continue;
2248 }
2249 const double azimuthDegrees = home.azimuthTo(oldCoord);
2250
2251 QGeoCoordinate newCoord = home.atDistanceAndAzimuth(distanceMeters, azimuthDegrees + degreesCW);
2252 item->setCoordinate(newCoord);
2253 }
2254
2255 setDirty(true);
2256}
2257
2258void MissionController::_updateTimeout()
2259{
2260 QGeoCoordinate firstCoordinate;
2261 QGeoCoordinate takeoffCoordinate;
2262 QGCGeoBoundingCube boundingCube;
2263 double north = 0.0;
2264 double south = 180.0;
2265 double east = 0.0;
2266 double west = 360.0;
2267 double minAlt = QGCGeoBoundingCube::MaxAlt;
2268 double maxAlt = QGCGeoBoundingCube::MinAlt;
2269 for (int i = 1; i < _visualItems->count(); i++) {
2270 VisualMissionItem* item = qobject_cast<VisualMissionItem*>(_visualItems->get(i));
2271 if(item->isSimpleItem()) {
2272 SimpleMissionItem* pSimpleItem = qobject_cast<SimpleMissionItem*>(item);
2273 if(pSimpleItem) {
2274 switch(pSimpleItem->command()) {
2275 case MAV_CMD_NAV_TAKEOFF:
2276 case MAV_CMD_NAV_WAYPOINT:
2277 case MAV_CMD_NAV_LAND:
2278 if(pSimpleItem->coordinate().isValid()) {
2279 double alt = 0.0;
2280 if (!pSimpleItem->altitude()->rawValue().isNull() && !qIsNaN(pSimpleItem->altitude()->rawValue().toDouble())) {
2281 alt = pSimpleItem->altitude()->rawValue().toDouble();
2282 }
2283 if((MAV_CMD)pSimpleItem->command() == MAV_CMD_NAV_TAKEOFF) {
2284 takeoffCoordinate = pSimpleItem->coordinate();
2285 takeoffCoordinate.setAltitude(alt);
2286 minAlt = maxAlt = alt;
2287 } else if(!firstCoordinate.isValid()) {
2288 firstCoordinate = pSimpleItem->coordinate();
2289 }
2290 double lat = pSimpleItem->coordinate().latitude() + 90.0;
2291 double lon = pSimpleItem->coordinate().longitude() + 180.0;
2292 north = fmax(north, lat);
2293 south = fmin(south, lat);
2294 east = fmax(east, lon);
2295 west = fmin(west, lon);
2296 minAlt = fmin(minAlt, alt);
2297 maxAlt = fmax(maxAlt, alt);
2298 }
2299 break;
2300 default:
2301 break;
2302 }
2303 }
2304 } else {
2305 ComplexMissionItem* pComplexItem = qobject_cast<ComplexMissionItem*>(item);
2306 if(pComplexItem) {
2307 QGCGeoBoundingCube bc = pComplexItem->boundingCube();
2308 if(bc.isValid()) {
2309 if(!firstCoordinate.isValid() && pComplexItem->entryCoordinate().isValid()) {
2310 firstCoordinate = pComplexItem->entryCoordinate();
2311 }
2312 north = fmax(north, bc.pointNW.latitude() + 90.0);
2313 south = fmin(south, bc.pointSE.latitude() + 90.0);
2314 east = fmax(east, bc.pointNW.longitude() + 180.0);
2315 west = fmin(west, bc.pointSE.longitude() + 180.0);
2316 minAlt = fmin(minAlt, bc.pointNW.altitude());
2317 maxAlt = fmax(maxAlt, bc.pointSE.altitude());
2318 }
2319 }
2320 }
2321 }
2322 //-- Figure out where this thing is taking off from
2323 if(!takeoffCoordinate.isValid()) {
2324 if(firstCoordinate.isValid()) {
2325 takeoffCoordinate = firstCoordinate;
2326 } else {
2328 }
2329 }
2330 //-- Build bounding "cube"
2331 boundingCube = QGCGeoBoundingCube(
2332 QGeoCoordinate(north - 90.0, west - 180.0, minAlt),
2333 QGeoCoordinate(south - 90.0, east - 180.0, maxAlt));
2334 if(_travelBoundingCube != boundingCube || _takeoffCoordinate != takeoffCoordinate) {
2335 _takeoffCoordinate = takeoffCoordinate;
2336 _travelBoundingCube = boundingCube;
2338 qCDebug(MissionControllerLog) << "Bounding cube:" << _travelBoundingCube.pointNW << _travelBoundingCube.pointSE;
2339 }
2340}
2341
2342void MissionController::_complexBoundingBoxChanged()
2343{
2344 _updateTimer.start(UPDATE_TIMEOUT);
2345}
2346
2348 const int count = _visualItems->count();
2349
2350 for (int i = 0; i < count; i++) {
2351 QObject *obj = _visualItems->get(i);
2352
2353 LandingComplexItem *landingItem = qobject_cast<LandingComplexItem *>(obj);
2354 if (landingItem) {
2355 return landingItem == item;
2356 }
2357 }
2358
2359 return false;
2360}
2361
2363{
2364 return _visualItems->count() <= 1;
2365}
2366
2367void MissionController::_recalcPlanViewState(void)
2368{
2369 setCurrentPlanViewSeqNum(_currentPlanViewSeqNum, true /* force */);
2370}
2371
2372void MissionController::_allItemsRemoved(void)
2373{
2374 // When there are no mission items we track changes to firmware/vehicle type. This allows a vehicle connection
2375 // to adjust these items.
2376 _controllerVehicle->trackFirmwareVehicleTypeChanges();
2377}
2378
2379void MissionController::_firstItemAdded(void)
2380{
2381 // As soon as the first item is added we lock the firmware/vehicle type to current values. So if you then connect a vehicle
2382 // it will not affect these values.
2383 _controllerVehicle->stopTrackingFirmwareVehicleTypeChanges();
2384}
2385
2387{
2388 if (_managerVehicle->isOfflineEditingVehicle()) {
2390 }
2391 if (_managerVehicle->armed() && _managerVehicle->flightMode() == _managerVehicle->missionFlightMode()) {
2393 }
2394 if (_controllerVehicle->firmwareType() != _managerVehicle->firmwareType() || QGCMAVLink::vehicleClass(_controllerVehicle->vehicleType()) != QGCMAVLink::vehicleClass(_managerVehicle->vehicleType())) {
2396 }
2398}
2399
2404
2413
2415{
2416 if (_globalAltFrame != altFrame) {
2417 _globalAltFrame = altFrame;
2419 }
2420}
#define UPDATE_TIMEOUT
How often we check for bounding box changes.
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:134
QVariant rawValue() const
Value after translation.
Definition Fact.h:85
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)
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)
void reset(Vehicle *controllerVehicle, Vehicle *managerVehicle, bool missionContainsVTOLTakeoff)
Resets the flight status fields to defaults based on vehicle properties.
const MissionFlightStatus_t & status() const
void recalc(QmlObjectListModel *visualItems, MissionSettingsItem *settingsItem, Vehicle *controllerVehicle, Vehicle *managerVehicle, AppSettings *appSettings, PlanViewSettings *planViewSettings, bool missionContainsVTOLTakeoff)
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)
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)
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:498
void defaultHoverSpeedChanged(double hoverSpeed)
void defaultCruiseSpeedChanged(double cruiseSpeed)
QString missionFlightMode() const
Definition Vehicle.cc:2536
void _offlineVehicleTypeSettingChanged(QVariant varVehicleType)
Definition Vehicle.cc:487
QString flightMode() const
Definition Vehicle.cc:1466
bool vtol() const
Definition Vehicle.cc:1761
bool rover() const
Definition Vehicle.cc:1741
MAV_TYPE vehicleType() const
Definition Vehicle.h:432
FirmwarePlugin * firmwarePlugin()
Provides access to the Firmware Plugin for this Vehicle.
Definition Vehicle.h:448
bool isOfflineEditingVehicle() const
Definition Vehicle.h:514
MAV_AUTOPILOT firmwareType() const
Definition Vehicle.h:431
void _offlineFirmwareTypeSettingChanged(QVariant varFirmwareType)
Definition Vehicle.cc:474
void trackFirmwareVehicleTypeChanges(void)
Definition Vehicle.cc:212
double defaultHoverSpeed() const
Definition Vehicle.h:529
void stopTrackingFirmwareVehicleTypeChanges(void)
Definition Vehicle.cc:221
bool initialPlanRequestComplete() const
Definition Vehicle.h:715
bool fixedWing() const
Definition Vehicle.cc:1736
double defaultCruiseSpeed() const
Definition Vehicle.h:528
bool armed() const
Definition Vehicle.h:453
VehicleSupports * supports()
Definition Vehicle.h:406
MissionManager * missionManager()
Definition Vehicle.h:574
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:12
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:109
int batteriesRequired
-1 for not supported
int batteryChangePoint
-1 for not supported, 0 for not needed
static constexpr VehicleClass_t VehicleClassGeneric