QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
PlanMasterController.cc
Go to the documentation of this file.
2#include "AppMessages.h"
3#include "QGCCorePlugin.h"
5#include "Vehicle.h"
7#include "SettingsManager.h"
8#include "AppSettings.h"
9#include "JsonParsing.h"
10#include "MissionManager.h"
11#include "KMLPlanDomDocument.h"
12#include "PlanCreator.h"
13#include "QmlObjectListModel.h"
14#include "GeoFenceManager.h"
15#include "RallyPointManager.h"
16#include "QGCLoggingCategory.h"
17
18#include <QtCore/QFileInfo>
19#include <QtCore/QJsonDocument>
20
21QGC_LOGGING_CATEGORY(PlanMasterControllerLog, "PlanManager.PlanMasterController")
22
24 : QObject (parent)
25 , _multiVehicleMgr (MultiVehicleManager::instance())
26 , _controllerVehicle (new Vehicle(Vehicle::MAV_AUTOPILOT_TRACK, Vehicle::MAV_TYPE_TRACK, this))
27 , _managerVehicle (_controllerVehicle)
28 , _missionController (this)
29 , _geoFenceController (this)
30 , _rallyPointController (this)
31{
32 _commonInit();
33}
34
35#ifdef QGC_UNITTEST_BUILD
36PlanMasterController::PlanMasterController(MAV_AUTOPILOT firmwareType, MAV_TYPE vehicleType, QObject* parent)
37 : QObject (parent)
38 , _multiVehicleMgr (MultiVehicleManager::instance())
39 , _controllerVehicle (new Vehicle(firmwareType, vehicleType))
40 , _managerVehicle (_controllerVehicle)
41 , _missionController (this)
42 , _geoFenceController (this)
43 , _rallyPointController (this)
44{
45 _commonInit();
46}
47#endif
48
49void PlanMasterController::_commonInit(void)
50{
51 connect(&_missionController, &MissionController::dirtyChanged, this, &PlanMasterController::_updateOverallDirty);
52 connect(&_geoFenceController, &GeoFenceController::dirtyChanged, this, &PlanMasterController::_updateOverallDirty);
53 connect(&_rallyPointController, &RallyPointController::dirtyChanged, this, &PlanMasterController::_updateOverallDirty);
54
58
59 connect(this, &PlanMasterController::containsItemsChanged, this, &PlanMasterController::_updateShowCreateFromTemplate);
60
64
65 // Offline vehicle can change firmware/vehicle type
66 connect(_controllerVehicle, &Vehicle::vehicleTypeChanged, this, &PlanMasterController::_updatePlanCreatorsList);
67}
68
69
74
76{
77 _missionController.start (_flyView);
78 _geoFenceController.start (_flyView);
79 _rallyPointController.start (_flyView);
80
81 _activeVehicleChanged(_multiVehicleMgr->activeVehicle());
82 connect(_multiVehicleMgr, &MultiVehicleManager::activeVehicleChanged, this, &PlanMasterController::_activeVehicleChanged);
83
84 _updatePlanCreatorsList();
85}
86
87void PlanMasterController::startStaticActiveVehicle(Vehicle* vehicle, bool deleteWhenSendCompleted)
88{
89 _flyView = true;
90 _deleteWhenSendCompleted = deleteWhenSendCompleted;
91 _missionController.start(_flyView);
92 _geoFenceController.start(_flyView);
93 _rallyPointController.start(_flyView);
94 _activeVehicleChanged(vehicle);
95}
96
97void PlanMasterController::_activeVehicleChanged(Vehicle* activeVehicle)
98{
99 if (_managerVehicle == activeVehicle) {
100 // We are already setup for this vehicle
101 return;
102 }
103
104 qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged" << activeVehicle;
105
106 if (_managerVehicle) {
107 // Disconnect old vehicle. Be careful of wildcarding disconnect too much since _managerVehicle may equal _controllerVehicle
108 disconnect(_managerVehicle, &Vehicle::initialPlanRequestCompleteChanged, this, nullptr);
109 disconnect(_managerVehicle->missionManager(), nullptr, this, nullptr);
110 disconnect(_managerVehicle->geoFenceManager(), nullptr, this, nullptr);
111 disconnect(_managerVehicle->rallyPointManager(), nullptr, this, nullptr);
112
113 // Any in-flight transfer chain can never complete against the new vehicle's managers
114 _loadSequence = SyncSequence::Idle;
115 _sendSequence = SyncSequence::Idle;
116 }
117
118 bool newOffline = false;
119 if (activeVehicle == nullptr) {
120 // Since there is no longer an active vehicle we use the offline controller vehicle as the manager vehicle
121 _managerVehicle = _controllerVehicle;
122 newOffline = true;
123 } else {
124 newOffline = false;
125 _managerVehicle = activeVehicle;
126
127 // Update controllerVehicle to the currently connected vehicle
129 appSettings->offlineEditingFirmwareClass()->setRawValue(QGCMAVLink::firmwareClass(_managerVehicle->firmwareType()));
130 appSettings->offlineEditingVehicleClass()->setRawValue(QGCMAVLink::vehicleClass(_managerVehicle->vehicleType()));
131
132 // We use these signals to sequence upload and download to the multiple controller/managers
133 connect(_managerVehicle, &Vehicle::initialPlanRequestCompleteChanged, this, &PlanMasterController::_initialPlanRequestCompleteChanged);
134 connect(_managerVehicle->missionManager(), &MissionManager::newMissionItemsAvailable, this, &PlanMasterController::_loadMissionComplete);
135 connect(_managerVehicle->geoFenceManager(), &GeoFenceManager::loadComplete, this, &PlanMasterController::_loadGeoFenceComplete);
136 connect(_managerVehicle->rallyPointManager(), &RallyPointManager::loadComplete, this, &PlanMasterController::_loadRallyPointsComplete);
137 connect(_managerVehicle->missionManager(), &MissionManager::sendComplete, this, &PlanMasterController::_sendMissionComplete);
138 connect(_managerVehicle->geoFenceManager(), &GeoFenceManager::sendComplete, this, &PlanMasterController::_sendGeoFenceComplete);
139 connect(_managerVehicle->rallyPointManager(), &RallyPointManager::sendComplete, this, &PlanMasterController::_sendRallyPointsComplete);
140 }
141
142 _offline = newOffline;
143 emit offlineChanged(offline());
144 emit managerVehicleChanged(_managerVehicle);
145
146 if (_flyView) {
147 // We are in the Fly View
148 if (newOffline) {
149 // No active vehicle, clear mission
150 qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Fly View - No active vehicle, clearing stale plan";
151 removeAll();
152 } else {
153 // Fly view has changed to a new active vehicle, update to show correct mission
154 qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Fly View - New active vehicle, loading new plan from manager vehicle";
155 _showPlanFromManagerVehicle();
156 }
157 } else {
158 // We are in the Plan view.
159 if (containsItems()) {
160 // We have a plan which is from a different vehicle than the new active vehicle. By definition this plan requires and upload.
161 _setDirtyForUpload(true);
162
163 // The plan view has a stale plan in it
164 if (dirtyForSave()) {
165 // Plan is dirty, the user must decide what to do in all cases
166 qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Plan View - Previous dirty plan exists, no new active vehicle, sending promptForPlanUsageOnVehicleChange signal";
168 } else {
169 // Plan is not dirty
170 if (newOffline) {
171 // The active vehicle went away with no new active vehicle
172 qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Plan View - Previous clean plan exists, no new active vehicle, clear stale plan";
173 removeAll();
174 } else {
175 // We are transitioning from one active vehicle to another. Show the plan from the new vehicle.
176 qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Plan View - Previous clean plan exists, new active vehicle, loading from new manager vehicle";
177 _showPlanFromManagerVehicle();
178 }
179 }
180 } else {
181 // There is no previous Plan in the view
182 _setDirtyStates(false, false);
183 if (newOffline) {
184 // Nothing special to do in this case
185 qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Plan View - No previous plan, no longer connected to vehicle, nothing to do";
186 } else {
187 // Just show the plan from the new vehicle
188 qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Plan View - No previous plan, new active vehicle, loading from new manager vehicle";
189 _showPlanFromManagerVehicle();
190 }
191 }
192 }
193
194 // Vehicle changed so we need to signal everything
199
200 _updatePlanCreatorsList();
201}
202
204{
205 SharedLinkInterfacePtr sharedLink = _managerVehicle->vehicleLinkManager()->primaryLink().lock();
206 if (sharedLink) {
207 if (sharedLink->linkConfiguration()->isHighLatency()) {
208 QGC::showAppMessage(tr("Download not supported on high latency links."));
209 return;
210 }
211 } else {
212 // Vehicle is shutting down
213 return;
214 }
215
216 if (offline()) {
217 qCCritical(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle called while offline";
218 } else if (_flyView) {
219 qCCritical(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle called from Fly view";
220 } else if (syncInProgress()) {
221 qCCritical(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle called while syncInProgress";
222 } else {
223 _loadSequence = SyncSequence::Mission;
224 qCDebug(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle calling _missionController.loadFromVehicle";
225 _missionController.loadFromVehicle();
226 }
227}
228
229
230void PlanMasterController::_loadMissionComplete(void)
231{
232 if (_loadSequence != SyncSequence::Mission) {
233 return;
234 }
235 _loadSequence = SyncSequence::GeoFence;
236 if (_geoFenceController.supported()) {
237 qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadMissionComplete calling _geoFenceController.loadFromVehicle";
238 _geoFenceController.loadFromVehicle();
239 } else {
240 qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadMissionComplete GeoFence not supported skipping";
241 _geoFenceController.removeAll();
242 _loadGeoFenceComplete();
243 }
244}
245
246void PlanMasterController::_loadGeoFenceComplete(void)
247{
248 if (_loadSequence != SyncSequence::GeoFence) {
249 return;
250 }
251 _loadSequence = SyncSequence::RallyPoints;
252 if (_rallyPointController.supported()) {
253 qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadGeoFenceComplete calling _rallyPointController.loadFromVehicle";
254 _rallyPointController.loadFromVehicle();
255 } else {
256 qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadGeoFenceComplete Rally Points not supported skipping";
257 _rallyPointController.removeAll();
258 _loadRallyPointsComplete();
259 }
260}
261
262void PlanMasterController::_loadRallyPointsComplete(void)
263{
264 if (_loadSequence != SyncSequence::RallyPoints) {
265 return;
266 }
267 _loadSequence = SyncSequence::Idle;
268 qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadRallyPointsComplete";
269 // A plan just downloaded from the vehicle reflects exactly what is on the vehicle.
270 // The user has made no edits, so it must not be dirty for save or upload. Any previous
271 // file association no longer describes the editor contents.
272 _clearCurrentPlanFile();
273 _setDirtyStates(false /* dirtyForSave */, false /* dirtyForUpload */);
274}
275
276void PlanMasterController::_sendMissionComplete(void)
277{
278 if (_sendSequence != SyncSequence::Mission) {
279 return;
280 }
281 _sendSequence = SyncSequence::GeoFence;
282 if (_geoFenceController.supported()) {
283 qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle start GeoFence sendToVehicle";
284 _geoFenceController.sendToVehicle();
285 } else {
286 qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle GeoFence not supported skipping";
287 _sendGeoFenceComplete();
288 }
289}
290
291void PlanMasterController::_sendGeoFenceComplete(void)
292{
293 if (_sendSequence != SyncSequence::GeoFence) {
294 return;
295 }
296 _sendSequence = SyncSequence::RallyPoints;
297 if (_rallyPointController.supported()) {
298 qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle start rally sendToVehicle";
299 _rallyPointController.sendToVehicle();
300 } else {
301 qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle Rally Points not support skipping";
302 _sendRallyPointsComplete();
303 }
304}
305
306void PlanMasterController::_sendRallyPointsComplete(void)
307{
308 if (_sendSequence != SyncSequence::RallyPoints) {
309 return;
310 }
311 _sendSequence = SyncSequence::Idle;
312 qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle Rally Point send complete";
313 _setDirtyForUpload(false);
314 if (_deleteWhenSendCompleted) {
315 this->deleteLater();
316 }
317}
318
320{
321 SharedLinkInterfacePtr sharedLink = _managerVehicle->vehicleLinkManager()->primaryLink().lock();
322 if (sharedLink) {
323 if (sharedLink->linkConfiguration()->isHighLatency()) {
324 QGC::showAppMessage(tr("Upload not supported on high latency links."));
325 return;
326 }
327 } else {
328 // Vehicle is shutting down
329 return;
330 }
331
332 if (offline()) {
333 qCCritical(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle called while offline";
334 } else if (syncInProgress()) {
335 qCCritical(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle called while syncInProgress";
336 } else {
337 qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle start mission sendToVehicle";
338 _sendSequence = SyncSequence::Mission;
339 _missionController.sendToVehicle();
340 }
341}
342
343void PlanMasterController::loadFromFile(const QString& filename)
344{
345 QString errorString;
346 QString errorMessage = tr("Error loading Plan file (%1). %2").arg(filename).arg("%1");
347
348 if (filename.isEmpty()) {
349 return;
350 }
351
352 QFileInfo fileInfo(filename);
353 QFile file(filename);
354
355 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
356 errorString = file.errorString() + QStringLiteral(" ") + filename;
357 QGC::showAppMessage(errorMessage.arg(errorString));
358 return;
359 }
360
361 bool success = false;
362 if (fileInfo.suffix() == AppSettings::waypointsFileExtension || fileInfo.suffix() == QStringLiteral("txt")) {
363 success = _missionController.loadTextFile(file, errorString);
364 } else {
365 success = _loadPlanJson(file.readAll(), errorString);
366 }
367 if (!success) {
368 QGC::showAppMessage(errorMessage.arg(errorString));
369 }
370
371 if (success){
372 _currentPlanFile = QString::asprintf("%s/%s.%s", fileInfo.path().toLocal8Bit().data(), fileInfo.completeBaseName().toLocal8Bit().data(), AppSettings::planFileExtension);
374 _setDirtyStates(false /* dirtyForSave */, true /* dirtyForUpload */);
375 } else {
376 _clearCurrentPlanFile();
377 }
378}
379
380bool PlanMasterController::_loadPlanJson(const QByteArray& bytes, QString& errorString)
381{
382 QJsonDocument jsonDoc;
383 if (!JsonParsing::isJsonFile(bytes, jsonDoc, errorString)) {
384 return false;
385 }
386
387 QJsonObject json = jsonDoc.object();
388 //-- Allow plugins to pre process the load
390
391 int version;
393 return false;
394 }
395
396 const QList<JsonParsing::KeyValidateInfo> rgKeyInfo = {
397 { kJsonMissionObjectKey, QJsonValue::Object, true },
398 { kJsonGeoFenceObjectKey, QJsonValue::Object, true },
399 { kJsonRallyPointsObjectKey, QJsonValue::Object, true },
400 };
401 if (!JsonParsing::validateKeys(json, rgKeyInfo, errorString)) {
402 return false;
403 }
404
405 if (!_missionController.load(json[kJsonMissionObjectKey].toObject(), errorString) ||
406 !_geoFenceController.load(json[kJsonGeoFenceObjectKey].toObject(), errorString) ||
407 !_rallyPointController.load(json[kJsonRallyPointsObjectKey].toObject(), errorString)) {
408 return false;
409 }
410
411 //-- Allow plugins to post process the load
413 return true;
414}
415
417{
418 QJsonObject planJson;
419 QGCCorePlugin::instance()->preSaveToJson(this, planJson);
420 QJsonObject missionJson;
421 QJsonObject fenceJson;
422 QJsonObject rallyJson;
424 //-- Allow plugin to preemptly add its own keys to mission
425 QGCCorePlugin::instance()->preSaveToMissionJson(this, missionJson);
426 _missionController.save(missionJson);
427 //-- Allow plugin to add its own keys to mission
429 _geoFenceController.save(fenceJson);
430 _rallyPointController.save(rallyJson);
431 planJson[kJsonMissionObjectKey] = missionJson;
432 planJson[kJsonGeoFenceObjectKey] = fenceJson;
433 planJson[kJsonRallyPointsObjectKey] = rallyJson;
434 QGCCorePlugin::instance()->postSaveToJson(this, planJson);
435 return QJsonDocument(planJson);
436}
437
438bool
440{
441 if (!_currentPlanFile.isEmpty()) {
442 const bool saveSuccess = saveToFile(_currentPlanFile);
443 return saveSuccess;
444 }
445
446 return false;
447}
448
449bool PlanMasterController::saveToFile(const QString& filename)
450{
451 if (filename.isEmpty()) {
452 return false;
453 }
454
455 QString planFilename = filename;
456 if (!QFileInfo(filename).fileName().contains(".")) {
457 planFilename += QString(".%1").arg(fileExtension());
458 }
459
460 QFile file(planFilename);
461
462 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
463 QGC::showAppMessage(tr("Plan save error %1 : %2").arg(filename).arg(file.errorString()));
464 return false;
465 } else {
466 const QByteArray saveBytes = saveToJson().toJson();
467 const qint64 bytesWritten = file.write(saveBytes);
468 if (bytesWritten != saveBytes.size()) {
469 QGC::showAppMessage(tr("Plan save error %1 : %2").arg(filename).arg(file.errorString()));
470 return false;
471 }
472 if(_currentPlanFile != planFilename) {
473 _currentPlanFile = planFilename;
475 }
476 _setDirtyForSave(false);
477 }
478
479 return true;
480}
481
482void PlanMasterController::saveToKml(const QString& filename)
483{
484 if (filename.isEmpty()) {
485 return;
486 }
487
488 QString kmlFilename = filename;
489 if (!QFileInfo(filename).fileName().contains(".")) {
490 kmlFilename += QString(".%1").arg(kmlFileExtension());
491 }
492
493 QFile file(kmlFilename);
494
495 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
496 QGC::showAppMessage(tr("KML save error %1 : %2").arg(filename).arg(file.errorString()));
497 } else {
498 KMLPlanDomDocument planKML;
499 _missionController.addMissionToKML(planKML);
500 QTextStream stream(&file);
501 stream << planKML.toString();
502 file.close();
503 }
504}
505
507{
508 _suppressOverallDirtyUpdate = true;
509 _missionController.removeAll();
510 _geoFenceController.removeAll();
511 _rallyPointController.removeAll();
512 _suppressOverallDirtyUpdate = false;
513
514 _setDirtyStates(false, false);
515 if (_offline) {
516 _clearCurrentPlanFile();
517 }
519}
520
522{
523 if (!offline()) {
524 _missionController.removeAllFromVehicle();
525 if (_geoFenceController.supported()) {
526 _geoFenceController.removeAllFromVehicle();
527 }
528 if (_rallyPointController.supported()) {
529 _rallyPointController.removeAllFromVehicle();
530 }
531 _setDirtyForUpload(false);
532 _clearCurrentPlanFile();
533 } else {
534 qCCritical(PlanMasterControllerLog) << "PlanMasterController::removeAllFromVehicle called while offline";
535 }
537}
538
540{
541 return _missionController.containsItems() || _geoFenceController.containsItems() || _rallyPointController.containsItems();
542}
543
544void PlanMasterController::_updateShowCreateFromTemplate(void)
545{
546 // When the plan becomes empty, always return to template-selection mode regardless
547 // of how the items were removed.
548 if (!containsItems() && _userSelectedManualCreation) {
549 _userSelectedManualCreation = false;
551 }
552 const bool show = showCreateFromTemplate();
553 if (show != _showCreateFromTemplate) {
554 _showCreateFromTemplate = show;
556 }
557}
558
563
565{
566 return _currentPlanFile.isEmpty() ? QString() : QFileInfo(_currentPlanFile).completeBaseName();
567}
568
569void PlanMasterController::_clearCurrentPlanFile()
570{
571 if (!_currentPlanFile.isEmpty()) {
572 _currentPlanFile.clear();
574 }
575}
576
581
583{
584 QStringList filters;
585
586 filters << tr("Supported types (*.%1 *.%2 *.%3)").arg(AppSettings::planFileExtension).arg(AppSettings::waypointsFileExtension).arg("txt") <<
587 tr("All Files (*)");
588 return filters;
589}
590
591
593{
594 QStringList filters;
595
596 filters << tr("Plan Files (*.%1)").arg(fileExtension()) << tr("All Files (*)");
597 return filters;
598}
599
600void PlanMasterController::sendPlanToVehicle(Vehicle* vehicle, const QString& filename)
601{
602 // Use a transient PlanMasterController to accomplish this
603 PlanMasterController* controller = new PlanMasterController();
604 controller->startStaticActiveVehicle(vehicle, true /* deleteWhenSendCompleted */);
605 controller->loadFromFile(filename);
606 controller->sendToVehicle();
607}
608
609void PlanMasterController::_showPlanFromManagerVehicle(void)
610{
611 if (!_managerVehicle->initialPlanRequestComplete()) {
612 // The sub-controllers pick up the editor contents as the initial download arrives
613 _loadSequence = SyncSequence::InitialPlanLoad;
614 return;
615 }
616
617 // The crazy if structure is to handle the load propagating by itself through the system
618 if (!_missionController.showPlanFromManagerVehicle()) {
619 if (!_geoFenceController.showPlanFromManagerVehicle()) {
620 _rallyPointController.showPlanFromManagerVehicle();
621 }
622 }
623
624 // The editor now shows the vehicle's plan: not dirty, and any previous file
625 // association no longer describes the contents
626 _clearCurrentPlanFile();
627 _setDirtyStates(false, false);
628}
629
630void PlanMasterController::_initialPlanRequestCompleteChanged(bool initialPlanRequestComplete)
631{
632 if (!initialPlanRequestComplete || _loadSequence != SyncSequence::InitialPlanLoad) {
633 return;
634 }
635 _loadSequence = SyncSequence::Idle;
636 qCDebug(PlanMasterControllerLog) << "_initialPlanRequestCompleteChanged: initial download complete";
637 // Import the manager data into the editor (non-empty sub-controllers reject the automatic
638 // manager updates during download) and scrub dirty/file association.
639 _showPlanFromManagerVehicle();
640}
641
643{
644 return _missionController.syncInProgress() ||
645 _geoFenceController.syncInProgress() ||
646 _rallyPointController.syncInProgress();
647}
648
650{
651 return _missionController.isEmpty() &&
652 _geoFenceController.isEmpty() &&
653 _rallyPointController.isEmpty();
654}
655
656void PlanMasterController::_updateOverallDirty(void)
657{
658 if (syncInProgress() || _suppressOverallDirtyUpdate) {
659 return;
660 }
661
662 const bool saveDirty = _missionController.dirty() || _geoFenceController.dirty() || _rallyPointController.dirty();
663 if (saveDirty) {
664 _setDirtyForSave(true);
665 }
666}
667
668void PlanMasterController::_setDirtyForSave(bool dirtyForSave)
669{
670 if (!dirtyForSave) {
671 _clearElementDirtyStates();
672 }
673 if (_dirtyForSave != dirtyForSave) {
674 _dirtyForSave = dirtyForSave;
675 emit dirtyForSaveChanged(_dirtyForSave);
676
677 if (_dirtyForSave) {
678 _setDirtyForUpload(true);
679 }
680 }
681}
682
683void PlanMasterController::_setDirtyForUpload(bool dirtyForUpload)
684{
685 if (_dirtyForUpload != dirtyForUpload) {
686 _dirtyForUpload = dirtyForUpload;
687 emit dirtyForUploadChanged(_dirtyForUpload);
688 }
689}
690
691void PlanMasterController::_setDirtyStates(bool dirtyForSave, bool dirtyForUpload)
692{
693 if (!dirtyForSave) {
694 _clearElementDirtyStates();
695 }
696
697 const bool saveChanged = (_dirtyForSave != dirtyForSave);
698 const bool uploadChanged = (_dirtyForUpload != dirtyForUpload);
699
700 _dirtyForSave = dirtyForSave;
701 _dirtyForUpload = dirtyForUpload;
702
703 if (saveChanged) {
704 emit dirtyForSaveChanged(_dirtyForSave);
705 }
706 if (uploadChanged) {
707 emit dirtyForUploadChanged(_dirtyForUpload);
708 }
709}
710
711// Element controllers only signal on a dirty transition, so they must be clean whenever the plan is clean for save
712void PlanMasterController::_clearElementDirtyStates()
713{
714 _suppressOverallDirtyUpdate = true;
715 _missionController.setDirty(false);
716 _geoFenceController.setDirty(false);
717 _rallyPointController.setDirty(false);
718 _suppressOverallDirtyUpdate = false;
719}
720
721void PlanMasterController::_updatePlanCreatorsList(void)
722{
723 if (_flyView) {
724 return;
725 }
726
727 const auto vehicleClass = _managerVehicle->vehicleClass();
728
729 // Only rebuild if the vehicle class actually changed
730 if (_planCreators && _planCreatorsVehicleClass == vehicleClass) {
731 return;
732 }
733
734 if (!_planCreators) {
735 _planCreators = new QmlObjectListModel(this);
736 } else {
737 _planCreators->clearAndDeleteContents();
738 }
739
740 _planCreatorsVehicleClass = vehicleClass;
741
742 // Allow custom builds to provide their own list of plan creators
743 const QList<PlanCreator*> creators = QGCCorePlugin::instance()->planCreators(this);
744
745 // Filter by vehicle class and add to the model
746 for (PlanCreator* creator : creators) {
747 if (creator->supportsVehicleClass(vehicleClass)) {
748 _planCreators->append(creator);
749 } else {
750 delete creator;
751 }
752 }
753
754 emit planCreatorsChanged(_planCreators);
755}
756
758{
759 if (offline()) {
760 // There is no new vehicle so clear any previous plan
761 qCDebug(PlanMasterControllerLog) << "showPlanFromManagerVehicle: Plan View - No new vehicle, clear any previous plan";
762 removeAll();
763 } else {
764 // We have a new active vehicle, show the plan from that
765 qCDebug(PlanMasterControllerLog) << "showPlanFromManagerVehicle: Plan View - New vehicle available, show plan from new manager vehicle";
766 _showPlanFromManagerVehicle();
767 }
768}
769
770void PlanMasterController::setUserSelectedManualCreation(bool userSelectedManualCreation)
771{
772 if (_userSelectedManualCreation != userSelectedManualCreation) {
773 _userSelectedManualCreation = userSelectedManualCreation;
775 // Update showCreateFromTemplate directly — do not go through _updateShowCreateFromTemplate,
776 // which would immediately auto-clear the flag if the plan happens to be empty right now.
777 const bool show = showCreateFromTemplate();
778 if (show != _showCreateFromTemplate) {
779 _showCreateFromTemplate = show;
781 }
782 }
783}
std::shared_ptr< LinkInterface > SharedLinkInterfacePtr
QString errorString
#define QGC_LOGGING_CATEGORY(name, categoryStr)
Application Settings.
Definition AppSettings.h:10
static constexpr const char * planFileExtension
static constexpr const char * kmlFileExtension
static constexpr const char * waypointsFileExtension
bool showPlanFromManagerVehicle(void) final
void removeAllFromVehicle(void) final
void setDirty(bool dirty) final
bool containsItems(void) const final
void sendToVehicle(void) final
bool syncInProgress(void) const final
void removeAll(void) final
Removes all from controller only.
bool load(const QJsonObject &json, QString &errorString) final
bool supported(void) const final
true: controller is waiting for the current load to complete
void loadFromVehicle(void) final
bool isEmpty(void) const
bool dirty(void) const final
void start(bool flyView) final
Should be called immediately upon Component.onCompleted.
void save(QJsonObject &json) final
void loadComplete(void)
void sendComplete(bool error)
Used to convert a Plan to a KML document.
bool showPlanFromManagerVehicle(void) final
bool dirty(void) const final
bool loadTextFile(QFile &file, QString &errorString)
void removeAllFromVehicle(void) final
bool load(const QJsonObject &json, QString &errorString) final
void addMissionToKML(KMLPlanDomDocument &planKML)
bool syncInProgress(void) const final
void sendToVehicle(void) final
bool isEmpty(void) const
void save(QJsonObject &json) final
bool containsItems(void) const final
void removeAll(void) final
Removes all from controller only.
void setDirty(bool dirty) final
void start(bool flyView) final
Should be called immediately upon Component.onCompleted.
void loadFromVehicle(void) final
Vehicle * activeVehicle() const
void activeVehicleChanged(Vehicle *activeVehicle)
Base class for PlanCreator objects which are used to create a full plan in a single step.
Definition PlanCreator.h:16
void syncInProgressChanged(bool syncInProgress)
void dirtyChanged(bool dirty)
void sendComplete(bool error)
void newMissionItemsAvailable(bool removeAllRequested)
Master controller for mission, fence, rally.
Q_INVOKABLE void removeAll(void)
Removes all from controller only, sync required to remove from vehicle.
PlanMasterController(QObject *parent=nullptr)
Q_INVOKABLE bool saveToCurrent()
QStringList saveNameFilters(void) const
void promptForPlanUsageOnVehicleChange(void)
void showCreateFromTemplateChanged()
Q_INVOKABLE void saveToKml(const QString &filename)
void setUserSelectedManualCreation(bool userSelectedManualCreation)
Q_INVOKABLE void showPlanFromManagerVehicle(void)
Replaces any current plan with the plan from the manager vehicle even if offline.
static constexpr int kPlanFileVersion
Q_INVOKABLE void start(void)
Should be called immediately upon Component.onCompleted.
QStringList loadNameFilters(void) const
void planCreatorsChanged(QmlObjectListModel *planCreators)
Q_INVOKABLE void loadFromVehicle(void)
void managerVehicleChanged(Vehicle *managerVehicle)
QString kmlFileExtension(void) const
void syncInProgressChanged(void)
Q_INVOKABLE void loadFromFile(const QString &filename)
Q_INVOKABLE void startStaticActiveVehicle(Vehicle *vehicle, bool deleteWhenSendCompleted=false)
bool dirtyForUpload(void) const
static constexpr const char * kJsonGeoFenceObjectKey
void dirtyForSaveChanged(bool dirtyForSave)
QString currentPlanFileName(void) const
void userSelectedManualCreationChanged()
bool syncInProgress(void) const
void currentPlanFileChanged(void)
bool showCreateFromTemplate(void) const
bool dirtyForSave(void) const
QString fileExtension(void) const
~PlanMasterController()
Either active vehicle or _controllerVehicle if no active vehicle.
Q_INVOKABLE bool saveToFile(const QString &filename)
void dirtyForUploadChanged(bool dirtyForUpload)
static constexpr const char * kPlanFileType
static constexpr const char * kJsonMissionObjectKey
Q_INVOKABLE void removeAllFromVehicle(void)
Removes all from vehicle and controller.
static void sendPlanToVehicle(Vehicle *vehicle, const QString &filename)
void offlineChanged(bool offlineEditing)
Q_INVOKABLE void sendToVehicle(void)
bool userSelectedManualCreation(void) const
static constexpr const char * kJsonRallyPointsObjectKey
virtual void postSaveToMissionJson(PlanMasterController *pController, QJsonObject &missionJson)
Allows custom builds to add custom items to the mission section of the plan file after the item is cr...
virtual void preLoadFromJson(PlanMasterController *pController, QJsonObject &json)
Allows custom builds to load custom items from the plan file before the document is parsed.
virtual void preSaveToMissionJson(PlanMasterController *pController, QJsonObject &missionJson)
Allows custom builds to add custom items to the mission section of the plan file before the item is c...
virtual void preSaveToJson(PlanMasterController *pController, QJsonObject &json)
Allows custom builds to add custom items to the plan file before the document is created.
virtual void postSaveToJson(PlanMasterController *pController, QJsonObject &json)
Allows custom builds to add custom items to the plan file after the document is created.
virtual QList< PlanCreator * > planCreators(PlanMasterController *planMasterController)
static QGCCorePlugin * instance()
virtual void postLoadFromJson(PlanMasterController *pController, QJsonObject &json)
Allows custom builds to load custom items from the plan file after the document is parsed.
void append(QObject *object)
Caller maintains responsibility for object ownership and deletion.
void clearAndDeleteContents() override final
Clears the list and calls deleteLater on each entry.
bool containsItems(void) const final
void save(QJsonObject &json) final
bool supported(void) const final
true: controller is waiting for the current load to complete
void setDirty(bool dirty) final
bool showPlanFromManagerVehicle(void) final
bool syncInProgress(void) const final
bool dirty(void) const final
void start(bool flyView) final
Should be called immediately upon Component.onCompleted.
void removeAllFromVehicle(void) final
bool load(const QJsonObject &json, QString &errorString) final
void removeAll(void) final
Removes all from controller only.
void loadFromVehicle(void) final
void loadComplete(void)
void sendComplete(bool error)
static SettingsManager * instance()
AppSettings * appSettings() const
WeakLinkInterfacePtr primaryLink() const
QGCMAVLink::VehicleClass_t vehicleClass(void) const
Definition Vehicle.h:447
void initialPlanRequestCompleteChanged(bool initialPlanRequestComplete)
MAV_TYPE vehicleType() const
Definition Vehicle.h:446
VehicleLinkManager * vehicleLinkManager()
Definition Vehicle.h:597
MAV_AUTOPILOT firmwareType() const
Definition Vehicle.h:445
GeoFenceManager * geoFenceManager()
Definition Vehicle.h:593
RallyPointManager * rallyPointManager()
Definition Vehicle.h:594
bool initialPlanRequestComplete() const
Definition Vehicle.h:733
MissionManager * missionManager()
Definition Vehicle.h:592
void vehicleTypeChanged()
bool validateExternalQGCJsonFile(const QJsonObject &jsonObject, const QString &expectedFileType, int minSupportedVersion, int maxSupportedVersion, int &version, QString &errorString)
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.
bool isJsonFile(const QByteArray &bytes, QJsonDocument &jsonDoc, QString &errorString)
Determines whether an in-memory byte buffer contains parseable JSON content.
void saveQGCJsonFileHeader(QJsonObject &jsonObject, const QString &fileType, int version)
Saves the standard QGC file header (groundStation, fileType, version) into the json object.
void showAppMessage(const QString &message, const QString &title)
Modal application message. Queued if the UI isn't ready yet.
Definition AppMessages.cc:9