From c71cd4b5fc618b067d3ed5856d0f6b441c0c71d3 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 5 Aug 2019 10:01:01 +1000 Subject: [PATCH] Initial work on rendered feature handler interface Adds an interface for classes which provider custom handlers for features rendered as part of a map render job. QgsRenderedFeatureHandlerInterface objects are registered in the QgsMapSettings objects used to construct map render jobs. During the rendering operation, the handleRenderedFeature() method will be called once for every rendered feature, allowing the handler to perform some custom task based on the provided information. They can be used for custom tasks which operate on a set of rendered features, such as creating spatial indexes of the location and rendered symbology bounding box of all features rendered on a map. --- .../core/auto_generated/qgsmapsettings.sip.in | 21 ++++++ .../auto_generated/qgsrendercontext.sip.in | 18 +++++ .../qgsrenderedfeaturehandlerinterface.sip.in | 68 ++++++++++++++++++ python/core/core_auto.sip | 1 + src/core/CMakeLists.txt | 1 + src/core/qgsmapsettings.cpp | 10 +++ src/core/qgsmapsettings.h | 20 ++++++ src/core/qgsrendercontext.cpp | 11 +++ src/core/qgsrendercontext.h | 17 +++++ src/core/qgsrenderedfeaturehandlerinterface.h | 72 +++++++++++++++++++ tests/src/core/testqgsmapsettings.cpp | 26 +++++++ tests/src/python/test_qgsrendercontext.py | 32 ++++++++- 12 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 python/core/auto_generated/qgsrenderedfeaturehandlerinterface.sip.in create mode 100644 src/core/qgsrenderedfeaturehandlerinterface.h diff --git a/python/core/auto_generated/qgsmapsettings.sip.in b/python/core/auto_generated/qgsmapsettings.sip.in index f120428d69b..2f122cd4382 100644 --- a/python/core/auto_generated/qgsmapsettings.sip.in +++ b/python/core/auto_generated/qgsmapsettings.sip.in @@ -656,6 +656,27 @@ The default is to use no global simplification, and fallback to individual layer .. seealso:: :py:func:`setSimplifyMethod` +.. versionadded:: 3.10 +%End + + void addRenderedFeatureHandler( QgsRenderedFeatureHandlerInterface *handler ); +%Docstring +Adds a rendered feature ``handler`` to use while rendering the map settings. + +Ownership of ``handler`` is NOT transferred, and it is the caller's responsibility to ensure +that the handler exists for the lifetime of the map render job. + +.. seealso:: :py:func:`renderedFeatureHandlers` + +.. versionadded:: 3.10 +%End + + QList< QgsRenderedFeatureHandlerInterface * > renderedFeatureHandlers() const; +%Docstring +Returns the list of rendered feature handlers to use while rendering the map settings. + +.. seealso:: :py:func:`addRenderedFeatureHandler` + .. versionadded:: 3.10 %End diff --git a/python/core/auto_generated/qgsrendercontext.sip.in b/python/core/auto_generated/qgsrendercontext.sip.in index 350225dbdc6..3610f0906e5 100644 --- a/python/core/auto_generated/qgsrendercontext.sip.in +++ b/python/core/auto_generated/qgsrendercontext.sip.in @@ -569,6 +569,24 @@ Sets the text render ``format``, which dictates how text is rendered (e.g. as pa .. seealso:: :py:func:`textRenderFormat` .. versionadded:: 3.4.3 +%End + + QList renderedFeatureHandlers() const; +%Docstring +Returns the list of rendered feature handlers to use while rendering map layers. + +.. seealso:: :py:func:`hasRenderedFeatureHandlers` + +.. versionadded:: 3.10 +%End + + bool hasRenderedFeatureHandlers() const; +%Docstring +Returns ``True`` if the context has any rendered feature handlers. + +.. seealso:: :py:func:`renderedFeatureHandlers` + +.. versionadded:: 3.10 %End }; diff --git a/python/core/auto_generated/qgsrenderedfeaturehandlerinterface.sip.in b/python/core/auto_generated/qgsrenderedfeaturehandlerinterface.sip.in new file mode 100644 index 00000000000..ce180b75682 --- /dev/null +++ b/python/core/auto_generated/qgsrenderedfeaturehandlerinterface.sip.in @@ -0,0 +1,68 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/qgsrenderedfeaturehandlerinterface.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + +class QgsRenderedFeatureHandlerInterface +{ +%Docstring + +An interface for classes which provider custom handlers for features rendered +as part of a map render job. + +QgsRenderedFeatureHandlerInterface objects are registered in the QgsMapSettings +objects used to construct map render jobs. During the rendering operation, +the handleRenderedFeature() method will be called once for every rendered feature, +allowing the handler to perform some custom task based on the provided information. + +They can be used for custom tasks which operate on a set of rendered features, +such as creating spatial indexes of the location and rendered symbology bounding box +of all features rendered on a map. + +.. versionadded:: 3.10 +%End + +%TypeHeaderCode +#include "qgsrenderedfeaturehandlerinterface.h" +%End + public: + virtual ~QgsRenderedFeatureHandlerInterface(); + + struct RenderedFeatureContext + { + bool dummy; + }; + + virtual void handleRenderedFeature( const QgsFeature &feature, const QgsGeometry &renderedBounds, const QgsRenderedFeatureHandlerInterface::RenderedFeatureContext &context ) = 0; +%Docstring +Called whenever a ``feature`` is rendered during a map render job. + +The ``renderedBounds`` argument specifies the (approximate) bounds of the rendered feature's +symbology. E.g. for point geometry features, this will be the bounding box of the marker symbol +used to symbolize the point. ``renderedBounds`` geometries are specified in painter units (not +map units). + +.. warning:: + + This method may be called from many different threads (for multi-threaded map render operations), + and accordingly care must be taken to ensure that handleRenderedFeature() implementations are + appropriately thread safe. + +The ``context`` argument is used to provide additional context relating to the rendering of a feature. +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/qgsrenderedfeaturehandlerinterface.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 d93254c7e60..824593a6b11 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -112,6 +112,7 @@ %Include auto_generated/qgsreadwritelocker.sip %Include auto_generated/qgsrenderchecker.sip %Include auto_generated/qgsrendercontext.sip +%Include auto_generated/qgsrenderedfeaturehandlerinterface.sip %Include auto_generated/qgsrulebasedlabeling.sip %Include auto_generated/qgsruntimeprofiler.sip %Include auto_generated/qgsscalecalculator.sip diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index b39d7a58c26..e4d7225b87e 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -994,6 +994,7 @@ SET(QGIS_CORE_HDRS qgsreadwritelocker.h qgsrenderchecker.h qgsrendercontext.h + qgsrenderedfeaturehandlerinterface.h qgsrulebasedlabeling.h qgsruntimeprofiler.h qgsscalecalculator.h diff --git a/src/core/qgsmapsettings.cpp b/src/core/qgsmapsettings.cpp index 190b635a9cc..8ff24ec8cdb 100644 --- a/src/core/qgsmapsettings.cpp +++ b/src/core/qgsmapsettings.cpp @@ -677,3 +677,13 @@ void QgsMapSettings::setLabelBoundaryGeometry( const QgsGeometry &boundary ) { mLabelBoundaryGeometry = boundary; } + +void QgsMapSettings::addRenderedFeatureHandler( QgsRenderedFeatureHandlerInterface *handler ) +{ + mRenderedFeatureHandlers.append( handler ); +} + +QList QgsMapSettings::renderedFeatureHandlers() const +{ + return mRenderedFeatureHandlers; +} diff --git a/src/core/qgsmapsettings.h b/src/core/qgsmapsettings.h index 4a0765b1f62..4355205439f 100644 --- a/src/core/qgsmapsettings.h +++ b/src/core/qgsmapsettings.h @@ -38,6 +38,7 @@ class QPainter; class QgsCoordinateTransform; class QgsScaleCalculator; class QgsMapRendererJob; +class QgsRenderedFeatureHandlerInterface; /** * \class QgsLabelBlockingRegion @@ -578,6 +579,24 @@ class CORE_EXPORT QgsMapSettings */ const QgsVectorSimplifyMethod &simplifyMethod() const { return mSimplifyMethod; } + /** + * Adds a rendered feature \a handler to use while rendering the map settings. + * + * Ownership of \a handler is NOT transferred, and it is the caller's responsibility to ensure + * that the handler exists for the lifetime of the map render job. + * + * \see renderedFeatureHandlers() + * \since QGIS 3.10 + */ + void addRenderedFeatureHandler( QgsRenderedFeatureHandlerInterface *handler ); + + /** + * Returns the list of rendered feature handlers to use while rendering the map settings. + * \see addRenderedFeatureHandler() + * \since QGIS 3.10 + */ + QList< QgsRenderedFeatureHandlerInterface * > renderedFeatureHandlers() const; + protected: double mDpi; @@ -642,6 +661,7 @@ class CORE_EXPORT QgsMapSettings private: QList< QgsLabelBlockingRegion > mLabelBlockingRegions; + QList< QgsRenderedFeatureHandlerInterface * > mRenderedFeatureHandlers; }; Q_DECLARE_OPERATORS_FOR_FLAGS( QgsMapSettings::Flags ) diff --git a/src/core/qgsrendercontext.cpp b/src/core/qgsrendercontext.cpp index e4005f41e16..f17d388dfbe 100644 --- a/src/core/qgsrendercontext.cpp +++ b/src/core/qgsrendercontext.cpp @@ -59,6 +59,8 @@ QgsRenderContext::QgsRenderContext( const QgsRenderContext &rh ) , mTransformContext( rh.mTransformContext ) , mPathResolver( rh.mPathResolver ) , mTextRenderFormat( rh.mTextRenderFormat ) + , mRenderedFeatureHandlers( rh.mRenderedFeatureHandlers ) + , mHasRenderedFeatureHandlers( rh.mHasRenderedFeatureHandlers ) #ifdef QGISDEBUG , mHasTransformContext( rh.mHasTransformContext ) #endif @@ -88,6 +90,8 @@ QgsRenderContext &QgsRenderContext::operator=( const QgsRenderContext &rh ) mTransformContext = rh.mTransformContext; mPathResolver = rh.mPathResolver; mTextRenderFormat = rh.mTextRenderFormat; + mRenderedFeatureHandlers = rh.mRenderedFeatureHandlers; + mHasRenderedFeatureHandlers = rh.mHasRenderedFeatureHandlers; #ifdef QGISDEBUG mHasTransformContext = rh.mHasTransformContext; #endif @@ -185,6 +189,8 @@ QgsRenderContext QgsRenderContext::fromMapSettings( const QgsMapSettings &mapSet ctx.setPathResolver( mapSettings.pathResolver() ); ctx.setTextRenderFormat( mapSettings.textRenderFormat() ); ctx.setVectorSimplifyMethod( mapSettings.simplifyMethod() ); + ctx.mRenderedFeatureHandlers = mapSettings.renderedFeatureHandlers(); + ctx.mHasRenderedFeatureHandlers = !mapSettings.renderedFeatureHandlers().isEmpty(); //this flag is only for stopping during the current rendering progress, //so must be false at every new render operation ctx.setRenderingStopped( false ); @@ -460,4 +466,9 @@ double QgsRenderContext::convertMetersToMapUnits( double meters ) const return meters; } +QList QgsRenderContext::renderedFeatureHandlers() const +{ + return mRenderedFeatureHandlers; +} + diff --git a/src/core/qgsrendercontext.h b/src/core/qgsrendercontext.h index 5ef33cc737f..7ae011d9c26 100644 --- a/src/core/qgsrendercontext.h +++ b/src/core/qgsrendercontext.h @@ -38,6 +38,7 @@ class QPainter; class QgsAbstractGeometry; class QgsLabelingEngine; class QgsMapSettings; +class QgsRenderedFeatureHandlerInterface; /** @@ -595,6 +596,20 @@ class CORE_EXPORT QgsRenderContext mTextRenderFormat = format; } + /** + * Returns the list of rendered feature handlers to use while rendering map layers. + * \see hasRenderedFeatureHandlers() + * \since QGIS 3.10 + */ + QList renderedFeatureHandlers() const; + + /** + * Returns TRUE if the context has any rendered feature handlers. + * \see renderedFeatureHandlers() + * \since QGIS 3.10 + */ + bool hasRenderedFeatureHandlers() const { return mHasRenderedFeatureHandlers; } + private: Flags mFlags; @@ -653,6 +668,8 @@ class CORE_EXPORT QgsRenderContext QgsPathResolver mPathResolver; TextRenderFormat mTextRenderFormat = TextFormatAlwaysOutlines; + QList< QgsRenderedFeatureHandlerInterface * > mRenderedFeatureHandlers; + bool mHasRenderedFeatureHandlers = false; #ifdef QGISDEBUG bool mHasTransformContext = false; diff --git a/src/core/qgsrenderedfeaturehandlerinterface.h b/src/core/qgsrenderedfeaturehandlerinterface.h new file mode 100644 index 00000000000..8f04cce49d5 --- /dev/null +++ b/src/core/qgsrenderedfeaturehandlerinterface.h @@ -0,0 +1,72 @@ +/*************************************************************************** + qgsrenderedfeaturehandlerinterface.h + -------------------------------------- + Date : August 2019 + Copyright : (C) 2019 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 QGSRENDEREDFEATUREHANDLERINTERFACE_H +#define QGSRENDEREDFEATUREHANDLERINTERFACE_H + +#include "qgis_core.h" + +class QgsFeature; +class QgsGeometry; + +/** + * \ingroup core + * + * An interface for classes which provider custom handlers for features rendered + * as part of a map render job. + * + * QgsRenderedFeatureHandlerInterface objects are registered in the QgsMapSettings + * objects used to construct map render jobs. During the rendering operation, + * the handleRenderedFeature() method will be called once for every rendered feature, + * allowing the handler to perform some custom task based on the provided information. + * + * They can be used for custom tasks which operate on a set of rendered features, + * such as creating spatial indexes of the location and rendered symbology bounding box + * of all features rendered on a map. + * + * \since QGIS 3.10 + */ +class CORE_EXPORT QgsRenderedFeatureHandlerInterface +{ + public: + virtual ~QgsRenderedFeatureHandlerInterface() = default; + + struct CORE_EXPORT RenderedFeatureContext + { + ///@cond PRIVATE + // required to allow compilation only until real members are present + bool dummy; + ///@endcond + }; + + /** + * Called whenever a \a feature is rendered during a map render job. + * + * The \a renderedBounds argument specifies the (approximate) bounds of the rendered feature's + * symbology. E.g. for point geometry features, this will be the bounding box of the marker symbol + * used to symbolize the point. \a renderedBounds geometries are specified in painter units (not + * map units). + * + * \warning This method may be called from many different threads (for multi-threaded map render operations), + * and accordingly care must be taken to ensure that handleRenderedFeature() implementations are + * appropriately thread safe. + * + * The \a context argument is used to provide additional context relating to the rendering of a feature. + */ + virtual void handleRenderedFeature( const QgsFeature &feature, const QgsGeometry &renderedBounds, const QgsRenderedFeatureHandlerInterface::RenderedFeatureContext &context ) = 0; + +}; + +#endif // QGSRENDEREDFEATUREHANDLERINTERFACE_H diff --git a/tests/src/core/testqgsmapsettings.cpp b/tests/src/core/testqgsmapsettings.cpp index b00739e0cf5..e8b31ffce08 100644 --- a/tests/src/core/testqgsmapsettings.cpp +++ b/tests/src/core/testqgsmapsettings.cpp @@ -29,6 +29,15 @@ #include "qgsvectorlayer.h" #include "qgscoordinatereferencesystem.h" #include "qgsexpressioncontextutils.h" +#include "qgsrenderedfeaturehandlerinterface.h" + +class TestHandler : public QgsRenderedFeatureHandlerInterface +{ + public: + + void handleRenderedFeature( const QgsFeature &, const QgsGeometry &, const QgsRenderedFeatureHandlerInterface::RenderedFeatureContext & ) override {} + +}; class TestQgsMapSettings: public QObject { @@ -50,6 +59,7 @@ class TestQgsMapSettings: public QObject void testSetLayers(); void testLabelBoundary(); void testExpressionContext(); + void testRenderedFeatureHandlers(); private: QString toString( const QPolygonF &p, int decimalPlaces = 2 ) const; @@ -487,5 +497,21 @@ void TestQgsMapSettings::testExpressionContext() QCOMPARE( r.toString(), QStringLiteral( "WGS84" ) ); } +void TestQgsMapSettings::testRenderedFeatureHandlers() +{ + std::unique_ptr< TestHandler > testHandler = qgis::make_unique< TestHandler >(); + std::unique_ptr< TestHandler > testHandler2 = qgis::make_unique< TestHandler >(); + + std::unique_ptr< QgsMapSettings> mapSettings = qgis::make_unique< QgsMapSettings >(); + QVERIFY( mapSettings->renderedFeatureHandlers().isEmpty() ); + mapSettings->addRenderedFeatureHandler( testHandler.get() ); + mapSettings->addRenderedFeatureHandler( testHandler2.get() ); + QCOMPARE( mapSettings->renderedFeatureHandlers(), QList< QgsRenderedFeatureHandlerInterface * >() << testHandler.get() << testHandler2.get() ); + + //ownership should NOT be transferred, i.e. it won't delete the registered handlers upon QgsMapSettings destruction + mapSettings.reset(); + // should be no double-delete here +} + QGSTEST_MAIN( TestQgsMapSettings ) #include "testqgsmapsettings.moc" diff --git a/tests/src/python/test_qgsrendercontext.py b/tests/src/python/test_qgsrendercontext.py index ed3d6346e2b..b5420ebeb70 100644 --- a/tests/src/python/test_qgsrendercontext.py +++ b/tests/src/python/test_qgsrendercontext.py @@ -21,7 +21,8 @@ from qgis.core import (QgsRenderContext, QgsUnitTypes, QgsProject, QgsRectangle, - QgsVectorSimplifyMethod) + QgsVectorSimplifyMethod, + QgsRenderedFeatureHandlerInterface) from qgis.PyQt.QtCore import QSize from qgis.PyQt.QtGui import QPainter, QImage from qgis.testing import start_app, unittest @@ -32,6 +33,12 @@ import math start_app() +class TestFeatureHandler(QgsRenderedFeatureHandlerInterface): + + def handleRenderedFeature(self, feature, geometry, context): + pass + + class TestQgsRenderContext(unittest.TestCase): def testGettersSetters(self): @@ -135,6 +142,29 @@ class TestQgsRenderContext(unittest.TestCase): rc2 = QgsRenderContext(rc) self.assertEqual(rc2.vectorSimplifyMethod().simplifyHints(), QgsVectorSimplifyMethod.GeometrySimplification) + def testRenderedFeatureHandlers(self): + rc = QgsRenderContext() + self.assertFalse(rc.renderedFeatureHandlers()) + self.assertFalse(rc.hasRenderedFeatureHandlers()) + + ms = QgsMapSettings() + rc = QgsRenderContext.fromMapSettings(ms) + self.assertFalse(rc.renderedFeatureHandlers()) + self.assertFalse(rc.hasRenderedFeatureHandlers()) + + handler = TestFeatureHandler() + handler2 = TestFeatureHandler() + ms.addRenderedFeatureHandler(handler) + ms.addRenderedFeatureHandler(handler2) + + rc = QgsRenderContext.fromMapSettings(ms) + self.assertEqual(rc.renderedFeatureHandlers(), [handler, handler2]) + self.assertTrue(rc.hasRenderedFeatureHandlers()) + + rc2 = QgsRenderContext(rc) + self.assertEqual(rc2.renderedFeatureHandlers(), [handler, handler2]) + self.assertTrue(rc2.hasRenderedFeatureHandlers()) + def testRenderMetersInMapUnits(self): crs_wsg84 = QgsCoordinateReferenceSystem.fromOgcWmsCrs('EPSG:4326')