Skip to content

Commit 54e208b

Browse files
authoredMay 6, 2017
Merge pull request #4494 from nyalldawson/composer_async
Asyncronously render composer map previews
2 parents 7efcfee + 2b48026 commit 54e208b

File tree

3 files changed

+109
-31
lines changed

3 files changed

+109
-31
lines changed
 

‎src/core/composer/qgscomposermap.cpp

Lines changed: 87 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,16 @@ QgsComposerMap::~QgsComposerMap()
115115
{
116116
delete mOverviewStack;
117117
delete mGridStack;
118+
119+
if ( mPainterJob )
120+
{
121+
disconnect( mPainterJob.get(), &QgsMapRendererCustomPainterJob::finished, this, &QgsComposerMap::painterJobFinished );
122+
mPainterJob->cancel();
123+
mPainter->end();
124+
}
118125
}
119126

120-
/* This function is called by paint() and cache() to render the map. It does not override any functions
127+
/* This function is called by paint() to render the map. It does not override any functions
121128
from QGraphicsItem. */
122129
void QgsComposerMap::draw( QPainter *painter, const QgsRectangle &extent, QSizeF size, double dpi, double *forceWidthScale )
123130
{
@@ -206,12 +213,28 @@ void QgsComposerMap::cache()
206213
return;
207214
}
208215

209-
if ( mDrawing )
216+
if ( mPainterJob )
210217
{
211-
return;
218+
disconnect( mPainterJob.get(), &QgsMapRendererCustomPainterJob::finished, this, &QgsComposerMap::painterJobFinished );
219+
QgsMapRendererCustomPainterJob *oldJob = mPainterJob.release();
220+
QPainter *oldPainter = mPainter.release();
221+
QImage *oldImage = mCacheRenderingImage.release();
222+
connect( oldJob, &QgsMapRendererCustomPainterJob::finished, this, [oldPainter, oldJob, oldImage]
223+
{
224+
oldJob->deleteLater();
225+
delete oldPainter;
226+
delete oldImage;
227+
} );
228+
oldJob->cancelWithoutBlocking();
229+
}
230+
else
231+
{
232+
mCacheRenderingImage.reset( nullptr );
212233
}
213234

214-
mDrawing = true;
235+
Q_ASSERT( !mPainterJob );
236+
Q_ASSERT( !mPainter );
237+
Q_ASSERT( !mCacheRenderingImage );
215238

216239
double horizontalVScaleFactor = horizontalViewScaleFactor();
217240
if ( horizontalVScaleFactor < 0 )
@@ -242,38 +265,51 @@ void QgsComposerMap::cache()
242265
}
243266
}
244267

245-
mCacheImage = QImage( w, h, QImage::Format_ARGB32 );
268+
if ( w <= 0 || h <= 0 )
269+
return;
270+
271+
mCacheRenderingImage.reset( new QImage( w, h, QImage::Format_ARGB32 ) );
246272

247273
// set DPI of the image
248-
mCacheImage.setDotsPerMeterX( 1000 * w / widthMM );
249-
mCacheImage.setDotsPerMeterY( 1000 * h / heightMM );
274+
mCacheRenderingImage->setDotsPerMeterX( 1000 * w / widthMM );
275+
mCacheRenderingImage->setDotsPerMeterY( 1000 * h / heightMM );
250276

251277
if ( hasBackground() )
252278
{
253279
//Initially fill image with specified background color. This ensures that layers with blend modes will
254280
//preview correctly
255-
mCacheImage.fill( backgroundColor().rgba() );
281+
mCacheRenderingImage->fill( backgroundColor().rgba() );
256282
}
257283
else
258284
{
259285
//no background, but start with empty fill to avoid artifacts
260-
mCacheImage.fill( QColor( 255, 255, 255, 0 ).rgba() );
286+
mCacheRenderingImage->fill( QColor( 255, 255, 255, 0 ).rgba() );
261287
}
262288

263-
QPainter p( &mCacheImage );
289+
mPainter.reset( new QPainter( mCacheRenderingImage.get() ) );
290+
QgsMapSettings settings( mapSettings( ext, QSizeF( w, h ), mCacheRenderingImage->logicalDpiX() ) );
291+
mPainterJob.reset( new QgsMapRendererCustomPainterJob( settings, mPainter.get() ) );
292+
connect( mPainterJob.get(), &QgsMapRendererCustomPainterJob::finished, this, &QgsComposerMap::painterJobFinished );
293+
mPainterJob->start();
294+
}
264295

265-
draw( &p, ext, QSizeF( w, h ), mCacheImage.logicalDpiX() );
266-
p.end();
296+
void QgsComposerMap::painterJobFinished()
297+
{
298+
mPainter->end();
299+
mPainterJob.reset( nullptr );
300+
mPainter.reset( nullptr );
267301
mCacheUpdated = true;
268-
269-
mDrawing = false;
302+
mCacheFinalImage = std::move( mCacheRenderingImage );
303+
mLastRenderedImageOffsetX = 0;
304+
mLastRenderedImageOffsetY = 0;
305+
updateItem();
270306
}
271307

272308
void QgsComposerMap::paint( QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *pWidget )
273309
{
274310
Q_UNUSED( pWidget );
275311

276-
if ( !mComposition || !painter )
312+
if ( !mComposition || !painter || !painter->device() )
277313
{
278314
return;
279315
}
@@ -283,6 +319,9 @@ void QgsComposerMap::paint( QPainter *painter, const QStyleOptionGraphicsItem *,
283319
}
284320

285321
QRectF thisPaintRect = QRectF( 0, 0, QGraphicsRectItem::rect().width(), QGraphicsRectItem::rect().height() );
322+
if ( thisPaintRect.width() == 0 || thisPaintRect.height() == 0 )
323+
return;
324+
286325
painter->save();
287326
painter->setClipRect( thisPaintRect );
288327

@@ -297,22 +336,40 @@ void QgsComposerMap::paint( QPainter *painter, const QStyleOptionGraphicsItem *,
297336
}
298337
else if ( mComposition->plotStyle() == QgsComposition::Preview )
299338
{
300-
if ( mCacheImage.isNull() )
301-
cache();
302-
303-
//Background color is already included in cached image, so no need to draw
339+
if ( !mCacheFinalImage || mCacheFinalImage->isNull() )
340+
{
341+
// No initial render available - so draw some preview text alerting user
342+
drawBackground( painter );
343+
painter->setBrush( QBrush( QColor( 125, 125, 125, 125 ) ) );
344+
painter->drawRect( thisPaintRect );
345+
painter->setBrush( Qt::NoBrush );
346+
QFont messageFont;
347+
messageFont.setPointSize( 12 );
348+
painter->setFont( messageFont );
349+
painter->setPen( QColor( 255, 255, 255, 255 ) );
350+
painter->drawText( thisPaintRect, Qt::AlignCenter | Qt::AlignHCenter, tr( "Rendering map" ) );
351+
if ( !mPainterJob )
352+
{
353+
// this is the map's very first paint - trigger a cache update
354+
cache();
355+
}
356+
}
357+
else
358+
{
359+
//Background color is already included in cached image, so no need to draw
304360

305-
double imagePixelWidth = mCacheImage.width(); //how many pixels of the image are for the map extent?
306-
double scale = rect().width() / imagePixelWidth;
361+
double imagePixelWidth = mCacheFinalImage->width(); //how many pixels of the image are for the map extent?
362+
double scale = rect().width() / imagePixelWidth;
307363

308-
painter->save();
364+
painter->save();
309365

310-
painter->translate( mXOffset, mYOffset );
311-
painter->scale( scale, scale );
312-
painter->drawImage( 0, 0, mCacheImage );
366+
painter->translate( mLastRenderedImageOffsetX + mXOffset, mLastRenderedImageOffsetY + mYOffset );
367+
painter->scale( scale, scale );
368+
painter->drawImage( 0, 0, *mCacheFinalImage );
313369

314-
//restore rotation
315-
painter->restore();
370+
//restore rotation
371+
painter->restore();
372+
}
316373
}
317374
else if ( mComposition->plotStyle() == QgsComposition::Print ||
318375
mComposition->plotStyle() == QgsComposition::Postscript )
@@ -565,6 +622,8 @@ void QgsComposerMap::resize( double dx, double dy )
565622

566623
void QgsComposerMap::moveContent( double dx, double dy )
567624
{
625+
mLastRenderedImageOffsetX -= dx;
626+
mLastRenderedImageOffsetY -= dy;
568627
if ( !mDrawing )
569628
{
570629
transformShift( dx, dy );
@@ -673,7 +732,7 @@ void QgsComposerMap::setSceneRect( const QRectF &rectangle )
673732
mCacheUpdated = false;
674733

675734
updateBoundingRect();
676-
update();
735+
updateItem();
677736
emit itemChanged();
678737
emit extentChanged();
679738
}

‎src/core/composer/qgscomposermap.h

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class QgsFillSymbol;
4343
class QgsLineSymbol;
4444
class QgsVectorLayer;
4545
class QgsAnnotation;
46+
class QgsMapRendererCustomPainterJob;
4647

4748
/** \ingroup core
4849
* \class QgsComposerMap
@@ -491,6 +492,8 @@ class CORE_EXPORT QgsComposerMap : public QgsComposerItem
491492
private slots:
492493
void layersAboutToBeRemoved( QList<QgsMapLayer *> layers );
493494

495+
void painterJobFinished();
496+
494497
private:
495498

496499
//! Unique identifier
@@ -513,8 +516,15 @@ class CORE_EXPORT QgsComposerMap : public QgsComposerItem
513516
// to manually tweak each atlas preview page without affecting the actual original map extent.
514517
QgsRectangle mAtlasFeatureExtent;
515518

516-
// Cache used in composer preview
517-
QImage mCacheImage;
519+
// We have two images used for rendering/storing cached map images.
520+
// the first (mCacheFinalImage) is used ONLY for storing the most recent completed map render. It's always
521+
// used when drawing map item previews. The second (mCacheRenderingImage) is used temporarily while
522+
// rendering a new preview image in the background. If (and only if) the background render completes, then
523+
// mCacheRenderingImage is pushed into mCacheFinalImage, and used from then on when drawing the item preview.
524+
// This ensures that something is always shown in the map item, even while refreshing the preview image in the
525+
// background
526+
std::unique_ptr< QImage > mCacheFinalImage;
527+
std::unique_ptr< QImage > mCacheRenderingImage;
518528

519529
// Is cache up to date
520530
bool mCacheUpdated = false;
@@ -533,6 +543,9 @@ class CORE_EXPORT QgsComposerMap : public QgsComposerItem
533543
//! Offset in y direction for showing map cache image
534544
double mYOffset = 0.0;
535545

546+
double mLastRenderedImageOffsetX = 0.0;
547+
double mLastRenderedImageOffsetY = 0.0;
548+
536549
//! Map rotation
537550
double mMapRotation = 0;
538551

@@ -587,6 +600,10 @@ class CORE_EXPORT QgsComposerMap : public QgsComposerItem
587600
//! Margin size for atlas driven extents (percentage of feature size) - when in auto scaling mode
588601
double mAtlasMargin = 0.10;
589602

603+
std::unique_ptr< QPainter > mPainter;
604+
std::unique_ptr< QgsMapRendererCustomPainterJob > mPainterJob;
605+
bool mPainterCancelWait = false;
606+
590607
void init();
591608

592609
//! Resets the item tooltip to reflect current map id

‎tests/src/python/test_qgspallabeling_composer.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import os
2424
import subprocess
2525

26-
from qgis.PyQt.QtCore import QRect, QRectF, QSize, QSizeF, qDebug
26+
from qgis.PyQt.QtCore import QRect, QRectF, QSize, QSizeF, qDebug, QThreadPool
2727
from qgis.PyQt.QtGui import QImage, QColor, QPainter
2828
from qgis.PyQt.QtPrintSupport import QPrinter
2929
from qgis.PyQt.QtSvg import QSvgRenderer, QSvgGenerator
@@ -88,6 +88,8 @@ def tearDownClass(cls):
8888
TestQgsPalLabeling.tearDownClass()
8989
cls.removeMapLayer(cls.layer)
9090
cls.layer = None
91+
# avoid crash on finish, probably related to https://bugreports.qt.io/browse/QTBUG-35760
92+
QThreadPool.globalInstance().waitForDone()
9193

9294
def setUp(self):
9395
"""Run before each test."""

0 commit comments

Comments
 (0)
Please sign in to comment.