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 Nov 29 at 2:35

2 Answers 2

6

This is a bug (#54869) in the native (C++) Geometry By Expression tool as the tool is missing the input layer scope.

Instead, use the Processing parameter(name) function and pass it the 'INPUT' parameter, e.g.

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

Expression

Alternatively, the Geometry By Expression tool was ported from python to native C++ commit eec0ac0 for QGIS 3.12 and you can 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 yesterday
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

3
  • Doesn't seem to work correctly when there's multiple layers in the map, looks like it takes the centroid of all layers. E.g. with one layer in map correct output and with two layers in map incorrect output. Commented 7 hours ago
  • I was concentrating on using a variable to generate layer-names to avoid hard-coding, not so much on the very task to perform with geometry by expression as this did not seem to be the core of the question, but rather an example. This should probably be asked in a separate question. Commented 6 hours ago
  • Yes the question wasn't about the actual expression, but about accessing the input layer name. Your answer is a good effort and I learned something from it but I don't think it actually answers the question as asked (even disregarding the geometry issue I showed in the above comment) as it doesn't give you the input layer name but all layer names. Have figured it out though, see my updated answer that uses the parameter( 'INPUT') function. Commented 4 hours ago

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.