Skip to content
Permalink
Browse files

[FEATURE] Cache labeling result to avoid unnecessary redraws

when refreshing canvas

This change allows the labeling results to be cached to an image
following a map render. If the cached label result image can be
reused for the next render then it will be, avoiding the need
to redraw all layers participating in the labeling problem and
resolving the labeling solution.

Basically this means that canvas refreshes as a result of changes
to any NON-LABELED layer are much faster. (Changing a layer which
is part of the labeling solution still requires all labeled
layers to be completely redrawn)
  • Loading branch information
nyalldawson committed Feb 7, 2017
1 parent 64748aa commit 33eb4bc0c4b047d461c374cf7bf1b361f2402f91
@@ -23,6 +23,7 @@
#include "qgspallabeling.h"
#include "qgsvectorlayer.h"
#include "qgsrenderer.h"
#include "qgsmaplayerlistutils.h"

QgsMapRendererCustomPainterJob::QgsMapRendererCustomPainterJob( const QgsMapSettings& settings, QPainter* painter )
: QgsMapRendererJob( settings )
@@ -83,6 +84,7 @@ void QgsMapRendererCustomPainterJob::start()
}

mLayerJobs = prepareJobs( mPainter, mLabelingEngineV2 );
mLabelJob = prepareLabelingJob( mPainter, mLabelingEngineV2 );

QgsDebugMsg( "Rendering prepared in (seconds): " + QString( "%1" ).arg( prepareTime.elapsed() / 1000.0 ) );

@@ -112,7 +114,7 @@ void QgsMapRendererCustomPainterJob::cancel()
QgsDebugMsg( "QPAINTER canceling" );
disconnect( &mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsMapRendererCustomPainterJob::futureFinished );

mLabelingRenderContext.setRenderingStopped( true );
mLabelJob.context.setRenderingStopped( true );
for ( LayerRenderJobs::iterator it = mLayerJobs.begin(); it != mLayerJobs.end(); ++it )
{
it->context.setRenderingStopped( true );
@@ -187,10 +189,11 @@ void QgsMapRendererCustomPainterJob::futureFinished()
mRenderingTime = mRenderingStart.elapsed();
QgsDebugMsg( "QPAINTER futureFinished" );

logRenderingTime( mLayerJobs );
logRenderingTime( mLayerJobs, mLabelJob );

// final cleanup
cleanupJobs( mLayerJobs );
cleanupLabelJob( mLabelJob );

emit finished();
}
@@ -263,8 +266,38 @@ void QgsMapRendererCustomPainterJob::doRender()

QgsDebugMsg( "Done rendering map layers" );

if ( mSettings.testFlag( QgsMapSettings::DrawLabeling ) && !mLabelingRenderContext.renderingStopped() )
drawLabeling( mSettings, mLabelingRenderContext, mLabelingEngineV2, mPainter );
if ( mSettings.testFlag( QgsMapSettings::DrawLabeling ) && !mLabelJob.context.renderingStopped() )
{
if ( !mLabelJob.cached )
{
QTime labelTime;
labelTime.start();

if ( mLabelJob.img )
{
QPainter painter;
mLabelJob.img->fill( 0 );
painter.begin( mLabelJob.img );
mLabelJob.context.setPainter( &painter );
drawLabeling( mSettings, mLabelJob.context, mLabelingEngineV2, &painter );
painter.end();
}
else
{
drawLabeling( mSettings, mLabelJob.context, mLabelingEngineV2, mPainter );
}

mLabelJob.complete = true;
mLabelJob.renderingTime = labelTime.elapsed();
mLabelJob.participatingLayers = _qgis_listRawToQPointer( mLabelingEngineV2->participatingLayers() );
}
}
if ( mLabelJob.img && mLabelJob.complete )
{
mPainter->setCompositionMode( QPainter::CompositionMode_SourceOver );
mPainter->setOpacity( 1.0 );
mPainter->drawImage( 0, 0, *mLabelJob.img );
}

QgsDebugMsg( "Rendering completed in (seconds): " + QString( "%1" ).arg( renderTime.elapsed() / 1000.0 ) );
}
@@ -83,11 +83,11 @@ class CORE_EXPORT QgsMapRendererCustomPainterJob : public QgsMapRendererJob
QPainter* mPainter;
QFuture<void> mFuture;
QFutureWatcher<void> mFutureWatcher;
QgsRenderContext mLabelingRenderContext;
QgsLabelingEngine* mLabelingEngineV2;

bool mActive;
LayerRenderJobs mLayerJobs;
LabelRenderJob mLabelJob;
bool mRenderSynchronously;

};
@@ -33,9 +33,13 @@
#include "qgsvectorlayerrenderer.h"
#include "qgsvectorlayer.h"
#include "qgscsexception.h"
#include "qgslabelingengine.h"
#include "qgsmaplayerlistutils.h"

///@cond PRIVATE

const QString QgsMapRendererJob::LABEL_CACHE_ID = QStringLiteral( "_labels_" );

QgsMapRendererJob::QgsMapRendererJob( const QgsMapSettings& settings )
: mSettings( settings )
, mCache( nullptr )
@@ -179,11 +183,36 @@ LayerRenderJobs QgsMapRendererJob::prepareJobs( QPainter* painter, QgsLabelingEn
QListIterator<QgsMapLayer*> li( mSettings.layers() );
li.toBack();

bool cacheValid = false;
if ( mCache )
{
bool cacheValid = mCache->init( mSettings.visibleExtent(), mSettings.scale() );
cacheValid = mCache->init( mSettings.visibleExtent(), mSettings.scale() );
QgsDebugMsg( QString( "CACHE VALID: %1" ).arg( cacheValid ) );
Q_UNUSED( cacheValid );
}

bool hasCachedLabels = false;
if ( cacheValid && mCache->hasCacheImage( LABEL_CACHE_ID ) )
{
// we may need to clear label cache and re-register labeled features - check for that here

// calculate which layers will be labeled
QSet< QgsMapLayer* > labeledLayers;
Q_FOREACH ( const QgsMapLayer* ml, mSettings.layers() )
{
QgsVectorLayer* vl = const_cast< QgsVectorLayer* >( qobject_cast<const QgsVectorLayer *>( ml ) );
if ( vl && QgsPalLabeling::staticWillUseLayer( vl ) )
labeledLayers << vl;
}

// can we reuse the cached label solution?
bool canUseCache = mCache->dependentLayers( LABEL_CACHE_ID ).toSet() == labeledLayers;
if ( !canUseCache )
{
// no - participating layers have changed
mCache->clearCacheImage( LABEL_CACHE_ID );
}

hasCachedLabels = canUseCache;
}

mGeometryCaches.clear();
@@ -229,8 +258,12 @@ LayerRenderJobs QgsMapRendererJob::prepareJobs( QPainter* painter, QgsLabelingEn
if ( mCache && ml->type() == QgsMapLayer::VectorLayer )
{
QgsVectorLayer* vl = qobject_cast<QgsVectorLayer *>( ml );
if ( vl->isEditable() || ( labelingEngine2 && QgsPalLabeling::staticWillUseLayer( vl ) ) )
bool requiresLabelRedraw = false;
requiresLabelRedraw = ( labelingEngine2 && QgsPalLabeling::staticWillUseLayer( vl ) ) && !hasCachedLabels;
if ( vl->isEditable() || requiresLabelRedraw )
{
mCache->clearCacheImage( ml->id() );
}
}

layerJobs.append( LayerRenderJob() );
@@ -312,6 +345,47 @@ LayerRenderJobs QgsMapRendererJob::prepareJobs( QPainter* painter, QgsLabelingEn
return layerJobs;
}

LabelRenderJob QgsMapRendererJob::prepareLabelingJob( QPainter* painter, QgsLabelingEngine* labelingEngine2 )
{
LabelRenderJob job;
job.context = QgsRenderContext::fromMapSettings( mSettings );
job.context.setPainter( painter );
job.context.setLabelingEngine( labelingEngine2 );
job.context.setExtent( mSettings.visibleExtent() );

This comment has been minimized.

Copy link
@m-kuhn

m-kuhn Jan 5, 2019

Member

@nyalldawson any idea why this is required? Looks like that's already done in QgsRendererContext::fromMapSettings 3 lines above


// if we can use the cache, let's do it and avoid rendering!
bool canUseCache = mCache && mCache->hasCacheImage( LABEL_CACHE_ID );
if ( canUseCache )
{
job.cached = true;
job.complete = true;
job.img = new QImage( mCache->cacheImage( LABEL_CACHE_ID ) );
job.context.setPainter( nullptr );
}
else
{
if ( mCache || !painter )
{
// Flattened image for drawing labels
QImage * mypFlattenedImage = nullptr;
mypFlattenedImage = new QImage( mSettings.outputSize().width(),
mSettings.outputSize().height(),
mSettings.outputImageFormat() );
if ( mypFlattenedImage->isNull() )
{
mErrors.append( Error( QStringLiteral( "labels" ), tr( "Insufficient memory for label image %1x%2" ).arg( mSettings.outputSize().width() ).arg( mSettings.outputSize().height() ) ) );
delete mypFlattenedImage;
}
else
{
job.img = mypFlattenedImage;
}
}
}

return job;
}


void QgsMapRendererJob::cleanupJobs( LayerRenderJobs& jobs )
{
@@ -343,13 +417,29 @@ void QgsMapRendererJob::cleanupJobs( LayerRenderJobs& jobs )
}
}


jobs.clear();

updateLayerGeometryCaches();
}

void QgsMapRendererJob::cleanupLabelJob( LabelRenderJob& job )
{
if ( job.img )
{
if ( mCache && !job.cached && !job.context.renderingStopped() )
{
QgsDebugMsg( "caching label result image" );
mCache->setCacheImage( LABEL_CACHE_ID, *job.img, _qgis_listQPointerToRaw( job.participatingLayers ) );
}

delete job.img;
job.img = nullptr;
}
}


QImage QgsMapRendererJob::composeImage( const QgsMapSettings& settings, const LayerRenderJobs& jobs )
QImage QgsMapRendererJob::composeImage( const QgsMapSettings& settings, const LayerRenderJobs& jobs, const LabelRenderJob& labelJob )
{
QImage image( settings.outputSize(), settings.outputImageFormat() );
image.fill( settings.backgroundColor().rgba() );
@@ -368,11 +458,21 @@ QImage QgsMapRendererJob::composeImage( const QgsMapSettings& settings, const La
painter.drawImage( 0, 0, *job.img );
}

// IMPORTANT - don't draw labelJob img before the label job is complete,
// as the image is uninitialized and full of garbage before the label job
// commences
if ( labelJob.img && labelJob.complete )
{
painter.setCompositionMode( QPainter::CompositionMode_SourceOver );
painter.setOpacity( 1.0 );
painter.drawImage( 0, 0, *labelJob.img );
}

painter.end();
return image;
}

void QgsMapRendererJob::logRenderingTime( const LayerRenderJobs& jobs )
void QgsMapRendererJob::logRenderingTime( const LayerRenderJobs& jobs, const LabelRenderJob& labelJob )
{
QSettings settings;
if ( !settings.value( QStringLiteral( "/Map/logCanvasRefreshEvent" ), false ).toBool() )
@@ -382,6 +482,8 @@ void QgsMapRendererJob::logRenderingTime( const LayerRenderJobs& jobs )
Q_FOREACH ( const LayerRenderJob& job, jobs )
elapsed.insert( job.renderingTime, job.layer ? job.layer->id() : QString() );

elapsed.insert( labelJob.renderingTime, tr( "Labeling" ) );

QList<int> tt( elapsed.uniqueKeys() );
qSort( tt.begin(), tt.end(), qGreater<int>() );
Q_FOREACH ( int t, tt )
@@ -56,6 +56,28 @@ struct LayerRenderJob

typedef QList<LayerRenderJob> LayerRenderJobs;

/** \ingroup core
* Structure keeping low-level label rendering job information.
*/
struct LabelRenderJob
{
QgsRenderContext context;

/**
* May be null if it is not necessary to draw to separate image (e.g. using composition modes which prevent "flattening" the layer).
* Note that if complete is false then img will be uninitialized and contain random data!.
*/
QImage* img = nullptr;
//! If true, img already contains cached image from previous rendering
bool cached = false;
//! If true then label render is complete
bool complete = false;
//! Time it took to render the labels in ms (it is -1 if not rendered or still rendering)
int renderingTime = -1;
//! List of layers which participated in the labeling solution
QList< QPointer< QgsMapLayer > > participatingLayers;
};

///@endcond PRIVATE

/** \ingroup core
@@ -153,6 +175,12 @@ class CORE_EXPORT QgsMapRendererJob : public QObject
*/
const QgsMapSettings& mapSettings() const;

/**
* QgsMapRendererCache ID string for cached label image.
* @note not available in Python bindings
*/
static const QString LABEL_CACHE_ID;

signals:

/**
@@ -180,15 +208,30 @@ class CORE_EXPORT QgsMapRendererJob : public QObject
//! @note not available in python bindings
LayerRenderJobs prepareJobs( QPainter* painter, QgsLabelingEngine* labelingEngine2 );

/**
* Prepares a labeling job.
* @note not available in python bindings
* @note added in QGIS 3.0
*/
LabelRenderJob prepareLabelingJob( QPainter* painter, QgsLabelingEngine* labelingEngine2 );

//! @note not available in python bindings
static QImage composeImage( const QgsMapSettings& settings, const LayerRenderJobs& jobs );
static QImage composeImage( const QgsMapSettings& settings, const LayerRenderJobs& jobs, const LabelRenderJob& labelJob );

//! @note not available in python bindings
void logRenderingTime( const LayerRenderJobs& jobs );
void logRenderingTime( const LayerRenderJobs& jobs, const LabelRenderJob& labelJob );

//! @note not available in python bindings
void cleanupJobs( LayerRenderJobs& jobs );

/**
* Handles clean up tasks for a label job, including deletion of images and storing cached
* label results.
* @note added in QGIS 3.0
* @note not available in python bindings
*/
void cleanupLabelJob( LabelRenderJob& job );

//! @note not available in Python bindings
static void drawLabeling( const QgsMapSettings& settings, QgsRenderContext& renderContext, QgsLabelingEngine* labelingEngine2, QPainter* painter );

0 comments on commit 33eb4bc

Please sign in to comment.
You can’t perform that action at this time.