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 "qgslogger.h"
24 #include "qgsrendercontext.h"
25 #include "qgsmaplayer.h"
26 #include "qgsproject.h"
27 #include "qgsmaplayerrenderer.h"
28 #include "qgsmaplayerstylemanager.h"
29 #include "qgsmaprenderercache.h"
30 #include "qgsmessagelog.h"
31 #include "qgspallabeling.h"
32 #include "qgsexception.h"
33 #include "qgslabelingengine.h"
34 #include "qgsmaplayerlistutils.h"
35 #include "qgsvectorlayerlabeling.h"
36 #include "qgssettings.h"
37 #include "qgsexpressioncontextutils.h"
38 #include "qgssymbol.h"
39 #include "qgsrenderer.h"
40 #include "qgssymbollayer.h"
41 #include "qgsvectorlayerutils.h"
42 #include "qgssymbollayerutils.h"
43 #include "qgsmaplayertemporalproperties.h"
44 #include "qgsmaplayerelevationproperties.h"
45 #include "qgsvectorlayerrenderer.h"
46 #include "qgsrendereditemresults.h"
47 
48 ///@cond PRIVATE
49 
50 const QString QgsMapRendererJob::LABEL_CACHE_ID = QStringLiteral( "_labels_" );
51 const QString QgsMapRendererJob::LABEL_PREVIEW_CACHE_ID = QStringLiteral( "_preview_labels_" );
52 
operator =(LayerRenderJob && other)53 LayerRenderJob &LayerRenderJob::operator=( LayerRenderJob &&other )
54 {
55   mContext = std::move( other.mContext );
56 
57   img = other.img;
58   other.img = nullptr;
59 
60   renderer = other.renderer;
61   other.renderer = nullptr;
62 
63   imageInitialized = other.imageInitialized;
64   blendMode = other.blendMode;
65   opacity = other.opacity;
66   cached = other.cached;
67   layer = other.layer;
68   completed = other.completed;
69   renderingTime = other.renderingTime;
70   estimatedRenderingTime = other.estimatedRenderingTime ;
71   errors = other.errors;
72   layerId = other.layerId;
73 
74   maskImage = other.maskImage;
75   other.maskImage = nullptr;
76 
77   firstPassJob = other.firstPassJob;
78   other.firstPassJob = nullptr;
79 
80   maskJobs = other.maskJobs;
81 
82   return *this;
83 }
84 
LayerRenderJob(LayerRenderJob && other)85 LayerRenderJob::LayerRenderJob( LayerRenderJob &&other )
86   : imageInitialized( other.imageInitialized )
87   , blendMode( other.blendMode )
88   , opacity( other.opacity )
89   , cached( other.cached )
90   , layer( other.layer )
91   , completed( other.completed )
92   , renderingTime( other.renderingTime )
93   , estimatedRenderingTime( other.estimatedRenderingTime )
94   , errors( other.errors )
95   , layerId( other.layerId )
96   , maskJobs( other.maskJobs )
97 {
98   mContext = std::move( other.mContext );
99 
100   img = other.img;
101   other.img = nullptr;
102 
103   renderer = other.renderer;
104   other.renderer = nullptr;
105 
106   maskImage = other.maskImage;
107   other.maskImage = nullptr;
108 
109   firstPassJob = other.firstPassJob;
110   other.firstPassJob = nullptr;
111 }
112 
imageCanBeComposed() const113 bool LayerRenderJob::imageCanBeComposed() const
114 {
115   if ( imageInitialized )
116   {
117     if ( renderer )
118     {
119       return renderer->isReadyToCompose();
120     }
121     else
122     {
123       return true;
124     }
125   }
126   else
127   {
128     return false;
129   }
130 }
131 
QgsMapRendererJob(const QgsMapSettings & settings)132 QgsMapRendererJob::QgsMapRendererJob( const QgsMapSettings &settings )
133   : mSettings( settings )
134   , mRenderedItemResults( std::make_unique< QgsRenderedItemResults >( settings.extent() ) )
135 {}
136 
137 QgsMapRendererJob::~QgsMapRendererJob() = default;
138 
start()139 void QgsMapRendererJob::start()
140 {
141   if ( mSettings.hasValidSettings() )
142     startPrivate();
143   else
144   {
145     mErrors.append( QgsMapRendererJob::Error( QString(), tr( "Invalid map settings" ) ) );
146     emit finished();
147   }
148 }
149 
layersRedrawnFromCache() const150 QStringList QgsMapRendererJob::layersRedrawnFromCache() const
151 {
152   return mLayersRedrawnFromCache;
153 }
154 
takeRenderedItemResults()155 QgsRenderedItemResults *QgsMapRendererJob::takeRenderedItemResults()
156 {
157   return mRenderedItemResults.release();
158 }
159 
QgsMapRendererQImageJob(const QgsMapSettings & settings)160 QgsMapRendererQImageJob::QgsMapRendererQImageJob( const QgsMapSettings &settings )
161   : QgsMapRendererJob( settings )
162 {
163 }
164 
165 
errors() const166 QgsMapRendererJob::Errors QgsMapRendererJob::errors() const
167 {
168   return mErrors;
169 }
170 
setCache(QgsMapRendererCache * cache)171 void QgsMapRendererJob::setCache( QgsMapRendererCache *cache )
172 {
173   mCache = cache;
174 }
175 
perLayerRenderingTime() const176 QHash<QgsMapLayer *, int> QgsMapRendererJob::perLayerRenderingTime() const
177 {
178   QHash<QgsMapLayer *, int> result;
179   for ( auto it = mPerLayerRenderingTime.constBegin(); it != mPerLayerRenderingTime.constEnd(); ++it )
180   {
181     if ( auto &&lKey = it.key() )
182       result.insert( lKey, it.value() );
183   }
184   return result;
185 }
186 
setLayerRenderingTimeHints(const QHash<QString,int> & hints)187 void QgsMapRendererJob::setLayerRenderingTimeHints( const QHash<QString, int> &hints )
188 {
189   mLayerRenderingTimeHints = hints;
190 }
191 
mapSettings() const192 const QgsMapSettings &QgsMapRendererJob::mapSettings() const
193 {
194   return mSettings;
195 }
196 
prepareLabelCache() const197 bool QgsMapRendererJob::prepareLabelCache() const
198 {
199   bool canCache = mCache;
200 
201   // calculate which layers will be labeled
202   QSet< QgsMapLayer * > labeledLayers;
203   const QList<QgsMapLayer *> layers = mSettings.layers();
204   for ( QgsMapLayer *ml : layers )
205   {
206     if ( QgsPalLabeling::staticWillUseLayer( ml ) )
207       labeledLayers << ml;
208 
209     switch ( ml->type() )
210     {
211       case QgsMapLayerType::VectorLayer:
212       {
213         QgsVectorLayer *vl = qobject_cast< QgsVectorLayer *>( ml );
214         if ( vl->labelsEnabled() && vl->labeling()->requiresAdvancedEffects() )
215         {
216           canCache = false;
217         }
218         break;
219       }
220 
221       case QgsMapLayerType::VectorTileLayer:
222       {
223         // TODO -- add detection of advanced labeling effects for vector tile layers
224         break;
225       }
226 
227       case QgsMapLayerType::RasterLayer:
228       case QgsMapLayerType::AnnotationLayer:
229       case QgsMapLayerType::PluginLayer:
230       case QgsMapLayerType::MeshLayer:
231       case QgsMapLayerType::PointCloudLayer:
232         break;
233     }
234 
235     if ( !canCache )
236       break;
237 
238   }
239 
240   if ( mCache && mCache->hasCacheImage( LABEL_CACHE_ID ) )
241   {
242     // we may need to clear label cache and re-register labeled features - check for that here
243 
244     // can we reuse the cached label solution?
245     bool canUseCache = canCache && qgis::listToSet( mCache->dependentLayers( LABEL_CACHE_ID ) ) == labeledLayers;
246     if ( !canUseCache )
247     {
248       // no - participating layers have changed
249       mCache->clearCacheImage( LABEL_CACHE_ID );
250     }
251   }
252   return canCache;
253 }
254 
255 
reprojectToLayerExtent(const QgsMapLayer * ml,const QgsCoordinateTransform & ct,QgsRectangle & extent,QgsRectangle & r2)256 bool QgsMapRendererJob::reprojectToLayerExtent( const QgsMapLayer *ml, const QgsCoordinateTransform &ct, QgsRectangle &extent, QgsRectangle &r2 )
257 {
258   bool res = true;
259   // we can safely use ballpark transforms without bothering the user here -- at the likely scale of layer extents there
260   // won't be an appreciable difference, and we aren't actually transforming any rendered points here anyway (just the layer extent)
261   QgsCoordinateTransform approxTransform = ct;
262   approxTransform.setBallparkTransformsAreAppropriate( true );
263 
264   try
265   {
266 #ifdef QGISDEBUG
267     // QgsLogger::debug<QgsRectangle>("Getting extent of canvas in layers CS. Canvas is ", extent, __FILE__, __FUNCTION__, __LINE__);
268 #endif
269     // Split the extent into two if the source CRS is
270     // geographic and the extent crosses the split in
271     // geographic coordinates (usually +/- 180 degrees,
272     // and is assumed to be so here), and draw each
273     // extent separately.
274     static const double SPLIT_COORD = 180.0;
275 
276     if ( ml->crs().isGeographic() )
277     {
278       if ( ml->type() == QgsMapLayerType::VectorLayer && !approxTransform.destinationCrs().isGeographic() )
279       {
280         // if we transform from a projected coordinate system check
281         // check if transforming back roughly returns the input
282         // extend - otherwise render the world.
283         QgsRectangle extent1 = approxTransform.transformBoundingBox( extent, Qgis::TransformDirection::Reverse );
284         QgsRectangle extent2 = approxTransform.transformBoundingBox( extent1, Qgis::TransformDirection::Forward );
285 
286         QgsDebugMsgLevel( QStringLiteral( "\n0:%1 %2x%3\n1:%4\n2:%5 %6x%7 (w:%8 h:%9)" )
287                           .arg( extent.toString() ).arg( extent.width() ).arg( extent.height() )
288                           .arg( extent1.toString(), extent2.toString() ).arg( extent2.width() ).arg( extent2.height() )
289                           .arg( std::fabs( 1.0 - extent2.width() / extent.width() ) )
290                           .arg( std::fabs( 1.0 - extent2.height() / extent.height() ) )
291                           , 3 );
292 
293         // can differ by a maximum of up to 20% of height/width
294         if ( qgsDoubleNear( extent2.xMinimum(), extent.xMinimum(), extent.width() * 0.2 )
295              && qgsDoubleNear( extent2.xMaximum(), extent.xMaximum(), extent.width() * 0.2 )
296              && qgsDoubleNear( extent2.yMinimum(), extent.yMinimum(), extent.height() * 0.2 )
297              && qgsDoubleNear( extent2.yMaximum(), extent.yMaximum(), extent.height() * 0.2 )
298            )
299         {
300           extent = extent1;
301         }
302         else
303         {
304           extent = QgsRectangle( -180.0, -90.0, 180.0, 90.0 );
305           res = false;
306         }
307       }
308       else
309       {
310         // Note: ll = lower left point
311         QgsPointXY ll = approxTransform.transform( extent.xMinimum(), extent.yMinimum(),
312                         Qgis::TransformDirection::Reverse );
313 
314         //   and ur = upper right point
315         QgsPointXY ur = approxTransform.transform( extent.xMaximum(), extent.yMaximum(),
316                         Qgis::TransformDirection::Reverse );
317 
318         QgsDebugMsgLevel( QStringLiteral( "in:%1 (ll:%2 ur:%3)" ).arg( extent.toString(), ll.toString(), ur.toString() ), 4 );
319 
320         extent = approxTransform.transformBoundingBox( extent, Qgis::TransformDirection::Reverse );
321 
322         QgsDebugMsgLevel( QStringLiteral( "out:%1 (w:%2 h:%3)" ).arg( extent.toString() ).arg( extent.width() ).arg( extent.height() ), 4 );
323 
324         if ( ll.x() > ur.x() )
325         {
326           // the coordinates projected in reverse order than what one would expect.
327           // we are probably looking at an area that includes longitude of 180 degrees.
328           // we need to take into account coordinates from two intervals: (-180,x1) and (x2,180)
329           // so let's use (-180,180). This hopefully does not add too much overhead. It is
330           // more straightforward than rendering with two separate extents and more consistent
331           // for rendering, labeling and caching as everything is rendered just in one go
332           extent.setXMinimum( -SPLIT_COORD );
333           extent.setXMaximum( SPLIT_COORD );
334           res = false;
335         }
336       }
337 
338       // TODO: the above rule still does not help if using a projection that covers the whole
339       // world. E.g. with EPSG:3857 the longitude spectrum -180 to +180 is mapped to approx.
340       // -2e7 to +2e7. Converting extent from -5e7 to +5e7 is transformed as -90 to +90,
341       // but in fact the extent should cover the whole world.
342     }
343     else // can't cross 180
344     {
345       if ( approxTransform.destinationCrs().isGeographic() &&
346            ( extent.xMinimum() <= -180 || extent.xMaximum() >= 180 ||
347              extent.yMinimum() <= -90 || extent.yMaximum() >= 90 ) )
348         // Use unlimited rectangle because otherwise we may end up transforming wrong coordinates.
349         // E.g. longitude -200 to +160 would be understood as +40 to +160 due to periodicity.
350         // We could try to clamp coords to (-180,180) for lon resp. (-90,90) for lat,
351         // but this seems like a safer choice.
352       {
353         extent = QgsRectangle( std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest(), std::numeric_limits<double>::max(), std::numeric_limits<double>::max() );
354         res = false;
355       }
356       else
357         extent = approxTransform.transformBoundingBox( extent, Qgis::TransformDirection::Reverse );
358     }
359   }
360   catch ( QgsCsException & )
361   {
362     QgsDebugMsg( QStringLiteral( "Transform error caught" ) );
363     extent = QgsRectangle( std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest(), std::numeric_limits<double>::max(), std::numeric_limits<double>::max() );
364     r2 = QgsRectangle( std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest(), std::numeric_limits<double>::max(), std::numeric_limits<double>::max() );
365     res = false;
366   }
367 
368   return res;
369 }
370 
allocateImage(QString layerId)371 QImage *QgsMapRendererJob::allocateImage( QString layerId )
372 {
373   QImage *image = new QImage( mSettings.deviceOutputSize(),
374                               mSettings.outputImageFormat() );
375   image->setDevicePixelRatio( static_cast<qreal>( mSettings.devicePixelRatio() ) );
376   image->setDotsPerMeterX( mSettings.devicePixelRatio() * 1000 * mSettings.outputDpi() / 25.4 );
377   image->setDotsPerMeterY( mSettings.devicePixelRatio() * 1000 * mSettings.outputDpi() / 25.4 );
378   if ( image->isNull() )
379   {
380     mErrors.append( Error( layerId, tr( "Insufficient memory for image %1x%2" ).arg( mSettings.outputSize().width() ).arg( mSettings.outputSize().height() ) ) );
381     delete image;
382     return nullptr;
383   }
384   return image;
385 }
386 
allocateImageAndPainter(QString layerId,QImage * & image)387 QPainter *QgsMapRendererJob::allocateImageAndPainter( QString layerId, QImage *&image )
388 {
389   QPainter *painter = nullptr;
390   image = allocateImage( layerId );
391   if ( image )
392   {
393     painter = new QPainter( image );
394     painter->setRenderHint( QPainter::Antialiasing, mSettings.testFlag( Qgis::MapSettingsFlag::Antialiasing ) );
395 #if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
396     painter->setRenderHint( QPainter::LosslessImageRendering, mSettings.testFlag( Qgis::MapSettingsFlag::LosslessImageRendering ) );
397 #endif
398   }
399   return painter;
400 }
401 
prepareJobs(QPainter * painter,QgsLabelingEngine * labelingEngine2,bool deferredPainterSet)402 std::vector<LayerRenderJob> QgsMapRendererJob::prepareJobs( QPainter *painter, QgsLabelingEngine *labelingEngine2, bool deferredPainterSet )
403 {
404   std::vector< LayerRenderJob > layerJobs;
405 
406   // render all layers in the stack, starting at the base
407   QListIterator<QgsMapLayer *> li( mSettings.layers() );
408   li.toBack();
409 
410   if ( mCache )
411   {
412     bool cacheValid = mCache->updateParameters( mSettings.visibleExtent(), mSettings.mapToPixel() );
413     Q_UNUSED( cacheValid )
414     QgsDebugMsgLevel( QStringLiteral( "CACHE VALID: %1" ).arg( cacheValid ), 4 );
415   }
416 
417   bool requiresLabelRedraw = !( mCache && mCache->hasCacheImage( LABEL_CACHE_ID ) );
418 
419   while ( li.hasPrevious() )
420   {
421     QgsMapLayer *ml = li.previous();
422 
423     QgsDebugMsgLevel( QStringLiteral( "layer %1:  minscale:%2  maxscale:%3  scaledepvis:%4  blendmode:%5 isValid:%6" )
424                       .arg( ml->name() )
425                       .arg( ml->minimumScale() )
426                       .arg( ml->maximumScale() )
427                       .arg( ml->hasScaleBasedVisibility() )
428                       .arg( ml->blendMode() )
429                       .arg( ml->isValid() )
430                       , 3 );
431 
432     if ( !ml->isValid() )
433     {
434       QgsDebugMsgLevel( QStringLiteral( "Invalid Layer skipped" ), 3 );
435       continue;
436     }
437 
438     if ( !ml->isInScaleRange( mSettings.scale() ) ) //|| mOverview )
439     {
440       QgsDebugMsgLevel( QStringLiteral( "Layer not rendered because it is not within the defined visibility scale range" ), 3 );
441       continue;
442     }
443 
444     if ( mSettings.isTemporal() && ml->temporalProperties() && !ml->temporalProperties()->isVisibleInTemporalRange( mSettings.temporalRange() ) )
445     {
446       QgsDebugMsgLevel( QStringLiteral( "Layer not rendered because it is not visible within the map's time range" ), 3 );
447       continue;
448     }
449 
450     if ( !mSettings.zRange().isInfinite() && ml->elevationProperties() && !ml->elevationProperties()->isVisibleInZRange( mSettings.zRange() ) )
451     {
452       QgsDebugMsgLevel( QStringLiteral( "Layer not rendered because it is not visible within the map's z range" ), 3 );
453       continue;
454     }
455 
456     QgsRectangle r1 = mSettings.visibleExtent(), r2;
457     r1.grow( mSettings.extentBuffer() );
458     QgsCoordinateTransform ct;
459 
460     ct = mSettings.layerTransform( ml );
461     bool haveExtentInLayerCrs = true;
462     if ( ct.isValid() )
463     {
464       haveExtentInLayerCrs = reprojectToLayerExtent( ml, ct, r1, r2 );
465     }
466     QgsDebugMsgLevel( "extent: " + r1.toString(), 3 );
467     if ( !r1.isFinite() || !r2.isFinite() )
468     {
469       mErrors.append( Error( ml->id(), tr( "There was a problem transforming the layer's extent. Layer skipped." ) ) );
470       continue;
471     }
472 
473     QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( ml );
474 
475     // Force render of layers that are being edited
476     // or if there's a labeling engine that needs the layer to register features
477     if ( mCache )
478     {
479       const bool requiresLabeling = ( labelingEngine2 && QgsPalLabeling::staticWillUseLayer( ml ) ) && requiresLabelRedraw;
480       if ( ( vl && vl->isEditable() ) || requiresLabeling )
481       {
482         mCache->clearCacheImage( ml->id() );
483       }
484     }
485 
486     layerJobs.emplace_back( LayerRenderJob() );
487     LayerRenderJob &job = layerJobs.back();
488     job.layer = ml;
489     job.layerId = ml->id();
490     job.estimatedRenderingTime = mLayerRenderingTimeHints.value( ml->id(), 0 );
491 
492     job.setContext( std::make_unique< QgsRenderContext >( QgsRenderContext::fromMapSettings( mSettings ) ) );
493     job.context()->expressionContext().appendScope( QgsExpressionContextUtils::layerScope( ml ) );
494     job.context()->setPainter( painter );
495     job.context()->setLabelingEngine( labelingEngine2 );
496     job.context()->setCoordinateTransform( ct );
497     job.context()->setExtent( r1 );
498     if ( !haveExtentInLayerCrs )
499       job.context()->setFlag( Qgis::RenderContextFlag::ApplyClipAfterReprojection, true );
500 
501     if ( mFeatureFilterProvider )
502       job.context()->setFeatureFilterProvider( mFeatureFilterProvider );
503 
504     QgsMapLayerStyleOverride styleOverride( ml );
505     if ( mSettings.layerStyleOverrides().contains( ml->id() ) )
506       styleOverride.setOverrideStyle( mSettings.layerStyleOverrides().value( ml->id() ) );
507 
508     job.blendMode = ml->blendMode();
509 
510     // raster layer opacity is handled directly within the raster layer renderer, so don't
511     // apply default opacity handling here!
512     job.opacity = ml->type() != QgsMapLayerType::RasterLayer ? ml->opacity() : 1.0;
513 
514     // if we can use the cache, let's do it and avoid rendering!
515     if ( mCache && mCache->hasCacheImage( ml->id() ) )
516     {
517       job.cached = true;
518       job.imageInitialized = true;
519       job.img = new QImage( mCache->cacheImage( ml->id() ) );
520       job.img->setDevicePixelRatio( static_cast<qreal>( mSettings.devicePixelRatio() ) );
521       job.renderer = nullptr;
522       job.context()->setPainter( nullptr );
523       mLayersRedrawnFromCache.append( ml->id() );
524       continue;
525     }
526 
527     QElapsedTimer layerTime;
528     layerTime.start();
529     job.renderer = ml->createMapRenderer( *( job.context() ) );
530     if ( job.renderer )
531     {
532       job.renderer->setLayerRenderingTimeHint( job.estimatedRenderingTime );
533       job.context()->setFeedback( job.renderer->feedback() );
534     }
535 
536     // If we are drawing with an alternative blending mode then we need to render to a separate image
537     // before compositing this on the map. This effectively flattens the layer and prevents
538     // blending occurring between objects on the layer
539     if ( mCache || ( !painter && !deferredPainterSet ) || ( job.renderer && job.renderer->forceRasterRender() ) )
540     {
541       // Flattened image for drawing when a blending mode is set
542       job.context()->setPainter( allocateImageAndPainter( ml->id(), job.img ) );
543       if ( ! job.img )
544       {
545         delete job.renderer;
546         job.renderer = nullptr;
547         layerJobs.pop_back();
548         continue;
549       }
550     }
551 
552     job.renderingTime = layerTime.elapsed(); // include job preparation time in layer rendering time
553   }
554 
555   return layerJobs;
556 }
557 
prepareSecondPassJobs(std::vector<LayerRenderJob> & firstPassJobs,LabelRenderJob & labelJob)558 std::vector< LayerRenderJob > QgsMapRendererJob::prepareSecondPassJobs( std::vector< LayerRenderJob > &firstPassJobs, LabelRenderJob &labelJob )
559 {
560   std::vector< LayerRenderJob > secondPassJobs;
561 
562   // We will need to quickly access the associated rendering job of a layer
563   QHash<QString, LayerRenderJob *> layerJobMapping;
564 
565   // ... and whether a layer has a mask defined
566   QSet<QString> layerHasMask;
567 
568   struct MaskSource
569   {
570     QString layerId;
571     QString labelRuleId;
572     int labelMaskId;
573     MaskSource( const QString &layerId_, const QString &labelRuleId_, int labelMaskId_ ):
574       layerId( layerId_ ), labelRuleId( labelRuleId_ ), labelMaskId( labelMaskId_ ) {}
575   };
576 
577   // We collect for each layer, the set of symbol layers that will be "masked"
578   // and the list of source layers that have a mask
579   QHash<QString, QPair<QSet<QgsSymbolLayerId>, QList<MaskSource>>> maskedSymbolLayers;
580 
581   // First up, create a mapping of layer id to jobs. We need this to filter out any masking
582   // which refers to layers which we aren't rendering as part of this map render
583   for ( LayerRenderJob &job : firstPassJobs )
584   {
585     layerJobMapping[job.layerId] = &job;
586   }
587 
588   // next, collate a master list of masked layers, skipping over any which refer to layers
589   // which don't have a corresponding render job
590   for ( LayerRenderJob &job : firstPassJobs )
591   {
592     QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( job.layer );
593     if ( ! vl )
594       continue;
595 
596     // lambda function to factor code for both label masks and symbol layer masks
597     auto collectMasks = [&]( QHash<QString, QSet<QgsSymbolLayerId>> *masks, QString sourceLayerId, QString ruleId = QString(), int labelMaskId = -1 )
598     {
599       for ( auto it = masks->begin(); it != masks->end(); ++it )
600       {
601         auto lit = maskedSymbolLayers.find( it.key() );
602         if ( lit == maskedSymbolLayers.end() )
603         {
604           maskedSymbolLayers[it.key()] = qMakePair( it.value(), QList<MaskSource>() << MaskSource( sourceLayerId, ruleId, labelMaskId ) );
605         }
606         else
607         {
608           if ( lit->first != it.value() )
609           {
610             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() ) );
611             continue;
612           }
613           lit->second.push_back( MaskSource( sourceLayerId, ruleId, labelMaskId ) );
614         }
615       }
616       if ( ! masks->isEmpty() )
617         layerHasMask.insert( sourceLayerId );
618     };
619 
620     // collect label masks
621     QHash<QString, QHash<QString, QSet<QgsSymbolLayerId>>> labelMasks = QgsVectorLayerUtils::labelMasks( vl );
622     for ( auto it = labelMasks.begin(); it != labelMasks.end(); it++ )
623     {
624       QString labelRule = it.key();
625       // this is a hash of layer id to masks
626       QHash<QString, QSet<QgsSymbolLayerId>> masks = it.value();
627 
628       // filter out masks to those which we are actually rendering
629       QHash<QString, QSet<QgsSymbolLayerId>> usableMasks;
630       for ( auto mit = masks.begin(); mit != masks.end(); mit++ )
631       {
632         const QString sourceLayerId = mit.key();
633         // if we aren't rendering the source layer as part of this render, we can't process this mask
634         if ( !layerJobMapping.contains( sourceLayerId ) )
635           continue;
636         else
637           usableMasks.insert( sourceLayerId, mit.value() );
638       }
639 
640       if ( usableMasks.empty() )
641         continue;
642 
643       // group layers by QSet<QgsSymbolLayerReference>
644       QSet<QgsSymbolLayerReference> slRefs;
645       for ( auto mit = usableMasks.begin(); mit != usableMasks.end(); mit++ )
646       {
647         const QString sourceLayerId = mit.key();
648         // if we aren't rendering the source layer as part of this render, we can't process this mask
649         if ( !layerJobMapping.contains( sourceLayerId ) )
650           continue;
651 
652         for ( auto slIt = mit.value().begin(); slIt != mit.value().end(); slIt++ )
653         {
654           slRefs.insert( QgsSymbolLayerReference( mit.key(), *slIt ) );
655         }
656       }
657       // generate a new mask id for this set
658       int labelMaskId = labelJob.maskIdProvider.insertLabelLayer( vl->id(), it.key(), slRefs );
659 
660       // now collect masks
661       collectMasks( &usableMasks, vl->id(), labelRule, labelMaskId );
662     }
663 
664     // collect symbol layer masks
665     QHash<QString, QSet<QgsSymbolLayerId>> symbolLayerMasks = QgsVectorLayerUtils::symbolLayerMasks( vl );
666     collectMasks( &symbolLayerMasks, vl->id() );
667   }
668 
669   if ( maskedSymbolLayers.isEmpty() )
670     return secondPassJobs;
671 
672   // Now that we know some layers have a mask, we have to allocate a mask image and painter
673   // for them in the first pass job
674   for ( LayerRenderJob &job : firstPassJobs )
675   {
676     if ( job.img == nullptr )
677     {
678       job.context()->setPainter( allocateImageAndPainter( job.layerId, job.img ) );
679     }
680     if ( layerHasMask.contains( job.layerId ) )
681     {
682       // Note: we only need an alpha channel here, rather than a full RGBA image
683       job.context()->setMaskPainter( allocateImageAndPainter( job.layerId, job.maskImage ) );
684       job.maskImage->fill( 0 );
685     }
686   }
687 
688   // Allocate an image for labels
689   if ( labelJob.img == nullptr )
690   {
691     labelJob.img = allocateImage( QStringLiteral( "labels" ) );
692   }
693 
694   // Prepare label mask images
695   for ( int maskId = 0; maskId < labelJob.maskIdProvider.size(); maskId++ )
696   {
697     QImage *maskImage;
698     labelJob.context.setMaskPainter( allocateImageAndPainter( QStringLiteral( "label mask" ), maskImage ), maskId );
699     maskImage->fill( 0 );
700     labelJob.maskImages.push_back( maskImage );
701   }
702   labelJob.context.setMaskIdProvider( &labelJob.maskIdProvider );
703 
704   // Prepare second pass jobs
705   for ( LayerRenderJob &job : firstPassJobs )
706   {
707     QgsMapLayer *ml = job.layer;
708 
709     auto it = maskedSymbolLayers.find( ml->id() );
710     if ( it == maskedSymbolLayers.end() )
711       continue;
712 
713     QList<MaskSource> &sourceList = it->second;
714     const QSet<QgsSymbolLayerId> &symbolList = it->first;
715 
716     secondPassJobs.emplace_back( LayerRenderJob() );
717     LayerRenderJob &job2 = secondPassJobs.back();
718 
719     // copy the context from the initial job
720     job2.setContext( std::make_unique< QgsRenderContext >( *job.context() ) );
721     // also assign layer to match initial job
722     job2.layer = job.layer;
723     job2.layerId = job.layerId;
724     // associate first pass job with second pass job
725     job2.firstPassJob = &job;
726 
727     QgsVectorLayer *vl1 = qobject_cast<QgsVectorLayer *>( job.layer );
728 
729     // create a new destination image for the second pass job, and update
730     // second pass job context accordingly
731     job2.context()->setMaskPainter( nullptr );
732     job2.context()->setPainter( allocateImageAndPainter( job.layerId, job2.img ) );
733     if ( ! job2.img )
734     {
735       secondPassJobs.pop_back();
736       continue;
737     }
738 
739     // Points to the first pass job. This will be needed during the second pass composition.
740     for ( MaskSource &source : sourceList )
741     {
742       if ( source.labelMaskId != -1 )
743         job2.maskJobs.push_back( qMakePair( nullptr, source.labelMaskId ) );
744       else
745         job2.maskJobs.push_back( qMakePair( layerJobMapping[source.layerId], -1 ) );
746     }
747 
748     // FIXME: another possibility here, to avoid allocating a new map renderer and reuse the one from
749     // the first pass job, would be to be able to call QgsMapLayerRenderer::render() with a QgsRenderContext.
750     QgsVectorLayerRenderer *mapRenderer = static_cast<QgsVectorLayerRenderer *>( vl1->createMapRenderer( *job2.context() ) );
751     job2.renderer = mapRenderer;
752     if ( job2.renderer )
753     {
754       job2.context()->setFeedback( job2.renderer->feedback() );
755     }
756 
757     // Modify the render context so that symbol layers get disabled as needed.
758     // The map renderer stores a reference to the context, so we can modify it even after the map renderer creation (what we need here)
759     job2.context()->setDisabledSymbolLayers( QgsSymbolLayerUtils::toSymbolLayerPointers( mapRenderer->featureRenderer(), symbolList ) );
760   }
761 
762   return secondPassJobs;
763 }
764 
prepareLabelingJob(QPainter * painter,QgsLabelingEngine * labelingEngine2,bool canUseLabelCache)765 LabelRenderJob QgsMapRendererJob::prepareLabelingJob( QPainter *painter, QgsLabelingEngine *labelingEngine2, bool canUseLabelCache )
766 {
767   LabelRenderJob job;
768   job.context = QgsRenderContext::fromMapSettings( mSettings );
769   job.context.setPainter( painter );
770   job.context.setLabelingEngine( labelingEngine2 );
771 
772   QgsRectangle r1 = mSettings.visibleExtent();
773   r1.grow( mSettings.extentBuffer() );
774   job.context.setExtent( r1 );
775 
776   job.context.setFeatureFilterProvider( mFeatureFilterProvider );
777   QgsCoordinateTransform ct;
778   ct.setDestinationCrs( mSettings.destinationCrs() );
779   job.context.setCoordinateTransform( ct );
780 
781   // if we can use the cache, let's do it and avoid rendering!
782   bool hasCache = canUseLabelCache && mCache && mCache->hasCacheImage( LABEL_CACHE_ID );
783   if ( hasCache )
784   {
785     job.cached = true;
786     job.complete = true;
787     job.img = new QImage( mCache->cacheImage( LABEL_CACHE_ID ) );
788     Q_ASSERT( job.img->devicePixelRatio() == mSettings.devicePixelRatio() );
789     job.context.setPainter( nullptr );
790   }
791   else
792   {
793     if ( canUseLabelCache && ( mCache || !painter ) )
794     {
795       job.img = allocateImage( QStringLiteral( "labels" ) );
796     }
797   }
798 
799   return job;
800 }
801 
802 
cleanupJobs(std::vector<LayerRenderJob> & jobs)803 void QgsMapRendererJob::cleanupJobs( std::vector<LayerRenderJob> &jobs )
804 {
805   for ( LayerRenderJob &job : jobs )
806   {
807     if ( job.img )
808     {
809       delete job.context()->painter();
810       job.context()->setPainter( nullptr );
811 
812       if ( mCache && !job.cached && job.completed && job.layer )
813       {
814         QgsDebugMsgLevel( QStringLiteral( "caching image for %1" ).arg( job.layerId ), 2 );
815         mCache->setCacheImageWithParameters( job.layerId, *job.img, mSettings.visibleExtent(), mSettings.mapToPixel(), QList< QgsMapLayer * >() << job.layer );
816         mCache->setCacheImageWithParameters( job.layerId + QStringLiteral( "_preview" ), *job.img, mSettings.visibleExtent(), mSettings.mapToPixel(), QList< QgsMapLayer * >() << job.layer );
817       }
818 
819       delete job.img;
820       job.img = nullptr;
821     }
822 
823     // delete the mask image and painter
824     if ( job.maskImage )
825     {
826       delete job.context()->maskPainter();
827       job.context()->setMaskPainter( nullptr );
828       delete job.maskImage;
829     }
830 
831     if ( job.renderer )
832     {
833       const QStringList errors = job.renderer->errors();
834       for ( const QString &message : errors )
835         mErrors.append( Error( job.renderer->layerId(), message ) );
836 
837       mRenderedItemResults->appendResults( job.renderer->takeRenderedItemDetails(), *job.context() );
838 
839       delete job.renderer;
840       job.renderer = nullptr;
841     }
842 
843     if ( job.layer )
844       mPerLayerRenderingTime.insert( job.layer, job.renderingTime );
845   }
846 
847   jobs.clear();
848 }
849 
cleanupSecondPassJobs(std::vector<LayerRenderJob> & jobs)850 void QgsMapRendererJob::cleanupSecondPassJobs( std::vector< LayerRenderJob > &jobs )
851 {
852   for ( LayerRenderJob &job : jobs )
853   {
854     if ( job.img )
855     {
856       delete job.context()->painter();
857       job.context()->setPainter( nullptr );
858 
859       delete job.img;
860       job.img = nullptr;
861     }
862 
863     if ( job.renderer )
864     {
865       delete job.renderer;
866       job.renderer = nullptr;
867     }
868 
869     if ( job.layer )
870       mPerLayerRenderingTime.insert( job.layer, job.renderingTime );
871   }
872 
873   jobs.clear();
874 }
875 
cleanupLabelJob(LabelRenderJob & job)876 void QgsMapRendererJob::cleanupLabelJob( LabelRenderJob &job )
877 {
878   if ( job.img )
879   {
880     if ( mCache && !job.cached && !job.context.renderingStopped() )
881     {
882       QgsDebugMsgLevel( QStringLiteral( "caching label result image" ), 2 );
883       mCache->setCacheImageWithParameters( LABEL_CACHE_ID, *job.img, mSettings.visibleExtent(), mSettings.mapToPixel(), _qgis_listQPointerToRaw( job.participatingLayers ) );
884       mCache->setCacheImageWithParameters( LABEL_PREVIEW_CACHE_ID, *job.img, mSettings.visibleExtent(), mSettings.mapToPixel(), _qgis_listQPointerToRaw( job.participatingLayers ) );
885     }
886 
887     delete job.img;
888     job.img = nullptr;
889   }
890 
891   for ( int maskId = 0; maskId < job.maskImages.size(); maskId++ )
892   {
893     delete job.context.maskPainter( maskId );
894     job.context.setMaskPainter( nullptr, maskId );
895     delete job.maskImages[maskId];
896   }
897 }
898 
899 
900 #define DEBUG_RENDERING 0
901 
composeImage(const QgsMapSettings & settings,const std::vector<LayerRenderJob> & jobs,const LabelRenderJob & labelJob,const QgsMapRendererCache * cache)902 QImage QgsMapRendererJob::composeImage( const QgsMapSettings &settings,
903                                         const std::vector<LayerRenderJob> &jobs,
904                                         const LabelRenderJob &labelJob,
905                                         const QgsMapRendererCache *cache
906                                       )
907 {
908   QImage image( settings.deviceOutputSize(), settings.outputImageFormat() );
909   image.setDevicePixelRatio( settings.devicePixelRatio() );
910   image.setDotsPerMeterX( static_cast<int>( settings.outputDpi() * 39.37 ) );
911   image.setDotsPerMeterY( static_cast<int>( settings.outputDpi() * 39.37 ) );
912   image.fill( settings.backgroundColor().rgba() );
913 
914   QPainter painter( &image );
915 
916 #if DEBUG_RENDERING
917   int i = 0;
918 #endif
919   for ( const LayerRenderJob &job : jobs )
920   {
921     if ( job.layer && job.layer->customProperty( QStringLiteral( "rendering/renderAboveLabels" ) ).toBool() )
922       continue; // skip layer for now, it will be rendered after labels
923 
924     QImage img = layerImageToBeComposed( settings, job, cache );
925     if ( img.isNull() )
926       continue; // image is not prepared and not even in cache
927 
928     painter.setCompositionMode( job.blendMode );
929     painter.setOpacity( job.opacity );
930 
931 #if DEBUG_RENDERING
932     img.save( QString( "/tmp/final_%1.png" ).arg( i ) );
933     i++;
934 #endif
935 
936     painter.drawImage( 0, 0, img );
937   }
938 
939   // IMPORTANT - don't draw labelJob img before the label job is complete,
940   // as the image is uninitialized and full of garbage before the label job
941   // commences
942   if ( labelJob.img && labelJob.complete )
943   {
944     painter.setCompositionMode( QPainter::CompositionMode_SourceOver );
945     painter.setOpacity( 1.0 );
946     painter.drawImage( 0, 0, *labelJob.img );
947   }
948   // when checking for a label cache image, we only look for those which would be drawn between 30% and 300% of the
949   // original size. We don't want to draw massive pixelated labels on top of everything else, and we also don't need
950   // to draw tiny unreadable labels... better to draw nothing in this case and wait till the updated label results are ready!
951   else if ( cache && cache->hasAnyCacheImage( LABEL_PREVIEW_CACHE_ID, 0.3, 3 ) )
952   {
953     const QImage labelCacheImage = cache->transformedCacheImage( LABEL_PREVIEW_CACHE_ID, settings.mapToPixel() );
954     painter.setCompositionMode( QPainter::CompositionMode_SourceOver );
955     painter.setOpacity( 1.0 );
956     painter.drawImage( 0, 0, labelCacheImage );
957   }
958 
959   // render any layers with the renderAboveLabels flag now
960   for ( const LayerRenderJob &job : jobs )
961   {
962     if ( !job.layer || !job.layer->customProperty( QStringLiteral( "rendering/renderAboveLabels" ) ).toBool() )
963       continue;
964 
965     QImage img = layerImageToBeComposed( settings, job, cache );
966     if ( img.isNull() )
967       continue; // image is not prepared and not even in cache
968 
969     painter.setCompositionMode( job.blendMode );
970     painter.setOpacity( job.opacity );
971 
972     painter.drawImage( 0, 0, img );
973   }
974 
975   painter.end();
976 #if DEBUG_RENDERING
977   image.save( "/tmp/final.png" );
978 #endif
979   return image;
980 }
981 
layerImageToBeComposed(const QgsMapSettings & settings,const LayerRenderJob & job,const QgsMapRendererCache * cache)982 QImage QgsMapRendererJob::layerImageToBeComposed(
983   const QgsMapSettings &settings,
984   const LayerRenderJob &job,
985   const QgsMapRendererCache *cache
986 )
987 {
988   if ( job.imageCanBeComposed() )
989   {
990     Q_ASSERT( job.img );
991     return *job.img;
992   }
993   else
994   {
995     if ( cache && cache->hasAnyCacheImage( job.layerId + QStringLiteral( "_preview" ) ) )
996     {
997       return cache->transformedCacheImage( job.layerId + QStringLiteral( "_preview" ), settings.mapToPixel() );
998     }
999     else
1000       return QImage();
1001   }
1002 }
1003 
composeSecondPass(std::vector<LayerRenderJob> & secondPassJobs,LabelRenderJob & labelJob)1004 void QgsMapRendererJob::composeSecondPass( std::vector<LayerRenderJob> &secondPassJobs, LabelRenderJob &labelJob )
1005 {
1006 #if DEBUG_RENDERING
1007   int i = 0;
1008 #endif
1009   // compose the second pass with the mask
1010   for ( LayerRenderJob &job : secondPassJobs )
1011   {
1012 #if DEBUG_RENDERING
1013     i++;
1014     job.img->save( QString( "/tmp/second_%1.png" ).arg( i ) );
1015     int mask = 0;
1016 #endif
1017 
1018     // Merge all mask images into the first one if we have more than one mask image
1019     if ( job.maskJobs.size() > 1 )
1020     {
1021       QPainter *maskPainter = nullptr;
1022       for ( QPair<LayerRenderJob *, int> p : job.maskJobs )
1023       {
1024         QImage *maskImage = p.first ? p.first->maskImage : labelJob.maskImages[p.second];
1025 #if DEBUG_RENDERING
1026         maskImage->save( QString( "/tmp/mask_%1_%2.png" ).arg( i ).arg( mask++ ) );
1027 #endif
1028         if ( ! maskPainter )
1029         {
1030           maskPainter = p.first ? p.first->context()->maskPainter() : labelJob.context.maskPainter( p.second );
1031         }
1032         else
1033         {
1034           maskPainter->drawImage( 0, 0, *maskImage );
1035         }
1036       }
1037     }
1038 
1039     if ( ! job.maskJobs.isEmpty() )
1040     {
1041       // All have been merged into the first
1042       QPair<LayerRenderJob *, int> p = *job.maskJobs.begin();
1043       QImage *maskImage = p.first ? p.first->maskImage : labelJob.maskImages[p.second];
1044 #if DEBUG_RENDERING
1045       maskImage->save( QString( "/tmp/mask_%1.png" ).arg( i ) );
1046 #endif
1047 
1048       // Only retain parts of the second rendering that are "inside" the mask image
1049       QPainter *painter = job.context()->painter();
1050       painter->setCompositionMode( QPainter::CompositionMode_DestinationIn );
1051 
1052       //Create an "alpha binarized" image of the maskImage to :
1053       //* Eliminate antialiasing artifact
1054       //* Avoid applying mask opacity to elements under the mask but not masked
1055       QImage maskBinAlpha = maskImage->createMaskFromColor( 0 );
1056       QVector<QRgb> mswTable;
1057       mswTable.push_back( qRgba( 0, 0, 0, 255 ) );
1058       mswTable.push_back( qRgba( 0, 0, 0, 0 ) );
1059       maskBinAlpha.setColorTable( mswTable );
1060       painter->drawImage( 0, 0, maskBinAlpha );
1061 #if DEBUG_RENDERING
1062       job.img->save( QString( "/tmp/second_%1_a.png" ).arg( i ) );
1063 #endif
1064 
1065       // Modify the first pass' image ...
1066       {
1067         QPainter tempPainter;
1068 
1069         // reuse the first pass painter, if available
1070         QPainter *painter1 = job.firstPassJob->context()->painter();
1071         if ( ! painter1 )
1072         {
1073           tempPainter.begin( job.firstPassJob->img );
1074           painter1 = &tempPainter;
1075         }
1076 #if DEBUG_RENDERING
1077         job.firstPassJob->img->save( QString( "/tmp/second_%1_first_pass_1.png" ).arg( i ) );
1078 #endif
1079         // ... first retain parts that are "outside" the mask image
1080         painter1->setCompositionMode( QPainter::CompositionMode_DestinationOut );
1081         painter1->drawImage( 0, 0, *maskImage );
1082 
1083 #if DEBUG_RENDERING
1084         job.firstPassJob->img->save( QString( "/tmp/second_%1_first_pass_2.png" ).arg( i ) );
1085 #endif
1086         // ... and overpaint the second pass' image on it
1087         painter1->setCompositionMode( QPainter::CompositionMode_DestinationOver );
1088         painter1->drawImage( 0, 0, *job.img );
1089 #if DEBUG_RENDERING
1090         job.img->save( QString( "/tmp/second_%1_b.png" ).arg( i ) );
1091         if ( job.firstPassJob )
1092           job.firstPassJob->img->save( QString( "/tmp/second_%1_first_pass_3.png" ).arg( i ) );
1093 #endif
1094       }
1095     }
1096   }
1097 }
1098 
logRenderingTime(const std::vector<LayerRenderJob> & jobs,const std::vector<LayerRenderJob> & secondPassJobs,const LabelRenderJob & labelJob)1099 void QgsMapRendererJob::logRenderingTime( const std::vector< LayerRenderJob > &jobs, const std::vector< LayerRenderJob > &secondPassJobs, const LabelRenderJob &labelJob )
1100 {
1101   if ( !settingsLogCanvasRefreshEvent.value() )
1102     return;
1103 
1104   QMultiMap<int, QString> elapsed;
1105   for ( const LayerRenderJob &job : jobs )
1106     elapsed.insert( job.renderingTime, job.layerId );
1107   for ( const LayerRenderJob &job : secondPassJobs )
1108     elapsed.insert( job.renderingTime, job.layerId + QString( " (second pass)" ) );
1109 
1110   elapsed.insert( labelJob.renderingTime, tr( "Labeling" ) );
1111 
1112   QList<int> tt( elapsed.uniqueKeys() );
1113   std::sort( tt.begin(), tt.end(), std::greater<int>() );
1114   for ( int t : std::as_const( tt ) )
1115   {
1116     QgsMessageLog::logMessage( tr( "%1 ms: %2" ).arg( t ).arg( QStringList( elapsed.values( t ) ).join( QLatin1String( ", " ) ) ), tr( "Rendering" ) );
1117   }
1118   QgsMessageLog::logMessage( QStringLiteral( "---" ), tr( "Rendering" ) );
1119 }
1120 
drawLabeling(QgsRenderContext & renderContext,QgsLabelingEngine * labelingEngine2,QPainter * painter)1121 void QgsMapRendererJob::drawLabeling( QgsRenderContext &renderContext, QgsLabelingEngine *labelingEngine2, QPainter *painter )
1122 {
1123   QgsDebugMsgLevel( QStringLiteral( "Draw labeling start" ), 5 );
1124 
1125   QElapsedTimer t;
1126   t.start();
1127 
1128   // Reset the composition mode before rendering the labels
1129   painter->setCompositionMode( QPainter::CompositionMode_SourceOver );
1130 
1131   renderContext.setPainter( painter );
1132 
1133   if ( labelingEngine2 )
1134   {
1135     labelingEngine2->run( renderContext );
1136   }
1137 
1138   QgsDebugMsgLevel( QStringLiteral( "Draw labeling took (seconds): %1" ).arg( t.elapsed() / 1000. ), 2 );
1139 }
1140 
drawLabeling(const QgsMapSettings & settings,QgsRenderContext & renderContext,QgsLabelingEngine * labelingEngine2,QPainter * painter)1141 void QgsMapRendererJob::drawLabeling( const QgsMapSettings &settings, QgsRenderContext &renderContext, QgsLabelingEngine *labelingEngine2, QPainter *painter )
1142 {
1143   Q_UNUSED( settings )
1144 
1145   drawLabeling( renderContext, labelingEngine2, painter );
1146 }
1147 
1148 ///@endcond PRIVATE
1149