From 22d007a7701208ba28c61f78c0449fc5f11d5011 Mon Sep 17 00:00:00 2001 From: Holden Date: Tue, 27 Aug 2024 17:46:43 -0400 Subject: [PATCH] AnalyzeView: Fix GeoTagWorker Threading --- src/AnalyzeView/CMakeLists.txt | 1 - src/AnalyzeView/ExifParser.cc | 16 +- src/AnalyzeView/ExifParser.h | 7 +- src/AnalyzeView/GeoTagController.cc | 196 ++++++++++------ src/AnalyzeView/GeoTagController.h | 75 +++---- src/AnalyzeView/GeoTagPage.qml | 187 ++++++++-------- src/AnalyzeView/GeoTagWorker.cc | 271 ++++++++++++++--------- src/AnalyzeView/GeoTagWorker.h | 85 +++---- src/AnalyzeView/PX4LogParser.cc | 4 +- src/AnalyzeView/PX4LogParser.h | 2 +- src/AnalyzeView/ULogParser.cc | 4 +- src/AnalyzeView/ULogParser.h | 2 +- src/Utilities/QGCLoggingCategory.cc | 1 - src/Utilities/QGCLoggingCategory.h | 1 - test/AnalyzeView/CMakeLists.txt | 2 + test/AnalyzeView/ExifParserTest.cc | 6 +- test/AnalyzeView/GeoTagControllerTest.cc | 98 ++++++++ test/AnalyzeView/GeoTagControllerTest.h | 12 + test/AnalyzeView/PX4LogParserTest.cc | 4 +- test/AnalyzeView/ULogParserTest.cc | 4 +- test/CMakeLists.txt | 1 + test/UnitTestList.cc | 2 + 22 files changed, 611 insertions(+), 370 deletions(-) create mode 100644 test/AnalyzeView/GeoTagControllerTest.cc create mode 100644 test/AnalyzeView/GeoTagControllerTest.h diff --git a/src/AnalyzeView/CMakeLists.txt b/src/AnalyzeView/CMakeLists.txt index 9cd572d2113..88e0c3de644 100644 --- a/src/AnalyzeView/CMakeLists.txt +++ b/src/AnalyzeView/CMakeLists.txt @@ -101,7 +101,6 @@ if(NOT exiv2_FOUND AND NOT LibExiv2_FOUND) GIT_REPOSITORY https://github.com/Exiv2/exiv2.git GIT_TAG v0.28.3 GIT_SHALLOW TRUE - GIT_PROGRESS TRUE ) set(EXIV2_ENABLE_XMP OFF CACHE INTERNAL "" FORCE) set(EXIV2_ENABLE_EXTERNAL_XMP OFF CACHE INTERNAL "" FORCE) diff --git a/src/AnalyzeView/ExifParser.cc b/src/AnalyzeView/ExifParser.cc index 5aa15cf440e..ad10ec2e12f 100644 --- a/src/AnalyzeView/ExifParser.cc +++ b/src/AnalyzeView/ExifParser.cc @@ -26,7 +26,7 @@ void init() ::atexit(Exiv2::XmpParser::terminate); } -double readTime(const QByteArray &buf) +QDateTime readTime(const QByteArray &buf) { try { // Convert QByteArray to std::string for Exiv2 @@ -36,7 +36,7 @@ double readTime(const QByteArray &buf) const Exiv2::ExifData &exifData = image->exifData(); if (exifData.empty()) { qCWarning(ExifParserLog) << "No EXIF data found in the image."; - return -1.0; + return QDateTime(); } // Read DateTimeOriginal @@ -45,7 +45,7 @@ double readTime(const QByteArray &buf) const Exiv2::ExifData::const_iterator pos = exifData.findKey(key); if (pos == exifData.end()) { qCWarning(ExifParserLog) << "No DateTimeOriginal found."; - return -1.0; + return QDateTime(); } const std::string dateTimeOriginal = pos->toString(); @@ -54,7 +54,7 @@ double readTime(const QByteArray &buf) if (createDateList.size() < 2) { qCWarning(ExifParserLog) << "Invalid date/time format: " << createDateList; - return -1.0; + return QDateTime(); } const QStringList dateList = createDateList[0].split(':'); @@ -62,7 +62,7 @@ double readTime(const QByteArray &buf) if ((dateList.size() < 3) || (timeList.size() < 3)) { qCWarning(ExifParserLog) << "Could not parse creation date/time: " << dateList << " " << timeList; - return -1.0; + return QDateTime(); } const QDate date(dateList[0].toInt(), dateList[1].toInt(), dateList[2].toInt()); @@ -70,14 +70,14 @@ double readTime(const QByteArray &buf) const QDateTime tagTime(date, time); - return (tagTime.toMSecsSinceEpoch() / 1000.0); + return tagTime; } catch (const Exiv2::Error &e) { qCWarning(ExifParserLog) << "Error reading EXIF data:" << e.what(); - return -1.0; + return QDateTime(); } } -bool write(QByteArray &buf, const GeoTagWorker::cameraFeedbackPacket &geotag) +bool write(QByteArray &buf, const GeoTagWorker::CameraFeedbackPacket &geotag) { try { // Convert QByteArray to std::string for Exiv2 diff --git a/src/AnalyzeView/ExifParser.h b/src/AnalyzeView/ExifParser.h index dd804758d00..98b4ad0668b 100644 --- a/src/AnalyzeView/ExifParser.h +++ b/src/AnalyzeView/ExifParser.h @@ -17,8 +17,9 @@ class QByteArray; Q_DECLARE_LOGGING_CATEGORY(ExifParserLog) -namespace ExifParser { +namespace ExifParser +{ void init(); - double readTime(const QByteArray &buf); - bool write(QByteArray &buf, const GeoTagWorker::cameraFeedbackPacket &geotag); + QDateTime readTime(const QByteArray &buf); + bool write(QByteArray &buf, const GeoTagWorker::CameraFeedbackPacket &geotag); } diff --git a/src/AnalyzeView/GeoTagController.cc b/src/AnalyzeView/GeoTagController.cc index da32c74ac37..fbad9731363 100644 --- a/src/AnalyzeView/GeoTagController.cc +++ b/src/AnalyzeView/GeoTagController.cc @@ -8,120 +8,172 @@ ****************************************************************************/ #include "GeoTagController.h" +#include "GeoTagWorker.h" #include "QGCLoggingCategory.h" #include +#include +#include #include QGC_LOGGING_CATEGORY(GeoTagControllerLog, "qgc.analyzeview.geotagcontroller") -GeoTagController::GeoTagController() - : _progress(0) - , _inProgress(false) +GeoTagController::GeoTagController(QObject *parent) + : QObject(parent) + , _worker(new GeoTagWorker()) + , _workerThread(new QThread(this)) { - connect(&_worker, &GeoTagWorker::progressChanged, this, &GeoTagController::_workerProgressChanged); - connect(&_worker, &GeoTagWorker::error, this, &GeoTagController::_workerError); - connect(&_worker, &GeoTagWorker::started, this, &GeoTagController::inProgressChanged); - connect(&_worker, &GeoTagWorker::finished, this, &GeoTagController::inProgressChanged); + // qCDebug(GeoTagControllerLog) << Q_FUNC_INFO << this; + + _worker->moveToThread(_workerThread); + + (void) connect(_worker, &GeoTagWorker::progressChanged, this, &GeoTagController::_workerProgressChanged); + (void) connect(_worker, &GeoTagWorker::error, this, &GeoTagController::_workerError); + (void) connect(_workerThread, &QThread::started, _worker, &GeoTagWorker::process); + (void) connect(_workerThread, &QThread::started, this, &GeoTagController::inProgressChanged); + (void) connect(_workerThread, &QThread::finished, this, &GeoTagController::inProgressChanged); } GeoTagController::~GeoTagController() { + cancelTagging(); + delete _worker; + // qCDebug(GeoTagControllerLog) << Q_FUNC_INFO << this; } -void GeoTagController::setLogFile(QString filename) +void GeoTagController::cancelTagging() { - filename = QUrl(filename).toLocalFile(); - if (!filename.isEmpty()) { - _worker.setLogFile(filename); - emit logFileChanged(filename); - } + (void) QMetaObject::invokeMethod(_worker, "cancelTagging", Qt::AutoConnection); + (void) QMetaObject::invokeMethod(_workerThread, "quit", Qt::AutoConnection); + + _workerThread->wait(); } -void GeoTagController::setImageDirectory(QString dir) +QString GeoTagController::logFile() const { - dir = QUrl(dir).toLocalFile(); - if (!dir.isEmpty()) { - _worker.setImageDirectory(dir); - emit imageDirectoryChanged(dir); - if(_worker.saveDirectory() == "") { - QDir saveDirectory = QDir(_worker.imageDirectory() + kTagged); - if(saveDirectory.exists()) { - _setErrorMessage(tr("Images have alreay been tagged. Existing images will be removed.")); - return; - } - } + return _worker->logFile(); +} + +QString GeoTagController::imageDirectory() const +{ + return _worker->imageDirectory(); +} + +QString GeoTagController::saveDirectory() const +{ + return _worker->saveDirectory(); +} + +bool GeoTagController::inProgress() const +{ + return _workerThread->isRunning(); +} + +void GeoTagController::setLogFile(const QString &filename) +{ + if (filename.isEmpty()) { + _setErrorMessage(tr("Empty Filename.")); + return; + } + + const QFileInfo logFileInfo = QFileInfo(filename); + if (!logFileInfo.exists() || !logFileInfo.isFile()) { + _setErrorMessage(tr("Invalid Filename.")); + return; } - _errorMessage.clear(); - emit errorMessageChanged(_errorMessage); + + _worker->setLogFile(filename); + emit logFileChanged(filename); + + _setErrorMessage(QString()); } -void GeoTagController::setSaveDirectory(QString dir) +void GeoTagController::setImageDirectory(const QString &dir) { - dir = QUrl(dir).toLocalFile(); - if (!dir.isEmpty()) { - _worker.setSaveDirectory(dir); - emit saveDirectoryChanged(dir); - //-- Check and see if there are images already there - QDir saveDirectory = QDir(_worker.saveDirectory()); - saveDirectory.setFilter(QDir::Files | QDir::Readable | QDir::NoSymLinks | QDir::Writable); - QStringList nameFilters; - nameFilters << "*.jpg" << "*.JPG"; - saveDirectory.setNameFilters(nameFilters); - QStringList imageList = saveDirectory.entryList(); - if(!imageList.isEmpty()) { - _setErrorMessage(tr("The save folder already contains images.")); + if (dir.isEmpty()) { + _setErrorMessage(tr("Invalid Directory.")); + return; + } + + const QFileInfo imageDirectoryInfo = QFileInfo(dir); + if (!imageDirectoryInfo.exists() || !imageDirectoryInfo.isDir()) { + _setErrorMessage(tr("Invalid Directory.")); + return; + } + + _worker->setImageDirectory(dir); + emit imageDirectoryChanged(dir); + + if (_worker->saveDirectory().isEmpty()) { + const QDir saveDirectory = QDir(_worker->imageDirectory() + kTagged); + if (saveDirectory.exists()) { + _setErrorMessage(tr("Images have already been tagged. Existing images will be removed.")); return; } } - _errorMessage.clear(); - emit errorMessageChanged(_errorMessage); + + _setErrorMessage(QString()); +} + +void GeoTagController::setSaveDirectory(const QString &dir) +{ + if (dir.isEmpty()) { + _setErrorMessage(tr("Invalid Directory.")); + return; + } + + const QFileInfo saveDirectoryInfo = QFileInfo(dir); + if (!saveDirectoryInfo.exists() || !saveDirectoryInfo.isDir()) { + _setErrorMessage(tr("Invalid Directory.")); + return; + } + + _worker->setSaveDirectory(dir); + emit saveDirectoryChanged(dir); + + QDir saveDirectory = QDir(_worker->saveDirectory()); + saveDirectory.setFilter(QDir::Files | QDir::Readable | QDir::NoSymLinks | QDir::Writable); + + QStringList nameFilters; + nameFilters << "*.jpg" << "*.JPG"; + saveDirectory.setNameFilters(nameFilters); + + const QStringList imageList = saveDirectory.entryList(); + if (!imageList.isEmpty()) { + _setErrorMessage(tr("The save folder already contains images.")); + return; + } + + _setErrorMessage(QString()); } void GeoTagController::startTagging() { - _errorMessage.clear(); - emit errorMessageChanged(_errorMessage); - QDir imageDirectory = QDir(_worker.imageDirectory()); - if(!imageDirectory.exists()) { + _setErrorMessage(QString()); + + const QDir imageDirectory = QDir(_worker->imageDirectory()); + if (!imageDirectory.exists()) { _setErrorMessage(tr("Cannot find the image directory.")); return; } - if(_worker.saveDirectory() == "") { - QDir oldTaggedFolder = QDir(_worker.imageDirectory() + kTagged); - if(oldTaggedFolder.exists()) { + + if (_worker->saveDirectory().isEmpty()) { + QDir oldTaggedFolder = QDir(_worker->imageDirectory() + kTagged); + if (oldTaggedFolder.exists()) { oldTaggedFolder.removeRecursively(); - if(!imageDirectory.mkdir(_worker.imageDirectory() + kTagged)) { + if (!imageDirectory.mkdir(_worker->imageDirectory() + kTagged)) { _setErrorMessage(tr("Couldn't replace the previously tagged images")); return; } } } else { - QDir saveDirectory = QDir(_worker.saveDirectory()); - if(!saveDirectory.exists()) { + const QDir saveDirectory = QDir(_worker->saveDirectory()); + if (!saveDirectory.exists()) { _setErrorMessage(tr("Cannot find the save directory.")); return; } } - _worker.start(); -} - -void GeoTagController::_workerProgressChanged(double progress) -{ - _progress = progress; - emit progressChanged(progress); -} -void GeoTagController::_workerError(QString errorMessage) -{ - _errorMessage = errorMessage; - emit errorMessageChanged(errorMessage); -} - - -void GeoTagController::_setErrorMessage(const QString& error) -{ - _errorMessage = error; - emit errorMessageChanged(error); + (void) QMetaObject::invokeMethod(_workerThread, "start", Qt::AutoConnection); } diff --git a/src/AnalyzeView/GeoTagController.h b/src/AnalyzeView/GeoTagController.h index 2311c282678..ae97c6689be 100644 --- a/src/AnalyzeView/GeoTagController.h +++ b/src/AnalyzeView/GeoTagController.h @@ -9,12 +9,13 @@ #pragma once +#include #include #include -#include #include -#include "GeoTagWorker.h" +class GeoTagWorker; +class QThread; Q_DECLARE_LOGGING_CATEGORY(GeoTagControllerLog) @@ -24,56 +25,56 @@ class GeoTagController : public QObject Q_OBJECT QML_ELEMENT -public: - GeoTagController(); - ~GeoTagController(); - Q_PROPERTY(QString logFile READ logFile WRITE setLogFile NOTIFY logFileChanged) Q_PROPERTY(QString imageDirectory READ imageDirectory WRITE setImageDirectory NOTIFY imageDirectoryChanged) Q_PROPERTY(QString saveDirectory READ saveDirectory WRITE setSaveDirectory NOTIFY saveDirectoryChanged) + Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY errorMessageChanged) + Q_PROPERTY(double progress READ progress NOTIFY progressChanged) + Q_PROPERTY(bool inProgress READ inProgress NOTIFY inProgressChanged) - /// Set to an error message is geotagging fails - Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY errorMessageChanged) +public: + explicit GeoTagController(QObject *parent = nullptr); + ~GeoTagController(); + + Q_INVOKABLE void startTagging(); + Q_INVOKABLE void cancelTagging(); + + QString logFile() const; + QString imageDirectory() const; + QString saveDirectory() const; /// Progress indicator: 0-100 - Q_PROPERTY(double progress READ progress NOTIFY progressChanged) + double progress() const { return _progress; } /// true: Currently in the process of tagging - Q_PROPERTY(bool inProgress READ inProgress NOTIFY inProgressChanged) + bool inProgress() const; - Q_INVOKABLE void startTagging(); - Q_INVOKABLE void cancelTagging() { _worker.cancelTagging(); } - - QString logFile () const { return _worker.logFile(); } - QString imageDirectory () const { return _worker.imageDirectory(); } - QString saveDirectory () const { return _worker.saveDirectory(); } - double progress () const { return _progress; } - bool inProgress () const { return _worker.isRunning(); } - QString errorMessage () const { return _errorMessage; } + /// Set to an error message if geotagging fails + QString errorMessage() const { return _errorMessage; } - void setLogFile (QString file); - void setImageDirectory (QString dir); - void setSaveDirectory (QString dir); + void setLogFile(const QString &file); + void setImageDirectory(const QString &dir); + void setSaveDirectory(const QString &dir); signals: - void logFileChanged (QString logFile); - void imageDirectoryChanged (QString imageDirectory); - void saveDirectoryChanged (QString saveDirectory); - void progressChanged (double progress); - void inProgressChanged (); - void errorMessageChanged (QString errorMessage); + void logFileChanged(const QString &logFile); + void imageDirectoryChanged(const QString &imageDirectory); + void saveDirectoryChanged(const QString &saveDirectory); + void progressChanged(double progress); + void inProgressChanged(); + void errorMessageChanged(const QString &errorMessage); private slots: - void _workerProgressChanged (double progress); - void _workerError (QString errorMsg); - void _setErrorMessage (const QString& error); + void _workerProgressChanged(double progress) { if (progress != _progress) { _progress = progress; emit progressChanged(_progress); } } + void _setErrorMessage(const QString &errorMsg) { if (errorMsg != _errorMessage) { _errorMessage = errorMsg; emit errorMessageChanged(_errorMessage); } } + void _workerError(const QString &errorMsg) { _setErrorMessage(errorMsg); } private: - QString _errorMessage; - double _progress; - bool _inProgress; - - GeoTagWorker _worker; + QString _errorMessage; + double _progress = 0.; + bool _inProgress = false; + GeoTagWorker *_worker = nullptr; + QThread *_workerThread = nullptr; - static constexpr const char* kTagged = "/TAGGED"; + static constexpr const char *kTagged = "/TAGGED"; }; diff --git a/src/AnalyzeView/GeoTagPage.qml b/src/AnalyzeView/GeoTagPage.qml index 7c8ec8b3918..c97a377601c 100644 --- a/src/AnalyzeView/GeoTagPage.qml +++ b/src/AnalyzeView/GeoTagPage.qml @@ -13,138 +13,147 @@ import QtQuick.Dialogs import QtQuick.Layouts import QGroundControl -import QGroundControl.Palette -import QGroundControl.FactSystem -import QGroundControl.FactControls import QGroundControl.Controls import QGroundControl.ScreenTools import QGroundControl.Controllers AnalyzePage { - id: geoTagPage - pageComponent: pageComponent - pageDescription: qsTr("Used to tag a set of images from a survey mission with gps coordinates. You must provide the binary log from the flight as well as the directory which contains the images to tag.") + pageComponent: pageComponent + pageDescription: qsTr("Used to tag a set of images from a survey mission with gps coordinates. You must provide the binary log from the flight as well as the directory which contains the images to tag.") - readonly property real _margin: ScreenTools.defaultFontPixelWidth * 2 - readonly property real _minWidth: ScreenTools.defaultFontPixelWidth * 20 - readonly property real _maxWidth: ScreenTools.defaultFontPixelWidth * 30 + readonly property real _margin: ScreenTools.defaultFontPixelWidth * 2 + readonly property real _minWidth: ScreenTools.defaultFontPixelWidth * 20 + readonly property real _maxWidth: ScreenTools.defaultFontPixelWidth * 30 Component { - id: pageComponent + id: pageComponent + GridLayout { - columns: 2 - columnSpacing: _margin - rowSpacing: ScreenTools.defaultFontPixelWidth * 2 - width: availableWidth + columns: 2 + columnSpacing: _margin + rowSpacing: ScreenTools.defaultFontPixelWidth * 2 + width: availableWidth + BusyIndicator { - running: geoController.progress > 0 && geoController.progress < 100 && geoController.errorMessage === "" - width: progressBar.height - height: progressBar.height - Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter + running: (geoController.progress > 0) && (geoController.progress < 100) && !geoController.errorMessage + width: progressBar.height + height: progressBar.height + Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter } - //----------------------------------------------------------------- + ProgressBar { - id: progressBar - to: 100 - value: geoController.progress - opacity: geoController.progress > 0 ? 1 : 0.25 - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter + id: progressBar + to: 100 + value: geoController.progress + opacity: (geoController.progress > 0) ? 1 : 0.25 + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter } - //----------------------------------------------------------------- + QGCLabel { - text: geoController.errorMessage - color: "red" - font.bold: true - font.pointSize: ScreenTools.mediumFontPointSize - horizontalAlignment:Text.AlignHCenter - Layout.alignment: Qt.AlignHCenter - Layout.columnSpan: 2 + text: geoController.errorMessage + color: "red" + font.bold: true + font.pointSize: ScreenTools.mediumFontPointSize + horizontalAlignment: Text.AlignHCenter + Layout.alignment: Qt.AlignHCenter + Layout.columnSpan: 2 } - //----------------------------------------------------------------- - //-- Log File + QGCButton { - text: qsTr("Select log file") - onClicked: openLogFile.openForLoad() - Layout.minimumWidth:_minWidth - Layout.maximumWidth:_maxWidth - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter + text: qsTr("Select log file") + Layout.minimumWidth: _minWidth + Layout.maximumWidth: _maxWidth + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + onClicked: openLogFile.openForLoad() + QGCFileDialog { - id: openLogFile - title: qsTr("Select log file") - nameFilters: [qsTr("ULog file (*.ulg)"), qsTr("PX4 log file (*.px4log)"), qsTr("All Files (*)")] - defaultSuffix: "ulg" + id: openLogFile + title: qsTr("Select log file") + nameFilters: [qsTr("ULog file (*.ulg)"), qsTr("PX4 log file (*.px4log)"), qsTr("All Files (*)")] + defaultSuffix: "ulg" onAcceptedForLoad: (file) => { - geoController.logFile = openLogFile.file + geoController.logFile = file close() } } } + QGCLabel { - text: geoController.logFile - elide: Text.ElideLeft - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter + text: geoController.logFile + elide: Text.ElideLeft + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter } - //----------------------------------------------------------------- - //-- Image Directory + QGCButton { - text: qsTr("Select image directory") - onClicked: selectImageDir.openForLoad() - Layout.minimumWidth:_minWidth - Layout.maximumWidth:_maxWidth - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter + text: qsTr("Select image directory") + Layout.minimumWidth: _minWidth + Layout.maximumWidth: _maxWidth + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + onClicked: selectImageDir.openForLoad() + QGCFileDialog { - id: selectImageDir - title: qsTr("Select image directory") - selectFolder: true + id: selectImageDir + title: qsTr("Select image directory") + selectFolder: true onAcceptedForLoad: (file) => { - geoController.imageDirectory = selectImageDir.file + geoController.imageDirectory = file close() } } } + QGCLabel { - text: geoController.imageDirectory - elide: Text.ElideLeft - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter + text: geoController.imageDirectory + elide: Text.ElideLeft + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter } - //----------------------------------------------------------------- - //-- Save Directory + QGCButton { - text: qsTr("(Optionally) Select save directory") - onClicked: selectDestDir.openForLoad() - Layout.minimumWidth:_minWidth - Layout.maximumWidth:_maxWidth - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter + text: qsTr("(Optionally) Select save directory") + Layout.minimumWidth: _minWidth + Layout.maximumWidth: _maxWidth + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + onClicked: selectDestDir.openForLoad() + QGCFileDialog { - id: selectDestDir - title: qsTr("Select save directory") - selectFolder: true + id: selectDestDir + title: qsTr("Select save directory") + selectFolder: true onAcceptedForLoad: (file) => { - geoController.saveDirectory = selectDestDir.file + geoController.saveDirectory = file close() } } } + QGCLabel { - text: geoController.saveDirectory === "" ? (geoController.imageDirectory === "" ? qsTr("/TAGGED folder in your image folder") : geoController.imageDirectory + qsTr("/TAGGED")) : geoController.saveDirectory - elide: Text.ElideLeft - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter + text: { + if (geoController.saveDirectory) { + return geoController.saveDirectory; + } else if (geoController.imageDirectory) { + return geoController.imageDirectory + qsTr("/TAGGED"); + } else { + return qsTr("/TAGGED folder in your image folder"); + } + } + elide: Text.ElideLeft + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter } - //----------------------------------------------------------------- - //-- Execute + QGCButton { - text: geoController.inProgress ? qsTr("Cancel Tagging") : qsTr("Start Tagging") - width: ScreenTools.defaultFontPixelWidth * 30 - enabled: (geoController.imageDirectory !== "" && geoController.logFile !== "") || geoController.inProgress - Layout.alignment: Qt.AlignHCenter - Layout.columnSpan: 2 + text: geoController.inProgress ? qsTr("Cancel Tagging") : qsTr("Start Tagging") + enabled: (geoController.imageDirectory && geoController.logFile) || geoController.inProgress + Layout.minimumWidth: _minWidth + Layout.maximumWidth: _maxWidth + Layout.alignment: Qt.AlignHCenter + Layout.columnSpan: 2 onClicked: { if (geoController.inProgress) { geoController.cancelTagging() diff --git a/src/AnalyzeView/GeoTagWorker.cc b/src/AnalyzeView/GeoTagWorker.cc index 91aecb614ed..234ae6f470b 100644 --- a/src/AnalyzeView/GeoTagWorker.cc +++ b/src/AnalyzeView/GeoTagWorker.cc @@ -7,6 +7,8 @@ * ****************************************************************************/ +// TODO: https://github.com/PX4/PX4-Autopilot/blob/main/Tools/geotag_images_ulog.py + #include "GeoTagWorker.h" #include "ExifParser.h" #include "ULogParser.h" @@ -17,175 +19,234 @@ QGC_LOGGING_CATEGORY(GeoTagWorkerLog, "qgc.analyzeview.geotagworker") -GeoTagWorker::GeoTagWorker() - : _cancel(false) +GeoTagWorker::GeoTagWorker(QObject *parent) + : QObject(parent) { + // qCDebug(GeoTagWorkerLog) << Q_FUNC_INFO << this; + +#ifdef QT_DEBUG + (void) connect(this, &GeoTagWorker::error, this, [](const QString &errorMsg) { + qCDebug(GeoTagWorkerLog) << errorMsg; + }, Qt::AutoConnection); +#endif +} +GeoTagWorker::~GeoTagWorker() +{ + // qCDebug(GeoTagWorkerLog) << Q_FUNC_INFO << this; } -void GeoTagWorker::run() +bool GeoTagWorker::process() { _cancel = false; emit progressChanged(1); - double nSteps = 5; - // Load Images + using StepFunction = bool (GeoTagWorker::*)(); + const StepFunction steps[] = { + &GeoTagWorker::_loadImages, + &GeoTagWorker::_parseExif, + &GeoTagWorker::_parseLogs, + &GeoTagWorker::_calibrate, + &GeoTagWorker::_tagImages + }; + + for (StepFunction step : steps) { + if (_cancel) { + emit error(tr("Tagging cancelled")); + return false; + } + + if (!(this->*step)()) { + return false; + } + } + + emit progressChanged(100); + emit taggingComplete(); + + return true; +} + +bool GeoTagWorker::_loadImages() +{ _imageList.clear(); + QDir imageDirectory = QDir(_imageDirectory); - imageDirectory.setFilter(QDir::Files | QDir::Readable | QDir::NoSymLinks | QDir::Writable); + imageDirectory.setFilter(QDir::Files | QDir::Readable | QDir::NoSymLinks); imageDirectory.setSorting(QDir::Name); + QStringList nameFilters; nameFilters << "*.jpg" << "*.JPG"; imageDirectory.setNameFilters(nameFilters); + _imageList = imageDirectory.entryInfoList(); - if(_imageList.isEmpty()) { + if (_imageList.isEmpty()) { emit error(tr("The image directory doesn't contain images, make sure your images are of the JPG format")); - return; + return false; } - emit progressChanged((100/nSteps)); - // Parse EXIF - _imageTime.clear(); - for (int i = 0; i < _imageList.size(); ++i) { - QFile file(_imageList.at(i).absoluteFilePath()); - if (!file.open(QIODevice::ReadOnly)) { - emit error(tr("Geotagging failed. Couldn't open an image.")); - return; - } - QByteArray imageBuffer = file.readAll(); - file.close(); + emit progressChanged(100. / kSteps); - _imageTime.append(ExifParser::readTime(imageBuffer)); + return true; +} - emit progressChanged((100/nSteps) + ((100/nSteps) / _imageList.size())*i); +bool GeoTagWorker::_parseExif() +{ + _imageTimestamps.clear(); + // TODO: QtConcurrent::mapped? + for (const QFileInfo& fileInfo : _imageList) { if (_cancel) { - qCDebug(GeotaggingLog) << "Tagging cancelled"; emit error(tr("Tagging cancelled")); - return; + return false; + } + + QFile file(fileInfo.absoluteFilePath()); + if (!file.open(QIODevice::ReadOnly)) { + emit error(tr("Geotagging failed. Couldn't open image: %1").arg(fileInfo.fileName())); + return false; + } + + const QByteArray imageBuffer = file.readAll(); + file.close(); + + const QDateTime imageTime = ExifParser::readTime(imageBuffer); + if (!imageTime.isValid()) { + emit error(tr("Geotagging failed. Couldn't extract time from image: %1").arg(fileInfo.fileName())); + return false; } + + (void) _imageTimestamps.append(imageTime.toSecsSinceEpoch()); } - // Load log - bool isULog = _logFile.endsWith(".ulg", Qt::CaseSensitive); + emit progressChanged(2.0 * (100.0 / kSteps)); + + return true; +} + +bool GeoTagWorker::_parseLogs() +{ + _triggerList.clear(); + QFile file(_logFile); if (!file.open(QIODevice::ReadOnly)) { emit error(tr("Geotagging failed. Couldn't open log file.")); - return; + return false; } - QByteArray log = file.readAll(); + + const QByteArray log = file.readAll(); file.close(); - // Instantiate appropriate parser - _triggerList.clear(); bool parseComplete = false; QString errorString; - if (isULog) { + if (_logFile.endsWith(".ulg", Qt::CaseSensitive)) { parseComplete = ULogParser::getTagsFromLog(log, _triggerList, errorString); } else { parseComplete = PX4LogParser::getTagsFromLog(log, _triggerList); } if (!parseComplete) { - if (_cancel) { - qCDebug(GeotaggingLog) << "Tagging cancelled"; - emit error(tr("Tagging cancelled")); - return; - } else { - qCDebug(GeotaggingLog) << "Log parsing failed"; - errorString = tr("%1 - tagging cancelled").arg(errorString.isEmpty() ? tr("Log parsing failed") : errorString); - emit error(errorString); - return; - } + emit error(errorString.isEmpty() ? tr("Log parsing failed") : errorString); + return false; + } + + qCDebug(GeoTagWorkerLog) << "Found" << _triggerList.count() << "trigger logs."; + if (_imageList.count() > _triggerList.count()) { + qCDebug(GeoTagWorkerLog) << "Detected missing feedback packets."; + } else if (_imageList.count() < _triggerList.count()) { + qCDebug(GeoTagWorkerLog) << "Detected missing image frames."; + } + + emit progressChanged(3. * (100. / kSteps)); + + return true; +} + +bool GeoTagWorker::_calibrate() +{ + _imageIndices.clear(); + _imageOffsets.clear(); + _triggerIndices.clear(); + + if (_triggerList.isEmpty() || _imageTimestamps.isEmpty()) { + emit error(tr("Calibration failed: No triggers or images available.")); + return false; } - emit progressChanged(3*(100/nSteps)); - qCDebug(GeotaggingLog) << "Found " << _triggerList.count() << " trigger logs."; + const qint64 lastImageTimestamp = _imageTimestamps.last(); + const qint64 lastTriggerTimestamp = _triggerList.last().timestamp; - if (_cancel) { - qCDebug(GeotaggingLog) << "Tagging cancelled"; - emit error(tr("Tagging cancelled")); - return; + for (int i = 0; i < _imageTimestamps.size(); ++i) { + const qint64 offset = lastImageTimestamp - _imageTimestamps[i]; + (void) _imageOffsets.insert(offset, i); } - // Filter Trigger - if (!triggerFiltering()) { - qCDebug(GeotaggingLog) << "Geotagging failed in trigger filtering"; - emit error(tr("Geotagging failed in trigger filtering")); - return; + for (const CameraFeedbackPacket &trigger : _triggerList) { + const qint64 triggerOffset = lastTriggerTimestamp - trigger.timestamp; + if (_imageOffsets.contains(triggerOffset)) { + const int imageIndex = _imageOffsets[triggerOffset]; + (void) _imageIndices.append(imageIndex); + (void) _triggerIndices.append(&trigger - &_triggerList[0]); + } } - emit progressChanged(4*(100/nSteps)); - if (_cancel) { - qCDebug(GeotaggingLog) << "Tagging cancelled"; - emit error(tr("Tagging cancelled")); - return; + if (_imageIndices.isEmpty()) { + emit error(tr("Calibration failed: No matching triggers found for images.")); + return false; } - // Tag images - auto maxIndex = std::min(_imageIndices.count(), _triggerIndices.count()); - maxIndex = std::min(maxIndex, _imageList.count()); - for(int i = 0; i < maxIndex; i++) { - int imageIndex = _imageIndices[i]; + emit progressChanged(4. * (100. / kSteps)); + + return true; +} + +bool GeoTagWorker::_tagImages() +{ + const qsizetype maxIndex = std::min(_imageIndices.count(), _triggerIndices.count()); + for (int i = 0; i < maxIndex; i++) { + if (_cancel) { + emit error(tr("Tagging cancelled")); + return false; + } + + const int imageIndex = _imageIndices[i]; if (imageIndex >= _imageList.count()) { emit error(tr("Geotagging failed. Requesting image #%1, but only %2 images present.").arg(imageIndex).arg(_imageList.count())); - return; + return false; } - QFile fileRead(_imageList.at(_imageIndices[i]).absoluteFilePath()); + + const QFileInfo &imageInfo = _imageList.at(imageIndex); + QFile fileRead(imageInfo.absoluteFilePath()); if (!fileRead.open(QIODevice::ReadOnly)) { emit error(tr("Geotagging failed. Couldn't open an image.")); - return; + return false; } + QByteArray imageBuffer = fileRead.readAll(); fileRead.close(); - if (!ExifParser::write(imageBuffer, _triggerList[_triggerIndices[i]])) { - emit error(tr("Geotagging failed. Couldn't write to image.")); - return; - } else { - QFile fileWrite; - if(_saveDirectory == "") { - fileWrite.setFileName(_imageDirectory + "/TAGGED/" + _imageList.at(_imageIndices[i]).fileName()); - } else { - fileWrite.setFileName(_saveDirectory + "/" + _imageList.at(_imageIndices[i]).fileName()); - } - if (!fileWrite.open(QFile::WriteOnly)) { - emit error(tr("Geotagging failed. Couldn't write to an image.")); - return; - } - fileWrite.write(imageBuffer); - fileWrite.close(); + if (!ExifParser::write(imageBuffer, _triggerList[imageIndex])) { + emit error(tr("Geotagging failed. Couldn't write to image: %1").arg(imageInfo.fileName())); + return false; } - emit progressChanged(4*(100/nSteps) + ((100/nSteps) / maxIndex)*i); - if (_cancel) { - qCDebug(GeotaggingLog) << "Tagging cancelled"; - emit error(tr("Tagging cancelled")); - return; + QFile fileWrite; + if (_saveDirectory.isEmpty()) { + fileWrite.setFileName(_imageDirectory + "/TAGGED/" + imageInfo.fileName()); + } else { + fileWrite.setFileName(_saveDirectory + "/" + imageInfo.fileName()); } - } - if (_cancel) { - qCDebug(GeotaggingLog) << "Tagging cancelled"; - emit error(tr("Tagging cancelled")); - return; - } + if (!fileWrite.open(QFile::WriteOnly)) { + emit error(tr("Geotagging failed. Couldn't write to image: %1").arg(imageInfo.fileName())); + return false; + } - emit progressChanged(100); -} + fileWrite.write(imageBuffer); + fileWrite.close(); -bool GeoTagWorker::triggerFiltering() -{ - _imageIndices.clear(); - _triggerIndices.clear(); - if(_imageList.count() > _triggerList.count()) { // Logging dropouts - qCDebug(GeotaggingLog) << "Detected missing feedback packets."; - } else if (_imageList.count() < _triggerList.count()) { // Camera skipped frames - qCDebug(GeotaggingLog) << "Detected missing image frames."; - } - for(int i = 0; i < _imageList.count() && i < _triggerList.count(); i++) { - _imageIndices.append(static_cast(_triggerList[i].imageSequence)); - _triggerIndices.append(i); + emit progressChanged(4. * (100. / kSteps) + ((100. / kSteps) / maxIndex) * i); } + return true; } diff --git a/src/AnalyzeView/GeoTagWorker.h b/src/AnalyzeView/GeoTagWorker.h index 0889828edc4..602b6f57558 100644 --- a/src/AnalyzeView/GeoTagWorker.h +++ b/src/AnalyzeView/GeoTagWorker.h @@ -9,61 +9,66 @@ #pragma once -#include -#include -#include #include #include +#include +#include Q_DECLARE_LOGGING_CATEGORY(GeoTagWorkerLog) -class GeoTagWorker : public QThread +class GeoTagWorker : public QObject { Q_OBJECT public: - GeoTagWorker(); - - void setLogFile (const QString& logFile) { _logFile = logFile; } - void setImageDirectory (const QString& imageDirectory) { _imageDirectory = imageDirectory; } - void setSaveDirectory (const QString& saveDirectory) { _saveDirectory = saveDirectory; } + explicit GeoTagWorker(QObject *parent = nullptr); + ~GeoTagWorker(); - QString logFile () const { return _logFile; } - QString imageDirectory () const { return _imageDirectory; } - QString saveDirectory () const { return _saveDirectory; } + QString logFile() const { return _logFile; } + void setLogFile(const QString &logFile) { _logFile = logFile; } + QString imageDirectory() const { return _imageDirectory; } + void setImageDirectory(const QString &imageDirectory) { _imageDirectory = imageDirectory; } + QString saveDirectory() const { return _saveDirectory; } + void setSaveDirectory(const QString &saveDirectory) { _saveDirectory = saveDirectory; } - void cancelTagging () { _cancel = true; } - - struct cameraFeedbackPacket { - double timestamp; - double timestampUTC; - uint32_t imageSequence; - double latitude; - double longitude; - float altitude; - float groundDistance; - float attitudeQuaternion[4]; - uint8_t captureResult; + struct CameraFeedbackPacket { + double timestamp = 0.; + double timestampUTC = 0.; + uint32_t imageSequence = 0; + double latitude = 0.; + double longitude = 0.; + float altitude = 0.; + float groundDistance = 0.; + float attitudeQuaternion[4]{}; + uint8_t captureResult = 0; }; -protected: - void run() final; - signals: - void error (QString errorMsg); - void taggingComplete (); - void progressChanged (double progress); + void error(const QString &errorMsg); + void progressChanged(double progress); + void taggingComplete(); + +public slots: + bool process(); + void cancelTagging() { _cancel = true; } private: - bool triggerFiltering(); + bool _loadImages(); + bool _parseExif(); + bool _parseLogs(); + bool _calibrate(); + bool _tagImages(); + + bool _cancel = false; + QString _logFile; + QString _imageDirectory; + QString _saveDirectory; + QFileInfoList _imageList; + QList _imageTimestamps; + QList _triggerList; + QList _imageIndices; + QList _imageOffsets; + QList _triggerIndices; - bool _cancel; - QString _logFile; - QString _imageDirectory; - QString _saveDirectory; - QFileInfoList _imageList; - QList _imageTime; - QList _triggerList; - QList _imageIndices; - QList _triggerIndices; + static constexpr double kSteps = 5.; }; diff --git a/src/AnalyzeView/PX4LogParser.cc b/src/AnalyzeView/PX4LogParser.cc index 12ef498e267..21f9e240e89 100644 --- a/src/AnalyzeView/PX4LogParser.cc +++ b/src/AnalyzeView/PX4LogParser.cc @@ -35,7 +35,7 @@ static constexpr const int triggerLengths[2] = {8, 4}; namespace PX4LogParser { -bool getTagsFromLog(const QByteArray& log, QList& cameraFeedback) +bool getTagsFromLog(const QByteArray& log, QList& cameraFeedback) { // extract header information: message lengths const uint8_t* iptr = reinterpret_cast(log.mid(log.indexOf(gposHeaderHeader) + 4, 1).constData()); @@ -60,7 +60,7 @@ bool getTagsFromLog(const QByteArray& log, QList(log.mid(index + triggerOffsets[0], triggerLengths[0]).data()); diff --git a/src/AnalyzeView/PX4LogParser.h b/src/AnalyzeView/PX4LogParser.h index ede7dc1d402..17994f1a5d6 100644 --- a/src/AnalyzeView/PX4LogParser.h +++ b/src/AnalyzeView/PX4LogParser.h @@ -18,5 +18,5 @@ Q_DECLARE_LOGGING_CATEGORY(PX4LogParserLog) namespace PX4LogParser { - bool getTagsFromLog(const QByteArray &log, QList &cameraFeedback); + bool getTagsFromLog(const QByteArray &log, QList &cameraFeedback); } diff --git a/src/AnalyzeView/ULogParser.cc b/src/AnalyzeView/ULogParser.cc index fde59160397..4897f2c0731 100644 --- a/src/AnalyzeView/ULogParser.cc +++ b/src/AnalyzeView/ULogParser.cc @@ -22,7 +22,7 @@ QGC_LOGGING_CATEGORY(ULogParserLog, "qgc.analyzeview.ulogparser") namespace ULogParser { -bool getTagsFromLog(const QByteArray &log, QList &cameraFeedback, QString &errorMessage) +bool getTagsFromLog(const QByteArray &log, QList &cameraFeedback, QString &errorMessage) { errorMessage.clear(); @@ -51,7 +51,7 @@ bool getTagsFromLog(const QByteArray &log, QList subscription = data->subscription("camera_capture"); for (const TypedDataView &sample : *subscription) { - GeoTagWorker::cameraFeedbackPacket feedback = {0}; + GeoTagWorker::CameraFeedbackPacket feedback = {0}; try { feedback.timestamp = sample.at("timestamp").as() / 1.0e6; // to seconds diff --git a/src/AnalyzeView/ULogParser.h b/src/AnalyzeView/ULogParser.h index 5006e88f164..c813a49a45a 100644 --- a/src/AnalyzeView/ULogParser.h +++ b/src/AnalyzeView/ULogParser.h @@ -22,5 +22,5 @@ Q_DECLARE_LOGGING_CATEGORY(ULogParserLog) namespace ULogParser { /// Get GeoTags from a ULog /// @return true if failed, errorMessage set - bool getTagsFromLog(const QByteArray &log, QList &cameraFeedback, QString &errorMessage); + bool getTagsFromLog(const QByteArray &log, QList &cameraFeedback, QString &errorMessage); } // namespace ULogParser diff --git a/src/Utilities/QGCLoggingCategory.cc b/src/Utilities/QGCLoggingCategory.cc index fb514ed23d1..fc144b505e2 100644 --- a/src/Utilities/QGCLoggingCategory.cc +++ b/src/Utilities/QGCLoggingCategory.cc @@ -23,7 +23,6 @@ QGC_LOGGING_CATEGORY(FirmwareUpgradeVerboseLog, "FirmwareUpgradeVerboseLog") QGC_LOGGING_CATEGORY(MissionCommandsLog, "MissionCommandsLog") QGC_LOGGING_CATEGORY(MissionItemLog, "MissionItemLog") QGC_LOGGING_CATEGORY(ParameterManagerLog, "ParameterManagerLog") -QGC_LOGGING_CATEGORY(GeotaggingLog, "GeotaggingLog") QGC_LOGGING_CATEGORY(RTKGPSLog, "RTKGPSLog") QGC_LOGGING_CATEGORY(GuidedActionsControllerLog, "GuidedActionsControllerLog") QGC_LOGGING_CATEGORY(LocalizationLog, "LocalizationLog") diff --git a/src/Utilities/QGCLoggingCategory.h b/src/Utilities/QGCLoggingCategory.h index d42652c46a3..0c74f5d47cf 100644 --- a/src/Utilities/QGCLoggingCategory.h +++ b/src/Utilities/QGCLoggingCategory.h @@ -19,7 +19,6 @@ Q_DECLARE_LOGGING_CATEGORY(FirmwareUpgradeVerboseLog) Q_DECLARE_LOGGING_CATEGORY(MissionCommandsLog) Q_DECLARE_LOGGING_CATEGORY(MissionItemLog) Q_DECLARE_LOGGING_CATEGORY(ParameterManagerLog) -Q_DECLARE_LOGGING_CATEGORY(GeotaggingLog) Q_DECLARE_LOGGING_CATEGORY(RTKGPSLog) Q_DECLARE_LOGGING_CATEGORY(GuidedActionsControllerLog) Q_DECLARE_LOGGING_CATEGORY(LocalizationLog) diff --git a/test/AnalyzeView/CMakeLists.txt b/test/AnalyzeView/CMakeLists.txt index 449f8b887be..5f1451e46b5 100644 --- a/test/AnalyzeView/CMakeLists.txt +++ b/test/AnalyzeView/CMakeLists.txt @@ -4,6 +4,8 @@ qt_add_library(AnalyzeViewTest STATIC ExifParserTest.cc ExifParserTest.h + GeoTagControllerTest.cc + GeoTagControllerTest.h LogDownloadTest.cc LogDownloadTest.h MavlinkLogTest.cc diff --git a/test/AnalyzeView/ExifParserTest.cc b/test/AnalyzeView/ExifParserTest.cc index f5fc35e70f3..40ab270ede7 100644 --- a/test/AnalyzeView/ExifParserTest.cc +++ b/test/AnalyzeView/ExifParserTest.cc @@ -12,13 +12,13 @@ void ExifParserTest::_readTimeTest() const QByteArray imageBuffer = file.readAll(); file.close(); - const double imageTime = ExifParser::readTime(imageBuffer); + const qint64 imageTime = ExifParser::readTime(imageBuffer).toSecsSinceEpoch(); const QDate date(2008, 10, 22); const QTime time(16, 28, 39); const QDateTime tagTime(date, time); - const double expectedTime = (tagTime.toMSecsSinceEpoch() / 1000.0); + const qint64 expectedTime = tagTime.toSecsSinceEpoch(); QCOMPARE(imageTime, expectedTime); } @@ -31,7 +31,7 @@ void ExifParserTest::_writeTest() QByteArray imageBuffer = file.readAll(); file.close(); - struct GeoTagWorker::cameraFeedbackPacket data; + struct GeoTagWorker::CameraFeedbackPacket data; data.latitude = 37.225; data.longitude = -80.425; diff --git a/test/AnalyzeView/GeoTagControllerTest.cc b/test/AnalyzeView/GeoTagControllerTest.cc new file mode 100644 index 00000000000..78fac915f6b --- /dev/null +++ b/test/AnalyzeView/GeoTagControllerTest.cc @@ -0,0 +1,98 @@ +#include "GeoTagControllerTest.h" +#include "GeoTagController.h" +#include "GeoTagWorker.h" + +#include +#include + +void GeoTagControllerTest::_geoTagControllerTest() +{ + const QDir tempDir = QDir(QDir::tempPath()); + const QString imageDirPath = QDir::tempPath() + "/QGC_GEOTAG_TEST"; + const QString subDirName = QString("QGC_GEOTAG_TEST"); + if (!tempDir.exists(subDirName)) { + QVERIFY(tempDir.mkdir(subDirName)); + } else { + QDir subDir(QDir::tempPath() + "/" + subDirName); + const QStringList files = subDir.entryList(QDir::Files); + for (const QString &fileName : files) { + QVERIFY(subDir.remove(fileName)); + } + } + + QFile file(":/DSCN0010.jpg"); + for (int i = 0; i < 58; ++i) { + QVERIFY(file.copy(imageDirPath + QStringLiteral("/geotag_temp_image_%1.jpg").arg(i))); + } + + QDir imageDir = QDir(imageDirPath); + if (!imageDir.exists("TAGGED")) { + QVERIFY(imageDir.mkdir("TAGGED")); + } else { + QDir subDir(imageDirPath + "/TAGGED"); + const QStringList files = subDir.entryList(QDir::Files); + for (const QString &fileName : files) { + QVERIFY(subDir.remove(fileName)); + } + } + + GeoTagController* const controller = new GeoTagController(this); + + const QFileInfo log = QFileInfo(":/SampleULog.ulg"); + controller->setLogFile(log.filePath()); + controller->setImageDirectory(imageDirPath + "/"); + controller->setSaveDirectory(controller->imageDirectory() + "/TAGGED"); + + QVERIFY(!controller->logFile().isEmpty()); + QVERIFY(!controller->imageDirectory().isEmpty()); + QVERIFY(!controller->saveDirectory().isEmpty()); + + QSignalSpy spyGeoTagControllerProgress(controller, &GeoTagController::progressChanged); + + controller->startTagging(); + + QCOMPARE(spyGeoTagControllerProgress.wait(1000), true); + + QVERIFY(controller->progress() > 0); + QCOMPARE(controller->errorMessage(), ""); +} + +void GeoTagControllerTest::_geoTagWorkerTest() +{ + const QDir tempDir = QDir(QDir::tempPath()); + const QString imageDirPath = QDir::tempPath() + "/QGC_GEOTAG_TEST"; + const QString subDirName = QString("QGC_GEOTAG_TEST"); + if (!tempDir.exists(subDirName)) { + QVERIFY(tempDir.mkdir(subDirName)); + } else { + QDir subDir(QDir::tempPath() + "/" + subDirName); + const QStringList files = subDir.entryList(QDir::Files); + for (const QString &fileName : files) { + QVERIFY(subDir.remove(fileName)); + } + } + + QFile file(":/DSCN0010.jpg"); + for (int i = 0; i < 58; ++i) { + QVERIFY(file.copy(imageDirPath + QStringLiteral("/geotag_temp_image_%1.jpg").arg(i))); + } + + QDir imageDir = QDir(imageDirPath); + if (!imageDir.exists("TAGGED")) { + QVERIFY(imageDir.mkdir("TAGGED")); + } else { + QDir subDir(imageDirPath + "/TAGGED"); + const QStringList files = subDir.entryList(QDir::Files); + for (const QString &fileName : files) { + QVERIFY(subDir.remove(fileName)); + } + } + + const QFileInfo log = QFileInfo(":/SampleULog.ulg"); + GeoTagWorker* const worker = new GeoTagWorker(this); + worker->setLogFile(log.filePath()); + worker->setImageDirectory(imageDirPath + "/"); + worker->setSaveDirectory(worker->imageDirectory() + "/TAGGED"); + + QVERIFY(worker->process()); +} diff --git a/test/AnalyzeView/GeoTagControllerTest.h b/test/AnalyzeView/GeoTagControllerTest.h new file mode 100644 index 00000000000..e51b646bb18 --- /dev/null +++ b/test/AnalyzeView/GeoTagControllerTest.h @@ -0,0 +1,12 @@ +#pragma once + +#include "UnitTest.h" + +class GeoTagControllerTest : public UnitTest +{ + Q_OBJECT + +private slots: + void _geoTagControllerTest(); + void _geoTagWorkerTest(); +}; diff --git a/test/AnalyzeView/PX4LogParserTest.cc b/test/AnalyzeView/PX4LogParserTest.cc index 5a9416fffff..70eb2709df7 100644 --- a/test/AnalyzeView/PX4LogParserTest.cc +++ b/test/AnalyzeView/PX4LogParserTest.cc @@ -12,11 +12,11 @@ void PX4LogParserTest::_getTagsFromLogTest() const QByteArray logBuffer = file.readAll(); file.close(); - QList cameraFeedback; + QList cameraFeedback; QVERIFY(PX4LogParser::getTagsFromLog(logBuffer, cameraFeedback)); QVERIFY(!cameraFeedback.isEmpty()); - GeoTagWorker::cameraFeedbackPacket firstCameraFeedback = cameraFeedback.first(); + GeoTagWorker::CameraFeedbackPacket firstCameraFeedback = cameraFeedback.first(); QVERIFY(!qFuzzyIsNull(firstCameraFeedback.timestamp)); QVERIFY(firstCameraFeedback.imageSequence != 0);*/ } diff --git a/test/AnalyzeView/ULogParserTest.cc b/test/AnalyzeView/ULogParserTest.cc index 06531333922..b83bcdc9e3d 100644 --- a/test/AnalyzeView/ULogParserTest.cc +++ b/test/AnalyzeView/ULogParserTest.cc @@ -12,13 +12,13 @@ void ULogParserTest::_getTagsFromLogTest() const QByteArray logBuffer = file.readAll(); file.close(); - QList cameraFeedback; + QList cameraFeedback; QString errorMessage; QVERIFY(ULogParser::getTagsFromLog(logBuffer, cameraFeedback, errorMessage)); QVERIFY(errorMessage.isEmpty()); QVERIFY(!cameraFeedback.isEmpty()); - const GeoTagWorker::cameraFeedbackPacket firstCameraFeedback = cameraFeedback.constFirst(); + const GeoTagWorker::CameraFeedbackPacket firstCameraFeedback = cameraFeedback.constFirst(); // QVERIFY(!qFuzzyIsNull(firstCameraFeedback.timestamp)); QVERIFY(firstCameraFeedback.imageSequence != 0); } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 24f743bcd45..ef75ce1f526 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -29,6 +29,7 @@ add_qgc_test(ADSBTest) add_subdirectory(AnalyzeView) add_qgc_test(ExifParserTest) +add_qgc_test(GeoTagControllerTest) # add_qgc_test(LogDownloadTest) # add_qgc_test(MavlinkLogTest) add_qgc_test(PX4LogParserTest) diff --git a/test/UnitTestList.cc b/test/UnitTestList.cc index 62476b9e8f9..7a408f67211 100644 --- a/test/UnitTestList.cc +++ b/test/UnitTestList.cc @@ -17,6 +17,7 @@ // AnalyzeView #include "ExifParserTest.h" +#include "GeoTagControllerTest.h" // #include "MavlinkLogTest.h" // #include "LogDownloadTest.h" #include "PX4LogParserTest.h" @@ -113,6 +114,7 @@ int runTests(bool stress, QStringView unitTestOptions) // AnalyzeView UT_REGISTER_TEST(ExifParserTest) + UT_REGISTER_TEST(GeoTagControllerTest) // UT_REGISTER_TEST(MavlinkLogTest) // UT_REGISTER_TEST(LogDownloadTest) UT_REGISTER_TEST(PX4LogParserTest)