Skip to content

Commit

Permalink
[feature] use smarter map redraws: avoid map flickering by usage of c…
Browse files Browse the repository at this point in the history
…ached layer image before the new rendering is done
  • Loading branch information
PeterPetrik authored and wonder-sk committed Jan 14, 2021
1 parent 6b834f4 commit d883d41
Show file tree
Hide file tree
Showing 12 changed files with 438 additions and 24 deletions.
10 changes: 10 additions & 0 deletions python/core/auto_generated/qgsmaplayerrenderer.sip.in
Expand Up @@ -96,8 +96,18 @@ Returns the render context associated with the renderer.
%End


bool isReadyToCompose() const;
%Docstring
Returns whether the renderer already drawn (at
least partially) some data to resulting image

.. versionadded:: 3.18
%End

protected:



};

/************************************************************************
Expand Down
52 changes: 48 additions & 4 deletions python/core/auto_generated/qgsmaprenderercache.sip.in
Expand Up @@ -21,6 +21,10 @@ the cache listens to :py:func:`~repaintRequested` signals from dependent layers.
If triggered, the cache removes the rendered image (and disconnects from the
layers).

When user pans/zooms the canvas, the cache is also used in rendering period
for particular layer after first render update and moment layer actually
partially rendered something in the resulting image

The class is thread-safe (multiple classes can access the same instance safely).

.. versionadded:: 2.4
Expand All @@ -40,15 +44,28 @@ Invalidates the cache contents, clearing all cached images.
.. seealso:: :py:func:`clearCacheImage`
%End

bool init( const QgsRectangle &extent, double scale );
bool init( const QgsRectangle &extent, double scale ) /Deprecated/;
%Docstring
Initialize cache: set new parameters and clears the cache if any
Initialize cache: sets extent and scale parameters and clears the cache if any
parameters have changed since last initialization.

Deprecated, use :py:func:`~QgsMapRendererCache.updateParameters` and :py:func:`~QgsMapRendererCache.clear`

:return: flag whether the parameters are the same as last time
%End

void setCacheImage( const QString &cacheKey, const QImage &image, const QList< QgsMapLayer * > &dependentLayers = QList< QgsMapLayer * >() );
bool updateParameters( const QgsRectangle &extent, const QgsMapToPixel &mtp );
%Docstring
Sets extent and scale parameters

:return: flag whether the parameters are the same as last time

.. versionadded:: 3.18
%End

void setCacheImage( const QString &cacheKey,
const QImage &image,
const QList< QgsMapLayer * > &dependentLayers = QList< QgsMapLayer * >() );
%Docstring
Set the cached ``image`` for a particular ``cacheKey``. The ``cacheKey`` usually
matches the :py:func:`QgsMapLayer.id()` which the image is a render of.
Expand All @@ -61,11 +78,22 @@ repaint then the cache image will be cleared.

bool hasCacheImage( const QString &cacheKey ) const;
%Docstring
Returns ``True`` if the cache contains an image with the specified ``cacheKey``.
Returns ``True`` if the cache contains an image with the specified ``cacheKey``
that has the same extent and scale as the cache's global extent and scale

.. seealso:: :py:func:`cacheImage`

.. versionadded:: 3.0
%End

bool hasAnyCacheImage( const QString &cacheKey ) const;
%Docstring
Returns ``True`` if the cache contains an image with the specified ``cacheKey``
with any cache's parameters (extent and scale)

.. seealso:: :py:func:`transformedCacheImage`

.. versionadded:: 3.18
%End

QImage cacheImage( const QString &cacheKey ) const;
Expand All @@ -77,6 +105,22 @@ Returns a null image if it is not cached.
.. seealso:: :py:func:`setCacheImage`

.. seealso:: :py:func:`hasCacheImage`
%End

QImage transformedCacheImage( const QString &cacheKey, const QgsMapToPixel &mtp ) const;
%Docstring
Returns the cached image for the specified ``cacheKey`` transformed
to the particular extent and scale.

The ``cacheKey`` usually matches the :py:func:`QgsMapLayer.id()` which
the image is a render of.
Returns a null image if it is not cached.

.. seealso:: :py:func:`setCacheImage`

.. seealso:: :py:func:`hasCacheImageWithAnyParameters`

.. versionadded:: 3.18
%End

QList< QgsMapLayer * > dependentLayers( const QString &cacheKey ) const;
Expand Down
1 change: 1 addition & 0 deletions python/core/auto_generated/qgsmaprendererjob.sip.in
Expand Up @@ -190,6 +190,7 @@ emitted when asynchronous rendering is finished (or canceled).




};


Expand Down
26 changes: 26 additions & 0 deletions src/core/qgsmaplayerrenderer.h
Expand Up @@ -110,10 +110,36 @@ class CORE_EXPORT QgsMapLayerRenderer
*/
const QgsRenderContext *renderContext() const SIP_SKIP { return mContext; }

/**
* Returns whether the renderer already drawn (at
* least partially) some data to resulting image
*
* \since QGIS 3.18
*/
bool isReadyToCompose() const { return mReadyToCompose; }

protected:
QStringList mErrors;
QString mLayerID;

// TODO QGIS 4.0 - make false as default

/**
* The flag must be set to false in renderer's constructor
* if wants to use the smarter map redraws functionality
* https://github.com/qgis/QGIS-Enhancement-Proposals/issues/181
*
* The flag must be set to true by renderer when
* the data is fetched and the renderer actually
* started to update the destination image.
*
* When the flag is set to false, the image from
* QgsMapRendererCache is used instead to avoid flickering.
*
* \since QGIS 3.18
*/
bool mReadyToCompose = true;

private:

// TODO QGIS 4.0 - make reference instead of pointer!
Expand Down
88 changes: 88 additions & 0 deletions src/core/qgsmaprenderercache.cpp
Expand Up @@ -17,6 +17,11 @@

#include "qgsmaplayer.h"
#include "qgsmaplayerlistutils.h"
#include "qgsapplication.h"

#include <QImage>
#include <QPainter>
#include <algorithm>

QgsMapRendererCache::QgsMapRendererCache()
{
Expand Down Expand Up @@ -95,6 +100,25 @@ bool QgsMapRendererCache::init( const QgsRectangle &extent, double scale )
// set new params
mExtent = extent;
mScale = scale;
mMtp = QgsMapToPixel::fromScale( scale, QgsUnitTypes::DistanceUnit::DistanceUnknownUnit );

return false;
}

bool QgsMapRendererCache::updateParameters( const QgsRectangle &extent, const QgsMapToPixel &mtp )
{
QMutexLocker lock( &mMutex );

// check whether the params are the same
if ( extent == mExtent &&
mtp.transform() == mMtp.transform() )
return true;

// set new params

mExtent = extent;
mScale = 1.0;
mMtp = mtp;

return false;
}
Expand All @@ -105,6 +129,8 @@ void QgsMapRendererCache::setCacheImage( const QString &cacheKey, const QImage &

CacheParameters params;
params.cachedImage = image;
params.cachedExtent = mExtent;
params.cachedMtp = mMtp;

// connect to the layer to listen to layer's repaintRequested() signals
const auto constDependentLayers = dependentLayers;
Expand All @@ -126,6 +152,21 @@ void QgsMapRendererCache::setCacheImage( const QString &cacheKey, const QImage &
}

bool QgsMapRendererCache::hasCacheImage( const QString &cacheKey ) const
{
QMutexLocker lock( &mMutex );
if ( mCachedImages.contains( cacheKey ) )
{
const CacheParameters params = mCachedImages[cacheKey];
return ( params.cachedExtent == mExtent &&
params.cachedMtp.transform() == mMtp.transform() );
}
else
{
return false;
}
}

bool QgsMapRendererCache::hasAnyCacheImage( const QString &cacheKey ) const
{
return mCachedImages.contains( cacheKey );
}
Expand All @@ -136,6 +177,53 @@ QImage QgsMapRendererCache::cacheImage( const QString &cacheKey ) const
return mCachedImages.value( cacheKey ).cachedImage;
}

static QPointF _transform( const QgsMapToPixel &mtp, const QgsPointXY &point, double scale )
{
qreal x = point.x(), y = point.y();
mtp.transformInPlace( x, y );
return QPointF( x, y ) * scale;
}

QImage QgsMapRendererCache::transformedCacheImage( const QString &cacheKey, const QgsMapToPixel &mtp ) const
{
QMutexLocker lock( &mMutex );
const CacheParameters params = mCachedImages.value( cacheKey );

if ( params.cachedExtent == mExtent &&
mtp.transform() == mMtp.transform() )
{
return params.cachedImage;
}
else
{
QgsRectangle intersection = mExtent.intersect( params.cachedExtent );
if ( intersection.isNull() )
return QImage();

// Calculate target rect
const QPointF ulT = _transform( mtp, QgsPointXY( intersection.xMinimum(), intersection.yMaximum() ), 1.0 );
const QPointF lrT = _transform( mtp, QgsPointXY( intersection.xMaximum(), intersection.yMinimum() ), 1.0 );
const QRectF targetRect( ulT.x(), ulT.y(), lrT.x() - ulT.x(), lrT.y() - ulT.y() );

// Calculate source rect
const QPointF ulS = _transform( params.cachedMtp, QgsPointXY( intersection.xMinimum(), intersection.yMaximum() ), params.cachedImage.devicePixelRatio() );
const QPointF lrS = _transform( params.cachedMtp, QgsPointXY( intersection.xMaximum(), intersection.yMinimum() ), params.cachedImage.devicePixelRatio() );
const QRectF sourceRect( ulS.x(), ulS.y(), lrS.x() - ulS.x(), lrS.y() - ulS.y() );

// Draw image
QImage ret( params.cachedImage.size(), params.cachedImage.format() );
ret.setDevicePixelRatio( params.cachedImage.devicePixelRatio() );
ret.setDotsPerMeterX( params.cachedImage.dotsPerMeterX() );
ret.setDotsPerMeterY( params.cachedImage.dotsPerMeterY() );
ret.fill( Qt::transparent );
QPainter painter;
painter.begin( &ret );
painter.drawImage( targetRect, params.cachedImage, sourceRect );
painter.end();
return ret;
}
}

QList< QgsMapLayer * > QgsMapRendererCache::dependentLayers( const QString &cacheKey ) const
{
if ( mCachedImages.contains( cacheKey ) )
Expand Down
58 changes: 53 additions & 5 deletions src/core/qgsmaprenderercache.h
Expand Up @@ -35,6 +35,10 @@
* If triggered, the cache removes the rendered image (and disconnects from the
* layers).
*
* When user pans/zooms the canvas, the cache is also used in rendering period
* for particular layer after first render update and moment layer actually
* partially rendered something in the resulting image
*
* The class is thread-safe (multiple classes can access the same instance safely).
*
* \since QGIS 2.4
Expand All @@ -53,11 +57,23 @@ class CORE_EXPORT QgsMapRendererCache : public QObject
void clear();

/**
* Initialize cache: set new parameters and clears the cache if any
* Initialize cache: sets extent and scale parameters and clears the cache if any
* parameters have changed since last initialization.
*
* Deprecated, use updateParameters() and clear()
*
* \returns flag whether the parameters are the same as last time
*/
bool Q_DECL_DEPRECATED init( const QgsRectangle &extent, double scale ) SIP_DEPRECATED;

/**
* Sets extent and scale parameters
*
* \returns flag whether the parameters are the same as last time
*
* \since QGIS 3.18
*/
bool init( const QgsRectangle &extent, double scale );
bool updateParameters( const QgsRectangle &extent, const QgsMapToPixel &mtp );

/**
* Set the cached \a image for a particular \a cacheKey. The \a cacheKey usually
Expand All @@ -67,15 +83,29 @@ class CORE_EXPORT QgsMapRendererCache : public QObject
* repaint then the cache image will be cleared.
* \see cacheImage()
*/
void setCacheImage( const QString &cacheKey, const QImage &image, const QList< QgsMapLayer * > &dependentLayers = QList< QgsMapLayer * >() );
void setCacheImage( const QString &cacheKey,
const QImage &image,
const QList< QgsMapLayer * > &dependentLayers = QList< QgsMapLayer * >() );

/**
* Returns TRUE if the cache contains an image with the specified \a cacheKey.
* Returns TRUE if the cache contains an image with the specified \a cacheKey
* that has the same extent and scale as the cache's global extent and scale
*
* \see cacheImage()
* \since QGIS 3.0
*/
bool hasCacheImage( const QString &cacheKey ) const;

/**
* Returns TRUE if the cache contains an image with the specified \a cacheKey
* with any cache's parameters (extent and scale)
*
* \see transformedCacheImage()
*
* \since QGIS 3.18
*/
bool hasAnyCacheImage( const QString &cacheKey ) const;

/**
* Returns the cached image for the specified \a cacheKey. The \a cacheKey usually
* matches the QgsMapLayer::id() which the image is a render of.
Expand All @@ -85,6 +115,20 @@ class CORE_EXPORT QgsMapRendererCache : public QObject
*/
QImage cacheImage( const QString &cacheKey ) const;

/**
* Returns the cached image for the specified \a cacheKey transformed
* to the particular extent and scale.
*
* The \a cacheKey usually matches the QgsMapLayer::id() which
* the image is a render of.
* Returns a null image if it is not cached.
* \see setCacheImage()
* \see hasCacheImageWithAnyParameters()
*
* \since QGIS 3.18
*/
QImage transformedCacheImage( const QString &cacheKey, const QgsMapToPixel &mtp ) const;

/**
* Returns a list of map layers on which an image in the cache depends.
* \since QGIS 3.0
Expand Down Expand Up @@ -114,6 +158,8 @@ class CORE_EXPORT QgsMapRendererCache : public QObject
{
QImage cachedImage;
QgsWeakMapLayerPointerList dependentLayers;
QgsRectangle cachedExtent;
QgsMapToPixel cachedMtp;
};

//! Invalidate cache contents (without locking)
Expand All @@ -126,7 +172,9 @@ class CORE_EXPORT QgsMapRendererCache : public QObject

mutable QMutex mMutex;
QgsRectangle mExtent;
double mScale = 0;
QgsMapToPixel mMtp;

double mScale = -1.0; //DEPRECATED

//! Map of cache key to cache parameters
QMap<QString, CacheParameters> mCachedImages;
Expand Down

0 comments on commit d883d41

Please sign in to comment.