QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
TerrainTileManager.cc
Go to the documentation of this file.
2#include "TerrainTile.h"
5#include "QGeoMapReplyQGC.h"
6#include "QGCMapUrlEngine.h"
8#include "SettingsManager.h"
9#include "FlightMapSettings.h"
10#include "QGCLoggingCategory.h"
11#include "QGCGeo.h"
12
13#include <QtCore/QDateTime>
14#include <QtLocation/private/qgeotilespec_p.h>
15#include <QtNetwork/QNetworkAccessManager>
16#include <QtNetwork/QNetworkRequest>
17
18#include <limits>
19
20#include "QGCNetworkHelper.h"
21
22QGC_LOGGING_CATEGORY(TerrainTileManagerLog, "Terrain.TerrainTileManager")
23
24namespace {
25 constexpr int kMaxCarpetGridSize = 10000;
26}
27
28Q_GLOBAL_STATIC(TerrainTileManager, _terrainTileManager)
29
31{
32 return _terrainTileManager();
33}
34
36 : QObject(parent)
37 , _networkManager(new QNetworkAccessManager(this))
38{
39 qCDebug(TerrainTileManagerLog) << this;
40
41 QGCNetworkHelper::configureProxy(_networkManager);
42}
43
45{
46 qDeleteAll(_tiles);
47
48 qCDebug(TerrainTileManagerLog) << this;
49}
50
51bool TerrainTileManager::getAltitudesForCoordinates(const QList<QGeoCoordinate> &coordinates, QList<double> &altitudes, bool &error)
52{
53 error = false;
54
55 const QString elevationProviderName = SettingsManager::instance()->flightMapSettings()->elevationMapProvider()->rawValue().toString();
56 const SharedMapProvider provider = UrlFactory::getMapProviderFromProviderType(elevationProviderName);
57 for (const QGeoCoordinate &coordinate: coordinates) {
58 const QString tileHash = UrlFactory::getTileHash(
59 provider->getMapName(),
60 provider->long2tileX(coordinate.longitude(), 1),
61 provider->lat2tileY(coordinate.latitude(), 1),
62 1
63 );
64 qCDebug(TerrainTileManagerLog) << "hash:coordinate" << tileHash << coordinate;
65
66 TerrainTile* const tile = _getCachedTile(tileHash);
67 if (tile) {
68 const double elevation = tile->elevation(coordinate);
69 if (qIsNaN(elevation)) {
70 error = true;
71 qCWarning(TerrainTileManagerLog) << "Internal Error: missing elevation in tile cache";
72 } else {
73 qCDebug(TerrainTileManagerLog) << "returning elevation from tile cache" << elevation;
74 }
75 altitudes.push_back(elevation);
76 } else if (_isFailedTile(tileHash)) {
77 // Tile fetch failed recently; short-circuit to avoid hammering the server with repeated requests
78 // (e.g. uninitialized 0,0 coordinates from MAVLink TERRAIN_REQUEST returning HTTP 500).
79 error = true;
80 altitudes.push_back(qQNaN());
81 } else if (_state != TerrainQuery::State::Downloading) {
82 QGeoTileSpec spec;
83 spec.setX(provider->long2tileX(coordinate.longitude(), 1));
84 spec.setY(provider->lat2tileY(coordinate.latitude(), 1));
85 spec.setZoom(1);
86 spec.setMapId(provider->getMapId());
87 const QNetworkRequest request = QGeoTileFetcherQGC::getNetworkRequest(spec.mapId(), spec.x(), spec.y(), spec.zoom());
88 QGeoTiledMapReplyQGC *reply = new QGeoTiledMapReplyQGC(_networkManager, request, spec, this);
89 (void) connect(reply, &QGeoTiledMapReplyQGC::finished, this, &TerrainTileManager::_terrainDone);
90 if (reply->init()) {
92 } else {
93 reply->deleteLater();
94 }
95 return false;
96 } else {
97 return false;
98 }
99 }
100
101 return true;
102}
103
104void TerrainTileManager::addCoordinateQuery(TerrainQueryInterface *terrainQueryInterface, const QList<QGeoCoordinate> &coordinates)
105{
106 qCDebug(TerrainTileManagerLog) << "count" << coordinates.count();
107
108 if (coordinates.isEmpty()) {
109 return;
110 }
111
112 bool error;
113 QList<double> altitudes;
114 if (!getAltitudesForCoordinates(coordinates, altitudes, error)) {
115 qCDebug(TerrainTileManagerLog) << "queue count" << _requestQueue.count();
116 const QueuedRequestInfo_t queuedRequestInfo = {
117 terrainQueryInterface,
119 0,
120 0,
121 coordinates,
122 false,
123 0,
124 0
125 };
126 _requestQueue.enqueue(queuedRequestInfo);
127 return;
128 }
129
130 if (error) {
131 QList<double> noAltitudes;
132 qCWarning(TerrainTileManagerLog) << "signalling failure due to internal error";
133 terrainQueryInterface->signalCoordinateHeights(false, noAltitudes);
134 return;
135 }
136
137 qCDebug(TerrainTileManagerLog) << "all altitudes taken from cached data";
138 terrainQueryInterface->signalCoordinateHeights((coordinates.count() == altitudes.count()), altitudes);
139}
140
141void TerrainTileManager::addPathQuery(TerrainQueryInterface *terrainQueryInterface, const QGeoCoordinate &startPoint, const QGeoCoordinate &endPoint)
142{
143 double distanceBetween;
144 double finalDistanceBetween;
145 const QList<QGeoCoordinate> coordinates = _pathQueryToCoords(startPoint, endPoint, distanceBetween, finalDistanceBetween);
146
147 bool error;
148 QList<double> altitudes;
149 if (!getAltitudesForCoordinates(coordinates, altitudes, error)) {
150 qCDebug(TerrainTileManagerLog) << "queue count" << _requestQueue.count();
151 const QueuedRequestInfo_t queuedRequestInfo = {
152 terrainQueryInterface,
154 distanceBetween,
155 finalDistanceBetween,
156 coordinates,
157 false,
158 0,
159 0
160 };
161 _requestQueue.enqueue(queuedRequestInfo);
162 return;
163 }
164
165 if (error) {
166 QList<double> noAltitudes;
167 qCWarning(TerrainTileManagerLog) << "signalling failure due to internal error";
168 terrainQueryInterface->signalPathHeights(false, distanceBetween, finalDistanceBetween, noAltitudes);
169 return;
170 }
171
172 qCDebug(TerrainTileManagerLog) << "all altitudes taken from cached data";
173 terrainQueryInterface->signalPathHeights((coordinates.count() == altitudes.count()), distanceBetween, finalDistanceBetween, altitudes);
174}
175
176void TerrainTileManager::addCarpetQuery(TerrainQueryInterface *terrainQueryInterface, const QGeoCoordinate &swCoord, const QGeoCoordinate &neCoord, bool statsOnly)
177{
178 if (swCoord.longitude() > neCoord.longitude() || swCoord.latitude() > neCoord.latitude()) {
179 qCWarning(TerrainTileManagerLog) << "Invalid carpet bounds: SW must be south-west of NE";
180 terrainQueryInterface->signalCarpetHeights(false, qQNaN(), qQNaN(), QList<QList<double>>());
181 return;
182 }
183
184 const int gridSizeLat = qCeil((neCoord.latitude() - swCoord.latitude()) / TerrainTileCopernicus::kTileValueSpacingDegrees);
185 const int gridSizeLon = qCeil((neCoord.longitude() - swCoord.longitude()) / TerrainTileCopernicus::kTileValueSpacingDegrees);
186
187 if (gridSizeLat <= 0 || gridSizeLon <= 0) {
188 qCWarning(TerrainTileManagerLog) << "Carpet area too small";
189 terrainQueryInterface->signalCarpetHeights(false, qQNaN(), qQNaN(), QList<QList<double>>());
190 return;
191 }
192
193 if (gridSizeLat > kMaxCarpetGridSize || gridSizeLon > kMaxCarpetGridSize) {
194 qCWarning(TerrainTileManagerLog) << "Carpet area too large"
195 << "gridSizeLat:" << gridSizeLat
196 << "gridSizeLon:" << gridSizeLon
197 << "maxGridSize:" << kMaxCarpetGridSize;
198 terrainQueryInterface->signalCarpetHeights(false, qQNaN(), qQNaN(), QList<QList<double>>());
199 return;
200 }
201
202 QList<QGeoCoordinate> coordinates;
203 for (int latIdx = 0; latIdx <= gridSizeLat; latIdx++) {
204 const double lat = swCoord.latitude() + (latIdx * TerrainTileCopernicus::kTileValueSpacingDegrees);
205 for (int lonIdx = 0; lonIdx <= gridSizeLon; lonIdx++) {
206 const double lon = swCoord.longitude() + (lonIdx * TerrainTileCopernicus::kTileValueSpacingDegrees);
207 (void) coordinates.append(QGeoCoordinate(lat, lon));
208 }
209 }
210
211 bool error;
212 QList<double> altitudes;
213 if (!getAltitudesForCoordinates(coordinates, altitudes, error)) {
214 qCDebug(TerrainTileManagerLog) << "carpet query queued, count" << _requestQueue.count();
215 const QueuedRequestInfo_t queuedRequestInfo = {
216 terrainQueryInterface,
218 0,
219 0,
220 coordinates,
221 statsOnly,
222 gridSizeLat + 1,
223 gridSizeLon + 1
224 };
225 _requestQueue.enqueue(queuedRequestInfo);
226 return;
227 }
228
229 if (error) {
230 qCWarning(TerrainTileManagerLog) << "signalling carpet failure due to internal error";
231 terrainQueryInterface->signalCarpetHeights(false, qQNaN(), qQNaN(), QList<QList<double>>());
232 return;
233 }
234
235 double minHeight, maxHeight;
236 QList<QList<double>> carpet;
237 _processCarpetResults(altitudes, gridSizeLat + 1, gridSizeLon + 1, statsOnly, minHeight, maxHeight, carpet);
238
239 qCDebug(TerrainTileManagerLog) << "carpet altitudes from cached data, min:" << minHeight << "max:" << maxHeight;
240 terrainQueryInterface->signalCarpetHeights(true, minHeight, maxHeight, carpet);
241}
242
243QList<QGeoCoordinate> TerrainTileManager::_pathQueryToCoords(const QGeoCoordinate &fromCoord, const QGeoCoordinate &toCoord, double &distanceBetween, double &finalDistanceBetween)
244{
245 const double totalDistance = QGCGeo::geodesicDistance(fromCoord, toCoord);
246 // TODO: get spacing from terrainQueryInterface
247 const int numPoints = qMax(2, qCeil(totalDistance / TerrainTileCopernicus::kTileValueSpacingMeters) + 1);
248
249 QList<QGeoCoordinate> coordinates = QGCGeo::interpolatePath(fromCoord, toCoord, numPoints);
250
251 if (coordinates.size() >= 2) {
252 distanceBetween = QGCGeo::geodesicDistance(coordinates[0], coordinates[1]);
253 finalDistanceBetween = QGCGeo::geodesicDistance(coordinates[coordinates.size() - 2], coordinates.last());
254 } else {
255 distanceBetween = finalDistanceBetween = totalDistance;
256 }
257
258 qCDebug(TerrainTileManagerLog) << "fromCoord:toCoord:distanceBetween:finalDistanceBetween:coordCount"
259 << fromCoord << toCoord << distanceBetween << finalDistanceBetween << coordinates.count();
260
261 return coordinates;
262}
263
264void TerrainTileManager::_tileFailed()
265{
266 QList<double> noAltitudes;
267
268 for (const QueuedRequestInfo_t &requestInfo: _requestQueue) {
269 if (requestInfo.terrainQueryInterface.isNull()) {
270 continue;
271 }
272 switch (requestInfo.queryMode) {
274 requestInfo.terrainQueryInterface->signalCoordinateHeights(false, noAltitudes);
275 break;
277 requestInfo.terrainQueryInterface->signalPathHeights(false, requestInfo.distanceBetween, requestInfo.finalDistanceBetween, noAltitudes);
278 break;
280 requestInfo.terrainQueryInterface->signalCarpetHeights(false, qQNaN(), qQNaN(), QList<QList<double>>());
281 break;
282 default:
283 continue;
284 }
285 }
286
287 _requestQueue.clear();
288}
289
290void TerrainTileManager::_terrainDone()
291{
293
294 QGeoTiledMapReplyQGC* const reply = qobject_cast<QGeoTiledMapReplyQGC*>(QObject::sender());
295 if (!reply) {
296 qCWarning(TerrainTileManagerLog) << "Elevation tile fetched but invalid reply data type.";
297 return;
298 }
299 reply->deleteLater();
300
301 const QByteArray responseBytes = reply->mapImageData();
302 const QGeoTileSpec spec = reply->tileSpec();
303
304 const QString hash = UrlFactory::getTileHash(UrlFactory::getProviderTypeFromQtMapId(spec.mapId()), spec.x(), spec.y(), spec.zoom());
305
306 if (reply->error() != QGeoTiledMapReplyQGC::NoError) {
307 const bool firstFailure = _recordFailedTile(hash);
308 if (firstFailure) {
309 qCWarning(TerrainTileManagerLog) << "Elevation tile fetching returned error:" << reply->errorString();
310 } else {
311 qCDebug(TerrainTileManagerLog) << "Elevation tile fetching returned error (suppressed):" << reply->errorString();
312 }
313 _tileFailed();
314 return;
315 }
316
317 if (responseBytes.isEmpty()) {
318 const bool firstFailure = _recordFailedTile(hash);
319 if (firstFailure) {
320 qCWarning(TerrainTileManagerLog) << "Error in fetching elevation tile. Empty response.";
321 } else {
322 qCDebug(TerrainTileManagerLog) << "Error in fetching elevation tile. Empty response (suppressed).";
323 }
324 _tileFailed();
325 return;
326 }
327
328 _clearFailedTile(hash);
329
330 qCDebug(TerrainTileManagerLog) << "Received some bytes of terrain data:" << responseBytes.size();
331
332 _cacheTile(responseBytes, hash);
333
334 for (qsizetype i = _requestQueue.count() - 1; i >= 0; i--) {
335 bool error;
336 QList<double> altitudes;
337 QueuedRequestInfo_t &requestInfo = _requestQueue[i];
338
339 if (requestInfo.terrainQueryInterface.isNull()) {
340 _requestQueue.removeAt(i);
341 continue;
342 }
343
344 if (!getAltitudesForCoordinates(requestInfo.coordinates, altitudes, error)) {
345 continue;
346 }
347
348 switch (requestInfo.queryMode) {
350 if (error) {
351 qCWarning(TerrainTileManagerLog) << "signalling failure due to internal error";
352 QList<double> noAltitudes;
353 requestInfo.terrainQueryInterface->signalCoordinateHeights(false, noAltitudes);
354 } else {
355 qCDebug(TerrainTileManagerLog) << "All altitudes taken from cached data";
356 requestInfo.terrainQueryInterface->signalCoordinateHeights(requestInfo.coordinates.count() == altitudes.count(), altitudes);
357 }
358 break;
360 if (error) {
361 qCWarning(TerrainTileManagerLog) << "signalling failure due to internal error";
362 QList<double> noAltitudes;
363 requestInfo.terrainQueryInterface->signalPathHeights(false, requestInfo.distanceBetween, requestInfo.finalDistanceBetween, noAltitudes);
364 } else {
365 qCDebug(TerrainTileManagerLog) << "All altitudes taken from cached data";
366 requestInfo.terrainQueryInterface->signalPathHeights(requestInfo.coordinates.count() == altitudes.count(), requestInfo.distanceBetween, requestInfo.finalDistanceBetween, altitudes);
367 }
368 break;
370 if (error) {
371 qCWarning(TerrainTileManagerLog) << "signalling carpet failure due to internal error";
372 requestInfo.terrainQueryInterface->signalCarpetHeights(false, qQNaN(), qQNaN(), QList<QList<double>>());
373 } else {
374 double minHeight, maxHeight;
375 QList<QList<double>> carpet;
376 _processCarpetResults(altitudes, requestInfo.carpetGridSizeLat, requestInfo.carpetGridSizeLon,
377 requestInfo.carpetStatsOnly, minHeight, maxHeight, carpet);
378
379 qCDebug(TerrainTileManagerLog) << "carpet altitudes from cached data, min:" << minHeight << "max:" << maxHeight;
380 requestInfo.terrainQueryInterface->signalCarpetHeights(true, minHeight, maxHeight, carpet);
381 }
382 break;
383 default:
384 break;
385 }
386
387 _requestQueue.removeAt(i);
388 }
389}
390
391void TerrainTileManager::_cacheTile(const QByteArray &data, const QString &hash)
392{
393 TerrainTile* const terrainTile = new TerrainTile(data);
394 if (!terrainTile->isValid()) {
395 delete terrainTile;
396 qCWarning(TerrainTileManagerLog) << "Received invalid tile";
397 return;
398 }
399
400 QMutexLocker locker(&_tilesMutex);
401 if (!_tiles.contains(hash)) {
402 (void) _tiles.insert(hash, terrainTile);
403 } else {
404 delete terrainTile;
405 }
406}
407
408TerrainTile *TerrainTileManager::_getCachedTile(const QString &hash)
409{
410 QMutexLocker locker(&_tilesMutex);
411
412 if (!_tiles.contains(hash)) {
413 return nullptr;
414 }
415
416 TerrainTile* const tile = _tiles[hash];
417 if (!tile->isValid()) {
418 return nullptr;
419 }
420
421 return tile;
422}
423
424bool TerrainTileManager::_isFailedTile(const QString &hash)
425{
426 QMutexLocker locker(&_tilesMutex);
427
428 const auto it = _failedTiles.constFind(hash);
429 if (it == _failedTiles.constEnd()) {
430 return false;
431 }
432 if (QDateTime::currentMSecsSinceEpoch() - it.value() < kFailedTileBackoffMs) {
433 return true;
434 }
435 _failedTiles.erase(it);
436 return false;
437}
438
439bool TerrainTileManager::_recordFailedTile(const QString &hash)
440{
441 QMutexLocker locker(&_tilesMutex);
442
443 const qint64 now = QDateTime::currentMSecsSinceEpoch();
444
445 // Opportunistic, throttled sweep: failed-tile entries are useless once their backoff
446 // window expires, so prune stale ones while we already hold the lock. Throttling to at
447 // most once per backoff window keeps this O(n) sweep from running on every failure during
448 // a burst of distinct failures.
449 if (now - _lastFailedTileSweepMs >= kFailedTileBackoffMs) {
450 _lastFailedTileSweepMs = now;
451 for (auto it = _failedTiles.begin(); it != _failedTiles.end();) {
452 if (now - it.value() >= kFailedTileBackoffMs) {
453 it = _failedTiles.erase(it);
454 } else {
455 ++it;
456 }
457 }
458 }
459
460 const bool firstFailure = !_failedTiles.contains(hash);
461 _failedTiles.insert(hash, now);
462 return firstFailure;
463}
464
465void TerrainTileManager::_clearFailedTile(const QString &hash)
466{
467 QMutexLocker locker(&_tilesMutex);
468
469 _failedTiles.remove(hash);
470}
471
472void TerrainTileManager::_processCarpetResults(const QList<double> &altitudes, int gridSizeLat, int gridSizeLon,
473 bool statsOnly, double &minHeight, double &maxHeight, QList<QList<double>> &carpet)
474{
475 minHeight = std::numeric_limits<double>::max();
476 maxHeight = std::numeric_limits<double>::lowest();
477
478 int idx = 0;
479 for (int latIdx = 0; latIdx < gridSizeLat; latIdx++) {
480 QList<double> row;
481 for (int lonIdx = 0; lonIdx < gridSizeLon; lonIdx++) {
482 const double height = altitudes[idx++];
483 minHeight = qMin(minHeight, height);
484 maxHeight = qMax(maxHeight, height);
485 if (!statsOnly) {
486 (void) row.append(height);
487 }
488 }
489 if (!statsOnly) {
490 (void) carpet.append(row);
491 }
492 }
493}
Q_GLOBAL_STATIC(FirmwarePluginFactoryRegister, _firmwarePluginFactoryRegisterInstance)
Error error
Geographic coordinate conversion utilities using GeographicLib.
#define QGC_LOGGING_CATEGORY(name, categoryStr)
std::shared_ptr< const MapProvider > SharedMapProvider
static QNetworkRequest getNetworkRequest(int mapId, int x, int y, int zoom)
FlightMapSettings * flightMapSettings() const
static SettingsManager * instance()
Base class for offline/online terrain queries.
void signalPathHeights(bool success, double distanceBetween, double finalDistanceBetween, const QList< double > &heights)
void signalCoordinateHeights(bool success, const QList< double > &heights)
void signalCarpetHeights(bool success, double minHeight, double maxHeight, const QList< QList< double > > &carpet)
static constexpr double kTileValueSpacingDegrees
1 Arc-Second spacing of elevation values
static constexpr double kTileValueSpacingMeters
bool getAltitudesForCoordinates(const QList< QGeoCoordinate > &coordinates, QList< double > &altitudes, bool &error)
TerrainTileManager(QObject *parent=nullptr)
void addPathQuery(TerrainQueryInterface *terrainQueryInterface, const QGeoCoordinate &startPoint, const QGeoCoordinate &endPoint)
void addCarpetQuery(TerrainQueryInterface *terrainQueryInterface, const QGeoCoordinate &swCoord, const QGeoCoordinate &neCoord, bool statsOnly)
void addCoordinateQuery(TerrainQueryInterface *terrainQueryInterface, const QList< QGeoCoordinate > &coordinates)
double elevation(const QGeoCoordinate &coordinate) const
bool isValid() const
Definition TerrainTile.h:19
static QString getTileHash(QStringView type, int x, int y, int z)
static QString getProviderTypeFromQtMapId(int qtMapId)
static std::shared_ptr< const MapProvider > getMapProviderFromProviderType(QStringView type)
QList< QGeoCoordinate > interpolatePath(const QGeoCoordinate &from, const QGeoCoordinate &to, int numPoints)
Definition QGCGeo.cc:308
double geodesicDistance(const QGeoCoordinate &from, const QGeoCoordinate &to)
Definition QGCGeo.cc:229
void configureProxy(QNetworkAccessManager *manager)
Set up default proxy configuration on a network manager.