12#include <QtGui/QImage>
13#include <QtNetwork/QNetworkAccessManager>
14#include <QtNetwork/QNetworkReply>
34 "fetcher grid cap must match the patch mesh cap");
38QString terrariumProviderType()
44double heightAtPixel(
const QImage& image,
int px,
int py)
46 const QRgb rgb = image.pixel(px, py);
47 return (qRed(rgb) * 256.0) + qGreen(rgb) + (qBlue(rgb) / 256.0) - 32768.0;
54double heightAtUV(
const QImage& image,
double u,
double v)
56 const int w = image.width();
57 const int h = image.height();
58 const double px = (u * w) - 0.5;
59 const double py = (v * h) - 0.5;
60 const int x0 = qBound(0,
static_cast<int>(std::floor(px)), w - 1);
61 const int y0 = qBound(0,
static_cast<int>(std::floor(py)), h - 1);
62 const int x1 = qMin(x0 + 1, w - 1);
63 const int y1 = qMin(y0 + 1, h - 1);
64 const double fx = qBound(0.0, px - x0, 1.0);
65 const double fy = qBound(0.0, py - y0, 1.0);
67 const double north = (heightAtPixel(image, x0, y0) * (1.0 - fx)) + (heightAtPixel(image, x1, y0) * fx);
68 const double south = (heightAtPixel(image, x0, y1) * (1.0 - fx)) + (heightAtPixel(image, x1, y1) * fx);
69 return (north * (1.0 - fy)) + (south * fy);
74QList<float> sampleGrid(
const QImage& image,
const QRectF& subWindow,
int gridSize)
77 heights.reserve(qsizetype(gridSize + 1) * (gridSize + 1));
78 for (
int row = 0; row <= gridSize; row++) {
79 const double v = subWindow.y() + (subWindow.height() * row / gridSize);
80 for (
int col = 0; col <= gridSize; col++) {
81 const double u = subWindow.x() + (subWindow.width() * col / gridSize);
82 heights.append(
static_cast<float>(heightAtUV(image, u, v)));
90constexpr int kTileSizePixels = 256;
93QImage decodeTile(
const QByteArray& data)
96 if (!image.loadFromData(data) || image.width() != kTileSizePixels || image.height() != kTileSizePixels) {
99 return image.convertToFormat(QImage::Format_RGB32);
106 grid.
width = image.width();
107 grid.
height = image.height();
109 for (
int py = 0; py < grid.
height; py++) {
110 for (
int px = 0; px < grid.
width; px++) {
111 grid.
heights.append(
static_cast<float>(heightAtPixel(image, px, py)));
132 _networkManager(networkManager ? networkManager : new QNetworkAccessManager(this)),
133 _mapId(
UrlFactory::getQtMapIdFromProviderType(terrariumProviderType()))
141 qCWarning(GeoMapTerrariumTileFetcherLog)
142 <<
"requestPatchHeights rejected: key" << key <<
"gridSize" << gridSize;
143 _pending.insert(requestId, PendingRequest{});
144 _failAsync(requestId);
148 qCDebug(GeoMapTerrariumTileFetcherLog) <<
"requestPatchHeights: elevation provider not registered";
149 _pending.insert(requestId, PendingRequest{});
150 _failAsync(requestId);
156 PendingRequest pending;
157 pending.gridSize = gridSize;
158 pending.fetchKey = fetchKeyFor(key);
160 pending.subWindow = QRectF(0, 0, 1, 1);
163 const double scale = 1.0 / (1LL << shift);
164 pending.subWindow = QRectF((key.
x - (qint64(pending.fetchKey.x) << shift)) * scale,
165 (key.
y - (qint64(pending.fetchKey.y) << shift)) * scale, scale, scale);
167 _pending.insert(requestId, pending);
168 _lastFetchKey = pending.fetchKey;
170 const bool inFlight = _fetchInFlight(pending.fetchKey);
171 _waiters[pending.fetchKey].append(requestId);
172 qCDebug(GeoMapTerrariumTileFetcherVerboseLog)
173 <<
"requestPatchHeights: key" << key <<
"requestId" << requestId <<
"fetchKey" << pending.fetchKey
174 << (inFlight ?
"(joining in-flight fetch)" :
"(starting fetch)");
179 if (!_startFetch(pending.fetchKey)) {
180 qCDebug(GeoMapTerrariumTileFetcherLog) <<
"requestPatchHeights: could not start fetch for" << pending.fetchKey;
181 _waiters.remove(pending.fetchKey);
182 _failAsync(requestId);
190 qCWarning(GeoMapTerrariumTileFetcherLog)
191 <<
"requestTile rejected: key" << key <<
"heightFieldSet" << (
_heightField !=
nullptr);
195 qCDebug(GeoMapTerrariumTileFetcherLog) <<
"requestTile: elevation provider not registered";
204 const bool inFlight = _fetchInFlight(fetchKey);
205 _fieldRequests.insert(fetchKey);
206 _lastFetchKey = fetchKey;
207 qCDebug(GeoMapTerrariumTileFetcherVerboseLog)
208 <<
"requestTile: fetchKey" << fetchKey << (inFlight ?
"(joining in-flight fetch)" :
"(starting fetch)");
213 if (!_startFetch(fetchKey)) {
214 qCDebug(GeoMapTerrariumTileFetcherLog) <<
"requestTile: could not start fetch for" << fetchKey;
215 _fieldRequests.remove(fetchKey);
221bool TerrariumTileFetcher::_fetchInFlight(
const TileMath::TileKey& fetchKey)
const
223 return _waiters.contains(fetchKey) || _fieldRequests.contains(fetchKey);
228 const QString providerType = terrariumProviderType();
232 const std::unique_ptr<QGCCacheTile> guard(tile);
233 if (!_fetchInFlight(fetchKey)) {
239 const QImage image = tile ? decodeTile(tile->
img) : QImage();
240 if (image.isNull()) {
241 qCDebug(GeoMapTerrariumTileFetcherLog)
242 <<
"tile" << fetchKey <<
"cached entry unusable, fetching from network";
243 _fetchFromNetwork(fetchKey);
246 qCDebug(GeoMapTerrariumTileFetcherVerboseLog) <<
"tile" << fetchKey <<
"served from cache";
247 _deliverAll(fetchKey, image);
250 if (!_fetchInFlight(fetchKey)) {
253 qCDebug(GeoMapTerrariumTileFetcherVerboseLog) <<
"tile" << fetchKey <<
"not cached, fetching from network";
254 _fetchFromNetwork(fetchKey);
258 qCDebug(GeoMapTerrariumTileFetcherLog) <<
"could not queue cache lookup for" << fetchKey;
267 const auto pendingIt = _pending.constFind(requestId);
268 if (pendingIt == _pending.cend()) {
272 _pending.erase(pendingIt);
274 const auto waitersIt = _waiters.find(fetchKey);
275 if (waitersIt == _waiters.end()) {
278 waitersIt->removeOne(requestId);
279 if (!waitersIt->isEmpty()) {
282 _waiters.erase(waitersIt);
283 if (_fieldRequests.contains(fetchKey)) {
286 QNetworkReply*
const reply = _activeReplies.take(fetchKey);
288 qCDebug(GeoMapTerrariumTileFetcherVerboseLog) <<
"cancelRequest: aborting network fetch for" << fetchKey;
290 reply->deleteLater();
294void TerrariumTileFetcher::_failAsync(
int requestId)
297 QMetaObject::invokeMethod(
300 if (_pending.contains(requestId)) {
301 _finishFailed(requestId);
304 Qt::QueuedConnection);
309 if (_activeReplies.contains(fetchKey)) {
312 const QNetworkRequest request =
314 QNetworkReply*
const reply = _networkManager->get(request);
315 _activeReplies.insert(fetchKey, reply);
317 connect(reply, &QNetworkReply::finished,
this, [
this, fetchKey, reply] {
318 reply->deleteLater();
319 if (_activeReplies.value(fetchKey) != reply) {
322 _activeReplies.remove(fetchKey);
323 if (!_fetchInFlight(fetchKey)) {
326 if (reply->error() != QNetworkReply::NoError) {
327 _failAll(fetchKey, QStringLiteral(
"network error: %1 (http %2)")
328 .arg(reply->errorString())
329 .arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()));
332 const QByteArray data = reply->readAll();
333 if (data.isEmpty()) {
334 _failAll(fetchKey, QStringLiteral(
"network fetch returned empty body"));
339 const QImage image = decodeTile(data);
340 if (image.isNull()) {
341 _failAll(fetchKey, QStringLiteral(
"network body is not a terrarium tile, %1 bytes").arg(data.size()));
344 qCDebug(GeoMapTerrariumTileFetcherVerboseLog)
345 <<
"tile" << fetchKey <<
"fetched from network," << data.size() <<
"bytes, caching";
347 const QString providerType = terrariumProviderType();
350 _deliverAll(fetchKey, image);
354void TerrariumTileFetcher::_deliverAll(
const TileMath::TileKey& fetchKey,
const QImage& image)
356 const bool forField = _fieldRequests.remove(fetchKey) &&
_heightField;
361 const QList<int> requestIds = _waiters.take(fetchKey);
362 qCDebug(GeoMapTerrariumTileFetcherVerboseLog)
363 <<
"delivering tile" << fetchKey <<
"to" << requestIds.count() <<
"waiters, field insert:" << forField;
364 for (
int requestId : requestIds) {
365 if (_pending.contains(requestId)) {
366 _deliver(requestId, image);
371void TerrariumTileFetcher::_failAll(
const TileMath::TileKey& fetchKey,
const QString& reason)
375 _fieldRequests.remove(fetchKey);
377 const QList<int> requestIds = _waiters.take(fetchKey);
378 if (_shouldWarnFailure()) {
379 qCWarning(GeoMapTerrariumTileFetcherLog)
380 <<
"tile" << fetchKey <<
"failed:" << reason <<
"- failing" << requestIds.count() <<
"waiters";
382 qCDebug(GeoMapTerrariumTileFetcherLog) <<
"tile" << fetchKey <<
"failed:" << reason <<
"- failing"
383 << requestIds.count() <<
"waiters (warning suppressed)";
385 for (
int requestId : requestIds) {
386 if (_pending.contains(requestId)) {
387 _finishFailed(requestId);
392bool TerrariumTileFetcher::_shouldWarnFailure()
394 if (_failureWarnTimer.isValid() && (_failureWarnTimer.elapsed() < kFailureWarnIntervalMs)) {
397 _failureWarnTimer.restart();
401void TerrariumTileFetcher::_deliver(
int requestId,
const QImage& image)
403 const PendingRequest pending = _pending.take(requestId);
404 emit
patchHeightsReady(requestId, sampleGrid(image, pending.subWindow, pending.gridSize));
407void TerrariumTileFetcher::_finishFailed(
int requestId)
409 _pending.remove(requestId);
#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 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
Request the vertex height grid for a patch. Returns a request id (> 0).
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