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