QGIS API Documentation  3.22.4-Białowieża (ce8e65e95e)
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 QString &referer, QgsFeedback *feedback )
32  : mEventLoop( new QEventLoop )
33  , mFeedback( feedback )
34  , mAuthCfg( authid )
35  , mReferer( referer )
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  if ( !mReferer.isEmpty() )
100  request.setRawHeader( "Referer", mReferer.toUtf8() );
101 
102  if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkRequest( request, mAuthCfg ) )
103  {
104  QgsMessageLog::logMessage( tr( "network request update failed for authentication config" ), tr( "Network" ) );
105  }
106 
108  connect( reply, &QgsTileDownloadManagerReply::finished, this, &QgsVectorTileLoader::tileReplyFinished );
109  mReplies << reply;
110 }
111 
112 void QgsVectorTileLoader::tileReplyFinished()
113 {
114  QgsTileDownloadManagerReply *reply = qobject_cast<QgsTileDownloadManagerReply *>( sender() );
115 
116  int reqX = reply->request().attribute( static_cast<QNetworkRequest::Attribute>( QNetworkRequest::User + 1 ) ).toInt();
117  int reqY = reply->request().attribute( static_cast<QNetworkRequest::Attribute>( QNetworkRequest::User + 2 ) ).toInt();
118  int reqZ = reply->request().attribute( static_cast<QNetworkRequest::Attribute>( QNetworkRequest::User + 3 ) ).toInt();
119  QgsTileXYZ tileID( reqX, reqY, reqZ );
120 
121  if ( reply->error() == QNetworkReply::NoError )
122  {
123  // TODO: handle redirections?
124 
125  QgsDebugMsgLevel( QStringLiteral( "Tile download successful: " ) + tileID.toString(), 2 );
126  QByteArray rawData = reply->data();
127  mReplies.removeOne( reply );
128  reply->deleteLater();
129 
130  emit tileRequestFinished( QgsVectorTileRawData( tileID, rawData ) );
131  }
132  else
133  {
134  QgsDebugMsg( QStringLiteral( "Tile download failed! " ) + reply->errorString() );
135  mReplies.removeOne( reply );
136  reply->deleteLater();
137 
138  emit tileRequestFinished( QgsVectorTileRawData( tileID, QByteArray() ) );
139  }
140 
141  if ( mReplies.isEmpty() )
142  {
143  // exist the event loop
144  QMetaObject::invokeMethod( mEventLoop.get(), "quit", Qt::QueuedConnection );
145  }
146 }
147 
148 void QgsVectorTileLoader::canceled()
149 {
150  QgsDebugMsgLevel( QStringLiteral( "Canceling %1 pending requests" ).arg( mReplies.count() ), 2 );
151  qDeleteAll( mReplies );
152  mReplies.clear();
153 
154  // stop blocking download
155  mEventLoop->quit();
156 
157 }
158 
160 
161 QList<QgsVectorTileRawData> QgsVectorTileLoader::blockingFetchTileRawData( const QString &sourceType, const QString &sourcePath, const QgsTileMatrix &tileMatrix, const QPointF &viewCenter, const QgsTileRange &range, const QString &authid, const QString &referer )
162 {
163  QList<QgsVectorTileRawData> rawTiles;
164 
165  QgsMbTiles mbReader( sourcePath );
166  bool isUrl = ( sourceType == QLatin1String( "xyz" ) );
167  if ( !isUrl )
168  {
169  bool res = mbReader.open();
170  Q_UNUSED( res );
171  Q_ASSERT( res );
172  }
173 
174  QVector<QgsTileXYZ> tiles = QgsVectorTileUtils::tilesInRange( range, tileMatrix.zoomLevel() );
176  for ( QgsTileXYZ id : std::as_const( tiles ) )
177  {
178  QByteArray rawData = isUrl ? loadFromNetwork( id, tileMatrix, sourcePath, authid, referer ) : loadFromMBTiles( id, mbReader );
179  if ( !rawData.isEmpty() )
180  {
181  rawTiles.append( QgsVectorTileRawData( id, rawData ) );
182  }
183  }
184  return rawTiles;
185 }
186 
187 QByteArray QgsVectorTileLoader::loadFromNetwork( const QgsTileXYZ &id, const QgsTileMatrix &tileMatrix, const QString &requestUrl, const QString &authid, const QString &referer )
188 {
189  QString url = QgsVectorTileUtils::formatXYZUrlTemplate( requestUrl, id, tileMatrix );
190  QNetworkRequest nr;
191  nr.setUrl( QUrl( url ) );
192 
193  if ( !referer.isEmpty() )
194  nr.setRawHeader( "Referer", referer.toUtf8() );
195 
197  req.setAuthCfg( authid );
198  QgsDebugMsgLevel( QStringLiteral( "Blocking request: " ) + url, 2 );
199  QgsBlockingNetworkRequest::ErrorCode errCode = req.get( nr );
200  if ( errCode != QgsBlockingNetworkRequest::NoError )
201  {
202  QgsDebugMsg( QStringLiteral( "Request failed: " ) + url );
203  return QByteArray();
204  }
205  QgsNetworkReplyContent reply = req.reply();
206  QgsDebugMsgLevel( QStringLiteral( "Request successful, content size %1" ).arg( reply.content().size() ), 2 );
207  return reply.content();
208 }
209 
210 
211 QByteArray QgsVectorTileLoader::loadFromMBTiles( const QgsTileXYZ &id, QgsMbTiles &mbTileReader )
212 {
213  // MBTiles uses TMS specs with Y starting at the bottom while XYZ uses Y starting at the top
214  int rowTMS = pow( 2, id.zoomLevel() ) - id.row() - 1;
215  QByteArray gzippedTileData = mbTileReader.tileData( id.zoomLevel(), id.column(), rowTMS );
216  if ( gzippedTileData.isEmpty() )
217  {
218  QgsDebugMsg( QStringLiteral( "Failed to get tile " ) + id.toString() );
219  return QByteArray();
220  }
221 
222  QByteArray data;
223  if ( !QgsMbTiles::decodeGzip( gzippedTileData, data ) )
224  {
225  QgsDebugMsg( QStringLiteral( "Failed to decompress tile " ) + id.toString() );
226  return QByteArray();
227  }
228 
229  QgsDebugMsgLevel( QStringLiteral( "Tile blob size %1 -> uncompressed size %2" ).arg( gzippedTileData.size() ).arg( data.size() ), 2 );
230  return data;
231 }
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
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:210
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:104
int zoomLevel() const
Returns zoom level of the tile matrix.
Definition: qgstiles.h:122
Range of tiles in a tile matrix to be rendered.
Definition: qgstiles.h:67
Stores coordinates of a tile in a tile matrix set.
Definition: qgstiles.h:34
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 QByteArray loadFromMBTiles(const QgsTileXYZ &id, QgsMbTiles &mbTileReader)
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)
static QList< QgsVectorTileRawData > blockingFetchTileRawData(const QString &sourceType, const QString &sourcePath, const QgsTileMatrix &tileMatrix, const QPointF &viewCenter, const QgsTileRange &range, const QString &authid, const QString &referer)
Returns raw tile data for the specified range of tiles. Blocks the caller until all tiles are fetched...
static QByteArray loadFromNetwork(const QgsTileXYZ &id, const QgsTileMatrix &tileMatrix, const QString &requestUrl, const QString &authid, const QString &referer)
Returns raw tile data for a single tile, doing a HTTP request. Block the caller until tile data are d...
QgsVectorTileLoader(const QString &uri, const QgsTileMatrix &tileMatrix, const QgsTileRange &range, const QPointF &viewCenter, const QString &authid, const QString &referer, 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 void sortTilesByDistanceFromCenter(QVector< QgsTileXYZ > &tiles, const QPointF &center)
Orders tile requests according to the distance from view center (given in tile matrix coords)
static QVector< QgsTileXYZ > tilesInRange(const QgsTileRange &range, int zoomLevel)
Returns a list of tiles in the given tile range.
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
#define QgsSetRequestInitiatorClass(request, _class)
#define QgsSetRequestInitiatorId(request, str)