QGIS API Documentation 3.32.0-Lima (311a8cb8a6)
qgsmaprendererjob.cpp
Go to the documentation of this file.
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 <QPicture>
24
25#include "qgslogger.h"
26#include "qgsrendercontext.h"
27#include "qgsmaplayer.h"
28#include "qgsmaplayerrenderer.h"
29#include "qgsmaprenderercache.h"
30#include "qgsrasterlayer.h"
31#include "qgsmessagelog.h"
32#include "qgspallabeling.h"
33#include "qgsexception.h"
34#include "qgslabelingengine.h"
38#include "qgsrenderer.h"
39#include "qgssymbollayer.h"
40#include "qgsvectorlayerutils.h"
41#include "qgssymbollayerutils.h"
44#include "qgsmaplayerstyle.h"
47#include "qgsmaskpaintdevice.h"
48#include "qgsrasterrenderer.h"
49#include "qgselevationmap.h"
51#include "qgssettingstree.h"
52
54
56
57const QString QgsMapRendererJob::LABEL_CACHE_ID = QStringLiteral( "_labels_" );
58const QString QgsMapRendererJob::ELEVATION_MAP_CACHE_PREFIX = QStringLiteral( "_elevation_map_" );
59const QString QgsMapRendererJob::LABEL_PREVIEW_CACHE_ID = QStringLiteral( "_preview_labels_" );
60
61LayerRenderJob &LayerRenderJob::operator=( LayerRenderJob &&other )
62{
63 mContext = std::move( other.mContext );
64
65 img = other.img;
66 other.img = nullptr;
67
68 renderer = other.renderer;
69 other.renderer = nullptr;
70
71 imageInitialized = other.imageInitialized;
72 blendMode = other.blendMode;
73 opacity = other.opacity;
74 cached = other.cached;
75 layer = other.layer;
76 renderAboveLabels = other.renderAboveLabels;
77 completed = other.completed;
78 renderingTime = other.renderingTime;
79 estimatedRenderingTime = other.estimatedRenderingTime ;
80 errors = other.errors;
81 layerId = other.layerId;
82
83 maskPaintDevice = std::move( other.maskPaintDevice );
84
85 firstPassJob = other.firstPassJob;
86 other.firstPassJob = nullptr;
87
88 picture = std::move( other.picture );
89
90 maskJobs = other.maskJobs;
91
92 maskRequiresLayerRasterization = other.maskRequiresLayerRasterization;
93
94 elevationMap = other.elevationMap;
95 maskPainter = std::move( other.maskPainter );
96
97 return *this;
98}
99
100LayerRenderJob::LayerRenderJob( LayerRenderJob &&other )
101 : imageInitialized( other.imageInitialized )
102 , blendMode( other.blendMode )
103 , opacity( other.opacity )
104 , cached( other.cached )
105 , renderAboveLabels( other.renderAboveLabels )
106 , layer( other.layer )
107 , completed( other.completed )
108 , renderingTime( other.renderingTime )
109 , estimatedRenderingTime( other.estimatedRenderingTime )
110 , errors( other.errors )
111 , layerId( other.layerId )
112 , maskPainter( nullptr ) // should this be other.maskPainter??
113 , maskRequiresLayerRasterization( other.maskRequiresLayerRasterization )
114 , maskJobs( other.maskJobs )
115{
116 mContext = std::move( other.mContext );
117
118 img = other.img;
119 other.img = nullptr;
120
121 renderer = other.renderer;
122 other.renderer = nullptr;
123
124 elevationMap = other.elevationMap;
125 other.elevationMap = nullptr;
126
127 maskPaintDevice = std::move( other.maskPaintDevice );
128
129 firstPassJob = other.firstPassJob;
130 other.firstPassJob = nullptr;
131
132 picture = std::move( other.picture );
133}
134
135bool LayerRenderJob::imageCanBeComposed() const
136{
137 if ( imageInitialized )
138 {
139 if ( renderer )
140 {
141 return renderer->isReadyToCompose();
142 }
143 else
144 {
145 return true;
146 }
147 }
148 else
149 {
150 return false;
151 }
152}
153
155 : mSettings( settings )
156 , mRenderedItemResults( std::make_unique< QgsRenderedItemResults >( settings.extent() ) )
157 , mLabelingEngineFeedback( new QgsLabelingEngineFeedback( this ) )
158{}
159
161
163{
165 startPrivate();
166 else
167 {
168 mErrors.append( QgsMapRendererJob::Error( QString(), tr( "Invalid map settings" ) ) );
169 emit finished();
170 }
171}
172
174{
176}
177
179{
180 return mRenderedItemResults.release();
181}
182
184 : QgsMapRendererJob( settings )
185{
186}
187
188
190{
191 return mErrors;
192}
193
195{
196 mCache = cache;
197}
198
200{
201 return mLabelingEngineFeedback;
202}
203
204QHash<QgsMapLayer *, int> QgsMapRendererJob::perLayerRenderingTime() const
205{
206 QHash<QgsMapLayer *, int> result;
207 for ( auto it = mPerLayerRenderingTime.constBegin(); it != mPerLayerRenderingTime.constEnd(); ++it )
208 {
209 if ( auto &&lKey = it.key() )
210 result.insert( lKey, it.value() );
211 }
212 return result;
213}
214
215void QgsMapRendererJob::setLayerRenderingTimeHints( const QHash<QString, int> &hints )
216{
218}
219
221{
222 return mSettings;
223}
224
226{
227 bool canCache = mCache;
228
229 // calculate which layers will be labeled
230 QSet< QgsMapLayer * > labeledLayers;
231 const QList<QgsMapLayer *> layers = mSettings.layers();
232 for ( QgsMapLayer *ml : layers )
233 {
235 labeledLayers << ml;
236
237 switch ( ml->type() )
238 {
239 case Qgis::LayerType::Vector:
240 {
241 QgsVectorLayer *vl = qobject_cast< QgsVectorLayer *>( ml );
242 if ( vl->labelsEnabled() && vl->labeling()->requiresAdvancedEffects() )
243 {
244 canCache = false;
245 }
246 break;
247 }
248
249 case Qgis::LayerType::VectorTile:
250 {
251 // TODO -- add detection of advanced labeling effects for vector tile layers
252 break;
253 }
254
255 case Qgis::LayerType::Raster:
256 case Qgis::LayerType::Annotation:
257 case Qgis::LayerType::Plugin:
258 case Qgis::LayerType::Mesh:
259 case Qgis::LayerType::PointCloud:
260 case Qgis::LayerType::Group:
261 break;
262 }
263
264 if ( !canCache )
265 break;
266
267 }
268
270 {
271 // we may need to clear label cache and re-register labeled features - check for that here
272
273 // can we reuse the cached label solution?
274 const QList< QgsMapLayer * > labelDependentLayers = mCache->dependentLayers( LABEL_CACHE_ID );
275 bool canUseCache = canCache && QSet< QgsMapLayer * >( labelDependentLayers.begin(), labelDependentLayers.end() ) == labeledLayers;
276 if ( !canUseCache )
277 {
278 // no - participating layers have changed
280 }
281 }
282 return canCache;
283}
284
285
286bool QgsMapRendererJob::reprojectToLayerExtent( const QgsMapLayer *ml, const QgsCoordinateTransform &ct, QgsRectangle &extent, QgsRectangle &r2 )
287{
288 bool res = true;
289 // we can safely use ballpark transforms without bothering the user here -- at the likely scale of layer extents there
290 // won't be an appreciable difference, and we aren't actually transforming any rendered points here anyway (just the layer extent)
291 QgsCoordinateTransform approxTransform = ct;
292 approxTransform.setBallparkTransformsAreAppropriate( true );
293
294 try
295 {
296#ifdef QGISDEBUG
297 // QgsLogger::debug<QgsRectangle>("Getting extent of canvas in layers CS. Canvas is ", extent, __FILE__, __FUNCTION__, __LINE__);
298#endif
299 // Split the extent into two if the source CRS is
300 // geographic and the extent crosses the split in
301 // geographic coordinates (usually +/- 180 degrees,
302 // and is assumed to be so here), and draw each
303 // extent separately.
304 static const double SPLIT_COORD = 180.0;
305
306 if ( ml->crs().isGeographic() )
307 {
308 if ( ml->type() == Qgis::LayerType::Vector && !approxTransform.destinationCrs().isGeographic() )
309 {
310 // if we transform from a projected coordinate system check
311 // check if transforming back roughly returns the input
312 // extend - otherwise render the world.
313 QgsRectangle extent1 = approxTransform.transformBoundingBox( extent, Qgis::TransformDirection::Reverse );
314 QgsRectangle extent2 = approxTransform.transformBoundingBox( extent1, Qgis::TransformDirection::Forward );
315
316 QgsDebugMsgLevel( QStringLiteral( "\n0:%1 %2x%3\n1:%4\n2:%5 %6x%7 (w:%8 h:%9)" )
317 .arg( extent.toString() ).arg( extent.width() ).arg( extent.height() )
318 .arg( extent1.toString(), extent2.toString() ).arg( extent2.width() ).arg( extent2.height() )
319 .arg( std::fabs( 1.0 - extent2.width() / extent.width() ) )
320 .arg( std::fabs( 1.0 - extent2.height() / extent.height() ) )
321 , 3 );
322
323 // can differ by a maximum of up to 20% of height/width
324 if ( qgsDoubleNear( extent2.xMinimum(), extent.xMinimum(), extent.width() * 0.2 )
325 && qgsDoubleNear( extent2.xMaximum(), extent.xMaximum(), extent.width() * 0.2 )
326 && qgsDoubleNear( extent2.yMinimum(), extent.yMinimum(), extent.height() * 0.2 )
327 && qgsDoubleNear( extent2.yMaximum(), extent.yMaximum(), extent.height() * 0.2 )
328 )
329 {
330 extent = extent1;
331 }
332 else
333 {
334 extent = QgsRectangle( -180.0, -90.0, 180.0, 90.0 );
335 res = false;
336 }
337 }
338 else
339 {
340 // Note: ll = lower left point
341 QgsPointXY ll = approxTransform.transform( extent.xMinimum(), extent.yMinimum(),
342 Qgis::TransformDirection::Reverse );
343
344 // and ur = upper right point
345 QgsPointXY ur = approxTransform.transform( extent.xMaximum(), extent.yMaximum(),
346 Qgis::TransformDirection::Reverse );
347
348 QgsDebugMsgLevel( QStringLiteral( "in:%1 (ll:%2 ur:%3)" ).arg( extent.toString(), ll.toString(), ur.toString() ), 4 );
349
350 extent = approxTransform.transformBoundingBox( extent, Qgis::TransformDirection::Reverse );
351
352 QgsDebugMsgLevel( QStringLiteral( "out:%1 (w:%2 h:%3)" ).arg( extent.toString() ).arg( extent.width() ).arg( extent.height() ), 4 );
353
354 if ( ll.x() > ur.x() )
355 {
356 // the coordinates projected in reverse order than what one would expect.
357 // we are probably looking at an area that includes longitude of 180 degrees.
358 // we need to take into account coordinates from two intervals: (-180,x1) and (x2,180)
359 // so let's use (-180,180). This hopefully does not add too much overhead. It is
360 // more straightforward than rendering with two separate extents and more consistent
361 // for rendering, labeling and caching as everything is rendered just in one go
362 extent.setXMinimum( -SPLIT_COORD );
363 extent.setXMaximum( SPLIT_COORD );
364 res = false;
365 }
366 }
367
368 // TODO: the above rule still does not help if using a projection that covers the whole
369 // world. E.g. with EPSG:3857 the longitude spectrum -180 to +180 is mapped to approx.
370 // -2e7 to +2e7. Converting extent from -5e7 to +5e7 is transformed as -90 to +90,
371 // but in fact the extent should cover the whole world.
372 }
373 else // can't cross 180
374 {
375 if ( approxTransform.destinationCrs().isGeographic() &&
376 ( extent.xMinimum() <= -180 || extent.xMaximum() >= 180 ||
377 extent.yMinimum() <= -90 || extent.yMaximum() >= 90 ) )
378 // Use unlimited rectangle because otherwise we may end up transforming wrong coordinates.
379 // E.g. longitude -200 to +160 would be understood as +40 to +160 due to periodicity.
380 // We could try to clamp coords to (-180,180) for lon resp. (-90,90) for lat,
381 // but this seems like a safer choice.
382 {
383 extent = QgsRectangle( std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest(), std::numeric_limits<double>::max(), std::numeric_limits<double>::max() );
384 res = false;
385 }
386 else
387 extent = approxTransform.transformBoundingBox( extent, Qgis::TransformDirection::Reverse );
388 }
389 }
390 catch ( QgsCsException & )
391 {
392 QgsDebugError( QStringLiteral( "Transform error caught" ) );
393 extent = QgsRectangle( std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest(), std::numeric_limits<double>::max(), std::numeric_limits<double>::max() );
394 r2 = QgsRectangle( std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest(), std::numeric_limits<double>::max(), std::numeric_limits<double>::max() );
395 res = false;
396 }
397
398 return res;
399}
400
401QImage *QgsMapRendererJob::allocateImage( QString layerId )
402{
403 QImage *image = new QImage( mSettings.deviceOutputSize(),
405 image->setDevicePixelRatio( static_cast<qreal>( mSettings.devicePixelRatio() ) );
406 image->setDotsPerMeterX( 1000 * mSettings.outputDpi() / 25.4 );
407 image->setDotsPerMeterY( 1000 * mSettings.outputDpi() / 25.4 );
408 if ( image->isNull() )
409 {
410 mErrors.append( Error( layerId, tr( "Insufficient memory for image %1x%2" ).arg( mSettings.outputSize().width() ).arg( mSettings.outputSize().height() ) ) );
411 delete image;
412 return nullptr;
413 }
414 return image;
415}
416
417QgsElevationMap *QgsMapRendererJob::allocateElevationMap( QString layerId )
418{
419 std::unique_ptr<QgsElevationMap> elevationMap = std::make_unique<QgsElevationMap>( mSettings.deviceOutputSize() );
420 if ( !elevationMap->isValid() )
421 {
422 mErrors.append( Error( layerId, tr( "Insufficient memory for elevation map %1x%2" ).arg( mSettings.outputSize().width() ).arg( mSettings.outputSize().height() ) ) );
423 return nullptr;
424 }
425 return elevationMap.release();
426}
427
428QPainter *QgsMapRendererJob::allocateImageAndPainter( QString layerId, QImage *&image, const QgsRenderContext *context )
429{
430 QPainter *painter = nullptr;
431 image = allocateImage( layerId );
432 if ( image )
433 {
434 painter = new QPainter( image );
435 context->setPainterFlagsUsingContext( painter );
436 }
437 return painter;
438}
439
440QgsMapRendererJob::PictureAndPainter QgsMapRendererJob::allocatePictureAndPainter( const QgsRenderContext *context )
441{
442 std::unique_ptr<QPicture> picture = std::make_unique<QPicture>();
443 QPainter *painter = new QPainter( picture.get() );
444 context->setPainterFlagsUsingContext( painter );
445 return { std::move( picture ), painter };
446}
447
448std::vector<LayerRenderJob> QgsMapRendererJob::prepareJobs( QPainter *painter, QgsLabelingEngine *labelingEngine2, bool deferredPainterSet )
449{
450 std::vector< LayerRenderJob > layerJobs;
451
452 // render all layers in the stack, starting at the base
453 QListIterator<QgsMapLayer *> li( mSettings.layers() );
454 li.toBack();
455
456 if ( mCache )
457 {
459 Q_UNUSED( cacheValid )
460 QgsDebugMsgLevel( QStringLiteral( "CACHE VALID: %1" ).arg( cacheValid ), 4 );
461 }
462
463 bool requiresLabelRedraw = !( mCache && mCache->hasCacheImage( LABEL_CACHE_ID ) );
464
465 while ( li.hasPrevious() )
466 {
467 QgsMapLayer *ml = li.previous();
468
469 QgsDebugMsgLevel( QStringLiteral( "layer %1: minscale:%2 maxscale:%3 scaledepvis:%4 blendmode:%5 isValid:%6" )
470 .arg( ml->name() )
471 .arg( ml->minimumScale() )
472 .arg( ml->maximumScale() )
473 .arg( ml->hasScaleBasedVisibility() )
474 .arg( ml->blendMode() )
475 .arg( ml->isValid() )
476 , 3 );
477
478 if ( !ml->isValid() )
479 {
480 QgsDebugMsgLevel( QStringLiteral( "Invalid Layer skipped" ), 3 );
481 continue;
482 }
483
484 if ( !ml->isInScaleRange( mSettings.scale() ) ) //|| mOverview )
485 {
486 QgsDebugMsgLevel( QStringLiteral( "Layer not rendered because it is not within the defined visibility scale range" ), 3 );
487 continue;
488 }
489
491 {
492 QgsDebugMsgLevel( QStringLiteral( "Layer not rendered because it is not visible within the map's time range" ), 3 );
493 continue;
494 }
495
497 {
498 QgsDebugMsgLevel( QStringLiteral( "Layer not rendered because it is not visible within the map's z range" ), 3 );
499 continue;
500 }
501
505
506 ct = mSettings.layerTransform( ml );
507 bool haveExtentInLayerCrs = true;
508 if ( ct.isValid() )
509 {
510 haveExtentInLayerCrs = reprojectToLayerExtent( ml, ct, r1, r2 );
511 }
512 QgsDebugMsgLevel( "extent: " + r1.toString(), 3 );
513 if ( !r1.isFinite() || !r2.isFinite() )
514 {
515 mErrors.append( Error( ml->id(), tr( "There was a problem transforming the layer's extent. Layer skipped." ) ) );
516 continue;
517 }
518
519 QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( ml );
520
521 // Force render of layers that are being edited
522 // or if there's a labeling engine that needs the layer to register features
523 if ( mCache )
524 {
525 const bool requiresLabeling = ( labelingEngine2 && QgsPalLabeling::staticWillUseLayer( ml ) ) && requiresLabelRedraw;
526 if ( ( vl && vl->isEditable() ) || requiresLabeling )
527 {
528 mCache->clearCacheImage( ml->id() );
529 }
530 }
531
532 layerJobs.emplace_back( LayerRenderJob() );
533 LayerRenderJob &job = layerJobs.back();
534 job.layer = ml;
535 job.layerId = ml->id();
536 job.renderAboveLabels = ml->customProperty( QStringLiteral( "rendering/renderAboveLabels" ) ).toBool();
537 job.estimatedRenderingTime = mLayerRenderingTimeHints.value( ml->id(), 0 );
538
539 job.setContext( std::make_unique< QgsRenderContext >( QgsRenderContext::fromMapSettings( mSettings ) ) );
540 if ( !ml->customProperty( QStringLiteral( "_noset_layer_expression_context" ) ).toBool() )
541 job.context()->expressionContext().appendScope( QgsExpressionContextUtils::layerScope( ml ) );
542 job.context()->setPainter( painter );
543 job.context()->setLabelingEngine( labelingEngine2 );
544 job.context()->setLabelSink( labelSink() );
545 job.context()->setCoordinateTransform( ct );
546 job.context()->setExtent( r1 );
547
548 // Also check geographic, see: https://github.com/qgis/QGIS/issues/45200
549 if ( !haveExtentInLayerCrs || ( ct.isValid() && ( ct.sourceCrs().isGeographic() != ct.destinationCrs().isGeographic() ) ) )
550 job.context()->setFlag( Qgis::RenderContextFlag::ApplyClipAfterReprojection, true );
551
552 if ( mFeatureFilterProvider )
553 job.context()->setFeatureFilterProvider( mFeatureFilterProvider );
554
555 QgsMapLayerStyleOverride styleOverride( ml );
556 if ( mSettings.layerStyleOverrides().contains( ml->id() ) )
557 styleOverride.setOverrideStyle( mSettings.layerStyleOverrides().value( ml->id() ) );
558
559 job.blendMode = ml->blendMode();
560
561 if ( ml->type() == Qgis::LayerType::Raster )
562 {
563 // raster layers are abnormal wrt opacity handling -- opacity is sometimes handled directly within the raster layer renderer
564 QgsRasterLayer *rl = qobject_cast< QgsRasterLayer * >( ml );
566 {
567 job.opacity = 1.0;
568 }
569 else
570 {
571 job.opacity = ml->opacity();
572 }
573 }
574 else
575 {
576 job.opacity = ml->opacity();
577 }
578
580
581 // if we can use the cache, let's do it and avoid rendering!
583 && mCache && mCache->hasCacheImage( ml->id() ) )
584 {
585 job.cached = true;
586 job.imageInitialized = true;
587 job.img = new QImage( mCache->cacheImage( ml->id() ) );
588 if ( shadingRenderer.isActive() &&
589 ml->elevationProperties() &&
592 job.elevationMap = new QgsElevationMap( mCache->cacheImage( ELEVATION_MAP_CACHE_PREFIX + ml->id() ) );
593 job.img->setDevicePixelRatio( static_cast<qreal>( mSettings.devicePixelRatio() ) );
594 job.renderer = nullptr;
595 job.context()->setPainter( nullptr );
596 mLayersRedrawnFromCache.append( ml->id() );
597 continue;
598 }
599
600 QElapsedTimer layerTime;
601 layerTime.start();
602 job.renderer = ml->createMapRenderer( *( job.context() ) );
603 if ( job.renderer )
604 {
605 job.renderer->setLayerRenderingTimeHint( job.estimatedRenderingTime );
606 job.context()->setFeedback( job.renderer->feedback() );
607 }
608
609 // If we are drawing with an alternative blending mode then we need to render to a separate image
610 // before compositing this on the map. This effectively flattens the layer and prevents
611 // blending occurring between objects on the layer
612 if ( mCache || ( !painter && !deferredPainterSet ) || ( job.renderer && job.renderer->forceRasterRender() ) )
613 {
614 // Flattened image for drawing when a blending mode is set
615 job.context()->setPainter( allocateImageAndPainter( ml->id(), job.img, job.context() ) );
616 if ( ! job.img )
617 {
618 delete job.renderer;
619 job.renderer = nullptr;
620 layerJobs.pop_back();
621 continue;
622 }
623 }
624
625 if ( shadingRenderer.isActive()
626 && ml->elevationProperties()
628 {
629 job.elevationMap = allocateElevationMap( ml->id() );
630 job.context()->setElevationMap( job.elevationMap );
631 }
632
633 job.renderingTime = layerTime.elapsed(); // include job preparation time in layer rendering time
634 }
635
636 return layerJobs;
637}
638
639std::vector< LayerRenderJob > QgsMapRendererJob::prepareSecondPassJobs( std::vector< LayerRenderJob > &firstPassJobs, LabelRenderJob &labelJob )
640{
641 std::vector< LayerRenderJob > secondPassJobs;
642
643 // We will need to quickly access the associated rendering job of a layer
644 QHash<QString, LayerRenderJob *> layerJobMapping;
645
646 // ... and layer that contains a mask (and whether there is effects implied or not)
647 QMap<QString, bool> maskLayerHasEffects;
648 QMap<int, bool> labelHasEffects;
649
650 struct MaskSource
651 {
652 QString layerId;
653 QString labelRuleId;
654 int labelMaskId;
655 bool hasEffects;
656 MaskSource( const QString &layerId_, const QString &labelRuleId_, int labelMaskId_, bool hasEffects_ ):
657 layerId( layerId_ ), labelRuleId( labelRuleId_ ), labelMaskId( labelMaskId_ ), hasEffects( hasEffects_ ) {}
658 };
659
660 // We collect for each layer, the set of symbol layers that will be "masked"
661 // and the list of source layers that have a mask
662 QHash<QString, QPair<QSet<QString>, QList<MaskSource>>> maskedSymbolLayers;
663
666
667 // First up, create a mapping of layer id to jobs. We need this to filter out any masking
668 // which refers to layers which we aren't rendering as part of this map render
669 for ( LayerRenderJob &job : firstPassJobs )
670 {
671 layerJobMapping[job.layerId] = &job;
672 }
673
674 // next, collate a master list of masked layers, skipping over any which refer to layers
675 // which don't have a corresponding render job
676 for ( LayerRenderJob &job : firstPassJobs )
677 {
678 QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( job.layer );
679 if ( ! vl )
680 continue;
681
682 // lambda function to factor code for both label masks and symbol layer masks
683 auto collectMasks = [&]( QgsMaskedLayers * masks, QString sourceLayerId, QString ruleId = QString(), int labelMaskId = -1 )
684 {
685 bool hasEffects = false;
686 for ( auto it = masks->begin(); it != masks->end(); ++it )
687 {
688 auto lit = maskedSymbolLayers.find( it.key() );
689 if ( lit == maskedSymbolLayers.end() )
690 {
691 maskedSymbolLayers[it.key()] = qMakePair( it.value().symbolLayerIds, QList<MaskSource>() << MaskSource( sourceLayerId, ruleId, labelMaskId, it.value().hasEffects ) );
692 }
693 else
694 {
695 if ( lit->first != it.value().symbolLayerIds )
696 {
697 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() ) );
698 continue;
699 }
700 lit->second.push_back( MaskSource( sourceLayerId, ruleId, labelMaskId, hasEffects ) );
701 }
702 hasEffects |= it.value().hasEffects;
703 }
704 if ( ! masks->isEmpty() && labelMaskId == -1 )
705 maskLayerHasEffects[ sourceLayerId ] = hasEffects;
706 };
707
708 // collect label masks
709 QHash<QString, QgsMaskedLayers> labelMasks = QgsVectorLayerUtils::labelMasks( vl );
710 for ( auto it = labelMasks.begin(); it != labelMasks.end(); it++ )
711 {
712 QString labelRule = it.key();
713 // this is a hash of layer id to masks
714 QgsMaskedLayers masks = it.value();
715
716 // filter out masks to those which we are actually rendering
717 QgsMaskedLayers usableMasks;
718 for ( auto mit = masks.begin(); mit != masks.end(); mit++ )
719 {
720 const QString sourceLayerId = mit.key();
721 // if we aren't rendering the source layer as part of this render, we can't process this mask
722 if ( !layerJobMapping.contains( sourceLayerId ) )
723 continue;
724 else
725 usableMasks.insert( sourceLayerId, mit.value() );
726 }
727
728 if ( usableMasks.empty() )
729 continue;
730
731 // group layers by QSet<QgsSymbolLayerReference>
732 QSet<QgsSymbolLayerReference> slRefs;
733 bool hasEffects = false;
734 for ( auto mit = usableMasks.begin(); mit != usableMasks.end(); mit++ )
735 {
736 const QString sourceLayerId = mit.key();
737 // if we aren't rendering the source layer as part of this render, we can't process this mask
738 if ( !layerJobMapping.contains( sourceLayerId ) )
739 continue;
740
741 for ( const QString &symbolLayerId : mit.value().symbolLayerIds )
742 slRefs.insert( QgsSymbolLayerReference( sourceLayerId, symbolLayerId ) );
743
744 hasEffects |= mit.value().hasEffects;
745 }
746 // generate a new mask id for this set
747 int labelMaskId = labelJob.maskIdProvider.insertLabelLayer( vl->id(), it.key(), slRefs );
748 labelHasEffects[ labelMaskId ] = hasEffects;
749
750 // now collect masks
751 collectMasks( &usableMasks, vl->id(), labelRule, labelMaskId );
752 }
753
754 // collect symbol layer masks
756 collectMasks( &symbolLayerMasks, vl->id() );
757 }
758
759 if ( maskedSymbolLayers.isEmpty() )
760 return secondPassJobs;
761
762 // Prepare label mask images
763 for ( int maskId = 0; maskId < labelJob.maskIdProvider.size(); maskId++ )
764 {
765 QPaintDevice *maskPaintDevice = nullptr;
766 QPainter *maskPainter = nullptr;
767 if ( forceVector && !labelHasEffects[ maskId ] )
768 {
769 // set a painter to get all masking instruction in order to later clip masked symbol layer
770 maskPaintDevice = new QgsMaskPaintDevice( true );
771 maskPainter = new QPainter( maskPaintDevice );
772 }
773 else
774 {
775 // Note: we only need an alpha channel here, rather than a full RGBA image
776 QImage *maskImage = nullptr;
777 maskPainter = allocateImageAndPainter( QStringLiteral( "label mask" ), maskImage, &labelJob.context );
778 maskImage->fill( 0 );
779 maskPaintDevice = maskImage;
780 }
781
782 labelJob.context.setMaskPainter( maskPainter, maskId );
783 labelJob.maskPainters.push_back( std::unique_ptr<QPainter>( maskPainter ) );
784 labelJob.maskPaintDevices.push_back( std::unique_ptr<QPaintDevice>( maskPaintDevice ) );
785 }
786 labelJob.context.setMaskIdProvider( &labelJob.maskIdProvider );
787
788 // Prepare second pass jobs
789 // - For raster rendering or vector rendering if effects are involved
790 // 1st pass, 2nd pass and mask are rendered in QImage and composed in composeSecondPass
791 // - For vector rendering if no effects are involved
792 // 1st pass is rendered in QImage, clip paths are generated according to mask and used during
793 // masked symbol layer rendering during second pass, which is rendered in QPicture, second
794 // pass job picture
795
796 // Allocate an image for labels
797 if ( !labelJob.img && !forceVector )
798 {
799 labelJob.img = allocateImage( QStringLiteral( "labels" ) );
800 }
801 else if ( !labelJob.picture && forceVector )
802 {
803 labelJob.picture.reset( new QPicture() );
804 }
805
806 // first we initialize painter and mask painter for all jobs
807 for ( LayerRenderJob &job : firstPassJobs )
808 {
809 job.maskRequiresLayerRasterization = false;
810
811 auto it = maskedSymbolLayers.find( job.layerId );
812 if ( it != maskedSymbolLayers.end() )
813 {
814 const QList<MaskSource> &sourceList = it->second;
815 for ( const MaskSource &source : sourceList )
816 {
817 job.maskRequiresLayerRasterization |= source.hasEffects;
818 }
819 }
820
821 // update first pass job painter and device if needed
822 const bool isRasterRendering = !forceVector || job.maskRequiresLayerRasterization || ( job.renderer && job.renderer->forceRasterRender() );
823 if ( isRasterRendering && !job.img )
824 {
825 job.context()->setPainter( allocateImageAndPainter( job.layerId, job.img, job.context() ) );
826 }
827 else if ( !isRasterRendering && !job.picture )
828 {
829 PictureAndPainter pictureAndPainter = allocatePictureAndPainter( job.context() );
830 job.picture = std::move( pictureAndPainter.first );
831 job.context()->setPainter( pictureAndPainter.second );
832 // force recreation of layer renderer so it initialize correctly the renderer
833 // especially the RasterLayerRender that need logicalDpiX from painting device
834 job.renderer = job.layer->createMapRenderer( *( job.context() ) );
835 }
836
837 // for layer that mask, generate mask in first pass job
838 if ( maskLayerHasEffects.contains( job.layerId ) )
839 {
840 QPaintDevice *maskPaintDevice = nullptr;
841 QPainter *maskPainter = nullptr;
842 if ( forceVector && !maskLayerHasEffects[ job.layerId ] )
843 {
844 // set a painter to get all masking instruction in order to later clip masked symbol layer
845 maskPaintDevice = new QgsMaskPaintDevice();
846 maskPainter = new QPainter( maskPaintDevice );
847 }
848 else
849 {
850 // Note: we only need an alpha channel here, rather than a full RGBA image
851 QImage *maskImage = nullptr;
852 maskPainter = allocateImageAndPainter( job.layerId, maskImage, job.context() );
853 maskImage->fill( 0 );
854 maskPaintDevice = maskImage;
855 }
856
857 job.context()->setMaskPainter( maskPainter );
858 job.maskPainter.reset( maskPainter );
859 job.maskPaintDevice.reset( maskPaintDevice );
860 }
861 }
862
863 for ( LayerRenderJob &job : firstPassJobs )
864 {
865 QgsMapLayer *ml = job.layer;
866
867 auto it = maskedSymbolLayers.find( job.layerId );
868 if ( it == maskedSymbolLayers.end() )
869 continue;
870
871 QList<MaskSource> &sourceList = it->second;
872 const QSet<QString> symbolList = it->first;
873
874 secondPassJobs.emplace_back( LayerRenderJob() );
875 LayerRenderJob &job2 = secondPassJobs.back();
876
877 job2.maskRequiresLayerRasterization = job.maskRequiresLayerRasterization;
878
879 // Points to the masking jobs. This will be needed during the second pass composition.
880 for ( MaskSource &source : sourceList )
881 {
882 if ( source.labelMaskId != -1 )
883 job2.maskJobs.push_back( qMakePair( nullptr, source.labelMaskId ) );
884 else
885 job2.maskJobs.push_back( qMakePair( layerJobMapping[source.layerId], -1 ) );
886 }
887
888 // copy the context from the initial job
889 job2.setContext( std::make_unique< QgsRenderContext >( *job.context() ) );
890 // also assign layer to match initial job
891 job2.layer = job.layer;
892 job2.renderAboveLabels = job.renderAboveLabels;
893 job2.layerId = job.layerId;
894
895 // associate first pass job with second pass job
896 job2.firstPassJob = &job;
897
898 if ( !forceVector || job2.maskRequiresLayerRasterization )
899 {
900 job2.context()->setPainter( allocateImageAndPainter( job.layerId, job2.img, job2.context() ) );
901 }
902 else
903 {
904 PictureAndPainter pictureAndPainter = allocatePictureAndPainter( job2.context() );
905 job2.picture = std::move( pictureAndPainter.first );
906 job2.context()->setPainter( pictureAndPainter.second );
907 }
908
909 if ( ! job2.img && ! job2.picture )
910 {
911 secondPassJobs.pop_back();
912 continue;
913 }
914
915 // FIXME: another possibility here, to avoid allocating a new map renderer and reuse the one from
916 // the first pass job, would be to be able to call QgsMapLayerRenderer::render() with a QgsRenderContext.
917 QgsVectorLayerRenderer *mapRenderer = static_cast<QgsVectorLayerRenderer *>( ml->createMapRenderer( *job2.context() ) );
918 job2.renderer = mapRenderer;
919 if ( job2.renderer )
920 {
921 job2.context()->setFeedback( job2.renderer->feedback() );
922 }
923
924 // Render only the non masked symbol layer and we will compose 2nd pass with mask and first pass rendering in composeSecondPass
925 // If vector output is enabled, disabled symbol layers would be actually rendered and masked with clipping path set in QgsMapRendererJob::initSecondPassJobs
926 job2.context()->setDisabledSymbolLayersV2( symbolList );
927 }
928
929 return secondPassJobs;
930}
931
932void QgsMapRendererJob::initSecondPassJobs( std::vector< LayerRenderJob > &secondPassJobs, LabelRenderJob &labelJob ) const
933{
935 return;
936
937 for ( LayerRenderJob &job : secondPassJobs )
938 {
939 if ( job.maskRequiresLayerRasterization )
940 continue;
941
942 // we draw disabled symbol layer but me mask them with clipping path produced during first pass job
943 // Resulting 2nd pass job picture will be the final rendering
944
945 for ( const QPair<LayerRenderJob *, int> &p : std::as_const( job.maskJobs ) )
946 {
947 QPainter *maskPainter = p.first ? p.first->maskPainter.get() : labelJob.maskPainters[p.second].get();
948 QPainterPath path = static_cast<QgsMaskPaintDevice *>( maskPainter->device() )->maskPainterPath();
949 for ( const QString &symbolLayerId : job.context()->disabledSymbolLayersV2() )
950 {
951 job.context()->addSymbolLayerClipPath( symbolLayerId, path );
952 }
953 }
954
955 job.context()->setDisabledSymbolLayersV2( QSet<QString>() );
956 }
957}
958
959LabelRenderJob QgsMapRendererJob::prepareLabelingJob( QPainter *painter, QgsLabelingEngine *labelingEngine2, bool canUseLabelCache )
960{
961 LabelRenderJob job;
963 job.context.setPainter( painter );
964 job.context.setLabelingEngine( labelingEngine2 );
965 job.context.setFeedback( mLabelingEngineFeedback );
966
969 job.context.setExtent( r1 );
970
971 job.context.setFeatureFilterProvider( mFeatureFilterProvider );
974 job.context.setCoordinateTransform( ct );
975
976 // no cache, no image allocation
978 return job;
979
980 // if we can use the cache, let's do it and avoid rendering!
981 bool hasCache = canUseLabelCache && mCache && mCache->hasCacheImage( LABEL_CACHE_ID );
982 if ( hasCache )
983 {
984 job.cached = true;
985 job.complete = true;
986 job.img = new QImage( mCache->cacheImage( LABEL_CACHE_ID ) );
987 Q_ASSERT( job.img->devicePixelRatio() == mSettings.devicePixelRatio() );
988 job.context.setPainter( nullptr );
989 }
990 else
991 {
992 if ( canUseLabelCache && ( mCache || !painter ) )
993 {
994 job.img = allocateImage( QStringLiteral( "labels" ) );
995 }
996 }
997
998 return job;
999}
1000
1001
1002void QgsMapRendererJob::cleanupJobs( std::vector<LayerRenderJob> &jobs )
1003{
1004 for ( LayerRenderJob &job : jobs )
1005 {
1006 if ( job.img )
1007 {
1008 delete job.context()->painter();
1009 job.context()->setPainter( nullptr );
1010
1011 if ( mCache && !job.cached && job.completed && job.layer )
1012 {
1013 QgsDebugMsgLevel( QStringLiteral( "caching image for %1" ).arg( job.layerId ), 2 );
1014 mCache->setCacheImageWithParameters( job.layerId, *job.img, mSettings.visibleExtent(), mSettings.mapToPixel(), QList< QgsMapLayer * >() << job.layer );
1015 mCache->setCacheImageWithParameters( job.layerId + QStringLiteral( "_preview" ), *job.img, mSettings.visibleExtent(), mSettings.mapToPixel(), QList< QgsMapLayer * >() << job.layer );
1016 }
1017
1018 delete job.img;
1019 job.img = nullptr;
1020 }
1021
1022 if ( job.elevationMap )
1023 {
1024 job.context()->setElevationMap( nullptr );
1025 if ( mCache && !job.cached && job.completed && job.layer )
1026 {
1027 QgsDebugMsgLevel( QStringLiteral( "caching elevation map for %1" ).arg( job.layerId ), 2 );
1029 ELEVATION_MAP_CACHE_PREFIX + job.layerId,
1030 job.elevationMap->rawElevationImage(),
1033 QList< QgsMapLayer * >() << job.layer );
1035 ELEVATION_MAP_CACHE_PREFIX + job.layerId + QStringLiteral( "_preview" ),
1036 job.elevationMap->rawElevationImage(),
1039 QList< QgsMapLayer * >() << job.layer );
1040 }
1041
1042 delete job.elevationMap;
1043 job.elevationMap = nullptr;
1044 }
1045
1046 if ( job.picture )
1047 {
1048 delete job.context()->painter();
1049 job.context()->setPainter( nullptr );
1050 job.picture.reset( nullptr );
1051 }
1052
1053 if ( job.renderer )
1054 {
1055 const QStringList errors = job.renderer->errors();
1056 for ( const QString &message : errors )
1057 mErrors.append( Error( job.renderer->layerId(), message ) );
1058
1059 mRenderedItemResults->appendResults( job.renderer->takeRenderedItemDetails(), *job.context() );
1060
1061 delete job.renderer;
1062 job.renderer = nullptr;
1063 }
1064
1065 if ( job.layer )
1066 mPerLayerRenderingTime.insert( job.layer, job.renderingTime );
1067
1068 job.maskPainter.reset( nullptr );
1069 job.maskPaintDevice.reset( nullptr );
1070 }
1071
1072 jobs.clear();
1073}
1074
1075void QgsMapRendererJob::cleanupSecondPassJobs( std::vector< LayerRenderJob > &jobs )
1076{
1077 for ( LayerRenderJob &job : jobs )
1078 {
1079 if ( job.img )
1080 {
1081 delete job.context()->painter();
1082 job.context()->setPainter( nullptr );
1083
1084 delete job.img;
1085 job.img = nullptr;
1086 }
1087
1088 if ( job.picture )
1089 {
1090 delete job.context()->painter();
1091 job.context()->setPainter( nullptr );
1092 }
1093
1094 if ( job.renderer )
1095 {
1096 delete job.renderer;
1097 job.renderer = nullptr;
1098 }
1099
1100 if ( job.layer )
1101 mPerLayerRenderingTime.insert( job.layer, job.renderingTime );
1102 }
1103
1104 jobs.clear();
1105}
1106
1107void QgsMapRendererJob::cleanupLabelJob( LabelRenderJob &job )
1108{
1109 if ( job.img )
1110 {
1111 if ( mCache && !job.cached && !job.context.renderingStopped() )
1112 {
1113 QgsDebugMsgLevel( QStringLiteral( "caching label result image" ), 2 );
1114 mCache->setCacheImageWithParameters( LABEL_CACHE_ID, *job.img, mSettings.visibleExtent(), mSettings.mapToPixel(), _qgis_listQPointerToRaw( job.participatingLayers ) );
1115 mCache->setCacheImageWithParameters( LABEL_PREVIEW_CACHE_ID, *job.img, mSettings.visibleExtent(), mSettings.mapToPixel(), _qgis_listQPointerToRaw( job.participatingLayers ) );
1116 }
1117
1118 delete job.img;
1119 job.img = nullptr;
1120 }
1121
1122 job.picture.reset( nullptr );
1123 job.maskPainters.clear();
1124 job.maskPaintDevices.clear();
1125}
1126
1127
1128#define DEBUG_RENDERING 0
1129
1130QImage QgsMapRendererJob::composeImage( const QgsMapSettings &settings,
1131 const std::vector<LayerRenderJob> &jobs,
1132 const LabelRenderJob &labelJob,
1133 const QgsMapRendererCache *cache
1134 )
1135{
1136 QImage image( settings.deviceOutputSize(), settings.outputImageFormat() );
1137 image.setDevicePixelRatio( settings.devicePixelRatio() );
1138 image.setDotsPerMeterX( static_cast<int>( settings.outputDpi() * 39.37 ) );
1139 image.setDotsPerMeterY( static_cast<int>( settings.outputDpi() * 39.37 ) );
1140 image.fill( settings.backgroundColor().rgba() );
1141
1142 const QgsElevationShadingRenderer mapShadingRenderer = settings.elevationShadingRenderer();
1143 std::unique_ptr<QgsElevationMap> mainElevationMap;
1144 if ( mapShadingRenderer.isActive() )
1145 mainElevationMap.reset( new QgsElevationMap( settings.outputSize() ) );
1146
1147 QPainter painter( &image );
1148
1149#if DEBUG_RENDERING
1150 int i = 0;
1151#endif
1152 for ( const LayerRenderJob &job : jobs )
1153 {
1154 if ( job.renderAboveLabels )
1155 continue; // skip layer for now, it will be rendered after labels
1156
1157 QImage img = layerImageToBeComposed( settings, job, cache );
1158 if ( img.isNull() )
1159 continue; // image is not prepared and not even in cache
1160
1161 painter.setCompositionMode( job.blendMode );
1162 painter.setOpacity( job.opacity );
1163
1164 if ( mainElevationMap )
1165 {
1166 QgsElevationMap layerElevationMap = layerElevationToBeComposed( settings, job, cache );
1167 if ( layerElevationMap.isValid() )
1168 mainElevationMap->combine( layerElevationMap, mapShadingRenderer.combinedElevationMethod() );
1169 }
1170
1171
1172#if DEBUG_RENDERING
1173 img.save( QString( "/tmp/final_%1.png" ).arg( i ) );
1174 i++;
1175#endif
1176
1177 painter.drawImage( 0, 0, img );
1178 }
1179
1180 if ( mapShadingRenderer.isActive() && mainElevationMap )
1181 {
1182 mapShadingRenderer.renderShading( *mainElevationMap.get(), image, QgsRenderContext::fromMapSettings( settings ) );
1183 }
1184
1185 // IMPORTANT - don't draw labelJob img before the label job is complete,
1186 // as the image is uninitialized and full of garbage before the label job
1187 // commences
1188 if ( labelJob.img && labelJob.complete )
1189 {
1190 painter.setCompositionMode( QPainter::CompositionMode_SourceOver );
1191 painter.setOpacity( 1.0 );
1192 painter.drawImage( 0, 0, *labelJob.img );
1193 }
1194 // when checking for a label cache image, we only look for those which would be drawn between 30% and 300% of the
1195 // original size. We don't want to draw massive pixelated labels on top of everything else, and we also don't need
1196 // to draw tiny unreadable labels... better to draw nothing in this case and wait till the updated label results are ready!
1197 else if ( cache && cache->hasAnyCacheImage( LABEL_PREVIEW_CACHE_ID, 0.3, 3 ) )
1198 {
1199 const QImage labelCacheImage = cache->transformedCacheImage( LABEL_PREVIEW_CACHE_ID, settings.mapToPixel() );
1200 painter.setCompositionMode( QPainter::CompositionMode_SourceOver );
1201 painter.setOpacity( 1.0 );
1202 painter.drawImage( 0, 0, labelCacheImage );
1203 }
1204
1205 // render any layers with the renderAboveLabels flag now
1206 for ( const LayerRenderJob &job : jobs )
1207 {
1208 if ( !job.renderAboveLabels )
1209 continue;
1210
1211 QImage img = layerImageToBeComposed( settings, job, cache );
1212 if ( img.isNull() )
1213 continue; // image is not prepared and not even in cache
1214
1215 painter.setCompositionMode( job.blendMode );
1216 painter.setOpacity( job.opacity );
1217
1218 painter.drawImage( 0, 0, img );
1219 }
1220
1221 painter.end();
1222#if DEBUG_RENDERING
1223 image.save( "/tmp/final.png" );
1224#endif
1225 return image;
1226}
1227
1229 const QgsMapSettings &settings,
1230 const LayerRenderJob &job,
1231 const QgsMapRendererCache *cache
1232)
1233{
1234 if ( job.imageCanBeComposed() )
1235 {
1236 Q_ASSERT( job.img );
1237 return *job.img;
1238 }
1239 else
1240 {
1241 if ( cache && cache->hasAnyCacheImage( job.layerId + QStringLiteral( "_preview" ) ) )
1242 {
1243 return cache->transformedCacheImage( job.layerId + QStringLiteral( "_preview" ), settings.mapToPixel() );
1244 }
1245 else
1246 return QImage();
1247 }
1248}
1249
1250QgsElevationMap QgsMapRendererJob::layerElevationToBeComposed( const QgsMapSettings &settings, const LayerRenderJob &job, const QgsMapRendererCache *cache )
1251{
1252 if ( job.imageCanBeComposed() && job.elevationMap )
1253 {
1254 return *job.elevationMap;
1255 }
1256 else
1257 {
1258 if ( cache && cache->hasAnyCacheImage( ELEVATION_MAP_CACHE_PREFIX + job.layerId + QStringLiteral( "_preview" ) ) )
1259 return QgsElevationMap( cache->transformedCacheImage( ELEVATION_MAP_CACHE_PREFIX + job.layerId + QStringLiteral( "_preview" ), settings.mapToPixel() ) );
1260 else
1261 return QgsElevationMap();
1262 }
1263}
1264
1265void QgsMapRendererJob::composeSecondPass( std::vector<LayerRenderJob> &secondPassJobs, LabelRenderJob &labelJob, bool forceVector )
1266{
1267 // compose the second pass with the mask
1268 for ( LayerRenderJob &job : secondPassJobs )
1269 {
1270 const bool isRasterRendering = !forceVector || job.maskRequiresLayerRasterization;
1271
1272 // Merge all mask images into the first one if we have more than one mask image
1273 if ( isRasterRendering && job.maskJobs.size() > 1 )
1274 {
1275 QPainter *maskPainter = nullptr;
1276 for ( QPair<LayerRenderJob *, int> p : job.maskJobs )
1277 {
1278 QImage *maskImage = static_cast<QImage *>( p.first ? p.first->maskPaintDevice.get() : labelJob.maskPaintDevices[p.second].get() );
1279 if ( !maskPainter )
1280 {
1281 maskPainter = p.first ? p.first->maskPainter.get() : labelJob.maskPainters[ p.second ].get();
1282 }
1283 else
1284 {
1285 maskPainter->drawImage( 0, 0, *maskImage );
1286 }
1287 }
1288 }
1289
1290 if ( ! job.maskJobs.isEmpty() )
1291 {
1292 // All have been merged into the first
1293 QPair<LayerRenderJob *, int> p = *job.maskJobs.begin();
1294 if ( isRasterRendering )
1295 {
1296 QImage *maskImage = static_cast<QImage *>( p.first ? p.first->maskPaintDevice.get() : labelJob.maskPaintDevices[p.second].get() );
1297
1298 // Only retain parts of the second rendering that are "inside" the mask image
1299 QPainter *painter = job.context()->painter();
1300
1301 painter->setCompositionMode( QPainter::CompositionMode_DestinationIn );
1302
1303 //Create an "alpha binarized" image of the maskImage to :
1304 //* Eliminate antialiasing artifact
1305 //* Avoid applying mask opacity to elements under the mask but not masked
1306 QImage maskBinAlpha = maskImage->createMaskFromColor( 0 );
1307 QVector<QRgb> mswTable;
1308 mswTable.push_back( qRgba( 0, 0, 0, 255 ) );
1309 mswTable.push_back( qRgba( 0, 0, 0, 0 ) );
1310 maskBinAlpha.setColorTable( mswTable );
1311 painter->drawImage( 0, 0, maskBinAlpha );
1312
1313 // Modify the first pass' image ...
1314 {
1315 QPainter tempPainter;
1316
1317 // reuse the first pass painter, if available
1318 QPainter *painter1 = job.firstPassJob->context()->painter();
1319 if ( ! painter1 )
1320 {
1321 tempPainter.begin( job.firstPassJob->img );
1322 painter1 = &tempPainter;
1323 }
1324
1325 // ... first retain parts that are "outside" the mask image
1326 painter1->setCompositionMode( QPainter::CompositionMode_DestinationOut );
1327 painter1->drawImage( 0, 0, *maskImage );
1328
1329 // ... and overpaint the second pass' image on it
1330 painter1->setCompositionMode( QPainter::CompositionMode_DestinationOver );
1331 painter1->drawImage( 0, 0, *job.img );
1332 }
1333 }
1334 else
1335 {
1336 job.firstPassJob->picture = std::move( job.picture );
1337 job.picture = nullptr;
1338 }
1339 }
1340 }
1341}
1342
1343void QgsMapRendererJob::logRenderingTime( const std::vector< LayerRenderJob > &jobs, const std::vector< LayerRenderJob > &secondPassJobs, const LabelRenderJob &labelJob )
1344{
1346 return;
1347
1348 QMultiMap<int, QString> elapsed;
1349 for ( const LayerRenderJob &job : jobs )
1350 elapsed.insert( job.renderingTime, job.layerId );
1351 for ( const LayerRenderJob &job : secondPassJobs )
1352 elapsed.insert( job.renderingTime, job.layerId + QString( " (second pass)" ) );
1353
1354 elapsed.insert( labelJob.renderingTime, tr( "Labeling" ) );
1355
1356 QList<int> tt( elapsed.uniqueKeys() );
1357 std::sort( tt.begin(), tt.end(), std::greater<int>() );
1358 for ( int t : std::as_const( tt ) )
1359 {
1360 QgsMessageLog::logMessage( tr( "%1 ms: %2" ).arg( t ).arg( QStringList( elapsed.values( t ) ).join( QLatin1String( ", " ) ) ), tr( "Rendering" ) );
1361 }
1362 QgsMessageLog::logMessage( QStringLiteral( "---" ), tr( "Rendering" ) );
1363}
1364
1365void QgsMapRendererJob::drawLabeling( QgsRenderContext &renderContext, QgsLabelingEngine *labelingEngine2, QPainter *painter )
1366{
1367 QgsDebugMsgLevel( QStringLiteral( "Draw labeling start" ), 5 );
1368
1369 QElapsedTimer t;
1370 t.start();
1371
1372 // Reset the composition mode before rendering the labels
1373 painter->setCompositionMode( QPainter::CompositionMode_SourceOver );
1374
1375 renderContext.setPainter( painter );
1376
1377 if ( labelingEngine2 )
1378 {
1379 labelingEngine2->run( renderContext );
1380 }
1381
1382 QgsDebugMsgLevel( QStringLiteral( "Draw labeling took (seconds): %1" ).arg( t.elapsed() / 1000. ), 2 );
1383}
1384
1385void QgsMapRendererJob::drawLabeling( const QgsMapSettings &settings, QgsRenderContext &renderContext, QgsLabelingEngine *labelingEngine2, QPainter *painter )
1386{
1387 Q_UNUSED( settings )
1388
1389 drawLabeling( renderContext, labelingEngine2, painter );
1390}
1391
@ InternalLayerOpacityHandling
The renderer internally handles the raster layer's opacity, so the default layer level opacity handli...
@ ApplyClipAfterReprojection
Feature geometry clipping to mapExtent() must be performed after the geometries are transformed using...
@ ForceVectorOutput
Vector graphics should not be cached and drawn as raster images.
@ ForceRasterMasks
Force symbol masking to be applied using a raster method. This is considerably faster when compared t...
virtual bool requiresAdvancedEffects() const =0
Returns true if drawing labels requires advanced effects like composition modes, which could prevent ...
Class for doing transforms between two map coordinate systems.
QgsCoordinateReferenceSystem sourceCrs() const
Returns the source coordinate reference system, which the transform will transform coordinates from.
void setBallparkTransformsAreAppropriate(bool appropriate)
Sets whether approximate "ballpark" results are appropriate for this coordinate transform.
void setDestinationCrs(const QgsCoordinateReferenceSystem &crs)
Sets the destination coordinate reference system.
bool isValid() const
Returns true if the coordinate transform is valid, ie both the source and destination CRS have been s...
QgsCoordinateReferenceSystem destinationCrs() const
Returns the destination coordinate reference system, which the transform will transform coordinates t...
QgsRectangle transformBoundingBox(const QgsRectangle &rectangle, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward, bool handle180Crossover=false) const SIP_THROW(QgsCsException)
Transforms a rectangle from the source CRS to the destination CRS.
QgsPointXY transform(const QgsPointXY &point, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward) const SIP_THROW(QgsCsException)
Transform the point from the source CRS to the destination CRS.
Custom exception class for Coordinate Reference System related exceptions.
Definition: qgsexception.h:67
bool isInfinite() const
Returns true if the range consists of all possible values.
Definition: qgsrange.h:247
Stores digital elevation model in a raster image which may get updated as a part of map layer renderi...
bool isValid() const
Returns whether the elevation map is valid.
This class can render elevation shading on an image with different methods (eye dome lighting,...
Qgis::ElevationMapCombineMethod combinedElevationMethod() const
Returns the method used when conbining different elevation sources.
bool isActive() const
Returns whether this shading renderer is active.
void renderShading(const QgsElevationMap &elevation, QImage &image, const QgsRenderContext &context) const
Render shading on image condidering the elevation map elevation and the renderer context context If e...
static QgsExpressionContextScope * layerScope(const QgsMapLayer *layer)
Creates a new scope which contains variables and functions relating to a QgsMapLayer.
QgsFeedback subclass for granular reporting of labeling engine progress.
The QgsLabelingEngine class provides map labeling functionality.
virtual void run(QgsRenderContext &context)=0
Runs the labeling job.
static void warning(const QString &msg)
Goes to qWarning.
Definition: qgslogger.cpp:131
virtual bool hasElevation() const
Returns true if the layer has an elevation or z component.
virtual bool isVisibleInZRange(const QgsDoubleRange &range) const
Returns true if the layer should be visible and rendered for the specified z range.
virtual void setLayerRenderingTimeHint(int time)
Sets approximate render time (in ms) for the layer to render.
Restore overridden layer style on destruction.
virtual bool isVisibleInTemporalRange(const QgsDateTimeRange &range) const
Returns true if the layer should be visible and rendered for the specified time range.
Base class for all map layer types.
Definition: qgsmaplayer.h:73
QString name
Definition: qgsmaplayer.h:76
bool isInScaleRange(double scale) const
Tests whether the layer should be visible at the specified scale.
Q_INVOKABLE QVariant customProperty(const QString &value, const QVariant &defaultValue=QVariant()) const
Read a custom property from layer.
QgsCoordinateReferenceSystem crs
Definition: qgsmaplayer.h:79
QString id() const
Returns the layer's unique ID, which is used to access this layer from QgsProject.
Qgis::LayerType type
Definition: qgsmaplayer.h:80
QPainter::CompositionMode blendMode() const
Returns the current blending mode for a layer.
bool hasScaleBasedVisibility() const
Returns whether scale based visibility is enabled for the layer.
virtual QgsMapLayerTemporalProperties * temporalProperties()
Returns the layer's temporal properties.
Definition: qgsmaplayer.h:1526
virtual QgsMapLayerRenderer * createMapRenderer(QgsRenderContext &rendererContext)=0
Returns new instance of QgsMapLayerRenderer that will be used for rendering of given context.
bool isValid
Definition: qgsmaplayer.h:81
double minimumScale() const
Returns the minimum map scale (i.e.
virtual QgsMapLayerElevationProperties * elevationProperties()
Returns the layer's elevation properties.
Definition: qgsmaplayer.h:1533
double opacity
Definition: qgsmaplayer.h:82
double maximumScale() const
Returns the maximum map scale (i.e.
This class is responsible for keeping cache of rendered images resulting from a map rendering job.
bool updateParameters(const QgsRectangle &extent, const QgsMapToPixel &mtp)
Sets extent and scale parameters.
QList< QgsMapLayer * > dependentLayers(const QString &cacheKey) const
Returns a list of map layers on which an image in the cache depends.
bool hasCacheImage(const QString &cacheKey) const
Returns true if the cache contains an image with the specified cacheKey that has the same extent and ...
QImage cacheImage(const QString &cacheKey) const
Returns the cached image for the specified cacheKey.
bool hasAnyCacheImage(const QString &cacheKey, double minimumScaleThreshold=0, double maximumScaleThreshold=0) const
Returns true if the cache contains an image with the specified cacheKey with any cache's parameters (...
void setCacheImageWithParameters(const QString &cacheKey, const QImage &image, const QgsRectangle &extent, const QgsMapToPixel &mapToPixel, const QList< QgsMapLayer * > &dependentLayers=QList< QgsMapLayer * >())
Set the cached image for a particular cacheKey, using a specific extent and mapToPixel (which may dif...
void clearCacheImage(const QString &cacheKey)
Removes an image from the cache with matching cacheKey.
QImage transformedCacheImage(const QString &cacheKey, const QgsMapToPixel &mtp) const
Returns the cached image for the specified cacheKey transformed to the particular extent and scale.
Abstract base class for map rendering implementations.
void logRenderingTime(const std::vector< LayerRenderJob > &jobs, const std::vector< LayerRenderJob > &secondPassJobs, const LabelRenderJob &labelJob)
static QgsElevationMap layerElevationToBeComposed(const QgsMapSettings &settings, const LayerRenderJob &job, const QgsMapRendererCache *cache)
static QImage composeImage(const QgsMapSettings &settings, const std::vector< LayerRenderJob > &jobs, const LabelRenderJob &labelJob, const QgsMapRendererCache *cache=nullptr)
void cleanupSecondPassJobs(std::vector< LayerRenderJob > &jobs)
void setCache(QgsMapRendererCache *cache)
Assign a cache to be used for reading and storing rendered images of individual layers.
QHash< QgsMapLayer *, int > perLayerRenderingTime() const
Returns the render time (in ms) per layer.
void initSecondPassJobs(std::vector< LayerRenderJob > &secondPassJobs, LabelRenderJob &labelJob) const
Initialize secondPassJobs according to what have been rendered (mask clipping path e....
static QImage layerImageToBeComposed(const QgsMapSettings &settings, const LayerRenderJob &job, const QgsMapRendererCache *cache)
QHash< QString, int > mLayerRenderingTimeHints
Approximate expected layer rendering time per layer, by layer ID.
std::unique_ptr< QgsRenderedItemResults > mRenderedItemResults
Errors errors() const
List of errors that happened during the rendering job - available when the rendering has been finishe...
static Q_DECL_DEPRECATED void drawLabeling(const QgsMapSettings &settings, QgsRenderContext &renderContext, QgsLabelingEngine *labelingEngine2, QPainter *painter)
static const QString LABEL_PREVIEW_CACHE_ID
QgsMapRendererCache ID string for cached label image during preview compositions only.
std::vector< LayerRenderJob > prepareJobs(QPainter *painter, QgsLabelingEngine *labelingEngine2, bool deferredPainterSet=false)
Creates a list of layer rendering jobs and prepares them for later render.
void cleanupJobs(std::vector< LayerRenderJob > &jobs)
const QgsMapSettings & mapSettings() const
Returns map settings with which this job was started.
QgsMapRendererCache * mCache
void finished()
emitted when asynchronous rendering is finished (or canceled).
QgsMapSettings mSettings
static const QgsSettingsEntryBool * settingsLogCanvasRefreshEvent
Settings entry log canvas refresh event.
QgsMapRendererJob(const QgsMapSettings &settings)
~QgsMapRendererJob() override
void start()
Start the rendering job and immediately return.
QStringList mLayersRedrawnFromCache
QStringList layersRedrawnFromCache() const
Returns a list of the layer IDs for all layers which were redrawn from cached images.
QList< QgsMapRendererJob::Error > Errors
static const QString LABEL_CACHE_ID
QgsMapRendererCache ID string for cached label image.
static const QString ELEVATION_MAP_CACHE_PREFIX
QgsMapRendererCache prefix string for cached elevation map image.
QHash< QgsWeakMapLayerPointer, int > mPerLayerRenderingTime
Render time (in ms) per layer, by layer ID.
QgsRenderedItemResults * takeRenderedItemResults()
Takes the rendered item results from the map render job and returns them.
QgsLabelingEngineFeedback * labelingEngineFeedback()
Returns the associated labeling engine feedback object.
static void composeSecondPass(std::vector< LayerRenderJob > &secondPassJobs, LabelRenderJob &labelJob, bool forceVector=false)
Compose second pass images into first pass images.
std::vector< LayerRenderJob > prepareSecondPassJobs(std::vector< LayerRenderJob > &firstPassJobs, LabelRenderJob &labelJob)
Prepares jobs for a second pass, if selective masks exist (from labels or symbol layers).
LabelRenderJob prepareLabelingJob(QPainter *painter, QgsLabelingEngine *labelingEngine2, bool canUseLabelCache=true)
Prepares a labeling job.
void setLayerRenderingTimeHints(const QHash< QString, int > &hints)
Sets approximate render times (in ms) for map layers.
void cleanupLabelJob(LabelRenderJob &job)
Handles clean up tasks for a label job, including deletion of images and storing cached label results...
QgsLabelSink * labelSink() const
Returns the label sink associated to this rendering job.
bool prepareLabelCache() const
Prepares the cache for storing the result of labeling.
QgsMapRendererQImageJob(const QgsMapSettings &settings)
The QgsMapSettings class contains configuration for rendering of the map.
QSize deviceOutputSize() const
Returns the device output size of the map render.
QList< QgsMapLayer * > layers(bool expandGroupLayers=false) const
Returns the list of layers which will be rendered in the map.
double scale() const
Returns the calculated map scale.
QgsCoordinateTransform layerTransform(const QgsMapLayer *layer) const
Returns the coordinate transform from layer's CRS to destination CRS.
QgsDoubleRange zRange() const
Returns the range of z-values which will be visible in the map.
QColor backgroundColor() const
Returns the background color of the map.
const QgsMapToPixel & mapToPixel() const
float devicePixelRatio() const
Returns the device pixel ratio.
QSize outputSize() const
Returns the size of the resulting map image, in pixels.
QImage::Format outputImageFormat() const
format of internal QImage, default QImage::Format_ARGB32_Premultiplied
double extentBuffer() const
Returns the buffer in map units to use around the visible extent for rendering symbols whose correspo...
QgsRectangle visibleExtent() const
Returns the actual extent derived from requested extent that takes output image size into account.
const QgsElevationShadingRenderer & elevationShadingRenderer() const
Returns the shading renderer used to render shading on the entire map.
double outputDpi() const
Returns the DPI (dots per inch) used for conversion between real world units (e.g.
bool testFlag(Qgis::MapSettingsFlag flag) const
Check whether a particular flag is enabled.
QMap< QString, QString > layerStyleOverrides() const
Returns the map of map layer style overrides (key: layer ID, value: style name) where a different sty...
bool hasValidSettings() const
Check whether the map settings are valid and can be used for rendering.
QgsCoordinateReferenceSystem destinationCrs() const
Returns the destination coordinate reference system for the map render.
Mask painter device that can be used to register everything painted into a QPainterPath used later as...
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::MessageLevel::Warning, bool notifyUser=true)
Adds a message to the log instance (and creates it if necessary).
static bool staticWillUseLayer(const QgsMapLayer *layer)
Called to find out whether a specified layer is used for labeling.
A class to represent a 2D point.
Definition: qgspointxy.h:59
QString toString(int precision=-1) const
Returns a string representation of the point (x, y) with a preset precision.
Definition: qgspointxy.cpp:51
Q_GADGET double x
Definition: qgspointxy.h:62
Represents a raster layer.
QgsRasterRenderer * renderer() const
Returns the raster's renderer.
virtual Qgis::RasterRendererFlags flags() const
Returns flags which dictate renderer behavior.
A rectangle specified with double values.
Definition: qgsrectangle.h:42
QString toString(int precision=16) const
Returns a string representation of form xmin,ymin : xmax,ymax Coordinates will be truncated to the sp...
double yMaximum() const SIP_HOLDGIL
Returns the y maximum value (top side of rectangle).
Definition: qgsrectangle.h:193
double xMaximum() const SIP_HOLDGIL
Returns the x maximum value (right side of rectangle).
Definition: qgsrectangle.h:183
double xMinimum() const SIP_HOLDGIL
Returns the x minimum value (left side of rectangle).
Definition: qgsrectangle.h:188
double yMinimum() const SIP_HOLDGIL
Returns the y minimum value (bottom side of rectangle).
Definition: qgsrectangle.h:198
void setXMaximum(double x) SIP_HOLDGIL
Set the maximum x value.
Definition: qgsrectangle.h:156
void setXMinimum(double x) SIP_HOLDGIL
Set the minimum x value.
Definition: qgsrectangle.h:151
double height() const SIP_HOLDGIL
Returns the height of the rectangle.
Definition: qgsrectangle.h:230
void grow(double delta)
Grows the rectangle in place by the specified amount.
Definition: qgsrectangle.h:296
double width() const SIP_HOLDGIL
Returns the width of the rectangle.
Definition: qgsrectangle.h:223
bool isFinite() const
Returns true if the rectangle has finite boundaries.
Definition: qgsrectangle.h:559
Contains information about the context of a rendering operation.
void setPainterFlagsUsingContext(QPainter *painter=nullptr) const
Sets relevant flags on a destination painter, using the flags and settings currently defined for the ...
void setPainter(QPainter *p)
Sets the destination QPainter for the render operation.
static QgsRenderContext fromMapSettings(const QgsMapSettings &mapSettings)
create initialized QgsRenderContext instance from given QgsMapSettings
Stores collated details of rendered items during a map rendering operation.
T value(const QString &dynamicKeyPart=QString()) const
Returns settings value.
A boolean settings entry.
static QgsSettingsTreeNode * sTreeMap
Type used to refer to a specific symbol layer in a symbol of a layer.
const QgsDateTimeRange & temporalRange() const
Returns the datetime range for the object.
bool isTemporal() const
Returns true if the object's temporal range is enabled, and the object will be filtered when renderin...
Implementation of threaded rendering for vector layers.
static QgsMaskedLayers symbolLayerMasks(const QgsVectorLayer *)
Returns all masks that may be defined on symbol layers for a given vector layer.
static QHash< QString, QgsMaskedLayers > labelMasks(const QgsVectorLayer *)
Returns masks defined in labeling options of a layer.
Represents a vector layer which manages a vector based data sets.
bool labelsEnabled() const
Returns whether the layer contains labels which are enabled and should be drawn.
const QgsAbstractVectorLayerLabeling * labeling() const
Access to const labeling configuration.
bool isEditable() const FINAL
Returns true if the provider is in editing mode.
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:3988
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
#define QgsDebugError(str)
Definition: qgslogger.h:38
QHash< QString, QgsMaskedLayer > QgsMaskedLayers
masked layers where key is the layer id