Skip to content

Commit 0f5b3fd

Browse files
authored
Merge pull request #4110 from nyalldawson/label_cache
[FEATURE] Cache labeling result to avoid unnecessary redraws when refreshing canvas
2 parents 61523c8 + a08137f commit 0f5b3fd

19 files changed

+770
-44
lines changed

python/core/qgsmaprenderercustompainterjob.sip

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class QgsMapRendererCustomPainterJob : QgsMapRendererJob
2121
virtual void cancel();
2222
virtual void waitForFinished();
2323
virtual bool isActive() const;
24+
virtual bool usedCachedLabels() const;
2425
virtual QgsLabelingResults* takeLabelingResults() /Transfer/;
2526

2627
//! @note not available in python bindings

python/core/qgsmaprendererjob.sip

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ class QgsMapRendererJob : QObject
2424
//! Tell whether the rendering job is currently running in background.
2525
virtual bool isActive() const = 0;
2626

27+
virtual bool usedCachedLabels() const = 0;
28+
2729
//! Get pointer to internal labeling engine (in order to get access to the results)
2830
virtual QgsLabelingResults* takeLabelingResults() = 0 /Transfer/;
2931

python/core/qgsmaprendererparalleljob.sip

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class QgsMapRendererParallelJob : QgsMapRendererQImageJob
2020
virtual void cancel();
2121
virtual void waitForFinished();
2222
virtual bool isActive() const;
23+
virtual bool usedCachedLabels() const;
2324

2425
virtual QgsLabelingResults* takeLabelingResults() /Transfer/;
2526

python/core/qgsmaprenderersequentialjob.sip

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class QgsMapRendererSequentialJob : QgsMapRendererQImageJob
2121
virtual void cancel();
2222
virtual void waitForFinished();
2323
virtual bool isActive() const;
24+
virtual bool usedCachedLabels() const;
2425

2526
virtual QgsLabelingResults* takeLabelingResults() /Transfer/;
2627

src/core/qgsmaprenderercustompainterjob.cpp

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include "qgspallabeling.h"
2424
#include "qgsvectorlayer.h"
2525
#include "qgsrenderer.h"
26+
#include "qgsmaplayerlistutils.h"
2627

2728
QgsMapRendererCustomPainterJob::QgsMapRendererCustomPainterJob( const QgsMapSettings& settings, QPainter* painter )
2829
: QgsMapRendererJob( settings )
@@ -82,7 +83,9 @@ void QgsMapRendererCustomPainterJob::start()
8283
mLabelingEngineV2->setMapSettings( mSettings );
8384
}
8485

86+
bool canUseLabelCache = prepareLabelCache();
8587
mLayerJobs = prepareJobs( mPainter, mLabelingEngineV2 );
88+
mLabelJob = prepareLabelingJob( mPainter, mLabelingEngineV2, canUseLabelCache );
8689

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

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

115-
mLabelingRenderContext.setRenderingStopped( true );
118+
mLabelJob.context.setRenderingStopped( true );
116119
for ( LayerRenderJobs::iterator it = mLayerJobs.begin(); it != mLayerJobs.end(); ++it )
117120
{
118121
it->context.setRenderingStopped( true );
@@ -154,6 +157,10 @@ bool QgsMapRendererCustomPainterJob::isActive() const
154157
return mActive;
155158
}
156159

160+
bool QgsMapRendererCustomPainterJob::usedCachedLabels() const
161+
{
162+
return mLabelJob.cached;
163+
}
157164

158165
QgsLabelingResults* QgsMapRendererCustomPainterJob::takeLabelingResults()
159166
{
@@ -187,10 +194,11 @@ void QgsMapRendererCustomPainterJob::futureFinished()
187194
mRenderingTime = mRenderingStart.elapsed();
188195
QgsDebugMsg( "QPAINTER futureFinished" );
189196

190-
logRenderingTime( mLayerJobs );
197+
logRenderingTime( mLayerJobs, mLabelJob );
191198

192199
// final cleanup
193200
cleanupJobs( mLayerJobs );
201+
cleanupLabelJob( mLabelJob );
194202

195203
emit finished();
196204
}
@@ -263,8 +271,38 @@ void QgsMapRendererCustomPainterJob::doRender()
263271

264272
QgsDebugMsg( "Done rendering map layers" );
265273

266-
if ( mSettings.testFlag( QgsMapSettings::DrawLabeling ) && !mLabelingRenderContext.renderingStopped() )
267-
drawLabeling( mSettings, mLabelingRenderContext, mLabelingEngineV2, mPainter );
274+
if ( mSettings.testFlag( QgsMapSettings::DrawLabeling ) && !mLabelJob.context.renderingStopped() )
275+
{
276+
if ( !mLabelJob.cached )
277+
{
278+
QTime labelTime;
279+
labelTime.start();
280+
281+
if ( mLabelJob.img )
282+
{
283+
QPainter painter;
284+
mLabelJob.img->fill( 0 );
285+
painter.begin( mLabelJob.img );
286+
mLabelJob.context.setPainter( &painter );
287+
drawLabeling( mSettings, mLabelJob.context, mLabelingEngineV2, &painter );
288+
painter.end();
289+
}
290+
else
291+
{
292+
drawLabeling( mSettings, mLabelJob.context, mLabelingEngineV2, mPainter );
293+
}
294+
295+
mLabelJob.complete = true;
296+
mLabelJob.renderingTime = labelTime.elapsed();
297+
mLabelJob.participatingLayers = _qgis_listRawToQPointer( mLabelingEngineV2->participatingLayers() );
298+
}
299+
}
300+
if ( mLabelJob.img && mLabelJob.complete )
301+
{
302+
mPainter->setCompositionMode( QPainter::CompositionMode_SourceOver );
303+
mPainter->setOpacity( 1.0 );
304+
mPainter->drawImage( 0, 0, *mLabelJob.img );
305+
}
268306

269307
QgsDebugMsg( "Rendering completed in (seconds): " + QString( "%1" ).arg( renderTime.elapsed() / 1000.0 ) );
270308
}

src/core/qgsmaprenderercustompainterjob.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class CORE_EXPORT QgsMapRendererCustomPainterJob : public QgsMapRendererJob
4141
virtual void cancel() override;
4242
virtual void waitForFinished() override;
4343
virtual bool isActive() const override;
44+
virtual bool usedCachedLabels() const override;
4445
virtual QgsLabelingResults* takeLabelingResults() override;
4546

4647
//! @note not available in python bindings
@@ -83,11 +84,11 @@ class CORE_EXPORT QgsMapRendererCustomPainterJob : public QgsMapRendererJob
8384
QPainter* mPainter;
8485
QFuture<void> mFuture;
8586
QFutureWatcher<void> mFutureWatcher;
86-
QgsRenderContext mLabelingRenderContext;
8787
QgsLabelingEngine* mLabelingEngineV2;
8888

8989
bool mActive;
9090
LayerRenderJobs mLayerJobs;
91+
LabelRenderJob mLabelJob;
9192
bool mRenderSynchronously;
9293

9394
};

src/core/qgsmaprendererjob.cpp

Lines changed: 118 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,14 @@
3333
#include "qgsvectorlayerrenderer.h"
3434
#include "qgsvectorlayer.h"
3535
#include "qgscsexception.h"
36+
#include "qgslabelingengine.h"
37+
#include "qgsmaplayerlistutils.h"
38+
#include "qgsvectorlayerlabeling.h"
3639

3740
///@cond PRIVATE
3841

42+
const QString QgsMapRendererJob::LABEL_CACHE_ID = QStringLiteral( "_labels_" );
43+
3944
QgsMapRendererJob::QgsMapRendererJob( const QgsMapSettings& settings )
4045
: mSettings( settings )
4146
, mCache( nullptr )
@@ -66,6 +71,39 @@ const QgsMapSettings& QgsMapRendererJob::mapSettings() const
6671
return mSettings;
6772
}
6873

74+
bool QgsMapRendererJob::prepareLabelCache() const
75+
{
76+
bool canCache = mCache;
77+
78+
// calculate which layers will be labeled
79+
QSet< QgsMapLayer* > labeledLayers;
80+
Q_FOREACH ( const QgsMapLayer* ml, mSettings.layers() )
81+
{
82+
QgsVectorLayer* vl = const_cast< QgsVectorLayer* >( qobject_cast<const QgsVectorLayer *>( ml ) );
83+
if ( vl && QgsPalLabeling::staticWillUseLayer( vl ) )
84+
labeledLayers << vl;
85+
if ( vl && vl->labeling() && vl->labeling()->requiresAdvancedEffects( vl ) )
86+
{
87+
canCache = false;
88+
break;
89+
}
90+
}
91+
92+
if ( mCache && mCache->hasCacheImage( LABEL_CACHE_ID ) )
93+
{
94+
// we may need to clear label cache and re-register labeled features - check for that here
95+
96+
// can we reuse the cached label solution?
97+
bool canUseCache = canCache && mCache->dependentLayers( LABEL_CACHE_ID ).toSet() == labeledLayers;
98+
if ( !canUseCache )
99+
{
100+
// no - participating layers have changed
101+
mCache->clearCacheImage( LABEL_CACHE_ID );
102+
}
103+
}
104+
return canCache;
105+
}
106+
69107

70108
bool QgsMapRendererJob::reprojectToLayerExtent( const QgsMapLayer *ml, const QgsCoordinateTransform& ct, QgsRectangle &extent, QgsRectangle &r2 )
71109
{
@@ -179,13 +217,15 @@ LayerRenderJobs QgsMapRendererJob::prepareJobs( QPainter* painter, QgsLabelingEn
179217
QListIterator<QgsMapLayer*> li( mSettings.layers() );
180218
li.toBack();
181219

220+
bool cacheValid = false;
182221
if ( mCache )
183222
{
184-
bool cacheValid = mCache->init( mSettings.visibleExtent(), mSettings.scale() );
223+
cacheValid = mCache->init( mSettings.visibleExtent(), mSettings.scale() );
185224
QgsDebugMsg( QString( "CACHE VALID: %1" ).arg( cacheValid ) );
186-
Q_UNUSED( cacheValid );
187225
}
188226

227+
bool requiresLabelRedraw = !( mCache && mCache->hasCacheImage( LABEL_CACHE_ID ) );
228+
189229
mGeometryCaches.clear();
190230

191231
while ( li.hasPrevious() )
@@ -229,8 +269,12 @@ LayerRenderJobs QgsMapRendererJob::prepareJobs( QPainter* painter, QgsLabelingEn
229269
if ( mCache && ml->type() == QgsMapLayer::VectorLayer )
230270
{
231271
QgsVectorLayer* vl = qobject_cast<QgsVectorLayer *>( ml );
232-
if ( vl->isEditable() || ( labelingEngine2 && QgsPalLabeling::staticWillUseLayer( vl ) ) )
272+
bool requiresLabeling = false;
273+
requiresLabeling = ( labelingEngine2 && QgsPalLabeling::staticWillUseLayer( vl ) ) && requiresLabelRedraw;
274+
if ( vl->isEditable() || requiresLabeling )
275+
{
233276
mCache->clearCacheImage( ml->id() );
277+
}
234278
}
235279

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

359+
LabelRenderJob QgsMapRendererJob::prepareLabelingJob( QPainter* painter, QgsLabelingEngine* labelingEngine2, bool canUseLabelCache )
360+
{
361+
LabelRenderJob job;
362+
job.context = QgsRenderContext::fromMapSettings( mSettings );
363+
job.context.setPainter( painter );
364+
job.context.setLabelingEngine( labelingEngine2 );
365+
job.context.setExtent( mSettings.visibleExtent() );
366+
367+
// if we can use the cache, let's do it and avoid rendering!
368+
bool hasCache = canUseLabelCache && mCache && mCache->hasCacheImage( LABEL_CACHE_ID );
369+
if ( hasCache )
370+
{
371+
job.cached = true;
372+
job.complete = true;
373+
job.img = new QImage( mCache->cacheImage( LABEL_CACHE_ID ) );
374+
job.context.setPainter( nullptr );
375+
}
376+
else
377+
{
378+
if ( canUseLabelCache && ( mCache || !painter ) )
379+
{
380+
// Flattened image for drawing labels
381+
QImage * mypFlattenedImage = nullptr;
382+
mypFlattenedImage = new QImage( mSettings.outputSize().width(),
383+
mSettings.outputSize().height(),
384+
mSettings.outputImageFormat() );
385+
if ( mypFlattenedImage->isNull() )
386+
{
387+
mErrors.append( Error( QStringLiteral( "labels" ), tr( "Insufficient memory for label image %1x%2" ).arg( mSettings.outputSize().width() ).arg( mSettings.outputSize().height() ) ) );
388+
delete mypFlattenedImage;
389+
}
390+
else
391+
{
392+
job.img = mypFlattenedImage;
393+
}
394+
}
395+
}
396+
397+
return job;
398+
}
399+
315400

316401
void QgsMapRendererJob::cleanupJobs( LayerRenderJobs& jobs )
317402
{
@@ -343,13 +428,29 @@ void QgsMapRendererJob::cleanupJobs( LayerRenderJobs& jobs )
343428
}
344429
}
345430

431+
346432
jobs.clear();
347433

348434
updateLayerGeometryCaches();
349435
}
350436

437+
void QgsMapRendererJob::cleanupLabelJob( LabelRenderJob& job )
438+
{
439+
if ( job.img )
440+
{
441+
if ( mCache && !job.cached && !job.context.renderingStopped() )
442+
{
443+
QgsDebugMsg( "caching label result image" );
444+
mCache->setCacheImage( LABEL_CACHE_ID, *job.img, _qgis_listQPointerToRaw( job.participatingLayers ) );
445+
}
351446

352-
QImage QgsMapRendererJob::composeImage( const QgsMapSettings& settings, const LayerRenderJobs& jobs )
447+
delete job.img;
448+
job.img = nullptr;
449+
}
450+
}
451+
452+
453+
QImage QgsMapRendererJob::composeImage( const QgsMapSettings& settings, const LayerRenderJobs& jobs, const LabelRenderJob& labelJob )
353454
{
354455
QImage image( settings.outputSize(), settings.outputImageFormat() );
355456
image.fill( settings.backgroundColor().rgba() );
@@ -368,11 +469,21 @@ QImage QgsMapRendererJob::composeImage( const QgsMapSettings& settings, const La
368469
painter.drawImage( 0, 0, *job.img );
369470
}
370471

472+
// IMPORTANT - don't draw labelJob img before the label job is complete,
473+
// as the image is uninitialized and full of garbage before the label job
474+
// commences
475+
if ( labelJob.img && labelJob.complete )
476+
{
477+
painter.setCompositionMode( QPainter::CompositionMode_SourceOver );
478+
painter.setOpacity( 1.0 );
479+
painter.drawImage( 0, 0, *labelJob.img );
480+
}
481+
371482
painter.end();
372483
return image;
373484
}
374485

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

496+
elapsed.insert( labelJob.renderingTime, tr( "Labeling" ) );
497+
385498
QList<int> tt( elapsed.uniqueKeys() );
386499
std::sort( tt.begin(), tt.end(), std::greater<int>() );
387500
Q_FOREACH ( int t, tt )

0 commit comments

Comments
 (0)