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