[feature] New function "load_layer"

This function (available only in Processing expressions for now),
allows loading a map layer via a source string and provider name.

It is designed to allow use of the expression functions which
directly reference map layers (such as the aggregate functions)
with a hardcoded layer path, eg. then permitting these functions
to be used outside of a project (such as via the qgis_process tool)
This commit is contained in:
Nyall Dawson 2022-12-22 07:22:43 +10:00
parent 3cca02bb5d
commit b23eb1d250
7 changed files with 232 additions and 3 deletions

View File

@ -388,6 +388,7 @@ Creates a new scope which contains functions relating to mesh layer element ``el
}; };
/************************************************************************ /************************************************************************
* This file has been generated automatically from * * This file has been generated automatically from *
* * * *

View File

@ -0,0 +1,19 @@
{
"name": "load_layer",
"type": "function",
"groups": ["Map Layers"],
"description": "Loads a layer by source URI and provider name.",
"arguments": [{
"arg": "uri",
"description": "layer source URI string"
},
{
"arg": "provider",
"description": "layer data provider name"
}],
"examples": [{
"expression": "layer_property(load_layer('c:/data/roads.shp', 'ogr'), 'feature_count')",
"returns": "count of features from the c:/data/roads.shp vector layer"
}],
"tags": ["layer", "vector", "raster", "mesh", "point", "cloud"]
}

View File

@ -35,6 +35,9 @@
#include "qgstriangularmesh.h" #include "qgstriangularmesh.h"
#include "qgsvectortileutils.h" #include "qgsvectortileutils.h"
#include "qgsmeshlayer.h" #include "qgsmeshlayer.h"
#include "qgsexpressionnodeimpl.h"
#include "qgsproviderregistry.h"
#include "qgsmaplayerfactory.h"
QgsExpressionContextScope *QgsExpressionContextUtils::globalScope() QgsExpressionContextScope *QgsExpressionContextUtils::globalScope()
{ {
@ -917,6 +920,7 @@ void QgsExpressionContextUtils::registerContextFunctions()
QgsExpression::registerFunction( new GetProcessingParameterValue( QVariantMap() ) ); QgsExpression::registerFunction( new GetProcessingParameterValue( QVariantMap() ) );
QgsExpression::registerFunction( new GetCurrentFormFieldValue( ) ); QgsExpression::registerFunction( new GetCurrentFormFieldValue( ) );
QgsExpression::registerFunction( new GetCurrentParentFormFieldValue( ) ); QgsExpression::registerFunction( new GetCurrentParentFormFieldValue( ) );
QgsExpression::registerFunction( new LoadLayerFunction( ) );
} }
bool QgsScopedExpressionFunction::usesGeometry( const QgsExpressionNodeFunction *node ) const bool QgsScopedExpressionFunction::usesGeometry( const QgsExpressionNodeFunction *node ) const
@ -1296,4 +1300,110 @@ QgsExpressionContextScope *QgsExpressionContextUtils::meshExpressionScope( QgsMe
return scope.release(); return scope.release();
} }
QVariant LoadLayerFunction::func( const QVariantList &, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * )
{
parent->setEvalErrorString( QObject::tr( "Invalid arguments for load_layer function" ) );
return QVariant();
}
bool LoadLayerFunction::isStatic( const QgsExpressionNodeFunction *node, QgsExpression *parent, const QgsExpressionContext *context ) const
{
if ( node->args()->count() > 1 )
{
if ( !context )
return false;
QPointer< QgsMapLayerStore > store( context->loadedLayerStore() );
if ( !store )
{
parent->setEvalErrorString( QObject::tr( "load_layer cannot be used in this context" ) );
return false;
}
QgsExpressionNode *uriNode = node->args()->at( 0 );
QgsExpressionNode *providerNode = node->args()->at( 1 );
if ( !uriNode->isStatic( parent, context ) )
{
parent->setEvalErrorString( QObject::tr( "load_layer requires a static value for the uri argument" ) );
return false;
}
if ( !providerNode->isStatic( parent, context ) )
{
parent->setEvalErrorString( QObject::tr( "load_layer requires a static value for the provider argument" ) );
return false;
}
const QString uri = uriNode->eval( parent, context ).toString();
if ( uri.isEmpty() )
{
parent->setEvalErrorString( QObject::tr( "Invalid uri argument for load_layer" ) );
return false;
}
const QString providerKey = providerNode->eval( parent, context ).toString();
if ( providerKey.isEmpty() )
{
parent->setEvalErrorString( QObject::tr( "Invalid provider argument for load_layer" ) );
return false;
}
const QgsCoordinateTransformContext transformContext = context->variable( QStringLiteral( "_project_transform_context" ) ).value<QgsCoordinateTransformContext>();
bool res = false;
auto loadLayer = [ uri, providerKey, store, node, parent, &res, &transformContext ]
{
QgsProviderMetadata *metadata = QgsProviderRegistry::instance()->providerMetadata( providerKey );
if ( !metadata )
{
parent->setEvalErrorString( QObject::tr( "Invalid provider argument for load_layer" ) );
return;
}
if ( metadata->supportedLayerTypes().empty() )
{
parent->setEvalErrorString( QObject::tr( "Cannot use %1 provider for load_layer" ).arg( providerKey ) );
return;
}
QgsMapLayerFactory::LayerOptions layerOptions( transformContext );
layerOptions.loadAllStoredStyles = false;
layerOptions.loadDefaultStyle = false;
QgsMapLayer *layer = QgsMapLayerFactory::createLayer( uri, uri, metadata->supportedLayerTypes().value( 0 ), layerOptions, providerKey );
if ( !layer )
{
parent->setEvalErrorString( QObject::tr( "Could not load_layer with uri: %1" ).arg( uri ) );
return;
}
if ( !layer->isValid() )
{
delete layer;
parent->setEvalErrorString( QObject::tr( "Could not load_layer with uri: %1" ).arg( uri ) );
return;
}
store->addMapLayer( layer );
node->setCachedStaticValue( QVariant::fromValue( QgsWeakMapLayerPointer( layer ) ) );
res = true;
};
// Make sure we load the layer on the thread where the store lives
if ( QThread::currentThread() == store->thread() )
loadLayer();
else
QMetaObject::invokeMethod( store, loadLayer, Qt::BlockingQueuedConnection );
return res;
}
return false;
}
QgsScopedExpressionFunction *LoadLayerFunction::clone() const
{
return new LoadLayerFunction();
}
///@endcond ///@endcond

View File

@ -359,6 +359,24 @@ class CORE_EXPORT QgsExpressionContextUtils
}; };
///@cond PRIVATE
#ifndef SIP_RUN
class LoadLayerFunction : public QgsScopedExpressionFunction
{
public:
LoadLayerFunction()
: QgsScopedExpressionFunction( QStringLiteral( "load_layer" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "uri" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "provider" ) ), QStringLiteral( "Map Layers" ) )
{}
QVariant func( const QVariantList &, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * ) override;
bool isStatic( const QgsExpressionNodeFunction *node, QgsExpression *parent, const QgsExpressionContext *context ) const override;
QgsScopedExpressionFunction *clone() const override;
};
#endif
///@endcond
#ifndef SIP_RUN #ifndef SIP_RUN
/** /**

View File

@ -17,6 +17,7 @@
#include "qgsxmlutils.h" #include "qgsxmlutils.h"
#include "qgsexpression.h" #include "qgsexpression.h"
#include "qgsmaplayerstore.h" #include "qgsmaplayerstore.h"
#include "qgsexpressioncontextutils.h"
const QString QgsExpressionContext::EXPR_FIELDS( QStringLiteral( "_fields_" ) ); const QString QgsExpressionContext::EXPR_FIELDS( QStringLiteral( "_fields_" ) );
const QString QgsExpressionContext::EXPR_ORIGINAL_VALUE( QStringLiteral( "value" ) ); const QString QgsExpressionContext::EXPR_ORIGINAL_VALUE( QStringLiteral( "value" ) );
@ -295,9 +296,15 @@ bool QgsExpressionContextScope::writeXml( QDomElement &element, QDomDocument &do
// QgsExpressionContext // QgsExpressionContext
// //
QgsExpressionContext::QgsExpressionContext()
{
mLoadLayerFunction = std::make_unique< LoadLayerFunction >();
}
QgsExpressionContext::QgsExpressionContext( const QList<QgsExpressionContextScope *> &scopes ) QgsExpressionContext::QgsExpressionContext( const QList<QgsExpressionContextScope *> &scopes )
: mStack( scopes ) : mStack( scopes )
{ {
mLoadLayerFunction = std::make_unique< LoadLayerFunction >();
} }
QgsExpressionContext::QgsExpressionContext( const QgsExpressionContext &other ) : mStack{} QgsExpressionContext::QgsExpressionContext( const QgsExpressionContext &other ) : mStack{}
@ -311,6 +318,7 @@ QgsExpressionContext::QgsExpressionContext( const QgsExpressionContext &other )
mCachedValues = other.mCachedValues; mCachedValues = other.mCachedValues;
mFeedback = other.mFeedback; mFeedback = other.mFeedback;
mDestinationStore = other.mDestinationStore; mDestinationStore = other.mDestinationStore;
mLoadLayerFunction = std::make_unique< LoadLayerFunction >();
} }
QgsExpressionContext &QgsExpressionContext::operator=( QgsExpressionContext &&other ) noexcept QgsExpressionContext &QgsExpressionContext::operator=( QgsExpressionContext &&other ) noexcept
@ -533,8 +541,10 @@ QString QgsExpressionContext::description( const QString &name ) const
bool QgsExpressionContext::hasFunction( const QString &name ) const bool QgsExpressionContext::hasFunction( const QString &name ) const
{ {
const auto constMStack = mStack; if ( name.compare( QLatin1String( "load_layer" ) ) == 0 && mDestinationStore )
for ( const QgsExpressionContextScope *scope : constMStack ) return true;
for ( const QgsExpressionContextScope *scope : mStack )
{ {
if ( scope->hasFunction( name ) ) if ( scope->hasFunction( name ) )
return true; return true;
@ -551,6 +561,10 @@ QStringList QgsExpressionContext::functionNames() const
for ( const QString &name : functionNames ) for ( const QString &name : functionNames )
result.insert( name ); result.insert( name );
} }
if ( mDestinationStore )
result.insert( QStringLiteral( "load_layer" ) );
QStringList listResult( result.constBegin(), result.constEnd() ); QStringList listResult( result.constBegin(), result.constEnd() );
listResult.sort(); listResult.sort();
return listResult; return listResult;
@ -558,6 +572,11 @@ QStringList QgsExpressionContext::functionNames() const
QgsExpressionFunction *QgsExpressionContext::function( const QString &name ) const QgsExpressionFunction *QgsExpressionContext::function( const QString &name ) const
{ {
if ( name.compare( QLatin1String( "load_layer" ) ) == 0 && mDestinationStore )
{
return mLoadLayerFunction.get();
}
//iterate through stack backwards, so that higher priority variables take precedence //iterate through stack backwards, so that higher priority variables take precedence
QList< QgsExpressionContextScope * >::const_iterator it = mStack.constEnd(); QList< QgsExpressionContextScope * >::const_iterator it = mStack.constEnd();
while ( it != mStack.constBegin() ) while ( it != mStack.constBegin() )

View File

@ -29,6 +29,7 @@
class QgsReadWriteContext; class QgsReadWriteContext;
class QgsMapLayerStore; class QgsMapLayerStore;
class LoadLayerFunction;
/** /**
* \ingroup core * \ingroup core
@ -481,7 +482,7 @@ class CORE_EXPORT QgsExpressionContext
public: public:
//! Constructor for QgsExpressionContext //! Constructor for QgsExpressionContext
QgsExpressionContext() = default; QgsExpressionContext();
/** /**
* Initializes the context with given list of scopes. * Initializes the context with given list of scopes.
@ -936,6 +937,7 @@ class CORE_EXPORT QgsExpressionContext
QgsFeedback *mFeedback = nullptr; QgsFeedback *mFeedback = nullptr;
std::unique_ptr< LoadLayerFunction > mLoadLayerFunction;
QPointer< QgsMapLayerStore > mDestinationStore; QPointer< QgsMapLayerStore > mDestinationStore;
// Cache is mutable because we want to be able to add cached values to const contexts // Cache is mutable because we want to be able to add cached values to const contexts

View File

@ -5650,6 +5650,66 @@ class TestQgsExpression: public QObject
QCOMPARE( QgsExpressionUtils::toLocalizedString( QString( "hello world" ) ), QStringLiteral( "hello world" ) ); QCOMPARE( QgsExpressionUtils::toLocalizedString( QString( "hello world" ) ), QStringLiteral( "hello world" ) );
} }
void testLoadLayer()
{
QgsExpressionContext context;
QgsMapLayerStore store;
// load_layer is only available when a destination store is set
QVERIFY( !context.hasFunction( QStringLiteral( "load_layer" ) ) );
QVERIFY( !context.functionNames().contains( QStringLiteral( "load_layer" ) ) );
QVERIFY( !context.function( QStringLiteral( "load_layer" ) ) );
context.setLoadedLayerStore( &store );
QVERIFY( context.hasFunction( QStringLiteral( "load_layer" ) ) );
QVERIFY( context.functionNames().contains( QStringLiteral( "load_layer" ) ) );
QVERIFY( context.function( QStringLiteral( "load_layer" ) ) );
const QString pointsFileName = QStringLiteral( TEST_DATA_DIR ) + '/' + "points.shp";
QgsExpression exp( QStringLiteral( "layer_property(load_layer('%1', 'ogr'), 'feature_count')" ).arg( pointsFileName ) );
QVERIFY( exp.prepare( &context ) );
QVERIFY( !exp.hasEvalError() );
QCOMPARE( exp.evaluate( &context ).toInt(), 17 );
// non-static arguments are not allowed
QgsFields fields;
fields.append( QgsField( QStringLiteral( "first_field" ), QVariant::Int ) );
QgsFeature f( fields );
f.setAttributes( QgsAttributes() << 11 );
context.setFields( fields );
context.setFeature( f );
exp = QgsExpression( QStringLiteral( "layer_property(load_layer('%1' || \"first_field\", 'ogr'), 'feature_count')" ).arg( pointsFileName ) );
QVERIFY( exp.prepare( &context ) );
QVERIFY( exp.hasEvalError() );
QCOMPARE( exp.evalErrorString(), QStringLiteral( "load_layer requires a static value for the uri argument" ) );
exp = QgsExpression( QStringLiteral( "layer_property(load_layer('%1', 'ogr' || \"first_field\"), 'feature_count')" ).arg( pointsFileName ) );
QVERIFY( exp.prepare( &context ) );
QVERIFY( exp.hasEvalError() );
QCOMPARE( exp.evalErrorString(), QStringLiteral( "load_layer requires a static value for the provider argument" ) );
// invalid provider
exp = QgsExpression( QStringLiteral( "layer_property(load_layer('%1', 'magic'), 'feature_count')" ).arg( pointsFileName ) );
QVERIFY( exp.prepare( &context ) );
QVERIFY( exp.hasEvalError() );
QCOMPARE( exp.evalErrorString(), QStringLiteral( "Invalid provider argument for load_layer" ) );
// invalid uri
exp = QgsExpression( QStringLiteral( "layer_property(load_layer('nope', 'ogr'), 'feature_count')" ) );
QVERIFY( exp.prepare( &context ) );
QVERIFY( exp.hasEvalError() );
QCOMPARE( exp.evalErrorString(), QStringLiteral( "Could not load_layer with uri: nope" ) );
// raster layer
const QString rasterFileName = QStringLiteral( TEST_DATA_DIR ) + '/' + "tenbytenraster.asc";
exp = QgsExpression( QStringLiteral( "layer_property(load_layer('%1', 'gdal'), 'type')" ).arg( rasterFileName ) );
QVERIFY( exp.prepare( &context ) );
QVERIFY( !exp.hasEvalError() );
QCOMPARE( exp.evaluate( &context ).toString(), QStringLiteral( "Raster" ) );
}
}; };
QGSTEST_MAIN( TestQgsExpression ) QGSTEST_MAIN( TestQgsExpression )