From 660433d9a961480d8a8d502e264c93f2a5c88b6d Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 30 Aug 2021 10:47:45 +1000 Subject: [PATCH] [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. --- .../qgsrenderedannotationitemdetails.sip.in | 57 +++++++ .../maprenderer/qgsmaprendererjob.sip.in | 12 ++ .../maprenderer/qgsrendereditemresults.sip.in | 53 +++++++ .../auto_generated/qgsmaplayerrenderer.sip.in | 24 +++ .../qgsrendereditemdetails.sip.in | 61 ++++++++ python/core/core_auto.sip | 3 + python/gui/auto_generated/qgsmapcanvas.sip.in | 11 ++ src/core/CMakeLists.txt | 7 + .../qgsannotationlayerrenderer.cpp | 27 +++- .../annotations/qgsannotationlayerrenderer.h | 5 +- .../qgsrenderedannotationitemdetails.cpp | 29 ++++ .../qgsrenderedannotationitemdetails.h | 65 ++++++++ src/core/maprenderer/qgsmaprendererjob.cpp | 11 ++ src/core/maprenderer/qgsmaprendererjob.h | 16 ++ .../qgsmaprenderersequentialjob.cpp | 3 + .../qgsmaprendererstagedrenderjob.cpp | 1 + .../maprenderer/qgsrendereditemresults.cpp | 129 ++++++++++++++++ src/core/maprenderer/qgsrendereditemresults.h | 89 +++++++++++ src/core/qgsmaplayerrenderer.cpp | 30 ++++ src/core/qgsmaplayerrenderer.h | 27 +++- src/core/qgsrendereditemdetails.cpp | 19 +++ src/core/qgsrendereditemdetails.h | 68 +++++++++ src/gui/qgsmapcanvas.cpp | 13 ++ src/gui/qgsmapcanvas.h | 25 +++ tests/src/python/test_qgsannotationlayer.py | 143 +++++++++++++++++- 25 files changed, 912 insertions(+), 16 deletions(-) create mode 100644 python/core/auto_generated/annotations/qgsrenderedannotationitemdetails.sip.in create mode 100644 python/core/auto_generated/maprenderer/qgsrendereditemresults.sip.in create mode 100644 python/core/auto_generated/qgsrendereditemdetails.sip.in create mode 100644 src/core/annotations/qgsrenderedannotationitemdetails.cpp create mode 100644 src/core/annotations/qgsrenderedannotationitemdetails.h create mode 100644 src/core/maprenderer/qgsrendereditemresults.cpp create mode 100644 src/core/maprenderer/qgsrendereditemresults.h create mode 100644 src/core/qgsmaplayerrenderer.cpp create mode 100644 src/core/qgsrendereditemdetails.cpp create mode 100644 src/core/qgsrendereditemdetails.h diff --git a/python/core/auto_generated/annotations/qgsrenderedannotationitemdetails.sip.in b/python/core/auto_generated/annotations/qgsrenderedannotationitemdetails.sip.in new file mode 100644 index 00000000000..fed89220235 --- /dev/null +++ b/python/core/auto_generated/annotations/qgsrenderedannotationitemdetails.sip.in @@ -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( "" ).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 * + ************************************************************************/ diff --git a/python/core/auto_generated/maprenderer/qgsmaprendererjob.sip.in b/python/core/auto_generated/maprenderer/qgsmaprendererjob.sip.in index 1fd40fed406..1e883628777 100644 --- a/python/core/auto_generated/maprenderer/qgsmaprendererjob.sip.in +++ b/python/core/auto_generated/maprenderer/qgsmaprendererjob.sip.in @@ -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). + }; diff --git a/python/core/auto_generated/maprenderer/qgsrendereditemresults.sip.in b/python/core/auto_generated/maprenderer/qgsrendereditemresults.sip.in new file mode 100644 index 00000000000..76e63d62a5a --- /dev/null +++ b/python/core/auto_generated/maprenderer/qgsrendereditemresults.sip.in @@ -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 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 * + ************************************************************************/ diff --git a/python/core/auto_generated/qgsmaplayerrenderer.sip.in b/python/core/auto_generated/qgsmaplayerrenderer.sip.in index ec22a28ff3a..d2840a74a59 100644 --- a/python/core/auto_generated/qgsmaplayerrenderer.sip.in +++ b/python/core/auto_generated/qgsmaplayerrenderer.sip.in @@ -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 + }; /************************************************************************ diff --git a/python/core/auto_generated/qgsrendereditemdetails.sip.in b/python/core/auto_generated/qgsrendereditemdetails.sip.in new file mode 100644 index 00000000000..e9fc619114e --- /dev/null +++ b/python/core/auto_generated/qgsrendereditemdetails.sip.in @@ -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( 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 * + ************************************************************************/ diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index 3106721e58a..8205a099c2d 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -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 diff --git a/python/gui/auto_generated/qgsmapcanvas.sip.in b/python/gui/auto_generated/qgsmapcanvas.sip.in index eedd46eec56..3501e399810 100644 --- a/python/gui/auto_generated/qgsmapcanvas.sip.in +++ b/python/gui/auto_generated/qgsmapcanvas.sip.in @@ -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 ); diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index a090940c431..9f5e9ea201b 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -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 diff --git a/src/core/annotations/qgsannotationlayerrenderer.cpp b/src/core/annotations/qgsannotationlayerrenderer.cpp index e9afda7c100..42af0dee46b 100644 --- a/src/core/annotations/qgsannotationlayerrenderer.cpp +++ b/src/core/annotations/qgsannotationlayerrenderer.cpp @@ -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; } diff --git a/src/core/annotations/qgsannotationlayerrenderer.h b/src/core/annotations/qgsannotationlayerrenderer.h index 91af3b6045a..4715e06d433 100644 --- a/src/core/annotations/qgsannotationlayerrenderer.h +++ b/src/core/annotations/qgsannotationlayerrenderer.h @@ -23,6 +23,9 @@ #include "qgis_sip.h" #include "qgsmaplayerrenderer.h" #include "qgsannotationitem.h" +#include +#include +#include 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; diff --git a/src/core/annotations/qgsrenderedannotationitemdetails.cpp b/src/core/annotations/qgsrenderedannotationitemdetails.cpp new file mode 100644 index 00000000000..7945d4dea83 --- /dev/null +++ b/src/core/annotations/qgsrenderedannotationitemdetails.cpp @@ -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 ); +} diff --git a/src/core/annotations/qgsrenderedannotationitemdetails.h b/src/core/annotations/qgsrenderedannotationitemdetails.h new file mode 100644 index 00000000000..535e88947f5 --- /dev/null +++ b/src/core/annotations/qgsrenderedannotationitemdetails.h @@ -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( "" ).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 diff --git a/src/core/maprenderer/qgsmaprendererjob.cpp b/src/core/maprenderer/qgsmaprendererjob.cpp index f97046e29be..50747df4a8a 100644 --- a/src/core/maprenderer/qgsmaprendererjob.cpp +++ b/src/core/maprenderer/qgsmaprendererjob.cpp @@ -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 &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; } diff --git a/src/core/maprenderer/qgsmaprendererjob.h b/src/core/maprenderer/qgsmaprendererjob.h index 16c6985cb53..8de8fbd1d5d 100644 --- a/src/core/maprenderer/qgsmaprendererjob.h +++ b/src/core/maprenderer/qgsmaprendererjob.h @@ -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. diff --git a/src/core/maprenderer/qgsmaprenderersequentialjob.cpp b/src/core/maprenderer/qgsmaprenderersequentialjob.cpp index 3d8a9d97644..15f6eb5c182 100644 --- a/src/core/maprenderer/qgsmaprenderersequentialjob.cpp +++ b/src/core/maprenderer/qgsmaprenderersequentialjob.cpp @@ -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 diff --git a/src/core/maprenderer/qgsmaprendererstagedrenderjob.cpp b/src/core/maprenderer/qgsmaprendererstagedrenderjob.cpp index 1578b4eb497..00a85096bb1 100644 --- a/src/core/maprenderer/qgsmaprendererstagedrenderjob.cpp +++ b/src/core/maprenderer/qgsmaprendererstagedrenderjob.cpp @@ -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 ) diff --git a/src/core/maprenderer/qgsrendereditemresults.cpp b/src/core/maprenderer/qgsrendereditemresults.cpp new file mode 100644 index 00000000000..30f43b66520 --- /dev/null +++ b/src/core/maprenderer/qgsrendereditemresults.cpp @@ -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 +{ + 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 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 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 QgsRenderedItemResults::renderedAnnotationItemsInBounds( const QgsRectangle &bounds ) const +{ + QList 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 &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 ) ); + } +} + + diff --git a/src/core/maprenderer/qgsrendereditemresults.h b/src/core/maprenderer/qgsrendereditemresults.h new file mode 100644 index 00000000000..b2923553bf5 --- /dev/null +++ b/src/core/maprenderer/qgsrendereditemresults.h @@ -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 +#include +#include + +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 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 diff --git a/src/core/qgsmaplayerrenderer.cpp b/src/core/qgsmaplayerrenderer.cpp new file mode 100644 index 00000000000..e7fb8ae733c --- /dev/null +++ b/src/core/qgsmaplayerrenderer.cpp @@ -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 QgsMapLayerRenderer::takeRenderedItemDetails() +{ + return std::move( mRenderedItemDetails ); +} + +void QgsMapLayerRenderer::appendRenderedItemDetails( QgsRenderedItemDetails *details ) +{ + mRenderedItemDetails.append( details ); +} diff --git a/src/core/qgsmaplayerrenderer.h b/src/core/qgsmaplayerrenderer.h index 281e4ac720f..193264c7f03 100644 --- a/src/core/qgsmaplayerrenderer.h +++ b/src/core/qgsmaplayerrenderer.h @@ -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 mRenderedItemDetails; }; #endif // QGSMAPLAYERRENDERER_H diff --git a/src/core/qgsrendereditemdetails.cpp b/src/core/qgsrendereditemdetails.cpp new file mode 100644 index 00000000000..70de62652a4 --- /dev/null +++ b/src/core/qgsrendereditemdetails.cpp @@ -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; diff --git a/src/core/qgsrendereditemdetails.h b/src/core/qgsrendereditemdetails.h new file mode 100644 index 00000000000..babc35fd5e8 --- /dev/null +++ b/src/core/qgsrendereditemdetails.h @@ -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( 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 diff --git a/src/gui/qgsmapcanvas.cpp b/src/gui/qgsmapcanvas.cpp index 1c051de13a0..ba96bcd97b2 100644 --- a/src/gui/qgsmapcanvas.cpp +++ b/src/gui/qgsmapcanvas.cpp @@ -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 diff --git a/src/gui/qgsmapcanvas.h b/src/gui/qgsmapcanvas.h index 88d6e64b42c..b52f6937b8a 100644 --- a/src/gui/qgsmapcanvas.h +++ b/src/gui/qgsmapcanvas.h @@ -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; diff --git a/tests/src/python/test_qgsannotationlayer.py b/tests/src/python/test_qgsannotationlayer.py index 431e39e5872..f89f8e44af3 100644 --- a/tests/src/python/test_qgsannotationlayer.py +++ b/tests/src/python/test_qgsannotationlayer.py @@ -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 += "

Render {}

\n".format(name) temp_dir = QDir.tempPath() + '/'