#include "CircularSurvey.h" #include "RoutingThread.h" // QGC #include "JsonHelper.h" #include "QGCApplication.h" #include "QGCLoggingCategory.h" // Wima #include "snake.h" #define CLIPPER_SCALE 1000000 #include "clipper/clipper.hpp" #include "Geometry/GenericCircle.h" #include "Snake/SnakeTile.h" // boost #include #include #include "CircularGenerator.h" #include "LinearGenerator.h" // ToDo: Check what happened to _transectsDirty QGC_LOGGING_CATEGORY(CircularSurveyLog, "CircularSurveyLog") template constexpr typename std::underlying_type::type integral(T value) { return static_cast::type>(value); } const char *CircularSurvey::settingsGroup = "CircularSurvey"; const char *CircularSurvey::jsonComplexItemTypeValue = "CircularSurvey"; const char *CircularSurvey::variantName = "Variant"; const QString CircularSurvey::name(tr("Circular Survey")); CircularSurvey::CircularSurvey(PlanMasterController *masterController, bool flyView, const QString &kmlOrShpFile, QObject *parent) : TransectStyleComplexItem(masterController, flyView, settingsGroup, parent), _state(STATE::IDLE), _metaDataMap(FactMetaData::createMapFromJsonFile( QStringLiteral(":/json/CircularSurvey.SettingsGroup.json"), this)), _variant(settingsGroup, _metaDataMap[variantName]), _areaData(std::make_shared()), _pWorker(std::make_unique()) { Q_UNUSED(kmlOrShpFile) _editorQml = "qrc:/qml/CircularSurveyItemEditor.qml"; // Connect facts. connect(&this->_variant, &Fact::rawValueChanged, this, &CircularSurvey::_changeVariant); // Connect worker. connect(this->_pWorker.get(), &RoutingThread::result, this, &CircularSurvey::_setTransects); connect(this->_pWorker.get(), &RoutingThread::calculatingChanged, this, &CircularSurvey::calculatingChanged); // Register Generators. auto lg = std::make_shared(this->_areaData); registerGenerator(lg->name(), lg); auto cg = std::make_shared(this->_areaData); registerGenerator(cg->name(), cg); } CircularSurvey::~CircularSurvey() {} void CircularSurvey::reverse() { this->_setState(STATE::REVERT_PATH); this->_rebuildTransects(); } void CircularSurvey::setPlanData(const AreaData &d) { *this->_areaData = d; } const AreaData &CircularSurvey::planData() const { return *this->_areaData; } AreaData &CircularSurvey::planData() { return *this->_areaData; } QStringList CircularSurvey::variantNames() const { return _variantNames; } bool CircularSurvey::load(const QJsonObject &complexObject, int sequenceNumber, QString &errorString) { // We need to pull version first to determine what validation/conversion // needs to be performed QList versionKeyInfoList = { {JsonHelper::jsonVersionKey, QJsonValue::Double, true}, }; if (!JsonHelper::validateKeys(complexObject, versionKeyInfoList, errorString)) { return false; } int version = complexObject[JsonHelper::jsonVersionKey].toInt(); if (version != 1) { errorString = tr("Survey items do not support version %1").arg(version); return false; } QList keyInfoList = { {VisualMissionItem::jsonTypeKey, QJsonValue::String, true}, {ComplexMissionItem::jsonComplexItemTypeKey, QJsonValue::String, true}, {variantName, QJsonValue::Double, false}, }; if (!JsonHelper::validateKeys(complexObject, keyInfoList, errorString)) { return false; } QString itemType = complexObject[VisualMissionItem::jsonTypeKey].toString(); QString complexType = complexObject[ComplexMissionItem::jsonComplexItemTypeKey].toString(); if (itemType != VisualMissionItem::jsonTypeComplexItemValue || complexType != jsonComplexItemTypeValue) { errorString = tr("%1 does not support loading this complex mission item " "type: %2:%3") .arg(qgcApp()->applicationName()) .arg(itemType) .arg(complexType); return false; } _ignoreRecalc = true; setSequenceNumber(sequenceNumber); if (!_surveyAreaPolygon.loadFromJson(complexObject, true /* required */, errorString)) { _surveyAreaPolygon.clear(); return false; } if (!load(complexObject, sequenceNumber, errorString)) { _ignoreRecalc = false; return false; } _variant.setRawValue(complexObject[variantName].toInt()); _ignoreRecalc = false; if (_cameraShots == 0) { // Shot count was possibly not available from plan file _recalcCameraShots(); } return true; } QString CircularSurvey::mapVisualQML() const { return QStringLiteral("CircularSurveyMapVisual.qml"); } void CircularSurvey::save(QJsonArray &planItems) { QJsonObject saveObject; _save(saveObject); saveObject[JsonHelper::jsonVersionKey] = 1; saveObject[VisualMissionItem::jsonTypeKey] = VisualMissionItem::jsonTypeComplexItemValue; saveObject[ComplexMissionItem::jsonComplexItemTypeKey] = jsonComplexItemTypeValue; saveObject[variantName] = double(_variant.rawValue().toUInt()); // Polygon shape _surveyAreaPolygon.saveToJson(saveObject); planItems.append(saveObject); } bool CircularSurvey::specifiesCoordinate() const { return _transects.count() > 0 ? _transects.first().count() > 0 : false; } bool CircularSurvey::_switchToGenerator( const CircularSurvey::PtrGenerator &newG) { if (this->_pGenerator != newG) { if (this->_pGenerator != nullptr) { disconnect(this->_pGenerator.get(), &routing::GeneratorBase::generatorChanged, this, &CircularSurvey::_rebuildTransects); } this->_pGenerator = newG; connect(this->_pGenerator.get(), &routing::GeneratorBase::generatorChanged, this, &CircularSurvey::_rebuildTransects); emit generatorChanged(); this->_setState(STATE::IDLE); _rebuildTransects(); return true; } else { return false; } } void CircularSurvey::_setState(CircularSurvey::STATE state) { if (this->_state != state) { auto oldState = this->_state; this->_state = state; if (_calculating(oldState) != _calculating(state)) { emit calculatingChanged(); } } } bool CircularSurvey::_calculating(CircularSurvey::STATE state) const { return state == STATE::ROUTING; } void CircularSurvey::_changeVariant() { this->_setState(STATE::CHANGE_VARIANT); this->_rebuildTransects(); } bool CircularSurvey::_updateWorker() { // Reset data. this->_transects.clear(); this->_variantVector.clear(); this->_variantNames.clear(); emit variantNamesChanged(); if (this->_areaData->isValid()) { // Prepare data. auto origin = this->_areaData->origin(); origin.setAltitude(0); if (!origin.isValid()) { qCDebug(CircularSurveyLog) << "_updateWorker(): origin invalid." << origin; return false; } // Convert safe area. auto geoSafeArea = this->_areaData->joinedArea().coordinateList(); if (!(geoSafeArea.size() >= 3)) { qCDebug(CircularSurveyLog) << "_updateWorker(): safe area invalid." << geoSafeArea; return false; } for (auto &v : geoSafeArea) { if (v.isValid()) { v.setAltitude(0); } else { qCDebug(CircularSurveyLog) << "_updateWorker(): safe area contains invalid coordinate." << geoSafeArea; return false; } } // Routing par. RoutingParameter par; par.numSolutions = 5; auto &safeAreaENU = par.safeArea; snake::areaToEnu(origin, geoSafeArea, safeAreaENU); // Create generator. if (this->_pGenerator) { routing::GeneratorBase::Generator g; // Transect generator. if (this->_pGenerator->get(g)) { // Start/Restart routing worker. this->_pWorker->route(par, g); return true; } else { qCDebug(CircularSurveyLog) << "_updateWorker(): generator creation failed."; return false; } } else { qCDebug(CircularSurveyLog) << "_updateWorker(): pGenerator == nullptr, number of registered " "generators: " << this->_generatorList.size(); return false; } } else { qCDebug(CircularSurveyLog) << "_updateWorker(): plan data invalid."; return false; } } void CircularSurvey::_changeVariantWorker() { auto variant = this->_variant.rawValue().toUInt(); // Find old variant and run. Old run corresponts with empty list. std::size_t old_variant = std::numeric_limits::max(); for (std::size_t i = 0; i < std::size_t(this->_variantVector.size()); ++i) { const auto &variantCoordinates = this->_variantVector.at(i); if (variantCoordinates.isEmpty()) { old_variant = i; break; } } // Swap route. if (variant != old_variant) { // Swap in new variant. if (variant < std::size_t(this->_variantVector.size())) { if (old_variant != std::numeric_limits::max()) { // this->_transects containes a route, swap it back to // this->_solutionVector auto &oldVariantCoordinates = this->_variantVector[old_variant]; oldVariantCoordinates.swap(this->_transects); } auto &newVariantCoordinates = this->_variantVector[variant]; this->_transects.swap(newVariantCoordinates); } else { // error qCDebug(CircularSurveyLog) << "Variant out of bounds (variant =" << variant << ")."; qCDebug(CircularSurveyLog) << "Resetting variant to zero."; disconnect(&this->_variant, &Fact::rawValueChanged, this, &CircularSurvey::_changeVariant); this->_variant.setCookedValue(QVariant(0)); connect(&this->_variant, &Fact::rawValueChanged, this, &CircularSurvey::_changeVariant); if (this->_variantVector.size() > 0) { this->_changeVariantWorker(); } } } } void CircularSurvey::_reverseWorker() { if (this->_transects.size() > 0) { auto &t = this->_transects.front(); std::reverse(t.begin(), t.end()); } } double CircularSurvey::timeBetweenShots() { return 0; } QString CircularSurvey::commandDescription() const { return tr("Circular Survey"); } QString CircularSurvey::commandName() const { return tr("Circular Survey"); } QString CircularSurvey::abbreviation() const { return tr("C.S."); } TransectStyleComplexItem::ReadyForSaveState CircularSurvey::readyForSaveState() const { if (TransectStyleComplexItem::readyForSaveState() == TransectStyleComplexItem::ReadyForSaveState::ReadyForSave) { if (this->_state == STATE::IDLE) { return ReadyForSaveState::ReadyForSave; } else { return ReadyForSaveState::NotReadyForSaveData; } } else { return TransectStyleComplexItem::readyForSaveState(); } } double CircularSurvey::additionalTimeDelay() const { return 0; } QString CircularSurvey::patternName() const { return name; } bool CircularSurvey::registerGenerator( const QString &name, std::shared_ptr g) { if (name.isEmpty()) { qCDebug(CircularSurveyLog) << "registerGenerator(): empty name string."; return false; } if (!g) { qCDebug(CircularSurveyLog) << "registerGenerator(): empty generator."; return false; } if (this->_generatorNameList.contains(name)) { qCDebug(CircularSurveyLog) << "registerGenerator(): generator " "already registered."; return false; } else { this->_generatorNameList.push_back(name); this->_generatorList.push_back(g); if (this->_generatorList.size() == 1) { _switchToGenerator(g); } emit generatorNameListChanged(); return true; } } bool CircularSurvey::unregisterGenerator(const QString &name) { auto index = this->_generatorNameList.indexOf(name); if (index >= 0) { // Is this the current generator? const auto &g = this->_generatorList.at(index); if (g == this->_pGenerator) { if (index > 0) { _switchToGenerator(this->_generatorList.at(index - 1)); } else { _switchToGenerator(nullptr); qCDebug(CircularSurveyLog) << "unregisterGenerator(): last generator unregistered."; } } this->_generatorNameList.removeAt(index); this->_generatorList.removeAt(index); emit generatorNameListChanged(); return true; } else { qCDebug(CircularSurveyLog) << "unregisterGenerator(): generator " << name << " not registered."; return false; } } bool CircularSurvey::unregisterGenerator(int index) { if (index > 0 && index < this->_generatorNameList.size()) { return unregisterGenerator(this->_generatorNameList.at(index)); } else { qCDebug(CircularSurveyLog) << "unregisterGenerator(): index (" << index << ") out" "of bounds ( " << this->_generatorList.size() << " )."; return false; } } bool CircularSurvey::switchToGenerator(const QString &name) { auto index = this->_generatorNameList.indexOf(name); if (index >= 0) { _switchToGenerator(this->_generatorList.at(index)); return true; } else { qCDebug(CircularSurveyLog) << "switchToGenerator(): generator " << name << " not registered."; return false; } } bool CircularSurvey::switchToGenerator(int index) { if (index >= 0) { _switchToGenerator(this->_generatorList.at(index)); return true; } else { qCDebug(CircularSurveyLog) << "unregisterGenerator(): index (" << index << ") out" "of bounds ( " << this->_generatorNameList.size() << " )."; return false; } } QStringList CircularSurvey::generatorNameList() { return this->_generatorNameList; } routing::GeneratorBase *CircularSurvey::generator() { return _pGenerator.get(); } int CircularSurvey::generatorIndex() { return this->_generatorList.indexOf(this->_pGenerator); } void CircularSurvey::_rebuildTransectsPhase1(void) { auto start = std::chrono::high_resolution_clock::now(); switch (this->_state) { case STATE::SKIPP: qCDebug(CircularSurveyLog) << "rebuildTransectsPhase1: skipp."; this->_setState(STATE::IDLE); break; case STATE::CHANGE_VARIANT: qCDebug(CircularSurveyLog) << "rebuildTransectsPhase1: variant change."; this->_changeVariantWorker(); this->_setState(STATE::IDLE); break; case STATE::REVERT_PATH: qCDebug(CircularSurveyLog) << "rebuildTransectsPhase1: reverse."; this->_reverseWorker(); this->_setState(STATE::IDLE); break; case STATE::IDLE: case STATE::ROUTING: this->_setState(STATE::ROUTING); qCDebug(CircularSurveyLog) << "rebuildTransectsPhase1: update."; if (!this->_updateWorker()) { this->_setState(STATE::IDLE); } break; } qCDebug(CircularSurveyLog) << "rebuildTransectsPhase1(): " << std::chrono::duration_cast( std::chrono::high_resolution_clock::now() - start) .count() << " ms"; } // no cameraShots in Circular Survey, add if desired void CircularSurvey::_recalcCameraShots() { _cameraShots = 0; } void CircularSurvey::_setTransects(CircularSurvey::PtrRoutingData pRoute) { // Store solutions. auto ori = this->_areaData->origin(); ori.setAltitude(0); const auto &transectsENU = pRoute->transects; QVector variantVector; const auto nSolutions = pRoute->solutionVector.size(); for (std::size_t j = 0; j < nSolutions; ++j) { Variant var{QList()}; const auto &solution = pRoute->solutionVector.at(j); if (solution.size() > 0) { const auto &route = solution.at(0); const auto &path = route.path; const auto &info = route.info; if (info.size() > 1) { // Find index of first waypoint. std::size_t idxFirst = 0; const auto &infoFirst = info.at(1); const auto &firstTransect = transectsENU[infoFirst.index]; if (firstTransect.size() > 0) { const auto &firstWaypoint = infoFirst.reversed ? firstTransect.back() : firstTransect.front(); double th = 0.01; for (std::size_t i = 0; i < path.size(); ++i) { auto dist = bg::distance(path[i], firstWaypoint); if (dist < th) { idxFirst = i; break; } } // Find index of last waypoint. std::size_t idxLast = path.size() - 1; const auto &infoLast = info.at(info.size() - 2); const auto &lastTransect = transectsENU[infoLast.index]; if (lastTransect.size() > 0) { const auto &lastWaypoint = infoLast.reversed ? lastTransect.front() : lastTransect.back(); for (long i = path.size() - 1; i >= 0; --i) { auto dist = bg::distance(path[i], lastWaypoint); if (dist < th) { idxLast = i; break; } } // Convert to geo coordinates. auto &list = var.front(); for (std::size_t i = idxFirst; i <= idxLast; ++i) { auto &vertex = path[i]; QGeoCoordinate c; snake::fromENU(ori, vertex, c); list.append(CoordInfo_t{c, CoordTypeInterior}); } } else { qCDebug(CircularSurveyLog) << "_setTransects(): lastTransect.size() == 0"; } } else { qCDebug(CircularSurveyLog) << "_setTransects(): firstTransect.size() == 0"; } } else { qCDebug(CircularSurveyLog) << "_setTransects(): transectsInfo.size() <= 1"; } } else { qCDebug(CircularSurveyLog) << "_setTransects(): solution.size() == 0"; } if (var.size() > 0 && var.front().size() > 0) { variantVector.push_back(std::move(var)); } } // Assign routes if no error occured. if (variantVector.size() > 0) { // Swap first route to _transects. this->_variantVector.swap(variantVector); // If the transects are getting rebuilt then any previously loaded // mission items are now invalid. if (_loadedMissionItemsParent) { _loadedMissionItems.clear(); _loadedMissionItemsParent->deleteLater(); _loadedMissionItemsParent = nullptr; } // Add route variant names. this->_variantNames.clear(); for (std::size_t i = 1; i <= std::size_t(this->_variantVector.size()); ++i) { this->_variantNames.append(QString::number(i)); } emit variantNamesChanged(); disconnect(&this->_variant, &Fact::rawValueChanged, this, &CircularSurvey::_changeVariant); this->_variant.setCookedValue(QVariant(0)); connect(&this->_variant, &Fact::rawValueChanged, this, &CircularSurvey::_changeVariant); this->_changeVariantWorker(); this->_setState(STATE::SKIPP); this->_rebuildTransects(); } else { qCDebug(CircularSurveyLog) << "_setTransects(): failed, variantVector empty."; this->_setState(STATE::IDLE); } } Fact *CircularSurvey::variant() { return &_variant; } bool CircularSurvey::calculating() const { return this->_calculating(this->_state); } /*! \class CircularSurveyComplexItem \inmodule Wima \brief The \c CircularSurveyComplexItem class provides a survey mission item with circular transects around a point of interest. CircularSurveyComplexItem class provides a survey mission item with circular transects around a point of interest. Within the \c Wima module it's used to scan a defined area with constant angle (circular transects) to the base station (point of interest). \sa WimaArea */