Skip to content

Commit 33eb4bc

Browse files
committedFeb 7, 2017
[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)
1 parent 64748aa commit 33eb4bc

7 files changed

+251
-41
lines changed
 

‎src/core/qgsmaprenderercustompainterjob.cpp

Lines changed: 37 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 )
@@ -83,6 +84,7 @@ void QgsMapRendererCustomPainterJob::start()
8384
}
8485

8586
mLayerJobs = prepareJobs( mPainter, mLabelingEngineV2 );
87+
mLabelJob = prepareLabelingJob( mPainter, mLabelingEngineV2 );
8688

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

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

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

190-
logRenderingTime( mLayerJobs );
192+
logRenderingTime( mLayerJobs, mLabelJob );
191193

192194
// final cleanup
193195
cleanupJobs( mLayerJobs );
196+
cleanupLabelJob( mLabelJob );
194197

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

264267
QgsDebugMsg( "Done rendering map layers" );
265268

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

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

‎src/core/qgsmaprenderercustompainterjob.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,11 @@ class CORE_EXPORT QgsMapRendererCustomPainterJob : public QgsMapRendererJob
8383
QPainter* mPainter;
8484
QFuture<void> mFuture;
8585
QFutureWatcher<void> mFutureWatcher;
86-
QgsRenderContext mLabelingRenderContext;
8786
QgsLabelingEngine* mLabelingEngineV2;
8887

8988
bool mActive;
9089
LayerRenderJobs mLayerJobs;
90+
LabelRenderJob mLabelJob;
9191
bool mRenderSynchronously;
9292

9393
};

‎src/core/qgsmaprendererjob.cpp

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

3739
///@cond PRIVATE
3840

41+
const QString QgsMapRendererJob::LABEL_CACHE_ID = QStringLiteral( "_labels_" );
42+
3943
QgsMapRendererJob::QgsMapRendererJob( const QgsMapSettings& settings )
4044
: mSettings( settings )
4145
, mCache( nullptr )
@@ -179,11 +183,36 @@ LayerRenderJobs QgsMapRendererJob::prepareJobs( QPainter* painter, QgsLabelingEn
179183
QListIterator<QgsMapLayer*> li( mSettings.layers() );
180184
li.toBack();
181185

186+
bool cacheValid = false;
182187
if ( mCache )
183188
{
184-
bool cacheValid = mCache->init( mSettings.visibleExtent(), mSettings.scale() );
189+
cacheValid = mCache->init( mSettings.visibleExtent(), mSettings.scale() );
185190
QgsDebugMsg( QString( "CACHE VALID: %1" ).arg( cacheValid ) );
186-
Q_UNUSED( cacheValid );
191+
}
192+
193+
bool hasCachedLabels = false;
194+
if ( cacheValid && mCache->hasCacheImage( LABEL_CACHE_ID ) )
195+
{
196+
// we may need to clear label cache and re-register labeled features - check for that here
197+
198+
// calculate which layers will be labeled
199+
QSet< QgsMapLayer* > labeledLayers;
200+
Q_FOREACH ( const QgsMapLayer* ml, mSettings.layers() )
201+
{
202+
QgsVectorLayer* vl = const_cast< QgsVectorLayer* >( qobject_cast<const QgsVectorLayer *>( ml ) );
203+
if ( vl && QgsPalLabeling::staticWillUseLayer( vl ) )
204+
labeledLayers << vl;
205+
}
206+
207+
// can we reuse the cached label solution?
208+
bool canUseCache = mCache->dependentLayers( LABEL_CACHE_ID ).toSet() == labeledLayers;
209+
if ( !canUseCache )
210+
{
211+
// no - participating layers have changed
212+
mCache->clearCacheImage( LABEL_CACHE_ID );
213+
}
214+
215+
hasCachedLabels = canUseCache;
187216
}
188217

189218
mGeometryCaches.clear();
@@ -229,8 +258,12 @@ LayerRenderJobs QgsMapRendererJob::prepareJobs( QPainter* painter, QgsLabelingEn
229258
if ( mCache && ml->type() == QgsMapLayer::VectorLayer )
230259
{
231260
QgsVectorLayer* vl = qobject_cast<QgsVectorLayer *>( ml );
232-
if ( vl->isEditable() || ( labelingEngine2 && QgsPalLabeling::staticWillUseLayer( vl ) ) )
261+
bool requiresLabelRedraw = false;
262+
requiresLabelRedraw = ( labelingEngine2 && QgsPalLabeling::staticWillUseLayer( vl ) ) && !hasCachedLabels;
263+
if ( vl->isEditable() || requiresLabelRedraw )
264+
{
233265
mCache->clearCacheImage( ml->id() );
266+
}
234267
}
235268

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

348+
LabelRenderJob QgsMapRendererJob::prepareLabelingJob( QPainter* painter, QgsLabelingEngine* labelingEngine2 )
349+
{
350+
LabelRenderJob job;
351+
job.context = QgsRenderContext::fromMapSettings( mSettings );
352+
job.context.setPainter( painter );
353+
job.context.setLabelingEngine( labelingEngine2 );
354+
job.context.setExtent( mSettings.visibleExtent() );
Code has comments. Press enter to view.
355+
356+
// if we can use the cache, let's do it and avoid rendering!
357+
bool canUseCache = mCache && mCache->hasCacheImage( LABEL_CACHE_ID );
358+
if ( canUseCache )
359+
{
360+
job.cached = true;
361+
job.complete = true;
362+
job.img = new QImage( mCache->cacheImage( LABEL_CACHE_ID ) );
363+
job.context.setPainter( nullptr );
364+
}
365+
else
366+
{
367+
if ( mCache || !painter )
368+
{
369+
// Flattened image for drawing labels
370+
QImage * mypFlattenedImage = nullptr;
371+
mypFlattenedImage = new QImage( mSettings.outputSize().width(),
372+
mSettings.outputSize().height(),
373+
mSettings.outputImageFormat() );
374+
if ( mypFlattenedImage->isNull() )
375+
{
376+
mErrors.append( Error( QStringLiteral( "labels" ), tr( "Insufficient memory for label image %1x%2" ).arg( mSettings.outputSize().width() ).arg( mSettings.outputSize().height() ) ) );
377+
delete mypFlattenedImage;
378+
}
379+
else
380+
{
381+
job.img = mypFlattenedImage;
382+
}
383+
}
384+
}
385+
386+
return job;
387+
}
388+
315389

316390
void QgsMapRendererJob::cleanupJobs( LayerRenderJobs& jobs )
317391
{
@@ -343,13 +417,29 @@ void QgsMapRendererJob::cleanupJobs( LayerRenderJobs& jobs )
343417
}
344418
}
345419

420+
346421
jobs.clear();
347422

348423
updateLayerGeometryCaches();
349424
}
350425

426+
void QgsMapRendererJob::cleanupLabelJob( LabelRenderJob& job )
427+
{
428+
if ( job.img )
429+
{
430+
if ( mCache && !job.cached && !job.context.renderingStopped() )
431+
{
432+
QgsDebugMsg( "caching label result image" );
433+
mCache->setCacheImage( LABEL_CACHE_ID, *job.img, _qgis_listQPointerToRaw( job.participatingLayers ) );
434+
}
435+
436+
delete job.img;
437+
job.img = nullptr;
438+
}
439+
}
440+
351441

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

461+
// IMPORTANT - don't draw labelJob img before the label job is complete,
462+
// as the image is uninitialized and full of garbage before the label job
463+
// commences
464+
if ( labelJob.img && labelJob.complete )
465+
{
466+
painter.setCompositionMode( QPainter::CompositionMode_SourceOver );
467+
painter.setOpacity( 1.0 );
468+
painter.drawImage( 0, 0, *labelJob.img );
469+
}
470+
371471
painter.end();
372472
return image;
373473
}
374474

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

485+
elapsed.insert( labelJob.renderingTime, tr( "Labeling" ) );
486+
385487
QList<int> tt( elapsed.uniqueKeys() );
386488
qSort( tt.begin(), tt.end(), qGreater<int>() );
387489
Q_FOREACH ( int t, tt )

‎src/core/qgsmaprendererjob.h

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,28 @@ struct LayerRenderJob
5656

5757
typedef QList<LayerRenderJob> LayerRenderJobs;
5858

59+
/** \ingroup core
60+
* Structure keeping low-level label rendering job information.
61+
*/
62+
struct LabelRenderJob
63+
{
64+
QgsRenderContext context;
65+
66+
/**
67+
* May be null if it is not necessary to draw to separate image (e.g. using composition modes which prevent "flattening" the layer).
68+
* Note that if complete is false then img will be uninitialized and contain random data!.
69+
*/
70+
QImage* img = nullptr;
71+
//! If true, img already contains cached image from previous rendering
72+
bool cached = false;
73+
//! If true then label render is complete
74+
bool complete = false;
75+
//! Time it took to render the labels in ms (it is -1 if not rendered or still rendering)
76+
int renderingTime = -1;
77+
//! List of layers which participated in the labeling solution
78+
QList< QPointer< QgsMapLayer > > participatingLayers;
79+
};
80+
5981
///@endcond PRIVATE
6082

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

178+
/**
179+
* QgsMapRendererCache ID string for cached label image.
180+
* @note not available in Python bindings
181+
*/
182+
static const QString LABEL_CACHE_ID;
183+
156184
signals:
157185

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

211+
/**
212+
* Prepares a labeling job.
213+
* @note not available in python bindings
214+
* @note added in QGIS 3.0
215+
*/
216+
LabelRenderJob prepareLabelingJob( QPainter* painter, QgsLabelingEngine* labelingEngine2 );
217+
183218
//! @note not available in python bindings
184-
static QImage composeImage( const QgsMapSettings& settings, const LayerRenderJobs& jobs );
219+
static QImage composeImage( const QgsMapSettings& settings, const LayerRenderJobs& jobs, const LabelRenderJob& labelJob );
185220

186221
//! @note not available in python bindings
187-
void logRenderingTime( const LayerRenderJobs& jobs );
222+
void logRenderingTime( const LayerRenderJobs& jobs, const LabelRenderJob& labelJob );
188223

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

227+
/**
228+
* Handles clean up tasks for a label job, including deletion of images and storing cached
229+
* label results.
230+
* @note added in QGIS 3.0
231+
* @note not available in python bindings
232+
*/
233+
void cleanupLabelJob( LabelRenderJob& job );
234+
192235
//! @note not available in Python bindings
193236
static void drawLabeling( const QgsMapSettings& settings, QgsRenderContext& renderContext, QgsLabelingEngine* labelingEngine2, QPainter* painter );
194237

‎src/core/qgsmaprendererparalleljob.cpp

Lines changed: 57 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include "qgspallabeling.h"
2323
#include "qgsproject.h"
2424
#include "qgsmaplayer.h"
25+
#include "qgsmaplayerlistutils.h"
2526

2627
#include <QtConcurrentMap>
2728

@@ -63,6 +64,7 @@ void QgsMapRendererParallelJob::start()
6364
}
6465

6566
mLayerJobs = prepareJobs( nullptr, mLabelingEngineV2 );
67+
mLabelJob = prepareLabelingJob( nullptr, mLabelingEngineV2 );
6668

6769
QgsDebugMsg( QString( "QThreadPool max thread count is %1" ).arg( QThreadPool::globalInstance()->maxThreadCount() ) );
6870

@@ -81,7 +83,7 @@ void QgsMapRendererParallelJob::cancel()
8183

8284
QgsDebugMsg( QString( "PARALLEL cancel at status %1" ).arg( mStatus ) );
8385

84-
mLabelingRenderContext.setRenderingStopped( true );
86+
mLabelJob.context.setRenderingStopped( true );
8587
for ( LayerRenderJobs::iterator it = mLayerJobs.begin(); it != mLayerJobs.end(); ++it )
8688
{
8789
it->context.setRenderingStopped( true );
@@ -162,7 +164,7 @@ QgsLabelingResults* QgsMapRendererParallelJob::takeLabelingResults()
162164
QImage QgsMapRendererParallelJob::renderedImage()
163165
{
164166
if ( mStatus == RenderingLayers )
165-
return composeImage( mSettings, mLayerJobs );
167+
return composeImage( mSettings, mLayerJobs, mLabelJob );
166168
else
167169
return mFinalImage; // when rendering labels or idle
168170
}
@@ -172,15 +174,11 @@ void QgsMapRendererParallelJob::renderLayersFinished()
172174
Q_ASSERT( mStatus == RenderingLayers );
173175

174176
// compose final image
175-
mFinalImage = composeImage( mSettings, mLayerJobs );
176-
177-
logRenderingTime( mLayerJobs );
178-
179-
cleanupJobs( mLayerJobs );
177+
mFinalImage = composeImage( mSettings, mLayerJobs, mLabelJob );
180178

181179
QgsDebugMsg( "PARALLEL layers finished" );
182180

183-
if ( mSettings.testFlag( QgsMapSettings::DrawLabeling ) && !mLabelingRenderContext.renderingStopped() )
181+
if ( mSettings.testFlag( QgsMapSettings::DrawLabeling ) && !mLabelJob.context.renderingStopped() )
184182
{
185183
mStatus = RenderingLabels;
186184

@@ -201,6 +199,12 @@ void QgsMapRendererParallelJob::renderingFinished()
201199
{
202200
QgsDebugMsg( "PARALLEL finished" );
203201

202+
logRenderingTime( mLayerJobs, mLabelJob );
203+
204+
cleanupJobs( mLayerJobs );
205+
206+
cleanupLabelJob( mLabelJob );
207+
204208
mStatus = Idle;
205209

206210
mRenderingTime = mRenderingStart.elapsed();
@@ -249,27 +253,53 @@ void QgsMapRendererParallelJob::renderLayerStatic( LayerRenderJob& job )
249253

250254
void QgsMapRendererParallelJob::renderLabelsStatic( QgsMapRendererParallelJob* self )
251255
{
252-
QPainter painter( &self->mFinalImage );
256+
LabelRenderJob& job = self->mLabelJob;
253257

254-
try
258+
if ( !job.cached )
255259
{
256-
drawLabeling( self->mSettings, self->mLabelingRenderContext, self->mLabelingEngineV2, &painter );
260+
QTime labelTime;
261+
labelTime.start();
262+
263+
QPainter painter;
264+
if ( job.img )
265+
{
266+
job.img->fill( 0 );
267+
painter.begin( job.img );
268+
}
269+
else
270+
{
271+
painter.begin( &self->mFinalImage );
272+
}
273+
274+
// draw the labels!
275+
try
276+
{
277+
drawLabeling( self->mSettings, job.context, self->mLabelingEngineV2, &painter );
278+
}
279+
catch ( QgsException & e )
280+
{
281+
Q_UNUSED( e );
282+
QgsDebugMsg( "Caught unhandled QgsException: " + e.what() );
283+
}
284+
catch ( std::exception & e )
285+
{
286+
Q_UNUSED( e );
287+
QgsDebugMsg( "Caught unhandled std::exception: " + QString::fromAscii( e.what() ) );
288+
}
289+
catch ( ... )
290+
{
291+
QgsDebugMsg( "Caught unhandled unknown exception" );
292+
}
293+
294+
painter.end();
295+
296+
job.renderingTime = labelTime.elapsed();
297+
job.complete = true;
298+
job.participatingLayers = _qgis_listRawToQPointer( self->mLabelingEngineV2->participatingLayers() );
299+
if ( job.img )
300+
{
301+
self->mFinalImage = composeImage( self->mSettings, self->mLayerJobs, self->mLabelJob );
302+
}
257303
}
258-
catch ( QgsException & e )
259-
{
260-
Q_UNUSED( e );
261-
QgsDebugMsg( "Caught unhandled QgsException: " + e.what() );
262-
}
263-
catch ( std::exception & e )
264-
{
265-
Q_UNUSED( e );
266-
QgsDebugMsg( "Caught unhandled std::exception: " + QString::fromAscii( e.what() ) );
267-
}
268-
catch ( ... )
269-
{
270-
QgsDebugMsg( "Caught unhandled unknown exception" );
271-
}
272-
273-
painter.end();
274304
}
275305

‎src/core/qgsmaprendererparalleljob.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ class CORE_EXPORT QgsMapRendererParallelJob : public QgsMapRendererQImageJob
5252

5353
private:
5454

55+
//! @note not available in Python bindings
5556
static void renderLayerStatic( LayerRenderJob& job );
57+
//! @note not available in Python bindings
5658
static void renderLabelsStatic( QgsMapRendererParallelJob* self );
5759

5860
QImage mFinalImage;
@@ -64,10 +66,10 @@ class CORE_EXPORT QgsMapRendererParallelJob : public QgsMapRendererQImageJob
6466
QFutureWatcher<void> mFutureWatcher;
6567

6668
LayerRenderJobs mLayerJobs;
69+
LabelRenderJob mLabelJob;
6770

6871
//! New labeling engine
6972
QgsLabelingEngine* mLabelingEngineV2;
70-
QgsRenderContext mLabelingRenderContext;
7173
QFuture<void> mLabelingFuture;
7274
QFutureWatcher<void> mLabelingFutureWatcher;
7375

‎src/core/qgsmaprenderersequentialjob.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ QImage QgsMapRendererSequentialJob::renderedImage()
110110
{
111111
if ( isActive() && mCache )
112112
// this will allow immediate display of cached layers and at the same time updates of the layer being rendered
113-
return composeImage( mSettings, mInternalJob->jobs() );
113+
return composeImage( mSettings, mInternalJob->jobs(), LabelRenderJob() );
114114
else
115115
return mImage;
116116
}

0 commit comments

Comments
 (0)
Please sign in to comment.