QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
TerrariumTileFetcher.cc
Go to the documentation of this file.
1/****************************************************************************
2 *
3 * (c) 2009-2024 QGROUNDCONTROL PROJECT <http://www.qgroundcontrol.org>
4 *
5 * QGroundControl is licensed according to the terms in the file
6 * COPYING.md in the root of the source code directory.
7 *
8 ****************************************************************************/
9
11
12#include <memory>
13#include <utility>
14
15#include <QtCore/QVarLengthArray>
16#include <QtGui/QImage>
17#include <QtNetwork/QNetworkAccessManager>
18#include <QtNetwork/QNetworkReply>
19
20#include "BilinearUV.h"
22#include "HeightField.h"
23#include "PatchGeometry.h"
24#include "QGCCacheTile.h"
25#include "QGCLoggingCategory.h"
26#include "QGCMapEngine.h"
27#include "QGCMapTasks.h"
28#include "QGCMapUrlEngine.h"
30#include "QGeoTileFetcherQGC.h"
31
32QGC_LOGGING_CATEGORY(GeoMapTerrariumTileFetcherLog, "GeoMap.TerrariumTileFetcher")
33QGC_LOGGING_CATEGORY(GeoMapTerrariumTileFetcherVerboseLog, "GeoMap.TerrariumTileFetcher.Verbose")
34
35static_assert(TerrariumTileFetcher::kMaxGridSize == PatchGeometry::kMaxGridSize,
36 "fetcher grid cap must match the patch mesh cap");
37
38namespace {
39
40QString terrariumProviderType()
41{
42 return QString::fromLatin1(TerrariumElevationProvider::kProviderKey);
43}
44
46double heightAtPixel(const QImage& image, int px, int py)
47{
48 const QRgb rgb = image.pixel(px, py);
49 return (qRed(rgb) * 256.0) + qGreen(rgb) + (qBlue(rgb) / 256.0) - 32768.0;
50}
51
54double heightAtUV(const QImage& image, double u, double v)
55{
56 const auto at = [&image](int x, int y) { return heightAtPixel(image, x, y); };
57 return bilinearAtUV(image.width(), image.height(), u, v, at);
58}
59
62QList<float> sampleGrid(const QImage& image, const QRectF& subWindow, int gridSize)
63{
64 QList<float> heights;
65 heights.reserve(qsizetype(gridSize + 1) * (gridSize + 1));
66 for (int row = 0; row <= gridSize; row++) {
67 const double v = subWindow.y() + (subWindow.height() * row / gridSize);
68 for (int col = 0; col <= gridSize; col++) {
69 const double u = subWindow.x() + (subWindow.width() * col / gridSize);
70 heights.append(static_cast<float>(heightAtUV(image, u, v)));
71 }
72 }
73 return heights;
74}
75
78constexpr int kTileSizePixels = 256;
79
81QImage decodeTile(const QByteArray& data)
82{
83 QImage image;
84 if (!image.loadFromData(data) || image.width() != kTileSizePixels || image.height() != kTileSizePixels) {
85 return QImage();
86 }
87 return image.convertToFormat(QImage::Format_RGB32);
88}
89
91ElevationTilePyramid::Grid gridFromImage(const QImage& image)
92{
94 grid.width = image.width();
95 grid.height = image.height();
96 grid.heights.reserve(qsizetype(grid.width) * grid.height);
97 for (int py = 0; py < grid.height; py++) {
98 for (int px = 0; px < grid.width; px++) {
99 grid.heights.append(static_cast<float>(heightAtPixel(image, px, py)));
100 }
101 }
102 return grid;
103}
104
107TileMath::TileKey fetchKeyFor(const TileMath::TileKey& key)
108{
110 return key;
111 }
112 const int shift = key.zoom - TerrariumTileFetcher::kMaxTileZoom;
113 return TileMath::TileKey{key.x >> shift, key.y >> shift, TerrariumTileFetcher::kMaxTileZoom};
114}
115
119QVarLengthArray<TileMath::TileKey, 4> anchorKeysFor(const TileMath::TileKey& fetchKey)
120{
121 QVarLengthArray<TileMath::TileKey, 4> anchors;
122 if (fetchKey.zoom <= TerrariumTileFetcher::kAnchorZoom) {
123 return anchors;
124 }
125 const int shift = fetchKey.zoom - TerrariumTileFetcher::kAnchorZoom;
126 anchors.append(TileMath::TileKey{fetchKey.x >> shift, fetchKey.y >> shift, TerrariumTileFetcher::kAnchorZoom});
127 for (int dy = -1; dy <= 1; dy++) {
128 for (int dx = -1; dx <= 1; dx++) {
129 const TileMath::TileKey neighbor{fetchKey.x + dx, fetchKey.y + dy, fetchKey.zoom};
130 if (!TileMath::isValidKey(neighbor)) {
131 continue;
132 }
133 const TileMath::TileKey anchor{neighbor.x >> shift, neighbor.y >> shift, TerrariumTileFetcher::kAnchorZoom};
134 if (!anchors.contains(anchor)) {
135 anchors.append(anchor);
136 }
137 }
138 }
139 return anchors;
140}
141
142} // namespace
143
144TerrariumTileFetcher::TerrariumTileFetcher(QObject* parent, QNetworkAccessManager* networkManager)
145 : HeightSource(parent),
146 _networkManager(networkManager ? networkManager : new QNetworkAccessManager(this)),
147 _mapId(UrlFactory::getQtMapIdFromProviderType(terrariumProviderType()))
148{}
149
151{
152 const int requestId = _nextRequestId();
153
154 if ((gridSize <= 0) || (gridSize > kMaxGridSize) || !TileMath::isValidKey(key)) {
155 qCWarning(GeoMapTerrariumTileFetcherLog)
156 << "requestPatchHeights rejected: key" << key << "gridSize" << gridSize;
157 _pending.insert(requestId, PendingRequest{});
158 _failAsync(requestId);
159 return requestId;
160 }
161 if (_mapId < 0) {
162 qCDebug(GeoMapTerrariumTileFetcherLog) << "requestPatchHeights: elevation provider not registered";
163 _pending.insert(requestId, PendingRequest{});
164 _failAsync(requestId);
165 return requestId;
166 }
167
168 // Deeper than the dataset: fetch the top-zoom ancestor and sample the
169 // patch's sub-window of it
170 PendingRequest pending;
171 pending.gridSize = gridSize;
172 pending.fetchKey = fetchKeyFor(key);
173 if (key.zoom <= kMaxTileZoom) {
174 pending.subWindow = QRectF(0, 0, 1, 1);
175 } else {
176 const int shift = key.zoom - kMaxTileZoom;
177 const double scale = 1.0 / (1LL << shift);
178 pending.subWindow = QRectF((key.x - (qint64(pending.fetchKey.x) << shift)) * scale,
179 (key.y - (qint64(pending.fetchKey.y) << shift)) * scale, scale, scale);
180 }
181 _pending.insert(requestId, pending);
182 _lastFetchKey = pending.fetchKey;
183
184 const bool inFlight = _fetchInFlight(pending.fetchKey);
185 _waiters[pending.fetchKey].append(requestId);
186 qCDebug(GeoMapTerrariumTileFetcherVerboseLog)
187 << "requestPatchHeights: key" << key << "requestId" << requestId << "fetchKey" << pending.fetchKey
188 << (inFlight ? "(joining in-flight fetch)" : "(starting fetch)");
189 if (inFlight) {
190 return requestId; // a fetch for this tile is already in flight; it serves all waiters
191 }
192
193 if (!_startFetch(pending.fetchKey)) {
194 qCDebug(GeoMapTerrariumTileFetcherLog) << "requestPatchHeights: could not start fetch for" << pending.fetchKey;
195 _waiters.remove(pending.fetchKey);
196 _failAsync(requestId);
197 }
198 return requestId;
199}
200
202{
203 if (!_heightField || !TileMath::isValidKey(key)) {
204 qCWarning(GeoMapTerrariumTileFetcherLog)
205 << "requestTile rejected: key" << key << "heightFieldSet" << (_heightField != nullptr);
206 return false;
207 }
208 if (_mapId < 0) {
209 qCDebug(GeoMapTerrariumTileFetcherLog) << "requestTile: elevation provider not registered";
210 return false;
211 }
212
213 const TileMath::TileKey fetchKey = fetchKeyFor(key);
214 if (_heightField->hasTile(fetchKey) || _fieldRequests.contains(fetchKey) || _held.contains(fetchKey)) {
215 return true;
216 }
217
218 // Anchors first: their fetches queue ahead, and _insertIntoField holds this tile until they land.
219 // An anchor that fails to start stops blocking, as a failed fetch does in _failAll.
220 for (const TileMath::TileKey& anchor : anchorKeysFor(fetchKey)) {
221 (void) requestTile(anchor);
222 }
223
224 const bool inFlight = _fetchInFlight(fetchKey);
225 _fieldRequests.insert(fetchKey);
226 _lastFetchKey = fetchKey;
227 qCDebug(GeoMapTerrariumTileFetcherVerboseLog)
228 << "requestTile: fetchKey" << fetchKey << (inFlight ? "(joining in-flight fetch)" : "(starting fetch)");
229 if (inFlight) {
230 return true; // piggyback on the fetch already serving patch waiters
231 }
232
233 if (!_startFetch(fetchKey)) {
234 qCDebug(GeoMapTerrariumTileFetcherLog) << "requestTile: could not start fetch for" << fetchKey;
235 _fieldRequests.remove(fetchKey);
236 return false;
237 }
238 return true;
239}
240
241bool TerrariumTileFetcher::_fetchInFlight(const TileMath::TileKey& fetchKey) const
242{
243 return _waiters.contains(fetchKey) || _fieldRequests.contains(fetchKey);
244}
245
246bool TerrariumTileFetcher::_startFetch(const TileMath::TileKey& fetchKey)
247{
248 const QString providerType = terrariumProviderType();
249 QGCFetchTileTask* const task =
250 QGeoFileTileCacheQGC::createFetchTileTask(providerType, fetchKey.x, fetchKey.y, fetchKey.zoom);
251 connect(task, &QGCFetchTileTask::tileFetched, this, [this, fetchKey](QGCCacheTile* tile) {
252 const std::unique_ptr<QGCCacheTile> guard(tile); // caller-owned per task contract
253 if (!_fetchInFlight(fetchKey)) {
254 return; // all interest cancelled while the lookup was in flight
255 }
256 // An unusable cached body (empty or corrupt) is a miss, not a
257 // failure: failing here would re-read the same bytes on every retry,
258 // blocking the network fallback until cache eviction
259 const QImage image = tile ? decodeTile(tile->img) : QImage();
260 if (image.isNull()) {
261 qCDebug(GeoMapTerrariumTileFetcherLog)
262 << "tile" << fetchKey << "cached entry unusable, fetching from network";
263 _fetchFromNetwork(fetchKey);
264 return;
265 }
266 qCDebug(GeoMapTerrariumTileFetcherVerboseLog) << "tile" << fetchKey << "served from cache";
267 _deliverAll(fetchKey, image);
268 });
269 connect(task, &QGCMapTask::error, this, [this, fetchKey](QGCMapTask::TaskType, const QString&) {
270 if (!_fetchInFlight(fetchKey)) {
271 return;
272 }
273 qCDebug(GeoMapTerrariumTileFetcherVerboseLog) << "tile" << fetchKey << "not cached, fetching from network";
274 _fetchFromNetwork(fetchKey);
275 });
276
277 if (!getQGCMapEngine()->addTask(task)) {
278 qCDebug(GeoMapTerrariumTileFetcherLog) << "could not queue cache lookup for" << fetchKey;
279 task->deleteLater(); // never enqueued: the worker will not clean it up
280 return false;
281 }
282 return true;
283}
284
286{
287 const auto pendingIt = _pending.constFind(requestId);
288 if (pendingIt == _pending.cend()) {
289 return;
290 }
291 const TileMath::TileKey fetchKey = pendingIt->fetchKey;
292 _pending.erase(pendingIt);
293
294 const auto waitersIt = _waiters.find(fetchKey);
295 if (waitersIt == _waiters.end()) {
296 return;
297 }
298 waitersIt->removeOne(requestId);
299 if (!waitersIt->isEmpty()) {
300 return; // other requests still wait on this tile's fetch
301 }
302 _waiters.erase(waitersIt);
303 if (_fieldRequests.contains(fetchKey)) {
304 return; // the field still wants this tile: keep the fetch alive
305 }
306 QNetworkReply* const reply = _activeReplies.take(fetchKey);
307 if (reply) {
308 qCDebug(GeoMapTerrariumTileFetcherVerboseLog) << "cancelRequest: aborting network fetch for" << fetchKey;
309 reply->abort(); // finished handler is a no-op once no waiters remain
310 reply->deleteLater(); // don't rely on abort emitting finished for cleanup
311 }
312}
313
314void TerrariumTileFetcher::_failAsync(int requestId)
315{
316 // Deliver asynchronously so callers see one consistent flow
317 QMetaObject::invokeMethod(
318 this,
319 [this, requestId] {
320 if (_pending.contains(requestId)) {
321 _finishFailed(requestId);
322 }
323 },
324 Qt::QueuedConnection);
325}
326
327void TerrariumTileFetcher::_fetchFromNetwork(const TileMath::TileKey& fetchKey)
328{
329 if (_activeReplies.contains(fetchKey)) {
330 return; // a reply is already in flight for this tile
331 }
332 const QNetworkRequest request =
333 QGeoTileFetcherQGC::getNetworkRequest(_mapId, fetchKey.x, fetchKey.y, fetchKey.zoom);
334 QNetworkReply* const reply = _networkManager->get(request);
335 _activeReplies.insert(fetchKey, reply);
336
337 connect(reply, &QNetworkReply::finished, this, [this, fetchKey, reply] {
338 reply->deleteLater();
339 if (_activeReplies.value(fetchKey) != reply) {
340 return; // stale: cancelled (and possibly superseded by a newer fetch)
341 }
342 _activeReplies.remove(fetchKey);
343 if (!_fetchInFlight(fetchKey)) {
344 return; // all interest cancelled (abort also lands here)
345 }
346 if (reply->error() != QNetworkReply::NoError) {
347 _failAll(fetchKey, QStringLiteral("network error: %1 (http %2)")
348 .arg(reply->errorString())
349 .arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()));
350 return;
351 }
352 const QByteArray data = reply->readAll();
353 if (data.isEmpty()) {
354 _failAll(fetchKey, QStringLiteral("network fetch returned empty body"));
355 return;
356 }
357 // Never cache bodies that delivery would reject (e.g. HTTP-200 error
358 // pages): a cached invalid tile would fail every retry from then on
359 const QImage image = decodeTile(data);
360 if (image.isNull()) {
361 _failAll(fetchKey, QStringLiteral("network body is not a terrarium tile, %1 bytes").arg(data.size()));
362 return;
363 }
364 qCDebug(GeoMapTerrariumTileFetcherVerboseLog)
365 << "tile" << fetchKey << "fetched from network," << data.size() << "bytes, caching";
366 // Store back so the shared cache serves this tile from now on
367 const QString providerType = terrariumProviderType();
368 QGeoFileTileCacheQGC::cacheTile(providerType, fetchKey.x, fetchKey.y, fetchKey.zoom, data,
369 UrlFactory::getImageFormat(providerType, data));
370 _deliverAll(fetchKey, image);
371 });
372}
373
374void TerrariumTileFetcher::_deliverAll(const TileMath::TileKey& fetchKey, const QImage& image)
375{
376 const bool forField = _fieldRequests.remove(fetchKey) && _heightField;
377 if (forField) {
378 _insertIntoField(fetchKey, gridFromImage(image));
379 }
380
381 const QList<int> requestIds = _waiters.take(fetchKey);
382 qCDebug(GeoMapTerrariumTileFetcherVerboseLog)
383 << "delivering tile" << fetchKey << "to" << requestIds.count() << "waiters, field insert:" << forField;
384 for (int requestId : requestIds) {
385 if (_pending.contains(requestId)) {
386 _deliver(requestId, image);
387 }
388 }
389}
390
391void TerrariumTileFetcher::_failAll(const TileMath::TileKey& fetchKey, const QString& reason)
392{
393 // A failed tile inserts nothing: the field keeps its current estimate, and
394 // clearing the in-flight key lets a later request retry
395 _fieldRequests.remove(fetchKey);
396 // Better fine data with a cliff risk than none at all
397 const int released = _releaseHeld(fetchKey);
398 if (released > 0) {
399 qCDebug(GeoMapTerrariumTileFetcherLog)
400 << "anchor" << fetchKey << "failed, released" << released << "held tiles";
401 }
402
403 const QList<int> requestIds = _waiters.take(fetchKey);
404 if (_shouldWarnFailure()) {
405 qCWarning(GeoMapTerrariumTileFetcherLog)
406 << "tile" << fetchKey << "failed:" << reason << "- failing" << requestIds.count() << "waiters";
407 } else {
408 qCDebug(GeoMapTerrariumTileFetcherLog) << "tile" << fetchKey << "failed:" << reason << "- failing"
409 << requestIds.count() << "waiters (warning suppressed)";
410 }
411 for (int requestId : requestIds) {
412 if (_pending.contains(requestId)) {
413 _finishFailed(requestId);
414 }
415 }
416}
417
418bool TerrariumTileFetcher::_shouldWarnFailure()
419{
420 if (_failureWarnTimer.isValid() && (_failureWarnTimer.elapsed() < kFailureWarnIntervalMs)) {
421 return false;
422 }
423 _failureWarnTimer.restart();
424 return true;
425}
426
427void TerrariumTileFetcher::_insertIntoField(const TileMath::TileKey& fetchKey, ElevationTilePyramid::Grid grid)
428{
429 QSet<TileMath::TileKey> missingAnchors;
430 for (const TileMath::TileKey& anchor : anchorKeysFor(fetchKey)) {
431 if (!_heightField->hasTile(anchor) && _fieldRequests.contains(anchor)) {
432 missingAnchors.insert(anchor);
433 }
434 }
435 if (!missingAnchors.isEmpty()) {
436 qCDebug(GeoMapTerrariumTileFetcherVerboseLog)
437 << "holding tile" << fetchKey << "until anchors" << missingAnchors;
438 _held.insert(fetchKey, HeldTile{std::move(grid), std::move(missingAnchors)});
439 return;
440 }
441 _heightField->insertTile(fetchKey, std::move(grid));
442 (void) _releaseHeld(fetchKey);
443}
444
445int TerrariumTileFetcher::_releaseHeld(const TileMath::TileKey& anchor)
446{
447 QList<std::pair<TileMath::TileKey, ElevationTilePyramid::Grid>> ready;
448 for (auto it = _held.begin(); it != _held.end();) {
449 if (it->anchors.remove(anchor) && it->anchors.isEmpty()) {
450 ready.append({it.key(), std::move(it->grid)});
451 it = _held.erase(it);
452 } else {
453 ++it;
454 }
455 }
456 // Inserted after the walk: regionChanged handlers may re-enter requestTile
457 for (auto& [key, grid] : ready) {
458 _heightField->insertTile(key, std::move(grid));
459 }
460 return int(ready.count());
461}
462
463void TerrariumTileFetcher::_deliver(int requestId, const QImage& image)
464{
465 const PendingRequest pending = _pending.take(requestId);
466 emit patchHeightsReady(requestId, sampleGrid(image, pending.subWindow, pending.gridSize));
467}
468
469void TerrariumTileFetcher::_finishFailed(int requestId)
470{
471 _pending.remove(requestId);
472 emit patchHeightsFailed(requestId);
473}
double bilinearAtUV(int w, int h, double u, double v, SampleAt at)
Definition BilinearUV.h:24
#define QGC_LOGGING_CATEGORY(name, categoryStr)
QGCMapEngine * getQGCMapEngine()
bool insertTile(const TileMath::TileKey &key, ElevationTilePyramid::Grid grid)
bool hasTile(const TileMath::TileKey &key) const
True when the backing pyramid holds this exact tile.
Definition HeightField.h:76
void patchHeightsReady(int requestId, const QList< float > &heights)
heights has (gridSize+1)^2 entries, row-major from the north-west corner
void patchHeightsFailed(int requestId)
HeightField * _heightField
tile delivery target for requestTile (not owned)
int _nextRequestId()
void tileFetched(QGCCacheTile *tile)
bool addTask(QGCMapTask *task)
void error(QGCMapTask::TaskType type, const QString &errorString)
static QGCFetchTileTask * createFetchTileTask(const QString &type, int x, int y, int z)
static void cacheTile(const QString &type, int x, int y, int z, const QByteArray &image, const QString &format, qulonglong set=UINT64_MAX)
static QNetworkRequest getNetworkRequest(int mapId, int x, int y, int zoom)
static constexpr const char * kProviderKey
static constexpr int kAnchorZoom
static constexpr int kMaxGridSize
Largest supported patch grid (matches PatchGeometry::kMaxGridSize)
static constexpr int kMaxTileZoom
Highest zoom the terrarium dataset serves (deeper patches sample the ancestor)
bool requestTile(const TileMath::TileKey &key) override
TerrariumTileFetcher(QObject *parent=nullptr, QNetworkAccessManager *networkManager=nullptr)
networkManager overrides the internally created one (test injection seam)
void cancelRequest(int requestId) final
Cancel a pending request. No signal is emitted for a cancelled request.
int requestPatchHeights(const TileMath::TileKey &key, int gridSize) final
static QString getImageFormat(QStringView type, QByteArrayView image)
bool isValidKey(const TileKey &key)
True if zoom is within [kMinZoom, kMaxZoom] and x/y address a tile at that zoom.
Definition TileMath.cc:44
Decoded elevation samples for one tile, row-major from the NW corner.
QByteArray img