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