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