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...
This commit is contained in:
Nyall Dawson 2023-03-06 12:12:22 +10:00
parent 11f5a6947a
commit f261855490
3 changed files with 248 additions and 0 deletions

View File

@ -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<QString, QSet<QString>> 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 *
* *

View File

@ -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<QString, QSet<QString> > QgsMapHitTestTask::results() const
{
return mResults;
}
void QgsMapHitTestTask::prepare()
{
const QList< QgsMapLayer * > layers = mSettings.layers( true );
for ( QgsMapLayer *layer : layers )
{
QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( 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;
}

View File

@ -19,6 +19,8 @@
#include "qgis_sip.h"
#include "qgsmapsettings.h"
#include "qgsgeometry.h"
#include "qgstaskmanager.h"
#include "qgscoordinatetransform.h"
#include <QSet>
@ -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<QString, QSet<QString>> 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<QString, QSet<QString>> mResults;
std::unique_ptr< QgsFeedback > mFeedback;
};
#endif // QGSMAPHITTEST_H