Allow geometries to be set separate to features in expression contexts

Refs #46455 -- we need a way to separate these too, as we don't always
want $geometry to refer to a feature's geometry
This commit is contained in:
Nyall Dawson 2022-02-17 13:43:48 +10:00
parent a8eaf46995
commit 11a8d40a86
4 changed files with 197 additions and 2 deletions

View File

@ -333,6 +333,49 @@ Removes any feature associated with the scope.
.. seealso:: :py:func:`hasFeature`
.. versionadded:: 3.0
%End
bool hasGeometry() const;
%Docstring
Returns ``True`` if the scope has a geometry associated with it.
.. seealso:: :py:func:`geometry`
.. versionadded:: 3.24
%End
QgsGeometry geometry() const;
%Docstring
Sets the geometry associated with the scope.
.. seealso:: :py:func:`setGeometry`
.. seealso:: :py:func:`hasGeometry`
.. versionadded:: 3.24
%End
void setGeometry( const QgsGeometry &geometry );
%Docstring
Convenience function for setting a ``geometry`` for the scope. Any existing
geometry set by the scope will be overwritten.
.. seealso:: :py:func:`removeGeometry`
.. seealso:: :py:func:`geometry`
.. versionadded:: 3.24
%End
void removeGeometry();
%Docstring
Removes any geometry associated with the scope.
.. seealso:: :py:func:`setGeometry`
.. seealso:: :py:func:`hasGeometry`
.. versionadded:: 3.24
%End
void setFields( const QgsFields &fields );
@ -692,6 +735,35 @@ Returns ``True`` if the context has a feature associated with it.
Convenience function for retrieving the feature for the context, if set.
.. seealso:: :py:func:`setFeature`
%End
void setGeometry( const QgsGeometry &geometry );
%Docstring
Convenience function for setting a ``geometry`` for the context. The geometry
will be set within the last scope of the context, so will override any
existing geometries within the context.
.. seealso:: :py:func:`geometry`
.. versionadded:: 3.24
%End
bool hasGeometry() const;
%Docstring
Returns ``True`` if the context has a geometry associated with it.
.. seealso:: :py:func:`geometry`
.. versionadded:: 3.24
%End
QgsGeometry geometry() const;
%Docstring
Convenience function for retrieving the geometry for the context, if set.
.. seealso:: :py:func:`setGeometry`
.. versionadded:: 3.24
%End
void setFields( const QgsFields &fields );

View File

@ -45,6 +45,8 @@ QgsExpressionContextScope::QgsExpressionContextScope( const QgsExpressionContext
, mVariables( other.mVariables )
, mHasFeature( other.mHasFeature )
, mFeature( other.mFeature )
, mHasGeometry( other.mHasGeometry )
, mGeometry( other.mGeometry )
{
QHash<QString, QgsScopedExpressionFunction * >::const_iterator it = other.mFunctions.constBegin();
for ( ; it != other.mFunctions.constEnd(); ++it )
@ -59,6 +61,8 @@ QgsExpressionContextScope &QgsExpressionContextScope::operator=( const QgsExpres
mVariables = other.mVariables;
mHasFeature = other.mHasFeature;
mFeature = other.mFeature;
mHasGeometry = other.mHasGeometry;
mGeometry = other.mGeometry;
qDeleteAll( mFunctions );
mFunctions.clear();
@ -529,8 +533,7 @@ void QgsExpressionContext::setFeature( const QgsFeature &feature )
bool QgsExpressionContext::hasFeature() const
{
const auto constMStack = mStack;
for ( const QgsExpressionContextScope *scope : constMStack )
for ( const QgsExpressionContextScope *scope : mStack )
{
if ( scope->hasFeature() )
return true;
@ -551,6 +554,37 @@ QgsFeature QgsExpressionContext::feature() const
return QgsFeature();
}
void QgsExpressionContext::setGeometry( const QgsGeometry &geometry )
{
if ( mStack.isEmpty() )
mStack.append( new QgsExpressionContextScope() );
mStack.last()->setGeometry( geometry );
}
bool QgsExpressionContext::hasGeometry() const
{
for ( const QgsExpressionContextScope *scope : mStack )
{
if ( scope->hasGeometry() )
return true;
}
return false;
}
QgsGeometry QgsExpressionContext::geometry() const
{
//iterate through stack backwards, so that higher priority variables take precedence
QList< QgsExpressionContextScope * >::const_iterator it = mStack.constEnd();
while ( it != mStack.constBegin() )
{
--it;
if ( ( *it )->hasGeometry() )
return ( *it )->geometry();
}
return QgsGeometry();
}
void QgsExpressionContext::setFields( const QgsFields &fields )
{
if ( mStack.isEmpty() )

View File

@ -326,6 +326,39 @@ class CORE_EXPORT QgsExpressionContextScope
*/
void removeFeature() { mHasFeature = false; mFeature = QgsFeature(); }
/**
* Returns TRUE if the scope has a geometry associated with it.
* \see geometry()
* \since QGIS 3.24
*/
bool hasGeometry() const { return mHasGeometry; }
/**
* Sets the geometry associated with the scope.
* \see setGeometry()
* \see hasGeometry()
* \since QGIS 3.24
*/
QgsGeometry geometry() const { return mGeometry; }
/**
* Convenience function for setting a \a geometry for the scope. Any existing
* geometry set by the scope will be overwritten.
* \see removeGeometry()
* \see geometry()
* \since QGIS 3.24
*/
void setGeometry( const QgsGeometry &geometry ) { mHasGeometry = true; mGeometry = geometry; }
/**
* Removes any geometry associated with the scope.
* \see setGeometry()
* \see hasGeometry()
* \since QGIS 3.24
*/
void removeGeometry() { mHasGeometry = false; mGeometry = QgsGeometry(); }
/**
* Convenience function for setting a fields for the scope. Any existing
* fields set by the scope will be overwritten.
@ -353,6 +386,8 @@ class CORE_EXPORT QgsExpressionContextScope
QHash<QString, QgsScopedExpressionFunction * > mFunctions;
bool mHasFeature = false;
QgsFeature mFeature;
bool mHasGeometry = false;
QgsGeometry mGeometry;
};
/**
@ -650,6 +685,30 @@ class CORE_EXPORT QgsExpressionContext
*/
QgsFeature feature() const;
/**
* Convenience function for setting a \a geometry for the context. The geometry
* will be set within the last scope of the context, so will override any
* existing geometries within the context.
* \see geometry()
* \since QGIS 3.24
*/
void setGeometry( const QgsGeometry &geometry );
/**
* Returns TRUE if the context has a geometry associated with it.
* \see geometry()
* \since QGIS 3.24
*/
bool hasGeometry() const;
/**
* Convenience function for retrieving the geometry for the context, if set.
* \see setGeometry()
* \since QGIS 3.24
*/
QgsGeometry geometry() const;
/**
* Convenience function for setting a fields for the context. The fields
* will be set within the last scope of the context, so will override any

View File

@ -44,6 +44,7 @@ class TestQgsExpressionContext : public QObject
void contextStackFunctions();
void evaluate();
void setFeature();
void setGeometry();
void setFields();
void takeScopes();
void highlighted();
@ -486,6 +487,35 @@ void TestQgsExpressionContext::setFeature()
QCOMPARE( contextWithScope.feature().id(), 50LL );
}
void TestQgsExpressionContext::setGeometry()
{
QgsGeometry g( QgsGeometry::fromPointXY( QgsPointXY( 1, 2 ) ) );
QgsExpressionContextScope scope;
scope.setGeometry( g );
QVERIFY( scope.hasGeometry() );
QCOMPARE( scope.geometry().asWkt(), QStringLiteral( "Point (1 2)" ) );
scope.removeGeometry();
QVERIFY( !scope.hasGeometry() );
QVERIFY( scope.geometry().isNull() );
//test setting a geometry in a context with no scopes
QgsExpressionContext emptyContext;
QVERIFY( !emptyContext.hasGeometry() );
QVERIFY( emptyContext.geometry().isNull() );
emptyContext.setGeometry( g );
//setGeometry should have created a scope
QCOMPARE( emptyContext.scopeCount(), 1 );
QVERIFY( emptyContext.hasGeometry() );
QCOMPARE( emptyContext.geometry().asWkt(), QStringLiteral( "Point (1 2)" ) );
QgsExpressionContext contextWithScope;
contextWithScope << new QgsExpressionContextScope();
contextWithScope.setGeometry( g );
QCOMPARE( contextWithScope.scopeCount(), 1 );
QVERIFY( contextWithScope.hasGeometry() );
QCOMPARE( contextWithScope.geometry().asWkt(), QStringLiteral( "Point (1 2)" ) );
}
void TestQgsExpressionContext::setFields()
{
QgsFields fields;