#include "MeasurementArea.h" #include "HashFunctions.h" #include "geometry.h" #include "nemo_interface/MeasurementTile.h" #include #include "QtConcurrentRun" #include #include #include #include "JsonHelper.h" #include "QGCLoggingCategory.h" #include "QmlObjectListHelper.h" #ifndef MAX_TILES #define MAX_TILES 1000 #endif QString randomId(); using namespace geometry; namespace trans = bg::strategy::transform; // Aux function bool getTiles(const FPolygon &area, Length tileHeight, Length tileWidth, Area minTileArea, std::vector &tiles, BoundingBox &bbox); QGC_LOGGING_CATEGORY(MeasurementAreaLog, "MeasurementAreaLog") namespace { const char *tileArrayKey = "TileArray"; } // namespace const char *MeasurementArea::settingsGroup = "MeasurementArea"; const char *tileHeightKey = "TileHeight"; const char *tileWidthName = "TileWidth"; const char *minTileAreaKey = "MinTileAreaPercent"; const char *showTilesKey = "ShowTiles"; const char *tileKey = "Tiles"; const char *MeasurementArea::nameString = "Measurement Area"; MeasurementArea::MeasurementArea(QObject *parent) : GeoArea(parent), _metaDataMap(FactMetaData::createMapFromJsonFile( QStringLiteral(":/json/MeasurementArea.SettingsGroup.json"), this /* QObject parent */)), _tileHeight(SettingsFact(settingsGroup, _metaDataMap[tileHeightKey], this /* QObject parent */)), _tileWidth(SettingsFact(settingsGroup, _metaDataMap[tileWidthName], this /* QObject parent */)), _minTileAreaPercent(SettingsFact(settingsGroup, _metaDataMap[minTileAreaKey], this /* QObject parent */)), _showTiles(SettingsFact(settingsGroup, _metaDataMap[showTilesKey], this /* QObject parent */)), _tiles(new QmlObjectListModel()), _state(STATE::IDLE) { init(); } MeasurementArea::MeasurementArea(const MeasurementArea &other, QObject *parent) : GeoArea(other, parent), _metaDataMap(FactMetaData::createMapFromJsonFile( QStringLiteral(":/json/MeasurementArea.SettingsGroup.json"), this /* QObject parent */)), _tileHeight(SettingsFact(settingsGroup, _metaDataMap[tileHeightKey], this /* QObject parent */)), _tileWidth(SettingsFact(settingsGroup, _metaDataMap[tileWidthName], this /* QObject parent */)), _minTileAreaPercent(SettingsFact(settingsGroup, _metaDataMap[minTileAreaKey], this /* QObject parent */)), _showTiles(SettingsFact(settingsGroup, _metaDataMap[showTilesKey], this /* QObject parent */)), _tiles(new QmlObjectListModel()), _state(STATE::IDLE) { init(); disableUpdate(); _tileHeight = other._tileHeight; _tileWidth = other._tileWidth; _minTileAreaPercent = other._minTileAreaPercent; _showTiles = other._showTiles; if (other.ready()) { for (int i = 0; i < other._tiles->count(); ++i) { _tiles->append( qobject_cast(other._tiles->operator[](i)) ->clone(_tiles.get())); } _indexMap = other._indexMap; enableUpdate(); } else { enableUpdate(); doUpdate(); } } MeasurementArea &MeasurementArea::operator=(const MeasurementArea &other) { GeoArea::operator=(other); disableUpdate(); _tileHeight = other._tileHeight; _tileWidth = other._tileWidth; _minTileAreaPercent = other._minTileAreaPercent; _showTiles = other._showTiles; if (other.ready()) { _tiles->clearAndDeleteContents(); for (int i = 0; i < other._tiles->count(); ++i) { _tiles->append( qobject_cast(other._tiles->operator[](i)) ->clone(_tiles.get())); } _indexMap = other._indexMap; enableUpdate(); } else { enableUpdate(); doUpdate(); } return *this; } MeasurementArea::~MeasurementArea() { _tiles->clearAndDeleteContents(); } QString MeasurementArea::mapVisualQML() const { return QStringLiteral("MeasurementAreaMapVisual.qml"); // return QStringLiteral(""); } QString MeasurementArea::editorQML() const { return QStringLiteral("MeasurementAreaEditor.qml"); } MeasurementArea *MeasurementArea::clone(QObject *parent) const { return new MeasurementArea(*this, parent); } Fact *MeasurementArea::tileHeight() { return &_tileHeight; } Fact *MeasurementArea::tileWidth() { return &_tileWidth; } Fact *MeasurementArea::minTileArea() { return &_minTileAreaPercent; } Fact *MeasurementArea::showTiles() { return &_showTiles; } QmlObjectListModel *MeasurementArea::tiles() { return _tiles.get(); } const QmlObjectListModel *MeasurementArea::tiles() const { return _tiles.get(); } int MeasurementArea::maxTiles() const { return MAX_TILES; } bool MeasurementArea::ready() const { return this->_state == STATE::IDLE; } bool MeasurementArea::measurementCompleted() const { if (ready()) { for (int i = 0; i < _tiles->count(); ++i) { const auto tile = qobject_cast(_tiles->get(i)); if (!qFuzzyCompare(tile->progress(), 100)) { return false; } } return true; } else { return false; } } bool MeasurementArea::saveToJson(QJsonObject &json) { if (ready()) { if (this->GeoArea::saveToJson(json)) { json[tileHeightKey] = _tileHeight.rawValue().toDouble(); json[tileWidthName] = _tileWidth.rawValue().toDouble(); json[minTileAreaKey] = _minTileAreaPercent.rawValue().toDouble(); json[showTilesKey] = _showTiles.rawValue().toBool(); json[areaTypeKey] = nameString; // save tiles QJsonArray jsonTileArray; for (int i = 0; i < _tiles->count(); ++i) { auto tile = qobject_cast(_tiles->get(i)); QJsonObject jsonTile; tile->saveToJson(jsonTile); jsonTileArray.append(jsonTile); } json[tileArrayKey] = std::move(jsonTileArray); return true; } else { qCDebug(MeasurementAreaLog) << "saveToJson(): error inside GeoArea::saveToJson()."; } } else { qCDebug(MeasurementAreaLog) << "saveToJson(): not ready()."; } return false; } bool MeasurementArea::loadFromJson(const QJsonObject &json, QString &errorString) { if (this->GeoArea::loadFromJson(json, errorString)) { disableUpdate(); bool retVal = true; // load parameters necessary for tile calculation. if (!json.contains(tileHeightKey) || !json[tileHeightKey].isDouble()) { errorString.append(tr("Could not load tile height!\n")); retVal = false; } else { _tileHeight.setRawValue(json[tileHeightKey].toDouble()); } if (!json.contains(tileWidthName) || !json[tileWidthName].isDouble()) { errorString.append(tr("Could not load tile width!\n")); retVal = false; } else { _tileWidth.setRawValue(json[tileWidthName].toDouble()); } if (!json.contains(minTileAreaKey) || !json[minTileAreaKey].isDouble()) { errorString.append(tr("Could not load minimal tile area!\n")); retVal = false; } else { _minTileAreaPercent.setRawValue(json[minTileAreaKey].toDouble()); } // load less important parameters if (json.contains(showTilesKey) || !json[showTilesKey].isBool()) { _showTiles.setRawValue(json[showTilesKey].toBool()); } // load tiles bool tileError = false; if (json.contains(tileArrayKey) && json[tileArrayKey].isArray()) { QString e; _tiles->clearAndDeleteContents(); for (auto &&jsonTile : json[tileArrayKey].toArray()) { auto tile = new MeasurementTile(this); if (tile->loadFromJson(jsonTile.toObject(), e)) { _tiles->append(tile); } else { tile->deleteLater(); qCWarning(MeasurementAreaLog) << e; tileError = true; break; } } if (!tileError) { this->_indexMap.clear(); for (int i = 0; i < _tiles->count(); ++i) { auto tile = qobject_cast(_tiles->get(i)); auto it = _indexMap.find(tile->id()); // find unique id if (it != _indexMap.end()) { auto newId = MeasurementTile::randomId(); constexpr long counterMax = 1e6; unsigned long counter = 0; for (; counter <= counterMax; ++counter) { it = _indexMap.find(newId); if (it == _indexMap.end()) { break; } else { newId = MeasurementTile::randomId(); } } if (counter != counterMax) { tile->setId(newId); tile->setProgress(0.0); } else { qCritical() << "MeasurementArea::storeTiles(): not able to find " "unique id!"; continue; } } _indexMap.insert(std::make_pair(tile->id(), i)); } } } else { qCWarning(MeasurementAreaLog) << "Not able to load tiles. tileArrayKey missing or wrong type."; if (json.contains(tileArrayKey)) { qCWarning(MeasurementAreaLog) << "tile array type: " << json[tileArrayKey].type(); } tileError = true; } // do update if error occurred. enableUpdate(); if (tileError) { doUpdate(); } return retVal; } else { return false; } } bool MeasurementArea::isCorrect() { if (GeoArea::isCorrect()) { if (ready()) { return true; } else { setErrorString( tr("Measurement Area tile calculation in progess. Please wait.")); } } return false; } void MeasurementArea::updateProgress(const ProgressArray &array) { if (ready() && array.size() > 0) { bool anyChanges = false; for (const auto &lp : array) { auto it = _indexMap.find(lp.id()); if (it != _indexMap.end()) { int tileIndex = it->second; auto *tile = _tiles->value(tileIndex); if (!qFuzzyCompare(lp.progress(), tile->progress())) { tile->setProgress(lp.progress()); anyChanges = true; } } } if (anyChanges) { emit progressChanged(); } } } void MeasurementArea::randomProgress() { if (ready()) { std::srand(std::time(nullptr)); ProgressArray progressArray; for (int i = 0; i < _tiles->count(); ++i) { auto tile = _tiles->value(i); Q_ASSERT(tile != nullptr); auto p = tile->progress(); p += std::rand() % 125; if (p > 100) { p = 100; } progressArray.append(LabeledProgress(p, tile->id())); } updateProgress(progressArray); } } void MeasurementArea::resetProgress() { if (ready()) { bool anyChanges = false; for (int i = 0; i < _tiles->count(); ++i) { auto tile = _tiles->value(i); Q_ASSERT(tile != nullptr); if (!qFuzzyCompare(tile->progress(), 0)) { tile->setProgress(0); anyChanges = true; } } if (anyChanges) { emit progressChanged(); } } } //! //! \brief MeasurementArea::doUpdate //! \pre MeasurementArea::deferUpdate must be called first, don't call //! this function directly! void MeasurementArea::doUpdate() { using namespace geometry; using namespace boost::units; auto start = std::chrono::high_resolution_clock::now(); if (this->_state != STATE::UPDATEING && this->_state != STATE::STOP) { const auto height = this->_tileHeight.rawValue().toDouble() * si::meter; const auto width = this->_tileWidth.rawValue().toDouble() * si::meter; const auto tileArea = width * height; const auto totalArea = this->area() * si::meter * si::meter; const auto estNumTiles = totalArea / tileArea; // Check some conditions. if (long(std::ceil(estNumTiles.value())) <= MAX_TILES && this->GeoArea::isCorrect()) { setState(STATE::UPDATEING); auto polygon = this->coordinateList(); for (auto &v : polygon) { v.setAltitude(0); } const auto minArea = this->_minTileAreaPercent.rawValue().toDouble() / 100 * tileArea; auto *th = this->thread(); auto future = QtConcurrent::run([polygon, th, height, width, minArea] { auto start = std::chrono::high_resolution_clock::now(); TilePtr pData(new QmlObjectListModel()); // Convert to ENU system. QGeoCoordinate origin = polygon.first(); FPolygon polygonENU; areaToEnu(origin, polygon, polygonENU); std::vector tilesENU; BoundingBox bbox; // Generate tiles. if (getTiles(polygonENU, height, width, minArea, tilesENU, bbox)) { // Convert to geo system. for (const auto &t : tilesENU) { auto geoTile = new MeasurementTile(pData.get()); for (const auto &v : t.outer()) { QGeoCoordinate geoVertex; fromENU(origin, v, geoVertex); geoTile->push_back(geoVertex); } pData->append(geoTile); } } pData->moveToThread(th); qCDebug(MeasurementAreaLog) << "doUpdate(): update time: " << std::chrono::duration_cast( std::chrono::high_resolution_clock::now() - start) .count() << " ms"; return pData; }); // QtConcurrent::run() this->_watcher.setFuture(future); } } qCDebug(MeasurementAreaLog) << "doUpdate(): execution time: " << std::chrono::duration_cast( std::chrono::high_resolution_clock::now() - start) .count() << " ms"; } void MeasurementArea::deferUpdate() { if (this->_state == STATE::IDLE || this->_state == STATE::DEFERED) { qCDebug(MeasurementAreaLog) << "defereUpdate(): defer update."; if (this->_state == STATE::IDLE) { this->_indexMap.clear(); this->_tiles->clearAndDeleteContents(); emit tilesChanged(); emit progressChanged(); } this->setState(STATE::DEFERED); this->_timer.start(100); } else if (this->_state == STATE::UPDATEING) { qCDebug(MeasurementAreaLog) << "defereUpdate(): restart."; setState(STATE::RESTARTING); } } void MeasurementArea::storeTiles() { auto start = std::chrono::high_resolution_clock::now(); if (this->_state == STATE::UPDATEING) { qCDebug(MeasurementAreaLog) << "storeTiles(): update."; _tiles->clearAndDeleteContents(); this->_tiles = this->_watcher.result(); this->_watcher.result().reset(); QQmlEngine::setObjectOwnership(this->_tiles.get(), QQmlEngine::CppOwnership); // update tileMap this->_indexMap.clear(); for (int i = 0; i < _tiles->count(); ++i) { auto tile = qobject_cast(_tiles->get(i)); auto it = _indexMap.find(tile->id()); // find unique id if (it != _indexMap.end()) { auto newId = MeasurementTile::randomId(); constexpr long counterMax = 1e6; unsigned long counter = 0; for (; counter <= counterMax; ++counter) { it = _indexMap.find(newId); if (it == _indexMap.end()) { break; } else { newId = MeasurementTile::randomId(); } } if (counter != counterMax) { tile->setId(newId); tile->setProgress(0.0); } else { qCritical() << "MeasurementArea::storeTiles(): not able to find unique id!"; continue; } } _indexMap.insert(std::make_pair(tile->id(), i)); } // This is expensive. Drawing tiles is expensive too. emit this->tilesChanged(); emit progressChanged(); setState(STATE::IDLE); } else if (this->_state == STATE::RESTARTING) { qCDebug(MeasurementAreaLog) << "storeTiles(): restart."; doUpdate(); } else if (this->_state == STATE::STOP) { qCDebug(MeasurementAreaLog) << "storeTiles(): stop."; } qCDebug(MeasurementAreaLog) << "storeTiles() execution time: " << std::chrono::duration_cast( std::chrono::high_resolution_clock::now() - start) .count() << " ms"; } void MeasurementArea::disableUpdate() { setState(STATE::STOP); this->_timer.stop(); } void MeasurementArea::enableUpdate() { if (this->_state == STATE::STOP) { setState(STATE::IDLE); } } void MeasurementArea::init() { this->setObjectName(nameString); QQmlEngine::setObjectOwnership(this->_tiles.get(), QQmlEngine::CppOwnership); connect(&this->_tileHeight, &Fact::rawValueChanged, this, &MeasurementArea::deferUpdate); connect(&this->_tileWidth, &Fact::rawValueChanged, this, &MeasurementArea::deferUpdate); connect(&this->_minTileAreaPercent, &Fact::rawValueChanged, this, &MeasurementArea::deferUpdate); connect(this, &GeoArea::pathChanged, this, &MeasurementArea::deferUpdate); this->_timer.setSingleShot(true); connect(&this->_timer, &QTimer::timeout, this, &MeasurementArea::doUpdate); connect(&this->_watcher, &QFutureWatcher>::finished, this, &MeasurementArea::storeTiles); } void MeasurementArea::setState(MeasurementArea::STATE s) { if (this->_state != s) { auto oldState = this->_state; this->_state = s; if (s == STATE::IDLE || oldState == STATE::IDLE) { emit readyChanged(); } } } void MeasurementArea::updateIds(const QList &array) { for (const auto &diff : array) { auto it = _indexMap.find(diff.oldTile.id()); if (it != _indexMap.end()) { int tileIndex = it->second; auto *tile = _tiles->value(tileIndex); if (diff.oldTile.coordinateList() == tile->coordinateList()) { // Change id and update _tileMap. const auto newId = diff.newTile.id(); tile->setId(newId); _indexMap.erase(it); auto ret = _indexMap.insert(std::make_pair(newId, tileIndex)); Q_ASSERT(ret.second == true /*insert success?*/); Q_UNUSED(ret); } } } } bool getTiles(const FPolygon &area, Length tileHeight, Length tileWidth, Area minTileArea, std::vector &tiles, BoundingBox &bbox) { if (area.outer().empty() || area.outer().size() < 4) { qCDebug(MeasurementAreaLog) << "Area has to few vertices."; return false; } if (tileWidth <= 0 * bu::si::meter || tileHeight <= 0 * bu::si::meter || minTileArea < 0 * bu::si::meter * bu::si::meter) { std::stringstream ss; ss << "Parameters tileWidth (" << tileWidth << "), tileHeight (" << tileHeight << "), minTileArea (" << minTileArea << ") must be positive."; qCDebug(MeasurementAreaLog) << ss.str().c_str(); return false; } if (bbox.corners.outer().size() != 5) { bbox.corners.clear(); minimalBoundingBox(area, bbox); } if (bbox.corners.outer().size() < 5) return false; double bboxWidth = bbox.width; double bboxHeight = bbox.height; FPoint origin = bbox.corners.outer()[0]; // cout << "Origin: " << origin[0] << " " << origin[1] << endl; // Transform _mArea polygon to bounding box coordinate system. trans::rotate_transformer rotate( bbox.angle * 180 / M_PI); trans::translate_transformer translate(-origin.get<0>(), -origin.get<1>()); FPolygon translated_polygon; FPolygon rotated_polygon; boost::geometry::transform(area, translated_polygon, translate); boost::geometry::transform(translated_polygon, rotated_polygon, rotate); bg::correct(rotated_polygon); // cout << bg::wkt(rotated_polygon) << endl; size_t iMax = ceil(bboxWidth / tileWidth.value()); size_t jMax = ceil(bboxHeight / tileHeight.value()); if (iMax < 1 || jMax < 1) { std::stringstream ss; ss << "Tile width (" << tileWidth << ") or tile height (" << tileHeight << ") to large for measurement area."; qCDebug(MeasurementAreaLog) << ss.str().c_str(); return false; } trans::rotate_transformer rotate_back( -bbox.angle * 180 / M_PI); trans::translate_transformer translate_back(origin.get<0>(), origin.get<1>()); for (size_t i = 0; i < iMax; ++i) { double x_min = tileWidth.value() * i; double x_max = x_min + tileWidth.value(); for (size_t j = 0; j < jMax; ++j) { double y_min = tileHeight.value() * j; double y_max = y_min + tileHeight.value(); FPolygon tile_unclipped; tile_unclipped.outer().push_back(FPoint{x_min, y_min}); tile_unclipped.outer().push_back(FPoint{x_min, y_max}); tile_unclipped.outer().push_back(FPoint{x_max, y_max}); tile_unclipped.outer().push_back(FPoint{x_max, y_min}); tile_unclipped.outer().push_back(FPoint{x_min, y_min}); std::deque boost_tiles; if (!boost::geometry::intersection(tile_unclipped, rotated_polygon, boost_tiles)) continue; for (FPolygon &t : boost_tiles) { if (bg::area(t) > minTileArea.value()) { // Transform boost_tile to world coordinate system. FPolygon rotated_tile; FPolygon translated_tile; boost::geometry::transform(t, rotated_tile, rotate_back); boost::geometry::transform(rotated_tile, translated_tile, translate_back); // Store tile and calculate center point. tiles.push_back(translated_tile); } } } } if (tiles.size() < 1) { std::stringstream ss; ss << "No tiles calculated. Is the minTileArea (" << minTileArea << ") parameter large enough?"; qCDebug(MeasurementAreaLog) << ss.str().c_str(); return false; } return true; } QString randomId() { std::srand(std::time(nullptr)); std::int64_t r = 0; for (int i = 0; i < 10; ++i) { r ^= std::rand(); } return QString::number(r); }