Skip to content

Commit

Permalink
[FEATURE] Cache labeling result to avoid unnecessary redraws
Browse files Browse the repository at this point in the history
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 33eb4bc
Show file tree
Hide file tree
Showing 7 changed files with 251 additions and 41 deletions.
41 changes: 37 additions & 4 deletions src/core/qgsmaprenderercustompainterjob.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 )
Expand Down Expand Up @@ -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 ) );

Expand Down Expand Up @@ -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 );
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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 ) );
}
Expand Down
2 changes: 1 addition & 1 deletion src/core/qgsmaprenderercustompainterjob.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

};
Expand Down
112 changes: 107 additions & 5 deletions src/core/qgsmaprendererjob.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 )
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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() );
Expand Down Expand Up @@ -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 )
{
Expand Down Expand Up @@ -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() );
Expand All @@ -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() )
Expand All @@ -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 )
Expand Down
47 changes: 45 additions & 2 deletions src/core/qgsmaprendererjob.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:

/**
Expand Down Expand Up @@ -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 );

Expand Down

0 comments on commit 33eb4bc

Please sign in to comment.