Branch data Line data Source code
1 : : /***************************************************************************
2 : : qgsmaprendererjob.cpp
3 : : --------------------------------------
4 : : Date : December 2013
5 : : Copyright : (C) 2013 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 "qgsmaprendererjob.h"
17 : :
18 : : #include <QPainter>
19 : : #include <QElapsedTimer>
20 : : #include <QTimer>
21 : : #include <QtConcurrentMap>
22 : :
23 : : #include "qgslogger.h"
24 : : #include "qgsrendercontext.h"
25 : : #include "qgsmaplayer.h"
26 : : #include "qgsproject.h"
27 : : #include "qgsmaplayerrenderer.h"
28 : : #include "qgsmaplayerstylemanager.h"
29 : : #include "qgsmaprenderercache.h"
30 : : #include "qgsmessagelog.h"
31 : : #include "qgspallabeling.h"
32 : : #include "qgsexception.h"
33 : : #include "qgslabelingengine.h"
34 : : #include "qgsmaplayerlistutils.h"
35 : : #include "qgsvectorlayerlabeling.h"
36 : : #include "qgssettings.h"
37 : : #include "qgsexpressioncontextutils.h"
38 : : #include "qgssymbol.h"
39 : : #include "qgsrenderer.h"
40 : : #include "qgssymbollayer.h"
41 : : #include "qgsvectorlayerutils.h"
42 : : #include "qgssymbollayerutils.h"
43 : : #include "qgsmaplayertemporalproperties.h"
44 : : #include "qgsmaplayerelevationproperties.h"
45 : : #include "qgsvectorlayerrenderer.h"
46 : :
47 : : ///@cond PRIVATE
48 : :
49 : 10 : const QString QgsMapRendererJob::LABEL_CACHE_ID = QStringLiteral( "_labels_" );
50 : 10 : const QString QgsMapRendererJob::LABEL_PREVIEW_CACHE_ID = QStringLiteral( "_preview_labels_" );
51 : :
52 : 0 : bool LayerRenderJob::imageCanBeComposed() const
53 : : {
54 : 0 : if ( imageInitialized )
55 : : {
56 : 0 : if ( renderer )
57 : : {
58 : 0 : return renderer->isReadyToCompose();
59 : : }
60 : : else
61 : : {
62 : 0 : return true;
63 : : }
64 : : }
65 : : else
66 : : {
67 : 0 : return false;
68 : : }
69 : 0 : }
70 : :
71 : 0 : QgsMapRendererJob::QgsMapRendererJob( const QgsMapSettings &settings )
72 : 0 : : mSettings( settings )
73 : :
74 : 0 : {
75 : 0 : }
76 : :
77 : :
78 : 0 : QgsMapRendererQImageJob::QgsMapRendererQImageJob( const QgsMapSettings &settings )
79 : 0 : : QgsMapRendererJob( settings )
80 : 0 : {
81 : 0 : }
82 : :
83 : :
84 : 0 : QgsMapRendererJob::Errors QgsMapRendererJob::errors() const
85 : : {
86 : 0 : return mErrors;
87 : : }
88 : :
89 : 0 : void QgsMapRendererJob::setCache( QgsMapRendererCache *cache )
90 : : {
91 : 0 : mCache = cache;
92 : 0 : }
93 : :
94 : 0 : QHash<QgsMapLayer *, int> QgsMapRendererJob::perLayerRenderingTime() const
95 : : {
96 : 0 : QHash<QgsMapLayer *, int> result;
97 : 0 : for ( auto it = mPerLayerRenderingTime.constBegin(); it != mPerLayerRenderingTime.constEnd(); ++it )
98 : : {
99 : 0 : if ( auto &&lKey = it.key() )
100 : 0 : result.insert( lKey, it.value() );
101 : 0 : }
102 : 0 : return result;
103 : 0 : }
104 : :
105 : 0 : void QgsMapRendererJob::setLayerRenderingTimeHints( const QHash<QString, int> &hints )
106 : : {
107 : 0 : mLayerRenderingTimeHints = hints;
108 : 0 : }
109 : :
110 : 0 : const QgsMapSettings &QgsMapRendererJob::mapSettings() const
111 : : {
112 : 0 : return mSettings;
113 : : }
114 : :
115 : 0 : bool QgsMapRendererJob::prepareLabelCache() const
116 : : {
117 : 0 : bool canCache = mCache;
118 : :
119 : : // calculate which layers will be labeled
120 : 0 : QSet< QgsMapLayer * > labeledLayers;
121 : 0 : const QList<QgsMapLayer *> layers = mSettings.layers();
122 : 0 : for ( QgsMapLayer *ml : layers )
123 : : {
124 : 0 : if ( QgsPalLabeling::staticWillUseLayer( ml ) )
125 : 0 : labeledLayers << ml;
126 : :
127 : 0 : switch ( ml->type() )
128 : : {
129 : : case QgsMapLayerType::VectorLayer:
130 : : {
131 : 0 : QgsVectorLayer *vl = qobject_cast< QgsVectorLayer *>( ml );
132 : 0 : if ( vl->labelsEnabled() && vl->labeling()->requiresAdvancedEffects() )
133 : : {
134 : 0 : canCache = false;
135 : 0 : }
136 : 0 : break;
137 : : }
138 : :
139 : : case QgsMapLayerType::VectorTileLayer:
140 : : {
141 : : // TODO -- add detection of advanced labeling effects for vector tile layers
142 : 0 : break;
143 : : }
144 : :
145 : : case QgsMapLayerType::RasterLayer:
146 : : case QgsMapLayerType::AnnotationLayer:
147 : : case QgsMapLayerType::PluginLayer:
148 : : case QgsMapLayerType::MeshLayer:
149 : : case QgsMapLayerType::PointCloudLayer:
150 : 0 : break;
151 : : }
152 : :
153 : 0 : if ( !canCache )
154 : 0 : break;
155 : :
156 : : }
157 : :
158 : 0 : if ( mCache && mCache->hasCacheImage( LABEL_CACHE_ID ) )
159 : : {
160 : : // we may need to clear label cache and re-register labeled features - check for that here
161 : :
162 : : // can we reuse the cached label solution?
163 : 0 : bool canUseCache = canCache && qgis::listToSet( mCache->dependentLayers( LABEL_CACHE_ID ) ) == labeledLayers;
164 : 0 : if ( !canUseCache )
165 : : {
166 : : // no - participating layers have changed
167 : 0 : mCache->clearCacheImage( LABEL_CACHE_ID );
168 : 0 : }
169 : 0 : }
170 : 0 : return canCache;
171 : 0 : }
172 : :
173 : :
174 : 0 : bool QgsMapRendererJob::reprojectToLayerExtent( const QgsMapLayer *ml, const QgsCoordinateTransform &ct, QgsRectangle &extent, QgsRectangle &r2 )
175 : : {
176 : 0 : bool res = true;
177 : : // we can safely use ballpark transforms without bothering the user here -- at the likely scale of layer extents there
178 : : // won't be an appreciable difference, and we aren't actually transforming any rendered points here anyway (just the layer extent)
179 : 0 : QgsCoordinateTransform approxTransform = ct;
180 : 0 : approxTransform.setBallparkTransformsAreAppropriate( true );
181 : :
182 : : try
183 : : {
184 : : #ifdef QGISDEBUG
185 : : // QgsLogger::debug<QgsRectangle>("Getting extent of canvas in layers CS. Canvas is ", extent, __FILE__, __FUNCTION__, __LINE__);
186 : : #endif
187 : : // Split the extent into two if the source CRS is
188 : : // geographic and the extent crosses the split in
189 : : // geographic coordinates (usually +/- 180 degrees,
190 : : // and is assumed to be so here), and draw each
191 : : // extent separately.
192 : : static const double SPLIT_COORD = 180.0;
193 : :
194 : 0 : if ( ml->crs().isGeographic() )
195 : : {
196 : 0 : if ( ml->type() == QgsMapLayerType::VectorLayer && !approxTransform.destinationCrs().isGeographic() )
197 : : {
198 : : // if we transform from a projected coordinate system check
199 : : // check if transforming back roughly returns the input
200 : : // extend - otherwise render the world.
201 : 0 : QgsRectangle extent1 = approxTransform.transformBoundingBox( extent, QgsCoordinateTransform::ReverseTransform );
202 : 0 : QgsRectangle extent2 = approxTransform.transformBoundingBox( extent1, QgsCoordinateTransform::ForwardTransform );
203 : :
204 : 0 : QgsDebugMsgLevel( QStringLiteral( "\n0:%1 %2x%3\n1:%4\n2:%5 %6x%7 (w:%8 h:%9)" )
205 : : .arg( extent.toString() ).arg( extent.width() ).arg( extent.height() )
206 : : .arg( extent1.toString(), extent2.toString() ).arg( extent2.width() ).arg( extent2.height() )
207 : : .arg( std::fabs( 1.0 - extent2.width() / extent.width() ) )
208 : : .arg( std::fabs( 1.0 - extent2.height() / extent.height() ) )
209 : : , 3 );
210 : :
211 : : // can differ by a maximum of up to 20% of height/width
212 : 0 : if ( qgsDoubleNear( extent2.xMinimum(), extent.xMinimum(), extent.width() * 0.2 )
213 : 0 : && qgsDoubleNear( extent2.xMaximum(), extent.xMaximum(), extent.width() * 0.2 )
214 : 0 : && qgsDoubleNear( extent2.yMinimum(), extent.yMinimum(), extent.height() * 0.2 )
215 : 0 : && qgsDoubleNear( extent2.yMaximum(), extent.yMaximum(), extent.height() * 0.2 )
216 : : )
217 : : {
218 : 0 : extent = extent1;
219 : 0 : }
220 : : else
221 : : {
222 : 0 : extent = QgsRectangle( -180.0, -90.0, 180.0, 90.0 );
223 : 0 : res = false;
224 : : }
225 : 0 : }
226 : : else
227 : : {
228 : : // Note: ll = lower left point
229 : 0 : QgsPointXY ll = approxTransform.transform( extent.xMinimum(), extent.yMinimum(),
230 : : QgsCoordinateTransform::ReverseTransform );
231 : :
232 : : // and ur = upper right point
233 : 0 : QgsPointXY ur = approxTransform.transform( extent.xMaximum(), extent.yMaximum(),
234 : : QgsCoordinateTransform::ReverseTransform );
235 : :
236 : 0 : QgsDebugMsgLevel( QStringLiteral( "in:%1 (ll:%2 ur:%3)" ).arg( extent.toString(), ll.toString(), ur.toString() ), 4 );
237 : :
238 : 0 : extent = approxTransform.transformBoundingBox( extent, QgsCoordinateTransform::ReverseTransform );
239 : :
240 : 0 : QgsDebugMsgLevel( QStringLiteral( "out:%1 (w:%2 h:%3)" ).arg( extent.toString() ).arg( extent.width() ).arg( extent.height() ), 4 );
241 : :
242 : 0 : if ( ll.x() > ur.x() )
243 : : {
244 : : // the coordinates projected in reverse order than what one would expect.
245 : : // we are probably looking at an area that includes longitude of 180 degrees.
246 : : // we need to take into account coordinates from two intervals: (-180,x1) and (x2,180)
247 : : // so let's use (-180,180). This hopefully does not add too much overhead. It is
248 : : // more straightforward than rendering with two separate extents and more consistent
249 : : // for rendering, labeling and caching as everything is rendered just in one go
250 : 0 : extent.setXMinimum( -SPLIT_COORD );
251 : 0 : extent.setXMaximum( SPLIT_COORD );
252 : 0 : res = false;
253 : 0 : }
254 : : }
255 : :
256 : : // TODO: the above rule still does not help if using a projection that covers the whole
257 : : // world. E.g. with EPSG:3857 the longitude spectrum -180 to +180 is mapped to approx.
258 : : // -2e7 to +2e7. Converting extent from -5e7 to +5e7 is transformed as -90 to +90,
259 : : // but in fact the extent should cover the whole world.
260 : 0 : }
261 : : else // can't cross 180
262 : : {
263 : 0 : if ( approxTransform.destinationCrs().isGeographic() &&
264 : 0 : ( extent.xMinimum() <= -180 || extent.xMaximum() >= 180 ||
265 : 0 : extent.yMinimum() <= -90 || extent.yMaximum() >= 90 ) )
266 : : // Use unlimited rectangle because otherwise we may end up transforming wrong coordinates.
267 : : // E.g. longitude -200 to +160 would be understood as +40 to +160 due to periodicity.
268 : : // We could try to clamp coords to (-180,180) for lon resp. (-90,90) for lat,
269 : : // but this seems like a safer choice.
270 : : {
271 : 0 : extent = QgsRectangle( std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest(), std::numeric_limits<double>::max(), std::numeric_limits<double>::max() );
272 : 0 : res = false;
273 : 0 : }
274 : : else
275 : 0 : extent = approxTransform.transformBoundingBox( extent, QgsCoordinateTransform::ReverseTransform );
276 : : }
277 : 0 : }
278 : : catch ( QgsCsException & )
279 : : {
280 : 0 : QgsDebugMsg( QStringLiteral( "Transform error caught" ) );
281 : 0 : extent = QgsRectangle( std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest(), std::numeric_limits<double>::max(), std::numeric_limits<double>::max() );
282 : 0 : r2 = QgsRectangle( std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest(), std::numeric_limits<double>::max(), std::numeric_limits<double>::max() );
283 : 0 : res = false;
284 : 0 : }
285 : :
286 : 0 : return res;
287 : 0 : }
288 : :
289 : 0 : QImage *QgsMapRendererJob::allocateImage( QString layerId )
290 : : {
291 : 0 : QImage *image = new QImage( mSettings.deviceOutputSize(),
292 : 0 : mSettings.outputImageFormat() );
293 : 0 : image->setDevicePixelRatio( static_cast<qreal>( mSettings.devicePixelRatio() ) );
294 : 0 : if ( image->isNull() )
295 : : {
296 : 0 : mErrors.append( Error( layerId, tr( "Insufficient memory for image %1x%2" ).arg( mSettings.outputSize().width() ).arg( mSettings.outputSize().height() ) ) );
297 : 0 : delete image;
298 : 0 : return nullptr;
299 : : }
300 : 0 : return image;
301 : 0 : }
302 : :
303 : 0 : QPainter *QgsMapRendererJob::allocateImageAndPainter( QString layerId, QImage *&image )
304 : : {
305 : 0 : QPainter *painter = nullptr;
306 : 0 : image = allocateImage( layerId );
307 : 0 : if ( image )
308 : : {
309 : 0 : painter = new QPainter( image );
310 : 0 : painter->setRenderHint( QPainter::Antialiasing, mSettings.testFlag( QgsMapSettings::Antialiasing ) );
311 : : #if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
312 : 0 : painter->setRenderHint( QPainter::LosslessImageRendering, mSettings.testFlag( QgsMapSettings::LosslessImageRendering ) );
313 : : #endif
314 : 0 : }
315 : 0 : return painter;
316 : 0 : }
317 : :
318 : 0 : LayerRenderJobs QgsMapRendererJob::prepareJobs( QPainter *painter, QgsLabelingEngine *labelingEngine2, bool deferredPainterSet )
319 : : {
320 : 0 : LayerRenderJobs layerJobs;
321 : :
322 : : // render all layers in the stack, starting at the base
323 : 0 : QListIterator<QgsMapLayer *> li( mSettings.layers() );
324 : 0 : li.toBack();
325 : :
326 : 0 : if ( mCache )
327 : : {
328 : 0 : bool cacheValid = mCache->updateParameters( mSettings.visibleExtent(), mSettings.mapToPixel() );
329 : : Q_UNUSED( cacheValid )
330 : 0 : QgsDebugMsgLevel( QStringLiteral( "CACHE VALID: %1" ).arg( cacheValid ), 4 );
331 : 0 : }
332 : :
333 : 0 : bool requiresLabelRedraw = !( mCache && mCache->hasCacheImage( LABEL_CACHE_ID ) );
334 : :
335 : 0 : while ( li.hasPrevious() )
336 : : {
337 : 0 : QgsMapLayer *ml = li.previous();
338 : :
339 : 0 : QgsDebugMsgLevel( QStringLiteral( "layer %1: minscale:%2 maxscale:%3 scaledepvis:%4 blendmode:%5 isValid:%6" )
340 : : .arg( ml->name() )
341 : : .arg( ml->minimumScale() )
342 : : .arg( ml->maximumScale() )
343 : : .arg( ml->hasScaleBasedVisibility() )
344 : : .arg( ml->blendMode() )
345 : : .arg( ml->isValid() )
346 : : , 3 );
347 : :
348 : 0 : if ( !ml->isValid() )
349 : : {
350 : 0 : QgsDebugMsgLevel( QStringLiteral( "Invalid Layer skipped" ), 3 );
351 : 0 : continue;
352 : : }
353 : :
354 : 0 : if ( !ml->isInScaleRange( mSettings.scale() ) ) //|| mOverview )
355 : : {
356 : 0 : QgsDebugMsgLevel( QStringLiteral( "Layer not rendered because it is not within the defined visibility scale range" ), 3 );
357 : 0 : continue;
358 : : }
359 : :
360 : 0 : if ( mSettings.isTemporal() && ml->temporalProperties() && !ml->temporalProperties()->isVisibleInTemporalRange( mSettings.temporalRange() ) )
361 : : {
362 : 0 : QgsDebugMsgLevel( QStringLiteral( "Layer not rendered because it is not visible within the map's time range" ), 3 );
363 : 0 : continue;
364 : : }
365 : :
366 : 0 : if ( !mSettings.zRange().isInfinite() && ml->elevationProperties() && !ml->elevationProperties()->isVisibleInZRange( mSettings.zRange() ) )
367 : : {
368 : 0 : QgsDebugMsgLevel( QStringLiteral( "Layer not rendered because it is not visible within the map's z range" ), 3 );
369 : 0 : continue;
370 : : }
371 : :
372 : 0 : QgsRectangle r1 = mSettings.visibleExtent(), r2;
373 : 0 : r1.grow( mSettings.extentBuffer() );
374 : 0 : QgsCoordinateTransform ct;
375 : :
376 : 0 : ct = mSettings.layerTransform( ml );
377 : 0 : bool haveExtentInLayerCrs = true;
378 : 0 : if ( ct.isValid() )
379 : : {
380 : 0 : haveExtentInLayerCrs = reprojectToLayerExtent( ml, ct, r1, r2 );
381 : 0 : }
382 : 0 : QgsDebugMsgLevel( "extent: " + r1.toString(), 3 );
383 : 0 : if ( !r1.isFinite() || !r2.isFinite() )
384 : : {
385 : 0 : mErrors.append( Error( ml->id(), tr( "There was a problem transforming the layer's extent. Layer skipped." ) ) );
386 : 0 : continue;
387 : : }
388 : :
389 : 0 : QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( ml );
390 : :
391 : : // Force render of layers that are being edited
392 : : // or if there's a labeling engine that needs the layer to register features
393 : 0 : if ( mCache )
394 : : {
395 : 0 : const bool requiresLabeling = ( labelingEngine2 && QgsPalLabeling::staticWillUseLayer( ml ) ) && requiresLabelRedraw;
396 : 0 : if ( ( vl && vl->isEditable() ) || requiresLabeling )
397 : : {
398 : 0 : mCache->clearCacheImage( ml->id() );
399 : 0 : }
400 : 0 : }
401 : :
402 : 0 : layerJobs.append( LayerRenderJob() );
403 : 0 : LayerRenderJob &job = layerJobs.last();
404 : 0 : job.cached = false;
405 : 0 : job.img = nullptr;
406 : 0 : job.layer = ml;
407 : 0 : job.layerId = ml->id();
408 : 0 : job.estimatedRenderingTime = mLayerRenderingTimeHints.value( ml->id(), 0 );
409 : 0 : job.renderingTime = -1;
410 : :
411 : 0 : job.context = QgsRenderContext::fromMapSettings( mSettings );
412 : 0 : job.context.expressionContext().appendScope( QgsExpressionContextUtils::layerScope( ml ) );
413 : 0 : job.context.setPainter( painter );
414 : 0 : job.context.setLabelingEngine( labelingEngine2 );
415 : 0 : job.context.setCoordinateTransform( ct );
416 : 0 : job.context.setExtent( r1 );
417 : 0 : if ( !haveExtentInLayerCrs )
418 : 0 : job.context.setFlag( QgsRenderContext::ApplyClipAfterReprojection, true );
419 : :
420 : 0 : if ( mFeatureFilterProvider )
421 : 0 : job.context.setFeatureFilterProvider( mFeatureFilterProvider );
422 : :
423 : 0 : QgsMapLayerStyleOverride styleOverride( ml );
424 : 0 : if ( mSettings.layerStyleOverrides().contains( ml->id() ) )
425 : 0 : styleOverride.setOverrideStyle( mSettings.layerStyleOverrides().value( ml->id() ) );
426 : :
427 : 0 : job.blendMode = ml->blendMode();
428 : :
429 : : // raster layer opacity is handled directly within the raster layer renderer, so don't
430 : : // apply default opacity handling here!
431 : 0 : job.opacity = ml->type() != QgsMapLayerType::RasterLayer ? ml->opacity() : 1.0;
432 : :
433 : : // if we can use the cache, let's do it and avoid rendering!
434 : 0 : if ( mCache && mCache->hasCacheImage( ml->id() ) )
435 : : {
436 : 0 : job.cached = true;
437 : 0 : job.imageInitialized = true;
438 : 0 : job.img = new QImage( mCache->cacheImage( ml->id() ) );
439 : 0 : job.img->setDevicePixelRatio( static_cast<qreal>( mSettings.devicePixelRatio() ) );
440 : 0 : job.renderer = nullptr;
441 : 0 : job.context.setPainter( nullptr );
442 : 0 : continue;
443 : : }
444 : :
445 : 0 : QElapsedTimer layerTime;
446 : 0 : layerTime.start();
447 : 0 : job.renderer = ml->createMapRenderer( job.context );
448 : 0 : if ( job.renderer )
449 : 0 : job.renderer->setLayerRenderingTimeHint( job.estimatedRenderingTime );
450 : :
451 : : // If we are drawing with an alternative blending mode then we need to render to a separate image
452 : : // before compositing this on the map. This effectively flattens the layer and prevents
453 : : // blending occurring between objects on the layer
454 : 0 : if ( mCache || ( !painter && !deferredPainterSet ) || ( job.renderer && job.renderer->forceRasterRender() ) )
455 : : {
456 : : // Flattened image for drawing when a blending mode is set
457 : 0 : job.context.setPainter( allocateImageAndPainter( ml->id(), job.img ) );
458 : 0 : if ( ! job.img )
459 : : {
460 : 0 : delete job.renderer;
461 : 0 : job.renderer = nullptr;
462 : 0 : layerJobs.removeLast();
463 : 0 : continue;
464 : : }
465 : 0 : }
466 : :
467 : 0 : job.renderingTime = layerTime.elapsed(); // include job preparation time in layer rendering time
468 : 0 : }
469 : :
470 : 0 : return layerJobs;
471 : 0 : }
472 : :
473 : 0 : LayerRenderJobs QgsMapRendererJob::prepareSecondPassJobs( LayerRenderJobs &firstPassJobs, LabelRenderJob &labelJob )
474 : : {
475 : 0 : LayerRenderJobs secondPassJobs;
476 : :
477 : : // We will need to quickly access the associated rendering job of a layer
478 : 0 : QHash<QString, LayerRenderJob *> layerJobMapping;
479 : :
480 : 0 : // ... and whether a layer has a mask defined
481 : 0 : QSet<QString> layerHasMask;
482 : :
483 : 0 : struct MaskSource
484 : : {
485 : : QString layerId;
486 : : QString labelRuleId;
487 : : int labelMaskId;
488 : 0 : MaskSource( const QString &layerId_, const QString &labelRuleId_, int labelMaskId_ ):
489 : 0 : layerId( layerId_ ), labelRuleId( labelRuleId_ ), labelMaskId( labelMaskId_ ) {}
490 : : };
491 : :
492 : : // We collect for each layer, the set of symbol layers that will be "masked"
493 : : // and the list of source layers that have a mask
494 : 0 : QHash<QString, QPair<QSet<QgsSymbolLayerId>, QList<MaskSource>>> maskedSymbolLayers;
495 : :
496 : 0 : for ( LayerRenderJob &job : firstPassJobs )
497 : : {
498 : 0 : QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( job.layer );
499 : 0 : if ( ! vl )
500 : 0 : continue;
501 : :
502 : 0 : layerJobMapping[job.layer->id()] = &job;
503 : :
504 : : // lambda function to factor code for both label masks and symbol layer masks
505 : 0 : auto collectMasks = [&]( QHash<QString, QSet<QgsSymbolLayerId>> *masks, QString sourceLayerId, QString ruleId = QString(), int labelMaskId = -1 )
506 : : {
507 : 0 : for ( auto it = masks->begin(); it != masks->end(); ++it )
508 : : {
509 : 0 : auto lit = maskedSymbolLayers.find( it.key() );
510 : 0 : if ( lit == maskedSymbolLayers.end() )
511 : : {
512 : 0 : maskedSymbolLayers[it.key()] = qMakePair( it.value(), QList<MaskSource>() << MaskSource( sourceLayerId, ruleId, labelMaskId ) );
513 : 0 : }
514 : : else
515 : : {
516 : 0 : if ( lit->first != it.value() )
517 : : {
518 : 0 : QgsLogger::warning( QStringLiteral( "Layer %1 : Different sets of symbol layers are masked by different sources ! Only one (arbitrary) set will be retained !" ).arg( it.key() ) );
519 : 0 : continue;
520 : : }
521 : 0 : lit->second.push_back( MaskSource( sourceLayerId, ruleId, labelMaskId ) );
522 : : }
523 : 0 : }
524 : 0 : if ( ! masks->isEmpty() )
525 : 0 : layerHasMask.insert( sourceLayerId );
526 : 0 : };
527 : :
528 : : // collect label masks
529 : 0 : QHash<QString, QHash<QString, QSet<QgsSymbolLayerId>>> labelMasks = QgsVectorLayerUtils::labelMasks( vl );
530 : 0 : for ( auto it = labelMasks.begin(); it != labelMasks.end(); it++ )
531 : : {
532 : 0 : QString labelRule = it.key();
533 : 0 : QHash<QString, QSet<QgsSymbolLayerId>> masks = it.value();
534 : :
535 : : // group layers by QSet<QgsSymbolLayerReference>
536 : 0 : QSet<QgsSymbolLayerReference> slRefs;
537 : 0 : for ( auto mit = masks.begin(); mit != masks.end(); mit++ )
538 : : {
539 : 0 : for ( auto slIt = mit.value().begin(); slIt != mit.value().end(); slIt++ )
540 : : {
541 : 0 : slRefs.insert( QgsSymbolLayerReference( mit.key(), *slIt ) );
542 : 0 : }
543 : 0 : }
544 : : // generate a new mask id for this set
545 : 0 : int labelMaskId = labelJob.maskIdProvider.insertLabelLayer( vl->id(), it.key(), slRefs );
546 : :
547 : : // now collect masks
548 : 0 : collectMasks( &masks, vl->id(), labelRule, labelMaskId );
549 : 0 : }
550 : :
551 : : // collect symbol layer masks
552 : 0 : QHash<QString, QSet<QgsSymbolLayerId>> symbolLayerMasks = QgsVectorLayerUtils::symbolLayerMasks( vl );
553 : 0 : collectMasks( &symbolLayerMasks, vl->id() );
554 : 0 : }
555 : :
556 : 0 : if ( maskedSymbolLayers.isEmpty() )
557 : 0 : return secondPassJobs;
558 : :
559 : : // Now that we know some layers have a mask, we have to allocate a mask image and painter
560 : : // for them in the first pass job
561 : 0 : for ( LayerRenderJob &job : firstPassJobs )
562 : : {
563 : 0 : QgsMapLayer *ml = job.layer;
564 : :
565 : 0 : if ( job.img == nullptr )
566 : : {
567 : 0 : job.context.setPainter( allocateImageAndPainter( ml->id(), job.img ) );
568 : 0 : }
569 : 0 : if ( layerHasMask.contains( ml->id() ) )
570 : : {
571 : : // Note: we only need an alpha channel here, rather than a full RGBA image
572 : 0 : job.context.setMaskPainter( allocateImageAndPainter( ml->id(), job.maskImage ) );
573 : 0 : job.maskImage->fill( 0 );
574 : 0 : }
575 : : }
576 : :
577 : : // Allocate an image for labels
578 : 0 : if ( labelJob.img == nullptr )
579 : : {
580 : 0 : labelJob.img = allocateImage( QStringLiteral( "labels" ) );
581 : 0 : }
582 : :
583 : : // Prepare label mask images
584 : 0 : for ( int maskId = 0; maskId < labelJob.maskIdProvider.size(); maskId++ )
585 : : {
586 : : QImage *maskImage;
587 : 0 : labelJob.context.setMaskPainter( allocateImageAndPainter( QStringLiteral( "label mask" ), maskImage ), maskId );
588 : 0 : maskImage->fill( 0 );
589 : 0 : labelJob.maskImages.push_back( maskImage );
590 : 0 : }
591 : 0 : labelJob.context.setMaskIdProvider( &labelJob.maskIdProvider );
592 : :
593 : : // Prepare second pass jobs
594 : 0 : for ( LayerRenderJob &job : firstPassJobs )
595 : : {
596 : 0 : QgsMapLayer *ml = job.layer;
597 : :
598 : 0 : auto it = maskedSymbolLayers.find( ml->id() );
599 : 0 : if ( it == maskedSymbolLayers.end() )
600 : 0 : continue;
601 : :
602 : 0 : QList<MaskSource> &sourceList = it->second;
603 : 0 : const QSet<QgsSymbolLayerId> &symbolList = it->first;
604 : :
605 : : // copy the initial job ...
606 : 0 : secondPassJobs.append( LayerRenderJob() );
607 : 0 : LayerRenderJob &job2 = secondPassJobs.last();
608 : 0 : job2 = job;
609 : 0 : job2.cached = false;
610 : 0 : job2.firstPassJob = &job;
611 : 0 : QgsVectorLayer *vl1 = qobject_cast<QgsVectorLayer *>( job.layer );
612 : :
613 : : // ... but clear the image
614 : 0 : job2.context.setMaskPainter( nullptr );
615 : 0 : job2.context.setPainter( allocateImageAndPainter( vl1->id(), job2.img ) );
616 : 0 : if ( ! job2.img )
617 : : {
618 : 0 : secondPassJobs.removeLast();
619 : 0 : continue;
620 : : }
621 : :
622 : : // Points to the first pass job. This will be needed during the second pass composition.
623 : 0 : for ( MaskSource &source : sourceList )
624 : : {
625 : 0 : if ( source.labelMaskId != -1 )
626 : 0 : job2.maskJobs.push_back( qMakePair( nullptr, source.labelMaskId ) );
627 : : else
628 : 0 : job2.maskJobs.push_back( qMakePair( layerJobMapping[source.layerId], -1 ) );
629 : : }
630 : :
631 : : // FIXME: another possibility here, to avoid allocating a new map renderer and reuse the one from
632 : : // the first pass job, would be to be able to call QgsMapLayerRenderer::render() with a QgsRenderContext.
633 : 0 : QgsVectorLayerRenderer *mapRenderer = static_cast<QgsVectorLayerRenderer *>( vl1->createMapRenderer( job2.context ) );
634 : 0 : job2.renderer = mapRenderer;
635 : :
636 : : // Modify the render context so that symbol layers get disabled as needed.
637 : : // The map renderer stores a reference to the context, so we can modify it even after the map renderer creation (what we need here)
638 : 0 : job2.context.setDisabledSymbolLayers( QgsSymbolLayerUtils::toSymbolLayerPointers( mapRenderer->featureRenderer(), symbolList ) );
639 : : }
640 : :
641 : 0 : return secondPassJobs;
642 : 0 : }
643 : :
644 : 0 : LabelRenderJob QgsMapRendererJob::prepareLabelingJob( QPainter *painter, QgsLabelingEngine *labelingEngine2, bool canUseLabelCache )
645 : : {
646 : 0 : LabelRenderJob job;
647 : 0 : job.context = QgsRenderContext::fromMapSettings( mSettings );
648 : 0 : job.context.setPainter( painter );
649 : 0 : job.context.setLabelingEngine( labelingEngine2 );
650 : 0 : job.context.setExtent( mSettings.visibleExtent() );
651 : 0 : job.context.setFeatureFilterProvider( mFeatureFilterProvider );
652 : 0 : QgsCoordinateTransform ct;
653 : 0 : ct.setDestinationCrs( mSettings.destinationCrs() );
654 : 0 : job.context.setCoordinateTransform( ct );
655 : :
656 : : // if we can use the cache, let's do it and avoid rendering!
657 : 0 : bool hasCache = canUseLabelCache && mCache && mCache->hasCacheImage( LABEL_CACHE_ID );
658 : 0 : if ( hasCache )
659 : : {
660 : 0 : job.cached = true;
661 : 0 : job.complete = true;
662 : 0 : job.img = new QImage( mCache->cacheImage( LABEL_CACHE_ID ) );
663 : : Q_ASSERT( job.img->devicePixelRatio() == mSettings.devicePixelRatio() );
664 : 0 : job.context.setPainter( nullptr );
665 : 0 : }
666 : : else
667 : : {
668 : 0 : if ( canUseLabelCache && ( mCache || !painter ) )
669 : : {
670 : 0 : job.img = allocateImage( QStringLiteral( "labels" ) );
671 : 0 : }
672 : : }
673 : :
674 : 0 : return job;
675 : 0 : }
676 : :
677 : :
678 : 0 : void QgsMapRendererJob::cleanupJobs( LayerRenderJobs &jobs )
679 : : {
680 : 0 : for ( LayerRenderJobs::iterator it = jobs.begin(); it != jobs.end(); ++it )
681 : : {
682 : 0 : LayerRenderJob &job = *it;
683 : 0 : if ( job.img )
684 : : {
685 : 0 : delete job.context.painter();
686 : 0 : job.context.setPainter( nullptr );
687 : :
688 : 0 : if ( mCache && !job.cached && job.completed && job.layer )
689 : : {
690 : 0 : QgsDebugMsgLevel( QStringLiteral( "caching image for %1" ).arg( job.layerId ), 2 );
691 : 0 : mCache->setCacheImageWithParameters( job.layerId, *job.img, mSettings.visibleExtent(), mSettings.mapToPixel(), QList< QgsMapLayer * >() << job.layer );
692 : 0 : mCache->setCacheImageWithParameters( job.layerId + QStringLiteral( "_preview" ), *job.img, mSettings.visibleExtent(), mSettings.mapToPixel(), QList< QgsMapLayer * >() << job.layer );
693 : 0 : }
694 : :
695 : 0 : delete job.img;
696 : 0 : job.img = nullptr;
697 : 0 : }
698 : :
699 : : // delete the mask image and painter
700 : 0 : if ( job.maskImage )
701 : : {
702 : 0 : delete job.context.maskPainter();
703 : 0 : job.context.setMaskPainter( nullptr );
704 : 0 : delete job.maskImage;
705 : 0 : }
706 : :
707 : 0 : if ( job.renderer )
708 : : {
709 : 0 : const auto constErrors = job.renderer->errors();
710 : 0 : for ( const QString &message : constErrors )
711 : 0 : mErrors.append( Error( job.renderer->layerId(), message ) );
712 : :
713 : 0 : delete job.renderer;
714 : 0 : job.renderer = nullptr;
715 : 0 : }
716 : :
717 : 0 : if ( job.layer )
718 : 0 : mPerLayerRenderingTime.insert( job.layer, job.renderingTime );
719 : 0 : }
720 : :
721 : 0 : jobs.clear();
722 : 0 : }
723 : :
724 : 0 : void QgsMapRendererJob::cleanupSecondPassJobs( LayerRenderJobs &jobs )
725 : : {
726 : 0 : for ( auto &job : jobs )
727 : : {
728 : 0 : if ( job.img )
729 : : {
730 : 0 : delete job.context.painter();
731 : 0 : job.context.setPainter( nullptr );
732 : :
733 : 0 : delete job.img;
734 : 0 : job.img = nullptr;
735 : 0 : }
736 : :
737 : 0 : if ( job.renderer )
738 : : {
739 : 0 : delete job.renderer;
740 : 0 : job.renderer = nullptr;
741 : 0 : }
742 : :
743 : 0 : if ( job.layer )
744 : 0 : mPerLayerRenderingTime.insert( job.layer, job.renderingTime );
745 : : }
746 : :
747 : 0 : jobs.clear();
748 : 0 : }
749 : :
750 : 0 : void QgsMapRendererJob::cleanupLabelJob( LabelRenderJob &job )
751 : : {
752 : 0 : if ( job.img )
753 : : {
754 : 0 : if ( mCache && !job.cached && !job.context.renderingStopped() )
755 : : {
756 : 0 : QgsDebugMsgLevel( QStringLiteral( "caching label result image" ), 2 );
757 : 0 : mCache->setCacheImageWithParameters( LABEL_CACHE_ID, *job.img, mSettings.visibleExtent(), mSettings.mapToPixel(), _qgis_listQPointerToRaw( job.participatingLayers ) );
758 : 0 : mCache->setCacheImageWithParameters( LABEL_PREVIEW_CACHE_ID, *job.img, mSettings.visibleExtent(), mSettings.mapToPixel(), _qgis_listQPointerToRaw( job.participatingLayers ) );
759 : 0 : }
760 : :
761 : 0 : delete job.img;
762 : 0 : job.img = nullptr;
763 : 0 : }
764 : :
765 : 0 : for ( int maskId = 0; maskId < job.maskImages.size(); maskId++ )
766 : : {
767 : 0 : delete job.context.maskPainter( maskId );
768 : 0 : job.context.setMaskPainter( nullptr, maskId );
769 : 0 : delete job.maskImages[maskId];
770 : 0 : }
771 : 0 : }
772 : :
773 : :
774 : : #define DEBUG_RENDERING 0
775 : :
776 : 0 : QImage QgsMapRendererJob::composeImage(
777 : : const QgsMapSettings &settings,
778 : : const LayerRenderJobs &jobs,
779 : : const LabelRenderJob &labelJob,
780 : : const QgsMapRendererCache *cache
781 : : )
782 : : {
783 : 0 : QImage image( settings.deviceOutputSize(), settings.outputImageFormat() );
784 : 0 : image.setDevicePixelRatio( settings.devicePixelRatio() );
785 : 0 : image.setDotsPerMeterX( static_cast<int>( settings.outputDpi() * 39.37 ) );
786 : 0 : image.setDotsPerMeterY( static_cast<int>( settings.outputDpi() * 39.37 ) );
787 : 0 : image.fill( settings.backgroundColor().rgba() );
788 : :
789 : 0 : QPainter painter( &image );
790 : :
791 : : #if DEBUG_RENDERING
792 : : int i = 0;
793 : : #endif
794 : 0 : for ( LayerRenderJobs::const_iterator it = jobs.constBegin(); it != jobs.constEnd(); ++it )
795 : : {
796 : 0 : const LayerRenderJob &job = *it;
797 : :
798 : 0 : if ( job.layer && job.layer->customProperty( QStringLiteral( "rendering/renderAboveLabels" ) ).toBool() )
799 : 0 : continue; // skip layer for now, it will be rendered after labels
800 : :
801 : 0 : QImage img = layerImageToBeComposed( settings, job, cache );
802 : 0 : if ( img.isNull() )
803 : 0 : continue; // image is not prepared and not even in cache
804 : :
805 : 0 : painter.setCompositionMode( job.blendMode );
806 : 0 : painter.setOpacity( job.opacity );
807 : :
808 : : #if DEBUG_RENDERING
809 : : img.save( QString( "/tmp/final_%1.png" ).arg( i ) );
810 : : i++;
811 : : #endif
812 : :
813 : 0 : painter.drawImage( 0, 0, img );
814 : 0 : }
815 : :
816 : : // IMPORTANT - don't draw labelJob img before the label job is complete,
817 : : // as the image is uninitialized and full of garbage before the label job
818 : : // commences
819 : 0 : if ( labelJob.img && labelJob.complete )
820 : : {
821 : 0 : painter.setCompositionMode( QPainter::CompositionMode_SourceOver );
822 : 0 : painter.setOpacity( 1.0 );
823 : 0 : painter.drawImage( 0, 0, *labelJob.img );
824 : 0 : }
825 : : // when checking for a label cache image, we only look for those which would be drawn between 30% and 300% of the
826 : : // original size. We don't want to draw massive pixelated labels on top of everything else, and we also don't need
827 : : // to draw tiny unreadable labels... better to draw nothing in this case and wait till the updated label results are ready!
828 : 0 : else if ( cache && cache->hasAnyCacheImage( LABEL_PREVIEW_CACHE_ID, 0.3, 3 ) )
829 : : {
830 : 0 : const QImage labelCacheImage = cache->transformedCacheImage( LABEL_PREVIEW_CACHE_ID, settings.mapToPixel() );
831 : 0 : painter.setCompositionMode( QPainter::CompositionMode_SourceOver );
832 : 0 : painter.setOpacity( 1.0 );
833 : 0 : painter.drawImage( 0, 0, labelCacheImage );
834 : 0 : }
835 : :
836 : : // render any layers with the renderAboveLabels flag now
837 : 0 : for ( LayerRenderJobs::const_iterator it = jobs.constBegin(); it != jobs.constEnd(); ++it )
838 : : {
839 : 0 : const LayerRenderJob &job = *it;
840 : :
841 : 0 : if ( !job.layer || !job.layer->customProperty( QStringLiteral( "rendering/renderAboveLabels" ) ).toBool() )
842 : 0 : continue;
843 : :
844 : 0 : QImage img = layerImageToBeComposed( settings, job, cache );
845 : 0 : if ( img.isNull() )
846 : 0 : continue; // image is not prepared and not even in cache
847 : :
848 : 0 : painter.setCompositionMode( job.blendMode );
849 : 0 : painter.setOpacity( job.opacity );
850 : :
851 : 0 : painter.drawImage( 0, 0, img );
852 : 0 : }
853 : :
854 : 0 : painter.end();
855 : : #if DEBUG_RENDERING
856 : : image.save( "/tmp/final.png" );
857 : : #endif
858 : 0 : return image;
859 : 0 : }
860 : :
861 : 0 : QImage QgsMapRendererJob::layerImageToBeComposed(
862 : : const QgsMapSettings &settings,
863 : : const LayerRenderJob &job,
864 : : const QgsMapRendererCache *cache
865 : : )
866 : : {
867 : 0 : if ( job.imageCanBeComposed() )
868 : : {
869 : : Q_ASSERT( job.img );
870 : 0 : return *job.img;
871 : : }
872 : : else
873 : : {
874 : 0 : if ( cache && cache->hasAnyCacheImage( job.layerId + QStringLiteral( "_preview" ) ) )
875 : : {
876 : 0 : return cache->transformedCacheImage( job.layerId + QStringLiteral( "_preview" ), settings.mapToPixel() );
877 : : }
878 : : else
879 : 0 : return QImage();
880 : : }
881 : 0 : }
882 : :
883 : 0 : void QgsMapRendererJob::composeSecondPass( LayerRenderJobs &secondPassJobs, LabelRenderJob &labelJob )
884 : : {
885 : : #if DEBUG_RENDERING
886 : : int i = 0;
887 : : #endif
888 : : // compose the second pass with the mask
889 : 0 : for ( LayerRenderJob &job : secondPassJobs )
890 : : {
891 : : #if DEBUG_RENDERING
892 : : i++;
893 : : job.img->save( QString( "/tmp/second_%1.png" ).arg( i ) );
894 : : int mask = 0;
895 : : #endif
896 : :
897 : : // Merge all mask images into the first one if we have more than one mask image
898 : 0 : if ( job.maskJobs.size() > 1 )
899 : : {
900 : 0 : QPainter *maskPainter = nullptr;
901 : 0 : for ( QPair<LayerRenderJob *, int> p : job.maskJobs )
902 : : {
903 : 0 : QImage *maskImage = p.first ? p.first->maskImage : labelJob.maskImages[p.second];
904 : : #if DEBUG_RENDERING
905 : : maskImage->save( QString( "/tmp/mask_%1_%2.png" ).arg( i ).arg( mask++ ) );
906 : : #endif
907 : 0 : if ( ! maskPainter )
908 : : {
909 : 0 : maskPainter = p.first ? p.first->context.maskPainter() : labelJob.context.maskPainter( p.second );
910 : 0 : }
911 : : else
912 : : {
913 : 0 : maskPainter->drawImage( 0, 0, *maskImage );
914 : : }
915 : : }
916 : 0 : }
917 : :
918 : 0 : if ( ! job.maskJobs.isEmpty() )
919 : : {
920 : : // All have been merged into the first
921 : 0 : QPair<LayerRenderJob *, int> p = *job.maskJobs.begin();
922 : 0 : QImage *maskImage = p.first ? p.first->maskImage : labelJob.maskImages[p.second];
923 : : #if DEBUG_RENDERING
924 : : maskImage->save( QString( "/tmp/mask_%1.png" ).arg( i ) );
925 : : #endif
926 : :
927 : : // Only retain parts of the second rendering that are "inside" the mask image
928 : 0 : QPainter *painter = job.context.painter();
929 : 0 : painter->setCompositionMode( QPainter::CompositionMode_DestinationIn );
930 : :
931 : : //Create an "alpha binarized" image of the maskImage to :
932 : : //* Eliminate antialiasing artifact
933 : : //* Avoid applying mask opacity to elements under the mask but not masked
934 : 0 : QImage maskBinAlpha = maskImage->createMaskFromColor( 0 );
935 : 0 : QVector<QRgb> mswTable;
936 : 0 : mswTable.push_back( qRgba( 0, 0, 0, 255 ) );
937 : 0 : mswTable.push_back( qRgba( 0, 0, 0, 0 ) );
938 : 0 : maskBinAlpha.setColorTable( mswTable );
939 : 0 : painter->drawImage( 0, 0, maskBinAlpha );
940 : : #if DEBUG_RENDERING
941 : : job.img->save( QString( "/tmp/second_%1_a.png" ).arg( i ) );
942 : : #endif
943 : :
944 : : // Modify the first pass' image ...
945 : : {
946 : 0 : QPainter tempPainter;
947 : :
948 : : // reuse the first pass painter, if available
949 : 0 : QPainter *painter1 = job.firstPassJob->context.painter();
950 : 0 : if ( ! painter1 )
951 : : {
952 : 0 : tempPainter.begin( job.firstPassJob->img );
953 : 0 : painter1 = &tempPainter;
954 : 0 : }
955 : : #if DEBUG_RENDERING
956 : : job.firstPassJob->img->save( QString( "/tmp/second_%1_first_pass_1.png" ).arg( i ) );
957 : : #endif
958 : : // ... first retain parts that are "outside" the mask image
959 : 0 : painter1->setCompositionMode( QPainter::CompositionMode_DestinationOut );
960 : 0 : painter1->drawImage( 0, 0, *maskImage );
961 : :
962 : : #if DEBUG_RENDERING
963 : : job.firstPassJob->img->save( QString( "/tmp/second_%1_first_pass_2.png" ).arg( i ) );
964 : : #endif
965 : : // ... and overpaint the second pass' image on it
966 : 0 : painter1->setCompositionMode( QPainter::CompositionMode_DestinationOver );
967 : 0 : painter1->drawImage( 0, 0, *job.img );
968 : : #if DEBUG_RENDERING
969 : : job.img->save( QString( "/tmp/second_%1_b.png" ).arg( i ) );
970 : : if ( job.firstPassJob )
971 : : job.firstPassJob->img->save( QString( "/tmp/second_%1_first_pass_3.png" ).arg( i ) );
972 : : #endif
973 : 0 : }
974 : 0 : }
975 : : }
976 : 0 : }
977 : :
978 : 0 : void QgsMapRendererJob::logRenderingTime( const LayerRenderJobs &jobs, const LayerRenderJobs &secondPassJobs, const LabelRenderJob &labelJob )
979 : : {
980 : 0 : QgsSettings settings;
981 : 0 : if ( !settings.value( QStringLiteral( "Map/logCanvasRefreshEvent" ), false ).toBool() )
982 : 0 : return;
983 : :
984 : 0 : QMultiMap<int, QString> elapsed;
985 : 0 : const auto constJobs = jobs;
986 : 0 : for ( const LayerRenderJob &job : constJobs )
987 : 0 : elapsed.insert( job.renderingTime, job.layerId );
988 : 0 : const auto constSecondPassJobs = secondPassJobs;
989 : 0 : for ( const LayerRenderJob &job : constSecondPassJobs )
990 : 0 : elapsed.insert( job.renderingTime, job.layerId + QString( " (second pass)" ) );
991 : :
992 : 0 : elapsed.insert( labelJob.renderingTime, tr( "Labeling" ) );
993 : :
994 : 0 : QList<int> tt( elapsed.uniqueKeys() );
995 : 0 : std::sort( tt.begin(), tt.end(), std::greater<int>() );
996 : 0 : const auto constTt = tt;
997 : 0 : for ( int t : constTt )
998 : : {
999 : 0 : QgsMessageLog::logMessage( tr( "%1 ms: %2" ).arg( t ).arg( QStringList( elapsed.values( t ) ).join( QLatin1String( ", " ) ) ), tr( "Rendering" ) );
1000 : : }
1001 : 0 : QgsMessageLog::logMessage( QStringLiteral( "---" ), tr( "Rendering" ) );
1002 : 0 : }
1003 : :
1004 : 0 : void QgsMapRendererJob::drawLabeling( QgsRenderContext &renderContext, QgsLabelingEngine *labelingEngine2, QPainter *painter )
1005 : : {
1006 : 0 : QgsDebugMsgLevel( QStringLiteral( "Draw labeling start" ), 5 );
1007 : :
1008 : 0 : QElapsedTimer t;
1009 : 0 : t.start();
1010 : :
1011 : : // Reset the composition mode before rendering the labels
1012 : 0 : painter->setCompositionMode( QPainter::CompositionMode_SourceOver );
1013 : :
1014 : 0 : renderContext.setPainter( painter );
1015 : :
1016 : 0 : if ( labelingEngine2 )
1017 : : {
1018 : 0 : labelingEngine2->run( renderContext );
1019 : 0 : }
1020 : :
1021 : 0 : QgsDebugMsgLevel( QStringLiteral( "Draw labeling took (seconds): %1" ).arg( t.elapsed() / 1000. ), 2 );
1022 : 0 : }
1023 : :
1024 : 0 : void QgsMapRendererJob::drawLabeling( const QgsMapSettings &settings, QgsRenderContext &renderContext, QgsLabelingEngine *labelingEngine2, QPainter *painter )
1025 : : {
1026 : 0 : Q_UNUSED( settings )
1027 : :
1028 : 0 : drawLabeling( renderContext, labelingEngine2, painter );
1029 : 0 : }
1030 : :
1031 : : ///@endcond PRIVATE
|