/**************************************************************************** * * (c) 2009-2020 QGROUNDCONTROL PROJECT * * QGroundControl is licensed according to the terms in the file * COPYING.md in the root of the source code directory. * ****************************************************************************/ #include "TerrainQuery.h" #include "QGCMapEngine.h" #include "QGeoMapReplyQGC.h" #include "QGCApplication.h" #include #include #include #include #include #include #include #include #include #include #include #include QGC_LOGGING_CATEGORY(TerrainQueryLog, "TerrainQueryLog") QGC_LOGGING_CATEGORY(TerrainQueryVerboseLog, "TerrainQueryVerboseLog") Q_GLOBAL_STATIC(TerrainAtCoordinateBatchManager, _TerrainAtCoordinateBatchManager) Q_GLOBAL_STATIC(TerrainTileManager, _terrainTileManager) TerrainAirMapQuery::TerrainAirMapQuery(QObject* parent) : TerrainQueryInterface(parent) { qCDebug(TerrainQueryVerboseLog) << "supportsSsl" << QSslSocket::supportsSsl() << "sslLibraryBuildVersionString" << QSslSocket::sslLibraryBuildVersionString(); } void TerrainAirMapQuery::requestCoordinateHeights(const QList& coordinates) { if (qgcApp()->runningUnitTests()) { UnitTestTerrainQuery(this).requestCoordinateHeights(coordinates); return; } QString points; for (const QGeoCoordinate& coord: coordinates) { points += QString::number(coord.latitude(), 'f', 10) + "," + QString::number(coord.longitude(), 'f', 10) + ","; } points = points.mid(0, points.length() - 1); // remove the last ',' from string QUrlQuery query; query.addQueryItem(QStringLiteral("points"), points); _queryMode = QueryModeCoordinates; _sendQuery(QString() /* path */, query); } void TerrainAirMapQuery::requestPathHeights(const QGeoCoordinate& fromCoord, const QGeoCoordinate& toCoord) { if (qgcApp()->runningUnitTests()) { UnitTestTerrainQuery(this).requestPathHeights(fromCoord, toCoord); return; } QString points; points += QString::number(fromCoord.latitude(), 'f', 10) + "," + QString::number(fromCoord.longitude(), 'f', 10) + ","; points += QString::number(toCoord.latitude(), 'f', 10) + "," + QString::number(toCoord.longitude(), 'f', 10); QUrlQuery query; query.addQueryItem(QStringLiteral("points"), points); _queryMode = QueryModePath; _sendQuery(QStringLiteral("/path"), query); } void TerrainAirMapQuery::requestCarpetHeights(const QGeoCoordinate& swCoord, const QGeoCoordinate& neCoord, bool statsOnly) { if (qgcApp()->runningUnitTests()) { UnitTestTerrainQuery(this).requestCarpetHeights(swCoord, neCoord, statsOnly); return; } QString points; points += QString::number(swCoord.latitude(), 'f', 10) + "," + QString::number(swCoord.longitude(), 'f', 10) + ","; points += QString::number(neCoord.latitude(), 'f', 10) + "," + QString::number(neCoord.longitude(), 'f', 10); QUrlQuery query; query.addQueryItem(QStringLiteral("points"), points); _queryMode = QueryModeCarpet; _carpetStatsOnly = statsOnly; _sendQuery(QStringLiteral("/carpet"), query); } void TerrainAirMapQuery::_sendQuery(const QString& path, const QUrlQuery& urlQuery) { QUrl url(QStringLiteral("https://api.airmap.com/elevation/v1/ele") + path); qCDebug(TerrainQueryLog) << "_sendQuery" << url; url.setQuery(urlQuery); QNetworkRequest request(url); QSslConfiguration sslConf = request.sslConfiguration(); sslConf.setPeerVerifyMode(QSslSocket::VerifyNone); request.setSslConfiguration(sslConf); QNetworkProxy tProxy; tProxy.setType(QNetworkProxy::DefaultProxy); _networkManager.setProxy(tProxy); QNetworkReply* networkReply = _networkManager.get(request); if (!networkReply) { qCWarning(TerrainQueryLog) << "QNetworkManager::Get did not return QNetworkReply"; _requestFailed(); return; } networkReply->ignoreSslErrors(); connect(networkReply, &QNetworkReply::finished, this, &TerrainAirMapQuery::_requestFinished); connect(networkReply, &QNetworkReply::sslErrors, this, &TerrainAirMapQuery::_sslErrors); #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) connect(networkReply, QOverload::of(&QNetworkReply::error), this, &TerrainAirMapQuery::_requestError); #else connect(networkReply, &QNetworkReply::errorOccurred, this, &TerrainAirMapQuery::_requestError); #endif } void TerrainAirMapQuery::_requestError(QNetworkReply::NetworkError code) { QNetworkReply* reply = qobject_cast(QObject::sender()); if (code != QNetworkReply::NoError) { qCWarning(TerrainQueryLog) << "_requestError error:url:data" << reply->error() << reply->url() << reply->readAll(); return; } } void TerrainAirMapQuery::_sslErrors(const QList &errors) { for (const auto &error : errors) { qCWarning(TerrainQueryLog) << "SSL error: " << error.errorString(); const auto &certificate = error.certificate(); if (!certificate.isNull()) { qCWarning(TerrainQueryLog) << "SSL Certificate problem: " << certificate.toText(); } } } void TerrainAirMapQuery::_requestFinished(void) { QNetworkReply* reply = qobject_cast(QObject::sender()); if (reply->error() != QNetworkReply::NoError) { qCWarning(TerrainQueryLog) << "_requestFinished error:url:data" << reply->error() << reply->url() << reply->readAll(); reply->deleteLater(); _requestFailed(); return; } QByteArray responseBytes = reply->readAll(); reply->deleteLater(); // Convert the response to Json QJsonParseError parseError; QJsonDocument responseJson = QJsonDocument::fromJson(responseBytes, &parseError); if (parseError.error != QJsonParseError::NoError) { qCWarning(TerrainQueryLog) << "_requestFinished unable to parse json:" << parseError.errorString(); _requestFailed(); return; } // Check airmap reponse status QJsonObject rootObject = responseJson.object(); QString status = rootObject["status"].toString(); if (status != "success") { qCWarning(TerrainQueryLog) << "_requestFinished status != success:" << status; _requestFailed(); return; } // Send back data const QJsonValue& jsonData = rootObject["data"]; qCDebug(TerrainQueryLog) << "_requestFinished success"; switch (_queryMode) { case QueryModeCoordinates: emit _parseCoordinateData(jsonData); break; case QueryModePath: emit _parsePathData(jsonData); break; case QueryModeCarpet: emit _parseCarpetData(jsonData); break; } } void TerrainAirMapQuery::_requestFailed(void) { switch (_queryMode) { case QueryModeCoordinates: emit coordinateHeightsReceived(false /* success */, QList() /* heights */); break; case QueryModePath: emit pathHeightsReceived(false /* success */, qQNaN() /* latStep */, qQNaN() /* lonStep */, QList() /* heights */); break; case QueryModeCarpet: emit carpetHeightsReceived(false /* success */, qQNaN() /* minHeight */, qQNaN() /* maxHeight */, QList>() /* carpet */); break; } } void TerrainAirMapQuery::_parseCoordinateData(const QJsonValue& coordinateJson) { QList heights; const QJsonArray& dataArray = coordinateJson.toArray(); for (int i = 0; i < dataArray.count(); i++) { heights.append(dataArray[i].toDouble()); } emit coordinateHeightsReceived(true /* success */, heights); } void TerrainAirMapQuery::_parsePathData(const QJsonValue& pathJson) { QJsonObject jsonObject = pathJson.toArray()[0].toObject(); QJsonArray stepArray = jsonObject["step"].toArray(); QJsonArray profileArray = jsonObject["profile"].toArray(); double latStep = stepArray[0].toDouble(); double lonStep = stepArray[1].toDouble(); QList heights; for (QJsonValue profileValue: profileArray) { heights.append(profileValue.toDouble()); } emit pathHeightsReceived(true /* success */, latStep, lonStep, heights); } void TerrainAirMapQuery::_parseCarpetData(const QJsonValue& carpetJson) { QJsonObject jsonObject = carpetJson.toArray()[0].toObject(); QJsonObject statsObject = jsonObject["stats"].toObject(); double minHeight = statsObject["min"].toDouble(); double maxHeight = statsObject["max"].toDouble(); QList> carpet; if (!_carpetStatsOnly) { QJsonArray carpetArray = jsonObject["carpet"].toArray(); for (int i=0; i()); for (int j=0; j& coordinates) { if (qgcApp()->runningUnitTests()) { UnitTestTerrainQuery(this).requestCoordinateHeights(coordinates); return; } if (coordinates.length() == 0) { return; } _terrainTileManager->addCoordinateQuery(this, coordinates); } void TerrainOfflineAirMapQuery::requestPathHeights(const QGeoCoordinate& fromCoord, const QGeoCoordinate& toCoord) { if (qgcApp()->runningUnitTests()) { UnitTestTerrainQuery(this).requestPathHeights(fromCoord, toCoord); return; } _terrainTileManager->addPathQuery(this, fromCoord, toCoord); } void TerrainOfflineAirMapQuery::requestCarpetHeights(const QGeoCoordinate& swCoord, const QGeoCoordinate& neCoord, bool statsOnly) { if (qgcApp()->runningUnitTests()) { UnitTestTerrainQuery(this).requestCarpetHeights(swCoord, neCoord, statsOnly); return; } // TODO Q_UNUSED(swCoord); Q_UNUSED(neCoord); Q_UNUSED(statsOnly); qWarning() << "Carpet queries are currently not supported from offline air map data"; } void TerrainOfflineAirMapQuery::_signalCoordinateHeights(bool success, QList heights) { emit coordinateHeightsReceived(success, heights); } void TerrainOfflineAirMapQuery::_signalPathHeights(bool success, double distanceBetween, double finalDistanceBetween, const QList& heights) { emit pathHeightsReceived(success, distanceBetween, finalDistanceBetween, heights); } void TerrainOfflineAirMapQuery::_signalCarpetHeights(bool success, double minHeight, double maxHeight, const QList>& carpet) { emit carpetHeightsReceived(success, minHeight, maxHeight, carpet); } TerrainTileManager::TerrainTileManager(void) { } void TerrainTileManager::addCoordinateQuery(TerrainOfflineAirMapQuery* terrainQueryInterface, const QList& coordinates) { qCDebug(TerrainQueryLog) << "TerrainTileManager::addCoordinateQuery count" << coordinates.count(); if (coordinates.length() > 0) { bool error; QList altitudes; if (!getAltitudesForCoordinates(coordinates, altitudes, error)) { qCDebug(TerrainQueryLog) << "TerrainTileManager::addPathQuery queue count" << _requestQueue.count(); QueuedRequestInfo_t queuedRequestInfo = { terrainQueryInterface, QueryMode::QueryModeCoordinates, 0, 0, coordinates }; _requestQueue.append(queuedRequestInfo); return; } if (error) { QList noAltitudes; qCWarning(TerrainQueryLog) << "addCoordinateQuery: signalling failure due to internal error"; terrainQueryInterface->_signalCoordinateHeights(false, noAltitudes); } else { qCDebug(TerrainQueryLog) << "addCoordinateQuery: All altitudes taken from cached data"; terrainQueryInterface->_signalCoordinateHeights(coordinates.count() == altitudes.count(), altitudes); } } } void TerrainTileManager::addPathQuery(TerrainOfflineAirMapQuery* terrainQueryInterface, const QGeoCoordinate &startPoint, const QGeoCoordinate &endPoint) { // Convert to individual coordinate queries //qDebug() << "TerrainTileManager::addPathQuery()" << startPoint; QList coordinates; double lat = startPoint.latitude(); double lon = startPoint.longitude(); <<<<<<< HEAD <<<<<<< HEAD double steps = ceil(endPoint.distanceTo(startPoint) / TerrainTile::terrainAltitudeSpacing); // if (qFuzzyIsNull(steps)) // return; ======= double steps = ceil(endPoint.distanceTo(startPoint) / TerrainTile::tileValueSpacingMeters); >>>>>>> upstream_merge ======= double steps = ceil(endPoint.distanceTo(startPoint) / TerrainTile::tileValueSpacingMeters); >>>>>>> upstream_merge double latDiff = endPoint.latitude() - lat; double lonDiff = endPoint.longitude() - lon; double distanceBetween; double finalDistanceBetween; if (steps == 0) { coordinates.append(startPoint); coordinates.append(endPoint); distanceBetween = finalDistanceBetween = coordinates[0].distanceTo(coordinates[1]); } else { for (double i = 0.0; i <= steps; i = i + 1) { coordinates.append(QGeoCoordinate(lat + latDiff * i / steps, lon + lonDiff * i / steps)); } // We always have one too many and we always want the last one to be the endpoint coordinates.last() = endPoint; distanceBetween = coordinates[0].distanceTo(coordinates[1]); finalDistanceBetween = coordinates[coordinates.count() - 2].distanceTo(coordinates.last()); } //qDebug() << "terrain" << startPoint.distanceTo(endPoint) << coordinates.count() << distanceBetween; qCDebug(TerrainQueryLog) << "TerrainTileManager::addPathQuery start:end:coordCount" << startPoint << endPoint << coordinates.count(); bool error; QList altitudes; if (!getAltitudesForCoordinates(coordinates, altitudes, error)) { qCDebug(TerrainQueryLog) << "TerrainTileManager::addPathQuery queue count" << _requestQueue.count(); QueuedRequestInfo_t queuedRequestInfo = { terrainQueryInterface, QueryMode::QueryModePath, distanceBetween, finalDistanceBetween, coordinates }; _requestQueue.append(queuedRequestInfo); return; } if (error) { QList noAltitudes; qCWarning(TerrainQueryLog) << "addPathQuery: signalling failure due to internal error"; terrainQueryInterface->_signalPathHeights(false, distanceBetween, finalDistanceBetween, noAltitudes); } else { qCDebug(TerrainQueryLog) << "addPathQuery: All altitudes taken from cached data"; terrainQueryInterface->_signalPathHeights(coordinates.count() == altitudes.count(), distanceBetween, finalDistanceBetween, altitudes); } } /// Either returns altitudes from cache or queues database request /// @param[out] error true: altitude not returned due to error, false: altitudes returned /// @return true: altitude returned (check error as well), false: database query queued (altitudes not returned) bool TerrainTileManager::getAltitudesForCoordinates(const QList& coordinates, QList& altitudes, bool& error) { error = false; for (const QGeoCoordinate& coordinate: coordinates) { QString tileHash = _getTileHash(coordinate); qCDebug(TerrainQueryLog) << "TerrainTileManager::getAltitudesForCoordinates hash:coordinate" << tileHash << coordinate; _tilesMutex.lock(); if (_tiles.contains(tileHash)) { if (_tiles[tileHash].isIn(coordinate)) { double elevation = _tiles[tileHash].elevation(coordinate); if (qIsNaN(elevation)) { error = true; qCWarning(TerrainQueryLog) << "TerrainTileManager::getAltitudesForCoordinates Internal Error: missing elevation in tile cache"; } else { qCDebug(TerrainQueryLog) << "TerrainTileManager::getAltitudesForCoordinates returning elevation from tile cache" << elevation; } altitudes.push_back(elevation); } else { qCWarning(TerrainQueryLog) << "TerrainTileManager::getAltitudesForCoordinates Internal Error: coordinate not in tile region"; altitudes.push_back(qQNaN()); error = true; } } else { if (_state != State::Downloading) { QNetworkRequest request = getQGCMapEngine()->urlFactory()->getTileURL("Airmap Elevation", getQGCMapEngine()->urlFactory()->long2tileX("Airmap Elevation",coordinate.longitude(), 1), getQGCMapEngine()->urlFactory()->lat2tileY("Airmap Elevation", coordinate.latitude(), 1), 1, &_networkManager); qCDebug(TerrainQueryLog) << "TerrainTileManager::getAltitudesForCoordinates query from database" << request.url(); QGeoTileSpec spec; spec.setX(getQGCMapEngine()->urlFactory()->long2tileX("Airmap Elevation", coordinate.longitude(), 1)); spec.setY(getQGCMapEngine()->urlFactory()->lat2tileY("Airmap Elevation", coordinate.latitude(), 1)); spec.setZoom(1); spec.setMapId(getQGCMapEngine()->urlFactory()->getIdFromType("Airmap Elevation")); QGeoTiledMapReplyQGC* reply = new QGeoTiledMapReplyQGC(&_networkManager, request, spec); connect(reply, &QGeoTiledMapReplyQGC::terrainDone, this, &TerrainTileManager::_terrainDone); _state = State::Downloading; } _tilesMutex.unlock(); return false; } _tilesMutex.unlock(); } return true; } void TerrainTileManager::_tileFailed(void) { QList noAltitudes; for (const QueuedRequestInfo_t& requestInfo: _requestQueue) { if (requestInfo.queryMode == QueryMode::QueryModeCoordinates) { requestInfo.terrainQueryInterface->_signalCoordinateHeights(false, noAltitudes); } else if (requestInfo.queryMode == QueryMode::QueryModePath) { requestInfo.terrainQueryInterface->_signalPathHeights(false, requestInfo.distanceBetween, requestInfo.finalDistanceBetween, noAltitudes); } } _requestQueue.clear(); } void TerrainTileManager::_terrainDone(QByteArray responseBytes, QNetworkReply::NetworkError error) { QGeoTiledMapReplyQGC* reply = qobject_cast(QObject::sender()); _state = State::Idle; if (!reply) { qCWarning(TerrainQueryLog) << "Elevation tile fetched but invalid reply data type."; return; } // remove from download queue QGeoTileSpec spec = reply->tileSpec(); QString hash = QGCMapEngine::getTileHash("Airmap Elevation", spec.x(), spec.y(), spec.zoom()); // handle potential errors if (error != QNetworkReply::NoError) { qCWarning(TerrainQueryLog) << "Elevation tile fetching returned error (" << error << ")"; _tileFailed(); reply->deleteLater(); return; } if (responseBytes.isEmpty()) { qCWarning(TerrainQueryLog) << "Error in fetching elevation tile. Empty response."; _tileFailed(); reply->deleteLater(); return; } qCDebug(TerrainQueryLog) << "Received some bytes of terrain data: " << responseBytes.size(); TerrainTile* terrainTile = new TerrainTile(responseBytes); if (terrainTile->isValid()) { _tilesMutex.lock(); if (!_tiles.contains(hash)) { _tiles.insert(hash, *terrainTile); } else { delete terrainTile; } _tilesMutex.unlock(); } else { delete terrainTile; qCWarning(TerrainQueryLog) << "Received invalid tile"; } reply->deleteLater(); // now try to query the data again for (int i = _requestQueue.count() - 1; i >= 0; i--) { bool error; QList altitudes; QueuedRequestInfo_t& requestInfo = _requestQueue[i]; if (getAltitudesForCoordinates(requestInfo.coordinates, altitudes, error)) { if (requestInfo.queryMode == QueryMode::QueryModeCoordinates) { if (error) { QList noAltitudes; qCWarning(TerrainQueryLog) << "_terrainDone(coordinateQuery): signalling failure due to internal error"; requestInfo.terrainQueryInterface->_signalCoordinateHeights(false, noAltitudes); } else { qCDebug(TerrainQueryLog) << "_terrainDone(coordinateQuery): All altitudes taken from cached data"; requestInfo.terrainQueryInterface->_signalCoordinateHeights(requestInfo.coordinates.count() == altitudes.count(), altitudes); } } else if (requestInfo.queryMode == QueryMode::QueryModePath) { if (error) { QList noAltitudes; qCWarning(TerrainQueryLog) << "_terrainDone(coordinateQuery): signalling failure due to internal error"; requestInfo.terrainQueryInterface->_signalPathHeights(false, requestInfo.distanceBetween, requestInfo.finalDistanceBetween, noAltitudes); } else { qCDebug(TerrainQueryLog) << "_terrainDone(coordinateQuery): All altitudes taken from cached data"; requestInfo.terrainQueryInterface->_signalPathHeights(requestInfo.coordinates.count() == altitudes.count(), requestInfo.distanceBetween, requestInfo.finalDistanceBetween, altitudes); } } _requestQueue.removeAt(i); } } } QString TerrainTileManager::_getTileHash(const QGeoCoordinate& coordinate) { QString ret = QGCMapEngine::getTileHash( "Airmap Elevation", getQGCMapEngine()->urlFactory()->long2tileX("Airmap Elevation", coordinate.longitude(), 1), getQGCMapEngine()->urlFactory()->lat2tileY("Airmap Elevation", coordinate.latitude(), 1), 1); qCDebug(TerrainQueryVerboseLog) << "Computing unique tile hash for " << coordinate << ret; return ret; } TerrainAtCoordinateBatchManager::TerrainAtCoordinateBatchManager(void) { _batchTimer.setSingleShot(true); _batchTimer.setInterval(_batchTimeout); connect(&_batchTimer, &QTimer::timeout, this, &TerrainAtCoordinateBatchManager::_sendNextBatch); connect(&_terrainQuery, &TerrainQueryInterface::coordinateHeightsReceived, this, &TerrainAtCoordinateBatchManager::_coordinateHeights); } void TerrainAtCoordinateBatchManager::addQuery(TerrainAtCoordinateQuery* terrainAtCoordinateQuery, const QList& coordinates) { if (coordinates.length() > 0) { connect(terrainAtCoordinateQuery, &TerrainAtCoordinateQuery::destroyed, this, &TerrainAtCoordinateBatchManager::_queryObjectDestroyed); QueuedRequestInfo_t queuedRequestInfo = { terrainAtCoordinateQuery, coordinates }; _requestQueue.append(queuedRequestInfo); if (!_batchTimer.isActive()) { _batchTimer.start(); } } } void TerrainAtCoordinateBatchManager::_sendNextBatch(void) { qCDebug(TerrainQueryLog) << "TerrainAtCoordinateBatchManager::_sendNextBatch _state:_requestQueue.count:_sentRequests.count" << _stateToString(_state) << _requestQueue.count() << _sentRequests.count(); if (_state != State::Idle) { // Waiting for last download the complete, wait some more qCDebug(TerrainQueryLog) << "TerrainAtCoordinateBatchManager::_sendNextBatch waiting for current batch, restarting timer"; _batchTimer.start(); return; } if (_requestQueue.count() == 0) { return; } _sentRequests.clear(); // Convert coordinates to point strings for json query QList coords; int requestQueueAdded = 0; for (const QueuedRequestInfo_t& requestInfo: _requestQueue) { SentRequestInfo_t sentRequestInfo = { requestInfo.terrainAtCoordinateQuery, false, requestInfo.coordinates.count() }; _sentRequests.append(sentRequestInfo); coords += requestInfo.coordinates; requestQueueAdded++; if (coords.count() > 50) { break; } } _requestQueue = _requestQueue.mid(requestQueueAdded); qCDebug(TerrainQueryLog) << "TerrainAtCoordinateBatchManager::_sendNextBatch requesting next batch _state:_requestQueue.count:_sentRequests.count" << _stateToString(_state) << _requestQueue.count() << _sentRequests.count(); _state = State::Downloading; _terrainQuery.requestCoordinateHeights(coords); } void TerrainAtCoordinateBatchManager::_batchFailed(void) { QList noHeights; for (const SentRequestInfo_t& sentRequestInfo: _sentRequests) { if (!sentRequestInfo.queryObjectDestroyed) { disconnect(sentRequestInfo.terrainAtCoordinateQuery, &TerrainAtCoordinateQuery::destroyed, this, &TerrainAtCoordinateBatchManager::_queryObjectDestroyed); sentRequestInfo.terrainAtCoordinateQuery->_signalTerrainData(false, noHeights); } } _sentRequests.clear(); } void TerrainAtCoordinateBatchManager::_queryObjectDestroyed(QObject* terrainAtCoordinateQuery) { // Remove/Mark deleted objects queries from queues qCDebug(TerrainQueryLog) << "_TerrainAtCoordinateQueryDestroyed TerrainAtCoordinateQuery" << terrainAtCoordinateQuery; int i = 0; while (i < _requestQueue.count()) { const QueuedRequestInfo_t& requestInfo = _requestQueue[i]; if (requestInfo.terrainAtCoordinateQuery == terrainAtCoordinateQuery) { qCDebug(TerrainQueryLog) << "Removing deleted provider from _requestQueue index:terrainAtCoordinateQuery" << i << requestInfo.terrainAtCoordinateQuery; _requestQueue.removeAt(i); } else { i++; } } for (int i=0; i<_sentRequests.count(); i++) { SentRequestInfo_t& sentRequestInfo = _sentRequests[i]; if (sentRequestInfo.terrainAtCoordinateQuery == terrainAtCoordinateQuery) { qCDebug(TerrainQueryLog) << "Zombieing deleted provider from _sentRequests index:terrainAtCoordinateQuery" << sentRequestInfo.terrainAtCoordinateQuery; sentRequestInfo.queryObjectDestroyed = true; } } } QString TerrainAtCoordinateBatchManager::_stateToString(State state) { switch (state) { case State::Idle: return QStringLiteral("Idle"); case State::Downloading: return QStringLiteral("Downloading"); } return QStringLiteral("State unknown"); } void TerrainAtCoordinateBatchManager::_coordinateHeights(bool success, QList heights) { _state = State::Idle; qCDebug(TerrainQueryLog) << "TerrainAtCoordinateBatchManager::_coordinateHeights signalled success:count" << success << heights.count(); if (!success) { _batchFailed(); return; } int currentIndex = 0; for (const SentRequestInfo_t& sentRequestInfo: _sentRequests) { if (!sentRequestInfo.queryObjectDestroyed) { qCDebug(TerrainQueryVerboseLog) << "TerrainAtCoordinateBatchManager::_coordinateHeights returned TerrainCoordinateQuery:count" << sentRequestInfo.terrainAtCoordinateQuery << sentRequestInfo.cCoord; disconnect(sentRequestInfo.terrainAtCoordinateQuery, &TerrainAtCoordinateQuery::destroyed, this, &TerrainAtCoordinateBatchManager::_queryObjectDestroyed); QList requestAltitudes = heights.mid(currentIndex, sentRequestInfo.cCoord); sentRequestInfo.terrainAtCoordinateQuery->_signalTerrainData(true, requestAltitudes); currentIndex += sentRequestInfo.cCoord; } } _sentRequests.clear(); if (_requestQueue.count()) { _batchTimer.start(); } } TerrainAtCoordinateQuery::TerrainAtCoordinateQuery(bool autoDelete) : _autoDelete(autoDelete) { } void TerrainAtCoordinateQuery::requestData(const QList& coordinates) { if (coordinates.length() == 0) { return; } _TerrainAtCoordinateBatchManager->addQuery(this, coordinates); } bool TerrainAtCoordinateQuery::getAltitudesForCoordinates(const QList& coordinates, QList& altitudes, bool& error) { return _terrainTileManager->getAltitudesForCoordinates(coordinates, altitudes, error); } void TerrainAtCoordinateQuery::_signalTerrainData(bool success, QList& heights) { emit terrainDataReceived(success, heights); if (_autoDelete) { deleteLater(); } } TerrainPathQuery::TerrainPathQuery(bool autoDelete) : _autoDelete (autoDelete) { qRegisterMetaType(); connect(&_terrainQuery, &TerrainQueryInterface::pathHeightsReceived, this, &TerrainPathQuery::_pathHeights); } void TerrainPathQuery::requestData(const QGeoCoordinate& fromCoord, const QGeoCoordinate& toCoord) { _terrainQuery.requestPathHeights(fromCoord, toCoord); } void TerrainPathQuery::_pathHeights(bool success, double distanceBetween, double finalDistanceBetween, const QList& heights) { PathHeightInfo_t pathHeightInfo; pathHeightInfo.distanceBetween = distanceBetween; pathHeightInfo.finalDistanceBetween = finalDistanceBetween; pathHeightInfo.heights = heights; emit terrainDataReceived(success, pathHeightInfo); if (_autoDelete) { deleteLater(); } } TerrainPolyPathQuery::TerrainPolyPathQuery(bool autoDelete) : _autoDelete (autoDelete) , _pathQuery (false /* autoDelete */) { connect(&_pathQuery, &TerrainPathQuery::terrainDataReceived, this, &TerrainPolyPathQuery::_terrainDataReceived); } void TerrainPolyPathQuery::requestData(const QVariantList& polyPath) { QList path; for (const QVariant& geoVar: polyPath) { path.append(geoVar.value()); } requestData(path); } void TerrainPolyPathQuery::requestData(const QList& polyPath) { qCDebug(TerrainQueryLog) << "TerrainPolyPathQuery::requestData count" << polyPath.count(); // Kick off first request _rgCoords = polyPath; _curIndex = 0; _pathQuery.requestData(_rgCoords[0], _rgCoords[1]); } void TerrainPolyPathQuery::_terrainDataReceived(bool success, const TerrainPathQuery::PathHeightInfo_t& pathHeightInfo) { qCDebug(TerrainQueryLog) << "TerrainPolyPathQuery::_terrainDataReceived success:_curIndex" << success << _curIndex; if (!success) { _rgPathHeightInfo.clear(); emit terrainDataReceived(false /* success */, _rgPathHeightInfo); return; } _rgPathHeightInfo.append(pathHeightInfo); if (++_curIndex >= _rgCoords.count() - 1) { // We've finished all requests qCDebug(TerrainQueryLog) << "TerrainPolyPathQuery::_terrainDataReceived complete"; emit terrainDataReceived(true /* success */, _rgPathHeightInfo); if (_autoDelete) { deleteLater(); } } else { _pathQuery.requestData(_rgCoords[_curIndex], _rgCoords[_curIndex+1]); } } const QGeoCoordinate UnitTestTerrainQuery::pointNemo{-48.875556, -123.392500}; const UnitTestTerrainQuery::Flat10Region UnitTestTerrainQuery::flat10Region{{ pointNemo, QGeoCoordinate{ pointNemo.latitude() - UnitTestTerrainQuery::regionExtentDeg, pointNemo.longitude() + UnitTestTerrainQuery::regionExtentDeg } }}; const double UnitTestTerrainQuery::Flat10Region::elevationMts = 10; const UnitTestTerrainQuery::LinearSlopeRegion UnitTestTerrainQuery::linearSlopeRegion{{ flat10Region.topRight(), QGeoCoordinate{ flat10Region.topRight().latitude() - UnitTestTerrainQuery::regionExtentDeg, flat10Region.topRight().longitude() + UnitTestTerrainQuery::regionExtentDeg } }}; const double UnitTestTerrainQuery::LinearSlopeRegion::minElevationMts = -100; const double UnitTestTerrainQuery::LinearSlopeRegion::maxElevationMts = 1000; const double UnitTestTerrainQuery::LinearSlopeRegion::dElevationMts = maxElevationMts-minElevationMts; UnitTestTerrainQuery::UnitTestTerrainQuery(TerrainQueryInterface* parent) :TerrainQueryInterface(parent) {} void UnitTestTerrainQuery::requestCoordinateHeights(const QList& coordinates) { QList result = requestCoordinateHeightsSync(coordinates); emit qobject_cast(parent())->coordinateHeightsReceived(result.size() == coordinates.size(), result); } void UnitTestTerrainQuery::requestPathHeights(const QGeoCoordinate& fromCoord, const QGeoCoordinate& toCoord) { QPair, QList> result = requestPathHeightsSync(fromCoord, toCoord); emit qobject_cast(parent())->pathHeightsReceived( result.second.size() > 0, result.first[0].distanceTo(result.first[1]), result.first[result.first.size()-2].distanceTo(result.first.back()), result.second ); } void UnitTestTerrainQuery::requestCarpetHeights(const QGeoCoordinate& swCoord, const QGeoCoordinate& neCoord, bool) { assert(swCoord.longitude() < neCoord.longitude()); assert(swCoord.latitude() < neCoord.latitude()); double min = std::numeric_limits::max(); double max = std::numeric_limits::min(); QList> carpet; for (double lat = swCoord.latitude(); lat < neCoord.latitude(); lat++) { QList row = requestPathHeightsSync({lat,swCoord.longitude()}, {lat,neCoord.longitude()}).second; if (row.size() == 0) { emit carpetHeightsReceived(false, qQNaN(), qQNaN(), QList>()); return; } for (const auto val : row) { min = std::min(val,min); max = std::max(val,max); } carpet.push_back(row); } emit qobject_cast(parent())->carpetHeightsReceived(true, min, max, carpet); } QPair, QList> UnitTestTerrainQuery::requestPathHeightsSync(const QGeoCoordinate& fromCoord, const QGeoCoordinate& toCoord) { QList coordinates; coordinates.push_back(fromCoord); //cast to pixels long x0 = std::floor(fromCoord.longitude()/one_second_deg); long x1 = std::floor(toCoord.longitude()/one_second_deg); long y0 = std::floor(fromCoord.latitude()/one_second_deg); long y1 = std::floor(toCoord.latitude()/one_second_deg); //bresenham line algo long dx = abs(x1-x0), sx = x0dy ? dx : -dy)/2, e2; while(true) { e2 = err; if (e2 >-dx) { err -= dy; x0 += sx; } if (e2 < dy) { err += dx; y0 += sy; } if ((x0==x1 && y0==y1)) { break; } coordinates.push_back({y0*one_second_deg, x0*one_second_deg}); } coordinates.push_back(toCoord); return QPair, QList>(coordinates, requestCoordinateHeightsSync(coordinates)); } QList UnitTestTerrainQuery::requestCoordinateHeightsSync(const QList& coordinates) { QList result; for (const auto& coordinate : coordinates) { if (flat10Region.contains(coordinate)) { result.push_back(UnitTestTerrainQuery::Flat10Region::elevationMts); } else if (linearSlopeRegion.contains(coordinate)) { //cast to one_second_deg grid and round to int to emulate SRTM1 even better long x = (coordinate.longitude() - linearSlopeRegion.topLeft().longitude())/one_second_deg; long dx = regionExtentDeg/one_second_deg; double fraction = 1.0 * x / dx; result.push_back( std::round( UnitTestTerrainQuery::LinearSlopeRegion::minElevationMts + (fraction * UnitTestTerrainQuery::LinearSlopeRegion::dElevationMts) ) ); } else { result.clear(); break; } } return result; }