Skip to content

Commit

Permalink
[api][expressions] Allow additional map layer stores to be associated
Browse files Browse the repository at this point in the history
with an expression context

This allows the various expression functions which can refer to
layers to also locate layers stored in these additional,
non-QgsProject::instance, store.

The immediate use case is paving the way for expressions to access
temporary layers created during the execution of processing jobs
(ie model steps). (This is just an API building block toward that
goal)
  • Loading branch information
nyalldawson committed Dec 15, 2022
1 parent 463ef5b commit 769c68b
Show file tree
Hide file tree
Showing 7 changed files with 313 additions and 17 deletions.
2 changes: 1 addition & 1 deletion CMakeLists.txt
Expand Up @@ -1003,7 +1003,7 @@ if (WITH_CORE AND WITH_BINDINGS)
include(SIPMacros)

set(SIP_INCLUDES ${PYQT_SIP_DIR} ${CMAKE_SOURCE_DIR}/python)
set(SIP_CONCAT_PARTS 20)
set(SIP_CONCAT_PARTS 21)

if (NOT BINDINGS_GLOBAL_INSTALL)
set(Python_SITEARCH ${QGIS_DATA_DIR}/python)
Expand Down
30 changes: 30 additions & 0 deletions python/core/auto_generated/qgsexpressioncontext.sip.in
Expand Up @@ -9,6 +9,7 @@




class QgsScopedExpressionFunction : QgsExpressionFunction
{
%Docstring(signature="appended")
Expand Down Expand Up @@ -461,6 +462,28 @@ Removes the passed variable from a list of hidden variables.
.. seealso:: :py:func:`addHiddenVariable`

.. versionadded:: 3.28
%End

void addLayerStore( QgsMapLayerStore *store );
%Docstring
Adds a layer ``store`` to the scope.

Ownership of the ``store`` is not transferred to the scope, it is the caller's
responsibility to ensure that the store remains alive for the duration of the
expression context.

.. seealso:: :py:func:`layerStores`

.. versionadded:: 3.30
%End

QList< QgsMapLayerStore * > layerStores() const;
%Docstring
Returns the list of layer stores associated with the scope.

.. seealso:: :py:func:`addLayerStore`

.. versionadded:: 3.30
%End

};
Expand Down Expand Up @@ -915,6 +938,13 @@ Clears all cached values from the context.
.. seealso:: :py:func:`cachedValue`

.. versionadded:: 2.16
%End

QList< QgsMapLayerStore * > layerStores() const;
%Docstring
Returns the list of layer stores associated with the context.

.. versionadded:: 3.30
%End

void setFeedback( QgsFeedback *feedback );
Expand Down
138 changes: 123 additions & 15 deletions src/core/expression/qgsexpressionutils.cpp
Expand Up @@ -57,7 +57,7 @@ QgsMapLayer *QgsExpressionUtils::getMapLayer( const QVariant &value, const QgsEx
return getMapLayerPrivate( value, context, parent );
}

QgsMapLayer *QgsExpressionUtils::getMapLayerPrivate( const QVariant &value, const QgsExpressionContext *, QgsExpression * )
QgsMapLayer *QgsExpressionUtils::getMapLayerPrivate( const QVariant &value, const QgsExpressionContext *context, QgsExpression * )
{
// First check if we already received a layer pointer
QPointer< QgsMapLayer > ml = value.value< QgsWeakMapLayerPointer >().data();
Expand All @@ -75,6 +75,38 @@ QgsMapLayer *QgsExpressionUtils::getMapLayerPrivate( const QVariant &value, cons
return ml;

const QString identifier = value.toString();

// check through layer stores from context
if ( context )
{
const QList< QgsMapLayerStore * > stores = context->layerStores();
for ( QgsMapLayerStore *store : stores )
{
QPointer< QgsMapLayerStore > storePointer( store );
auto findLayerInStoreFunction = [ storePointer, &ml, identifier ]
{
if ( QgsMapLayerStore *store = storePointer.data() )
{
// look for matching layer by id
ml = store->mapLayer( identifier );
return;

// Still nothing? Check for layer name
ml = store->mapLayersByName( identifier ).value( 0 );
}
};

// Make sure we only deal with the store on the thread where it lives.
// Anything else risks a crash.
if ( QThread::currentThread() == store->thread() )
findLayerInStoreFunction();
else
QMetaObject::invokeMethod( store, findLayerInStoreFunction, Qt::BlockingQueuedConnection );
if ( ml )
return ml;
}
}

// last resort - QgsProject instance. This is bad, we need to remove this!
auto getMapLayerFromProjectInstance = [ &ml, identifier ]
{
Expand Down Expand Up @@ -136,26 +168,102 @@ void QgsExpressionUtils::executeLambdaForMapLayer( const QVariant &value, const
return;
}

// if no layer stores, then this is only for layers in project and therefore associated with the main thread
auto runFunction = [ value, context, expression, &function, &foundLayer ]
if ( context->layerStores().empty() )
{
if ( QgsMapLayer *layer = getMapLayerPrivate( value, context, expression ) )
// if no layer stores, then this is only for layers in project and therefore associated with the main thread
auto runFunction = [ value, context, expression, &function, &foundLayer ]
{
foundLayer = true;
function( layer );
}
if ( QgsMapLayer *layer = getMapLayerPrivate( value, context, expression ) )
{
foundLayer = true;
function( layer );
}
else
{
foundLayer = false;
}
};

// Make sure we only deal with the project on the thread where it lives.
// Anything else risks a crash.
if ( QThread::currentThread() == QgsProject::instance()->thread() )
runFunction();
else
QMetaObject::invokeMethod( QgsProject::instance(), runFunction, Qt::BlockingQueuedConnection );
}
else
{
// if layer stores, then we can't be certain in advance of which thread the layer will have affinity with.
// So we need to fetch the layer and then run the function on the layer's thread.

const QString identifier = value.toString();
// check through layer stores from context
if ( context )
{
foundLayer = false;
const QList< QgsMapLayerStore * > stores = context->layerStores();

for ( QgsMapLayerStore *store : stores )
{
QPointer< QgsMapLayerStore > storePointer( store );
auto findLayerInStoreFunction = [ storePointer, identifier, function, &foundLayer ]
{
QgsMapLayer *ml = nullptr;
if ( QgsMapLayerStore *store = storePointer.data() )
{
// look for matching layer by id
ml = store->mapLayer( identifier );
if ( !ml )
{
// Still nothing? Check for layer name
ml = store->mapLayersByName( identifier ).value( 0 );
}

if ( ml )
{
function( ml );
foundLayer = true;
}
}
};

// Make sure we only deal with the store on the thread where it lives.
// Anything else risks a crash.
if ( QThread::currentThread() == store->thread() )
findLayerInStoreFunction();
else
QMetaObject::invokeMethod( store, findLayerInStoreFunction, Qt::BlockingQueuedConnection );

if ( foundLayer )
return;
}
}
};

// Make sure we only deal with the layer on the thread where it lives.
// Anything else risks a crash.
if ( QThread::currentThread() == QgsProject::instance()->thread() )
runFunction();
else
QMetaObject::invokeMethod( QgsProject::instance(), runFunction, Qt::BlockingQueuedConnection );
// last resort - QgsProject instance. This is bad, we need to remove this!
auto getMapLayerFromProjectInstance = [ value, &ml, identifier, &function, &foundLayer ]
{
QgsProject *project = QgsProject::instance();

// maybe it's a layer id?
QgsMapLayer *ml = project->mapLayer( identifier );

// Still nothing? Check for layer name
if ( !ml )
{
ml = project->mapLayersByName( identifier ).value( 0 );
}

if ( ml )
{
foundLayer = true;
function( ml );
}
};

if ( QThread::currentThread() == QgsProject::instance()->thread() )
getMapLayerFromProjectInstance();
else
QMetaObject::invokeMethod( QgsProject::instance(), getMapLayerFromProjectInstance, Qt::BlockingQueuedConnection );
}
}

QVariant QgsExpressionUtils::runMapLayerFunctionThreadSafe( const QVariant &value, const QgsExpressionContext *context, QgsExpression *expression, const std::function<QVariant( QgsMapLayer * )> &function, bool &foundLayer )
Expand Down
31 changes: 30 additions & 1 deletion src/core/qgsexpressioncontext.cpp
Expand Up @@ -14,9 +14,9 @@
***************************************************************************/

#include "qgsexpressioncontext.h"
#include "qgslogger.h"
#include "qgsxmlutils.h"
#include "qgsexpression.h"
#include "qgsmaplayerstore.h"

const QString QgsExpressionContext::EXPR_FIELDS( QStringLiteral( "_fields_" ) );
const QString QgsExpressionContext::EXPR_ORIGINAL_VALUE( QStringLiteral( "value" ) );
Expand Down Expand Up @@ -152,6 +152,22 @@ void QgsExpressionContextScope::removeHiddenVariable( const QString &hiddenVaria
mHiddenVariables.removeAt( mHiddenVariables.indexOf( hiddenVariable ) );
}

void QgsExpressionContextScope::addLayerStore( QgsMapLayerStore *store )
{
mLayerStores.append( store );
}

QList<QgsMapLayerStore *> QgsExpressionContextScope::layerStores() const
{
QList<QgsMapLayerStore *> res;
res.reserve( mLayerStores.size() );
for ( QgsMapLayerStore *store : std::as_const( mLayerStores ) )
{
if ( store )
res << store;
}
return res;
}

/// @cond PRIVATE
class QgsExpressionContextVariableCompare
Expand Down Expand Up @@ -688,6 +704,19 @@ void QgsExpressionContext::clearCachedValues() const
mCachedValues.clear();
}

QList<QgsMapLayerStore *> QgsExpressionContext::layerStores() const
{
//iterate through stack backwards, so that higher priority layer stores take precedence
QList< QgsExpressionContextScope * >::const_iterator it = mStack.constEnd();
QList<QgsMapLayerStore *> res;
while ( it != mStack.constBegin() )
{
--it;
res.append( ( *it )->layerStores() );
}
return res;
}

void QgsExpressionContext::setFeedback( QgsFeedback *feedback )
{
mFeedback = feedback;
Expand Down
32 changes: 32 additions & 0 deletions src/core/qgsexpressioncontext.h
Expand Up @@ -22,10 +22,13 @@
#include <QString>
#include <QStringList>
#include <QSet>
#include <QPointer>

#include "qgsexpressionfunction.h"
#include "qgsfeature.h"

class QgsReadWriteContext;
class QgsMapLayerStore;

/**
* \ingroup core
Expand Down Expand Up @@ -427,6 +430,26 @@ class CORE_EXPORT QgsExpressionContextScope
*/
void removeHiddenVariable( const QString &hiddenVariable );

/**
* Adds a layer \a store to the scope.
*
* Ownership of the \a store is not transferred to the scope, it is the caller's
* responsibility to ensure that the store remains alive for the duration of the
* expression context.
*
* \see layerStores()
* \since QGIS 3.30
*/
void addLayerStore( QgsMapLayerStore *store );

/**
* Returns the list of layer stores associated with the scope.
*
* \see addLayerStore()
* \since QGIS 3.30
*/
QList< QgsMapLayerStore * > layerStores() const;

private:
QString mName;
QHash<QString, StaticVariable> mVariables;
Expand All @@ -436,6 +459,8 @@ class CORE_EXPORT QgsExpressionContextScope
bool mHasGeometry = false;
QgsGeometry mGeometry;
QStringList mHiddenVariables;

QList< QPointer< QgsMapLayerStore > > mLayerStores;
};

/**
Expand Down Expand Up @@ -824,6 +849,13 @@ class CORE_EXPORT QgsExpressionContext
*/
void clearCachedValues() const;

/**
* Returns the list of layer stores associated with the context.
*
* \since QGIS 3.30
*/
QList< QgsMapLayerStore * > layerStores() const;

/**
* Attach a \a feedback object that can be queried regularly by the expression engine to check
* if expression evaluation should be canceled.
Expand Down

0 comments on commit 769c68b

Please sign in to comment.