1 /***************************************************************************
2   qgsvectortilelayerrenderer.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 "qgsvectortilelayerrenderer.h"
17 
18 #include <QElapsedTimer>
19 
20 #include "qgsexpressioncontextutils.h"
21 #include "qgsfeedback.h"
22 #include "qgslogger.h"
23 
24 #include "qgsvectortilemvtdecoder.h"
25 #include "qgsvectortilelayer.h"
26 #include "qgsvectortileloader.h"
27 #include "qgsvectortileutils.h"
28 
29 #include "qgslabelingengine.h"
30 #include "qgsvectortilelabeling.h"
31 #include "qgsmapclippingutils.h"
32 #include "qgsrendercontext.h"
33 
QgsVectorTileLayerRenderer(QgsVectorTileLayer * layer,QgsRenderContext & context)34 QgsVectorTileLayerRenderer::QgsVectorTileLayerRenderer( QgsVectorTileLayer *layer, QgsRenderContext &context )
35   : QgsMapLayerRenderer( layer->id(), &context )
36   , mSourceType( layer->sourceType() )
37   , mSourcePath( layer->sourcePath() )
38   , mSourceMinZoom( layer->sourceMinZoom() )
39   , mSourceMaxZoom( layer->sourceMaxZoom() )
40   , mRenderer( layer->renderer()->clone() )
41   , mDrawTileBoundaries( layer->isTileBorderRenderingEnabled() )
42   , mFeedback( new QgsFeedback )
43   , mLayerOpacity( layer->opacity() )
44 {
45 
46   QgsDataSourceUri dsUri;
47   dsUri.setEncodedUri( layer->source() );
48   mAuthCfg = dsUri.authConfigId();
49   mReferer = dsUri.param( QStringLiteral( "referer" ) );
50 
51   if ( QgsLabelingEngine *engine = context.labelingEngine() )
52   {
53     if ( layer->labeling() )
54     {
55       mLabelProvider = layer->labeling()->provider( layer );
56       if ( mLabelProvider )
57       {
58         engine->addProvider( mLabelProvider );
59       }
60     }
61   }
62 
63   mClippingRegions = QgsMapClippingUtils::collectClippingRegionsForLayer( *renderContext(), layer );
64 }
65 
render()66 bool QgsVectorTileLayerRenderer::render()
67 {
68   QgsRenderContext &ctx = *renderContext();
69 
70   if ( ctx.renderingStopped() )
71     return false;
72 
73   const QgsScopedQPainterState painterState( ctx.painter() );
74 
75   if ( !mClippingRegions.empty() )
76   {
77     bool needsPainterClipPath = false;
78     const QPainterPath path = QgsMapClippingUtils::calculatePainterClipRegion( mClippingRegions, *renderContext(), QgsMapLayerType::VectorTileLayer, needsPainterClipPath );
79     if ( needsPainterClipPath )
80       renderContext()->painter()->setClipPath( path, Qt::IntersectClip );
81   }
82 
83   QElapsedTimer tTotal;
84   tTotal.start();
85 
86   QgsDebugMsgLevel( QStringLiteral( "Vector tiles rendering extent: " ) + ctx.extent().toString( -1 ), 2 );
87   QgsDebugMsgLevel( QStringLiteral( "Vector tiles map scale 1 : %1" ).arg( ctx.rendererScale() ), 2 );
88 
89   mTileZoom = QgsVectorTileUtils::scaleToZoomLevel( ctx.rendererScale(), mSourceMinZoom, mSourceMaxZoom );
90   QgsDebugMsgLevel( QStringLiteral( "Vector tiles zoom level: %1" ).arg( mTileZoom ), 2 );
91 
92   mTileMatrix = QgsTileMatrix::fromWebMercator( mTileZoom );
93 
94   mTileRange = mTileMatrix.tileRangeFromExtent( ctx.extent() );
95   QgsDebugMsgLevel( QStringLiteral( "Vector tiles range X: %1 - %2  Y: %3 - %4" )
96                     .arg( mTileRange.startColumn() ).arg( mTileRange.endColumn() )
97                     .arg( mTileRange.startRow() ).arg( mTileRange.endRow() ), 2 );
98 
99   // view center is used to sort the order of tiles for fetching and rendering
100   const QPointF viewCenter = mTileMatrix.mapToTileCoordinates( ctx.extent().center() );
101 
102   if ( !mTileRange.isValid() )
103   {
104     QgsDebugMsgLevel( QStringLiteral( "Vector tiles - outside of range" ), 2 );
105     return true;   // nothing to do
106   }
107 
108   const bool isAsync = ( mSourceType == QLatin1String( "xyz" ) );
109 
110   std::unique_ptr<QgsVectorTileLoader> asyncLoader;
111   QList<QgsVectorTileRawData> rawTiles;
112   if ( !isAsync )
113   {
114     QElapsedTimer tFetch;
115     tFetch.start();
116     rawTiles = QgsVectorTileLoader::blockingFetchTileRawData( mSourceType, mSourcePath, mTileMatrix, viewCenter, mTileRange, mAuthCfg, mReferer );
117     QgsDebugMsgLevel( QStringLiteral( "Tile fetching time: %1" ).arg( tFetch.elapsed() / 1000. ), 2 );
118     QgsDebugMsgLevel( QStringLiteral( "Fetched tiles: %1" ).arg( rawTiles.count() ), 2 );
119   }
120   else
121   {
122     asyncLoader.reset( new QgsVectorTileLoader( mSourcePath, mTileMatrix, mTileRange, viewCenter, mAuthCfg, mReferer, mFeedback.get() ) );
123     QObject::connect( asyncLoader.get(), &QgsVectorTileLoader::tileRequestFinished, asyncLoader.get(), [this]( const QgsVectorTileRawData & rawTile )
124     {
125       QgsDebugMsgLevel( QStringLiteral( "Got tile asynchronously: " ) + rawTile.id.toString(), 2 );
126       if ( !rawTile.data.isEmpty() )
127         decodeAndDrawTile( rawTile );
128     } );
129   }
130 
131   if ( ctx.renderingStopped() )
132     return false;
133 
134   // add @zoom_level variable which can be used in styling
135   QgsExpressionContextScope *scope = new QgsExpressionContextScope( QObject::tr( "Tiles" ) ); // will be deleted by popper
136   scope->setVariable( QStringLiteral( "zoom_level" ), mTileZoom, true );
137   scope->setVariable( QStringLiteral( "vector_tile_zoom" ), QgsVectorTileUtils::scaleToZoom( ctx.rendererScale() ), true );
138   const QgsExpressionContextScopePopper popper( ctx.expressionContext(), scope );
139 
140   mRenderer->startRender( *renderContext(), mTileZoom, mTileRange );
141 
142   QMap<QString, QSet<QString> > requiredFields = mRenderer->usedAttributes( ctx );
143 
144   if ( mLabelProvider )
145   {
146     const QMap<QString, QSet<QString> > requiredFieldsLabeling = mLabelProvider->usedAttributes( ctx, mTileZoom );
147     for ( auto it = requiredFieldsLabeling.begin(); it != requiredFieldsLabeling.end(); ++it )
148     {
149       requiredFields[it.key()].unite( it.value() );
150     }
151   }
152 
153   for ( auto it = requiredFields.constBegin(); it != requiredFields.constEnd(); ++it )
154     mPerLayerFields[it.key()] = QgsVectorTileUtils::makeQgisFields( it.value() );
155 
156   mRequiredLayers = mRenderer->requiredLayers( ctx, mTileZoom );
157 
158   if ( mLabelProvider )
159   {
160     mLabelProvider->setFields( mPerLayerFields );
161     QSet<QString> attributeNames;  // we don't need this - already got referenced columns in provider constructor
162     if ( !mLabelProvider->prepare( ctx, attributeNames ) )
163     {
164       ctx.labelingEngine()->removeProvider( mLabelProvider );
165       mLabelProvider = nullptr; // provider is deleted by the engine
166     }
167 
168     mRequiredLayers.unite( mLabelProvider->requiredLayers( ctx, mTileZoom ) );
169   }
170 
171   if ( !isAsync )
172   {
173     for ( const QgsVectorTileRawData &rawTile : rawTiles )
174     {
175       if ( ctx.renderingStopped() )
176         break;
177 
178       decodeAndDrawTile( rawTile );
179     }
180   }
181   else
182   {
183     // Block until tiles are fetched and rendered. If the rendering gets canceled at some point,
184     // the async loader will catch the signal, abort requests and return from downloadBlocking()
185     asyncLoader->downloadBlocking();
186   }
187 
188   mRenderer->stopRender( ctx );
189 
190   QgsDebugMsgLevel( QStringLiteral( "Total time for decoding: %1" ).arg( mTotalDecodeTime / 1000. ), 2 );
191   QgsDebugMsgLevel( QStringLiteral( "Drawing time: %1" ).arg( mTotalDrawTime / 1000. ), 2 );
192   QgsDebugMsgLevel( QStringLiteral( "Total time: %1" ).arg( tTotal.elapsed() / 1000. ), 2 );
193 
194   return !ctx.renderingStopped();
195 }
196 
forceRasterRender() const197 bool QgsVectorTileLayerRenderer::forceRasterRender() const
198 {
199   return renderContext()->testFlag( Qgis::RenderContextFlag::UseAdvancedEffects ) && ( !qgsDoubleNear( mLayerOpacity, 1.0 ) );
200 }
201 
decodeAndDrawTile(const QgsVectorTileRawData & rawTile)202 void QgsVectorTileLayerRenderer::decodeAndDrawTile( const QgsVectorTileRawData &rawTile )
203 {
204   QgsRenderContext &ctx = *renderContext();
205 
206   QgsDebugMsgLevel( QStringLiteral( "Drawing tile " ) + rawTile.id.toString(), 2 );
207 
208   QElapsedTimer tLoad;
209   tLoad.start();
210 
211   // currently only MVT encoding supported
212   QgsVectorTileMVTDecoder decoder;
213   if ( !decoder.decode( rawTile.id, rawTile.data ) )
214   {
215     QgsDebugMsgLevel( QStringLiteral( "Failed to parse raw tile data! " ) + rawTile.id.toString(), 2 );
216     return;
217   }
218 
219   if ( ctx.renderingStopped() )
220     return;
221 
222   const QgsCoordinateTransform ct = ctx.coordinateTransform();
223 
224   QgsVectorTileRendererData tile( rawTile.id );
225   tile.setFields( mPerLayerFields );
226   tile.setFeatures( decoder.layerFeatures( mPerLayerFields, ct, &mRequiredLayers ) );
227 
228   try
229   {
230     tile.setTilePolygon( QgsVectorTileUtils::tilePolygon( rawTile.id, ct, mTileMatrix, ctx.mapToPixel() ) );
231   }
232   catch ( QgsCsException & )
233   {
234     QgsDebugMsgLevel( QStringLiteral( "Failed to generate tile polygon " ) + rawTile.id.toString(), 2 );
235     return;
236   }
237 
238   mTotalDecodeTime += tLoad.elapsed();
239 
240   // calculate tile polygon in screen coordinates
241 
242   if ( ctx.renderingStopped() )
243     return;
244 
245   // set up clipping so that rendering does not go behind tile's extent
246   const QgsScopedQPainterState savePainterState( ctx.painter() );
247   // we have to intersect with any existing painter clip regions, or we risk overwriting valid clip
248   // regions setup outside of the vector tile renderer (e.g. layout map clip region)
249   ctx.painter()->setClipRegion( QRegion( tile.tilePolygon() ), Qt::IntersectClip );
250 
251   QElapsedTimer tDraw;
252   tDraw.start();
253 
254   mRenderer->renderTile( tile, ctx );
255   mTotalDrawTime += tDraw.elapsed();
256 
257   if ( mLabelProvider )
258     mLabelProvider->registerTileFeatures( tile, ctx );
259 
260   if ( mDrawTileBoundaries )
261   {
262     const QgsScopedQPainterState savePainterState( ctx.painter() );
263     ctx.painter()->setClipping( false );
264 
265     QPen pen( Qt::red );
266     pen.setWidth( 3 );
267     ctx.painter()->setPen( pen );
268     ctx.painter()->drawPolygon( tile.tilePolygon() );
269   }
270 }
271