2

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 2 days ago
  • @Taras, thanks. I found a more relevant issue (#54869). Commented yesterday

1 Answer 1

3

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

An 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 QgsExpressionContextUtils.layerScope.

This is a diff -u of the two modifications:

@@ -23,6 +23,7 @@ from qgis.core import (QgsWkbTypes, QgsExpression, + QgsExpressionContextUtils, QgsGeometry, QgsProcessing, QgsProcessingAlgorithm, @@ -99,6 +100,7 @@ return False self.expression_context = self.createExpressionContext(parameters, context) + self.expression_context.appendScope(QgsExpressionContextUtils.layerScope(self.parameterAsLayer(parameters, 'INPUT', context))) self.expression.prepare(self.expression_context) 

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, QgsExpressionContextUtils, 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(QgsExpressionContextUtils.layerScope(self.parameterAsLayer(parameters, 'INPUT', context))) 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 5 hours ago
  • @AndreaGiudiceandrea sorry, I would love to but I don't speak C++ Commented 1 hour 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.