QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
TileImageSource.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
10#include "TileImageSource.h"
11
12#include <QtCore/QFile>
13#include <QtNetwork/QNetworkAccessManager>
14#include <QtNetwork/QNetworkReply>
15
16#include <memory>
17
18#include "MapProvider.h"
19#include "QGCCacheTile.h"
20#include "QGCLoggingCategory.h"
21#include "QGCMapEngine.h"
22#include "QGCMapTasks.h"
23#include "QGCMapUrlEngine.h"
25#include "QGeoTileFetcherQGC.h"
26
27QGC_LOGGING_CATEGORY(GeoMapTileImageSourceLog, "GeoMap.TileImageSource")
28QGC_LOGGING_CATEGORY(GeoMapTileImageSourceVerboseLog, "GeoMap.TileImageSource.Verbose")
29
30namespace {
31
32// Bing serves a placeholder image instead of an HTTP error where it has no
33// imagery; delivering or caching it as a tile would drape it onto patches
34bool isBingEmptyTile(int mapId, const QByteArray& data)
35{
36 static const QByteArray bingNoTileImage = [] {
37 QFile file(QStringLiteral(":/res/BingNoTileBytes.dat"));
38 return file.open(QFile::ReadOnly) ? file.readAll() : QByteArray();
39 }();
40
41 if (bingNoTileImage.isEmpty() || (data != bingNoTileImage)) {
42 return false;
43 }
45 return provider && provider->isBingProvider();
46}
47
48} // namespace
49
50TileImageSource::TileImageSource(const QString& mapType, QObject* parent, QNetworkAccessManager* networkManager)
51 : QObject(parent),
52 _mapType(mapType),
53 _mapId(UrlFactory::getQtMapIdFromProviderType(mapType)),
54 _networkManager(networkManager ? networkManager : new QNetworkAccessManager(this))
55{
56 if (_mapId < 0) {
57 qCWarning(GeoMapTileImageSourceLog) << "unknown map type" << _mapType << "- all tile requests will fail";
58 }
59}
60
62{
63 const int requestId = ++_requestIdCounter;
64 _pending.insert(requestId);
65
66 if (_mapId < 0) {
67 _failAsync(requestId); // unknown provider: don't spam the cache/URL factory per request
68 return requestId;
69 }
70 qCDebug(GeoMapTileImageSourceVerboseLog) << "requestTileImage: key" << key << "requestId" << requestId;
71
72 QGCFetchTileTask* const task = QGeoFileTileCacheQGC::createFetchTileTask(_mapType, key.x, key.y, key.zoom);
73 connect(task, &QGCFetchTileTask::tileFetched, this, [this, requestId, key](QGCCacheTile* tile) {
74 const std::unique_ptr<QGCCacheTile> guard(tile); // caller-owned per task contract
75 if (!_pending.contains(requestId)) {
76 return; // cancelled while the lookup was in flight
77 }
78 if (!tile || tile->img.isEmpty()) {
79 qCDebug(GeoMapTileImageSourceLog) << "tile" << key << "cache returned empty tile, fetching from network";
80 _fetchFromNetwork(requestId, key);
81 return;
82 }
83 _deliver(requestId, key, tile->img);
84 });
85 connect(task, &QGCMapTask::error, this, [this, requestId, key](QGCMapTask::TaskType, const QString&) {
86 if (!_pending.contains(requestId)) {
87 return;
88 }
89 qCDebug(GeoMapTileImageSourceVerboseLog) << "tile" << key << "not cached, fetching from network";
90 _fetchFromNetwork(requestId, key);
91 });
92
93 if (!getQGCMapEngine()->addTask(task)) {
94 qCDebug(GeoMapTileImageSourceLog) << "could not queue cache lookup for" << key;
95 task->deleteLater(); // never enqueued: the worker will not clean it up
96 _failAsync(requestId);
97 }
98 return requestId;
99}
100
101void TileImageSource::_failAsync(int requestId)
102{
103 // Deliver asynchronously so callers see one consistent flow
104 QMetaObject::invokeMethod(
105 this,
106 [this, requestId] {
107 if (_pending.contains(requestId)) {
108 _finishFailed(requestId);
109 }
110 },
111 Qt::QueuedConnection);
112}
113
115{
116 if (!_pending.remove(requestId)) {
117 return;
118 }
119 QNetworkReply* const reply = _activeReplies.take(requestId);
120 if (reply) {
121 reply->abort(); // finished handler is a no-op once the id is not pending
122 }
123}
124
125void TileImageSource::_fetchFromNetwork(int requestId, const TileMath::TileKey& key)
126{
127 const QNetworkRequest request = QGeoTileFetcherQGC::getNetworkRequest(_mapId, key.x, key.y, key.zoom);
128 QNetworkReply* const reply = _networkManager->get(request);
129 _activeReplies.insert(requestId, reply);
130
131 connect(reply, &QNetworkReply::finished, this, [this, requestId, key, reply] {
132 reply->deleteLater();
133 _activeReplies.remove(requestId);
134 if (!_pending.contains(requestId)) {
135 return; // cancelled (abort also lands here)
136 }
137 if (reply->error() != QNetworkReply::NoError) {
138 _warnFailure(key, QStringLiteral("network error: %1 (http %2)")
139 .arg(reply->errorString())
140 .arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()));
141 _finishFailed(requestId);
142 return;
143 }
144 const QByteArray data = reply->readAll();
145 if (data.isEmpty() || isBingEmptyTile(_mapId, data)) {
146 _warnFailure(key, data.isEmpty() ? QStringLiteral("network fetch returned empty body")
147 : QStringLiteral("network fetch returned Bing no-tile placeholder"));
148 _finishFailed(requestId);
149 return;
150 }
151 QImage image;
152 if (!image.loadFromData(data)) {
153 _warnFailure(key, QStringLiteral("network body failed to decode, %1 bytes").arg(data.size()));
154 _finishFailed(requestId); // never cache undecodable bodies (e.g. HTTP-200 error pages)
155 return;
156 }
157 // Store back so the shared cache serves this tile from now on
158 qCDebug(GeoMapTileImageSourceVerboseLog) << "tile" << key << "fetched from network, caching";
159 QGeoFileTileCacheQGC::cacheTile(_mapType, key.x, key.y, key.zoom, data,
160 UrlFactory::getImageFormat(_mapType, data));
161 _finishSucceeded(requestId, image);
162 });
163}
164
165void TileImageSource::_deliver(int requestId, const TileMath::TileKey& key, const QByteArray& data)
166{
167 QImage image;
168 if (isBingEmptyTile(_mapId, data) || !image.loadFromData(data)) {
169 // An unusable cached body (placeholder or corrupt) is a miss, not a
170 // failure: failing here would re-read the same bytes on every paced
171 // retry, blocking the network fallback until cache eviction
172 qCDebug(GeoMapTileImageSourceLog) << "tile" << key << "cached entry unusable, fetching from network";
173 _fetchFromNetwork(requestId, key);
174 return;
175 }
176 qCDebug(GeoMapTileImageSourceVerboseLog) << "tile" << key << "served from cache";
177 _finishSucceeded(requestId, image);
178}
179
180void TileImageSource::_finishSucceeded(int requestId, const QImage& image)
181{
182 _pending.remove(requestId);
183 emit tileImageReady(requestId, image);
184}
185
186void TileImageSource::_finishFailed(int requestId)
187{
188 _pending.remove(requestId);
189 emit tileImageFailed(requestId);
190}
191
192void TileImageSource::_warnFailure(const TileMath::TileKey& key, const QString& reason)
193{
194 // Fetch failures must be visible without logging configuration (matches
195 // the map tile / terrain query convention), but repeats are throttled so
196 // an outage doesn't flood the log
197 if (!_failureWarnTimer.isValid() || (_failureWarnTimer.elapsed() >= kFailureWarnIntervalMs)) {
198 _failureWarnTimer.restart();
199 qCWarning(GeoMapTileImageSourceLog) << "tile" << key << "failed:" << reason;
200 } else {
201 qCDebug(GeoMapTileImageSourceLog) << "tile" << key << "failed:" << reason << "(warning suppressed)";
202 }
203}
#define QGC_LOGGING_CATEGORY(name, categoryStr)
QGCMapEngine * getQGCMapEngine()
std::shared_ptr< const MapProvider > SharedMapProvider
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)
void tileImageFailed(int requestId)
void cancelRequest(int requestId)
Cancel a pending request, aborting any in-flight network fetch.
int requestTileImage(const TileMath::TileKey &key)
Request the image for a tile. Returns a request id (> 0).
TileImageSource(const QString &mapType, QObject *parent=nullptr, QNetworkAccessManager *networkManager=nullptr)
void tileImageReady(int requestId, const QImage &image)
static QString getImageFormat(QStringView type, QByteArrayView image)
static std::shared_ptr< const MapProvider > getMapProviderFromQtMapId(int qtMapId)
QByteArray img