"Fossies" - the Fresh Open Source Software Archive

Member "QGIS-final-3_14_16/src/core/qgsmaprendererjob.cpp" (11 Sep 2020, 34246 Bytes) of package /linux/misc/QGIS-final-3_14_16.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) C and C++ source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "qgsmaprendererjob.cpp" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 3_14_15_vs_3_14_16.

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