Skip to content

Commit

Permalink
[FEATURE][processing] New input type for expressions
Browse files Browse the repository at this point in the history
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 132e76a
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 13 deletions.
6 changes: 3 additions & 3 deletions python/plugins/processing/algs/qgis/ExtractByExpression.py
Expand Up @@ -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


Expand All @@ -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):
Expand Down
6 changes: 3 additions & 3 deletions python/plugins/processing/algs/qgis/GeometryByExpression.py
Expand Up @@ -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

Expand Down Expand Up @@ -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')))

Expand Down
6 changes: 3 additions & 3 deletions python/plugins/processing/algs/qgis/SelectByExpression.py
Expand Up @@ -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


Expand All @@ -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))
Expand Down
49 changes: 49 additions & 0 deletions python/plugins/processing/core/parameters.py
Expand Up @@ -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 = {
Expand Down
68 changes: 64 additions & 4 deletions python/plugins/processing/gui/wrappers.py
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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]'
Expand Down
Expand Up @@ -28,6 +28,7 @@

import math

from qgis.gui import QgsExpressionLineEdit
from qgis.PyQt.QtCore import Qt
from qgis.PyQt.QtWidgets import (QDialog,
QVBoxLayout,
Expand All @@ -47,6 +48,7 @@
ParameterMultipleInput,
ParameterNumber,
ParameterString,
ParameterExpression,
ParameterTableField,
ParameterExtent,
ParameterFile,
Expand All @@ -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'
Expand All @@ -77,6 +80,7 @@ class ModelerParameterDefinitionDialog(QDialog):
PARAMETER_NUMBER,
PARAMETER_RASTER,
PARAMETER_STRING,
PARAMETER_EXPRESSION,
PARAMETER_TABLE,
PARAMETER_TABLE_FIELD,
PARAMETER_VECTOR,
Expand Down Expand Up @@ -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')))
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 132e76a

Please sign in to comment.