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