24#include <QtCore/QDirIterator>
25#include <QtCore/QFileInfo>
26#include <QtCore/QJsonDocument>
27#include <QtCore/QRegularExpression>
35 , _managerVehicle (_controllerVehicle)
36 , _missionController (this)
37 , _geoFenceController (this)
38 , _rallyPointController (this)
47 , _controllerVehicle (new
Vehicle(firmwareType, vehicleType))
48 , _managerVehicle (_controllerVehicle)
49 , _missionController (this)
50 , _geoFenceController (this)
51 , _rallyPointController (this)
57void PlanMasterController::_commonInit(
void)
81void PlanMasterController::start(
void)
83 _missionController.
start (_flyView);
84 _geoFenceController.
start (_flyView);
85 _rallyPointController.
start (_flyView);
87 _activeVehicleChanged(_multiVehicleMgr->activeVehicle());
90 _updatePlanCreatorsList();
96 _deleteWhenSendCompleted = deleteWhenSendCompleted;
97 _missionController.
start(_flyView);
98 _geoFenceController.
start(_flyView);
99 _rallyPointController.
start(_flyView);
100 _activeVehicleChanged(vehicle);
103void PlanMasterController::_activeVehicleChanged(
Vehicle* activeVehicle)
105 if (_managerVehicle == activeVehicle) {
110 qCDebug(PlanMasterControllerLog) <<
"_activeVehicleChanged" << activeVehicle;
112 if (_managerVehicle) {
114 disconnect(_managerVehicle->
missionManager(),
nullptr,
this,
nullptr);
115 disconnect(_managerVehicle->
geoFenceManager(),
nullptr,
this,
nullptr);
119 bool newOffline =
false;
120 if (activeVehicle ==
nullptr) {
122 _managerVehicle = _controllerVehicle;
126 _managerVehicle = activeVehicle;
129 AppSettings* appSettings = SettingsManager::instance()->appSettings();
142 _offline = newOffline;
150 qCDebug(PlanMasterControllerLog) <<
"_activeVehicleChanged: Fly View - No active vehicle, clearing stale plan";
154 qCDebug(PlanMasterControllerLog) <<
"_activeVehicleChanged: Fly View - New active vehicle, loading new plan from manager vehicle";
155 _showPlanFromManagerVehicle();
161 _setDirtyForUpload(
true);
166 qCDebug(PlanMasterControllerLog) <<
"_activeVehicleChanged: Plan View - Previous dirty plan exists, no new active vehicle, sending promptForPlanUsageOnVehicleChange signal";
172 qCDebug(PlanMasterControllerLog) <<
"_activeVehicleChanged: Plan View - Previous clean plan exists, no new active vehicle, clear stale plan";
176 qCDebug(PlanMasterControllerLog) <<
"_activeVehicleChanged: Plan View - Previous clean plan exists, new active vehicle, loading from new manager vehicle";
177 _showPlanFromManagerVehicle();
182 _setDirtyStates(
false,
false);
185 qCDebug(PlanMasterControllerLog) <<
"_activeVehicleChanged: Plan View - No previous plan, no longer connected to vehicle, nothing to do";
188 qCDebug(PlanMasterControllerLog) <<
"_activeVehicleChanged: Plan View - No previous plan, new active vehicle, loading from new manager vehicle";
189 _showPlanFromManagerVehicle();
200 _updatePlanCreatorsList();
207 if (sharedLink->linkConfiguration()->isHighLatency()) {
208 qgcApp()->showAppMessage(tr(
"Download not supported on high latency links."));
217 qCCritical(PlanMasterControllerLog) <<
"PlanMasterController::loadFromVehicle called while offline";
218 }
else if (_flyView) {
219 qCCritical(PlanMasterControllerLog) <<
"PlanMasterController::loadFromVehicle called from Fly view";
221 qCCritical(PlanMasterControllerLog) <<
"PlanMasterController::loadFromVehicle called while syncInProgress";
223 _loadGeoFence =
true;
224 qCDebug(PlanMasterControllerLog) <<
"PlanMasterController::loadFromVehicle calling _missionController.loadFromVehicle";
230void PlanMasterController::_loadMissionComplete(
void)
232 if (!_flyView && _loadGeoFence) {
233 _loadGeoFence =
false;
234 _loadRallyPoints =
true;
236 qCDebug(PlanMasterControllerLog) <<
"PlanMasterController::_loadMissionComplete calling _geoFenceController.loadFromVehicle";
239 qCDebug(PlanMasterControllerLog) <<
"PlanMasterController::_loadMissionComplete GeoFence not supported skipping";
241 _loadGeoFenceComplete();
246void PlanMasterController::_loadGeoFenceComplete(
void)
248 if (!_flyView && _loadRallyPoints) {
249 _loadRallyPoints =
false;
251 qCDebug(PlanMasterControllerLog) <<
"PlanMasterController::_loadGeoFenceComplete calling _rallyPointController.loadFromVehicle";
254 qCDebug(PlanMasterControllerLog) <<
"PlanMasterController::_loadMissionComplete Rally Points not supported skipping";
256 _loadRallyPointsComplete();
261void PlanMasterController::_loadRallyPointsComplete(
void)
263 qCDebug(PlanMasterControllerLog) <<
"PlanMasterController::_loadRallyPointsComplete";
264 _setDirtyStates(
true ,
false );
267void PlanMasterController::_sendMissionComplete(
void)
270 _sendGeoFence =
false;
271 _sendRallyPoints =
true;
273 qCDebug(PlanMasterControllerLog) <<
"PlanMasterController::sendToVehicle start GeoFence sendToVehicle";
276 qCDebug(PlanMasterControllerLog) <<
"PlanMasterController::sendToVehicle GeoFence not supported skipping";
277 _sendGeoFenceComplete();
282void PlanMasterController::_sendGeoFenceComplete(
void)
284 if (_sendRallyPoints) {
285 _sendRallyPoints =
false;
287 qCDebug(PlanMasterControllerLog) <<
"PlanMasterController::sendToVehicle start rally sendToVehicle";
290 qCDebug(PlanMasterControllerLog) <<
"PlanMasterController::sendToVehicle Rally Points not support skipping";
291 _sendRallyPointsComplete();
296void PlanMasterController::_sendRallyPointsComplete(
void)
298 qCDebug(PlanMasterControllerLog) <<
"PlanMasterController::sendToVehicle Rally Point send complete";
299 _setDirtyForUpload(
false);
300 if (_deleteWhenSendCompleted) {
309 if (sharedLink->linkConfiguration()->isHighLatency()) {
310 qgcApp()->showAppMessage(tr(
"Upload not supported on high latency links."));
319 qCCritical(PlanMasterControllerLog) <<
"PlanMasterController::sendToVehicle called while offline";
321 qCCritical(PlanMasterControllerLog) <<
"PlanMasterController::sendToVehicle called while syncInProgress";
323 qCDebug(PlanMasterControllerLog) <<
"PlanMasterController::sendToVehicle start mission sendToVehicle";
324 _sendGeoFence =
true;
332 QString errorMessage = tr(
"Error loading Plan file (%1). %2").arg(filename).arg(
"%1");
334 if (filename.isEmpty()) {
338 QFileInfo fileInfo(filename);
339 QFile file(filename);
341 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
342 errorString = file.errorString() + QStringLiteral(
" ") + filename;
347 bool success =
false;
355 QJsonDocument jsonDoc;
356 QByteArray bytes = file.readAll();
363 QJsonObject json = jsonDoc.object();
365 QGCCorePlugin::instance()->preLoadFromJson(
this, json);
373 QList<JsonHelper::KeyValidateInfo> rgKeyInfo = {
389 QGCCorePlugin::instance()->postLoadFromJson(
this, json);
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 ,
true );
403 if (currentNameChanged) {
406 if (originalNameChanged) {
413 const bool hadFile = !_currentPlanFile.isEmpty();
414 const bool hadCurrentName = !_currentPlanFileName.isEmpty();
415 const bool hadOriginalName = !_originalPlanFileName.isEmpty();
417 _currentPlanFile.clear();
418 _currentPlanFileName.clear();
419 _originalPlanFileName.clear();
423 if (hadCurrentName) {
426 if (hadOriginalName) {
437 QJsonObject planJson;
438 QGCCorePlugin::instance()->preSaveToJson(
this, planJson);
439 QJsonObject missionJson;
440 QJsonObject fenceJson;
441 QJsonObject rallyJson;
444 QGCCorePlugin::instance()->preSaveToMissionJson(
this, missionJson);
445 _missionController.
save(missionJson);
447 QGCCorePlugin::instance()->postSaveToMissionJson(
this, missionJson);
448 _geoFenceController.
save(fenceJson);
449 _rallyPointController.
save(rallyJson);
453 QGCCorePlugin::instance()->postSaveToJson(
this, planJson);
454 return QJsonDocument(planJson);
460 if (!_currentPlanFile.isEmpty()) {
461 const bool saveSuccess =
saveToFile(_currentPlanFile);
470 if (filename.isEmpty()) {
474 QString planFilename = filename;
475 if (!QFileInfo(filename).fileName().contains(
".")) {
479 QFile file(planFilename);
481 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
482 qgcApp()->showAppMessage(tr(
"Plan save error %1 : %2").arg(filename).arg(file.errorString()));
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()));
491 if(_currentPlanFile != planFilename) {
492 _currentPlanFile = planFilename;
496 const QString savedBaseName = QFileInfo(planFilename).completeBaseName();
497 if (_currentPlanFileName != savedBaseName) {
498 _currentPlanFileName = savedBaseName;
501 if (_originalPlanFileName != savedBaseName) {
502 _originalPlanFileName = savedBaseName;
508 _setDirtyForSave(
false);
516 if (filename.isEmpty()) {
520 QString kmlFilename = filename;
521 if (!QFileInfo(filename).fileName().contains(
".")) {
525 QFile file(kmlFilename);
527 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
528 qgcApp()->showAppMessage(tr(
"KML save error %1 : %2").arg(filename).arg(file.errorString()));
532 QTextStream stream(&file);
533 stream << planKML.toString();
540 _suppressOverallDirtyUpdate =
true;
545 _geoFenceController.
setDirty(
false);
546 _rallyPointController.
setDirty(
false);
547 _suppressOverallDirtyUpdate =
false;
549 _setDirtyStates(
false,
false);
566 _setDirtyForUpload(
false);
569 qCCritical(PlanMasterControllerLog) <<
"PlanMasterController::removeAllFromVehicle called while offline";
587 QString sanitized = name.trimmed();
589 if (sanitized.endsWith(ext, Qt::CaseInsensitive)) {
590 sanitized.chop(ext.length());
591 sanitized = sanitized.trimmed();
593 sanitized.remove(QRegularExpression(QStringLiteral(
"[/\\\\:*?\"<>|]")));
594 if (_currentPlanFileName != sanitized) {
596 _currentPlanFileName = sanitized;
606 if (_currentPlanFileName.isEmpty()) {
614 return !_originalPlanFileName.isEmpty() && _currentPlanFileName != _originalPlanFileName;
619 if (_currentPlanFileName.isEmpty()) {
622 return QFile::exists(_resolvedPlanFilePath());
625QString PlanMasterController::_resolvedPlanFilePath()
const
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());
633void PlanMasterController::_clearFileNames()
635 const bool hadFile = !_currentPlanFile.isEmpty();
636 const bool hadCurrentName = !_currentPlanFileName.isEmpty();
637 const bool hadOriginalName = !_originalPlanFileName.isEmpty();
639 _currentPlanFile.clear();
640 _currentPlanFileName.clear();
641 _originalPlanFileName.clear();
645 if (hadCurrentName) {
648 if (hadOriginalName) {
675 filters << tr(
"Plan Files (*.%1)").arg(
fileExtension()) << tr(
"All Files (*)");
688void PlanMasterController::_showPlanFromManagerVehicle(
void)
704 _geoFenceController.
setDirty(
false);
705 _rallyPointController.
setDirty(
false);
706 _setDirtyStates(
false,
false);
718 return _missionController.
isEmpty() &&
719 _geoFenceController.
isEmpty() &&
720 _rallyPointController.
isEmpty();
723void PlanMasterController::_updateOverallDirty(
void)
729 const bool saveDirty = _missionController.
dirty() || _geoFenceController.
dirty() || _rallyPointController.
dirty();
731 _setDirtyForSave(
true);
735void PlanMasterController::_setDirtyForSave(
bool dirtyForSave)
742 _setDirtyForUpload(
true);
747void PlanMasterController::_setDirtyForUpload(
bool dirtyForUpload)
755void PlanMasterController::_setDirtyStates(
bool dirtyForSave,
bool dirtyForUpload)
757 const bool saveChanged = (_dirtyForSave !=
dirtyForSave);
771void PlanMasterController::_updatePlanCreatorsList(
void)
774 if (!_planCreators) {
783 if (_planCreators->
count() == 4) {
787 if (_planCreators->
count() != 4) {
798 qCDebug(PlanMasterControllerLog) <<
"showPlanFromManagerVehicle: Plan View - No new vehicle, clear any previous plan";
802 qCDebug(PlanMasterControllerLog) <<
"showPlanFromManagerVehicle: Plan View - New vehicle available, show plan from new manager vehicle";
803 _showPlanFromManagerVehicle();
817 if (archivePath.isEmpty()) {
821 if (!QFile::exists(archivePath)) {
822 qgcApp()->showAppMessage(tr(
"Archive file not found: %1").arg(archivePath));
827 qgcApp()->showAppMessage(tr(
"Not a supported archive format: %1").arg(archivePath));
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"));
837 _extractionOutputDir = tempPath;
839 if (_extractionJob ==
nullptr) {
842 this, &PlanMasterController::_handleExtractionFinished);
848void PlanMasterController::_handleExtractionFinished(
bool 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();
860 QDirIterator it(_extractionOutputDir, {planExt}, QDir::Files, QDirIterator::Subdirectories);
862 planPath = it.next();
865 if (planPath.isEmpty()) {
866 qgcApp()->showAppMessage(tr(
"No plan file found in archive"));
867 QDir(_extractionOutputDir).removeRecursively();
868 _extractionOutputDir.clear();
872 qCDebug(PlanMasterControllerLog) <<
"Found plan file in archive:" << planPath;
875 QDir(_extractionOutputDir).removeRecursively();
876 _extractionOutputDir.clear();
std::shared_ptr< LinkInterface > SharedLinkInterfacePtr
QObject wrapper for async compression operations using QtConcurrent/QPromise.
#define QGC_LOGGING_CATEGORY(name, categoryStr)
static constexpr const char * planFileExtension
static constexpr const char * kmlFileExtension
static constexpr const char * waypointsFileExtension
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 dirty(void) const final
void start(bool flyView) final
Should be called immediately upon Component.onCompleted.
void save(QJsonObject &json) final
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
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 containsItemsChanged()
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)
void containsItemsChanged()
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 loadFromVehicle(void)
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
void manualCreationChanged()
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)
QJsonDocument saveToJson()
void offlineChanged(bool offlineEditing)
static constexpr const char * kJsonRallyPointsObjectKey
bool containsItems(void) const
void finished(bool success)
void extractArchive(const QString &archivePath, const QString &outputDirectoryPath, qint64 maxBytes=0)
static VehicleClass_t vehicleClass(MAV_TYPE mavType)
static FirmwareClass_t firmwareClass(MAV_AUTOPILOT autopilot)
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 sendToVehicle(void) final
void sendComplete(bool error)
WeakLinkInterfacePtr primaryLink() const
void forceInitialPlanRequestComplete()
MAV_TYPE vehicleType() const
VehicleLinkManager * vehicleLinkManager()
MAV_AUTOPILOT firmwareType() const
GeoFenceManager * geoFenceManager()
RallyPointManager * rallyPointManager()
bool initialPlanRequestComplete() const
MissionManager * missionManager()
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.)