Nyall Dawson 37b899fb99 Add a pure virtual clone method to processing parameter definitions
And use it when we need to clone parameters (instead of more fragile
conversion to and from variants)

This fixes model loading which use algorithms which create python
subclasses of parameter definitions
2017-08-18 01:22:07 +10:00

274 lines
11 KiB
Python

# -*- coding: utf-8 -*-
"""
***************************************************************************
Aggregate.py
---------------------
Date : February 2017
Copyright : (C) 2017 by Arnaud Morvan
Email : arnaud dot morvan at camptocamp 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__ = 'Arnaud Morvan'
__date__ = 'February 2017'
__copyright__ = '(C) 2017, Arnaud Morvan'
# This will get replaced with a git SHA1 when you do a git archive
__revision__ = '$Format:%H$'
from qgis.core import (
QgsDistanceArea,
QgsExpression,
QgsExpressionContextUtils,
QgsFeature,
QgsFeatureSink,
QgsField,
QgsFields,
QgsGeometry,
QgsProcessingParameterDefinition,
QgsProcessingParameterExpression,
QgsProcessingParameterFeatureSink,
QgsProcessingParameterFeatureSource,
QgsProcessingException,
QgsProcessingUtils,
QgsWkbTypes,
)
from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm
class Aggregate(QgisAlgorithm):
INPUT = 'INPUT'
GROUP_BY = 'GROUP_BY'
AGGREGATES = 'AGGREGATES'
DISSOLVE = 'DISSOLVE'
OUTPUT = 'OUTPUT'
def group(self):
return self.tr('Vector geometry tools')
def name(self):
return 'aggregate'
def displayName(self):
return self.tr('Aggregate')
def initAlgorithm(self, config=None):
self.addParameter(QgsProcessingParameterFeatureSource(self.INPUT,
self.tr('Input layer')))
self.addParameter(QgsProcessingParameterExpression(self.GROUP_BY,
self.tr('Group by expression (NULL to group all features)'),
defaultValue='NULL',
optional=False,
parentLayerParameterName=self.INPUT))
class ParameterAggregates(QgsProcessingParameterDefinition):
def __init__(self, name, description, parentLayerParameterName='INPUT'):
super().__init__(name, description)
self._parentLayerParameter = parentLayerParameterName
def clone(self):
copy = ParameterAggregates(self.name(), self.description(), self._parentLayerParameter)
return copy
def type(self):
return 'aggregates'
def checkValueIsAcceptable(self, value, context=None):
if not isinstance(value, list):
return False
for field_def in value:
if not isinstance(field_def, dict):
return False
if not field_def.get('input', False):
return False
if not field_def.get('aggregate', False):
return False
if not field_def.get('name', False):
return False
if not field_def.get('type', False):
return False
return True
def valueAsPythonString(self, value, context):
return str(value)
def asScriptCode(self):
raise NotImplementedError()
@classmethod
def fromScriptCode(cls, name, description, isOptional, definition):
raise NotImplementedError()
def parentLayerParameter(self):
return self._parentLayerParameter
self.addParameter(ParameterAggregates(self.AGGREGATES,
description=self.tr('Aggregates')))
self.parameterDefinition(self.AGGREGATES).setMetadata({
'widget_wrapper': 'processing.algs.qgis.ui.AggregatesPanel.AggregatesWidgetWrapper'
})
self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT,
self.tr('Aggregated')))
def parameterAsAggregates(self, parameters, name, context):
return parameters[name]
def prepareAlgorithm(self, parameters, context, feedback):
source = self.parameterAsSource(parameters, self.INPUT, context)
group_by = self.parameterAsExpression(parameters, self.GROUP_BY, context)
aggregates = self.parameterAsAggregates(parameters, self.AGGREGATES, context)
da = QgsDistanceArea()
da.setSourceCrs(source.sourceCrs())
da.setEllipsoid(context.project().ellipsoid())
self.source = source
self.group_by = group_by
self.group_by_expr = self.createExpression(group_by, da, context)
self.geometry_expr = self.createExpression('collect($geometry, {})'.format(group_by), da, context)
self.fields = QgsFields()
self.fields_expr = []
for field_def in aggregates:
self.fields.append(QgsField(name=field_def['name'],
type=field_def['type'],
typeName="",
len=field_def['length'],
prec=field_def['precision']))
aggregate = field_def['aggregate']
if aggregate == 'first_value':
expression = field_def['input']
elif aggregate == 'concatenate':
expression = ('{}({}, {}, {}, \'{}\')'
.format(field_def['aggregate'],
field_def['input'],
group_by,
'TRUE',
field_def['delimiter']))
else:
expression = '{}({}, {})'.format(field_def['aggregate'],
field_def['input'],
group_by)
expr = self.createExpression(expression, da, context)
self.fields_expr.append(expr)
return True
def processAlgorithm(self, parameters, context, feedback):
expr_context = self.createExpressionContext(parameters, context)
self.group_by_expr.prepare(expr_context)
# Group features in memory layers
source = self.source
count = self.source.featureCount()
if count:
progress_step = 50.0 / count
current = 0
groups = {}
keys = [] # We need deterministic order for the tests
feature = QgsFeature()
for feature in self.source.getFeatures():
expr_context.setFeature(feature)
group_by_value = self.evaluateExpression(self.group_by_expr, expr_context)
# Get an hashable key for the dict
key = group_by_value
if isinstance(key, list):
key = tuple(key)
group = groups.get(key, None)
if group is None:
sink, id = QgsProcessingUtils.createFeatureSink(
'memory:',
context,
source.fields(),
source.wkbType(),
source.sourceCrs())
layer = QgsProcessingUtils.mapLayerFromString(id, context)
group = {
'sink': sink,
'layer': layer,
'feature': feature
}
groups[key] = group
keys.append(key)
group['sink'].addFeature(feature, QgsFeatureSink.FastInsert)
current += 1
feedback.setProgress(int(current * progress_step))
if feedback.isCanceled():
return
(sink, dest_id) = self.parameterAsSink(parameters,
self.OUTPUT,
context,
self.fields,
QgsWkbTypes.multiType(source.wkbType()),
source.sourceCrs())
# Calculate aggregates on memory layers
if len(keys):
progress_step = 50.0 / len(keys)
for current, key in enumerate(keys):
group = groups[key]
expr_context = self.createExpressionContext(parameters, context)
expr_context.appendScope(QgsExpressionContextUtils.layerScope(group['layer']))
expr_context.setFeature(group['feature'])
geometry = self.evaluateExpression(self.geometry_expr, expr_context)
if geometry is not None and not geometry.isEmpty():
geometry = QgsGeometry.unaryUnion(geometry.asGeometryCollection())
if geometry.isEmpty():
raise QgsProcessingException(
'Impossible to combine geometries for {} = {}'
.format(self.group_by, group_by_value))
attrs = []
for fields_expr in self.fields_expr:
attrs.append(self.evaluateExpression(fields_expr, expr_context))
# Write output feature
outFeat = QgsFeature()
if geometry is not None:
outFeat.setGeometry(geometry)
outFeat.setAttributes(attrs)
sink.addFeature(outFeat, QgsFeatureSink.FastInsert)
feedback.setProgress(50 + int(current * progress_step))
if feedback.isCanceled():
return
return {self.OUTPUT: dest_id}
def createExpression(self, text, da, context):
expr = QgsExpression(text)
expr.setGeomCalculator(da)
expr.setDistanceUnits(context.project().distanceUnits())
expr.setAreaUnits(context.project().areaUnits())
if expr.hasParserError():
raise QgsProcessingException(
self.tr(u'Parser error in expression "{}": {}')
.format(text, expr.parserErrorString()))
return expr
def evaluateExpression(self, expr, context):
value = expr.evaluate(context)
if expr.hasEvalError():
raise QgsProcessingException(
self.tr(u'Evaluation error in expression "{}": {}')
.format(expr.expression(), expr.evalErrorString()))
return value