[api] Add framework for collecting rendered item details during map

renderer operations

This follow a similar pattern as how labeling results could be collected
after a map render job, but generalises the API so that it can be
used for storing details of rendered items of any type.

It's currently used for storing details of rendered annotation items,
so that map tools can retrieve details of annotation items visible
in the canvas in an optimised way.
This commit is contained in:
Nyall Dawson 2021-08-30 10:47:45 +10:00
parent b09994abcc
commit 660433d9a9
25 changed files with 912 additions and 16 deletions

View File

@ -0,0 +1,57 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/core/annotations/qgsrenderedannotationitemdetails.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/
class QgsRenderedAnnotationItemDetails : QgsRenderedItemDetails
{
%Docstring(signature="appended")
Contains information about a rendered annotation item.
.. versionadded:: 3.22
%End
%TypeHeaderCode
#include "qgsrenderedannotationitemdetails.h"
%End
public:
QgsRenderedAnnotationItemDetails( const QString &layerId, const QString &itemId );
%Docstring
Constructor for QgsRenderedAnnotationItemDetails.
%End
SIP_PYOBJECT __repr__();
%MethodCode
QString str = QStringLiteral( "<QgsRenderedAnnotationItemDetails: %1 - %2>" ).arg( sipCpp->layerId(), sipCpp->itemId() );
sipRes = PyUnicode_FromString( str.toUtf8().constData() );
%End
virtual QgsRenderedAnnotationItemDetails *clone() const /Factory/;
QString layerId() const;
%Docstring
Returns the layer ID of the associated map layer.
%End
QString itemId() const;
%Docstring
Returns the item ID of the associated annotation item.
%End
};
/************************************************************************
* This file has been generated automatically from *
* *
* src/core/annotations/qgsrenderedannotationitemdetails.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/

View File

@ -49,6 +49,8 @@ The following subclasses are available:
QgsMapRendererJob( const QgsMapSettings &settings );
~QgsMapRendererJob();
void start();
%Docstring
Start the rendering job and immediately return.
@ -95,6 +97,15 @@ Gets pointer to internal labeling engine (in order to get access to the results)
This should not be used if cached labeling was redrawn - see :py:func:`~QgsMapRendererJob.usedCachedLabels`.
.. seealso:: :py:func:`usedCachedLabels`
%End
QgsRenderedItemResults *takeRenderedItemResults() /Transfer/;
%Docstring
Takes the rendered item results from the map render job and returns them.
Ownership is transferred to the caller.
.. versionadded:: 3.22
%End
void setFeatureFilterProvider( const QgsFeatureFilterProvider *f );
@ -194,6 +205,7 @@ emitted when asynchronous rendering is finished (or canceled).
};

View File

@ -0,0 +1,53 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/core/maprenderer/qgsrendereditemresults.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/
class QgsRenderedItemResults
{
%Docstring(signature="appended")
Stores collated details of rendered items during a map rendering operation.
.. versionadded:: 3.22
%End
%TypeHeaderCode
#include "qgsrendereditemresults.h"
%End
public:
QgsRenderedItemResults();
~QgsRenderedItemResults();
QList< QgsRenderedItemDetails * > renderedItems() const;
%Docstring
Returns a list of all rendered items.
%End
QList<const QgsRenderedAnnotationItemDetails *> renderedAnnotationItemsInBounds( const QgsRectangle &bounds ) const;
%Docstring
Returns a list with details of the rendered annotation items within the specified ``bounds``.
.. versionadded:: 3.22
%End
private:
QgsRenderedItemResults( const QgsRenderedItemResults & );
};
/************************************************************************
* This file has been generated automatically from *
* *
* src/core/maprenderer/qgsrendereditemresults.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/

View File

@ -110,11 +110,35 @@ least partially) some data
%End
QList< QgsRenderedItemDetails * > takeRenderedItemDetails() /TransferBack/;
%Docstring
Takes the list of rendered item details from the renderer.
Ownership of items is transferred to the caller.
.. seealso:: :py:func:`appendRenderedItemDetails`
.. versionadded:: 3.22
%End
protected:
void appendRenderedItemDetails( QgsRenderedItemDetails *details /Transfer/ );
%Docstring
Appends the ``details`` of a rendered item to the renderer.
Rendered item details can be retrieved by calling :py:func:`~QgsMapLayerRenderer.takeRenderedItemDetails`.
Ownership of ``details`` is transferred to the renderer.
.. seealso:: :py:func:`takeRenderedItemDetails`
.. versionadded:: 3.22
%End
};
/************************************************************************

View File

@ -0,0 +1,61 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/core/qgsrendereditemdetails.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/
class QgsRenderedItemDetails
{
%Docstring(signature="appended")
Base class for detailed information about a rendered item.
.. versionadded:: 3.22
%End
%TypeHeaderCode
#include "qgsrendereditemdetails.h"
%End
public:
%ConvertToSubClassCode
if ( dynamic_cast<QgsRenderedAnnotationItemDetails *>( sipCpp ) )
sipType = sipType_QgsRenderedAnnotationItemDetails;
else
sipType = 0;
%End
virtual ~QgsRenderedItemDetails();
virtual QgsRenderedItemDetails *clone() const = 0 /Factory/;
%Docstring
Clones the details.
%End
QgsRectangle boundingBox() const;
%Docstring
Returns the bounding box of the item (in map units).
.. seealso:: :py:func:`setBoundingBox`
%End
void setBoundingBox( const QgsRectangle &bounds );
%Docstring
Sets the bounding box of the item (in map units).
.. seealso:: :py:func:`boundingBox`
%End
};
/************************************************************************
* This file has been generated automatically from *
* *
* src/core/qgsrendereditemdetails.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/

View File

@ -152,6 +152,7 @@
%Include auto_generated/qgsrenderchecker.sip
%Include auto_generated/qgsrendercontext.sip
%Include auto_generated/qgsrenderedfeaturehandlerinterface.sip
%Include auto_generated/qgsrendereditemdetails.sip
%Include auto_generated/qgsrunprocess.sip
%Include auto_generated/qgsruntimeprofiler.sip
%Include auto_generated/qgsscalecalculator.sip
@ -212,6 +213,7 @@
%Include auto_generated/annotations/qgsannotationpointtextitem.sip
%Include auto_generated/annotations/qgsannotationpolygonitem.sip
%Include auto_generated/annotations/qgshtmlannotation.sip
%Include auto_generated/annotations/qgsrenderedannotationitemdetails.sip
%Include auto_generated/annotations/qgssvgannotation.sip
%Include auto_generated/annotations/qgstextannotation.sip
%Include auto_generated/auth/qgsauthcertutils.sip
@ -419,6 +421,7 @@
%Include auto_generated/maprenderer/qgsmaprendererparalleljob.sip
%Include auto_generated/maprenderer/qgsmaprenderersequentialjob.sip
%Include auto_generated/maprenderer/qgsmaprenderertask.sip
%Include auto_generated/maprenderer/qgsrendereditemresults.sip
%Include auto_generated/mesh/qgsmesh3daveraging.sip
%Include auto_generated/mesh/qgsmesheditor.sip
%Include auto_generated/mesh/qgsmeshdataprovider.sip

View File

@ -115,6 +115,17 @@ Since QGIS 3.20, if the ``allowOutdatedResults`` flag is ``False`` then outdated
as a result of an ongoing canvas render) will not be returned, and instead ``None`` will be returned.
.. versionadded:: 2.4
%End
const QgsRenderedItemResults *renderedItemResults( bool allowOutdatedResults = true ) const;
%Docstring
Gets access to the rendered item results (may be ``None``), which includes the results of rendering
annotation items in the canvas map.
If the ``allowOutdatedResults`` flag is ``False`` then outdated rendered item results (e.g.
as a result of an ongoing canvas render) will not be returned, and instead ``None`` will be returned.
.. versionadded:: 3.22
%End
void setCachingEnabled( bool enabled );

View File

@ -184,6 +184,7 @@ set(QGIS_CORE_SRCS
annotations/qgsannotationpointtextitem.cpp
annotations/qgsannotationpolygonitem.cpp
annotations/qgshtmlannotation.cpp
annotations/qgsrenderedannotationitemdetails.cpp
annotations/qgssvgannotation.cpp
annotations/qgstextannotation.cpp
@ -397,6 +398,7 @@ set(QGIS_CORE_SRCS
qgsmaplayerlegend.cpp
qgsmaplayermodel.cpp
qgsmaplayerproxymodel.cpp
qgsmaplayerrenderer.cpp
qgsmaplayerstore.cpp
qgsmaplayerstyle.cpp
qgsmaplayerstylemanager.cpp
@ -448,6 +450,7 @@ set(QGIS_CORE_SRCS
qgsremappingproxyfeaturesink.cpp
qgsrenderchecker.cpp
qgsrendercontext.cpp
qgsrendereditemdetails.cpp
qgsrunprocess.cpp
qgsruntimeprofiler.cpp
qgsscalecalculator.cpp
@ -588,6 +591,7 @@ set(QGIS_CORE_SRCS
maprenderer/qgsmaprenderersequentialjob.cpp
maprenderer/qgsmaprendererstagedrenderjob.cpp
maprenderer/qgsmaprenderertask.cpp
maprenderer/qgsrendereditemresults.cpp
pal/costcalculator.cpp
pal/feature.cpp
@ -1084,6 +1088,7 @@ set(QGIS_CORE_HDRS
qgsrenderchecker.h
qgsrendercontext.h
qgsrenderedfeaturehandlerinterface.h
qgsrendereditemdetails.h
qgsrunprocess.h
qgsruntimeprofiler.h
qgsscalecalculator.h
@ -1158,6 +1163,7 @@ set(QGIS_CORE_HDRS
annotations/qgsannotationpolygonitem.h
annotations/qgsannotationregistry.h
annotations/qgshtmlannotation.h
annotations/qgsrenderedannotationitemdetails.h
annotations/qgssvgannotation.h
annotations/qgstextannotation.h
@ -1404,6 +1410,7 @@ set(QGIS_CORE_HDRS
maprenderer/qgsmaprenderersequentialjob.h
maprenderer/qgsmaprendererstagedrenderjob.h
maprenderer/qgsmaprenderertask.h
maprenderer/qgsrendereditemresults.h
mesh/qgsmesh3daveraging.h
mesh/qgsmesheditor.h

View File

@ -17,6 +17,7 @@
#include "qgsannotationlayerrenderer.h"
#include "qgsannotationlayer.h"
#include "qgsfeedback.h"
#include "qgsrenderedannotationitemdetails.h"
QgsAnnotationLayerRenderer::QgsAnnotationLayerRenderer( QgsAnnotationLayer *layer, QgsRenderContext &context )
: QgsMapLayerRenderer( layer->id(), &context )
@ -39,15 +40,18 @@ QgsAnnotationLayerRenderer::QgsAnnotationLayerRenderer( QgsAnnotationLayer *laye
mItems.reserve( items.size() );
std::transform( items.begin(), items.end(), std::back_inserter( mItems ),
[layer]( const QString & id ) -> QgsAnnotationItem* { return layer->item( id )->clone(); } );
[layer]( const QString & id ) ->std::pair< QString, std::unique_ptr< QgsAnnotationItem > >
{
return std::make_pair( id, std::unique_ptr< QgsAnnotationItem >( layer->item( id )->clone() ) );
} );
std::sort( mItems.begin(), mItems.end(), []( QgsAnnotationItem * a, QgsAnnotationItem * b ) { return a->zIndex() < b->zIndex(); } ); //clazy:exclude=detaching-member
std::sort( mItems.begin(), mItems.end(), [](
const std::pair< QString, std::unique_ptr< QgsAnnotationItem > > &a,
const std::pair< QString, std::unique_ptr< QgsAnnotationItem > > &b )
{ return a.second->zIndex() < b.second->zIndex(); } );
}
QgsAnnotationLayerRenderer::~QgsAnnotationLayerRenderer()
{
qDeleteAll( mItems );
}
QgsAnnotationLayerRenderer::~QgsAnnotationLayerRenderer() = default;
QgsFeedback *QgsAnnotationLayerRenderer::feedback() const
{
@ -59,7 +63,7 @@ bool QgsAnnotationLayerRenderer::render()
QgsRenderContext &context = *renderContext();
bool canceled = false;
for ( QgsAnnotationItem *item : std::as_const( mItems ) )
for ( const std::pair< QString, std::unique_ptr< QgsAnnotationItem > > &item : std::as_const( mItems ) )
{
if ( mFeedback->isCanceled() )
{
@ -67,7 +71,14 @@ bool QgsAnnotationLayerRenderer::render()
break;
}
item->render( context, mFeedback.get() );
const QgsRectangle bounds = item.second->boundingBox( context );
if ( bounds.intersects( context.extent() ) )
{
item.second->render( context, mFeedback.get() );
std::unique_ptr< QgsRenderedAnnotationItemDetails > details = std::make_unique< QgsRenderedAnnotationItemDetails >( mLayerID, item.first );
details->setBoundingBox( bounds );
appendRenderedItemDetails( details.release() );
}
}
return !canceled;
}

View File

@ -23,6 +23,9 @@
#include "qgis_sip.h"
#include "qgsmaplayerrenderer.h"
#include "qgsannotationitem.h"
#include <tuple>
#include <vector>
#include <memory>
class QgsAnnotationLayer;
@ -47,7 +50,7 @@ class CORE_EXPORT QgsAnnotationLayerRenderer : public QgsMapLayerRenderer
bool forceRasterRender() const override;
private:
QVector< QgsAnnotationItem *> mItems;
std::vector < std::pair< QString, std::unique_ptr< QgsAnnotationItem > > > mItems;
std::unique_ptr< QgsFeedback > mFeedback;
double mLayerOpacity = 1.0;

View File

@ -0,0 +1,29 @@
/***************************************************************************
qgsrenderedannotationitemdetails.cpp
----------------
copyright : (C) 2021 by Nyall Dawson
email : nyall dot dawson at gmail dot com
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include "qgsrenderedannotationitemdetails.h"
QgsRenderedAnnotationItemDetails::QgsRenderedAnnotationItemDetails( const QString &layerId, const QString &itemId )
: mLayerId( layerId )
, mItemId( itemId )
{
}
QgsRenderedAnnotationItemDetails *QgsRenderedAnnotationItemDetails::clone() const
{
return new QgsRenderedAnnotationItemDetails( *this );
}

View File

@ -0,0 +1,65 @@
/***************************************************************************
qgsrenderedannotationitemdetails.h
----------------
copyright : (C) 2021 by Nyall Dawson
email : nyall dot dawson at gmail dot com
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#ifndef QGSRENDEREDANNOTATIONITEMDETAILS_H
#define QGSRENDEREDANNOTATIONITEMDETAILS_H
#include "qgis_core.h"
#include "qgis_sip.h"
#include "qgsrendereditemdetails.h"
/**
* \ingroup core
* \brief Contains information about a rendered annotation item.
* \since QGIS 3.22
*/
class CORE_EXPORT QgsRenderedAnnotationItemDetails : public QgsRenderedItemDetails
{
public:
/**
* Constructor for QgsRenderedAnnotationItemDetails.
*/
QgsRenderedAnnotationItemDetails( const QString &layerId, const QString &itemId );
#ifdef SIP_RUN
SIP_PYOBJECT __repr__();
% MethodCode
QString str = QStringLiteral( "<QgsRenderedAnnotationItemDetails: %1 - %2>" ).arg( sipCpp->layerId(), sipCpp->itemId() );
sipRes = PyUnicode_FromString( str.toUtf8().constData() );
% End
#endif
QgsRenderedAnnotationItemDetails *clone() const override SIP_FACTORY;
/**
* Returns the layer ID of the associated map layer.
*/
QString layerId() const { return mLayerId; }
/**
* Returns the item ID of the associated annotation item.
*/
QString itemId() const { return mItemId; }
private:
QString mLayerId;
QString mItemId;
};
#endif // QGSRENDEREDANNOTATIONITEMDETAILS_H

View File

@ -43,6 +43,7 @@
#include "qgsmaplayertemporalproperties.h"
#include "qgsmaplayerelevationproperties.h"
#include "qgsvectorlayerrenderer.h"
#include "qgsrendereditemresults.h"
///@cond PRIVATE
@ -130,8 +131,11 @@ bool LayerRenderJob::imageCanBeComposed() const
QgsMapRendererJob::QgsMapRendererJob( const QgsMapSettings &settings )
: mSettings( settings )
, mRenderedItemResults( std::make_unique< QgsRenderedItemResults >() )
{}
QgsMapRendererJob::~QgsMapRendererJob() = default;
void QgsMapRendererJob::start()
{
if ( mSettings.hasValidSettings() )
@ -143,6 +147,11 @@ void QgsMapRendererJob::start()
}
}
QgsRenderedItemResults *QgsMapRendererJob::takeRenderedItemResults()
{
return mRenderedItemResults.release();
}
QgsMapRendererQImageJob::QgsMapRendererQImageJob( const QgsMapSettings &settings )
: QgsMapRendererJob( settings )
{
@ -810,6 +819,8 @@ void QgsMapRendererJob::cleanupJobs( std::vector<LayerRenderJob> &jobs )
for ( const QString &message : errors )
mErrors.append( Error( job.renderer->layerId(), message ) );
mRenderedItemResults->appendResults( job.renderer->takeRenderedItemDetails(), *job.context() );
delete job.renderer;
job.renderer = nullptr;
}

View File

@ -37,6 +37,7 @@ class QgsLabelingResults;
class QgsMapLayerRenderer;
class QgsMapRendererCache;
class QgsFeatureFilterProvider;
class QgsRenderedItemResults;
#ifndef SIP_RUN
/// @cond PRIVATE
@ -250,6 +251,8 @@ class CORE_EXPORT QgsMapRendererJob : public QObject SIP_ABSTRACT
QgsMapRendererJob( const QgsMapSettings &settings );
~QgsMapRendererJob() override;
/**
* Start the rendering job and immediately return.
* Does nothing if the rendering is already in progress.
@ -291,6 +294,15 @@ class CORE_EXPORT QgsMapRendererJob : public QObject SIP_ABSTRACT
*/
virtual QgsLabelingResults *takeLabelingResults() = 0 SIP_TRANSFER;
/**
* Takes the rendered item results from the map render job and returns them.
*
* Ownership is transferred to the caller.
*
* \since QGIS 3.22
*/
QgsRenderedItemResults *takeRenderedItemResults() SIP_TRANSFER;
/**
* Set the feature filter provider used by the QgsRenderContext of
* each LayerRenderJob.
@ -421,6 +433,10 @@ class CORE_EXPORT QgsMapRendererJob : public QObject SIP_ABSTRACT
*/
bool mRecordRenderingTime = true;
#ifndef SIP_RUN
std::unique_ptr< QgsRenderedItemResults > mRenderedItemResults;
#endif
/**
* Prepares the cache for storing the result of labeling. Returns FALSE if
* the render cannot use cached labels and should not cache the result.

View File

@ -19,6 +19,7 @@
#include "qgsmaprenderercustompainterjob.h"
#include "qgspallabeling.h"
#include "qgslabelingresults.h"
#include "qgsrendereditemresults.h"
QgsMapRendererSequentialJob::QgsMapRendererSequentialJob( const QgsMapSettings &settings )
: QgsMapRendererQImageJob( settings )
@ -138,6 +139,8 @@ void QgsMapRendererSequentialJob::internalFinished()
mLabelingResults.reset( mInternalJob->takeLabelingResults() );
mUsedCachedLabels = mInternalJob->usedCachedLabels();
mRenderedItemResults.reset( mInternalJob->takeRenderedItemResults() );
mErrors = mInternalJob->errors();
// now we are in a slot called from mInternalJob - do not delete it immediately

View File

@ -21,6 +21,7 @@
#include "qgsproject.h"
#include "qgsmaplayerrenderer.h"
#include "qgsmaplayerlistutils.h"
#include "qgsrendereditemresults.h"
QgsMapRendererStagedRenderJob::QgsMapRendererStagedRenderJob( const QgsMapSettings &settings, Flags flags )
: QgsMapRendererAbstractCustomPainterJob( settings )

View File

@ -0,0 +1,129 @@
/***************************************************************************
qgsrendereditemresults.cpp
-------------------
begin : August 2021
copyright : (C) Nyall Dawson
email : nyall dot dawson at gmail dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include "qgsrendereditemresults.h"
#include "qgsrendereditemdetails.h"
#include "qgsrendercontext.h"
#include "qgslogger.h"
#include "qgsrenderedannotationitemdetails.h"
#include "RTree.h"
///@cond PRIVATE
class QgsRenderedItemResultsSpatialIndex : public RTree<const QgsRenderedItemDetails *, float, 2, float>
{
public:
void insert( const QgsRenderedItemDetails *details, const QgsRectangle &bounds )
{
std::array< float, 4 > scaledBounds = scaleBounds( bounds );
this->Insert(
{
scaledBounds[0], scaledBounds[ 1]
},
{
scaledBounds[2], scaledBounds[3]
},
details );
}
/**
* Performs an intersection check against the index, for data intersecting the specified \a bounds.
*
* The \a callback function will be called once for each matching data object encountered.
*/
bool intersects( const QgsRectangle &bounds, const std::function< bool( const QgsRenderedItemDetails *details )> &callback ) const
{
std::array< float, 4 > scaledBounds = scaleBounds( bounds );
this->Search(
{
scaledBounds[0], scaledBounds[ 1]
},
{
scaledBounds[2], scaledBounds[3]
},
callback );
return true;
}
private:
std::array<float, 4> scaleBounds( const QgsRectangle &bounds ) const
{
return
{
static_cast< float >( bounds.xMinimum() ),
static_cast< float >( bounds.yMinimum() ),
static_cast< float >( bounds.xMaximum() ),
static_cast< float >( bounds.yMaximum() )
};
}
};
///@endcond
QgsRenderedItemResults::QgsRenderedItemResults()
: mAnnotationItemsIndex( std::make_unique< QgsRenderedItemResultsSpatialIndex >() )
{
}
QgsRenderedItemResults::~QgsRenderedItemResults() = default;
QList<QgsRenderedItemDetails *> QgsRenderedItemResults::renderedItems() const
{
QList< QgsRenderedItemDetails * > res;
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
res.reserve( static_cast< int >( mDetails.size() ) );
#else
res.reserve( mDetails.size() );
#endif
std::transform( mDetails.begin(), mDetails.end(), std::back_inserter( res ), []( const auto & detail ) {return detail.get();} );
return res;
}
QList<const QgsRenderedAnnotationItemDetails *> QgsRenderedItemResults::renderedAnnotationItemsInBounds( const QgsRectangle &bounds ) const
{
QList<const QgsRenderedAnnotationItemDetails *> res;
mAnnotationItemsIndex->intersects( bounds, [&res]( const QgsRenderedItemDetails * details )->bool
{
res << qgis::down_cast< const QgsRenderedAnnotationItemDetails * >( details );
return true;
} );
return res;
}
void QgsRenderedItemResults::appendResults( const QList<QgsRenderedItemDetails *> &results, const QgsRenderContext &context )
{
const QgsCoordinateTransform layerToMapTransform = context.coordinateTransform();
for ( QgsRenderedItemDetails *details : results )
{
try
{
const QgsRectangle transformedBounds = layerToMapTransform.transformBoundingBox( details->boundingBox() );
details->setBoundingBox( transformedBounds );
}
catch ( QgsCsException & )
{
QgsDebugMsg( QStringLiteral( "Could not transform rendered item's bounds to map CRS" ) );
}
if ( QgsRenderedAnnotationItemDetails *annotationDetails = dynamic_cast< QgsRenderedAnnotationItemDetails * >( details ) )
mAnnotationItemsIndex->insert( annotationDetails, annotationDetails->boundingBox() );
mDetails.emplace_back( std::unique_ptr< QgsRenderedItemDetails >( details ) );
}
}

View File

@ -0,0 +1,89 @@
/***************************************************************************
qgsrendereditemresults.h
-------------------
begin : August 2021
copyright : (C) Nyall Dawson
email : nyall dot dawson at gmail dot com
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#ifndef QGSRENDEREDITEMRESULTS_H
#define QGSRENDEREDITEMRESULTS_H
#include "qgis_core.h"
#include "qgis_sip.h"
#include <memory>
#include <QList>
#include <vector>
class QgsRenderedItemDetails;
class QgsRenderContext;
class QgsRenderedAnnotationItemDetails;
class QgsRectangle;
///@cond PRIVATE
class QgsRenderedItemResultsSpatialIndex;
///@endcond
/**
* \ingroup core
* \brief Stores collated details of rendered items during a map rendering operation.
* \since QGIS 3.22
*/
class CORE_EXPORT QgsRenderedItemResults
{
public:
QgsRenderedItemResults();
~QgsRenderedItemResults();
//! QgsRenderedItemResults cannot be copied.
QgsRenderedItemResults( const QgsRenderedItemResults & ) = delete;
//! QgsRenderedItemResults cannot be copied.
QgsRenderedItemResults &operator=( const QgsRenderedItemResults &rh ) = delete;
/**
* Returns a list of all rendered items.
*/
QList< QgsRenderedItemDetails * > renderedItems() const;
/**
* Returns a list with details of the rendered annotation items within the specified \a bounds.
*
* \since QGIS 3.22
*/
QList<const QgsRenderedAnnotationItemDetails *> renderedAnnotationItemsInBounds( const QgsRectangle &bounds ) const;
#ifndef SIP_RUN
/**
* Appends rendered item details to the results object.
*
* Ownership of \a results is transferred to the this object.
*
* The render \a context argument is used to specify the render context used to render the items. It will be used
* to transform the details to the destination map CRS.
*
* \note Not available in Python bindings.
*/
void appendResults( const QList< QgsRenderedItemDetails * > &results, const QgsRenderContext &context );
#endif
private:
#ifdef SIP_RUN
QgsRenderedItemResults( const QgsRenderedItemResults & );
#endif
std::vector< std::unique_ptr< QgsRenderedItemDetails > > mDetails;
std::unique_ptr< QgsRenderedItemResultsSpatialIndex > mAnnotationItemsIndex;
};
#endif // QGSRENDEREDITEMRESULTS_H

View File

@ -0,0 +1,30 @@
/***************************************************************************
qgsmaplayerrenderer.cpp
--------------------------------------
Date : August 2021
Copyright : (C) 2021 by Nyall Dawson
Email : nyall dot dawson at gmail dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include "qgsmaplayerrenderer.h"
#include "qgsrendereditemdetails.h"
QgsMapLayerRenderer::~QgsMapLayerRenderer() = default;
QList<QgsRenderedItemDetails *> QgsMapLayerRenderer::takeRenderedItemDetails()
{
return std::move( mRenderedItemDetails );
}
void QgsMapLayerRenderer::appendRenderedItemDetails( QgsRenderedItemDetails *details )
{
mRenderedItemDetails.append( details );
}

View File

@ -23,6 +23,7 @@
class QgsFeedback;
class QgsRenderContext;
class QgsRenderedItemDetails;
/**
* \ingroup core
@ -62,7 +63,7 @@ class CORE_EXPORT QgsMapLayerRenderer
, mContext( context )
{}
virtual ~QgsMapLayerRenderer() = default;
virtual ~QgsMapLayerRenderer();
/**
* Do the rendering (based on data stored in the class).
@ -137,6 +138,16 @@ class CORE_EXPORT QgsMapLayerRenderer
*/
virtual void setLayerRenderingTimeHint( int time ) SIP_SKIP { Q_UNUSED( time ) }
/**
* Takes the list of rendered item details from the renderer.
*
* Ownership of items is transferred to the caller.
*
* \see appendRenderedItemDetails()
* \since QGIS 3.22
*/
QList< QgsRenderedItemDetails * > takeRenderedItemDetails() SIP_TRANSFERBACK;
protected:
QStringList mErrors;
QString mLayerID;
@ -169,6 +180,18 @@ class CORE_EXPORT QgsMapLayerRenderer
*/
static constexpr int MAX_TIME_TO_USE_CACHED_PREVIEW_IMAGE = 3000 SIP_SKIP;
/**
* Appends the \a details of a rendered item to the renderer.
*
* Rendered item details can be retrieved by calling takeRenderedItemDetails().
*
* Ownership of \a details is transferred to the renderer.
*
* \see takeRenderedItemDetails()
* \since QGIS 3.22
*/
void appendRenderedItemDetails( QgsRenderedItemDetails *details SIP_TRANSFER );
private:
// TODO QGIS 4.0 - make reference instead of pointer!
@ -179,6 +202,8 @@ class CORE_EXPORT QgsMapLayerRenderer
* \since QGIS 3.10
*/
QgsRenderContext *mContext = nullptr;
QList<QgsRenderedItemDetails *> mRenderedItemDetails;
};
#endif // QGSMAPLAYERRENDERER_H

View File

@ -0,0 +1,19 @@
/***************************************************************************
qgsrendereditemdetails.cpp
----------------
copyright : (C) 2021 by Nyall Dawson
email : nyall dot dawson at gmail dot com
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include "qgsrendereditemdetails.h"
QgsRenderedItemDetails::~QgsRenderedItemDetails() = default;

View File

@ -0,0 +1,68 @@
/***************************************************************************
qgsrendereditemdetails.h
----------------
copyright : (C) 2021 by Nyall Dawson
email : nyall dot dawson at gmail dot com
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#ifndef QGSRENDEREDITEMDETAILS_H
#define QGSRENDEREDITEMDETAILS_H
#include "qgis_core.h"
#include "qgis_sip.h"
#include "qgsrectangle.h"
/**
* \ingroup core
* \brief Base class for detailed information about a rendered item.
* \since QGIS 3.22
*/
class CORE_EXPORT QgsRenderedItemDetails
{
public:
#ifdef SIP_RUN
SIP_CONVERT_TO_SUBCLASS_CODE
if ( dynamic_cast<QgsRenderedAnnotationItemDetails *>( sipCpp ) )
sipType = sipType_QgsRenderedAnnotationItemDetails;
else
sipType = 0;
SIP_END
#endif
virtual ~QgsRenderedItemDetails();
/**
* Clones the details.
*/
virtual QgsRenderedItemDetails *clone() const = 0 SIP_FACTORY;
/**
* Returns the bounding box of the item (in map units).
*
* \see setBoundingBox()
*/
QgsRectangle boundingBox() const { return mBounds; }
/**
* Sets the bounding box of the item (in map units).
*
* \see boundingBox()
*/
void setBoundingBox( const QgsRectangle &bounds ) { mBounds = bounds; }
private:
QgsRectangle mBounds;
};
#endif // QGSRENDEREDITEMDETAILS_H

View File

@ -92,6 +92,7 @@ email : sherman at mrcc.com
#include "qgslabelingresults.h"
#include "qgsmaplayerutils.h"
#include "qgssettingsregistrygui.h"
#include "qgsrendereditemresults.h"
/**
* \ingroup gui
@ -472,6 +473,14 @@ const QgsLabelingResults *QgsMapCanvas::labelingResults( bool allowOutdatedResul
return mLabelingResults.get();
}
const QgsRenderedItemResults *QgsMapCanvas::renderedItemResults( bool allowOutdatedResults ) const
{
if ( !allowOutdatedResults && mRenderedItemResultsOutdated )
return nullptr;
return mRenderedItemResults.get();
}
void QgsMapCanvas::setCachingEnabled( bool enabled )
{
if ( enabled == isCachingEnabled() )
@ -583,6 +592,7 @@ void QgsMapCanvas::refresh()
mRefreshTimer->start( 1 );
mLabelingResultsOutdated = true;
mRenderedItemResultsOutdated = true;
}
void QgsMapCanvas::refreshMap()
@ -697,6 +707,9 @@ void QgsMapCanvas::rendererJobFinished()
}
mLabelingResultsOutdated = false;
mRenderedItemResults.reset( mJob->takeRenderedItemResults() );
mRenderedItemResultsOutdated = false;
QImage img = mJob->renderedImage();
// emit renderComplete to get our decorations drawn

View File

@ -70,6 +70,7 @@ class QgsSnappingUtils;
class QgsRubberBand;
class QgsMapCanvasAnnotationItem;
class QgsReferencedRectangle;
class QgsRenderedItemResults;
class QgsTemporalController;
@ -170,6 +171,17 @@ class GUI_EXPORT QgsMapCanvas : public QGraphicsView, public QgsExpressionContex
*/
const QgsLabelingResults *labelingResults( bool allowOutdatedResults = true ) const;
/**
* Gets access to the rendered item results (may be NULLPTR), which includes the results of rendering
* annotation items in the canvas map.
*
* If the \a allowOutdatedResults flag is FALSE then outdated rendered item results (e.g.
* as a result of an ongoing canvas render) will not be returned, and instead NULLPTR will be returned.
*
* \since QGIS 3.22
*/
const QgsRenderedItemResults *renderedItemResults( bool allowOutdatedResults = true ) const;
/**
* Set whether to cache images of rendered layers
* \since QGIS 2.4
@ -1301,6 +1313,19 @@ class GUI_EXPORT QgsMapCanvas : public QGraphicsView, public QgsExpressionContex
//! TRUE if the labeling results stored in mLabelingResults are outdated (e.g. as a result of an ongoing canvas render)
bool mLabelingResultsOutdated = false;
/**
* Rendered results from the recently rendered map.
* \since QGIS 3.22
*/
std::unique_ptr< QgsRenderedItemResults > mRenderedItemResults;
/**
* TRUE if the rendered item results stored in mRenderedItemResults are outdated (e.g. as a result of an ongoing canvas render)
*
* \since QGIS 3.22
*/
bool mRenderedItemResultsOutdated = false;
//! Whether layers are rendered sequentially or in parallel
bool mUseParallelRendering = false;

View File

@ -38,6 +38,9 @@ from qgis.core import (QgsMapSettings,
QgsAnnotationMarkerItem,
QgsLineSymbol,
QgsMarkerSymbol,
QgsMapRendererSequentialJob,
QgsMapRendererParallelJob,
QgsGeometry
)
from qgis.testing import start_app, unittest
@ -247,17 +250,17 @@ class TestQgsAnnotationLayer(unittest.TestCase):
item.setSymbol(
QgsFillSymbol.createSimple({'color': '200,100,100', 'outline_color': 'black', 'outline_width': '2'}))
item.setZIndex(3)
layer.addItem(item)
i1_id = layer.addItem(item)
item = QgsAnnotationLineItem(QgsLineString([QgsPoint(11, 13), QgsPoint(12, 13), QgsPoint(12, 15)]))
item.setSymbol(QgsLineSymbol.createSimple({'color': '#ffff00', 'line_width': '3'}))
item.setZIndex(2)
layer.addItem(item)
i2_id = layer.addItem(item)
item = QgsAnnotationMarkerItem(QgsPoint(12, 13))
item.setSymbol(QgsMarkerSymbol.createSimple({'color': '100,200,200', 'size': '6', 'outline_color': 'black'}))
item.setZIndex(1)
layer.addItem(item)
i3_id = layer.addItem(item)
settings = QgsMapSettings()
settings.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:4326'))
@ -282,6 +285,17 @@ class TestQgsAnnotationLayer(unittest.TestCase):
self.assertTrue(self.imageCheck('layer_render', 'layer_render', image))
# also check details of rendered items
item_details = renderer.takeRenderedItemDetails()
self.assertEqual([i.layerId() for i in item_details], [layer.id()] * 3)
self.assertCountEqual([i.itemId() for i in item_details], [i1_id, i2_id, i3_id])
self.assertEqual([i.boundingBox() for i in item_details if i.itemId() == i1_id][0],
QgsRectangle(12, 13, 14, 15))
self.assertEqual([i.boundingBox() for i in item_details if i.itemId() == i2_id][0],
QgsRectangle(11, 13, 12, 15))
self.assertEqual([i.boundingBox() for i in item_details if i.itemId() == i3_id][0],
QgsRectangle(12, 13, 12, 13))
def testRenderWithTransform(self):
layer = QgsAnnotationLayer('test', QgsAnnotationLayer.LayerOptions(QgsProject.instance().transformContext()))
self.assertTrue(layer.isValid())
@ -291,17 +305,17 @@ class TestQgsAnnotationLayer(unittest.TestCase):
item.setSymbol(
QgsFillSymbol.createSimple({'color': '200,100,100', 'outline_color': 'black', 'outline_width': '2'}))
item.setZIndex(1)
layer.addItem(item)
i1_id = layer.addItem(item)
item = QgsAnnotationLineItem(QgsLineString([QgsPoint(11, 13), QgsPoint(12, 13), QgsPoint(12, 15)]))
item.setSymbol(QgsLineSymbol.createSimple({'color': '#ffff00', 'line_width': '3'}))
item.setZIndex(2)
layer.addItem(item)
i2_id = layer.addItem(item)
item = QgsAnnotationMarkerItem(QgsPoint(12, 13))
item.setSymbol(QgsMarkerSymbol.createSimple({'color': '100,200,200', 'size': '6', 'outline_color': 'black'}))
item.setZIndex(3)
layer.addItem(item)
i3_id = layer.addItem(item)
layer.setCrs(QgsCoordinateReferenceSystem('EPSG:4326'))
@ -331,6 +345,123 @@ class TestQgsAnnotationLayer(unittest.TestCase):
self.assertTrue(self.imageCheck('layer_render_transform', 'layer_render_transform', image))
# also check details of rendered items
item_details = renderer.takeRenderedItemDetails()
self.assertEqual([i.layerId() for i in item_details], [layer.id()] * 3)
self.assertCountEqual([i.itemId() for i in item_details], [i1_id, i2_id, i3_id])
self.assertEqual([i.boundingBox() for i in item_details if i.itemId() == i1_id][0],
QgsRectangle(11.5, 13, 12, 13.5))
self.assertEqual([i.boundingBox() for i in item_details if i.itemId() == i2_id][0],
QgsRectangle(11, 13, 12, 15))
self.assertEqual([i.boundingBox() for i in item_details if i.itemId() == i3_id][0],
QgsRectangle(12, 13, 12, 13))
def test_render_via_job(self):
"""
Test rendering an annotation layer via a map render job
"""
layer = QgsAnnotationLayer('test', QgsAnnotationLayer.LayerOptions(QgsProject.instance().transformContext()))
self.assertTrue(layer.isValid())
item = QgsAnnotationPolygonItem(
QgsPolygon(QgsLineString([QgsPoint(11.5, 13), QgsPoint(12, 13), QgsPoint(12, 13.5), QgsPoint(11.5, 13)])))
item.setSymbol(
QgsFillSymbol.createSimple({'color': '200,100,100', 'outline_color': 'black', 'outline_width': '2'}))
item.setZIndex(1)
i1_id = layer.addItem(item)
item = QgsAnnotationLineItem(QgsLineString([QgsPoint(11, 13), QgsPoint(12, 13), QgsPoint(12, 15)]))
item.setSymbol(QgsLineSymbol.createSimple({'color': '#ffff00', 'line_width': '3'}))
item.setZIndex(2)
i2_id = layer.addItem(item)
item = QgsAnnotationMarkerItem(QgsPoint(12, 13))
item.setSymbol(QgsMarkerSymbol.createSimple({'color': '100,200,200', 'size': '6', 'outline_color': 'black'}))
item.setZIndex(3)
i3_id = layer.addItem(item)
layer.setCrs(QgsCoordinateReferenceSystem('EPSG:4326'))
settings = QgsMapSettings()
settings.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:4326'))
settings.setExtent(QgsRectangle(10, 10, 18, 18))
settings.setOutputSize(QSize(200, 200))
settings.setLayers([layer])
job = QgsMapRendererParallelJob(settings)
job.start()
job.waitForFinished()
# check rendered item results
item_results = job.takeRenderedItemResults()
item_details = item_results.renderedItems()
self.assertEqual(len(item_details), 3)
self.assertEqual([i.layerId() for i in item_details], [layer.id()] * 3)
self.assertCountEqual([i.itemId() for i in item_details], [i1_id, i2_id, i3_id])
self.assertCountEqual([i.itemId() for i in item_results.renderedAnnotationItemsInBounds(QgsRectangle(0, 0, 1, 1))], [])
self.assertCountEqual(
[i.itemId() for i in item_results.renderedAnnotationItemsInBounds(QgsRectangle(10, 10, 11, 18))], [i2_id])
self.assertCountEqual(
[i.itemId() for i in item_results.renderedAnnotationItemsInBounds(QgsRectangle(10, 10, 12, 18))], [i1_id, i2_id, i3_id])
# bounds should be in map crs
self.assertEqual([i.boundingBox() for i in item_details if i.itemId() == i1_id][0],
QgsRectangle(11.5, 13, 12, 13.5))
self.assertEqual([i.boundingBox() for i in item_details if i.itemId() == i2_id][0],
QgsRectangle(11, 13, 12, 15))
self.assertEqual([i.boundingBox() for i in item_details if i.itemId() == i3_id][0],
QgsRectangle(12, 13, 12, 13))
def test_render_via_job_with_transform(self):
"""
Test rendering an annotation layer via a map render job
"""
layer = QgsAnnotationLayer('test', QgsAnnotationLayer.LayerOptions(QgsProject.instance().transformContext()))
self.assertTrue(layer.isValid())
item = QgsAnnotationPolygonItem(
QgsPolygon(QgsLineString([QgsPoint(11.5, 13), QgsPoint(12, 13), QgsPoint(12, 13.5), QgsPoint(11.5, 13)])))
item.setSymbol(
QgsFillSymbol.createSimple({'color': '200,100,100', 'outline_color': 'black', 'outline_width': '2'}))
item.setZIndex(1)
i1_id = layer.addItem(item)
item = QgsAnnotationLineItem(QgsLineString([QgsPoint(11, 13), QgsPoint(12, 13), QgsPoint(12, 15)]))
item.setSymbol(QgsLineSymbol.createSimple({'color': '#ffff00', 'line_width': '3'}))
item.setZIndex(2)
i2_id = layer.addItem(item)
item = QgsAnnotationMarkerItem(QgsPoint(12, 13))
item.setSymbol(QgsMarkerSymbol.createSimple({'color': '100,200,200', 'size': '6', 'outline_color': 'black'}))
item.setZIndex(3)
i3_id = layer.addItem(item)
layer.setCrs(QgsCoordinateReferenceSystem('EPSG:4326'))
settings = QgsMapSettings()
settings.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:3857'))
settings.setExtent(QgsRectangle(1250958, 1386945, 1420709, 1532518))
settings.setOutputSize(QSize(200, 200))
settings.setLayers([layer])
job = QgsMapRendererSequentialJob(settings)
job.start()
job.waitForFinished()
# check rendered item results
item_results = job.takeRenderedItemResults()
item_details = item_results.renderedItems()
self.assertEqual(len(item_details), 3)
self.assertEqual([i.layerId() for i in item_details], [layer.id()] * 3)
self.assertCountEqual([i.itemId() for i in item_details], [i1_id, i2_id, i3_id])
# bounds should be in map crs
self.assertEqual([QgsGeometry.fromRect(i.boundingBox()).asWkt(0) for i in item_details if i.itemId() == i1_id][0],
'Polygon ((1280174 1459732, 1335834 1459732, 1335834 1516914, 1280174 1516914, 1280174 1459732))')
self.assertEqual([QgsGeometry.fromRect(i.boundingBox()).asWkt(0) for i in item_details if i.itemId() == i2_id][0],
'Polygon ((1224514 1459732, 1335834 1459732, 1335834 1689200, 1224514 1689200, 1224514 1459732))')
self.assertEqual([QgsGeometry.fromRect(i.boundingBox()).asWkt(0) for i in item_details if i.itemId() == i3_id][0],
'Polygon ((1335834 1459732, 1335834 1459732, 1335834 1459732, 1335834 1459732, 1335834 1459732))')
def imageCheck(self, name, reference_image, image):
TestQgsAnnotationLayer.report += "<h2>Render {}</h2>\n".format(name)
temp_dir = QDir.tempPath() + '/'