diff --git a/src/QtLocationPlugin/ElevationMapProvider.cpp b/src/QtLocationPlugin/ElevationMapProvider.cpp index c362464b5037..b14741dcfeda 100644 --- a/src/QtLocationPlugin/ElevationMapProvider.cpp +++ b/src/QtLocationPlugin/ElevationMapProvider.cpp @@ -6,8 +6,16 @@ #include "QGCMapEngine.h" #include "TerrainTile.h" +/* +License for the COPERNICUS dataset hosted on https://terrain-ce.suite.auterion.com/: + +© DLR e.V. 2010-2014 and © Airbus Defence and Space GmbH 2014-2018 provided under COPERNICUS +by the European Union and ESA; all rights reserved. + +*/ + ElevationProvider::ElevationProvider(const QString& imageFormat, quint32 averageSize, QGeoMapType::MapStyle mapType, QObject* parent) - : MapProvider(QStringLiteral("https://api.airmap.com/"), imageFormat, averageSize, mapType, parent) {} + : MapProvider(QStringLiteral("https://terrain-ce.suite.auterion.com/"), imageFormat, averageSize, mapType, parent) {} //----------------------------------------------------------------------------- int AirmapElevationProvider::long2tileX(const double lon, const int z) const { @@ -24,7 +32,7 @@ int AirmapElevationProvider::lat2tileY(const double lat, const int z) const { QString AirmapElevationProvider::_getURL(const int x, const int y, const int zoom, QNetworkAccessManager* networkManager) { Q_UNUSED(networkManager) Q_UNUSED(zoom) - return QString("https://api.airmap.com/elevation/v1/ele/carpet?points=%1,%2,%3,%4") + return QString("https://terrain-ce.suite.auterion.com/api/v1/carpet?points=%1,%2,%3,%4") .arg(static_cast(y) * TerrainTile::tileSizeDegrees - 90.0) .arg(static_cast(x) * TerrainTile::tileSizeDegrees - 180.0) .arg(static_cast(y + 1) * TerrainTile::tileSizeDegrees - 90.0) diff --git a/src/QtLocationPlugin/QGCMapEngine.cpp b/src/QtLocationPlugin/QGCMapEngine.cpp index 15774db3683e..b4806067640a 100644 --- a/src/QtLocationPlugin/QGCMapEngine.cpp +++ b/src/QtLocationPlugin/QGCMapEngine.cpp @@ -35,7 +35,7 @@ Q_DECLARE_METATYPE(QList) static const char* kDbFileName = "qgcMapCache.db"; static QLocale kLocale; -#define CACHE_PATH_VERSION "300" +#define CACHE_PATH_VERSION "301" struct stQGeoTileCacheQGCMapTypes { const char* name; diff --git a/src/TerrainTile.cc b/src/TerrainTile.cc index 654847fc58b6..d1960fd1803c 100644 --- a/src/TerrainTile.cc +++ b/src/TerrainTile.cc @@ -31,140 +31,128 @@ const char* TerrainTile::_jsonMinElevationKey = "min"; const char* TerrainTile::_jsonAvgElevationKey = "avg"; const char* TerrainTile::_jsonCarpetKey = "carpet"; -TerrainTile::TerrainTile() - : _minElevation(-1.0) - , _maxElevation(-1.0) - , _avgElevation(-1.0) - , _data(nullptr) - , _gridSizeLat(-1) - , _gridSizeLon(-1) - , _isValid(false) +TerrainTile::TerrainTile(const QByteArray& byteArray) { + // Copy tile info + _tileInfo = *reinterpret_cast(byteArray.constData()); -} - -TerrainTile::~TerrainTile() -{ - if (_data) { - for (int i = 0; i < _gridSizeLat; i++) { - delete[] _data[i]; - } - delete[] _data; - _data = nullptr; + // Check feasibility + if ((_tileInfo.neLon - _tileInfo.swLon) < 0.0 || (_tileInfo.neLat - _tileInfo.swLat) < 0.0) { + qCWarning(TerrainTileLog) << this << "Tile extent is infeasible"; + _isValid = false; + return; } -} -TerrainTile::TerrainTile(QByteArray byteArray) - : _minElevation(-1.0) - , _maxElevation(-1.0) - , _avgElevation(-1.0) - , _data(nullptr) - , _gridSizeLat(-1) - , _gridSizeLon(-1) - , _isValid(false) -{ + _cellSizeLat = (_tileInfo.neLat - _tileInfo.swLat) / _tileInfo.gridSizeLat; + _cellSizeLon = (_tileInfo.neLon - _tileInfo.swLon) / _tileInfo.gridSizeLon; + + qCDebug(TerrainTileLog) << this << "TileInfo: south west: " << _tileInfo.swLat << _tileInfo.swLon; + qCDebug(TerrainTileLog) << this << "TileInfo: north east: " << _tileInfo.neLat << _tileInfo.gridSizeLon; + qCDebug(TerrainTileLog) << this << "TileInfo: dimensions: " << _tileInfo.gridSizeLat << "by" << _tileInfo.gridSizeLat; + qCDebug(TerrainTileLog) << this << "TileInfo: min, max, avg: " << _tileInfo.minElevation << _tileInfo.maxElevation << _tileInfo.avgElevation; + qCDebug(TerrainTileLog) << this << "TileInfo: cell size: " << _cellSizeLat << _cellSizeLon; + int cTileHeaderBytes = static_cast(sizeof(TileInfo_t)); int cTileBytesAvailable = byteArray.size(); if (cTileBytesAvailable < cTileHeaderBytes) { - qWarning() << "Terrain tile binary data too small for TileInfo_s header"; + qCWarning(TerrainTileLog) << "Terrain tile binary data too small for TileInfo_s header"; return; } - const TileInfo_t* tileInfo = reinterpret_cast(byteArray.constData()); - _southWest.setLatitude(tileInfo->swLat); - _southWest.setLongitude(tileInfo->swLon); - _northEast.setLatitude(tileInfo->neLat); - _northEast.setLongitude(tileInfo->neLon); - _minElevation = tileInfo->minElevation; - _maxElevation = tileInfo->maxElevation; - _avgElevation = tileInfo->avgElevation; - _gridSizeLat = tileInfo->gridSizeLat; - _gridSizeLon = tileInfo->gridSizeLon; - - qCDebug(TerrainTileLog) << "Loading terrain tile: " << _southWest << " - " << _northEast; - qCDebug(TerrainTileLog) << "min:max:avg:sizeLat:sizeLon" << _minElevation << _maxElevation << _avgElevation << _gridSizeLat << _gridSizeLon; - - int cTileDataBytes = static_cast(sizeof(int16_t)) * _gridSizeLat * _gridSizeLon; + int cTileDataBytes = static_cast(sizeof(int16_t)) * _tileInfo.gridSizeLat * _tileInfo.gridSizeLon; if (cTileBytesAvailable < cTileHeaderBytes + cTileDataBytes) { - qWarning() << "Terrain tile binary data too small for tile data"; + qCWarning(TerrainTileLog) << "Terrain tile binary data too small for tile data"; return; } - _data = new int16_t*[_gridSizeLat]; - for (int k = 0; k < _gridSizeLat; k++) { - _data[k] = new int16_t[_gridSizeLon]; + _data = new int16_t*[_tileInfo.gridSizeLat]; + for (int k = 0; k < _tileInfo.gridSizeLat; k++) { + _data[k] = new int16_t[_tileInfo.gridSizeLon]; } int valueIndex = 0; const int16_t* pTileData = reinterpret_cast(&reinterpret_cast(byteArray.constData())[cTileHeaderBytes]); - for (int i = 0; i < _gridSizeLat; i++) { - for (int j = 0; j < _gridSizeLon; j++) { + for (int i = 0; i < _tileInfo.gridSizeLat; i++) { + for (int j = 0; j < _tileInfo.gridSizeLon; j++) { _data[i][j] = pTileData[valueIndex++]; } } _isValid = true; +} - return; +TerrainTile::~TerrainTile() +{ + if (!_data) { + return; + } + + for (unsigned i = 0; i < static_cast(_tileInfo.gridSizeLat); i++) { + delete[] _data[i]; + } + + delete[] _data; } double TerrainTile::elevation(const QGeoCoordinate& coordinate) const { - if (_isValid && _southWest.isValid() && _northEast.isValid()) { - qCDebug(TerrainTileLog) << "elevation: " << coordinate << " , in sw " << _southWest << " , ne " << _northEast; - - // The lat/lon values in _northEast and _southWest coordinates can have rounding errors such that the coordinate - // request may be slightly outside the tile box specified by these values. So we clamp the incoming values to the - // edges of the tile if needed. - - double clampedLon = qMax(coordinate.longitude(), _southWest.longitude()); - double clampedLat = qMax(coordinate.latitude(), _southWest.latitude()); - - // Calc the index of the southernmost and westernmost index data value - int lonIndex = qFloor((clampedLon - _southWest.longitude()) / tileValueSpacingDegrees); - int latIndex = qFloor((clampedLat - _southWest.latitude()) / tileValueSpacingDegrees); - - // Calc how far along in between the known values the requested lat/lon is fractionally - double lonIndexLongitude = _southWest.longitude() + (static_cast(lonIndex) * tileValueSpacingDegrees); - double lonFraction = (clampedLon - lonIndexLongitude) / tileValueSpacingDegrees; - double latIndexLatitude = _southWest.latitude() + (static_cast(latIndex) * tileValueSpacingDegrees); - double latFraction = (clampedLat - latIndexLatitude) / tileValueSpacingDegrees; - - // Calc the elevation as the average across the four known points - double known00 = _data[latIndex][lonIndex]; - double known01 = _data[latIndex][lonIndex+1]; - double known10 = _data[latIndex+1][lonIndex]; - double known11 = _data[latIndex+1][lonIndex+1]; - double lonValue1 = known00 + ((known01 - known00) * lonFraction); - double lonValue2 = known10 + ((known11 - known10) * lonFraction); - double latValue = lonValue1 + ((lonValue2 - lonValue1) * latFraction); - - return latValue; - } else { - qCWarning(TerrainTileLog) << "elevation: Internal error - invalid tile"; + if (!_isValid || !_data) { + qCWarning(TerrainTileLog) << this << "Request for elevation, but tile is invalid."; return qQNaN(); } -} -QGeoCoordinate TerrainTile::centerCoordinate(void) const -{ - return _southWest.atDistanceAndAzimuth(_southWest.distanceTo(_northEast) / 2.0, _southWest.azimuthTo(_northEast)); + const double latDeltaSw = coordinate.latitude() - _tileInfo.swLat; + const double lonDeltaSw = coordinate.longitude() - _tileInfo.swLon; + + const bool latOutside = latDeltaSw < 0.0 || latDeltaSw > (_tileInfo.neLat - _tileInfo.swLat); + const bool lonOutside = lonDeltaSw < 0.0 || lonDeltaSw > (_tileInfo.neLon - _tileInfo.swLon); + + if (latOutside || lonOutside) { + qCWarning(TerrainTileLog) << this << "Internal error: coordinate outside tile bounds:" << coordinate; + return qQNaN(); + } + + const int16_t latIndex = qFloor(latDeltaSw / _cellSizeLat); + const int16_t lonIndex = qFloor(lonDeltaSw / _cellSizeLon); + + const bool latIndexInvalid = latIndex < 0 || latIndex > (_tileInfo.gridSizeLat - 1); + const bool lonIndexInvalid = lonIndex < 0 || lonIndex > (_tileInfo.gridSizeLon - 1); + + if (latIndexInvalid || lonIndexInvalid) { + qCWarning(TerrainTileLog) << this << "Internal error: invalid array indices" << latIndex << lonIndex; + return qQNaN(); + } + + const auto elevation = _data[latIndex][lonIndex]; + + // Print warning if elevation is outside min/max of tile meta data + if (elevation < _tileInfo.minElevation) { + qCWarning(TerrainTileLog) << this << "Warning: elevation read is below min elevation in tile:" << elevation << "<" << _tileInfo.minElevation; + } + else if (elevation > _tileInfo.maxElevation) { + qCWarning(TerrainTileLog) << this << "Warning: elevation read is above max elevation in tile:" << elevation << ">" << _tileInfo.maxElevation; + } + +#ifdef QT_DEBUG + qCDebug(TerrainTileLog) << this << "latIndex, lonIndex:" << latIndex << lonIndex << "elevation:" << elevation; +#endif + + return static_cast(elevation); } -QByteArray TerrainTile::serializeFromAirMapJson(QByteArray input) +QByteArray TerrainTile::serializeFromAirMapJson(const QByteArray& input) { QJsonParseError parseError; QJsonDocument document = QJsonDocument::fromJson(input, &parseError); if (parseError.error != QJsonParseError::NoError) { - QByteArray emptyArray; - return emptyArray; + qCWarning(TerrainTileLog) << "TerrainTile::serializeFromAirMapJson: Terrain tile json doc parse error" << parseError.errorString(); + return QByteArray(); } if (!document.isObject()) { - qCDebug(TerrainTileLog) << "Terrain tile json doc is no object"; - QByteArray emptyArray; - return emptyArray; + qCWarning(TerrainTileLog) << "TerrainTile::serializeFromAirMapJson: Terrain tile json doc is no object"; + return QByteArray(); } QJsonObject rootObject = document.object(); @@ -174,15 +162,13 @@ QByteArray TerrainTile::serializeFromAirMapJson(QByteArray input) { _jsonDataKey, QJsonValue::Object, true }, }; if (!JsonHelper::validateKeys(rootObject, rootVersionKeyInfoList, errorString)) { - qCDebug(TerrainTileLog) << "Error in reading json: " << errorString; - QByteArray emptyArray; - return emptyArray; + qCWarning(TerrainTileLog) << "TerrainTile::serializeFromAirMapJson: Error in reading json: " << errorString; + return QByteArray(); } if (rootObject[_jsonStatusKey].toString() != "success") { - qCDebug(TerrainTileLog) << "Invalid terrain tile."; - QByteArray emptyArray; - return emptyArray; + qCWarning(TerrainTileLog) << "TerrainTile::serializeFromAirMapJson: Invalid terrain tile."; + return QByteArray(); } const QJsonObject& dataObject = rootObject[_jsonDataKey].toObject(); QList dataVersionKeyInfoList = { @@ -191,9 +177,8 @@ QByteArray TerrainTile::serializeFromAirMapJson(QByteArray input) { _jsonCarpetKey, QJsonValue::Array, true }, }; if (!JsonHelper::validateKeys(dataObject, dataVersionKeyInfoList, errorString)) { - qCDebug(TerrainTileLog) << "Error in reading json: " << errorString; - QByteArray emptyArray; - return emptyArray; + qCWarning(TerrainTileLog) << "TerrainTile::serializeFromAirMapJson: Error in reading json: " << errorString; + return QByteArray(); } // Bounds @@ -203,16 +188,14 @@ QByteArray TerrainTile::serializeFromAirMapJson(QByteArray input) { _jsonNorthEastKey, QJsonValue::Array, true }, }; if (!JsonHelper::validateKeys(boundsObject, boundsVersionKeyInfoList, errorString)) { - qCDebug(TerrainTileLog) << "Error in reading json: " << errorString; - QByteArray emptyArray; - return emptyArray; + qCWarning(TerrainTileLog) << "TerrainTile::serializeFromAirMapJson: Error in reading json: " << errorString; + return QByteArray(); } const QJsonArray& swArray = boundsObject[_jsonSouthWestKey].toArray(); const QJsonArray& neArray = boundsObject[_jsonNorthEastKey].toArray(); if (swArray.count() < 2 || neArray.count() < 2 ) { - qCDebug(TerrainTileLog) << "Incomplete bounding location"; - QByteArray emptyArray; - return emptyArray; + qCWarning(TerrainTileLog) << "TerrainTile::serializeFromAirMapJson: Incomplete bounding location"; + return QByteArray(); } // Stats @@ -223,60 +206,46 @@ QByteArray TerrainTile::serializeFromAirMapJson(QByteArray input) { _jsonAvgElevationKey, QJsonValue::Double, true }, }; if (!JsonHelper::validateKeys(statsObject, statsVersionKeyInfoList, errorString)) { - qCDebug(TerrainTileLog) << "Error in reading json: " << errorString; - QByteArray emptyArray; - return emptyArray; + qCWarning(TerrainTileLog) << "TerrainTile::serializeFromAirMapJson: Error in reading json: " << errorString; + return QByteArray(); } - // Carpet const QJsonArray& carpetArray = dataObject[_jsonCarpetKey].toArray(); - int gridSizeLat = carpetArray.count(); - int gridSizeLon = carpetArray[0].toArray().count(); - - TileInfo_t tileInfo; + // Tile meta data + TerrainTile::TileInfo_t tileInfo; tileInfo.swLat = swArray[0].toDouble(); tileInfo.swLon = swArray[1].toDouble(); tileInfo.neLat = neArray[0].toDouble(); tileInfo.neLon = neArray[1].toDouble(); - tileInfo.minElevation = static_cast(statsObject[_jsonMinElevationKey].toInt()); - tileInfo.maxElevation = static_cast(statsObject[_jsonMaxElevationKey].toInt()); + tileInfo.minElevation = static_cast(statsObject[_jsonMinElevationKey].toDouble()); + tileInfo.maxElevation = static_cast(statsObject[_jsonMaxElevationKey].toDouble()); tileInfo.avgElevation = statsObject[_jsonAvgElevationKey].toDouble(); - tileInfo.gridSizeLat = static_cast(gridSizeLat); - tileInfo.gridSizeLon = static_cast(gridSizeLon); - - // We require 1-arc second value spacing - double neCornerLatExpected = tileInfo.swLat + ((tileInfo.gridSizeLat - 1) * tileValueSpacingDegrees); - double neCornerLonExpected = tileInfo.swLon + ((tileInfo.gridSizeLon - 1) * tileValueSpacingDegrees); - if (!QGC::fuzzyCompare(tileInfo.neLat, neCornerLatExpected) || !QGC::fuzzyCompare(tileInfo.neLon, neCornerLonExpected)) { - qCWarning(TerrainTileLog) << QStringLiteral("serialize: Internal error - distance between values incorrect neExpected(%1, %2) neActual(%3, %4) sw(%5, %6) gridSize(%7, %8)") - .arg(neCornerLatExpected).arg(neCornerLonExpected).arg(tileInfo.neLat).arg(tileInfo.neLon).arg(tileInfo.swLat).arg(tileInfo.swLon).arg(tileInfo.gridSizeLat).arg(tileInfo.gridSizeLon); - QByteArray emptyArray; - return emptyArray; - } + tileInfo.gridSizeLat = static_cast(carpetArray.count()); + tileInfo.gridSizeLon = static_cast(carpetArray[0].toArray().count()); - int cTileHeaderBytes = static_cast(sizeof(TileInfo_t)); - int cTileDataBytes = static_cast(sizeof(int16_t)) * gridSizeLat * gridSizeLon; + const auto cTileNumHeaderBytes = static_cast(sizeof(TileInfo_t)); + const auto cTileNumDataBytes = static_cast(sizeof(int16_t)) * tileInfo.gridSizeLat * tileInfo.gridSizeLon; - QByteArray byteArray(cTileHeaderBytes + cTileDataBytes, 0); + QByteArray res; + res.resize(cTileNumHeaderBytes + cTileNumDataBytes); - TileInfo_t* pTileInfo = reinterpret_cast(byteArray.data()); - int16_t* pTileData = reinterpret_cast(&reinterpret_cast(byteArray.data())[cTileHeaderBytes]); + TileInfo_t* pTileInfo = reinterpret_cast(res.data()); + int16_t* pTileData = reinterpret_cast(&reinterpret_cast(res.data())[cTileNumHeaderBytes]); *pTileInfo = tileInfo; int valueIndex = 0; - for (int i = 0; i < gridSizeLat; i++) { + for (unsigned i = 0; i < static_cast(tileInfo.gridSizeLat); i++) { const QJsonArray& row = carpetArray[i].toArray(); - if (row.count() < gridSizeLon) { - qCDebug(TerrainTileLog) << "Expected row array of " << gridSizeLon << ", instead got " << row.count(); - QByteArray emptyArray; - return emptyArray; + if (row.count() < tileInfo.gridSizeLon) { + qCDebug(TerrainTileLog) << "Expected row array of " << tileInfo.gridSizeLon << ", instead got " << row.count(); + return QByteArray(); } - for (int j = 0; j < gridSizeLon; j++) { + for (unsigned j = 0; j < static_cast(tileInfo.gridSizeLon); j++) { pTileData[valueIndex++] = static_cast(row[j].toDouble()); } } - return byteArray; + return res; } diff --git a/src/TerrainTile.h b/src/TerrainTile.h index bd1417df621c..4261ac5a9c18 100644 --- a/src/TerrainTile.h +++ b/src/TerrainTile.h @@ -16,7 +16,7 @@ Q_DECLARE_LOGGING_CATEGORY(TerrainTileLog) class TerrainTile { public: - TerrainTile(); + TerrainTile() = default; ~TerrainTile(); /** @@ -24,7 +24,7 @@ class TerrainTile * * @param document */ - TerrainTile(QByteArray byteArray); + TerrainTile(const QByteArray& byteArray); /** * Check whether valid data is loaded @@ -46,30 +46,29 @@ class TerrainTile * * @return minimum elevation */ - double minElevation(void) const { return _minElevation; } + double minElevation(void) const { return _isValid ? static_cast(_tileInfo.minElevation) : qQNaN(); } /** * Accessor for the maximum elevation of the tile * * @return maximum elevation */ - double maxElevation(void) const { return _maxElevation; } + double maxElevation(void) const { return _isValid ? static_cast(_tileInfo.maxElevation) : qQNaN(); } /** * Accessor for the average elevation of the tile * * @return average elevation */ - double avgElevation(void) const { return _avgElevation; } + double avgElevation(void) const { return _isValid ? _tileInfo.avgElevation : qQNaN(); } /** * Accessor for the center coordinate * * @return center coordinate */ - QGeoCoordinate centerCoordinate(void) const; - static QByteArray serializeFromAirMapJson(QByteArray input); + static QByteArray serializeFromAirMapJson(const QByteArray& input); static constexpr double tileSizeDegrees = 0.01; ///< Each terrain tile represents a square area .01 degrees in lat/lon static constexpr double tileValueSpacingDegrees = 1.0 / 3600; ///< 1 Arc-Second spacing of elevation values @@ -85,16 +84,10 @@ class TerrainTile int16_t gridSizeLon; } TileInfo_t; - QGeoCoordinate _southWest; /// South west corner of the tile - QGeoCoordinate _northEast; /// North east corner of the tile - - int16_t _minElevation; /// Minimum elevation in tile - int16_t _maxElevation; /// Maximum elevation in tile - double _avgElevation; /// Average elevation of the tile - + TileInfo_t _tileInfo; int16_t** _data; /// 2D elevation data array - int16_t _gridSizeLat; /// data grid size in latitude direction - int16_t _gridSizeLon; /// data grid size in longitude direction + double _cellSizeLat; /// data grid size in latitude direction + double _cellSizeLon; /// data grid size in longitude direction bool _isValid; /// data loaded is valid // Json keys