Skip to content

Commit

Permalink
Merge pull request #4494 from nyalldawson/composer_async
Browse files Browse the repository at this point in the history
Asyncronously render composer map previews
  • Loading branch information
nyalldawson authored May 6, 2017
2 parents 7efcfee + 2b48026 commit 54e208b
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 31 deletions.
115 changes: 87 additions & 28 deletions src/core/composer/qgscomposermap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,16 @@ QgsComposerMap::~QgsComposerMap()
{
delete mOverviewStack;
delete mGridStack;

if ( mPainterJob )
{
disconnect( mPainterJob.get(), &QgsMapRendererCustomPainterJob::finished, this, &QgsComposerMap::painterJobFinished );
mPainterJob->cancel();
mPainter->end();
}
}

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

if ( mDrawing )
if ( mPainterJob )
{
return;
disconnect( mPainterJob.get(), &QgsMapRendererCustomPainterJob::finished, this, &QgsComposerMap::painterJobFinished );
QgsMapRendererCustomPainterJob *oldJob = mPainterJob.release();
QPainter *oldPainter = mPainter.release();
QImage *oldImage = mCacheRenderingImage.release();
connect( oldJob, &QgsMapRendererCustomPainterJob::finished, this, [oldPainter, oldJob, oldImage]
{
oldJob->deleteLater();
delete oldPainter;
delete oldImage;
} );
oldJob->cancelWithoutBlocking();
}
else
{
mCacheRenderingImage.reset( nullptr );
}

mDrawing = true;
Q_ASSERT( !mPainterJob );
Q_ASSERT( !mPainter );
Q_ASSERT( !mCacheRenderingImage );

double horizontalVScaleFactor = horizontalViewScaleFactor();
if ( horizontalVScaleFactor < 0 )
Expand Down Expand Up @@ -242,38 +265,51 @@ void QgsComposerMap::cache()
}
}

mCacheImage = QImage( w, h, QImage::Format_ARGB32 );
if ( w <= 0 || h <= 0 )
return;

mCacheRenderingImage.reset( new QImage( w, h, QImage::Format_ARGB32 ) );

// set DPI of the image
mCacheImage.setDotsPerMeterX( 1000 * w / widthMM );
mCacheImage.setDotsPerMeterY( 1000 * h / heightMM );
mCacheRenderingImage->setDotsPerMeterX( 1000 * w / widthMM );
mCacheRenderingImage->setDotsPerMeterY( 1000 * h / heightMM );

if ( hasBackground() )
{
//Initially fill image with specified background color. This ensures that layers with blend modes will
//preview correctly
mCacheImage.fill( backgroundColor().rgba() );
mCacheRenderingImage->fill( backgroundColor().rgba() );
}
else
{
//no background, but start with empty fill to avoid artifacts
mCacheImage.fill( QColor( 255, 255, 255, 0 ).rgba() );
mCacheRenderingImage->fill( QColor( 255, 255, 255, 0 ).rgba() );
}

QPainter p( &mCacheImage );
mPainter.reset( new QPainter( mCacheRenderingImage.get() ) );
QgsMapSettings settings( mapSettings( ext, QSizeF( w, h ), mCacheRenderingImage->logicalDpiX() ) );
mPainterJob.reset( new QgsMapRendererCustomPainterJob( settings, mPainter.get() ) );
connect( mPainterJob.get(), &QgsMapRendererCustomPainterJob::finished, this, &QgsComposerMap::painterJobFinished );
mPainterJob->start();
}

draw( &p, ext, QSizeF( w, h ), mCacheImage.logicalDpiX() );
p.end();
void QgsComposerMap::painterJobFinished()
{
mPainter->end();
mPainterJob.reset( nullptr );
mPainter.reset( nullptr );
mCacheUpdated = true;

mDrawing = false;
mCacheFinalImage = std::move( mCacheRenderingImage );
mLastRenderedImageOffsetX = 0;
mLastRenderedImageOffsetY = 0;
updateItem();
}

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

if ( !mComposition || !painter )
if ( !mComposition || !painter || !painter->device() )
{
return;
}
Expand All @@ -283,6 +319,9 @@ void QgsComposerMap::paint( QPainter *painter, const QStyleOptionGraphicsItem *,
}

QRectF thisPaintRect = QRectF( 0, 0, QGraphicsRectItem::rect().width(), QGraphicsRectItem::rect().height() );
if ( thisPaintRect.width() == 0 || thisPaintRect.height() == 0 )
return;

painter->save();
painter->setClipRect( thisPaintRect );

Expand All @@ -297,22 +336,40 @@ void QgsComposerMap::paint( QPainter *painter, const QStyleOptionGraphicsItem *,
}
else if ( mComposition->plotStyle() == QgsComposition::Preview )
{
if ( mCacheImage.isNull() )
cache();

//Background color is already included in cached image, so no need to draw
if ( !mCacheFinalImage || mCacheFinalImage->isNull() )
{
// No initial render available - so draw some preview text alerting user
drawBackground( painter );
painter->setBrush( QBrush( QColor( 125, 125, 125, 125 ) ) );
painter->drawRect( thisPaintRect );
painter->setBrush( Qt::NoBrush );
QFont messageFont;
messageFont.setPointSize( 12 );
painter->setFont( messageFont );
painter->setPen( QColor( 255, 255, 255, 255 ) );
painter->drawText( thisPaintRect, Qt::AlignCenter | Qt::AlignHCenter, tr( "Rendering map" ) );
if ( !mPainterJob )
{
// this is the map's very first paint - trigger a cache update
cache();
}
}
else
{
//Background color is already included in cached image, so no need to draw

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

painter->save();
painter->save();

painter->translate( mXOffset, mYOffset );
painter->scale( scale, scale );
painter->drawImage( 0, 0, mCacheImage );
painter->translate( mLastRenderedImageOffsetX + mXOffset, mLastRenderedImageOffsetY + mYOffset );
painter->scale( scale, scale );
painter->drawImage( 0, 0, *mCacheFinalImage );

//restore rotation
painter->restore();
//restore rotation
painter->restore();
}
}
else if ( mComposition->plotStyle() == QgsComposition::Print ||
mComposition->plotStyle() == QgsComposition::Postscript )
Expand Down Expand Up @@ -565,6 +622,8 @@ void QgsComposerMap::resize( double dx, double dy )

void QgsComposerMap::moveContent( double dx, double dy )
{
mLastRenderedImageOffsetX -= dx;
mLastRenderedImageOffsetY -= dy;
if ( !mDrawing )
{
transformShift( dx, dy );
Expand Down Expand Up @@ -673,7 +732,7 @@ void QgsComposerMap::setSceneRect( const QRectF &rectangle )
mCacheUpdated = false;

updateBoundingRect();
update();
updateItem();
emit itemChanged();
emit extentChanged();
}
Expand Down
21 changes: 19 additions & 2 deletions src/core/composer/qgscomposermap.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class QgsFillSymbol;
class QgsLineSymbol;
class QgsVectorLayer;
class QgsAnnotation;
class QgsMapRendererCustomPainterJob;

/** \ingroup core
* \class QgsComposerMap
Expand Down Expand Up @@ -491,6 +492,8 @@ class CORE_EXPORT QgsComposerMap : public QgsComposerItem
private slots:
void layersAboutToBeRemoved( QList<QgsMapLayer *> layers );

void painterJobFinished();

private:

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

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

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

double mLastRenderedImageOffsetX = 0.0;
double mLastRenderedImageOffsetY = 0.0;

//! Map rotation
double mMapRotation = 0;

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

std::unique_ptr< QPainter > mPainter;
std::unique_ptr< QgsMapRendererCustomPainterJob > mPainterJob;
bool mPainterCancelWait = false;

void init();

//! Resets the item tooltip to reflect current map id
Expand Down
4 changes: 3 additions & 1 deletion tests/src/python/test_qgspallabeling_composer.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import os
import subprocess

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

def setUp(self):
"""Run before each test."""
Expand Down

0 comments on commit 54e208b

Please sign in to comment.