diff --git a/python/plugins/processing/algs/qgis/ExtractByExpression.py b/python/plugins/processing/algs/qgis/ExtractByExpression.py index c83b67c5abc..4ce4f175c26 100644 --- a/python/plugins/processing/algs/qgis/ExtractByExpression.py +++ b/python/plugins/processing/algs/qgis/ExtractByExpression.py @@ -27,7 +27,12 @@ __revision__ = '$Format:%H$' from qgis.core import (QgsExpression, QgsFeatureRequest, QgsApplication, - QgsProcessingUtils) + QgsProcessingUtils, + QgsProcessingParameterFeatureSource, + QgsProcessingParameterExpression, + QgsProcessingParameterFeatureSink, + QgsProcessingOutputVectorLayer, + QgsProcessingParameterDefinition) from processing.core.GeoAlgorithmExecutionException import GeoAlgorithmExecutionException from processing.core.parameters import ParameterVector @@ -41,6 +46,7 @@ class ExtractByExpression(QgisAlgorithm): INPUT = 'INPUT' EXPRESSION = 'EXPRESSION' OUTPUT = 'OUTPUT' + FAIL_OUTPUT = 'FAIL_OUTPUT' def icon(self): return QgsApplication.getThemeIcon("/providerQgis.svg") @@ -56,11 +62,16 @@ class ExtractByExpression(QgisAlgorithm): def __init__(self): super().__init__() - self.addParameter(ParameterVector(self.INPUT, - self.tr('Input Layer'))) - self.addParameter(ParameterExpression(self.EXPRESSION, - self.tr("Expression"), parent_layer=self.INPUT)) - self.addOutput(OutputVector(self.OUTPUT, self.tr('Extracted (expression)'))) + self.addParameter(QgsProcessingParameterFeatureSource(self.INPUT, + self.tr('Input layer'))) + self.addParameter(QgsProcessingParameterExpression(self.EXPRESSION, + self.tr('Expression'), None, self.INPUT)) + + self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT, self.tr('Matching features'))) + self.addOutput(QgsProcessingOutputVectorLayer(self.OUTPUT, self.tr('Matching (expression)'))) + self.addParameter(QgsProcessingParameterFeatureSink(self.FAIL_OUTPUT, self.tr('Non-matching'), + QgsProcessingParameterDefinition.TypeVectorAny, None, True)) + self.addOutput(QgsProcessingOutputVectorLayer(self.FAIL_OUTPUT, self.tr('Non-matching (expression)'))) def name(self): return 'extractbyexpression' @@ -69,18 +80,48 @@ class ExtractByExpression(QgisAlgorithm): return self.tr('Extract by expression') def processAlgorithm(self, parameters, context, feedback): - layer = QgsProcessingUtils.mapLayerFromString(self.getParameterValue(self.INPUT), context) - expression_string = self.getParameterValue(self.EXPRESSION) - writer = self.getOutputFromName(self.OUTPUT).getVectorWriter(layer.fields(), layer.wkbType(), layer.crs(), - context) + source = self.parameterAsSource(parameters, self.INPUT, context) + expression_string = self.parameterAsExpression(parameters, self.EXPRESSION, context) + + (matching_sink, matching_sink_id) = self.parameterAsSink(parameters, self.OUTPUT, context, + source.fields(), source.wkbType(), source.sourceCrs()) + (nonmatching_sink, non_matching_sink_id) = self.parameterAsSink(parameters, self.FAIL_OUTPUT, context, + source.fields(), source.wkbType(), source.sourceCrs()) expression = QgsExpression(expression_string) - if not expression.hasParserError(): - req = QgsFeatureRequest().setFilterExpression(expression_string) - else: + if expression.hasParserError(): raise GeoAlgorithmExecutionException(expression.parserErrorString()) + expression_context = self.createExpressionContext(parameters, context) - for f in layer.getFeatures(req): - writer.addFeature(f) + if not nonmatching_sink: + # not saving failing features - so only fetch good features + req = QgsFeatureRequest().setFilterExpression(expression_string) + req.setExpressionContext(expression_context) - del writer + for f in source.getFeatures(req): + if feedback.isCanceled(): + break + matching_sink.addFeature(f) + else: + # saving non-matching features, so we need EVERYTHING + expression_context.setFields(source.fields()) + expression.prepare(expression_context) + + total = 100.0 / source.featureCount() + + for current, f in enumerate(source.getFeatures()): + if feedback.isCanceled(): + break + + expression_context.setFeature(f) + if expression.evaluate(expression_context): + matching_sink.addFeature(f) + else: + nonmatching_sink.addFeature(f) + + feedback.setProgress(int(current * total)) + + results = {self.OUTPUT: matching_sink_id} + if nonmatching_sink: + results[self.FAIL_OUTPUT] = non_matching_sink_id + return results diff --git a/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py b/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py index af89dfc7771..bcb04a165d6 100755 --- a/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py +++ b/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py @@ -48,7 +48,7 @@ from .QgisAlgorithm import QgisAlgorithm # from .RandomExtract import RandomExtract # from .RandomExtractWithinSubsets import RandomExtractWithinSubsets # from .ExtractByLocation import ExtractByLocation -# from .ExtractByExpression import ExtractByExpression +from .ExtractByExpression import ExtractByExpression # from .PointsInPolygon import PointsInPolygon # from .PointsInPolygonUnique import PointsInPolygonUnique # from .PointsInPolygonWeighted import PointsInPolygonWeighted @@ -247,7 +247,7 @@ class QGISAlgorithmProvider(QgsProcessingProvider): # Slope(), Ruggedness(), Hillshade(), # Relief(), ZonalStatisticsQgis(), # IdwInterpolation(), TinInterpolation(), - # RemoveNullGeometry(), ExtractByExpression(), + # RemoveNullGeometry(), # ExtendLines(), ExtractSpecificNodes(), # GeometryByExpression(), SnapGeometriesToLayer(), # PoleOfInaccessibility(), CreateAttributeIndex(), @@ -268,7 +268,8 @@ class QGISAlgorithmProvider(QgsProcessingProvider): CheckValidity(), Clip(), DeleteColumn(), - ExtentFromLayer() + ExtentFromLayer(), + ExtractByExpression() ] if hasPlotly: diff --git a/python/plugins/processing/gui/wrappers.py b/python/plugins/processing/gui/wrappers.py index 2edd5b5a4f2..cf7936aea10 100644 --- a/python/plugins/processing/gui/wrappers.py +++ b/python/plugins/processing/gui/wrappers.py @@ -38,6 +38,7 @@ from qgis.core import ( QgsApplication, QgsCoordinateReferenceSystem, QgsExpression, + QgsExpressionContextGenerator, QgsFieldProxyModel, QgsMapLayerProxyModel, QgsWkbTypes, @@ -1004,6 +1005,8 @@ class ExpressionWidgetWrapper(WidgetWrapper): def setLayer(self, layer): context = dataobjects.createContext() + if isinstance(layer, QgsProcessingFeatureSourceDefinition): + layer, ok = layer.source.valueAsString(context.expressionContext()) if isinstance(layer, str): layer = QgsProcessingUtils.mapLayerFromString(layer, context) self.widget.setLayer(layer) diff --git a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml index de5c066eea5..97917182c5d 100644 --- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml +++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml @@ -1258,18 +1258,18 @@ tests: # name: expected/remove_null_polys.gml # type: vector # -# - algorithm: qgis:extractbyexpression -# name: Extract by Expression -# params: -# EXPRESSION: left( "Name",1)='A' -# INPUT: -# name: polys.gml -# type: vector -# results: -# OUTPUT: -# name: expected/extract_expression.gml -# type: vector -# + - algorithm: qgis:extractbyexpression + name: Extract by Expression + params: + EXPRESSION: left( "Name",1)='A' + INPUT: + name: polys.gml + type: vector + results: + OUTPUT: + name: expected/extract_expression.gml + type: vector + # - algorithm: qgis:extendlines # name: Extend lines # params: