From f261855490f4350ba350b137b2f91a384c46c64d Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 6 Mar 2023 12:12:22 +1000 Subject: [PATCH] Create QgsMapHitTestTask, a QgsTask which executes a legend hit test in a background thread in a thread-safe way These legend hit tests can be very expensive to calculate, so background execution is desirable... --- .../core/auto_generated/qgsmaphittest.sip.in | 47 +++++++ src/core/qgsmaphittest.cpp | 128 ++++++++++++++++++ src/core/qgsmaphittest.h | 73 ++++++++++ 3 files changed, 248 insertions(+) diff --git a/python/core/auto_generated/qgsmaphittest.sip.in b/python/core/auto_generated/qgsmaphittest.sip.in index ba678f4ec61..aff817bc972 100644 --- a/python/core/auto_generated/qgsmaphittest.sip.in +++ b/python/core/auto_generated/qgsmaphittest.sip.in @@ -76,6 +76,53 @@ Tests whether a given legend key is visible for a specified layer. }; + +class QgsMapHitTestTask : QgsTask +{ +%Docstring(signature="appended") +Executes a QgsMapHitTest in a background thread. + +.. versionadded:: 3.32 +%End + +%TypeHeaderCode +#include "qgsmaphittest.h" +%End + public: + + QgsMapHitTestTask( const QgsMapSettings &settings, const QgsGeometry &polygon = QgsGeometry(), const QgsMapHitTest::LayerFilterExpression &layerFilterExpression = QgsMapHitTest::LayerFilterExpression() ); +%Docstring +Constructor for QgsMapHitTestTask, filtering by a visible geometry. + +:param settings: Map settings used to evaluate symbols +:param polygon: Polygon geometry to refine the hit test +:param layerFilterExpression: Expression string for each layer id to evaluate in order to refine the symbol selection +%End + + QgsMapHitTestTask( const QgsMapSettings &settings, const QgsMapHitTest::LayerFilterExpression &layerFilterExpression ); +%Docstring +Constructor for QgsMapHitTestTask, filtering by expressions. + +:param settings: Map settings used to evaluate symbols +:param layerFilterExpression: Expression string for each layer id to evaluate in order to refine the symbol selection +%End + + QMap> results() const; +%Docstring +Returns the hit test results, which are a map of layer ID to +visible symbol legend keys. +%End + + virtual void cancel(); + + + protected: + + virtual bool run(); + + +}; + /************************************************************************ * This file has been generated automatically from * * * diff --git a/src/core/qgsmaphittest.cpp b/src/core/qgsmaphittest.cpp index 3eb4372261c..5eb65793a9a 100644 --- a/src/core/qgsmaphittest.cpp +++ b/src/core/qgsmaphittest.cpp @@ -267,3 +267,131 @@ void QgsMapHitTest::runHitTestFeatureSource( QgsAbstractFeatureSource *source, r->stopRender( context ); } + +// +// QgsMapHitTestTask +// + +QgsMapHitTestTask::QgsMapHitTestTask( const QgsMapSettings &settings, const QgsGeometry &polygon, const QgsMapHitTest::LayerFilterExpression &layerFilterExpression ) + : QgsTask( tr( "Updating Legend" ), QgsTask::Flag::CanCancel | QgsTask::Flag::CancelWithoutPrompt | QgsTask::Flag::Silent ) + , mSettings( settings ) + , mLayerFilterExpression( layerFilterExpression ) + , mPolygon( polygon ) + , mOnlyExpressions( false ) +{ + prepare(); +} + +QgsMapHitTestTask::QgsMapHitTestTask( const QgsMapSettings &settings, const QgsMapHitTest::LayerFilterExpression &layerFilterExpression ) + : QgsTask( tr( "Updating Legend" ), QgsTask::Flag::CanCancel | QgsTask::Flag::CancelWithoutPrompt | QgsTask::Flag::Silent ) + , mSettings( settings ) + , mLayerFilterExpression( layerFilterExpression ) + , mOnlyExpressions( true ) +{ + prepare(); +} + +QMap > QgsMapHitTestTask::results() const +{ + return mResults; +} + +void QgsMapHitTestTask::prepare() +{ + const QList< QgsMapLayer * > layers = mSettings.layers( true ); + for ( QgsMapLayer *layer : layers ) + { + QgsVectorLayer *vl = qobject_cast( layer ); + if ( !vl || !vl->renderer() ) + continue; + + QgsMapLayerStyleOverride styleOverride( vl ); + if ( mSettings.layerStyleOverrides().contains( vl->id() ) ) + styleOverride.setOverrideStyle( mSettings.layerStyleOverrides().value( vl->id() ) ); + + if ( !mOnlyExpressions ) + { + if ( !vl->isInScaleRange( mSettings.scale() ) ) + { + continue; + } + } + + PreparedLayerData layerData; + layerData.source = std::make_unique< QgsVectorLayerFeatureSource >( vl ); + layerData.layerId = vl->id(); + layerData.crs = vl->crs(); + layerData.fields = vl->fields(); + layerData.renderer.reset( vl->renderer()->clone() ); + layerData.transform = mSettings.layerTransform( vl ); + layerData.extent = mSettings.outputExtentToLayerExtent( vl, mSettings.visibleExtent() ); + layerData.layerScope.reset( QgsExpressionContextUtils::layerScope( vl ) ); + + mPreparedData.emplace_back( std::move( layerData ) ); + } +} + +void QgsMapHitTestTask::cancel() +{ + if ( mFeedback ) + mFeedback->cancel(); + + QgsTask::cancel(); +} + +bool QgsMapHitTestTask::run() +{ + mFeedback = std::make_unique< QgsFeedback >(); + connect( mFeedback.get(), &QgsFeedback::progressChanged, this, &QgsTask::progressChanged ); + + std::unique_ptr< QgsMapHitTest > hitTest; + if ( !mOnlyExpressions ) + hitTest = std::make_unique< QgsMapHitTest >( mSettings, mPolygon, mLayerFilterExpression ); + else + hitTest = std::make_unique< QgsMapHitTest >( mSettings, mLayerFilterExpression ); + + // TODO: do we need this temp image? + QImage tmpImage( mSettings.outputSize(), mSettings.outputImageFormat() ); + tmpImage.setDotsPerMeterX( mSettings.outputDpi() * 25.4 ); + tmpImage.setDotsPerMeterY( mSettings.outputDpi() * 25.4 ); + QPainter painter( &tmpImage ); + + QgsRenderContext context = QgsRenderContext::fromMapSettings( mSettings ); + context.setPainter( &painter ); // we are not going to draw anything, but we still need a working painter + + int layerIdx = 0; + const int totalCount = mPreparedData.size(); + for ( auto &layerData : mPreparedData ) + { + mFeedback->setProgress( static_cast< double >( layerIdx ) / totalCount * 100.0 ); + if ( mFeedback->isCanceled() ) + break; + + QgsMapHitTest::SymbolSet &usedSymbols = hitTest->mHitTest[layerData.layerId]; + QgsMapHitTest::SymbolSet &usedSymbolsRuleKey = hitTest->mHitTestRuleKey[layerData.layerId]; + + context.setCoordinateTransform( layerData.transform ); + context.setExtent( layerData.extent ); + + QgsExpressionContextScope *layerScope = layerData.layerScope.release(); + QgsExpressionContextScopePopper scopePopper( context.expressionContext(), layerScope ); + + hitTest->runHitTestFeatureSource( layerData.source.get(), + layerData.layerId, + layerData.crs, + layerData.fields, + layerData.renderer.get(), + usedSymbols, + usedSymbolsRuleKey, + context, + mFeedback.get() ); + layerIdx++; + } + + mResults = hitTest->mHitTestRuleKey; + + mFeedback.reset(); + + return true; +} + diff --git a/src/core/qgsmaphittest.h b/src/core/qgsmaphittest.h index 740fb812dff..e46ed9f4f2b 100644 --- a/src/core/qgsmaphittest.h +++ b/src/core/qgsmaphittest.h @@ -19,6 +19,8 @@ #include "qgis_sip.h" #include "qgsmapsettings.h" #include "qgsgeometry.h" +#include "qgstaskmanager.h" +#include "qgscoordinatetransform.h" #include @@ -125,6 +127,77 @@ class CORE_EXPORT QgsMapHitTest //! Whether to use only expressions during the filtering bool mOnlyExpressions; + + friend class QgsMapHitTestTask; +}; + + +/** + * \ingroup core + * \brief Executes a QgsMapHitTest in a background thread. + * + * \since QGIS 3.32 + */ +class CORE_EXPORT QgsMapHitTestTask : public QgsTask +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsMapHitTestTask, filtering by a visible geometry. + * + * \param settings Map settings used to evaluate symbols + * \param polygon Polygon geometry to refine the hit test + * \param layerFilterExpression Expression string for each layer id to evaluate in order to refine the symbol selection + */ + QgsMapHitTestTask( const QgsMapSettings &settings, const QgsGeometry &polygon = QgsGeometry(), const QgsMapHitTest::LayerFilterExpression &layerFilterExpression = QgsMapHitTest::LayerFilterExpression() ); + + /** + * Constructor for QgsMapHitTestTask, filtering by expressions. + * + * \param settings Map settings used to evaluate symbols + * \param layerFilterExpression Expression string for each layer id to evaluate in order to refine the symbol selection + */ + QgsMapHitTestTask( const QgsMapSettings &settings, const QgsMapHitTest::LayerFilterExpression &layerFilterExpression ); + + /** + * Returns the hit test results, which are a map of layer ID to + * visible symbol legend keys. + */ + QMap> results() const; + + void cancel() override; + + protected: + + bool run() override; + + private: + + void prepare(); + + struct PreparedLayerData + { + std::unique_ptr< QgsAbstractFeatureSource > source; + QString layerId; + QgsCoordinateReferenceSystem crs; + QgsFields fields; + std::unique_ptr< QgsFeatureRenderer > renderer; + QgsRectangle extent; + QgsCoordinateTransform transform; + std::unique_ptr< QgsExpressionContextScope > layerScope; + }; + + std::vector< PreparedLayerData > mPreparedData; + + QgsMapSettings mSettings; + QgsMapHitTest::LayerFilterExpression mLayerFilterExpression; + QgsGeometry mPolygon; + bool mOnlyExpressions = false; + QMap> mResults; + + std::unique_ptr< QgsFeedback > mFeedback; }; #endif // QGSMAPHITTEST_H