QGIS API Documentation  3.24.2-Tisler (13c1a02865)
qgsvectortileloader.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgsvectortileloader.cpp
3  --------------------------------------
4  Date : March 2020
5  Copyright : (C) 2020 by Martin Dobias
6  Email : wonder dot sk at gmail dot com
7  ***************************************************************************
8  * *
9  * This program is free software; you can redistribute it and/or modify *
10  * it under the terms of the GNU General Public License as published by *
11  * the Free Software Foundation; either version 2 of the License, or *
12  * (at your option) any later version. *
13  * *
14  ***************************************************************************/
15 
16 #include "qgsvectortileloader.h"
17 
18 #include <QEventLoop>
19 
21 #include "qgslogger.h"
22 #include "qgsmbtiles.h"
24 #include "qgsvectortileutils.h"
25 #include "qgsapplication.h"
26 #include "qgsauthmanager.h"
27 #include "qgsmessagelog.h"
28 
29 #include "qgstiledownloadmanager.h"
30 
31 QgsVectorTileLoader::QgsVectorTileLoader( const QString &uri, const QgsTileMatrix &tileMatrix, const QgsTileRange &range, const QPointF &viewCenter, const QString &authid, const QgsHttpHeaders &headers, QgsFeedback *feedback )
32  : mEventLoop( new QEventLoop )
33  , mFeedback( feedback )
34  , mAuthCfg( authid )
35  , mHeaders( headers )
36 {
37  if ( feedback )
38  {
39  connect( feedback, &QgsFeedback::canceled, this, &QgsVectorTileLoader::canceled, Qt::QueuedConnection );
40 
41  // rendering could have been canceled before we started to listen to canceled() signal
42  // so let's check before doing the download and maybe quit prematurely
43  if ( feedback->isCanceled() )
44  return;
45  }
46 
47  QgsDebugMsgLevel( QStringLiteral( "Starting network loader" ), 2 );
48  QVector<QgsTileXYZ> tiles = QgsVectorTileUtils::tilesInRange( range, tileMatrix.zoomLevel() );
50  for ( QgsTileXYZ id : std::as_const( tiles ) )
51  {
52  loadFromNetworkAsync( id, tileMatrix, uri );
53  }
54 }
55 
57 {
58  QgsDebugMsgLevel( QStringLiteral( "Terminating network loader" ), 2 );
59 
60  if ( !mReplies.isEmpty() )
61  {
62  // this can happen when the loader is terminated without getting requests finalized
63  // (e.g. downloadBlocking() was not called)
64  canceled();
65  }
66 }
67 
69 {
70  if ( mFeedback && mFeedback->isCanceled() )
71  {
72  QgsDebugMsgLevel( QStringLiteral( "downloadBlocking - not staring event loop - canceled" ), 2 );
73  return; // nothing to do
74  }
75 
76  QgsDebugMsgLevel( QStringLiteral( "Starting event loop with %1 requests" ).arg( mReplies.count() ), 2 );
77 
78  mEventLoop->exec( QEventLoop::ExcludeUserInputEvents );
79 
80  QgsDebugMsgLevel( QStringLiteral( "downloadBlocking finished" ), 2 );
81 
82  Q_ASSERT( mReplies.isEmpty() );
83 }
84 
85 void QgsVectorTileLoader::loadFromNetworkAsync( const QgsTileXYZ &id, const QgsTileMatrix &tileMatrix, const QString &requestUrl )
86 {
87  QString url = QgsVectorTileUtils::formatXYZUrlTemplate( requestUrl, id, tileMatrix );
88  QNetworkRequest request( url );
89  QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsVectorTileLoader" ) );
90  QgsSetRequestInitiatorId( request, id.toString() );
91 
92  request.setAttribute( static_cast<QNetworkRequest::Attribute>( QNetworkRequest::User + 1 ), id.column() );
93  request.setAttribute( static_cast<QNetworkRequest::Attribute>( QNetworkRequest::User + 2 ), id.row() );
94  request.setAttribute( static_cast<QNetworkRequest::Attribute>( QNetworkRequest::User + 3 ), id.zoomLevel() );
95 
96  request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
97  request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
98 
99  mHeaders.updateNetworkRequest( request );
100 
101  if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkRequest( request, mAuthCfg ) )
102  {
103  QgsMessageLog::logMessage( tr( "network request update failed for authentication config" ), tr( "Network" ) );
104  }
105 
107  connect( reply, &QgsTileDownloadManagerReply::finished, this, &QgsVectorTileLoader::tileReplyFinished );
108  mReplies << reply;
109 }
110 
111 void QgsVectorTileLoader::tileReplyFinished()
112 {
113  QgsTileDownloadManagerReply *reply = qobject_cast<QgsTileDownloadManagerReply *>( sender() );
114 
115  int reqX = reply->request().attribute( static_cast<QNetworkRequest::Attribute>( QNetworkRequest::User + 1 ) ).toInt();
116  int reqY = reply->request().attribute( static_cast<QNetworkRequest::Attribute>( QNetworkRequest::User + 2 ) ).toInt();
117  int reqZ = reply->request().attribute( static_cast<QNetworkRequest::Attribute>( QNetworkRequest::User + 3 ) ).toInt();
118  QgsTileXYZ tileID( reqX, reqY, reqZ );
119 
120  if ( reply->error() == QNetworkReply::NoError )
121  {
122  // TODO: handle redirections?
123 
124  QgsDebugMsgLevel( QStringLiteral( "Tile download successful: " ) + tileID.toString(), 2 );
125  QByteArray rawData = reply->data();
126  mReplies.removeOne( reply );
127  reply->deleteLater();
128 
129  emit tileRequestFinished( QgsVectorTileRawData( tileID, rawData ) );
130  }
131  else
132  {
133  if ( reply->error() == QNetworkReply::ContentAccessDenied )
134  {
135  if ( reply->data().isEmpty() )
136  mError = tr( "Access denied" );
137  else
138  mError = tr( "Access denied: %1" ).arg( QString( reply->data() ) );
139  }
140 
141  QgsDebugMsg( QStringLiteral( "Tile download failed! " ) + reply->errorString() );
142  mReplies.removeOne( reply );
143  reply->deleteLater();
144 
145  emit tileRequestFinished( QgsVectorTileRawData( tileID, QByteArray() ) );
146  }
147 
148  if ( mReplies.isEmpty() )
149  {
150  // exist the event loop
151  QMetaObject::invokeMethod( mEventLoop.get(), "quit", Qt::QueuedConnection );
152  }
153 }
154 
155 void QgsVectorTileLoader::canceled()
156 {
157  QgsDebugMsgLevel( QStringLiteral( "Canceling %1 pending requests" ).arg( mReplies.count() ), 2 );
158  qDeleteAll( mReplies );
159  mReplies.clear();
160 
161  // stop blocking download
162  mEventLoop->quit();
163 
164 }
165 
167 {
168  return mError;
169 }
170 
172 
173 QList<QgsVectorTileRawData> QgsVectorTileLoader::blockingFetchTileRawData( const QString &sourceType, const QString &sourcePath, const QgsTileMatrix &tileMatrix, const QPointF &viewCenter, const QgsTileRange &range, const QString &authid, const QgsHttpHeaders &headers, QgsFeedback *feedback )
174 {
175  QList<QgsVectorTileRawData> rawTiles;
176 
177  QgsMbTiles mbReader( sourcePath );
178  bool isUrl = ( sourceType == QLatin1String( "xyz" ) );
179  if ( !isUrl )
180  {
181  bool res = mbReader.open();
182  Q_UNUSED( res );
183  Q_ASSERT( res );
184  }
185 
186  if ( feedback && feedback->isCanceled() )
187  return {};
188 
189  QVector<QgsTileXYZ> tiles = QgsVectorTileUtils::tilesInRange( range, tileMatrix.zoomLevel() );
190 
191  // if a tile matrix results in a HUGE number of tile requests, we skip the sort -- it can be expensive
192  if ( tiles.size() < 10000 )
194 
195  rawTiles.reserve( tiles.size() );
196  for ( QgsTileXYZ id : std::as_const( tiles ) )
197  {
198  if ( feedback && feedback->isCanceled() )
199  return rawTiles;
200 
201  QByteArray rawData = isUrl ? loadFromNetwork( id, tileMatrix, sourcePath, authid, headers, feedback ) : loadFromMBTiles( id, mbReader, feedback );
202  if ( !rawData.isEmpty() )
203  {
204  rawTiles.append( QgsVectorTileRawData( id, rawData ) );
205  }
206  }
207  return rawTiles;
208 }
209 
210 QByteArray QgsVectorTileLoader::loadFromNetwork( const QgsTileXYZ &id, const QgsTileMatrix &tileMatrix, const QString &requestUrl, const QString &authid, const QgsHttpHeaders &headers, QgsFeedback *feedback )
211 {
212  QString url = QgsVectorTileUtils::formatXYZUrlTemplate( requestUrl, id, tileMatrix );
213  QNetworkRequest nr;
214  nr.setUrl( QUrl( url ) );
215 
216  headers.updateNetworkRequest( nr );
217 
219  req.setAuthCfg( authid );
220  QgsDebugMsgLevel( QStringLiteral( "Blocking request: " ) + url, 2 );
221  QgsBlockingNetworkRequest::ErrorCode errCode = req.get( nr, false, feedback );
222  if ( errCode != QgsBlockingNetworkRequest::NoError )
223  {
224  QgsDebugMsg( QStringLiteral( "Request failed: " ) + url );
225  return QByteArray();
226  }
227  QgsNetworkReplyContent reply = req.reply();
228  QgsDebugMsgLevel( QStringLiteral( "Request successful, content size %1" ).arg( reply.content().size() ), 2 );
229  return reply.content();
230 }
231 
232 
233 QByteArray QgsVectorTileLoader::loadFromMBTiles( const QgsTileXYZ &id, QgsMbTiles &mbTileReader, QgsFeedback *feedback )
234 {
235  // MBTiles uses TMS specs with Y starting at the bottom while XYZ uses Y starting at the top
236  int rowTMS = pow( 2, id.zoomLevel() ) - id.row() - 1;
237  QByteArray gzippedTileData = mbTileReader.tileData( id.zoomLevel(), id.column(), rowTMS );
238  if ( gzippedTileData.isEmpty() )
239  {
240  return QByteArray();
241  }
242 
243  if ( feedback && feedback->isCanceled() )
244  return QByteArray();
245 
246  QByteArray data;
247  if ( !QgsMbTiles::decodeGzip( gzippedTileData, data ) )
248  {
249  QgsDebugMsg( QStringLiteral( "Failed to decompress tile " ) + id.toString() );
250  return QByteArray();
251  }
252 
253  QgsDebugMsgLevel( QStringLiteral( "Tile blob size %1 -> uncompressed size %2" ).arg( gzippedTileData.size() ).arg( data.size() ), 2 );
254  return data;
255 }
static QgsTileDownloadManager * tileDownloadManager()
Returns the application's tile download manager, used for download of map tiles when rendering.
static QgsAuthManager * authManager()
Returns the application's authentication manager instance.
bool updateNetworkRequest(QNetworkRequest &request, const QString &authcfg, const QString &dataprovider=QString())
Provider call to update a QNetworkRequest with an authentication config.
A thread safe class for performing blocking (sync) network requests, with full support for QGIS proxy...
ErrorCode get(QNetworkRequest &request, bool forceRefresh=false, QgsFeedback *feedback=nullptr)
Performs a "get" operation on the specified request.
void setAuthCfg(const QString &authCfg)
Sets the authentication config id which should be used during the request.
@ NoError
No error was encountered.
QgsNetworkReplyContent reply() const
Returns the content of the network reply, after a get() or post() request has been made.
Base class for feedback objects to be used for cancellation of something running in a worker thread.
Definition: qgsfeedback.h:45
void canceled()
Internal routines can connect to this signal if they use event loop.
bool isCanceled() const SIP_HOLDGIL
Tells whether the operation has been canceled already.
Definition: qgsfeedback.h:54
This class implements simple http header management.
bool updateNetworkRequest(QNetworkRequest &request) const
Updates a request by adding all the HTTP headers.
Utility class for reading and writing MBTiles files (which are SQLite3 databases).
Definition: qgsmbtiles.h:39
bool open()
Tries to open the file, returns true on success.
Definition: qgsmbtiles.cpp:32
QByteArray tileData(int z, int x, int y)
Returns raw tile data for given tile.
Definition: qgsmbtiles.cpp:146
static bool decodeGzip(const QByteArray &bytesIn, QByteArray &bytesOut)
Decodes gzip byte stream, returns true on success. Useful for reading vector tiles.
Definition: qgsmbtiles.cpp:211
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::MessageLevel::Warning, bool notifyUser=true)
Adds a message to the log instance (and creates it if necessary).
Encapsulates a network reply within a container which is inexpensive to copy and safe to pass between...
QByteArray content() const
Returns the reply content.
Reply object for tile download manager requests returned from calls to QgsTileDownloadManager::get().
QString errorString() const
Returns error string (only valid when already finished)
QNetworkRequest request() const
Returns the original request for this reply object.
QByteArray data() const
Returns binary data returned in the reply (only valid when already finished)
QNetworkReply::NetworkError error() const
Returns error code (only valid when already finished)
void finished()
Emitted when the reply has finished (either with a success or with a failure)
QgsTileDownloadManagerReply * get(const QNetworkRequest &request)
Starts a request.
Defines a matrix of tiles for a single zoom level: it is defined by its size (width *.
Definition: qgstiles.h:108
int zoomLevel() const
Returns the zoom level of the tile matrix.
Definition: qgstiles.h:146
Range of tiles in a tile matrix to be rendered.
Definition: qgstiles.h:71
Stores coordinates of a tile in a tile matrix set.
Definition: qgstiles.h:38
void tileRequestFinished(const QgsVectorTileRawData &rawTile)
Emitted when a tile request has finished. If a tile request has failed, the returned raw tile byte ar...
static QList< QgsVectorTileRawData > blockingFetchTileRawData(const QString &sourceType, const QString &sourcePath, const QgsTileMatrix &tileMatrix, const QPointF &viewCenter, const QgsTileRange &range, const QString &authid, const QgsHttpHeaders &headers, QgsFeedback *feedback=nullptr)
Returns raw tile data for the specified range of tiles. Blocks the caller until all tiles are fetched...
QString error() const
Returns a eventual error that occurred during loading, void if no error.
static QByteArray loadFromNetwork(const QgsTileXYZ &id, const QgsTileMatrix &tileMatrix, const QString &requestUrl, const QString &authid, const QgsHttpHeaders &headers, QgsFeedback *feedback=nullptr)
Returns raw tile data for a single tile, doing a HTTP request. Block the caller until tile data are d...
static QByteArray loadFromMBTiles(const QgsTileXYZ &id, QgsMbTiles &mbTileReader, QgsFeedback *feedback=nullptr)
Returns raw tile data for a single tile loaded from MBTiles file.
void downloadBlocking()
Blocks the caller until all asynchronous requests are finished (with a success or a failure)
QgsVectorTileLoader(const QString &uri, const QgsTileMatrix &tileMatrix, const QgsTileRange &range, const QPointF &viewCenter, const QString &authid, const QgsHttpHeaders &headers, QgsFeedback *feedback)
Constructs tile loader for doing asynchronous requests and starts network requests.
Keeps track of raw tile data that need to be decoded.
static QString formatXYZUrlTemplate(const QString &url, QgsTileXYZ tile, const QgsTileMatrix &tileMatrix)
Returns formatted tile URL string replacing {x}, {y}, {z} placeholders (or {-y} instead of {y} for TM...
static QVector< QgsTileXYZ > tilesInRange(QgsTileRange range, int zoomLevel)
Returns a list of tiles in the given tile range.
static void sortTilesByDistanceFromCenter(QVector< QgsTileXYZ > &tiles, QPointF center)
Orders tile requests according to the distance from view center (given in tile matrix coords)
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
#define QgsSetRequestInitiatorClass(request, _class)
#define QgsSetRequestInitiatorId(request, str)