Skip to content

Commit

Permalink
Resurrect Field Calculator algorithm, add test
Browse files Browse the repository at this point in the history
  • Loading branch information
nyalldawson committed Aug 20, 2017
1 parent d2a90f4 commit 6144b1c
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 97 deletions.
154 changes: 71 additions & 83 deletions python/plugins/processing/algs/qgis/FieldsCalculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,35 +29,31 @@
from qgis.core import (QgsExpression,
QgsExpressionContext,
QgsExpressionContextUtils,
QgsFeature,
QgsFeatureSink,
QgsField,
QgsDistanceArea,
QgsProject,
QgsApplication,
QgsProcessingUtils)
QgsProcessingParameterFeatureSource,
QgsProcessingParameterEnum,
QgsProcessingParameterNumber,
QgsProcessingParameterBoolean,
QgsProcessingParameterExpression,
QgsProcessingParameterString,
QgsProcessingParameterFeatureSink,
QgsProcessingException)
from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm
from processing.core.GeoAlgorithmExecutionException import GeoAlgorithmExecutionException
from processing.core.parameters import ParameterVector
from processing.core.parameters import ParameterString
from processing.core.parameters import ParameterNumber
from processing.core.parameters import ParameterBoolean
from processing.core.parameters import ParameterSelection
from processing.core.outputs import OutputVector

from .ui.FieldsCalculatorDialog import FieldsCalculatorDialog


class FieldsCalculator(QgisAlgorithm):

INPUT_LAYER = 'INPUT_LAYER'
INPUT = 'INPUT'
NEW_FIELD = 'NEW_FIELD'
FIELD_NAME = 'FIELD_NAME'
FIELD_TYPE = 'FIELD_TYPE'
FIELD_LENGTH = 'FIELD_LENGTH'
FIELD_PRECISION = 'FIELD_PRECISION'
FORMULA = 'FORMULA'
OUTPUT_LAYER = 'OUTPUT_LAYER'
OUTPUT = 'OUTPUT'

TYPES = [QVariant.Double, QVariant.Int, QVariant.String, QVariant.Date]

Expand All @@ -66,27 +62,26 @@ def group(self):

def __init__(self):
super().__init__()

def initAlgorithm(self, config=None):
self.type_names = [self.tr('Float'),
self.tr('Integer'),
self.tr('String'),
self.tr('Date')]

self.addParameter(ParameterVector(self.INPUT_LAYER,
self.tr('Input layer')))
self.addParameter(ParameterString(self.FIELD_NAME,
self.tr('Result field name')))
self.addParameter(ParameterSelection(self.FIELD_TYPE,
self.tr('Field type'), self.type_names))
self.addParameter(ParameterNumber(self.FIELD_LENGTH,
self.tr('Field length'), 1, 255, 10))
self.addParameter(ParameterNumber(self.FIELD_PRECISION,
self.tr('Field precision'), 0, 15, 3))
self.addParameter(ParameterBoolean(self.NEW_FIELD,
self.tr('Create new field'), True))
self.addParameter(ParameterString(self.FORMULA, self.tr('Formula')))
self.addOutput(OutputVector(self.OUTPUT_LAYER, self.tr('Calculated')))
def initAlgorithm(self, config=None):
self.addParameter(QgsProcessingParameterFeatureSource(self.INPUT, self.tr('Input layer')))
self.addParameter(QgsProcessingParameterString(self.FIELD_NAME,
self.tr('Result field name')))
self.addParameter(QgsProcessingParameterEnum(self.FIELD_TYPE,
self.tr('Field type'), options=self.type_names))
self.addParameter(QgsProcessingParameterNumber(self.FIELD_LENGTH,
self.tr('Field length'), minValue=1, maxValue=255, defaultValue=10))
self.addParameter(QgsProcessingParameterNumber(self.FIELD_PRECISION,
self.tr('Field precision'), minValue=0, maxValue=15, defaultValue=3))
self.addParameter(QgsProcessingParameterBoolean(self.NEW_FIELD,
self.tr('Create new field'), defaultValue=True))
self.addParameter(QgsProcessingParameterExpression(self.FORMULA, self.tr('Formula')))
self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT,
self.tr('Calculated')))

def name(self):
return 'fieldcalculator'
Expand All @@ -95,77 +90,70 @@ def displayName(self):
return self.tr('Field calculator')

def processAlgorithm(self, parameters, context, feedback):
layer = QgsProcessingUtils.mapLayerFromString(self.getParameterValue(self.INPUT_LAYER), context)
fieldName = self.getParameterValue(self.FIELD_NAME)
fieldType = self.TYPES[self.getParameterValue(self.FIELD_TYPE)]
width = self.getParameterValue(self.FIELD_LENGTH)
precision = self.getParameterValue(self.FIELD_PRECISION)
newField = self.getParameterValue(self.NEW_FIELD)
formula = self.getParameterValue(self.FORMULA)

output = self.getOutputFromName(self.OUTPUT_LAYER)

fields = layer.fields()
if newField:
fields.append(QgsField(fieldName, fieldType, '', width, precision))

writer = output.getVectorWriter(fields, layer.wkbType(), layer.crs(), context)

exp = QgsExpression(formula)

source = self.parameterAsSource(parameters, self.INPUT, context)
layer = self.parameterAsVectorLayer(parameters, self.INPUT, context)
field_name = self.parameterAsString(parameters, self.FIELD_NAME, context)
field_type = self.TYPES[self.parameterAsEnum(parameters, self.FIELD_TYPE, context)]
width = self.parameterAsInt(parameters, self.FIELD_LENGTH, context)
precision = self.parameterAsInt(parameters, self.FIELD_PRECISION, context)
new_field = self.parameterAsBool(parameters, self.NEW_FIELD, context)
formula = self.parameterAsString(parameters, self.FORMULA, context)

expression = QgsExpression(formula)
da = QgsDistanceArea()
da.setSourceCrs(layer.crs())
da.setSourceCrs(source.sourceCrs())
da.setEllipsoid(context.project().ellipsoid())
exp.setGeomCalculator(da)
exp.setDistanceUnits(context.project().distanceUnits())
exp.setAreaUnits(context.project().areaUnits())
expression.setGeomCalculator(da)

exp_context = QgsExpressionContext(QgsExpressionContextUtils.globalProjectLayerScopes(layer))
expression.setDistanceUnits(context.project().distanceUnits())
expression.setAreaUnits(context.project().areaUnits())

if not exp.prepare(exp_context):
raise GeoAlgorithmExecutionException(
self.tr('Evaluation error: {0}').format(exp.evalErrorString()))
fields = source.fields()
field_index = fields.lookupField(field_name)
if new_field or field_index < 0:
fields.append(QgsField(field_name, field_type, '', width, precision))

outFeature = QgsFeature()
outFeature.initAttributes(len(fields))
outFeature.setFields(fields)
(sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
fields, source.wkbType(), source.sourceCrs())

error = ''
calculationSuccess = True
exp_context = self.createExpressionContext(parameters, context)
if layer is not None:
exp_context.appendScope(QgsExpressionContextUtils.layerScope(layer))

features = QgsProcessingUtils.getFeatures(layer, context)
total = 100.0 / layer.featureCount() if layer.featureCount() else 0
if not expression.prepare(exp_context):
raise QgsProcessingException(
self.tr('Evaluation error: {0}').format(expression.parserErrorString()))

features = source.getFeatures()
total = 100.0 / source.featureCount() if source.featureCount() else 0

rownum = 1
for current, f in enumerate(features):
if feedback.isCanceled():
break

rownum = current + 1
exp_context.setFeature(f)
exp_context.lastScope().setVariable("row_number", rownum)
value = exp.evaluate(exp_context)
if exp.hasEvalError():
calculationSuccess = False
error = exp.evalErrorString()
break
value = expression.evaluate(exp_context)
if expression.hasEvalError():
feedback.reportError(expression.evalErrorString())
else:
outFeature.setGeometry(f.geometry())
for fld in f.fields():
outFeature[fld.name()] = f[fld.name()]
outFeature[fieldName] = value
writer.addFeature(outFeature, QgsFeatureSink.FastInsert)

attrs = f.attributes()
if new_field or field_index < 0:
attrs.append(value)
else:
attrs[field_index] = value
f.setAttributes(attrs)
sink.addFeature(f, QgsFeatureSink.FastInsert)
feedback.setProgress(int(current * total))
del writer

if not calculationSuccess:
raise GeoAlgorithmExecutionException(
self.tr('An error occurred while evaluating the calculation '
'string:\n{0}').format(error))
return {self.OUTPUT: dest_id}

def checkParameterValues(self, parameters, context):
newField = self.getParameterValue(self.NEW_FIELD)
fieldName = self.getParameterValue(self.FIELD_NAME).strip()
newField = self.parameterAsBool(parameters, self.NEW_FIELD, context)
fieldName = self.parameterAsString(parameters, self.FIELD_NAME, context).strip()
if newField and len(fieldName) == 0:
return self.tr('Field name is not set. Please enter a field name')
return False, self.tr('Field name is not set. Please enter a field name')
return super(FieldsCalculator, self).checkParameterValues(parameters, context)

def createCustomParametersWidget(self, parent):
Expand Down
5 changes: 3 additions & 2 deletions python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
from .ExtentFromLayer import ExtentFromLayer
from .ExtractNodes import ExtractNodes
from .ExtractSpecificNodes import ExtractSpecificNodes
from .FieldsCalculator import FieldsCalculator
from .FieldsMapper import FieldsMapper
from .FixedDistanceBuffer import FixedDistanceBuffer
from .FixGeometry import FixGeometry
Expand Down Expand Up @@ -166,7 +167,6 @@
# from .SelectByLocation import SelectByLocation
# from .SpatialJoin import SpatialJoin
# from .GeometryConvert import GeometryConvert
# from .FieldsCalculator import FieldsCalculator
# from .FieldPyculator import FieldsPyculator
# from .SelectByAttributeSum import SelectByAttributeSum
# from .DefineProjection import DefineProjection
Expand All @@ -190,7 +190,7 @@ def getAlgs(self):
# SelectByLocation(),
# ExtractByLocation(),
# SpatialJoin(),
# GeometryConvert(), FieldsCalculator(),
# GeometryConvert(),
# FieldsPyculator(),
# FieldsMapper(), SelectByAttributeSum()
# DefineProjection(),
Expand Down Expand Up @@ -227,6 +227,7 @@ def getAlgs(self):
ExtentFromLayer(),
ExtractNodes(),
ExtractSpecificNodes(),
FieldsCalculator(),
FieldsMapper(),
FixedDistanceBuffer(),
FixGeometry(),
Expand Down
34 changes: 23 additions & 11 deletions python/plugins/processing/algs/qgis/ui/FieldsCalculatorDialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@
from qgis.core import (QgsExpressionContextUtils,
QgsProcessingFeedback,
QgsSettings,
QgsProcessingUtils,
QgsMapLayerProxyModel,
QgsMessageLog)
QgsProperty,
QgsProject,
QgsMessageLog,
QgsProcessingOutputLayerDefinition)
from qgis.gui import QgsEncodingFileDialog
from qgis.utils import OverrideCursor

Expand All @@ -47,6 +49,8 @@
from processing.gui.AlgorithmExecutor import execute
from processing.tools import dataobjects
from processing.gui.Postprocessing import handleAlgorithmResults
from processing.gui.PostgisTableSelector import PostgisTableSelector
from processing.gui.ParameterGuiUtils import getFileFilter

pluginPath = os.path.dirname(__file__)
WIDGET, BASE = uic.loadUiType(
Expand All @@ -73,9 +77,6 @@ def __init__(self, alg):
super(FieldsCalculatorDialog, self).__init__(None)
self.setupUi(self)

self.feedback = FieldCalculatorFeedback(self)
self.feedback.progressChanged.connect(self.setPercentage)

self.executed = False
self.alg = alg
self.layer = None
Expand Down Expand Up @@ -144,8 +145,8 @@ def setupSpinboxes(self, index):
self.mOutputFieldPrecisionSpinBox.setEnabled(False)

def selectFile(self):
output = self.alg.getOutputFromName('OUTPUT_LAYER')
fileFilter = output.getFileFilter(self.alg)
output = self.alg.parameterDefinition('OUTPUT')
fileFilter = getFileFilter(output)

settings = QgsSettings()
if settings.contains('/Processing/LastOutputPath'):
Expand Down Expand Up @@ -200,17 +201,23 @@ def getParamValues(self):

layer = self.cmbInputLayer.currentLayer()

context = dataobjects.createContext()

parameters = {}
parameters['INPUT_LAYER'] = layer
parameters['INPUT'] = layer
parameters['FIELD_NAME'] = fieldName
parameters['FIELD_TYPE'] = self.mOutputFieldTypeComboBox.currentIndex()
parameters['FIELD_LENGTH'] = self.mOutputFieldWidthSpinBox.value()
parameters['FIELD_PRECISION'] = self.mOutputFieldPrecisionSpinBox.value()
parameters['NEW_FIELD'] = self.mNewFieldGroupBox.isChecked()
parameters['FORMULA'] = self.builder.expressionText()
parameters['OUTPUT_LAYER'] = self.leOutputFile.text().strip() or None

context = dataobjects.createContext()
output = QgsProcessingOutputLayerDefinition()
if self.leOutputFile.text().strip():
output.sink = QgsProperty.fromValue(self.leOutputFile.text().strip())
else:
output.sink = QgsProperty.fromValue('memory:')
output.destinationProject = context.project()
parameters['OUTPUT'] = output

ok, msg = self.alg.checkParameterValues(parameters, context)
if not ok:
Expand All @@ -224,10 +231,15 @@ def accept(self):
parameters = self.getParamValues()
if parameters:
with OverrideCursor(Qt.WaitCursor):
self.feedback = FieldCalculatorFeedback(self)
self.feedback.progressChanged.connect(self.setPercentage)

context = dataobjects.createContext()
ProcessingLog.addToLog(self.alg.asPythonCommand(parameters, context))

self.executed, results = execute(self.alg, parameters, context, self.feedback)
self.setPercentage(0)

if self.executed:
handleAlgorithmResults(self.alg,
context,
Expand Down
5 changes: 4 additions & 1 deletion python/plugins/processing/gui/TestTools.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@
QgsProcessingParameterRasterDestination,
QgsProcessingParameterFeatureSink,
QgsProcessingParameterVectorDestination,
QgsProcessingParameterFileDestination)
QgsProcessingParameterFileDestination,
QgsProcessingParameterEnum)
from qgis.PyQt.QtCore import QCoreApplication, QMetaObject
from qgis.PyQt.QtWidgets import QDialog, QVBoxLayout, QTextEdit, QMessageBox

Expand Down Expand Up @@ -223,6 +224,8 @@ def createTest(text):
params[param.name()] = int(token)
else:
params[param.name()] = float(token)
elif isinstance(param, QgsProcessingParameterEnum):
params[param.name()] = int(token)
else:
if token[0] == '"':
token = token[1:]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<GMLFeatureClassList>
<GMLFeatureClass>
<Name>field_calculator_points</Name>
<ElementPath>field_calculator_points</ElementPath>
<!--POINT-->
<GeometryType>1</GeometryType>
<SRSName>EPSG:4326</SRSName>
<DatasetSpecificInfo>
<FeatureCount>9</FeatureCount>
<ExtentXMin>0.00000</ExtentXMin>
<ExtentXMax>8.00000</ExtentXMax>
<ExtentYMin>-5.00000</ExtentYMin>
<ExtentYMax>3.00000</ExtentYMax>
</DatasetSpecificInfo>
<PropertyDefn>
<Name>id</Name>
<ElementPath>id</ElementPath>
<Type>Integer</Type>
</PropertyDefn>
<PropertyDefn>
<Name>id2</Name>
<ElementPath>id2</ElementPath>
<Type>Integer</Type>
</PropertyDefn>
<PropertyDefn>
<Name>calc</Name>
<ElementPath>calc</ElementPath>
<Type>Integer</Type>
</PropertyDefn>
</GMLFeatureClass>
</GMLFeatureClassList>
Loading

0 comments on commit 6144b1c

Please sign in to comment.