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
20 #include "qgsblockingnetworkrequest.h"
21 #include "qgslogger.h"
22 #include "qgsmbtiles.h"
23 #include "qgsnetworkaccessmanager.h"
24 #include "qgsvectortileutils.h"
25 #include "qgsapplication.h"
26 #include "qgsauthmanager.h"
27 #include "qgsmessagelog.h"
28
QgsVectorTileLoader(const QString & uri,const QgsTileMatrix & tileMatrix,const QgsTileRange & range,const QPointF & viewCenter,const QString & authid,const QString & referer,QgsFeedback * feedback)29 QgsVectorTileLoader::QgsVectorTileLoader( const QString &uri, const QgsTileMatrix &tileMatrix, const QgsTileRange &range, const QPointF &viewCenter, const QString &authid, const QString &referer, QgsFeedback *feedback )
30 : mEventLoop( new QEventLoop )
31 , mFeedback( feedback )
32 , mAuthCfg( authid )
33 , mReferer( referer )
34 {
35 if ( feedback )
36 {
37 connect( feedback, &QgsFeedback::canceled, this, &QgsVectorTileLoader::canceled, Qt::QueuedConnection );
38
39 // rendering could have been canceled before we started to listen to canceled() signal
40 // so let's check before doing the download and maybe quit prematurely
41 if ( feedback->isCanceled() )
42 return;
43 }
44
45 QgsDebugMsgLevel( QStringLiteral( "Starting network loader" ), 2 );
46 QVector<QgsTileXYZ> tiles = QgsVectorTileUtils::tilesInRange( range, tileMatrix.zoomLevel() );
47 QgsVectorTileUtils::sortTilesByDistanceFromCenter( tiles, viewCenter );
48 for ( QgsTileXYZ id : qgis::as_const( tiles ) )
49 {
50 loadFromNetworkAsync( id, tileMatrix, uri );
51 }
52 }
53
~QgsVectorTileLoader()54 QgsVectorTileLoader::~QgsVectorTileLoader()
55 {
56 QgsDebugMsgLevel( QStringLiteral( "Terminating network loader" ), 2 );
57
58 if ( !mReplies.isEmpty() )
59 {
60 // this can happen when the loader is terminated without getting requests finalized
61 // (e.g. downloadBlocking() was not called)
62 canceled();
63 }
64 }
65
downloadBlocking()66 void QgsVectorTileLoader::downloadBlocking()
67 {
68 if ( mFeedback && mFeedback->isCanceled() )
69 {
70 QgsDebugMsgLevel( QStringLiteral( "downloadBlocking - not staring event loop - canceled" ), 2 );
71 return; // nothing to do
72 }
73
74 QgsDebugMsgLevel( QStringLiteral( "Starting event loop with %1 requests" ).arg( mReplies.count() ), 2 );
75
76 mEventLoop->exec( QEventLoop::ExcludeUserInputEvents );
77
78 QgsDebugMsgLevel( QStringLiteral( "downloadBlocking finished" ), 2 );
79
80 Q_ASSERT( mReplies.isEmpty() );
81 }
82
loadFromNetworkAsync(const QgsTileXYZ & id,const QgsTileMatrix & tileMatrix,const QString & requestUrl)83 void QgsVectorTileLoader::loadFromNetworkAsync( const QgsTileXYZ &id, const QgsTileMatrix &tileMatrix, const QString &requestUrl )
84 {
85 QString url = QgsVectorTileUtils::formatXYZUrlTemplate( requestUrl, id, tileMatrix );
86 QNetworkRequest request( url );
87 QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsVectorTileLoader" ) );
88 QgsSetRequestInitiatorId( request, id.toString() );
89
90 request.setAttribute( static_cast<QNetworkRequest::Attribute>( QNetworkRequest::User + 1 ), id.column() );
91 request.setAttribute( static_cast<QNetworkRequest::Attribute>( QNetworkRequest::User + 2 ), id.row() );
92 request.setAttribute( static_cast<QNetworkRequest::Attribute>( QNetworkRequest::User + 3 ), id.zoomLevel() );
93
94 request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
95 request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
96
97 if ( !mReferer.isEmpty() )
98 request.setRawHeader( "Referer", mReferer.toUtf8() );
99
100 if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkRequest( request, mAuthCfg ) )
101 {
102 QgsMessageLog::logMessage( tr( "network request update failed for authentication config" ), tr( "Network" ) );
103 }
104
105 QNetworkReply *reply = QgsNetworkAccessManager::instance()->get( request );
106 connect( reply, &QNetworkReply::finished, this, &QgsVectorTileLoader::tileReplyFinished );
107
108 mReplies << reply;
109 }
110
tileReplyFinished()111 void QgsVectorTileLoader::tileReplyFinished()
112 {
113 QNetworkReply *reply = qobject_cast<QNetworkReply *>( 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->readAll();
126 mReplies.removeOne( reply );
127 reply->deleteLater();
128
129 emit tileRequestFinished( QgsVectorTileRawData( tileID, rawData ) );
130 }
131 else
132 {
133 QgsDebugMsg( QStringLiteral( "Tile download failed! " ) + reply->errorString() );
134 mReplies.removeOne( reply );
135 reply->deleteLater();
136
137 emit tileRequestFinished( QgsVectorTileRawData( tileID, QByteArray() ) );
138 }
139
140 if ( mReplies.isEmpty() )
141 {
142 // exist the event loop
143 QMetaObject::invokeMethod( mEventLoop.get(), "quit", Qt::QueuedConnection );
144 }
145 }
146
canceled()147 void QgsVectorTileLoader::canceled()
148 {
149 QgsDebugMsgLevel( QStringLiteral( "Canceling %1 pending requests" ).arg( mReplies.count() ), 2 );
150 const QList<QNetworkReply *> replies = mReplies;
151 for ( QNetworkReply *reply : replies )
152 {
153 reply->abort();
154 }
155 }
156
157 //////
158
blockingFetchTileRawData(const QString & sourceType,const QString & sourcePath,const QgsTileMatrix & tileMatrix,const QPointF & viewCenter,const QgsTileRange & range,const QString & authid,const QString & referer)159 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 )
160 {
161 QList<QgsVectorTileRawData> rawTiles;
162
163 QgsMbTiles mbReader( sourcePath );
164 bool isUrl = ( sourceType == QLatin1String( "xyz" ) );
165 if ( !isUrl )
166 {
167 bool res = mbReader.open();
168 Q_UNUSED( res );
169 Q_ASSERT( res );
170 }
171
172 QVector<QgsTileXYZ> tiles = QgsVectorTileUtils::tilesInRange( range, tileMatrix.zoomLevel() );
173 QgsVectorTileUtils::sortTilesByDistanceFromCenter( tiles, viewCenter );
174 for ( QgsTileXYZ id : qgis::as_const( tiles ) )
175 {
176 QByteArray rawData = isUrl ? loadFromNetwork( id, tileMatrix, sourcePath, authid, referer ) : loadFromMBTiles( id, mbReader );
177 if ( !rawData.isEmpty() )
178 {
179 rawTiles.append( QgsVectorTileRawData( id, rawData ) );
180 }
181 }
182 return rawTiles;
183 }
184
loadFromNetwork(const QgsTileXYZ & id,const QgsTileMatrix & tileMatrix,const QString & requestUrl,const QString & authid,const QString & referer)185 QByteArray QgsVectorTileLoader::loadFromNetwork( const QgsTileXYZ &id, const QgsTileMatrix &tileMatrix, const QString &requestUrl, const QString &authid, const QString &referer )
186 {
187 QString url = QgsVectorTileUtils::formatXYZUrlTemplate( requestUrl, id, tileMatrix );
188 QNetworkRequest nr;
189 nr.setUrl( QUrl( url ) );
190
191 if ( !referer.isEmpty() )
192 nr.setRawHeader( "Referer", referer.toUtf8() );
193
194 QgsBlockingNetworkRequest req;
195 req.setAuthCfg( authid );
196 QgsDebugMsgLevel( QStringLiteral( "Blocking request: " ) + url, 2 );
197 QgsBlockingNetworkRequest::ErrorCode errCode = req.get( nr );
198 if ( errCode != QgsBlockingNetworkRequest::NoError )
199 {
200 QgsDebugMsg( QStringLiteral( "Request failed: " ) + url );
201 return QByteArray();
202 }
203 QgsNetworkReplyContent reply = req.reply();
204 QgsDebugMsgLevel( QStringLiteral( "Request successful, content size %1" ).arg( reply.content().size() ), 2 );
205 return reply.content();
206 }
207
208
loadFromMBTiles(const QgsTileXYZ & id,QgsMbTiles & mbTileReader)209 QByteArray QgsVectorTileLoader::loadFromMBTiles( const QgsTileXYZ &id, QgsMbTiles &mbTileReader )
210 {
211 // MBTiles uses TMS specs with Y starting at the bottom while XYZ uses Y starting at the top
212 int rowTMS = pow( 2, id.zoomLevel() ) - id.row() - 1;
213 QByteArray gzippedTileData = mbTileReader.tileData( id.zoomLevel(), id.column(), rowTMS );
214 if ( gzippedTileData.isEmpty() )
215 {
216 QgsDebugMsg( QStringLiteral( "Failed to get tile " ) + id.toString() );
217 return QByteArray();
218 }
219
220 QByteArray data;
221 if ( !QgsMbTiles::decodeGzip( gzippedTileData, data ) )
222 {
223 QgsDebugMsg( QStringLiteral( "Failed to decompress tile " ) + id.toString() );
224 return QByteArray();
225 }
226
227 QgsDebugMsgLevel( QStringLiteral( "Tile blob size %1 -> uncompressed size %2" ).arg( gzippedTileData.size() ).arg( data.size() ), 2 );
228 return data;
229 }
230