From b23eb1d250ebf6cc9b27a7fc974efcf8f441a473 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 22 Dec 2022 07:22:43 +1000 Subject: [PATCH] [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) --- .../qgsexpressioncontextutils.sip.in | 1 + resources/function_help/json/load_layer | 19 +++ .../expression/qgsexpressioncontextutils.cpp | 110 ++++++++++++++++++ .../expression/qgsexpressioncontextutils.h | 18 +++ src/core/qgsexpressioncontext.cpp | 23 +++- src/core/qgsexpressioncontext.h | 4 +- tests/src/core/testqgsexpression.cpp | 60 ++++++++++ 7 files changed, 232 insertions(+), 3 deletions(-) create mode 100644 resources/function_help/json/load_layer diff --git a/python/core/auto_generated/expression/qgsexpressioncontextutils.sip.in b/python/core/auto_generated/expression/qgsexpressioncontextutils.sip.in index 2deb708460f..625ab0317db 100644 --- a/python/core/auto_generated/expression/qgsexpressioncontextutils.sip.in +++ b/python/core/auto_generated/expression/qgsexpressioncontextutils.sip.in @@ -388,6 +388,7 @@ Creates a new scope which contains functions relating to mesh layer element ``el }; + /************************************************************************ * This file has been generated automatically from * * * diff --git a/resources/function_help/json/load_layer b/resources/function_help/json/load_layer new file mode 100644 index 00000000000..191e8d0c98c --- /dev/null +++ b/resources/function_help/json/load_layer @@ -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"] +} diff --git a/src/core/expression/qgsexpressioncontextutils.cpp b/src/core/expression/qgsexpressioncontextutils.cpp index b39aa834c50..c1157bd9ff7 100644 --- a/src/core/expression/qgsexpressioncontextutils.cpp +++ b/src/core/expression/qgsexpressioncontextutils.cpp @@ -35,6 +35,9 @@ #include "qgstriangularmesh.h" #include "qgsvectortileutils.h" #include "qgsmeshlayer.h" +#include "qgsexpressionnodeimpl.h" +#include "qgsproviderregistry.h" +#include "qgsmaplayerfactory.h" QgsExpressionContextScope *QgsExpressionContextUtils::globalScope() { @@ -917,6 +920,7 @@ void QgsExpressionContextUtils::registerContextFunctions() QgsExpression::registerFunction( new GetProcessingParameterValue( QVariantMap() ) ); QgsExpression::registerFunction( new GetCurrentFormFieldValue( ) ); QgsExpression::registerFunction( new GetCurrentParentFormFieldValue( ) ); + QgsExpression::registerFunction( new LoadLayerFunction( ) ); } bool QgsScopedExpressionFunction::usesGeometry( const QgsExpressionNodeFunction *node ) const @@ -1296,4 +1300,110 @@ QgsExpressionContextScope *QgsExpressionContextUtils::meshExpressionScope( QgsMe 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(); + + 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 + diff --git a/src/core/expression/qgsexpressioncontextutils.h b/src/core/expression/qgsexpressioncontextutils.h index 9812d0a5f97..48c58616fd2 100644 --- a/src/core/expression/qgsexpressioncontextutils.h +++ b/src/core/expression/qgsexpressioncontextutils.h @@ -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 /** diff --git a/src/core/qgsexpressioncontext.cpp b/src/core/qgsexpressioncontext.cpp index 109ff61a493..c7af32c106a 100644 --- a/src/core/qgsexpressioncontext.cpp +++ b/src/core/qgsexpressioncontext.cpp @@ -17,6 +17,7 @@ #include "qgsxmlutils.h" #include "qgsexpression.h" #include "qgsmaplayerstore.h" +#include "qgsexpressioncontextutils.h" const QString QgsExpressionContext::EXPR_FIELDS( QStringLiteral( "_fields_" ) ); const QString QgsExpressionContext::EXPR_ORIGINAL_VALUE( QStringLiteral( "value" ) ); @@ -295,9 +296,15 @@ bool QgsExpressionContextScope::writeXml( QDomElement &element, QDomDocument &do // QgsExpressionContext // +QgsExpressionContext::QgsExpressionContext() +{ + mLoadLayerFunction = std::make_unique< LoadLayerFunction >(); +} + QgsExpressionContext::QgsExpressionContext( const QList &scopes ) : mStack( scopes ) { + mLoadLayerFunction = std::make_unique< LoadLayerFunction >(); } QgsExpressionContext::QgsExpressionContext( const QgsExpressionContext &other ) : mStack{} @@ -311,6 +318,7 @@ QgsExpressionContext::QgsExpressionContext( const QgsExpressionContext &other ) mCachedValues = other.mCachedValues; mFeedback = other.mFeedback; mDestinationStore = other.mDestinationStore; + mLoadLayerFunction = std::make_unique< LoadLayerFunction >(); } QgsExpressionContext &QgsExpressionContext::operator=( QgsExpressionContext &&other ) noexcept @@ -533,8 +541,10 @@ QString QgsExpressionContext::description( const QString &name ) const bool QgsExpressionContext::hasFunction( const QString &name ) const { - const auto constMStack = mStack; - for ( const QgsExpressionContextScope *scope : constMStack ) + if ( name.compare( QLatin1String( "load_layer" ) ) == 0 && mDestinationStore ) + return true; + + for ( const QgsExpressionContextScope *scope : mStack ) { if ( scope->hasFunction( name ) ) return true; @@ -551,6 +561,10 @@ QStringList QgsExpressionContext::functionNames() const for ( const QString &name : functionNames ) result.insert( name ); } + + if ( mDestinationStore ) + result.insert( QStringLiteral( "load_layer" ) ); + QStringList listResult( result.constBegin(), result.constEnd() ); listResult.sort(); return listResult; @@ -558,6 +572,11 @@ QStringList QgsExpressionContext::functionNames() 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 QList< QgsExpressionContextScope * >::const_iterator it = mStack.constEnd(); while ( it != mStack.constBegin() ) diff --git a/src/core/qgsexpressioncontext.h b/src/core/qgsexpressioncontext.h index 59a41dbbbfc..4763fcdb107 100644 --- a/src/core/qgsexpressioncontext.h +++ b/src/core/qgsexpressioncontext.h @@ -29,6 +29,7 @@ class QgsReadWriteContext; class QgsMapLayerStore; +class LoadLayerFunction; /** * \ingroup core @@ -481,7 +482,7 @@ class CORE_EXPORT QgsExpressionContext public: //! Constructor for QgsExpressionContext - QgsExpressionContext() = default; + QgsExpressionContext(); /** * Initializes the context with given list of scopes. @@ -936,6 +937,7 @@ class CORE_EXPORT QgsExpressionContext QgsFeedback *mFeedback = nullptr; + std::unique_ptr< LoadLayerFunction > mLoadLayerFunction; QPointer< QgsMapLayerStore > mDestinationStore; // Cache is mutable because we want to be able to add cached values to const contexts diff --git a/tests/src/core/testqgsexpression.cpp b/tests/src/core/testqgsexpression.cpp index 37830249cc2..f968ba24e2a 100644 --- a/tests/src/core/testqgsexpression.cpp +++ b/tests/src/core/testqgsexpression.cpp @@ -5650,6 +5650,66 @@ class TestQgsExpression: public QObject 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 )