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