5

When putting together an answer for this question, I attempted to use the @layer_name variable in an aggregate function to make the expression more generic and easier to use when running the "Geometry by expression" tool as a batch process over multiple layers.

The full expression I was trying to use is:

scale(
  geometry:=$geometry, 
  x_scale:=4, 
  y_scale:=4, 
  center:=centroid(
    aggregate(layer:=@layer_name, aggregate:='collect', expression:=@geometry)
  )
)

However, when I run the tool with that expression, the processing log shows the following error which I assume is because the @layer_name variable is empty:

Algorithm 'Geometry by expression' starting…
Input parameters:
{ 'EXPRESSION' : "scale(\n geometry:=$geometry, \n x_scale:=4, \n y_scale:=4, \n center:=centroid(\n\taggregate(layer:=@layer_name, aggregate:='collect', expression:=@geometry)\n )\n)", 'INPUT' : '/path/to/small.shp', 'OUTPUT' : 'TEMPORARY_OUTPUT', 'OUTPUT_GEOMETRY' : 0, 'WITH_M' : False, 'WITH_Z' : False }

Evaluation error: Cannot find layer with name or ID ''
Execution failed after 0.04 seconds

In the screenshot below are the parameters I set for the tool and you can see that the @layer_name variable is populated in the function/variable selector description on the right.

tool parameters

Notes:

  • the input layer is a permanent dataset, not a temporary layer.
  • the error is the same whether the output is a permanent dataset or temporary layer.
  • this isn't run in a model, but directly from the processing toolbox.

How can I avoid hardcoding the layer name in this expression so this tool (or a workaround using another tool) can be batched and applied to multiple input layers?

2
  • I can confirm this issue on my QGIS 3.42.2-Münster. Perhaps it is a bug, that should be reported, similar to this one: github.com/qgis/QGIS/issues/37347 Commented Nov 28 at 8:39
  • @Taras, thanks. I found a more relevant issue (#54869). Commented 2 days ago

2 Answers 2

6

This is a bug (#54869) in the native (C++) Geometry By Expression tool.

The Geometry By Expression tool was ported from python to native C++ commit eec0ac0 for QGIS 3.12.

A workaround is to modify the old python Geometry By Expression tool from QGIS 3.10 and add the missing layer scope to the expression context using QgsExpressionContext.appendScope and QgsProcessingFeatureSource.createExpressionContextScope

This is a diff -u of the change:

@@ -99,6 +99,7 @@
             return False
 
         self.expression_context = self.createExpressionContext(parameters, context)
+        self.expression_context.appendScope(self.parameterAsLayer(parameters, 'INPUT', context).createExpressionContextScope())
         self.expression.prepare(self.expression_context)
 
         return True

To use the updated script, in the Processing toolbox, click the script icon, select Create New Script... paste the following complete script in and save it.

Create New Script

The modified tool will be available from the Scripts provider:

Tool

# -*- coding: utf-8 -*-

"""
***************************************************************************
    GeometryByExpression.py
    -----------------------
    Date                 : October 2016
    Copyright            : (C) 2016 by Nyall Dawson
    Email                : nyall dot dawson at gmail dot com
***************************************************************************
*                                                                         *
*   This program is free software; you can redistribute it and/or modify  *
*   it under the terms of the GNU General Public License as published by  *
*   the Free Software Foundation; either version 2 of the License, or     *
*   (at your option) any later version.                                   *
*                                                                         *
***************************************************************************
"""

__author__ = 'Nyall Dawson'
__date__ = 'October 2016'
__copyright__ = '(C) 2016, Nyall Dawson'

from qgis.core import (QgsWkbTypes,
                       QgsExpression,
                       QgsGeometry,
                       QgsProcessing,
                       QgsProcessingAlgorithm,
                       QgsProcessingException,
                       QgsProcessingParameterBoolean,
                       QgsProcessingParameterEnum,
                       QgsProcessingParameterExpression,
                       QgsProcessingFeatureSource)

from processing.algs.qgis.QgisAlgorithm import QgisFeatureBasedAlgorithm


class GeometryByExpression(QgisFeatureBasedAlgorithm):

    OUTPUT_GEOMETRY = 'OUTPUT_GEOMETRY'
    WITH_Z = 'WITH_Z'
    WITH_M = 'WITH_M'
    EXPRESSION = 'EXPRESSION'

    def group(self):
        return self.tr('Vector geometry')

    def groupId(self):
        return 'vectorgeometry'

    def flags(self):
        return super().flags() & ~QgsProcessingAlgorithm.FlagSupportsInPlaceEdits

    def __init__(self):
        super().__init__()
        self.geometry_types = [self.tr('Polygon'),
                               'Line',
                               'Point']

    def initParameters(self, config=None):
        self.addParameter(QgsProcessingParameterEnum(
            self.OUTPUT_GEOMETRY,
            self.tr('Output geometry type'),
            options=self.geometry_types, defaultValue=0))
        self.addParameter(QgsProcessingParameterBoolean(self.WITH_Z,
                                                        self.tr('Output geometry has z dimension'), defaultValue=False))
        self.addParameter(QgsProcessingParameterBoolean(self.WITH_M,
                                                        self.tr('Output geometry has m values'), defaultValue=False))

        self.addParameter(QgsProcessingParameterExpression(self.EXPRESSION,
                                                           self.tr("Geometry expression"), defaultValue='$geometry', parentLayerParameterName='INPUT'))

    def name(self):
        return 'geometrybyexpression'

    def displayName(self):
        return self.tr('Geometry by expression')

    def outputName(self):
        return self.tr('Modified geometry')

    def prepareAlgorithm(self, parameters, context, feedback):
        self.geometry_type = self.parameterAsEnum(parameters, self.OUTPUT_GEOMETRY, context)
        self.wkb_type = None
        if self.geometry_type == 0:
            self.wkb_type = QgsWkbTypes.Polygon
        elif self.geometry_type == 1:
            self.wkb_type = QgsWkbTypes.LineString
        else:
            self.wkb_type = QgsWkbTypes.Point
        if self.parameterAsBoolean(parameters, self.WITH_Z, context):
            self.wkb_type = QgsWkbTypes.addZ(self.wkb_type)
        if self.parameterAsBoolean(parameters, self.WITH_M, context):
            self.wkb_type = QgsWkbTypes.addM(self.wkb_type)

        self.expression = QgsExpression(self.parameterAsString(parameters, self.EXPRESSION, context))
        if self.expression.hasParserError():
            feedback.reportError(self.expression.parserErrorString())
            return False

        self.expression_context = self.createExpressionContext(parameters, context)
        self.expression_context.appendScope(self.parameterAsLayer(parameters, 'INPUT', context).createExpressionContextScope())
        self.expression.prepare(self.expression_context)

        return True

    def outputWkbType(self, input_wkb_type):
        return self.wkb_type

    def inputLayerTypes(self):
        return [QgsProcessing.TypeVector]

    def sourceFlags(self):
        return QgsProcessingFeatureSource.FlagSkipGeometryValidityChecks

    def processFeature(self, feature, context, feedback):
        self.expression_context.setFeature(feature)
        value = self.expression.evaluate(self.expression_context)
        if self.expression.hasEvalError():
            raise QgsProcessingException(
                self.tr('Evaluation error: {0}').format(self.expression.evalErrorString()))

        if not value:
            feature.setGeometry(QgsGeometry())
        else:
            if not isinstance(value, QgsGeometry):
                raise QgsProcessingException(
                    self.tr('{} is not a geometry').format(value))
            feature.setGeometry(value)
        return [feature]
2
  • It would be great if you provided a Pull Request in order to fix the issue in current versions of QGIS. Commented yesterday
  • @AndreaGiudiceandrea sorry, I would love to but I don't speak C++ Commented 20 hours ago
2

An alternative that works with the existing installation is using varibale @layers (not @layer). I creates a list (array) of a map layers in the current project.

Then loop through this array with array_foreach() (lines 1-2 in the expression below). Inside the loop, get the layer name with function layer_property (), getting the layername: layer_property(@element,'name') (line 9). In the end, retain the first element of the array with index operator: [0].

The following expression, based the one you provided, works in batch mode and scales all vector layers in the project, see screenshot below:

array_foreach ( 
    @layers,
    scale(
        geometry:=$geometry,  
        x_scale:=4, 
        y_scale:=4, 
        center:=centroid(
            aggregate(
                layer:= layer_property (@element,'name'), 
                aggregate:='collect', 
                expression:=@geometry
            )
        )
    )
)[0]

enter image description here

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.