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