Skip to content
Permalink
Browse files

[FEATURE][processing] New input type for expressions

This adds a new input type for expression inputs. Expression
inputs can be linked to a parent layer so that the builder
shows the correct fields and layer variables.

It's designed for two use cases:

1. to be used when an algorithm specifically requires an expression,
eg Select by Expression and Extract by Expression.

2. to be potentially used as a replacement input instead of string
or number literals in algorithms. Eg - if the simplify algorithm
tolerance parameter was replaced with an expression paremeter, then
this expression would be evaluated for every feature before
simplifying that feature. It would allow parameters to be calculated
per feature, as opposed to the current approach of calculating
a parameter once before running the algorithm. It would also
mean algorithms like "variable distance buffer" would no longer
be needed, as a single "buffer" algorithm could then be used
for either a fixed distance, field based, or expression based
distance.
  • Loading branch information
nyalldawson committed Nov 11, 2016
1 parent 72118f9 commit 132e76a596c71e3d559deef19c36b414c01630ec
@@ -30,7 +30,7 @@
from processing.core.parameters import ParameterVector
from processing.core.outputs import OutputVector
from processing.core.GeoAlgorithm import GeoAlgorithm
from processing.core.parameters import ParameterString
from processing.core.parameters import ParameterString, ParameterExpression
from processing.tools import dataobjects


@@ -46,8 +46,8 @@ def defineCharacteristics(self):

self.addParameter(ParameterVector(self.INPUT,
self.tr('Input Layer')))
self.addParameter(ParameterString(self.EXPRESSION,
self.tr("Expression")))
self.addParameter(ParameterExpression(self.EXPRESSION,
self.tr("Expression"), parent_layer=self.INPUT))
self.addOutput(OutputVector(self.OUTPUT, self.tr('Extracted (expression)')))

def processAlgorithm(self, progress):
@@ -30,7 +30,7 @@
from processing.core.GeoAlgorithm import GeoAlgorithm
from processing.core.GeoAlgorithmExecutionException import GeoAlgorithmExecutionException
from processing.core.ProcessingLog import ProcessingLog
from processing.core.parameters import ParameterVector, ParameterSelection, ParameterBoolean, ParameterString
from processing.core.parameters import ParameterVector, ParameterSelection, ParameterBoolean, ParameterExpression
from processing.core.outputs import OutputVector
from processing.tools import dataobjects, vector

@@ -63,8 +63,8 @@ def defineCharacteristics(self):
self.addParameter(ParameterBoolean(self.WITH_M,
self.tr('Output geometry has m values'), False))

self.addParameter(ParameterString(self.EXPRESSION,
self.tr("Geometry expression"), '$geometry'))
self.addParameter(ParameterExpression(self.EXPRESSION,
self.tr("Geometry expression"), '$geometry', parent_layer=self.INPUT_LAYER))

self.addOutput(OutputVector(self.OUTPUT_LAYER, self.tr('Modified geometry')))

@@ -30,7 +30,7 @@
from processing.core.parameters import ParameterSelection
from processing.core.outputs import OutputVector
from processing.core.GeoAlgorithm import GeoAlgorithm
from processing.core.parameters import ParameterString
from processing.core.parameters import ParameterExpression
from processing.tools import dataobjects


@@ -52,8 +52,8 @@ def defineCharacteristics(self):

self.addParameter(ParameterVector(self.LAYERNAME,
self.tr('Input Layer')))
self.addParameter(ParameterString(self.EXPRESSION,
self.tr("Expression")))
self.addParameter(ParameterExpression(self.EXPRESSION,
self.tr("Expression"), parent_layer=self.LAYERNAME))
self.addParameter(ParameterSelection(self.METHOD,
self.tr('Modify current selection by'), self.methods, 0))
self.addOutput(OutputVector(self.RESULT, self.tr('Selected (expression)'), True))
@@ -1210,6 +1210,55 @@ def expressionContext(self):
return _expressionContext()


class ParameterExpression(Parameter):

default_metadata = {
'widget_wrapper': 'processing.gui.wrappers.ExpressionWidgetWrapper'
}

NEWLINE = '\n'
ESCAPED_NEWLINE = '\\n'

def __init__(self, name='', description='', default=None, optional=False, parent_layer=None):
Parameter.__init__(self, name, description, default, optional)
self.parent_layer = parent_layer

def setValue(self, obj):
if not bool(obj):
if not self.optional:
return False
self.value = None
return True

self.value = str(obj).replace(
ParameterString.ESCAPED_NEWLINE,
ParameterString.NEWLINE
)
return True

def getValueAsCommandLineParameter(self):
return ('"' + str(self.value.replace(ParameterExpression.NEWLINE,
ParameterExpression.ESCAPED_NEWLINE)) + '"'
if self.value is not None else str(None))

def getAsScriptCode(self):
param_type = ''
if self.optional:
param_type += 'optional '
param_type += 'expression'
return '##' + self.name + '=' + param_type + self.default

@classmethod
def fromScriptCode(self, line):
isOptional, name, definition = _splitParameterOptions(line)
descName = _createDescriptiveName(name)
default = definition.strip()[len('expression') + 1:]
if default:
return ParameterExpression(name, descName, default, optional=isOptional)
else:
return ParameterExpression(name, descName, optional=isOptional)


class ParameterTable(ParameterDataObject):

default_metadata = {
@@ -36,16 +36,28 @@

from qgis.core import QgsCoordinateReferenceSystem, QgsVectorLayer
from qgis.PyQt.QtWidgets import QCheckBox, QComboBox, QLineEdit, QPlainTextEdit
from qgis.gui import QgsFieldExpressionWidget
from qgis.PyQt.QtCore import pyqtSignal, QObject, QVariant

from processing.gui.NumberInputPanel import NumberInputPanel
from processing.gui.InputLayerSelectorPanel import InputLayerSelectorPanel
from processing.modeler.MultilineTextPanel import MultilineTextPanel
from processing.gui.CrsSelectionPanel import CrsSelectionPanel
from processing.gui.PointSelectionPanel import PointSelectionPanel
from processing.core.parameters import (ParameterBoolean, ParameterPoint, ParameterFile,
ParameterRaster, ParameterVector, ParameterNumber, ParameterString, ParameterTable,
ParameterTableField, ParameterExtent, ParameterFixedTable, ParameterCrs, _resolveLayers)
from processing.core.parameters import (ParameterBoolean,
ParameterPoint,
ParameterFile,
ParameterRaster,
ParameterVector,
ParameterNumber,
ParameterString,
ParameterExpression,
ParameterTable,
ParameterTableField,
ParameterExtent,
ParameterFixedTable,
ParameterCrs,
_resolveLayers)
from processing.core.ProcessingConfig import ProcessingConfig
from processing.gui.FileSelectionPanel import FileSelectionPanel
from processing.core.outputs import (OutputFile, OutputRaster, OutputVector, OutputNumber,
@@ -645,7 +657,7 @@ def createWidget(self):
else:
# strings, numbers, files and table fields are all allowed input types
strings = self.dialog.getAvailableValuesOfType([ParameterString, ParameterNumber, ParameterFile,
ParameterTableField], OutputString)
ParameterTableField, ParameterExpression], OutputString)
options = [(self.dialog.resolveValueDescription(s), s) for s in strings]
if self.param.multiline:
widget = MultilineTextPanel(options)
@@ -698,6 +710,54 @@ def validator(v):
return self.comboValue(validator)


class ExpressionWidgetWrapper(WidgetWrapper):

def createWidget(self):
if self.dialogType in (DIALOG_STANDARD, DIALOG_BATCH):
widget = QgsFieldExpressionWidget()
if self.param.default:
widget.setExpression(self.param.default)
else:
strings = self.dialog.getAvailableValuesOfType([ParameterExpression, ParameterString, ParameterNumber], OutputString)
options = [(self.dialog.resolveValueDescription(s), s) for s in strings]
widget = QComboBox()
widget.setEditable(True)
for desc, val in options:
widget.addItem(desc, val)
widget.setEditText(self.param.default or "")
return widget

def postInitialize(self, wrappers):
for wrapper in wrappers:
if wrapper.param.name == self.param.parent_layer:
if self.dialogType in (DIALOG_STANDARD, DIALOG_BATCH):
self.setLayer(wrapper.value())
wrapper.widgetValueHasChanged.connect(self.parentLayerChanged)
break

def parentLayerChanged(self, wrapper):
self.setLayer(wrapper.value())

def setLayer(self, layer):
if isinstance(layer, str):
layer = dataobjects.getObjectFromUri(_resolveLayers(layer))
self.widget.setLayer(layer)

def setValue(self, value):
if self.dialogType in (DIALOG_STANDARD, DIALOG_BATCH):
self.widget.setExpression(value)
else:
self.setComboValue(value)

def value(self):
if self.dialogType in (DIALOG_STANDARD, DIALOG_BATCH):
return self.widget.asExpression()
else:
def validator(v):
return bool(v) or self.param.optional
return self.comboValue(validator)


class TableWidgetWrapper(WidgetWrapper):

NOT_SELECTED = '[Not selected]'
@@ -28,6 +28,7 @@

import math

from qgis.gui import QgsExpressionLineEdit
from qgis.PyQt.QtCore import Qt
from qgis.PyQt.QtWidgets import (QDialog,
QVBoxLayout,
@@ -47,6 +48,7 @@
ParameterMultipleInput,
ParameterNumber,
ParameterString,
ParameterExpression,
ParameterTableField,
ParameterExtent,
ParameterFile,
@@ -62,6 +64,7 @@ class ModelerParameterDefinitionDialog(QDialog):
PARAMETER_TABLE = 'Table'
PARAMETER_VECTOR = 'Vector layer'
PARAMETER_STRING = 'String'
PARAMETER_EXPRESSION = 'Expression'
PARAMETER_BOOLEAN = 'Boolean'
PARAMETER_TABLE_FIELD = 'Table field'
PARAMETER_EXTENT = 'Extent'
@@ -77,6 +80,7 @@ class ModelerParameterDefinitionDialog(QDialog):
PARAMETER_NUMBER,
PARAMETER_RASTER,
PARAMETER_STRING,
PARAMETER_EXPRESSION,
PARAMETER_TABLE,
PARAMETER_TABLE_FIELD,
PARAMETER_VECTOR,
@@ -196,6 +200,25 @@ def setupUi(self):
if default:
self.defaultTextBox.setText(str(default))
self.verticalLayout.addWidget(self.defaultTextBox)
elif (self.paramType == ModelerParameterDefinitionDialog.PARAMETER_EXPRESSION or
isinstance(self.param, ParameterExpression)):
self.verticalLayout.addWidget(QLabel(self.tr('Default value')))
self.defaultEdit = QgsExpressionLineEdit()
if self.param is not None:
self.defaultEdit.setExpression(self.param.default)
self.verticalLayout.addWidget(self.defaultEdit)

self.verticalLayout.addWidget(QLabel(self.tr('Parent layer')))
self.parentCombo = QComboBox()
idx = 0
for param in list(self.alg.inputs.values()):
if isinstance(param.param, (ParameterVector, ParameterTable)):
self.parentCombo.addItem(param.param.description, param.param.name)
if self.param is not None:
if self.param.parent_layer == param.param.name:
self.parentCombo.setCurrentIndex(idx)
idx += 1
self.verticalLayout.addWidget(self.parentCombo)
elif (self.paramType == ModelerParameterDefinitionDialog.PARAMETER_STRING or
isinstance(self.param, ParameterString)):
self.verticalLayout.addWidget(QLabel(self.tr('Default value')))
@@ -314,6 +337,12 @@ def okPressed(self):
QMessageBox.warning(self, self.tr('Unable to define parameter'),
self.tr('Wrong or missing parameter values'))
return
elif (self.paramType == ModelerParameterDefinitionDialog.PARAMETER_EXPRESSION or
isinstance(self.param, ParameterExpression)):
parent = self.parentCombo.itemData(self.parentCombo.currentIndex())
self.param = ParameterExpression(name, description,
default=str(self.defaultEdit.expression()),
parent_layer=parent)
elif (self.paramType == ModelerParameterDefinitionDialog.PARAMETER_STRING or
isinstance(self.param, ParameterString)):
self.param = ParameterString(name, description,

0 comments on commit 132e76a

Please sign in to comment.
You can’t perform that action at this time.