diff --git a/resources/function_help/json/display_expression b/resources/function_help/json/display_expression new file mode 100644 index 00000000000..0c612ad7e35 --- /dev/null +++ b/resources/function_help/json/display_expression @@ -0,0 +1,39 @@ +{ + "name": "display_expression", + "type": "function", + "description": "Returns the display expression for a given feature in a layer. If called with no parameters, it evaluates the current feature. The expression is evaluated by default.", + "arguments": [ + { + "arg": "feature", + "optional": true, + "default": "current feature", + "description": "The feature which should be evaluated." + }, + { + "arg": "layer", + "optional": true, + "default": "current layer", + "description": "The layer (or its id or name)." + }, + { + "arg": "evaluate", + "description": "If the expression must be evaluated. If false, the expression will be returned as a string literal only (which could potentially be later evaluated using the 'eval' function).", + "optional": true, + "default": "true" + } + ], + "examples": [ + { + "expression": "display_expression()", + "returns": "The display expression of the current feature." + }, + { + "expression": "display_expression($currentfeature)", + "returns": "The display expression for a given feature." + }, + { + "expression": "display_expression('a_layer_id', $currentfeature, 'False')", + "returns": "The display expression of the current feature not evaluated." + } + ] +} diff --git a/resources/function_help/json/eval_template b/resources/function_help/json/eval_template new file mode 100644 index 00000000000..0fc55f2c35d --- /dev/null +++ b/resources/function_help/json/eval_template @@ -0,0 +1,13 @@ +{ + "name": "eval_template", + "type": "function", + "description": "Evaluates a template which is passed in a string. Useful to expand dynamic parameters passed as context variables or fields.", + "arguments": [{ + "arg": "template", + "description": "a template string" + }], + "examples": [{ + "expression": "eval_template('QGIS [% upper(\\\\'rocks\\\\') %]')", + "returns": "QGIS ROCKS" + }] +} diff --git a/resources/function_help/json/maptip b/resources/function_help/json/maptip new file mode 100644 index 00000000000..13011c12819 --- /dev/null +++ b/resources/function_help/json/maptip @@ -0,0 +1,39 @@ +{ + "name": "maptip", + "type": "function", + "description": "Returns the maptip for a given feature in a layer. If called with no parameters, it evaluates the current feature. The maptip is evaluated by default.", + "arguments": [ + { + "arg": "feature", + "optional": true, + "default": "current feature", + "description": "The feature which should be evaluated." + }, + { + "arg": "layer", + "optional": true, + "default": "current layer", + "description": "The layer (or its id or name)." + }, + { + "arg": "evaluate", + "description": "If the expression must be evaluated. If false, the expression will be returned as a string literal only (which could potentially be later evaluated using the 'eval_template' function).", + "optional": true, + "default": "true" + } + ], + "examples": [ + { + "expression": "maptip()", + "returns": "The maptip of the current feature." + }, + { + "expression": "maptip($currentFeature)", + "returns": "The maptip of the current feature." + }, + { + "expression": "maptip('a_layer_id', $currentFeature, 'False')", + "returns": "The maptip of the current feature not evaluated." + } + ] +} diff --git a/src/core/expression/qgsexpressionfunction.cpp b/src/core/expression/qgsexpressionfunction.cpp index 49f044d9237..014f9689fb4 100644 --- a/src/core/expression/qgsexpressionfunction.cpp +++ b/src/core/expression/qgsexpressionfunction.cpp @@ -272,6 +272,12 @@ static QVariant fcnGetVariable( const QVariantList &values, const QgsExpressionC return context->variable( name ); } +static QVariant fcnEvalTemplate( const QVariantList &values, const QgsExpressionContext *context, QgsExpression *parent, const QgsExpressionNodeFunction * ) +{ + QString templateString = QgsExpressionUtils::getStringValue( values.at( 0 ), parent ); + return QgsExpression::replaceExpressionText( templateString, context ); +} + static QVariant fcnEval( const QVariantList &values, const QgsExpressionContext *context, QgsExpression *parent, const QgsExpressionNodeFunction * ) { if ( !context ) @@ -1513,6 +1519,96 @@ static QVariant fcnAttributes( const QVariantList &values, const QgsExpressionCo return result; } +static QVariant fcnCoreFeatureMaptipDisplay( const QVariantList &values, const QgsExpressionContext *context, QgsExpression *parent, const bool isMaptip ) +{ + QgsVectorLayer *layer = nullptr; + QgsFeature feature; + bool evaluate = true; + + if ( values.isEmpty() ) + { + feature = context->feature(); + layer = QgsExpressionUtils::getVectorLayer( context->variable( QStringLiteral( "layer" ) ), parent ); + } + else if ( values.size() == 1 ) + { + layer = QgsExpressionUtils::getVectorLayer( context->variable( QStringLiteral( "layer" ) ), parent ); + feature = QgsExpressionUtils::getFeature( values.at( 0 ), parent ); + } + else if ( values.size() == 2 ) + { + layer = QgsExpressionUtils::getVectorLayer( values.at( 0 ), parent ); + feature = QgsExpressionUtils::getFeature( values.at( 1 ), parent ); + } + else if ( values.size() == 3 ) + { + layer = QgsExpressionUtils::getVectorLayer( values.at( 0 ), parent ); + feature = QgsExpressionUtils::getFeature( values.at( 1 ), parent ); + evaluate = values.value( 2 ).toBool(); + } + else + { + if ( isMaptip ) + { + parent->setEvalErrorString( QObject::tr( "Function `maptip` requires no more than three parameters. %1 given." ).arg( values.length() ) ); + } + else + { + parent->setEvalErrorString( QObject::tr( "Function `display` requires no more than three parameters. %1 given." ).arg( values.length() ) ); + } + return QVariant(); + } + + if ( !layer ) + { + parent->setEvalErrorString( QObject::tr( "The layer is not valid." ) ); + return QVariant( ); + } + + if ( !feature.isValid() ) + { + parent->setEvalErrorString( QObject::tr( "The feature is not valid." ) ); + return QVariant( ); + } + + if ( ! evaluate ) + { + if ( isMaptip ) + { + return layer->mapTipTemplate(); + } + else + { + return layer->displayExpression(); + } + } + + QgsExpressionContext subContext( *context ); + subContext.appendScopes( QgsExpressionContextUtils::globalProjectLayerScopes( layer ) ); + subContext.setFeature( feature ); + + if ( isMaptip ) + { + return QgsExpression::replaceExpressionText( layer->mapTipTemplate(), &subContext ); + } + else + { + QgsExpression exp( layer->displayExpression() ); + exp.prepare( &subContext ); + return exp.evaluate( &subContext ).toString(); + } +} + +static QVariant fcnFeatureDisplayExpression( const QVariantList &values, const QgsExpressionContext *context, QgsExpression *parent, const QgsExpressionNodeFunction * ) +{ + return fcnCoreFeatureMaptipDisplay( values, context, parent, false ); +} + +static QVariant fcnFeatureMaptip( const QVariantList &values, const QgsExpressionContext *context, QgsExpression *parent, const QgsExpressionNodeFunction * ) +{ + return fcnCoreFeatureMaptipDisplay( values, context, parent, true ); +} + static QVariant fcnIsSelected( const QVariantList &values, const QgsExpressionContext *context, QgsExpression *parent, const QgsExpressionNodeFunction * ) { QgsVectorLayer *layer = nullptr; @@ -5903,6 +5999,30 @@ const QList &QgsExpression::Functions() << new QgsStaticExpressionFunction( QStringLiteral( "attributes" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "feature" ), true ), fcnAttributes, QStringLiteral( "Record and Attributes" ), QString(), false, QSet() << QgsFeatureRequest::ALL_ATTRIBUTES ); + QgsStaticExpressionFunction *maptipFunc = new QgsStaticExpressionFunction( + QStringLiteral( "maptip" ), + -1, + fcnFeatureMaptip, + QStringLiteral( "Record and Attributes" ), + QString(), + false, + QSet() + ); + maptipFunc->setIsStatic( false ); + functions << maptipFunc; + + QgsStaticExpressionFunction *displayFunc = new QgsStaticExpressionFunction( + QStringLiteral( "display_expression" ), + -1, + fcnFeatureDisplayExpression, + QStringLiteral( "Record and Attributes" ), + QString(), + false, + QSet() + ); + displayFunc->setIsStatic( false ); + functions << displayFunc; + QgsStaticExpressionFunction *isSelectedFunc = new QgsStaticExpressionFunction( QStringLiteral( "is_selected" ), -1, @@ -6015,6 +6135,9 @@ const QList &QgsExpression::Functions() functions << varFunction; + + functions << new QgsStaticExpressionFunction( QStringLiteral( "eval_template" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "template" ) ), fcnEvalTemplate, QStringLiteral( "General" ), QString(), true ); + QgsStaticExpressionFunction *evalFunc = new QgsStaticExpressionFunction( QStringLiteral( "eval" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "expression" ) ), fcnEval, QStringLiteral( "General" ), QString(), true, QSet() << QgsFeatureRequest::ALL_ATTRIBUTES ); evalFunc->setIsStaticFunction( []( const QgsExpressionNodeFunction * node, QgsExpression * parent, const QgsExpressionContext * context ) diff --git a/tests/src/core/testqgsexpression.cpp b/tests/src/core/testqgsexpression.cpp index 6166add9a1f..40f74733aab 100644 --- a/tests/src/core/testqgsexpression.cpp +++ b/tests/src/core/testqgsexpression.cpp @@ -94,6 +94,8 @@ class TestQgsExpression: public QObject mPointsLayer->setAttributionUrl( QStringLiteral( "attribution url" ) ); mPointsLayer->setMinimumScale( 500 ); mPointsLayer->setMaximumScale( 1000 ); + mPointsLayer->setMapTipTemplate( QStringLiteral( "Maptip with class = [% \"Class\" %]" ) ); + mPointsLayer->setDisplayExpression( QStringLiteral( "'Display expression with class = ' || \"Class\"" ) ); mPointsLayerMetadata = new QgsVectorLayer( pointFileInfo.filePath(), pointFileInfo.completeBaseName() + "_metadata", QStringLiteral( "ogr" ) ); @@ -1411,6 +1413,11 @@ class TestQgsExpression: public QObject QTest::newRow( "right associativity" ) << "(2^3)^2" << false << QVariant( 64. ); QTest::newRow( "left associativity" ) << "1-(2-1)" << false << QVariant( 0 ); + // eval_template tests + QTest::newRow( "eval_template" ) << QStringLiteral( "eval_template(\'this is a [% \\'template\\' || \\'!\\' %]\')" ) << false << QVariant( "this is a template!" ); + QTest::newRow( "eval_template string" ) << QStringLiteral( "eval_template('string')" ) << false << QVariant( "string" ); + QTest::newRow( "eval_template expression" ) << QStringLiteral( "eval_template('a' || ' string')" ) << false << QVariant( "a string" ); + // layer_property tests QTest::newRow( "layer_property no layer" ) << "layer_property('','title')" << false << QVariant(); QTest::newRow( "layer_property bad layer" ) << "layer_property('bad','title')" << false << QVariant(); @@ -2015,6 +2022,63 @@ class TestQgsExpression: public QObject QTest::newRow( "group by with null value" ) << "sum(\"col1\", \"col4\")" << false << QVariant( 8 ); } + void maptip_display_data() + { + QTest::addColumn( "string" ); + QTest::addColumn( "feature" ); + QTest::addColumn( "layer" ); + QTest::addColumn( "evalError" ); + QTest::addColumn( "result" ); + + QgsFeature firstFeature = mPointsLayer->getFeature( 1 ); + QgsVectorLayer *noLayer = nullptr; + + QTest::newRow( "display not evaluated" ) << QStringLiteral( "display_expression(@layer_id, $currentfeature, False)" ) << firstFeature << mPointsLayer << false << QVariant( "'Display expression with class = ' || \"Class\"" ); + QTest::newRow( "display wrong layer" ) << QStringLiteral( "display_expression()" ) << firstFeature << noLayer << true << QVariant(); + QTest::newRow( "display wrong feature" ) << QStringLiteral( "display_expression()" ) << QgsFeature() << mPointsLayer << true << QVariant(); + + QTest::newRow( "maptip wrong feature" ) << QStringLiteral( "maptip()" ) << QgsFeature() << mPointsLayer << true << QVariant(); + QTest::newRow( "maptip wrong layer" ) << QStringLiteral( "maptip()" ) << firstFeature << noLayer << true << QVariant(); + QTest::newRow( "maptip not evaluated" ) << QStringLiteral( "maptip(@layer_id, $currentfeature, False)" ) << firstFeature << mPointsLayer << false << QVariant( "Maptip with class = [% \"Class\" %]" ); + + QTest::newRow( "maptip with 2 params" ) << QStringLiteral( "maptip(@layer_id, $currentfeature)" ) << firstFeature << mPointsLayer << false << QVariant( "Maptip with class = Biplane" ); + QTest::newRow( "maptip with 1 param" ) << QStringLiteral( "maptip($currentfeature)" ) << firstFeature << mPointsLayer << false << QVariant( "Maptip with class = Biplane" ); + QTest::newRow( "maptip with 0 param" ) << QStringLiteral( "maptip()" ) << firstFeature << mPointsLayer << false << QVariant( "Maptip with class = Biplane" ); + + QTest::newRow( "display with 2 params" ) << QStringLiteral( "display_expression(@layer_id, $currentfeature)" ) << firstFeature << mPointsLayer << false << QVariant( "Display expression with class = Biplane" ); + QTest::newRow( "display with 1 param" ) << QStringLiteral( "display_expression($currentfeature)" ) << firstFeature << mPointsLayer << false << QVariant( "Display expression with class = Biplane" ); + QTest::newRow( "display with 0 param" ) << QStringLiteral( "display_expression()" ) << firstFeature << mPointsLayer << false << QVariant( "Display expression with class = Biplane" ); + } + + void maptip_display() + { + QFETCH( QString, string ); + QFETCH( QgsFeature, feature ); + QFETCH( QgsVectorLayer *, layer ); + QFETCH( bool, evalError ); + QFETCH( QVariant, result ); + + QgsExpressionContext context; + context.appendScope( QgsExpressionContextUtils::globalScope() ); + context.appendScope( QgsExpressionContextUtils::projectScope( QgsProject::instance() ) ); + if ( layer ) + { + //layer->setDisplayExpression( QStringLiteral( "Display expression with class = ' || Class" ) ); + context.appendScope( QgsExpressionContextUtils::layerScope( layer ) ); + } + context.setFeature( feature ); + + QgsExpression exp( string ); + exp.prepare( &context ); + + if ( exp.hasParserError() ) + qDebug() << exp.parserErrorString(); + QCOMPARE( exp.hasParserError(), false ); + QVariant res = exp.evaluate( &context ); + QCOMPARE( exp.hasEvalError(), evalError ); + QCOMPARE( res.toString(), result.toString() ); + } + void selection() { QFETCH( QgsFeatureIds, selectedFeatures );