15#include <QtCore/QVarLengthArray>
16#include <QtGui/QImage>
17#include <QtNetwork/QNetworkAccessManager>
18#include <QtNetwork/QNetworkReply>
36 "fetcher grid cap must match the patch mesh cap");
40QString terrariumProviderType()
46double heightAtPixel(
const QImage& image,
int px,
int py)
48 const QRgb rgb = image.pixel(px, py);
49 return (qRed(rgb) * 256.0) + qGreen(rgb) + (qBlue(rgb) / 256.0) - 32768.0;
54double heightAtUV(
const QImage& image,
double u,
double v)
56 const auto at = [&image](
int x,
int y) {
return heightAtPixel(image, x, y); };
57 return bilinearAtUV(image.width(), image.height(), u, v, at);
62QList<float> sampleGrid(
const QImage& image,
const QRectF& subWindow,
int gridSize)
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)));
78constexpr int kTileSizePixels = 256;
81QImage decodeTile(
const QByteArray& data)
84 if (!image.loadFromData(data) || image.width() != kTileSizePixels || image.height() != kTileSizePixels) {
87 return image.convertToFormat(QImage::Format_RGB32);
94 grid.
width = image.width();
95 grid.
height = image.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)));
119QVarLengthArray<TileMath::TileKey, 4> anchorKeysFor(
const TileMath::TileKey& fetchKey)
121 QVarLengthArray<TileMath::TileKey, 4> anchors;
127 for (
int dy = -1; dy <= 1; dy++) {
128 for (
int dx = -1; dx <= 1; dx++) {
134 if (!anchors.contains(anchor)) {
135 anchors.append(anchor);
146 _networkManager(networkManager ? networkManager : new QNetworkAccessManager(this)),
147 _mapId(
UrlFactory::getQtMapIdFromProviderType(terrariumProviderType()))
155 qCWarning(GeoMapTerrariumTileFetcherLog)
156 <<
"requestPatchHeights rejected: key" << key <<
"gridSize" << gridSize;
157 _pending.insert(requestId, PendingRequest{});
158 _failAsync(requestId);
162 qCDebug(GeoMapTerrariumTileFetcherLog) <<
"requestPatchHeights: elevation provider not registered";
163 _pending.insert(requestId, PendingRequest{});
164 _failAsync(requestId);
170 PendingRequest pending;
171 pending.gridSize = gridSize;
172 pending.fetchKey = fetchKeyFor(key);
174 pending.subWindow = QRectF(0, 0, 1, 1);
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);
181 _pending.insert(requestId, pending);
182 _lastFetchKey = pending.fetchKey;
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)");
193 if (!_startFetch(pending.fetchKey)) {
194 qCDebug(GeoMapTerrariumTileFetcherLog) <<
"requestPatchHeights: could not start fetch for" << pending.fetchKey;
195 _waiters.remove(pending.fetchKey);
196 _failAsync(requestId);
204 qCWarning(GeoMapTerrariumTileFetcherLog)
205 <<
"requestTile rejected: key" << key <<
"heightFieldSet" << (
_heightField !=
nullptr);
209 qCDebug(GeoMapTerrariumTileFetcherLog) <<
"requestTile: elevation provider not registered";
214 if (
_heightField->
hasTile(fetchKey) || _fieldRequests.contains(fetchKey) || _held.contains(fetchKey)) {
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)");
233 if (!_startFetch(fetchKey)) {
234 qCDebug(GeoMapTerrariumTileFetcherLog) <<
"requestTile: could not start fetch for" << fetchKey;
235 _fieldRequests.remove(fetchKey);
241bool TerrariumTileFetcher::_fetchInFlight(
const TileMath::TileKey& fetchKey)
const
243 return _waiters.contains(fetchKey) || _fieldRequests.contains(fetchKey);
248 const QString providerType = terrariumProviderType();
252 const std::unique_ptr<QGCCacheTile> guard(tile);
253 if (!_fetchInFlight(fetchKey)) {
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);
266 qCDebug(GeoMapTerrariumTileFetcherVerboseLog) <<
"tile" << fetchKey <<
"served from cache";
267 _deliverAll(fetchKey, image);
270 if (!_fetchInFlight(fetchKey)) {
273 qCDebug(GeoMapTerrariumTileFetcherVerboseLog) <<
"tile" << fetchKey <<
"not cached, fetching from network";
274 _fetchFromNetwork(fetchKey);
278 qCDebug(GeoMapTerrariumTileFetcherLog) <<
"could not queue cache lookup for" << fetchKey;
287 const auto pendingIt = _pending.constFind(requestId);
288 if (pendingIt == _pending.cend()) {
292 _pending.erase(pendingIt);
294 const auto waitersIt = _waiters.find(fetchKey);
295 if (waitersIt == _waiters.end()) {
298 waitersIt->removeOne(requestId);
299 if (!waitersIt->isEmpty()) {
302 _waiters.erase(waitersIt);
303 if (_fieldRequests.contains(fetchKey)) {
306 QNetworkReply*
const reply = _activeReplies.take(fetchKey);
308 qCDebug(GeoMapTerrariumTileFetcherVerboseLog) <<
"cancelRequest: aborting network fetch for" << fetchKey;
310 reply->deleteLater();
314void TerrariumTileFetcher::_failAsync(
int requestId)
317 QMetaObject::invokeMethod(
320 if (_pending.contains(requestId)) {
321 _finishFailed(requestId);
324 Qt::QueuedConnection);
329 if (_activeReplies.contains(fetchKey)) {
332 const QNetworkRequest request =
334 QNetworkReply*
const reply = _networkManager->get(request);
335 _activeReplies.insert(fetchKey, reply);
337 connect(reply, &QNetworkReply::finished,
this, [
this, fetchKey, reply] {
338 reply->deleteLater();
339 if (_activeReplies.value(fetchKey) != reply) {
342 _activeReplies.remove(fetchKey);
343 if (!_fetchInFlight(fetchKey)) {
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()));
352 const QByteArray data = reply->readAll();
353 if (data.isEmpty()) {
354 _failAll(fetchKey, QStringLiteral(
"network fetch returned empty body"));
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()));
364 qCDebug(GeoMapTerrariumTileFetcherVerboseLog)
365 <<
"tile" << fetchKey <<
"fetched from network," << data.size() <<
"bytes, caching";
367 const QString providerType = terrariumProviderType();
370 _deliverAll(fetchKey, image);
374void TerrariumTileFetcher::_deliverAll(
const TileMath::TileKey& fetchKey,
const QImage& image)
376 const bool forField = _fieldRequests.remove(fetchKey) &&
_heightField;
378 _insertIntoField(fetchKey, gridFromImage(image));
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);
391void TerrariumTileFetcher::_failAll(
const TileMath::TileKey& fetchKey,
const QString& reason)
395 _fieldRequests.remove(fetchKey);
397 const int released = _releaseHeld(fetchKey);
399 qCDebug(GeoMapTerrariumTileFetcherLog)
400 <<
"anchor" << fetchKey <<
"failed, released" << released <<
"held tiles";
403 const QList<int> requestIds = _waiters.take(fetchKey);
404 if (_shouldWarnFailure()) {
405 qCWarning(GeoMapTerrariumTileFetcherLog)
406 <<
"tile" << fetchKey <<
"failed:" << reason <<
"- failing" << requestIds.count() <<
"waiters";
408 qCDebug(GeoMapTerrariumTileFetcherLog) <<
"tile" << fetchKey <<
"failed:" << reason <<
"- failing"
409 << requestIds.count() <<
"waiters (warning suppressed)";
411 for (
int requestId : requestIds) {
412 if (_pending.contains(requestId)) {
413 _finishFailed(requestId);
418bool TerrariumTileFetcher::_shouldWarnFailure()
420 if (_failureWarnTimer.isValid() && (_failureWarnTimer.elapsed() < kFailureWarnIntervalMs)) {
423 _failureWarnTimer.restart();
429 QSet<TileMath::TileKey> missingAnchors;
432 missingAnchors.insert(anchor);
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)});
442 (void) _releaseHeld(fetchKey);
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);
457 for (
auto& [key, grid] : ready) {
460 return int(ready.count());
463void TerrariumTileFetcher::_deliver(
int requestId,
const QImage& image)
465 const PendingRequest pending = _pending.take(requestId);
466 emit
patchHeightsReady(requestId, sampleGrid(image, pending.subWindow, pending.gridSize));
469void TerrariumTileFetcher::_finishFailed(
int requestId)
471 _pending.remove(requestId);
double bilinearAtUV(int w, int h, double u, double v, SampleAt at)
#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.
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)
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.
Decoded elevation samples for one tile, row-major from the NW corner.
QList< float > heights
meters