From bc192a60b25f31c93a33680fd6a23ea62c30cc21 Mon Sep 17 00:00:00 2001 From: Vincent Cloarec Date: Tue, 24 Aug 2021 14:56:16 -0400 Subject: [PATCH] Mesh expressions: add $vertex_as_point and $vertex_z functions (#44786) * mesh expression $vertex_as_point $vertex_Z_value * functions help * SIP, doc and indentation * fix SIP * fix help file * change function name and add Meshes group description * fix typo * fix strings and docs * typo --- .../qgsexpressioncontextutils.sip.in | 7 ++ .../auto_generated/mesh/qgsmeshlayer.sip.in | 10 ++ resources/function_help/json/$vertex_as_point | 8 ++ resources/function_help/json/$vertex_z | 8 ++ resources/function_help/json/Meshes | 5 + .../expression/qgsexpressioncontextutils.cpp | 94 +++++++++++++++++++ .../expression/qgsexpressioncontextutils.h | 6 ++ src/core/expression/qgsexpressionutils.h | 6 ++ src/core/mesh/qgsmeshlayer.cpp | 35 +++++++ src/core/mesh/qgsmeshlayer.h | 10 ++ tests/src/core/testqgsexpression.cpp | 23 +++++ tests/src/core/testqgsmeshlayer.cpp | 15 +++ 12 files changed, 227 insertions(+) create mode 100644 resources/function_help/json/$vertex_as_point create mode 100644 resources/function_help/json/$vertex_z create mode 100644 resources/function_help/json/Meshes diff --git a/python/core/auto_generated/expression/qgsexpressioncontextutils.sip.in b/python/core/auto_generated/expression/qgsexpressioncontextutils.sip.in index f1d799520fe..05cd51a6f4a 100644 --- a/python/core/auto_generated/expression/qgsexpressioncontextutils.sip.in +++ b/python/core/auto_generated/expression/qgsexpressioncontextutils.sip.in @@ -365,6 +365,13 @@ Creates a new scope which contains variables and functions relating to provider static void registerContextFunctions(); %Docstring Registers all known core functions provided by :py:class:`QgsExpressionContextScope` objects. +%End + + static QgsExpressionContextScope *meshExpressionScope() /Factory/; +%Docstring +Creates a new scope which contains functions relating to mesh layer elements (face, vertex, ...) + +.. versionadded:: 3.22 %End public: diff --git a/python/core/auto_generated/mesh/qgsmeshlayer.sip.in b/python/core/auto_generated/mesh/qgsmeshlayer.sip.in index a29f9d0a667..3bdc31a1b17 100644 --- a/python/core/auto_generated/mesh/qgsmeshlayer.sip.in +++ b/python/core/auto_generated/mesh/qgsmeshlayer.sip.in @@ -658,6 +658,16 @@ The returned position is in map coordinates. .. versionadded:: 3.14 +%End + + QList selectVerticesByExpression( const QString &expression, const QgsExpressionContext &expressionContext = QgsExpressionContext() ); +%Docstring +Returns a list of vertex indexes that meet the condition defined by ``expression`` with the context ``expressionContext`` + +To express the relation with a vertex, the expression can be defined with function returning value +linked to the current vertex, like " $vertex_z ", "$vertex_as_point" + +.. versionadded:: 3.22 %End QgsMeshDatasetGroupTreeItem *datasetGroupTreeRootItem() const; diff --git a/resources/function_help/json/$vertex_as_point b/resources/function_help/json/$vertex_as_point new file mode 100644 index 00000000000..93e42ebe954 --- /dev/null +++ b/resources/function_help/json/$vertex_as_point @@ -0,0 +1,8 @@ +{ + "name": "$vertex_as_point", + "type": "function", + "groups": ["Meshes"], + "description": "Returns the current vertex as a point geometry.", + "examples": [ { "expression":"geom_to_wkt( $vertex_as_point )", "returns":"'POINT(800 1500 41)'"} + ] +} diff --git a/resources/function_help/json/$vertex_z b/resources/function_help/json/$vertex_z new file mode 100644 index 00000000000..4d00030689c --- /dev/null +++ b/resources/function_help/json/$vertex_z @@ -0,0 +1,8 @@ +{ + "name": "$vertex_z", + "type": "function", + "groups": ["Meshes"], + "description": "Returns the Z value of the current mesh vertex.", + "examples": [ { "expression":"$vertex_z", "returns":"42"} + ] +} diff --git a/resources/function_help/json/Meshes b/resources/function_help/json/Meshes new file mode 100644 index 00000000000..ca112735f50 --- /dev/null +++ b/resources/function_help/json/Meshes @@ -0,0 +1,5 @@ +{ + "name": "Meshes", + "type": "group", + "description": "Contains functions which calculate or return mesh related values." +} diff --git a/src/core/expression/qgsexpressioncontextutils.cpp b/src/core/expression/qgsexpressioncontextutils.cpp index a551534a7ea..dd5bc1b2d4a 100644 --- a/src/core/expression/qgsexpressioncontextutils.cpp +++ b/src/core/expression/qgsexpressioncontextutils.cpp @@ -975,3 +975,97 @@ QgsScopedExpressionFunction *QgsExpressionContextUtils::GetLayerVisibility::clon func->mScaleBasedVisibilityDetails = mScaleBasedVisibilityDetails; return func; } + +// +// mesh expression context +// + +/// @cond PRIVATE +class CurrentVertexZValueExpressionFunction: public QgsScopedExpressionFunction +{ + public: + CurrentVertexZValueExpressionFunction(): + QgsScopedExpressionFunction( "$vertex_z", + 0, + QStringLiteral( "Meshes" ) ) + {} + + QgsScopedExpressionFunction *clone() const override {return new CurrentVertexZValueExpressionFunction();} + + QVariant func( const QVariantList &, const QgsExpressionContext *context, QgsExpression *, const QgsExpressionNodeFunction * ) override + { + if ( !context ) + return QVariant(); + + if ( !context->hasVariable( QStringLiteral( "_mesh_vertex_index" ) ) || !context->hasVariable( QStringLiteral( "_mesh_layer" ) ) ) + return QVariant(); + + int vertexIndex = context->variable( QStringLiteral( "_mesh_vertex_index" ) ).toInt(); + + QgsMeshLayer *layer = qobject_cast( qvariant_cast( context->variable( QStringLiteral( "_mesh_layer" ) ) ) ); + if ( !layer || !layer->nativeMesh() || layer->nativeMesh()->vertexCount() <= vertexIndex ) + return QVariant(); + + const QgsMeshVertex &vertex = layer->nativeMesh()->vertex( vertexIndex ); + if ( !vertex.isEmpty() ) + return vertex.z(); + else + return QVariant(); + } + + bool isStatic( const QgsExpressionNodeFunction *, QgsExpression *, const QgsExpressionContext * ) const override + { + return false; + } +}; + +class CurrentVertexExpressionFunction: public QgsScopedExpressionFunction +{ + public: + CurrentVertexExpressionFunction(): + QgsScopedExpressionFunction( "$vertex_as_point", + 0, + QStringLiteral( "Meshes" ) ) + {} + + QgsScopedExpressionFunction *clone() const override {return new CurrentVertexExpressionFunction();} + + QVariant func( const QVariantList &, const QgsExpressionContext *context, QgsExpression *, const QgsExpressionNodeFunction * ) override + { + if ( !context ) + return QVariant(); + + if ( !context->hasVariable( QStringLiteral( "_mesh_vertex_index" ) ) || !context->hasVariable( QStringLiteral( "_mesh_layer" ) ) ) + return QVariant(); + + int vertexIndex = context->variable( QStringLiteral( "_mesh_vertex_index" ) ).toInt(); + + QgsMeshLayer *layer = qobject_cast( qvariant_cast( context->variable( QStringLiteral( "_mesh_layer" ) ) ) ); + if ( !layer || !layer->nativeMesh() || layer->nativeMesh()->vertexCount() <= vertexIndex ) + return QVariant(); + + const QgsMeshVertex &vertex = layer->nativeMesh()->vertex( vertexIndex ); + if ( !vertex.isEmpty() ) + return QVariant::fromValue( QgsGeometry( new QgsPoint( vertex ) ) ); + else + return QVariant(); + } + + bool isStatic( const QgsExpressionNodeFunction *, QgsExpression *, const QgsExpressionContext * ) const override + { + return false; + } +}; + +QgsExpressionContextScope *QgsExpressionContextUtils::meshExpressionScope() +{ + QgsExpression::registerFunction( new CurrentVertexExpressionFunction, true ); + QgsExpression::registerFunction( new CurrentVertexZValueExpressionFunction, true ); + + std::unique_ptr scope = std::make_unique(); + scope->addFunction( "$vertex_as_point", new CurrentVertexExpressionFunction ); + scope->addFunction( "$vertex_z", new CurrentVertexZValueExpressionFunction ); + + return scope.release(); +} +///@endcond diff --git a/src/core/expression/qgsexpressioncontextutils.h b/src/core/expression/qgsexpressioncontextutils.h index 2f871fa6592..c45279b4238 100644 --- a/src/core/expression/qgsexpressioncontextutils.h +++ b/src/core/expression/qgsexpressioncontextutils.h @@ -321,6 +321,12 @@ class CORE_EXPORT QgsExpressionContextUtils */ static void registerContextFunctions(); + /** + * Creates a new scope which contains functions relating to mesh layer elements (face, vertex, ...) + * \since QGIS 3.22 + */ + static QgsExpressionContextScope *meshExpressionScope() SIP_FACTORY; + private: class GetLayerVisibility : public QgsScopedExpressionFunction diff --git a/src/core/expression/qgsexpressionutils.h b/src/core/expression/qgsexpressionutils.h index 6847505d6da..8ec45af5c74 100644 --- a/src/core/expression/qgsexpressionutils.h +++ b/src/core/expression/qgsexpressionutils.h @@ -27,6 +27,7 @@ #include "qgsproject.h" #include "qgsrelationmanager.h" #include "qgsvectorlayer.h" +#include "qgsmeshlayer.h" #include #include @@ -405,6 +406,11 @@ class QgsExpressionUtils return qobject_cast( getMapLayer( value, e ) ); } + static QgsMeshLayer *getMeshLayer( const QVariant &value, QgsExpression *e ) + { + return qobject_cast( getMapLayer( value, e ) ); + } + static QVariantList getListValue( const QVariant &value, QgsExpression *parent ) { if ( value.type() == QVariant::List || value.type() == QVariant::StringList ) diff --git a/src/core/mesh/qgsmeshlayer.cpp b/src/core/mesh/qgsmeshlayer.cpp index a64c5bccca1..1e0eb08eb41 100644 --- a/src/core/mesh/qgsmeshlayer.cpp +++ b/src/core/mesh/qgsmeshlayer.cpp @@ -42,6 +42,7 @@ #include "qgslayermetadataformatter.h" #include "qgsmesheditor.h" #include "qgsmessagelog.h" +#include "qgsexpressioncontextutils.h" QgsMeshLayer::QgsMeshLayer( const QString &meshLayerPath, const QString &baseName, @@ -1138,6 +1139,40 @@ QgsPointXY QgsMeshLayer::snapOnElement( QgsMesh::ElementType elementType, const return QgsPointXY(); // avoid warnings } +QList QgsMeshLayer::selectVerticesByExpression( const QString &expressionString, const QgsExpressionContext &expressionContext ) +{ + if ( !mNativeMesh ) + { + // lazy loading of mesh data + fillNativeMesh(); + } + + QList ret; + + if ( !mNativeMesh ) + return ret; + + QgsExpression expression( expressionString ); + QgsExpressionContext context = expressionContext; + + std::unique_ptr expScope( QgsExpressionContextUtils::meshExpressionScope() ); + + context.appendScope( expScope.release() ); + context.lastScope()->setVariable( QStringLiteral( "_mesh_layer" ), QVariant::fromValue( this ) ); + + expression.prepare( &context ); + + for ( int i = 0; i < mNativeMesh->vertexCount(); ++i ) + { + context.lastScope()->setVariable( QStringLiteral( "_mesh_vertex_index" ), i, false ); + + if ( expression.evaluate( &context ).toBool() ) + ret.append( i ); + } + + return ret; +} + QgsMeshDatasetIndex QgsMeshLayer::staticScalarDatasetIndex() const { return QgsMeshDatasetIndex( mRendererSettings.activeScalarDatasetGroup(), mStaticScalarDatasetIndex ); diff --git a/src/core/mesh/qgsmeshlayer.h b/src/core/mesh/qgsmeshlayer.h index f1cd401cea9..b710016f01c 100644 --- a/src/core/mesh/qgsmeshlayer.h +++ b/src/core/mesh/qgsmeshlayer.h @@ -670,6 +670,16 @@ class CORE_EXPORT QgsMeshLayer : public QgsMapLayer */ QgsPointXY snapOnElement( QgsMesh::ElementType elementType, const QgsPointXY &point, double searchRadius ); + /** + * Returns a list of vertex indexes that meet the condition defined by \a expression with the context \a expressionContext + * + * To express the relation with a vertex, the expression can be defined with function returning value + * linked to the current vertex, like " $vertex_z ", "$vertex_as_point" + * + * \since QGIS 3.22 + */ + QList selectVerticesByExpression( const QString &expression, const QgsExpressionContext &expressionContext = QgsExpressionContext() ); + /** * Returns the root items of the dataset group tree item * diff --git a/tests/src/core/testqgsexpression.cpp b/tests/src/core/testqgsexpression.cpp index 1e8c670aa15..a043091bc0b 100644 --- a/tests/src/core/testqgsexpression.cpp +++ b/tests/src/core/testqgsexpression.cpp @@ -65,6 +65,7 @@ class TestQgsExpression: public QObject QgsVectorLayer *mChildLayer2 = nullptr; // relation with composite keys QgsVectorLayer *mChildLayer = nullptr; QgsRasterLayer *mRasterLayer = nullptr; + QgsMeshLayer *mMeshLayer = nullptr; private slots: @@ -123,6 +124,11 @@ class TestQgsExpression: public QObject rasterFileInfo.completeBaseName() ); QgsProject::instance()->addMapLayer( mRasterLayer ); + QString meshFileName = testDataDir + "/mesh/quad_flower.2dm"; + mMeshLayer = new QgsMeshLayer( meshFileName, "mesh layer", "mdal" ); + mMeshLayer->updateTriangularMesh(); + QgsProject::instance()->addMapLayer( mMeshLayer ); + // test memory layer for get_feature tests mMemoryLayer = new QgsVectorLayer( QStringLiteral( "Point?field=col1:integer&field=col2:string" ), QStringLiteral( "test" ), QStringLiteral( "memory" ) ); QVERIFY( mMemoryLayer->isValid() ); @@ -257,6 +263,23 @@ class TestQgsExpression: public QObject QgsProject::instance()->relationManager()->addRelation( rel_ck ); } + void evalMeshElement() + { + QgsExpressionContext context; + context.appendScope( QgsExpressionContextUtils::meshExpressionScope() ); + context.lastScope()->setVariable( QStringLiteral( "_mesh_vertex_index" ), 2 ); + context.lastScope()->setVariable( QStringLiteral( "_mesh_layer" ), QVariant::fromValue( mMeshLayer ) ); + + QgsExpression expression( QStringLiteral( "$vertex_z" ) ); + QCOMPARE( expression.evaluate( &context ).toDouble(), 800.0 ); + + expression = QgsExpression( QStringLiteral( "$vertex_as_point" ) ); + QVariant out = expression.evaluate( &context ); + QgsGeometry outGeom = out.value(); + QgsGeometry geom( new QgsPoint( 2500, 2500, 800 ) ); + QCOMPARE( geom.equals( outGeom ), true ); + } + void cleanupTestCase() { QgsApplication::exitQgis(); diff --git a/tests/src/core/testqgsmeshlayer.cpp b/tests/src/core/testqgsmeshlayer.cpp index 11068608c03..e96fce9bd11 100644 --- a/tests/src/core/testqgsmeshlayer.cpp +++ b/tests/src/core/testqgsmeshlayer.cpp @@ -97,6 +97,8 @@ class TestQgsMeshLayer : public QObject void testMdalProviderQuerySublayers(); void testMdalProviderQuerySublayersFastScan(); + + void testSelectByExpression(); }; QString TestQgsMeshLayer::readFile( const QString &fname ) const @@ -1707,6 +1709,19 @@ void TestQgsMeshLayer::testMdalProviderQuerySublayersFastScan() QVERIFY( res.at( 0 ).skippedContainerScan() ); } +void TestQgsMeshLayer::testSelectByExpression() +{ + mMdalLayer->updateTriangularMesh(); + QgsExpressionContext expressionContext; + + QList selectedVerticesIndexes = mMdalLayer->selectVerticesByExpression( QStringLiteral( " $vertex_z > 30" ) ); + QCOMPARE( selectedVerticesIndexes, QList( {2, 3} ) ); + + selectedVerticesIndexes = mMdalLayer->selectVerticesByExpression( QStringLiteral( " x($vertex_as_point) > 1500" ) ); + QCOMPARE( selectedVerticesIndexes.count(), 3 ); + QCOMPARE( selectedVerticesIndexes, QList( {1, 2, 3} ) ); +} + void TestQgsMeshLayer::test_temporal() { const qint64 relativeTime_0 = -1000;