QGIS API Documentation  3.12.1-BucureČ™ti (121cc00ff0)
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 "qgslogger.h"
24 #include "qgsrendercontext.h"
25 #include "qgsmaplayer.h"
26 #include "qgsproject.h"
27 #include "qgsmaplayerrenderer.h"
29 #include "qgsmaprenderercache.h"
30 #include "qgsmessagelog.h"
31 #include "qgspallabeling.h"
32 #include "qgsvectorlayerrenderer.h"
33 #include "qgsvectorlayer.h"
34 #include "qgsexception.h"
35 #include "qgslabelingengine.h"
36 #include "qgsmaplayerlistutils.h"
37 #include "qgsvectorlayerlabeling.h"
38 #include "qgssettings.h"
40 #include "qgssymbol.h"
41 #include "qgsrenderer.h"
42 #include "qgssymbollayer.h"
43 #include "qgsvectorlayerutils.h"
44 #include "qgssymbollayerutils.h"
45 
47 
48 const QString QgsMapRendererJob::LABEL_CACHE_ID = QStringLiteral( "_labels_" );
49 
51  : mSettings( settings )
52 
53 {
54 }
55 
56 
58  : QgsMapRendererJob( settings )
59 {
60 }
61 
62 
64 {
65  return mErrors;
66 }
67 
69 {
70  mCache = cache;
71 }
72 
73 QHash<QgsMapLayer *, int> QgsMapRendererJob::perLayerRenderingTime() const
74 {
75  QHash<QgsMapLayer *, int> result;
76  for ( auto it = mPerLayerRenderingTime.constBegin(); it != mPerLayerRenderingTime.constEnd(); ++it )
77  {
78  if ( it.key() )
79  result.insert( it.key(), it.value() );
80  }
81  return result;
82 }
83 
85 {
86  return mSettings;
87 }
88 
90 {
91  bool canCache = mCache;
92 
93  // calculate which layers will be labeled
94  QSet< QgsMapLayer * > labeledLayers;
95  const QList<QgsMapLayer *> layers = mSettings.layers();
96  for ( const QgsMapLayer *ml : layers )
97  {
98  QgsVectorLayer *vl = const_cast< QgsVectorLayer * >( qobject_cast<const QgsVectorLayer *>( ml ) );
99  if ( vl && QgsPalLabeling::staticWillUseLayer( vl ) )
100  labeledLayers << vl;
101  if ( vl && vl->labelsEnabled() && vl->labeling()->requiresAdvancedEffects() )
102  {
103  canCache = false;
104  break;
105  }
106  }
107 
109  {
110  // we may need to clear label cache and re-register labeled features - check for that here
111 
112  // can we reuse the cached label solution?
113  bool canUseCache = canCache && mCache->dependentLayers( LABEL_CACHE_ID ).toSet() == labeledLayers;
114  if ( !canUseCache )
115  {
116  // no - participating layers have changed
118  }
119  }
120  return canCache;
121 }
122 
123 
124 bool QgsMapRendererJob::reprojectToLayerExtent( const QgsMapLayer *ml, const QgsCoordinateTransform &ct, QgsRectangle &extent, QgsRectangle &r2 )
125 {
126  bool split = false;
127 
128  try
129  {
130 #ifdef QGISDEBUG
131  // QgsLogger::debug<QgsRectangle>("Getting extent of canvas in layers CS. Canvas is ", extent, __FILE__, __FUNCTION__, __LINE__);
132 #endif
133  // Split the extent into two if the source CRS is
134  // geographic and the extent crosses the split in
135  // geographic coordinates (usually +/- 180 degrees,
136  // and is assumed to be so here), and draw each
137  // extent separately.
138  static const double SPLIT_COORD = 180.0;
139 
140  if ( ml->crs().isGeographic() )
141  {
143  {
144  // if we transform from a projected coordinate system check
145  // check if transforming back roughly returns the input
146  // extend - otherwise render the world.
149 
150  QgsDebugMsgLevel( QStringLiteral( "\n0:%1 %2x%3\n1:%4\n2:%5 %6x%7 (w:%8 h:%9)" )
151  .arg( extent.toString() ).arg( extent.width() ).arg( extent.height() )
152  .arg( extent1.toString(), extent2.toString() ).arg( extent2.width() ).arg( extent2.height() )
153  .arg( std::fabs( 1.0 - extent2.width() / extent.width() ) )
154  .arg( std::fabs( 1.0 - extent2.height() / extent.height() ) )
155  , 3 );
156 
157  // can differ by a maximum of up to 20% of height/width
158  if ( qgsDoubleNear( extent2.xMinimum(), extent.xMinimum(), extent.width() * 0.2 )
159  && qgsDoubleNear( extent2.xMaximum(), extent.xMaximum(), extent.width() * 0.2 )
160  && qgsDoubleNear( extent2.yMinimum(), extent.yMinimum(), extent.height() * 0.2 )
161  && qgsDoubleNear( extent2.yMaximum(), extent.yMaximum(), extent.height() * 0.2 )
162  )
163  {
164  extent = extent1;
165  }
166  else
167  {
168  extent = QgsRectangle( -180.0, -90.0, 180.0, 90.0 );
169  }
170  }
171  else
172  {
173  // Note: ll = lower left point
174  QgsPointXY ll = ct.transform( extent.xMinimum(), extent.yMinimum(),
176 
177  // and ur = upper right point
178  QgsPointXY ur = ct.transform( extent.xMaximum(), extent.yMaximum(),
180 
181  QgsDebugMsgLevel( QStringLiteral( "in:%1 (ll:%2 ur:%3)" ).arg( extent.toString(), ll.toString(), ur.toString() ), 4 );
182 
184 
185  QgsDebugMsgLevel( QStringLiteral( "out:%1 (w:%2 h:%3)" ).arg( extent.toString() ).arg( extent.width() ).arg( extent.height() ), 4 );
186 
187  if ( ll.x() > ur.x() )
188  {
189  // the coordinates projected in reverse order than what one would expect.
190  // we are probably looking at an area that includes longitude of 180 degrees.
191  // we need to take into account coordinates from two intervals: (-180,x1) and (x2,180)
192  // so let's use (-180,180). This hopefully does not add too much overhead. It is
193  // more straightforward than rendering with two separate extents and more consistent
194  // for rendering, labeling and caching as everything is rendered just in one go
195  extent.setXMinimum( -SPLIT_COORD );
196  extent.setXMaximum( SPLIT_COORD );
197  }
198  }
199 
200  // TODO: the above rule still does not help if using a projection that covers the whole
201  // world. E.g. with EPSG:3857 the longitude spectrum -180 to +180 is mapped to approx.
202  // -2e7 to +2e7. Converting extent from -5e7 to +5e7 is transformed as -90 to +90,
203  // but in fact the extent should cover the whole world.
204  }
205  else // can't cross 180
206  {
207  if ( ct.destinationCrs().isGeographic() &&
208  ( extent.xMinimum() <= -180 || extent.xMaximum() >= 180 ||
209  extent.yMinimum() <= -90 || extent.yMaximum() >= 90 ) )
210  // Use unlimited rectangle because otherwise we may end up transforming wrong coordinates.
211  // E.g. longitude -200 to +160 would be understood as +40 to +160 due to periodicity.
212  // We could try to clamp coords to (-180,180) for lon resp. (-90,90) for lat,
213  // but this seems like a safer choice.
214  extent = QgsRectangle( std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest(), std::numeric_limits<double>::max(), std::numeric_limits<double>::max() );
215  else
217  }
218  }
219  catch ( QgsCsException &cse )
220  {
221  Q_UNUSED( cse )
222  QgsDebugMsg( QStringLiteral( "Transform error caught" ) );
223  extent = QgsRectangle( std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest(), std::numeric_limits<double>::max(), std::numeric_limits<double>::max() );
224  r2 = QgsRectangle( std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest(), std::numeric_limits<double>::max(), std::numeric_limits<double>::max() );
225  }
226 
227  return split;
228 }
229 
230 QImage *QgsMapRendererJob::allocateImage( QString layerId )
231 {
232  QImage *image = new QImage( mSettings.deviceOutputSize(),
234  image->setDevicePixelRatio( static_cast<qreal>( mSettings.devicePixelRatio() ) );
235  if ( image->isNull() )
236  {
237  mErrors.append( Error( layerId, tr( "Insufficient memory for image %1x%2" ).arg( mSettings.outputSize().width() ).arg( mSettings.outputSize().height() ) ) );
238  delete image;
239  return nullptr;
240  }
241  return image;
242 }
243 
244 QPainter *QgsMapRendererJob::allocateImageAndPainter( QString layerId, QImage *&image )
245 {
246  QPainter *painter = nullptr;
247  image = allocateImage( layerId );
248  if ( image )
249  {
250  painter = new QPainter( image );
251  painter->setRenderHint( QPainter::Antialiasing, mSettings.testFlag( QgsMapSettings::Antialiasing ) );
252  }
253  return painter;
254 }
255 
256 LayerRenderJobs QgsMapRendererJob::prepareJobs( QPainter *painter, QgsLabelingEngine *labelingEngine2, bool deferredPainterSet )
257 {
258  LayerRenderJobs layerJobs;
259 
260  // render all layers in the stack, starting at the base
261  QListIterator<QgsMapLayer *> li( mSettings.layers() );
262  li.toBack();
263 
264  if ( mCache )
265  {
266  bool cacheValid = mCache->init( mSettings.visibleExtent(), mSettings.scale() );
267  Q_UNUSED( cacheValid )
268  QgsDebugMsgLevel( QStringLiteral( "CACHE VALID: %1" ).arg( cacheValid ), 4 );
269  }
270 
271  bool requiresLabelRedraw = !( mCache && mCache->hasCacheImage( LABEL_CACHE_ID ) );
272 
273  while ( li.hasPrevious() )
274  {
275  QgsMapLayer *ml = li.previous();
276 
277  QgsDebugMsgLevel( QStringLiteral( "layer %1: minscale:%2 maxscale:%3 scaledepvis:%4 blendmode:%5 isValid:%6" )
278  .arg( ml->name() )
279  .arg( ml->minimumScale() )
280  .arg( ml->maximumScale() )
281  .arg( ml->hasScaleBasedVisibility() )
282  .arg( ml->blendMode() )
283  .arg( ml->isValid() )
284  , 3 );
285 
286  if ( !ml->isValid() )
287  {
288  QgsDebugMsgLevel( QStringLiteral( "Invalid Layer skipped" ), 3 );
289  continue;
290  }
291 
292  if ( !ml->isInScaleRange( mSettings.scale() ) ) //|| mOverview )
293  {
294  QgsDebugMsgLevel( QStringLiteral( "Layer not rendered because it is not within the defined visibility scale range" ), 3 );
295  continue;
296  }
297 
299  r1.grow( mSettings.extentBuffer() );
301 
302  ct = mSettings.layerTransform( ml );
303  if ( ct.isValid() )
304  {
305  reprojectToLayerExtent( ml, ct, r1, r2 );
306  }
307  QgsDebugMsgLevel( "extent: " + r1.toString(), 3 );
308  if ( !r1.isFinite() || !r2.isFinite() )
309  {
310  mErrors.append( Error( ml->id(), tr( "There was a problem transforming the layer's extent. Layer skipped." ) ) );
311  continue;
312  }
313 
314  QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( ml );
315 
316  // Force render of layers that are being edited
317  // or if there's a labeling engine that needs the layer to register features
318  if ( mCache && vl )
319  {
320  bool requiresLabeling = false;
321  requiresLabeling = ( labelingEngine2 && QgsPalLabeling::staticWillUseLayer( vl ) ) && requiresLabelRedraw;
322  if ( vl->isEditable() || requiresLabeling )
323  {
324  mCache->clearCacheImage( ml->id() );
325  }
326  }
327 
328  layerJobs.append( LayerRenderJob() );
329  LayerRenderJob &job = layerJobs.last();
330  job.cached = false;
331  job.img = nullptr;
332  job.layer = ml;
333  job.layerId = ml->id();
334  job.renderingTime = -1;
335 
337  job.context.expressionContext().appendScope( QgsExpressionContextUtils::layerScope( ml ) );
338  job.context.setPainter( painter );
339  job.context.setLabelingEngine( labelingEngine2 );
340  job.context.setCoordinateTransform( ct );
341  job.context.setExtent( r1 );
342 
343  if ( mFeatureFilterProvider )
344  job.context.setFeatureFilterProvider( mFeatureFilterProvider );
345 
346  QgsMapLayerStyleOverride styleOverride( ml );
347  if ( mSettings.layerStyleOverrides().contains( ml->id() ) )
348  styleOverride.setOverrideStyle( mSettings.layerStyleOverrides().value( ml->id() ) );
349 
350  job.blendMode = ml->blendMode();
351  job.opacity = 1.0;
352  if ( vl )
353  {
354  job.opacity = vl->opacity();
355  }
356 
357  // if we can use the cache, let's do it and avoid rendering!
358  if ( mCache && mCache->hasCacheImage( ml->id() ) )
359  {
360  job.cached = true;
361  job.imageInitialized = true;
362  job.img = new QImage( mCache->cacheImage( ml->id() ) );
363  job.img->setDevicePixelRatio( static_cast<qreal>( mSettings.devicePixelRatio() ) );
364  job.renderer = nullptr;
365  job.context.setPainter( nullptr );
366  continue;
367  }
368 
369  // If we are drawing with an alternative blending mode then we need to render to a separate image
370  // before compositing this on the map. This effectively flattens the layer and prevents
371  // blending occurring between objects on the layer
372  if ( mCache || ( !painter && !deferredPainterSet ) || needTemporaryImage( ml ) )
373  {
374  // Flattened image for drawing when a blending mode is set
375  job.context.setPainter( allocateImageAndPainter( ml->id(), job.img ) );
376  if ( ! job.img )
377  {
378  layerJobs.removeLast();
379  continue;
380  }
381  }
382 
383  QElapsedTimer layerTime;
384  layerTime.start();
385  job.renderer = ml->createMapRenderer( job.context );
386  job.renderingTime = layerTime.elapsed(); // include job preparation time in layer rendering time
387  } // while (li.hasPrevious())
388 
389  return layerJobs;
390 }
391 
392 LayerRenderJobs QgsMapRendererJob::prepareSecondPassJobs( LayerRenderJobs &firstPassJobs, LabelRenderJob &labelJob )
393 {
394  LayerRenderJobs secondPassJobs;
395 
396  // We will need to quickly access the associated rendering job of a layer
397  QHash<QString, LayerRenderJob *> layerJobMapping;
398 
399  // ... and whether a layer has a mask defined
400  QSet<QString> layerHasMask;
401 
402  struct MaskSource
403  {
404  QString layerId;
405  QString labelRuleId;
406  int labelMaskId;
407  MaskSource( const QString &layerId_, const QString &labelRuleId_, int labelMaskId_ ):
408  layerId( layerId_ ), labelRuleId( labelRuleId_ ), labelMaskId( labelMaskId_ ) {}
409  };
410 
411  // We collect for each layer, the set of symbol layers that will be "masked"
412  // and the list of source layers that have a mask
413  QHash<QString, QPair<QSet<QgsSymbolLayerId>, QList<MaskSource>>> maskedSymbolLayers;
414 
415  for ( LayerRenderJob &job : firstPassJobs )
416  {
417  QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( job.layer );
418  if ( ! vl )
419  continue;
420 
421  layerJobMapping[job.layer->id()] = &job;
422 
423  // lambda function to factor code for both label masks and symbol layer masks
424  auto collectMasks = [&]( QHash<QString, QSet<QgsSymbolLayerId>> *masks, QString sourceLayerId, QString ruleId = QString(), int labelMaskId = -1 )
425  {
426  for ( auto it = masks->begin(); it != masks->end(); ++it )
427  {
428  auto lit = maskedSymbolLayers.find( it.key() );
429  if ( lit == maskedSymbolLayers.end() )
430  {
431  maskedSymbolLayers[it.key()] = qMakePair( it.value(), QList<MaskSource>() << MaskSource( sourceLayerId, ruleId, labelMaskId ) );
432  }
433  else
434  {
435  if ( lit->first != it.value() )
436  {
437  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() ) );
438  continue;
439  }
440  lit->second.push_back( MaskSource( sourceLayerId, ruleId, labelMaskId ) );
441  }
442  }
443  if ( ! masks->isEmpty() )
444  layerHasMask.insert( sourceLayerId );
445  };
446 
447  // collect label masks
448  QHash<QString, QHash<QString, QSet<QgsSymbolLayerId>>> labelMasks = QgsVectorLayerUtils::labelMasks( vl );
449  for ( auto it = labelMasks.begin(); it != labelMasks.end(); it++ )
450  {
451  QString labelRule = it.key();
452  QHash<QString, QSet<QgsSymbolLayerId>> masks = it.value();
453 
454  // group layers by QSet<QgsSymbolLayerReference>
455  QSet<QgsSymbolLayerReference> slRefs;
456  for ( auto mit = masks.begin(); mit != masks.end(); mit++ )
457  {
458  for ( auto slIt = mit.value().begin(); slIt != mit.value().end(); slIt++ )
459  {
460  slRefs.insert( QgsSymbolLayerReference( mit.key(), *slIt ) );
461  }
462  }
463  // generate a new mask id for this set
464  int labelMaskId = labelJob.maskIdProvider.insertLabelLayer( vl->id(), it.key(), slRefs );
465 
466  // now collect masks
467  collectMasks( &masks, vl->id(), labelRule, labelMaskId );
468  }
469 
470  // collect symbol layer masks
471  QHash<QString, QSet<QgsSymbolLayerId>> symbolLayerMasks = QgsVectorLayerUtils::symbolLayerMasks( vl );
472  collectMasks( &symbolLayerMasks, vl->id() );
473  }
474 
475  if ( maskedSymbolLayers.isEmpty() )
476  return secondPassJobs;
477 
478  // Now that we know some layers have a mask, we have to allocate a mask image and painter
479  // for them in the first pass job
480  for ( LayerRenderJob &job : firstPassJobs )
481  {
482  QgsMapLayer *ml = job.layer;
483 
484  if ( job.img == nullptr )
485  {
486  job.context.setPainter( allocateImageAndPainter( ml->id(), job.img ) );
487  }
488  if ( layerHasMask.contains( ml->id() ) )
489  {
490  // Note: we only need an alpha channel here, rather than a full RGBA image
491  job.context.setMaskPainter( allocateImageAndPainter( ml->id(), job.maskImage ) );
492  job.maskImage->fill( 0 );
493  }
494  }
495 
496  // Allocate an image for labels
497  if ( labelJob.img == nullptr )
498  {
499  labelJob.img = allocateImage( QStringLiteral( "labels" ) );
500  }
501 
502  // Prepare label mask images
503  for ( int maskId = 0; maskId < labelJob.maskIdProvider.size(); maskId++ )
504  {
505  QImage *maskImage;
506  labelJob.context.setMaskPainter( allocateImageAndPainter( QStringLiteral( "label mask" ), maskImage ), maskId );
507  maskImage->fill( 0 );
508  labelJob.maskImages.push_back( maskImage );
509  }
510  labelJob.context.setMaskIdProvider( &labelJob.maskIdProvider );
511 
512  // Prepare second pass jobs
513  for ( LayerRenderJob &job : firstPassJobs )
514  {
515  QgsMapLayer *ml = job.layer;
516 
517  auto it = maskedSymbolLayers.find( ml->id() );
518  if ( it == maskedSymbolLayers.end() )
519  continue;
520 
521  QList<MaskSource> &sourceList = it->second;
522  const QSet<QgsSymbolLayerId> &symbolList = it->first;
523 
524  // copy the initial job ...
525  secondPassJobs.append( LayerRenderJob() );
526  LayerRenderJob &job2 = secondPassJobs.last();
527  job2 = job;
528  job2.cached = false;
529  job2.firstPassJob = &job;
530  QgsVectorLayer *vl1 = qobject_cast<QgsVectorLayer *>( job.layer );
531 
532  // ... but clear the image
533  job2.context.setMaskPainter( nullptr );
534  job2.context.setPainter( allocateImageAndPainter( vl1->id(), job2.img ) );
535  if ( ! job2.img )
536  {
537  secondPassJobs.removeLast();
538  continue;
539  }
540 
541  // Points to the first pass job. This will be needed during the second pass composition.
542  for ( MaskSource &source : sourceList )
543  {
544  if ( source.labelMaskId != -1 )
545  job2.maskJobs.push_back( qMakePair( nullptr, source.labelMaskId ) );
546  else
547  job2.maskJobs.push_back( qMakePair( layerJobMapping[source.layerId], -1 ) );
548  }
549 
550  // FIXME: another possibility here, to avoid allocating a new map renderer and reuse the one from
551  // the first pass job, would be to be able to call QgsMapLayerRenderer::render() with a QgsRenderContext.
552  QgsVectorLayerRenderer *mapRenderer = static_cast<QgsVectorLayerRenderer *>( vl1->createMapRenderer( job2.context ) );
553  job2.renderer = mapRenderer;
554 
555  // Modify the render context so that symbol layers get disabled as needed.
556  // The map renderer stores a reference to the context, so we can modify it even after the map renderer creation (what we need here)
557  job2.context.setDisabledSymbolLayers( QgsSymbolLayerUtils::toSymbolLayerPointers( mapRenderer->featureRenderer(), symbolList ) );
558  }
559 
560  return secondPassJobs;
561 }
562 
563 LabelRenderJob QgsMapRendererJob::prepareLabelingJob( QPainter *painter, QgsLabelingEngine *labelingEngine2, bool canUseLabelCache )
564 {
565  LabelRenderJob job;
567  job.context.setPainter( painter );
568  job.context.setLabelingEngine( labelingEngine2 );
569  job.context.setExtent( mSettings.visibleExtent() );
570  job.context.setFeatureFilterProvider( mFeatureFilterProvider );
571 
572  // if we can use the cache, let's do it and avoid rendering!
573  bool hasCache = canUseLabelCache && mCache && mCache->hasCacheImage( LABEL_CACHE_ID );
574  if ( hasCache )
575  {
576  job.cached = true;
577  job.complete = true;
578  job.img = new QImage( mCache->cacheImage( LABEL_CACHE_ID ) );
579  Q_ASSERT( job.img->devicePixelRatio() == mSettings.devicePixelRatio() );
580  job.context.setPainter( nullptr );
581  }
582  else
583  {
584  if ( canUseLabelCache && ( mCache || !painter ) )
585  {
586  job.img = allocateImage( QStringLiteral( "labels" ) );
587  }
588  }
589 
590  return job;
591 }
592 
593 
594 void QgsMapRendererJob::cleanupJobs( LayerRenderJobs &jobs )
595 {
596  for ( LayerRenderJobs::iterator it = jobs.begin(); it != jobs.end(); ++it )
597  {
598  LayerRenderJob &job = *it;
599  if ( job.img )
600  {
601  delete job.context.painter();
602  job.context.setPainter( nullptr );
603 
604  if ( mCache && !job.cached && !job.context.renderingStopped() && job.layer )
605  {
606  QgsDebugMsgLevel( QStringLiteral( "caching image for %1" ).arg( job.layerId ), 2 );
607  mCache->setCacheImage( job.layerId, *job.img, QList< QgsMapLayer * >() << job.layer );
608  }
609 
610  delete job.img;
611  job.img = nullptr;
612  }
613 
614  // delete the mask image and painter
615  if ( job.maskImage )
616  {
617  delete job.context.maskPainter();
618  job.context.setMaskPainter( nullptr );
619  delete job.maskImage;
620  }
621 
622  if ( job.renderer )
623  {
624  const auto constErrors = job.renderer->errors();
625  for ( const QString &message : constErrors )
626  mErrors.append( Error( job.renderer->layerId(), message ) );
627 
628  delete job.renderer;
629  job.renderer = nullptr;
630  }
631 
632  if ( job.layer )
633  mPerLayerRenderingTime.insert( job.layer, job.renderingTime );
634  }
635 
636  jobs.clear();
637 }
638 
639 void QgsMapRendererJob::cleanupSecondPassJobs( LayerRenderJobs &jobs )
640 {
641  for ( auto &job : jobs )
642  {
643  if ( job.img )
644  {
645  delete job.context.painter();
646  job.context.setPainter( nullptr );
647 
648  delete job.img;
649  job.img = nullptr;
650  }
651 
652  if ( job.renderer )
653  {
654  delete job.renderer;
655  job.renderer = nullptr;
656  }
657 
658  if ( job.layer )
659  mPerLayerRenderingTime.insert( job.layer, job.renderingTime );
660  }
661 
662  jobs.clear();
663 }
664 
665 void QgsMapRendererJob::cleanupLabelJob( LabelRenderJob &job )
666 {
667  if ( job.img )
668  {
669  if ( mCache && !job.cached && !job.context.renderingStopped() )
670  {
671  QgsDebugMsgLevel( QStringLiteral( "caching label result image" ), 2 );
672  mCache->setCacheImage( LABEL_CACHE_ID, *job.img, _qgis_listQPointerToRaw( job.participatingLayers ) );
673  }
674 
675  delete job.img;
676  job.img = nullptr;
677  }
678 
679  for ( int maskId = 0; maskId < job.maskImages.size(); maskId++ )
680  {
681  delete job.context.maskPainter( maskId );
682  job.context.setMaskPainter( nullptr, maskId );
683  delete job.maskImages[maskId];
684  }
685 }
686 
687 
688 #define DEBUG_RENDERING 0
689 
690 QImage QgsMapRendererJob::composeImage( const QgsMapSettings &settings, const LayerRenderJobs &jobs, const LabelRenderJob &labelJob )
691 {
692  QImage image( settings.deviceOutputSize(), settings.outputImageFormat() );
693  image.setDevicePixelRatio( settings.devicePixelRatio() );
694  image.fill( settings.backgroundColor().rgba() );
695 
696  QPainter painter( &image );
697 
698 #if DEBUG_RENDERING
699  int i = 0;
700 #endif
701  for ( LayerRenderJobs::const_iterator it = jobs.constBegin(); it != jobs.constEnd(); ++it )
702  {
703  const LayerRenderJob &job = *it;
704 
705  if ( job.layer && job.layer->customProperty( QStringLiteral( "rendering/renderAboveLabels" ) ).toBool() )
706  continue; // skip layer for now, it will be rendered after labels
707 
708  if ( !job.imageInitialized )
709  continue; // img not safe to compose
710 
711  painter.setCompositionMode( job.blendMode );
712  painter.setOpacity( job.opacity );
713 
714 #if DEBUG_RENDERING
715  job.img->save( QString( "/tmp/final_%1.png" ).arg( i ) );
716  i++;
717 #endif
718  Q_ASSERT( job.img );
719 
720  painter.drawImage( 0, 0, *job.img );
721  }
722 
723  // IMPORTANT - don't draw labelJob img before the label job is complete,
724  // as the image is uninitialized and full of garbage before the label job
725  // commences
726  if ( labelJob.img && labelJob.complete )
727  {
728  painter.setCompositionMode( QPainter::CompositionMode_SourceOver );
729  painter.setOpacity( 1.0 );
730  painter.drawImage( 0, 0, *labelJob.img );
731  }
732 
733  // render any layers with the renderAboveLabels flag now
734  for ( LayerRenderJobs::const_iterator it = jobs.constBegin(); it != jobs.constEnd(); ++it )
735  {
736  const LayerRenderJob &job = *it;
737 
738  if ( !job.layer || !job.layer->customProperty( QStringLiteral( "rendering/renderAboveLabels" ) ).toBool() )
739  continue;
740 
741  if ( !job.imageInitialized )
742  continue; // img not safe to compose
743 
744  painter.setCompositionMode( job.blendMode );
745  painter.setOpacity( job.opacity );
746 
747  Q_ASSERT( job.img );
748 
749  painter.drawImage( 0, 0, *job.img );
750  }
751 
752  painter.end();
753 #if DEBUG_RENDERING
754  image.save( "/tmp/final.png" );
755 #endif
756  return image;
757 }
758 
759 void QgsMapRendererJob::composeSecondPass( LayerRenderJobs &secondPassJobs, LabelRenderJob &labelJob )
760 {
761 #if DEBUG_RENDERING
762  int i = 0;
763 #endif
764  // compose the second pass with the mask
765  for ( LayerRenderJob &job : secondPassJobs )
766  {
767 #if DEBUG_RENDERING
768  i++;
769  job.img->save( QString( "/tmp/second_%1.png" ).arg( i ) );
770  int mask = 0;
771 #endif
772 
773  // Merge all mask images into the first one if we have more than one mask image
774  if ( job.maskJobs.size() > 1 )
775  {
776  QPainter *maskPainter = nullptr;
777  for ( QPair<LayerRenderJob *, int> p : job.maskJobs )
778  {
779  QImage *maskImage = p.first ? p.first->maskImage : labelJob.maskImages[p.second];
780 #if DEBUG_RENDERING
781  maskImage->save( QString( "/tmp/mask_%1_%2.png" ).arg( i ).arg( mask++ ) );
782 #endif
783  if ( ! maskPainter )
784  {
785  maskPainter = p.first ? p.first->context.maskPainter() : labelJob.context.maskPainter( p.second );
786  }
787  else
788  {
789  maskPainter->drawImage( 0, 0, *maskImage );
790  }
791  }
792  }
793 
794  if ( ! job.maskJobs.isEmpty() )
795  {
796  // All have been merged into the first
797  QPair<LayerRenderJob *, int> p = *job.maskJobs.begin();
798  QImage *maskImage = p.first ? p.first->maskImage : labelJob.maskImages[p.second];
799 #if DEBUG_RENDERING
800  maskImage->save( QString( "/tmp/mask_%1.png" ).arg( i ) );
801 #endif
802 
803  // Only retain parts of the second rendering that are "inside" the mask image
804  QPainter *painter = job.context.painter();
805  painter->setCompositionMode( QPainter::CompositionMode_DestinationIn );
806  painter->drawImage( 0, 0, *maskImage );
807 #if DEBUG_RENDERING
808  job.img->save( QString( "/tmp/second_%1_a.png" ).arg( i ) );
809 #endif
810 
811  // Modify the first pass' image ...
812  {
813  QPainter tempPainter;
814 
815  // resue the first pass painter, if available
816  QPainter *painter1 = job.firstPassJob->context.painter();
817  if ( ! painter1 )
818  {
819  tempPainter.begin( job.firstPassJob->img );
820  painter1 = &tempPainter;
821  }
822 #if DEBUG_RENDERING
823  job.firstPassJob->img->save( QString( "/tmp/second_%1_first_pass_1.png" ).arg( i ) );
824 #endif
825  // ... first retain parts that are "outside" the mask image
826  painter1->setCompositionMode( QPainter::CompositionMode_DestinationOut );
827  painter1->drawImage( 0, 0, *maskImage );
828 
829 #if DEBUG_RENDERING
830  job.firstPassJob->img->save( QString( "/tmp/second_%1_first_pass_2.png" ).arg( i ) );
831 #endif
832  // ... and overpaint the second pass' image on it
833  painter1->setCompositionMode( QPainter::CompositionMode_SourceOver );
834  painter1->drawImage( 0, 0, *job.img );
835 #if DEBUG_RENDERING
836  job.img->save( QString( "/tmp/second_%1_b.png" ).arg( i ) );
837  if ( job.firstPassJob )
838  job.firstPassJob->img->save( QString( "/tmp/second_%1_first_pass_3.png" ).arg( i ) );
839 #endif
840  }
841  }
842  }
843 }
844 
845 void QgsMapRendererJob::logRenderingTime( const LayerRenderJobs &jobs, const LayerRenderJobs &secondPassJobs, const LabelRenderJob &labelJob )
846 {
847  QgsSettings settings;
848  if ( !settings.value( QStringLiteral( "Map/logCanvasRefreshEvent" ), false ).toBool() )
849  return;
850 
851  QMultiMap<int, QString> elapsed;
852  const auto constJobs = jobs;
853  for ( const LayerRenderJob &job : constJobs )
854  elapsed.insert( job.renderingTime, job.layerId );
855  const auto constSecondPassJobs = secondPassJobs;
856  for ( const LayerRenderJob &job : constSecondPassJobs )
857  elapsed.insert( job.renderingTime, job.layerId + QString( " (second pass)" ) );
858 
859  elapsed.insert( labelJob.renderingTime, tr( "Labeling" ) );
860 
861  QList<int> tt( elapsed.uniqueKeys() );
862  std::sort( tt.begin(), tt.end(), std::greater<int>() );
863  const auto constTt = tt;
864  for ( int t : constTt )
865  {
866  QgsMessageLog::logMessage( tr( "%1 ms: %2" ).arg( t ).arg( QStringList( elapsed.values( t ) ).join( QStringLiteral( ", " ) ) ), tr( "Rendering" ) );
867  }
868  QgsMessageLog::logMessage( QStringLiteral( "---" ), tr( "Rendering" ) );
869 }
870 
871 bool QgsMapRendererJob::needTemporaryImage( QgsMapLayer *ml )
872 {
873  switch ( ml->type() )
874  {
876  {
877  QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( ml );
878  if ( vl->renderer() && vl->renderer()->forceRasterRender() )
879  {
880  //raster rendering is forced for this layer
881  return true;
882  }
884  ( ( vl->blendMode() != QPainter::CompositionMode_SourceOver )
885  || ( vl->featureBlendMode() != QPainter::CompositionMode_SourceOver )
886  || ( !qgsDoubleNear( vl->opacity(), 1.0 ) ) ) )
887  {
888  //layer properties require rasterization
889  return true;
890  }
891  break;
892  }
894  {
895  // preview of intermediate raster rendering results requires a temporary output image
897  return true;
898  break;
899  }
900 
903  break;
904  }
905 
906  return false;
907 }
908 
909 void QgsMapRendererJob::drawLabeling( QgsRenderContext &renderContext, QgsLabelingEngine *labelingEngine2, QPainter *painter )
910 {
911  QgsDebugMsgLevel( QStringLiteral( "Draw labeling start" ), 5 );
912 
913  QElapsedTimer t;
914  t.start();
915 
916  // Reset the composition mode before rendering the labels
917  painter->setCompositionMode( QPainter::CompositionMode_SourceOver );
918 
919  renderContext.setPainter( painter );
920 
921  if ( labelingEngine2 )
922  {
923  labelingEngine2->run( renderContext );
924  }
925 
926  QgsDebugMsgLevel( QStringLiteral( "Draw labeling took (seconds): %1" ).arg( t.elapsed() / 1000. ), 2 );
927 }
928 
929 void QgsMapRendererJob::drawLabeling( const QgsMapSettings &settings, QgsRenderContext &renderContext, QgsLabelingEngine *labelingEngine2, QPainter *painter )
930 {
931  Q_UNUSED( settings )
932 
933  drawLabeling( renderContext, labelingEngine2, painter );
934 }
935 
QList< QgsMapLayer *> dependentLayers(const QString &cacheKey) const
Returns a list of map layers on which an image in the cache depends.
bool labelsEnabled() const
Returns whether the layer contains labels which are enabled and should be drawn.
A rectangle specified with double values.
Definition: qgsrectangle.h:41
Base class for all map layer types.
Definition: qgsmaplayer.h:79
const LayerRenderJobs & jobs() const
Abstract base class for map rendering implementations.
QgsMapLayerType type() const
Returns the type of the layer.
QgsFeatureRenderer * featureRenderer()
Returns the feature renderer.
void setXMaximum(double x)
Set the maximum x value.
Definition: qgsrectangle.h:135
This class is a composition of two QSettings instances:
Definition: qgssettings.h:58
void cleanupJobs(LayerRenderJobs &jobs)
Restore overridden layer style on destruction.
QSize deviceOutputSize() const
Returns the device output size of the map canvas This is equivalent to the output size multiplicated ...
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
static Q_DECL_DEPRECATED void drawLabeling(const QgsMapSettings &settings, QgsRenderContext &renderContext, QgsLabelingEngine *labelingEngine2, QPainter *painter)
void cleanupSecondPassJobs(LayerRenderJobs &jobs)
QgsPointXY transform(const QgsPointXY &point, TransformDirection direction=ForwardTransform) const SIP_THROW(QgsCsException)
Transform the point from the source CRS to the destination CRS.
static void warning(const QString &msg)
Goes to qWarning.
Definition: qgslogger.cpp:122
A class to represent a 2D point.
Definition: qgspointxy.h:43
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:315
QColor backgroundColor() const
Gets the background color of the map.
Whether to make extra effort to update map image with partially rendered layers (better for interacti...
bool forceRasterRender() const
Returns whether the renderer must render as a raster.
Definition: qgsrenderer.h:412
void setCacheImage(const QString &cacheKey, const QImage &image, const QList< QgsMapLayer * > &dependentLayers=QList< QgsMapLayer * >())
Set the cached image for a particular cacheKey.
void logRenderingTime(const LayerRenderJobs &jobs, const LayerRenderJobs &secondPassJobs, const LabelRenderJob &labelJob)
Errors errors() const
List of errors that happened during the rendering job - available when the rendering has been finishe...
QgsCoordinateReferenceSystem destinationCrs() const
Returns the destination coordinate reference system, which the transform will transform coordinates t...
bool isInScaleRange(double scale) const
Tests whether the layer should be visible at the specified scale.
QString toString(int precision=-1) const
Returns a string representation of the point (x, y) with a preset precision.
Definition: qgspointxy.cpp:51
QList< QgsMapLayer * > layers() const
Gets list of layers for map rendering The layers are stored in the reverse order of how they are rend...
static const QString LABEL_CACHE_ID
QgsMapRendererCache ID string for cached label image.
bool isValid() const
Returns the status of the layer.
Enable layer opacity and blending effects.
static QSet< const QgsSymbolLayer * > toSymbolLayerPointers(QgsFeatureRenderer *renderer, const QSet< QgsSymbolLayerId > &symbolLayerIds)
Converts a set of symbol layer id to a set of pointers to actual symbol layers carried by the feature...
static QHash< QString, QSet< QgsSymbolLayerId > > symbolLayerMasks(const QgsVectorLayer *)
Returns all masks that may be defined on symbol layers for a given vector layer.
QgsRectangle visibleExtent() const
Returns the actual extent derived from requested extent that takes takes output image size into accou...
bool isValid() const
Returns true if the coordinate transform is valid, ie both the source and destination CRS have been s...
void setCache(QgsMapRendererCache *cache)
Assign a cache to be used for reading and storing rendered images of individual layers.
The QgsMapSettings class contains configuration for rendering of the map.
static bool staticWillUseLayer(QgsVectorLayer *layer)
called to find out whether the layer is used for labeling
QgsCoordinateTransform layerTransform(const QgsMapLayer *layer) const
Returns the coordinate transform from layer&#39;s CRS to destination CRS.
static QImage composeImage(const QgsMapSettings &settings, const LayerRenderJobs &jobs, const LabelRenderJob &labelJob)
LabelRenderJob prepareLabelingJob(QPainter *painter, QgsLabelingEngine *labelingEngine2, bool canUseLabelCache=true)
Prepares a labeling job.
bool isEditable() const FINAL
Returns true if the provider is in editing mode.
bool prepareLabelCache() const
Prepares the cache for storing the result of labeling.
LayerRenderJobs prepareJobs(QPainter *painter, QgsLabelingEngine *labelingEngine2, bool deferredPainterSet=false)
Creates a list of layer rendering jobs and prepares them for later render.
virtual bool requiresAdvancedEffects() const =0
Returns true if drawing labels requires advanced effects like composition modes, which could prevent ...
QHash< QgsWeakMapLayerPointer, int > mPerLayerRenderingTime
Render time (in ms) per layer, by layer ID.
QString id() const
Returns the layer&#39;s unique ID, which is used to access this layer from QgsProject.
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
void grow(double delta)
Grows the rectangle in place by the specified amount.
Definition: qgsrectangle.h:275
bool init(const QgsRectangle &extent, double scale)
Initialize cache: set new parameters and clears the cache if any parameters have changed since last i...
double scale() const
Returns the calculated map scale.
virtual void run(QgsRenderContext &context)=0
Runs the labeling job.
double width() const
Returns the width of the rectangle.
Definition: qgsrectangle.h:202
QgsMapLayerRenderer * createMapRenderer(QgsRenderContext &rendererContext) FINAL
Returns new instance of QgsMapLayerRenderer that will be used for rendering of given context...
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::Warning, bool notifyUser=true)
Adds a message to the log instance (and creates it if necessary).
Enable anti-aliasing for map rendering.
QString toString(int precision=16) const
Returns a string representation of form xmin,ymin : xmax,ymax Coordinates will be truncated to the sp...
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...
void setPainter(QPainter *p)
Sets the destination QPainter for the render operation.
QImage cacheImage(const QString &cacheKey) const
Returns the cached image for the specified cacheKey.
QgsFeatureRenderer * renderer()
Returns renderer.
const QgsAbstractVectorLayerLabeling * labeling() const
Access to const labeling configuration.
double minimumScale() const
Returns the minimum map scale (i.e.
double maximumScale() const
Returns the maximum map scale (i.e.
QHash< QgsMapLayer *, int > perLayerRenderingTime() const
Returns the render time (in ms) per layer.
float devicePixelRatio() const
Returns device pixel ratio Common values are 1 for normal-dpi displays and 2 for high-dpi "retina" di...
QPainter::CompositionMode featureBlendMode() const
Returns the current blending mode for features.
QgsMapSettings mSettings
bool isFinite() const
Returns true if the rectangle has finite boundaries.
Definition: qgsrectangle.h:516
double x
Definition: qgspointxy.h:47
void cleanupLabelJob(LabelRenderJob &job)
Handles clean up tasks for a label job, including deletion of images and storing cached label results...
double yMinimum() const
Returns the y minimum value (bottom side of rectangle).
Definition: qgsrectangle.h:177
Implementation of threaded rendering for vector layers.
double xMaximum() const
Returns the x maximum value (right side of rectangle).
Definition: qgsrectangle.h:162
bool hasCacheImage(const QString &cacheKey) const
Returns true if the cache contains an image with the specified cacheKey.
The QgsLabelingEngine class provides map labeling functionality.
Contains information about the context of a rendering operation.
static QHash< QString, QHash< QString, QSet< QgsSymbolLayerId > > > labelMasks(const QgsVectorLayer *)
Returns masks defined in labeling options of a layer.
QMap< QString, QString > layerStyleOverrides() const
Gets map of map layer style overrides (key: layer ID, value: style name) where a different style shou...
QList< QgsMapRendererJob::Error > Errors
Transform from destination to source CRS.
LayerRenderJobs prepareSecondPassJobs(LayerRenderJobs &firstPassJobs, LabelRenderJob &labelJob)
Prepares jobs for a second pass, if selective masks exist (from labels or symbol layers).
bool hasScaleBasedVisibility() const
Returns whether scale based visibility is enabled for the layer.
static QgsRenderContext fromMapSettings(const QgsMapSettings &mapSettings)
create initialized QgsRenderContext instance from given QgsMapSettings
void clearCacheImage(const QString &cacheKey)
Removes an image from the cache with matching cacheKey.
Class for doing transforms between two map coordinate systems.
double xMinimum() const
Returns the x minimum value (left side of rectangle).
Definition: qgsrectangle.h:167
static void composeSecondPass(LayerRenderJobs &secondPassJobs, LabelRenderJob &labelJob)
Compose second pass images into first pass images.
virtual QgsMapLayerRenderer * createMapRenderer(QgsRenderContext &rendererContext)=0
Returns new instance of QgsMapLayerRenderer that will be used for rendering of given context...
QgsMapRendererQImageJob(const QgsMapSettings &settings)
double yMaximum() const
Returns the y maximum value (top side of rectangle).
Definition: qgsrectangle.h:172
QString name
Definition: qgsmaplayer.h:83
Transform from source to destination CRS.
This class is responsible for keeping cache of rendered images resulting from a map rendering job...
bool testFlag(Flag flag) const
Check whether a particular flag is enabled.
Custom exception class for Coordinate Reference System related exceptions.
Definition: qgsexception.h:65
Type used to refer to a specific symbol layer in a symbol of a layer.
static QgsExpressionContextScope * layerScope(const QgsMapLayer *layer)
Creates a new scope which contains variables and functions relating to a QgsMapLayer.
Represents a vector layer which manages a vector based data sets.
const QgsMapSettings & mapSettings() const
Returns map settings with which this job was started.
QgsMapRendererCache * mCache
QSize outputSize() const
Returns the size of the resulting map image.
QgsMapRendererJob(const QgsMapSettings &settings)
QgsRectangle transformBoundingBox(const QgsRectangle &rectangle, TransformDirection direction=ForwardTransform, bool handle180Crossover=false) const SIP_THROW(QgsCsException)
Transforms a rectangle from the source CRS to the destination CRS.
void setXMinimum(double x)
Set the minimum x value.
Definition: qgsrectangle.h:130
QgsCoordinateReferenceSystem crs
Definition: qgsmaplayer.h:86
QPainter::CompositionMode blendMode() const
Returns the current blending mode for a layer.
double height() const
Returns the height of the rectangle.
Definition: qgsrectangle.h:209