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 "QGCCompression.h"
17#include "QGCCompressionJob.h"
18#include "QGCLoggingCategory.h"
19
20#include <QtCore/QDir>
21#include <QtCore/QDirIterator>
22#include <QtCore/QFileInfo>
23#include <QtCore/QJsonDocument>
24#include <QtCore/QRegularExpression>
25
26QGC_LOGGING_CATEGORY(PlanMasterControllerLog, "PlanManager.PlanMasterController")
27
29 : QObject (parent)
30 , _multiVehicleMgr (MultiVehicleManager::instance())
31 , _controllerVehicle (new Vehicle(Vehicle::MAV_AUTOPILOT_TRACK, Vehicle::MAV_TYPE_TRACK, this))
32 , _managerVehicle (_controllerVehicle)
33 , _missionController (this)
34 , _geoFenceController (this)
35 , _rallyPointController (this)
36{
37 _commonInit();
38}
39
40#ifdef QGC_UNITTEST_BUILD
41PlanMasterController::PlanMasterController(MAV_AUTOPILOT firmwareType, MAV_TYPE vehicleType, QObject* parent)
42 : QObject (parent)
43 , _multiVehicleMgr (MultiVehicleManager::instance())
44 , _controllerVehicle (new Vehicle(firmwareType, vehicleType))
45 , _managerVehicle (_controllerVehicle)
46 , _missionController (this)
47 , _geoFenceController (this)
48 , _rallyPointController (this)
49{
50 _commonInit();
51}
52#endif
53
54void PlanMasterController::_commonInit(void)
55{
56 connect(&_missionController, &MissionController::dirtyChanged, this, &PlanMasterController::_updateOverallDirty);
57 connect(&_geoFenceController, &GeoFenceController::dirtyChanged, this, &PlanMasterController::_updateOverallDirty);
58 connect(&_rallyPointController, &RallyPointController::dirtyChanged, this, &PlanMasterController::_updateOverallDirty);
59
63
64 connect(this, &PlanMasterController::containsItemsChanged, this, &PlanMasterController::_updateShowCreateFromTemplate);
65
69
70 // Offline vehicle can change firmware/vehicle type
71 connect(_controllerVehicle, &Vehicle::vehicleTypeChanged, this, &PlanMasterController::_updatePlanCreatorsList);
72}
73
74
79
81{
82 _missionController.start (_flyView);
83 _geoFenceController.start (_flyView);
84 _rallyPointController.start (_flyView);
85
86 _activeVehicleChanged(_multiVehicleMgr->activeVehicle());
87 connect(_multiVehicleMgr, &MultiVehicleManager::activeVehicleChanged, this, &PlanMasterController::_activeVehicleChanged);
88
89 _updatePlanCreatorsList();
90}
91
92void PlanMasterController::startStaticActiveVehicle(Vehicle* vehicle, bool deleteWhenSendCompleted)
93{
94 _flyView = true;
95 _deleteWhenSendCompleted = deleteWhenSendCompleted;
96 _missionController.start(_flyView);
97 _geoFenceController.start(_flyView);
98 _rallyPointController.start(_flyView);
99 _activeVehicleChanged(vehicle);
100}
101
102void PlanMasterController::_activeVehicleChanged(Vehicle* activeVehicle)
103{
104 if (_managerVehicle == activeVehicle) {
105 // We are already setup for this vehicle
106 return;
107 }
108
109 qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged" << activeVehicle;
110
111 if (_managerVehicle) {
112 // Disconnect old vehicle. Be careful of wildcarding disconnect too much since _managerVehicle may equal _controllerVehicle
113 disconnect(_managerVehicle->missionManager(), nullptr, this, nullptr);
114 disconnect(_managerVehicle->geoFenceManager(), nullptr, this, nullptr);
115 disconnect(_managerVehicle->rallyPointManager(), nullptr, this, nullptr);
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->missionManager(), &MissionManager::newMissionItemsAvailable, this, &PlanMasterController::_loadMissionComplete);
134 connect(_managerVehicle->geoFenceManager(), &GeoFenceManager::loadComplete, this, &PlanMasterController::_loadGeoFenceComplete);
135 connect(_managerVehicle->rallyPointManager(), &RallyPointManager::loadComplete, this, &PlanMasterController::_loadRallyPointsComplete);
136 connect(_managerVehicle->missionManager(), &MissionManager::sendComplete, this, &PlanMasterController::_sendMissionComplete);
137 connect(_managerVehicle->geoFenceManager(), &GeoFenceManager::sendComplete, this, &PlanMasterController::_sendGeoFenceComplete);
138 connect(_managerVehicle->rallyPointManager(), &RallyPointManager::sendComplete, this, &PlanMasterController::_sendRallyPointsComplete);
139 }
140
141 _offline = newOffline;
142 emit offlineChanged(offline());
143 emit managerVehicleChanged(_managerVehicle);
144
145 if (_flyView) {
146 // We are in the Fly View
147 if (newOffline) {
148 // No active vehicle, clear mission
149 qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Fly View - No active vehicle, clearing stale plan";
150 removeAll();
151 } else {
152 // Fly view has changed to a new active vehicle, update to show correct mission
153 qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Fly View - New active vehicle, loading new plan from manager vehicle";
154 _showPlanFromManagerVehicle();
155 }
156 } else {
157 // We are in the Plan view.
158 if (containsItems()) {
159 // We have a plan which is from a different vehicle than the new active vehicle. By definition this plan requires and upload.
160 _setDirtyForUpload(true);
161
162 // The plan view has a stale plan in it
163 if (dirtyForSave()) {
164 // Plan is dirty, the user must decide what to do in all cases
165 qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Plan View - Previous dirty plan exists, no new active vehicle, sending promptForPlanUsageOnVehicleChange signal";
167 } else {
168 // Plan is not dirty
169 if (newOffline) {
170 // The active vehicle went away with no new active vehicle
171 qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Plan View - Previous clean plan exists, no new active vehicle, clear stale plan";
172 removeAll();
173 } else {
174 // We are transitioning from one active vehicle to another. Show the plan from the new vehicle.
175 qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Plan View - Previous clean plan exists, new active vehicle, loading from new manager vehicle";
176 _showPlanFromManagerVehicle();
177 }
178 }
179 } else {
180 // There is no previous Plan in the view
181 _setDirtyStates(false, false);
182 if (newOffline) {
183 // Nothing special to do in this case
184 qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Plan View - No previous plan, no longer connected to vehicle, nothing to do";
185 } else {
186 // Just show the plan from the new vehicle
187 qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Plan View - No previous plan, new active vehicle, loading from new manager vehicle";
188 _showPlanFromManagerVehicle();
189 }
190 }
191 }
192
193 // Vehicle changed so we need to signal everything
198
199 _updatePlanCreatorsList();
200}
201
203{
204 SharedLinkInterfacePtr sharedLink = _managerVehicle->vehicleLinkManager()->primaryLink().lock();
205 if (sharedLink) {
206 if (sharedLink->linkConfiguration()->isHighLatency()) {
207 QGC::showAppMessage(tr("Download not supported on high latency links."));
208 return;
209 }
210 } else {
211 // Vehicle is shutting down
212 return;
213 }
214
215 if (offline()) {
216 qCCritical(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle called while offline";
217 } else if (_flyView) {
218 qCCritical(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle called from Fly view";
219 } else if (syncInProgress()) {
220 qCCritical(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle called while syncInProgress";
221 } else {
222 _loadGeoFence = true;
223 qCDebug(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle calling _missionController.loadFromVehicle";
224 _missionController.loadFromVehicle();
225 }
226}
227
228
229void PlanMasterController::_loadMissionComplete(void)
230{
231 if (!_flyView && _loadGeoFence) {
232 _loadGeoFence = false;
233 _loadRallyPoints = true;
234 if (_geoFenceController.supported()) {
235 qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadMissionComplete calling _geoFenceController.loadFromVehicle";
236 _geoFenceController.loadFromVehicle();
237 } else {
238 qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadMissionComplete GeoFence not supported skipping";
239 _geoFenceController.removeAll();
240 _loadGeoFenceComplete();
241 }
242 }
243}
244
245void PlanMasterController::_loadGeoFenceComplete(void)
246{
247 if (!_flyView && _loadRallyPoints) {
248 _loadRallyPoints = false;
249 if (_rallyPointController.supported()) {
250 qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadGeoFenceComplete calling _rallyPointController.loadFromVehicle";
251 _rallyPointController.loadFromVehicle();
252 } else {
253 qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadMissionComplete Rally Points not supported skipping";
254 _rallyPointController.removeAll();
255 _loadRallyPointsComplete();
256 }
257 }
258}
259
260void PlanMasterController::_loadRallyPointsComplete(void)
261{
262 qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadRallyPointsComplete";
263 _setDirtyStates(containsItems() /* dirtyForSave */, false /* dirtyForUpload */);
264}
265
266void PlanMasterController::_sendMissionComplete(void)
267{
268 if (_sendGeoFence) {
269 _sendGeoFence = false;
270 _sendRallyPoints = true;
271 if (_geoFenceController.supported()) {
272 qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle start GeoFence sendToVehicle";
273 _geoFenceController.sendToVehicle();
274 } else {
275 qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle GeoFence not supported skipping";
276 _sendGeoFenceComplete();
277 }
278 }
279}
280
281void PlanMasterController::_sendGeoFenceComplete(void)
282{
283 if (_sendRallyPoints) {
284 _sendRallyPoints = false;
285 if (_rallyPointController.supported()) {
286 qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle start rally sendToVehicle";
287 _rallyPointController.sendToVehicle();
288 } else {
289 qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle Rally Points not support skipping";
290 _sendRallyPointsComplete();
291 }
292 }
293}
294
295void PlanMasterController::_sendRallyPointsComplete(void)
296{
297 qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle Rally Point send complete";
298 _setDirtyForUpload(false);
299 if (_deleteWhenSendCompleted) {
300 this->deleteLater();
301 }
302}
303
305{
306 SharedLinkInterfacePtr sharedLink = _managerVehicle->vehicleLinkManager()->primaryLink().lock();
307 if (sharedLink) {
308 if (sharedLink->linkConfiguration()->isHighLatency()) {
309 QGC::showAppMessage(tr("Upload not supported on high latency links."));
310 return;
311 }
312 } else {
313 // Vehicle is shutting down
314 return;
315 }
316
317 if (offline()) {
318 qCCritical(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle called while offline";
319 } else if (syncInProgress()) {
320 qCCritical(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle called while syncInProgress";
321 } else {
322 qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle start mission sendToVehicle";
323 _sendGeoFence = true;
324 _missionController.sendToVehicle();
325 }
326}
327
328void PlanMasterController::loadFromFile(const QString& filename)
329{
330 QString errorString;
331 QString errorMessage = tr("Error loading Plan file (%1). %2").arg(filename).arg("%1");
332
333 if (filename.isEmpty()) {
334 return;
335 }
336
337 QFileInfo fileInfo(filename);
338 QFile file(filename);
339
340 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
341 errorString = file.errorString() + QStringLiteral(" ") + filename;
342 QGC::showAppMessage(errorMessage.arg(errorString));
343 return;
344 }
345
346 bool success = false;
347 if (fileInfo.suffix() == AppSettings::waypointsFileExtension || fileInfo.suffix() == QStringLiteral("txt")) {
348 if (!_missionController.loadTextFile(file, errorString)) {
349 QGC::showAppMessage(errorMessage.arg(errorString));
350 } else {
351 success = true;
352 }
353 } else {
354 QJsonDocument jsonDoc;
355 QByteArray bytes = file.readAll();
356
357 if (!JsonParsing::isJsonFile(bytes, jsonDoc, errorString)) {
358 QGC::showAppMessage(errorMessage.arg(errorString));
359 return;
360 }
361
362 QJsonObject json = jsonDoc.object();
363 //-- Allow plugins to pre process the load
365
366 int version;
368 QGC::showAppMessage(errorMessage.arg(errorString));
369 return;
370 }
371
372 QList<JsonParsing::KeyValidateInfo> rgKeyInfo = {
373 { kJsonMissionObjectKey, QJsonValue::Object, true },
374 { kJsonGeoFenceObjectKey, QJsonValue::Object, true },
375 { kJsonRallyPointsObjectKey, QJsonValue::Object, true },
376 };
377 if (!JsonParsing::validateKeys(json, rgKeyInfo, errorString)) {
378 QGC::showAppMessage(errorMessage.arg(errorString));
379 return;
380 }
381
382 if (!_missionController.load(json[kJsonMissionObjectKey].toObject(), errorString) ||
383 !_geoFenceController.load(json[kJsonGeoFenceObjectKey].toObject(), errorString) ||
384 !_rallyPointController.load(json[kJsonRallyPointsObjectKey].toObject(), errorString)) {
385 QGC::showAppMessage(errorMessage.arg(errorString));
386 } else {
387 //-- Allow plugins to post process the load
389 success = true;
390 }
391 }
392
393 if (success){
394 const bool oldRenamed = planFileRenamed();
395 _currentPlanFile = QString::asprintf("%s/%s.%s", fileInfo.path().toLocal8Bit().data(), fileInfo.completeBaseName().toLocal8Bit().data(), AppSettings::planFileExtension);
396 const bool currentNameChanged = (_currentPlanFileName != fileInfo.completeBaseName());
397 const bool originalNameChanged = (_originalPlanFileName != fileInfo.completeBaseName());
398 _currentPlanFileName = fileInfo.completeBaseName();
399 _originalPlanFileName = _currentPlanFileName;
400 _setDirtyStates(false /* dirtyForSave */, true /* dirtyForUpload */);
402 if (currentNameChanged) {
404 }
405 if (originalNameChanged) {
407 }
408 if (oldRenamed != planFileRenamed()) {
410 }
411 } else {
412 const bool hadFile = !_currentPlanFile.isEmpty();
413 const bool hadCurrentName = !_currentPlanFileName.isEmpty();
414 const bool hadOriginalName = !_originalPlanFileName.isEmpty();
415 const bool wasRenamed = planFileRenamed();
416 _currentPlanFile.clear();
417 _currentPlanFileName.clear();
418 _originalPlanFileName.clear();
419 if (hadFile) {
421 }
422 if (hadCurrentName) {
424 }
425 if (hadOriginalName) {
427 }
428 if (wasRenamed != planFileRenamed()) {
430 }
431 }
432}
433
435{
436 QJsonObject planJson;
437 QGCCorePlugin::instance()->preSaveToJson(this, planJson);
438 QJsonObject missionJson;
439 QJsonObject fenceJson;
440 QJsonObject rallyJson;
442 //-- Allow plugin to preemptly add its own keys to mission
443 QGCCorePlugin::instance()->preSaveToMissionJson(this, missionJson);
444 _missionController.save(missionJson);
445 //-- Allow plugin to add its own keys to mission
447 _geoFenceController.save(fenceJson);
448 _rallyPointController.save(rallyJson);
449 planJson[kJsonMissionObjectKey] = missionJson;
450 planJson[kJsonGeoFenceObjectKey] = fenceJson;
451 planJson[kJsonRallyPointsObjectKey] = rallyJson;
452 QGCCorePlugin::instance()->postSaveToJson(this, planJson);
453 return QJsonDocument(planJson);
454}
455
456bool
458{
459 if (!_currentPlanFile.isEmpty()) {
460 const bool saveSuccess = saveToFile(_currentPlanFile);
461 return saveSuccess;
462 }
463
464 return false;
465}
466
467bool PlanMasterController::saveToFile(const QString& filename)
468{
469 if (filename.isEmpty()) {
470 return false;
471 }
472
473 QString planFilename = filename;
474 if (!QFileInfo(filename).fileName().contains(".")) {
475 planFilename += QString(".%1").arg(fileExtension());
476 }
477
478 QFile file(planFilename);
479
480 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
481 QGC::showAppMessage(tr("Plan save error %1 : %2").arg(filename).arg(file.errorString()));
482 return false;
483 } else {
484 const QByteArray saveBytes = saveToJson().toJson();
485 const qint64 bytesWritten = file.write(saveBytes);
486 if (bytesWritten != saveBytes.size()) {
487 QGC::showAppMessage(tr("Plan save error %1 : %2").arg(filename).arg(file.errorString()));
488 return false;
489 }
490 if(_currentPlanFile != planFilename) {
491 _currentPlanFile = planFilename;
493 }
494 const bool wasRenamed = planFileRenamed();
495 const QString savedBaseName = QFileInfo(planFilename).completeBaseName();
496 if (_currentPlanFileName != savedBaseName) {
497 _currentPlanFileName = savedBaseName;
499 }
500 if (_originalPlanFileName != savedBaseName) {
501 _originalPlanFileName = savedBaseName;
503 }
504 if (wasRenamed != planFileRenamed()) {
506 }
507 _setDirtyForSave(false);
508 }
509
510 return true;
511}
512
513void PlanMasterController::saveToKml(const QString& filename)
514{
515 if (filename.isEmpty()) {
516 return;
517 }
518
519 QString kmlFilename = filename;
520 if (!QFileInfo(filename).fileName().contains(".")) {
521 kmlFilename += QString(".%1").arg(kmlFileExtension());
522 }
523
524 QFile file(kmlFilename);
525
526 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
527 QGC::showAppMessage(tr("KML save error %1 : %2").arg(filename).arg(file.errorString()));
528 } else {
529 KMLPlanDomDocument planKML;
530 _missionController.addMissionToKML(planKML);
531 QTextStream stream(&file);
532 stream << planKML.toString();
533 file.close();
534 }
535}
536
538{
539 _suppressOverallDirtyUpdate = true;
540 _missionController.removeAll();
541 _geoFenceController.removeAll();
542 _rallyPointController.removeAll();
543 _missionController.setDirty(false);
544 _geoFenceController.setDirty(false);
545 _rallyPointController.setDirty(false);
546 _suppressOverallDirtyUpdate = false;
547
548 _setDirtyStates(false, false);
549 if (_offline) {
550 _clearFileNames();
551 }
553}
554
556{
557 if (!offline()) {
558 _missionController.removeAllFromVehicle();
559 if (_geoFenceController.supported()) {
560 _geoFenceController.removeAllFromVehicle();
561 }
562 if (_rallyPointController.supported()) {
563 _rallyPointController.removeAllFromVehicle();
564 }
565 _setDirtyForUpload(false);
566 _clearFileNames();
567 } else {
568 qCCritical(PlanMasterControllerLog) << "PlanMasterController::removeAllFromVehicle called while offline";
569 }
571}
572
574{
575 return _missionController.containsItems() || _geoFenceController.containsItems() || _rallyPointController.containsItems();
576}
577
578void PlanMasterController::_updateShowCreateFromTemplate(void)
579{
580 // When the plan becomes empty, always return to template-selection mode regardless
581 // of how the items were removed.
582 if (!containsItems() && _userSelectedManualCreation) {
583 _userSelectedManualCreation = false;
585 }
586 const bool show = showCreateFromTemplate();
587 if (show != _showCreateFromTemplate) {
588 _showCreateFromTemplate = show;
590 }
591}
592
597
599{
600 // Normalize to a base name: trim whitespace, strip known extension, remove illegal characters
601 QString sanitized = name.trimmed();
602 const QString ext = QStringLiteral(".") + fileExtension();
603 if (sanitized.endsWith(ext, Qt::CaseInsensitive)) {
604 sanitized.chop(ext.length());
605 sanitized = sanitized.trimmed();
606 }
607 sanitized.remove(QRegularExpression(QStringLiteral("[/\\\\:*?\"<>|]")));
608 if (_currentPlanFileName != sanitized) {
609 const bool wasRenamed = planFileRenamed();
610 _currentPlanFileName = sanitized;
612 if (wasRenamed != planFileRenamed()) {
614 }
615 }
616}
617
619{
620 if (_currentPlanFileName.isEmpty()) {
621 return false;
622 }
623 return saveToFile(_resolvedPlanFilePath());
624}
625
627{
628 return !_originalPlanFileName.isEmpty() && _currentPlanFileName != _originalPlanFileName;
629}
630
632{
633 if (_currentPlanFileName.isEmpty()) {
634 return false;
635 }
636 return QFile::exists(_resolvedPlanFilePath());
637}
638
639QString PlanMasterController::_resolvedPlanFilePath() const
640{
641 const QString dir = _currentPlanFile.isEmpty()
643 : QFileInfo(_currentPlanFile).path();
644 return QStringLiteral("%1/%2.%3").arg(dir, _currentPlanFileName, fileExtension());
645}
646
647void PlanMasterController::_clearFileNames()
648{
649 const bool hadFile = !_currentPlanFile.isEmpty();
650 const bool hadCurrentName = !_currentPlanFileName.isEmpty();
651 const bool hadOriginalName = !_originalPlanFileName.isEmpty();
652 const bool wasRenamed = planFileRenamed();
653 _currentPlanFile.clear();
654 _currentPlanFileName.clear();
655 _originalPlanFileName.clear();
656 if (hadFile) {
658 }
659 if (hadCurrentName) {
661 }
662 if (hadOriginalName) {
664 }
665 if (wasRenamed != planFileRenamed()) {
667 }
668}
669
674
676{
677 QStringList filters;
678
679 filters << tr("Supported types (*.%1 *.%2 *.%3)").arg(AppSettings::planFileExtension).arg(AppSettings::waypointsFileExtension).arg("txt") <<
680 tr("All Files (*)");
681 return filters;
682}
683
684
686{
687 QStringList filters;
688
689 filters << tr("Plan Files (*.%1)").arg(fileExtension()) << tr("All Files (*)");
690 return filters;
691}
692
693void PlanMasterController::sendPlanToVehicle(Vehicle* vehicle, const QString& filename)
694{
695 // Use a transient PlanMasterController to accomplish this
696 PlanMasterController* controller = new PlanMasterController();
697 controller->startStaticActiveVehicle(vehicle, true /* deleteWhenSendCompleted */);
698 controller->loadFromFile(filename);
699 controller->sendToVehicle();
700}
701
702void PlanMasterController::_showPlanFromManagerVehicle(void)
703{
704 if (!_managerVehicle->initialPlanRequestComplete()) {
705 // We need to wait until initial load is complete before we show anything.
706 return;
707 }
708
709 // The crazy if structure is to handle the load propagating by itself through the system
710 if (!_missionController.showPlanFromManagerVehicle()) {
711 if (!_geoFenceController.showPlanFromManagerVehicle()) {
712 _rallyPointController.showPlanFromManagerVehicle();
713 }
714 }
715
716 // Showing the vehicle plan should leave both dirty states clean.
717 _missionController.setDirty(false);
718 _geoFenceController.setDirty(false);
719 _rallyPointController.setDirty(false);
720 _setDirtyStates(false, false);
721}
722
724{
725 return _missionController.syncInProgress() ||
726 _geoFenceController.syncInProgress() ||
727 _rallyPointController.syncInProgress();
728}
729
731{
732 return _missionController.isEmpty() &&
733 _geoFenceController.isEmpty() &&
734 _rallyPointController.isEmpty();
735}
736
737void PlanMasterController::_updateOverallDirty(void)
738{
739 if (syncInProgress() || _suppressOverallDirtyUpdate) {
740 return;
741 }
742
743 const bool saveDirty = _missionController.dirty() || _geoFenceController.dirty() || _rallyPointController.dirty();
744 if (saveDirty) {
745 _setDirtyForSave(true);
746 }
747}
748
749void PlanMasterController::_setDirtyForSave(bool dirtyForSave)
750{
751 if (_dirtyForSave != dirtyForSave) {
752 _dirtyForSave = dirtyForSave;
753 emit dirtyForSaveChanged(_dirtyForSave);
754
755 if (_dirtyForSave) {
756 _setDirtyForUpload(true);
757 }
758 }
759}
760
761void PlanMasterController::_setDirtyForUpload(bool dirtyForUpload)
762{
763 if (_dirtyForUpload != dirtyForUpload) {
764 _dirtyForUpload = dirtyForUpload;
765 emit dirtyForUploadChanged(_dirtyForUpload);
766 }
767}
768
769void PlanMasterController::_setDirtyStates(bool dirtyForSave, bool dirtyForUpload)
770{
771 const bool saveChanged = (_dirtyForSave != dirtyForSave);
772 const bool uploadChanged = (_dirtyForUpload != dirtyForUpload);
773
774 _dirtyForSave = dirtyForSave;
775 _dirtyForUpload = dirtyForUpload;
776
777 if (saveChanged) {
778 emit dirtyForSaveChanged(_dirtyForSave);
779 }
780 if (uploadChanged) {
781 emit dirtyForUploadChanged(_dirtyForUpload);
782 }
783}
784
785void PlanMasterController::_updatePlanCreatorsList(void)
786{
787 if (_flyView) {
788 return;
789 }
790
791 const auto vehicleClass = _managerVehicle->vehicleClass();
792
793 // Only rebuild if the vehicle class actually changed
794 if (_planCreators && _planCreatorsVehicleClass == vehicleClass) {
795 return;
796 }
797
798 if (!_planCreators) {
799 _planCreators = new QmlObjectListModel(this);
800 } else {
801 _planCreators->clearAndDeleteContents();
802 }
803
804 _planCreatorsVehicleClass = vehicleClass;
805
806 // Allow custom builds to provide their own list of plan creators
807 const QList<PlanCreator*> creators = QGCCorePlugin::instance()->planCreators(this);
808
809 // Filter by vehicle class and add to the model
810 for (PlanCreator* creator : creators) {
811 if (creator->supportsVehicleClass(vehicleClass)) {
812 _planCreators->append(creator);
813 } else {
814 delete creator;
815 }
816 }
817
818 emit planCreatorsChanged(_planCreators);
819}
820
822{
823 if (offline()) {
824 // There is no new vehicle so clear any previous plan
825 qCDebug(PlanMasterControllerLog) << "showPlanFromManagerVehicle: Plan View - No new vehicle, clear any previous plan";
826 removeAll();
827 } else {
828 // We have a new active vehicle, show the plan from that
829 qCDebug(PlanMasterControllerLog) << "showPlanFromManagerVehicle: Plan View - New vehicle available, show plan from new manager vehicle";
830 _showPlanFromManagerVehicle();
831 }
832}
833
834void PlanMasterController::setUserSelectedManualCreation(bool userSelectedManualCreation)
835{
836 if (_userSelectedManualCreation != userSelectedManualCreation) {
837 _userSelectedManualCreation = userSelectedManualCreation;
839 // Update showCreateFromTemplate directly — do not go through _updateShowCreateFromTemplate,
840 // which would immediately auto-clear the flag if the plan happens to be empty right now.
841 const bool show = showCreateFromTemplate();
842 if (show != _showCreateFromTemplate) {
843 _showCreateFromTemplate = show;
845 }
846 }
847}
848
849void PlanMasterController::loadFromArchive(const QString& archivePath)
850{
851 if (archivePath.isEmpty()) {
852 return;
853 }
854
855 if (!QFile::exists(archivePath)) {
856 QGC::showAppMessage(tr("Archive file not found: %1").arg(archivePath));
857 return;
858 }
859
860 if (!QGCCompression::isArchiveFile(archivePath)) {
861 QGC::showAppMessage(tr("Not a supported archive format: %1").arg(archivePath));
862 return;
863 }
864
865 const QString tempPath = QDir::temp().filePath(QStringLiteral("qgc_plan_") + QString::number(QDateTime::currentMSecsSinceEpoch()));
866 if (!QDir().mkpath(tempPath)) {
867 QGC::showAppMessage(tr("Could not create temporary directory"));
868 return;
869 }
870
871 _extractionOutputDir = tempPath;
872
873 if (_extractionJob == nullptr) {
874 _extractionJob = new QGCCompressionJob(this);
875 connect(_extractionJob, &QGCCompressionJob::finished,
876 this, &PlanMasterController::_handleExtractionFinished);
877 }
878
879 _extractionJob->extractArchive(archivePath, tempPath);
880}
881
882void PlanMasterController::_handleExtractionFinished(bool success)
883{
884 if (!success) {
885 const QString error = _extractionJob != nullptr ? _extractionJob->errorString() : tr("Extraction failed");
886 QGC::showAppMessage(tr("Failed to extract plan archive: %1").arg(error));
887 QDir(_extractionOutputDir).removeRecursively();
888 _extractionOutputDir.clear();
889 return;
890 }
891
892 QString planPath;
893 const QString planExt = QStringLiteral("*.") + AppSettings::planFileExtension;
894 QDirIterator it(_extractionOutputDir, {planExt}, QDir::Files, QDirIterator::Subdirectories);
895 if (it.hasNext()) {
896 planPath = it.next();
897 }
898
899 if (planPath.isEmpty()) {
900 QGC::showAppMessage(tr("No plan file found in archive"));
901 QDir(_extractionOutputDir).removeRecursively();
902 _extractionOutputDir.clear();
903 return;
904 }
905
906 qCDebug(PlanMasterControllerLog) << "Found plan file in archive:" << planPath;
907 loadFromFile(planPath);
908
909 QDir(_extractionOutputDir).removeRecursively();
910 _extractionOutputDir.clear();
911}
std::shared_ptr< LinkInterface > SharedLinkInterfacePtr
QObject wrapper for async compression operations using QtConcurrent/QPromise.
QString errorString
Error error
#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
QString missionSavePath()
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)
void planFileRenamedChanged(void)
Q_INVOKABLE bool saveWithCurrentName()
Save using the (possibly renamed) currentPlanFileName.
void setCurrentPlanFileName(const QString &name)
Q_INVOKABLE bool saveToCurrent()
void originalPlanFileNameChanged(void)
QStringList saveNameFilters(void) const
void currentPlanFileNameChanged(void)
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
Q_INVOKABLE void loadFromArchive(const QString &archivePath)
void dirtyForSaveChanged(bool dirtyForSave)
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)
bool planFileRenamed(void) const
Q_INVOKABLE bool resolvedPlanFileExists() const
true if a file at the renamed path already exists on disk
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
QObject wrapper for compression operations with progress signals.
QString errorString() const
void finished(bool success)
void extractArchive(const QString &archivePath, const QString &outputDirectoryPath, qint64 maxBytes=0)
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:429
MAV_TYPE vehicleType() const
Definition Vehicle.h:428
VehicleLinkManager * vehicleLinkManager()
Definition Vehicle.h:575
MAV_AUTOPILOT firmwareType() const
Definition Vehicle.h:427
GeoFenceManager * geoFenceManager()
Definition Vehicle.h:571
RallyPointManager * rallyPointManager()
Definition Vehicle.h:572
bool initialPlanRequestComplete() const
Definition Vehicle.h:703
MissionManager * missionManager()
Definition Vehicle.h:570
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.
bool isArchiveFile(const QString &filePath)
Check if file path indicates an archive file (.zip, .tar, .tar.gz, etc.)
void showAppMessage(const QString &message, const QString &title)
Modal application message. Queued if the UI isn't ready yet.
Definition AppMessages.cc:9