QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
PlanMasterController.cc
Go to the documentation of this file.
2#include "QGCApplication.h"
3#include "QGCCorePlugin.h"
5#include "Vehicle.h"
6#include "SettingsManager.h"
7#include "AppSettings.h"
8#include "JsonHelper.h"
9#include "JsonParsing.h"
10#include "MissionManager.h"
11#include "KMLPlanDomDocument.h"
12#include "SurveyPlanCreator.h"
15#include "BlankPlanCreator.h"
16#include "QmlObjectListModel.h"
17#include "GeoFenceManager.h"
18#include "RallyPointManager.h"
19#include "QGCCompression.h"
20#include "QGCCompressionJob.h"
21#include "QGCLoggingCategory.h"
22
23#include <QtCore/QDir>
24#include <QtCore/QDirIterator>
25#include <QtCore/QFileInfo>
26#include <QtCore/QJsonDocument>
27#include <QtCore/QRegularExpression>
28
29QGC_LOGGING_CATEGORY(PlanMasterControllerLog, "PlanManager.PlanMasterController")
30
32 : QObject (parent)
33 , _multiVehicleMgr (MultiVehicleManager::instance())
34 , _controllerVehicle (new Vehicle(Vehicle::MAV_AUTOPILOT_TRACK, Vehicle::MAV_TYPE_TRACK, this))
35 , _managerVehicle (_controllerVehicle)
36 , _missionController (this)
37 , _geoFenceController (this)
38 , _rallyPointController (this)
39{
40 _commonInit();
41}
42
43#ifdef QT_DEBUG
44PlanMasterController::PlanMasterController(MAV_AUTOPILOT firmwareType, MAV_TYPE vehicleType, QObject* parent)
45 : QObject (parent)
46 , _multiVehicleMgr (MultiVehicleManager::instance())
47 , _controllerVehicle (new Vehicle(firmwareType, vehicleType))
48 , _managerVehicle (_controllerVehicle)
49 , _missionController (this)
50 , _geoFenceController (this)
51 , _rallyPointController (this)
52{
53 _commonInit();
54}
55#endif
56
57void PlanMasterController::_commonInit(void)
58{
59 connect(&_missionController, &MissionController::dirtyChanged, this, &PlanMasterController::_updateOverallDirty);
60 connect(&_geoFenceController, &GeoFenceController::dirtyChanged, this, &PlanMasterController::_updateOverallDirty);
61 connect(&_rallyPointController, &RallyPointController::dirtyChanged, this, &PlanMasterController::_updateOverallDirty);
62
66
70
71 // Offline vehicle can change firmware/vehicle type
72 connect(_controllerVehicle, &Vehicle::vehicleTypeChanged, this, &PlanMasterController::_updatePlanCreatorsList);
73}
74
75
80
81void PlanMasterController::start(void)
82{
83 _missionController.start (_flyView);
84 _geoFenceController.start (_flyView);
85 _rallyPointController.start (_flyView);
86
87 _activeVehicleChanged(_multiVehicleMgr->activeVehicle());
88 connect(_multiVehicleMgr, &MultiVehicleManager::activeVehicleChanged, this, &PlanMasterController::_activeVehicleChanged);
89
90 _updatePlanCreatorsList();
91}
92
93void PlanMasterController::startStaticActiveVehicle(Vehicle* vehicle, bool deleteWhenSendCompleted)
94{
95 _flyView = true;
96 _deleteWhenSendCompleted = deleteWhenSendCompleted;
97 _missionController.start(_flyView);
98 _geoFenceController.start(_flyView);
99 _rallyPointController.start(_flyView);
100 _activeVehicleChanged(vehicle);
101}
102
103void PlanMasterController::_activeVehicleChanged(Vehicle* activeVehicle)
104{
105 if (_managerVehicle == activeVehicle) {
106 // We are already setup for this vehicle
107 return;
108 }
109
110 qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged" << activeVehicle;
111
112 if (_managerVehicle) {
113 // Disconnect old vehicle. Be careful of wildcarding disconnect too much since _managerVehicle may equal _controllerVehicle
114 disconnect(_managerVehicle->missionManager(), nullptr, this, nullptr);
115 disconnect(_managerVehicle->geoFenceManager(), nullptr, this, nullptr);
116 disconnect(_managerVehicle->rallyPointManager(), nullptr, this, nullptr);
117 }
118
119 bool newOffline = false;
120 if (activeVehicle == nullptr) {
121 // Since there is no longer an active vehicle we use the offline controller vehicle as the manager vehicle
122 _managerVehicle = _controllerVehicle;
123 newOffline = true;
124 } else {
125 newOffline = false;
126 _managerVehicle = activeVehicle;
127
128 // Update controllerVehicle to the currently connected vehicle
129 AppSettings* appSettings = SettingsManager::instance()->appSettings();
130 appSettings->offlineEditingFirmwareClass()->setRawValue(QGCMAVLink::firmwareClass(_managerVehicle->firmwareType()));
131 appSettings->offlineEditingVehicleClass()->setRawValue(QGCMAVLink::vehicleClass(_managerVehicle->vehicleType()));
132
133 // We use these signals to sequence upload and download to the multiple controller/managers
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 qgcApp()->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 _loadGeoFence = true;
224 qCDebug(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle calling _missionController.loadFromVehicle";
225 _missionController.loadFromVehicle();
226 }
227}
228
229
230void PlanMasterController::_loadMissionComplete(void)
231{
232 if (!_flyView && _loadGeoFence) {
233 _loadGeoFence = false;
234 _loadRallyPoints = true;
235 if (_geoFenceController.supported()) {
236 qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadMissionComplete calling _geoFenceController.loadFromVehicle";
237 _geoFenceController.loadFromVehicle();
238 } else {
239 qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadMissionComplete GeoFence not supported skipping";
240 _geoFenceController.removeAll();
241 _loadGeoFenceComplete();
242 }
243 }
244}
245
246void PlanMasterController::_loadGeoFenceComplete(void)
247{
248 if (!_flyView && _loadRallyPoints) {
249 _loadRallyPoints = false;
250 if (_rallyPointController.supported()) {
251 qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadGeoFenceComplete calling _rallyPointController.loadFromVehicle";
252 _rallyPointController.loadFromVehicle();
253 } else {
254 qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadMissionComplete Rally Points not supported skipping";
255 _rallyPointController.removeAll();
256 _loadRallyPointsComplete();
257 }
258 }
259}
260
261void PlanMasterController::_loadRallyPointsComplete(void)
262{
263 qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadRallyPointsComplete";
264 _setDirtyStates(true /* dirtyForSave */, false /* dirtyForUpload */);
265}
266
267void PlanMasterController::_sendMissionComplete(void)
268{
269 if (_sendGeoFence) {
270 _sendGeoFence = false;
271 _sendRallyPoints = true;
272 if (_geoFenceController.supported()) {
273 qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle start GeoFence sendToVehicle";
274 _geoFenceController.sendToVehicle();
275 } else {
276 qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle GeoFence not supported skipping";
277 _sendGeoFenceComplete();
278 }
279 }
280}
281
282void PlanMasterController::_sendGeoFenceComplete(void)
283{
284 if (_sendRallyPoints) {
285 _sendRallyPoints = false;
286 if (_rallyPointController.supported()) {
287 qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle start rally sendToVehicle";
288 _rallyPointController.sendToVehicle();
289 } else {
290 qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle Rally Points not support skipping";
291 _sendRallyPointsComplete();
292 }
293 }
294}
295
296void PlanMasterController::_sendRallyPointsComplete(void)
297{
298 qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle Rally Point send complete";
299 _setDirtyForUpload(false);
300 if (_deleteWhenSendCompleted) {
301 this->deleteLater();
302 }
303}
304
306{
307 SharedLinkInterfacePtr sharedLink = _managerVehicle->vehicleLinkManager()->primaryLink().lock();
308 if (sharedLink) {
309 if (sharedLink->linkConfiguration()->isHighLatency()) {
310 qgcApp()->showAppMessage(tr("Upload not supported on high latency links."));
311 return;
312 }
313 } else {
314 // Vehicle is shutting down
315 return;
316 }
317
318 if (offline()) {
319 qCCritical(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle called while offline";
320 } else if (syncInProgress()) {
321 qCCritical(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle called while syncInProgress";
322 } else {
323 qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle start mission sendToVehicle";
324 _sendGeoFence = true;
325 _missionController.sendToVehicle();
326 }
327}
328
329void PlanMasterController::loadFromFile(const QString& filename)
330{
331 QString errorString;
332 QString errorMessage = tr("Error loading Plan file (%1). %2").arg(filename).arg("%1");
333
334 if (filename.isEmpty()) {
335 return;
336 }
337
338 QFileInfo fileInfo(filename);
339 QFile file(filename);
340
341 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
342 errorString = file.errorString() + QStringLiteral(" ") + filename;
343 qgcApp()->showAppMessage(errorMessage.arg(errorString));
344 return;
345 }
346
347 bool success = false;
348 if (fileInfo.suffix() == AppSettings::waypointsFileExtension || fileInfo.suffix() == QStringLiteral("txt")) {
349 if (!_missionController.loadTextFile(file, errorString)) {
350 qgcApp()->showAppMessage(errorMessage.arg(errorString));
351 } else {
352 success = true;
353 }
354 } else {
355 QJsonDocument jsonDoc;
356 QByteArray bytes = file.readAll();
357
358 if (!JsonParsing::isJsonFile(bytes, jsonDoc, errorString)) {
359 qgcApp()->showAppMessage(errorMessage.arg(errorString));
360 return;
361 }
362
363 QJsonObject json = jsonDoc.object();
364 //-- Allow plugins to pre process the load
365 QGCCorePlugin::instance()->preLoadFromJson(this, json);
366
367 int version;
369 qgcApp()->showAppMessage(errorMessage.arg(errorString));
370 return;
371 }
372
373 QList<JsonHelper::KeyValidateInfo> rgKeyInfo = {
374 { kJsonMissionObjectKey, QJsonValue::Object, true },
375 { kJsonGeoFenceObjectKey, QJsonValue::Object, true },
376 { kJsonRallyPointsObjectKey, QJsonValue::Object, true },
377 };
378 if (!JsonHelper::validateKeys(json, rgKeyInfo, errorString)) {
379 qgcApp()->showAppMessage(errorMessage.arg(errorString));
380 return;
381 }
382
383 if (!_missionController.load(json[kJsonMissionObjectKey].toObject(), errorString) ||
384 !_geoFenceController.load(json[kJsonGeoFenceObjectKey].toObject(), errorString) ||
385 !_rallyPointController.load(json[kJsonRallyPointsObjectKey].toObject(), errorString)) {
386 qgcApp()->showAppMessage(errorMessage.arg(errorString));
387 } else {
388 //-- Allow plugins to post process the load
389 QGCCorePlugin::instance()->postLoadFromJson(this, json);
390 success = true;
391 }
392 }
393
394 if (success){
395 const bool oldRenamed = planFileRenamed();
396 _currentPlanFile = QString::asprintf("%s/%s.%s", fileInfo.path().toLocal8Bit().data(), fileInfo.completeBaseName().toLocal8Bit().data(), AppSettings::planFileExtension);
397 const bool currentNameChanged = (_currentPlanFileName != fileInfo.completeBaseName());
398 const bool originalNameChanged = (_originalPlanFileName != fileInfo.completeBaseName());
399 _currentPlanFileName = fileInfo.completeBaseName();
400 _originalPlanFileName = _currentPlanFileName;
401 _setDirtyStates(false /* dirtyForSave */, true /* dirtyForUpload */);
403 if (currentNameChanged) {
405 }
406 if (originalNameChanged) {
408 }
409 if (oldRenamed != planFileRenamed()) {
411 }
412 } else {
413 const bool hadFile = !_currentPlanFile.isEmpty();
414 const bool hadCurrentName = !_currentPlanFileName.isEmpty();
415 const bool hadOriginalName = !_originalPlanFileName.isEmpty();
416 const bool wasRenamed = planFileRenamed();
417 _currentPlanFile.clear();
418 _currentPlanFileName.clear();
419 _originalPlanFileName.clear();
420 if (hadFile) {
422 }
423 if (hadCurrentName) {
425 }
426 if (hadOriginalName) {
428 }
429 if (wasRenamed != planFileRenamed()) {
431 }
432 }
433}
434
436{
437 QJsonObject planJson;
438 QGCCorePlugin::instance()->preSaveToJson(this, planJson);
439 QJsonObject missionJson;
440 QJsonObject fenceJson;
441 QJsonObject rallyJson;
443 //-- Allow plugin to preemptly add its own keys to mission
444 QGCCorePlugin::instance()->preSaveToMissionJson(this, missionJson);
445 _missionController.save(missionJson);
446 //-- Allow plugin to add its own keys to mission
447 QGCCorePlugin::instance()->postSaveToMissionJson(this, missionJson);
448 _geoFenceController.save(fenceJson);
449 _rallyPointController.save(rallyJson);
450 planJson[kJsonMissionObjectKey] = missionJson;
451 planJson[kJsonGeoFenceObjectKey] = fenceJson;
452 planJson[kJsonRallyPointsObjectKey] = rallyJson;
453 QGCCorePlugin::instance()->postSaveToJson(this, planJson);
454 return QJsonDocument(planJson);
455}
456
457bool
459{
460 if (!_currentPlanFile.isEmpty()) {
461 const bool saveSuccess = saveToFile(_currentPlanFile);
462 return saveSuccess;
463 }
464
465 return false;
466}
467
468bool PlanMasterController::saveToFile(const QString& filename)
469{
470 if (filename.isEmpty()) {
471 return false;
472 }
473
474 QString planFilename = filename;
475 if (!QFileInfo(filename).fileName().contains(".")) {
476 planFilename += QString(".%1").arg(fileExtension());
477 }
478
479 QFile file(planFilename);
480
481 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
482 qgcApp()->showAppMessage(tr("Plan save error %1 : %2").arg(filename).arg(file.errorString()));
483 return false;
484 } else {
485 const QByteArray saveBytes = saveToJson().toJson();
486 const qint64 bytesWritten = file.write(saveBytes);
487 if (bytesWritten != saveBytes.size()) {
488 qgcApp()->showAppMessage(tr("Plan save error %1 : %2").arg(filename).arg(file.errorString()));
489 return false;
490 }
491 if(_currentPlanFile != planFilename) {
492 _currentPlanFile = planFilename;
494 }
495 const bool wasRenamed = planFileRenamed();
496 const QString savedBaseName = QFileInfo(planFilename).completeBaseName();
497 if (_currentPlanFileName != savedBaseName) {
498 _currentPlanFileName = savedBaseName;
500 }
501 if (_originalPlanFileName != savedBaseName) {
502 _originalPlanFileName = savedBaseName;
504 }
505 if (wasRenamed != planFileRenamed()) {
507 }
508 _setDirtyForSave(false);
509 }
510
511 return true;
512}
513
514void PlanMasterController::saveToKml(const QString& filename)
515{
516 if (filename.isEmpty()) {
517 return;
518 }
519
520 QString kmlFilename = filename;
521 if (!QFileInfo(filename).fileName().contains(".")) {
522 kmlFilename += QString(".%1").arg(kmlFileExtension());
523 }
524
525 QFile file(kmlFilename);
526
527 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
528 qgcApp()->showAppMessage(tr("KML save error %1 : %2").arg(filename).arg(file.errorString()));
529 } else {
530 KMLPlanDomDocument planKML;
531 _missionController.addMissionToKML(planKML);
532 QTextStream stream(&file);
533 stream << planKML.toString();
534 file.close();
535 }
536}
537
539{
540 _suppressOverallDirtyUpdate = true;
541 _missionController.removeAll();
542 _geoFenceController.removeAll();
543 _rallyPointController.removeAll();
544 _missionController.setDirty(false);
545 _geoFenceController.setDirty(false);
546 _rallyPointController.setDirty(false);
547 _suppressOverallDirtyUpdate = false;
548
549 _setDirtyStates(false, false);
550 if (_offline) {
551 _clearFileNames();
552 }
553 setManualCreation(false);
554}
555
557{
558 if (!offline()) {
559 _missionController.removeAllFromVehicle();
560 if (_geoFenceController.supported()) {
561 _geoFenceController.removeAllFromVehicle();
562 }
563 if (_rallyPointController.supported()) {
564 _rallyPointController.removeAllFromVehicle();
565 }
566 _setDirtyForUpload(false);
567 _clearFileNames();
568 } else {
569 qCCritical(PlanMasterControllerLog) << "PlanMasterController::removeAllFromVehicle called while offline";
570 }
571 setManualCreation(false);
572}
573
575{
576 return _missionController.containsItems() || _geoFenceController.containsItems() || _rallyPointController.containsItems();
577}
578
583
585{
586 // Normalize to a base name: trim whitespace, strip known extension, remove illegal characters
587 QString sanitized = name.trimmed();
588 const QString ext = QStringLiteral(".") + fileExtension();
589 if (sanitized.endsWith(ext, Qt::CaseInsensitive)) {
590 sanitized.chop(ext.length());
591 sanitized = sanitized.trimmed();
592 }
593 sanitized.remove(QRegularExpression(QStringLiteral("[/\\\\:*?\"<>|]")));
594 if (_currentPlanFileName != sanitized) {
595 const bool wasRenamed = planFileRenamed();
596 _currentPlanFileName = sanitized;
598 if (wasRenamed != planFileRenamed()) {
600 }
601 }
602}
603
605{
606 if (_currentPlanFileName.isEmpty()) {
607 return false;
608 }
609 return saveToFile(_resolvedPlanFilePath());
610}
611
613{
614 return !_originalPlanFileName.isEmpty() && _currentPlanFileName != _originalPlanFileName;
615}
616
618{
619 if (_currentPlanFileName.isEmpty()) {
620 return false;
621 }
622 return QFile::exists(_resolvedPlanFilePath());
623}
624
625QString PlanMasterController::_resolvedPlanFilePath() const
626{
627 const QString dir = _currentPlanFile.isEmpty()
628 ? SettingsManager::instance()->appSettings()->missionSavePath()
629 : QFileInfo(_currentPlanFile).path();
630 return QStringLiteral("%1/%2.%3").arg(dir, _currentPlanFileName, fileExtension());
631}
632
633void PlanMasterController::_clearFileNames()
634{
635 const bool hadFile = !_currentPlanFile.isEmpty();
636 const bool hadCurrentName = !_currentPlanFileName.isEmpty();
637 const bool hadOriginalName = !_originalPlanFileName.isEmpty();
638 const bool wasRenamed = planFileRenamed();
639 _currentPlanFile.clear();
640 _currentPlanFileName.clear();
641 _originalPlanFileName.clear();
642 if (hadFile) {
644 }
645 if (hadCurrentName) {
647 }
648 if (hadOriginalName) {
650 }
651 if (wasRenamed != planFileRenamed()) {
653 }
654}
655
660
662{
663 QStringList filters;
664
665 filters << tr("Supported types (*.%1 *.%2 *.%3)").arg(AppSettings::planFileExtension).arg(AppSettings::waypointsFileExtension).arg("txt") <<
666 tr("All Files (*)");
667 return filters;
668}
669
670
672{
673 QStringList filters;
674
675 filters << tr("Plan Files (*.%1)").arg(fileExtension()) << tr("All Files (*)");
676 return filters;
677}
678
679void PlanMasterController::sendPlanToVehicle(Vehicle* vehicle, const QString& filename)
680{
681 // Use a transient PlanMasterController to accomplish this
682 PlanMasterController* controller = new PlanMasterController();
683 controller->startStaticActiveVehicle(vehicle, true /* deleteWhenSendCompleted */);
684 controller->loadFromFile(filename);
685 controller->sendToVehicle();
686}
687
688void PlanMasterController::_showPlanFromManagerVehicle(void)
689{
690 if (!_managerVehicle->initialPlanRequestComplete() && !syncInProgress()) {
691 // Something went wrong with initial load. All controllers are idle, so just force it off
692 _managerVehicle->forceInitialPlanRequestComplete();
693 }
694
695 // The crazy if structure is to handle the load propagating by itself through the system
696 if (!_missionController.showPlanFromManagerVehicle()) {
697 if (!_geoFenceController.showPlanFromManagerVehicle()) {
698 _rallyPointController.showPlanFromManagerVehicle();
699 }
700 }
701
702 // Showing the vehicle plan should leave both dirty states clean.
703 _missionController.setDirty(false);
704 _geoFenceController.setDirty(false);
705 _rallyPointController.setDirty(false);
706 _setDirtyStates(false, false);
707}
708
710{
711 return _missionController.syncInProgress() ||
712 _geoFenceController.syncInProgress() ||
713 _rallyPointController.syncInProgress();
714}
715
717{
718 return _missionController.isEmpty() &&
719 _geoFenceController.isEmpty() &&
720 _rallyPointController.isEmpty();
721}
722
723void PlanMasterController::_updateOverallDirty(void)
724{
725 if (syncInProgress() || _suppressOverallDirtyUpdate) {
726 return;
727 }
728
729 const bool saveDirty = _missionController.dirty() || _geoFenceController.dirty() || _rallyPointController.dirty();
730 if (saveDirty) {
731 _setDirtyForSave(true);
732 }
733}
734
735void PlanMasterController::_setDirtyForSave(bool dirtyForSave)
736{
737 if (_dirtyForSave != dirtyForSave) {
738 _dirtyForSave = dirtyForSave;
739 emit dirtyForSaveChanged(_dirtyForSave);
740
741 if (_dirtyForSave) {
742 _setDirtyForUpload(true);
743 }
744 }
745}
746
747void PlanMasterController::_setDirtyForUpload(bool dirtyForUpload)
748{
749 if (_dirtyForUpload != dirtyForUpload) {
750 _dirtyForUpload = dirtyForUpload;
751 emit dirtyForUploadChanged(_dirtyForUpload);
752 }
753}
754
755void PlanMasterController::_setDirtyStates(bool dirtyForSave, bool dirtyForUpload)
756{
757 const bool saveChanged = (_dirtyForSave != dirtyForSave);
758 const bool uploadChanged = (_dirtyForUpload != dirtyForUpload);
759
760 _dirtyForSave = dirtyForSave;
761 _dirtyForUpload = dirtyForUpload;
762
763 if (saveChanged) {
764 emit dirtyForSaveChanged(_dirtyForSave);
765 }
766 if (uploadChanged) {
767 emit dirtyForUploadChanged(_dirtyForUpload);
768 }
769}
770
771void PlanMasterController::_updatePlanCreatorsList(void)
772{
773 if (!_flyView) {
774 if (!_planCreators) {
775 _planCreators = new QmlObjectListModel(this);
776 _planCreators->append(new BlankPlanCreator(this, this));
777 _planCreators->append(new SurveyPlanCreator(this, this));
778 _planCreators->append(new CorridorScanPlanCreator(this, this));
779 emit planCreatorsChanged(_planCreators);
780 }
781
782 if (_managerVehicle->fixedWing()) {
783 if (_planCreators->count() == 4) {
784 _planCreators->removeAt(_planCreators->count() - 1);
785 }
786 } else {
787 if (_planCreators->count() != 4) {
788 _planCreators->append(new StructureScanPlanCreator(this, this));
789 }
790 }
791 }
792}
793
795{
796 if (offline()) {
797 // There is no new vehicle so clear any previous plan
798 qCDebug(PlanMasterControllerLog) << "showPlanFromManagerVehicle: Plan View - No new vehicle, clear any previous plan";
799 removeAll();
800 } else {
801 // We have a new active vehicle, show the plan from that
802 qCDebug(PlanMasterControllerLog) << "showPlanFromManagerVehicle: Plan View - New vehicle available, show plan from new manager vehicle";
803 _showPlanFromManagerVehicle();
804 }
805}
806
808{
809 if (_manualCreation != manualCreation) {
810 _manualCreation = manualCreation;
812 }
813}
814
815void PlanMasterController::loadFromArchive(const QString& archivePath)
816{
817 if (archivePath.isEmpty()) {
818 return;
819 }
820
821 if (!QFile::exists(archivePath)) {
822 qgcApp()->showAppMessage(tr("Archive file not found: %1").arg(archivePath));
823 return;
824 }
825
826 if (!QGCCompression::isArchiveFile(archivePath)) {
827 qgcApp()->showAppMessage(tr("Not a supported archive format: %1").arg(archivePath));
828 return;
829 }
830
831 const QString tempPath = QDir::temp().filePath(QStringLiteral("qgc_plan_") + QString::number(QDateTime::currentMSecsSinceEpoch()));
832 if (!QDir().mkpath(tempPath)) {
833 qgcApp()->showAppMessage(tr("Could not create temporary directory"));
834 return;
835 }
836
837 _extractionOutputDir = tempPath;
838
839 if (_extractionJob == nullptr) {
840 _extractionJob = new QGCCompressionJob(this);
841 connect(_extractionJob, &QGCCompressionJob::finished,
842 this, &PlanMasterController::_handleExtractionFinished);
843 }
844
845 _extractionJob->extractArchive(archivePath, tempPath);
846}
847
848void PlanMasterController::_handleExtractionFinished(bool success)
849{
850 if (!success) {
851 const QString error = _extractionJob != nullptr ? _extractionJob->errorString() : tr("Extraction failed");
852 qgcApp()->showAppMessage(tr("Failed to extract plan archive: %1").arg(error));
853 QDir(_extractionOutputDir).removeRecursively();
854 _extractionOutputDir.clear();
855 return;
856 }
857
858 QString planPath;
859 const QString planExt = QStringLiteral("*.") + AppSettings::planFileExtension;
860 QDirIterator it(_extractionOutputDir, {planExt}, QDir::Files, QDirIterator::Subdirectories);
861 if (it.hasNext()) {
862 planPath = it.next();
863 }
864
865 if (planPath.isEmpty()) {
866 qgcApp()->showAppMessage(tr("No plan file found in archive"));
867 QDir(_extractionOutputDir).removeRecursively();
868 _extractionOutputDir.clear();
869 return;
870 }
871
872 qCDebug(PlanMasterControllerLog) << "Found plan file in archive:" << planPath;
873 loadFromFile(planPath);
874
875 QDir(_extractionOutputDir).removeRecursively();
876 _extractionOutputDir.clear();
877}
std::shared_ptr< LinkInterface > SharedLinkInterfacePtr
#define qgcApp()
QObject wrapper for async compression operations using QtConcurrent/QPromise.
QString errorString
Error error
#define QGC_LOGGING_CATEGORY(name, categoryStr)
Application Settings.
Definition AppSettings.h:9
static constexpr const char * planFileExtension
Definition AppSettings.h:89
static constexpr const char * kmlFileExtension
Definition AppSettings.h:92
static constexpr const char * waypointsFileExtension
Definition AppSettings.h:90
Fact *offlineEditingFirmwareClass READ offlineEditingFirmwareClass CONSTANT Fact * offlineEditingFirmwareClass()
Fact *offlineEditingVehicleClass READ offlineEditingVehicleClass CONSTANT Fact * offlineEditingVehicleClass()
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
void activeVehicleChanged(Vehicle *activeVehicle)
void syncInProgressChanged(bool syncInProgress)
void dirtyChanged(bool dirty)
void sendComplete(bool error)
void newMissionItemsAvailable(bool removeAllRequested)
Master controller for mission, fence, rally.
void removeAll(void)
Removes all from controller only, synce required to remove from vehicle.
PlanMasterController(QObject *parent=nullptr)
void planFileRenamedChanged(void)
bool saveWithCurrentName()
Save using the (possibly renamed) currentPlanFileName.
void setCurrentPlanFileName(const QString &name)
void originalPlanFileNameChanged(void)
QStringList saveNameFilters(void) const
void currentPlanFileNameChanged(void)
void setManualCreation(bool manualCreation)
void promptForPlanUsageOnVehicleChange(void)
void saveToKml(const QString &filename)
void showPlanFromManagerVehicle(void)
Replaces any current plan with the plan from the manager vehicle even if offline.
static constexpr int kPlanFileVersion
QStringList loadNameFilters(void) const
void planCreatorsChanged(QmlObjectListModel *planCreators)
void managerVehicleChanged(Vehicle *managerVehicle)
QString kmlFileExtension(void) const
void syncInProgressChanged(void)
void loadFromFile(const QString &filename)
void startStaticActiveVehicle(Vehicle *vehicle, bool deleteWhenSendCompleted=false)
bool dirtyForUpload(void) const
static constexpr const char * kJsonGeoFenceObjectKey
void loadFromArchive(const QString &archivePath)
void dirtyForSaveChanged(bool dirtyForSave)
bool manualCreation(void) const
bool syncInProgress(void) const
void currentPlanFileChanged(void)
bool dirtyForSave(void) const
QString fileExtension(void) const
bool saveToFile(const QString &filename)
bool planFileRenamed(void) const
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
void removeAllFromVehicle(void)
Removes all from vehicle and controller.
static void sendPlanToVehicle(Vehicle *vehicle, const QString &filename)
void offlineChanged(bool offlineEditing)
static constexpr const char * kJsonRallyPointsObjectKey
void finished(bool success)
void extractArchive(const QString &archivePath, const QString &outputDirectoryPath, qint64 maxBytes=0)
void append(QObject *object)
Caller maintains responsibility for object ownership and deletion.
QObject * removeAt(int index)
int count() const override final
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)
WeakLinkInterfacePtr primaryLink() const
void forceInitialPlanRequestComplete()
Definition Vehicle.cc:3610
MAV_TYPE vehicleType() const
Definition Vehicle.h:428
VehicleLinkManager * vehicleLinkManager()
Definition Vehicle.h:580
MAV_AUTOPILOT firmwareType() const
Definition Vehicle.h:427
GeoFenceManager * geoFenceManager()
Definition Vehicle.h:576
RallyPointManager * rallyPointManager()
Definition Vehicle.h:577
bool initialPlanRequestComplete() const
Definition Vehicle.h:744
bool fixedWing() const
Definition Vehicle.cc:1844
MissionManager * missionManager()
Definition Vehicle.h:575
void vehicleTypeChanged()
bool validateExternalQGCJsonFile(const QJsonObject &jsonObject, const QString &expectedFileType, int minSupportedVersion, int maxSupportedVersion, int &version, QString &errorString)
returned error string if validation fails
bool validateKeys(const QJsonObject &jsonObject, const QList< KeyValidateInfo > &keyInfo, QString &errorString)
void saveQGCJsonFileHeader(QJsonObject &jsonObject, const QString &fileType, int version)
Saves the standard file header the json object.
bool isJsonFile(const QByteArray &bytes, QJsonDocument &jsonDoc, QString &errorString)
Determines whether an in-memory byte buffer contains parseable JSON content.
bool isArchiveFile(const QString &filePath)
Check if file path indicates an archive file (.zip, .tar, .tar.gz, etc.)