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