diff --git a/python/plugins/processing/algs/help/qgis.yaml b/python/plugins/processing/algs/help/qgis.yaml
index 9f3e6816b6e4..0eb07954e2b3 100755
--- a/python/plugins/processing/algs/help/qgis.yaml
+++ b/python/plugins/processing/algs/help/qgis.yaml
@@ -20,6 +20,17 @@ qgis:adduniquevalueindexfield: >
qgis:advancedpythonfieldcalculator: >
This algorithm adds a new attribute to a vector layer, with values resulting from applying an expression to each feature. The expression is defined as a Python function.
+qgis:aggregate: >
+ This algorithm take a vector or table layer and aggregate features based on a group by expression. Features for which group by expression return the same value are grouped together.
+
+ It is possible to group all source features together using constant value in group by parameter, example: NULL.
+
+ It is also possible to group features using multiple fields using Array function, example: Array("Field1", "Field2").
+
+ Geometries (if present) are combined into one multipart geometry for each group.
+
+ Output attributes are computed depending on each given aggregate definition.
+
qgis:barplot:
qgis:basicstatisticsforfields: >
diff --git a/python/plugins/processing/algs/qgis/Aggregate.py b/python/plugins/processing/algs/qgis/Aggregate.py
new file mode 100644
index 000000000000..60c84b11e86a
--- /dev/null
+++ b/python/plugins/processing/algs/qgis/Aggregate.py
@@ -0,0 +1,269 @@
+# -*- 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 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
diff --git a/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py b/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py
index 83614f28225f..84b0ec3039f1 100644
--- a/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py
+++ b/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py
@@ -41,6 +41,7 @@
from .QgisAlgorithm import QgisAlgorithm
from .AddTableField import AddTableField
+from .Aggregate import Aggregate
from .Aspect import Aspect
from .AutoincrementalField import AutoincrementalField
from .BasicStatistics import BasicStatisticsForField
@@ -204,6 +205,7 @@ def getAlgs(self):
# ExecuteSQL(), FindProjection(),
# ]
algs = [AddTableField(),
+ Aggregate(),
Aspect(),
AutoincrementalField(),
BasicStatisticsForField(),
diff --git a/python/plugins/processing/algs/qgis/ui/AggregatesPanel.py b/python/plugins/processing/algs/qgis/ui/AggregatesPanel.py
new file mode 100644
index 000000000000..1a321b592e93
--- /dev/null
+++ b/python/plugins/processing/algs/qgis/ui/AggregatesPanel.py
@@ -0,0 +1,184 @@
+# -*- coding: utf-8 -*-
+
+"""
+***************************************************************************
+ AggregatesPanel.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.PyQt.QtCore import (
+ QItemSelectionModel,
+ QAbstractTableModel,
+ QModelIndex,
+ QVariant,
+ Qt,
+ pyqtSlot,
+)
+from qgis.PyQt.QtGui import QBrush
+from qgis.PyQt.QtWidgets import (
+ QComboBox,
+ QHeaderView,
+ QLineEdit,
+ QSpacerItem,
+ QMessageBox,
+ QSpinBox,
+ QStyledItemDelegate,
+)
+
+from qgis.core import QgsExpression
+
+from processing.algs.qgis.ui.FieldsMappingPanel import (
+ ExpressionDelegate,
+ FieldsMappingModel,
+ FieldsMappingPanel,
+ FieldsMappingWidgetWrapper,
+ FieldTypeDelegate,
+)
+
+
+AGGREGATES = ['first_value']
+for function in QgsExpression.Functions():
+ if function.name()[0] == '_':
+ continue
+ if function.isDeprecated():
+ continue
+ # if ( func->isContextual() ):
+ if "Aggregates" in function.groups():
+ if function.name() in ('aggregate',
+ 'relation_aggregate'):
+ continue
+ AGGREGATES.append(function.name())
+AGGREGATES = sorted(AGGREGATES)
+
+
+class AggregatesModel(FieldsMappingModel):
+
+ def configure(self):
+ self.columns = [{
+ 'name': 'input',
+ 'type': QgsExpression,
+ 'header': self.tr("Input expression"),
+ 'persistentEditor': True
+ }, {
+ 'name': 'aggregate',
+ 'type': QVariant.String,
+ 'header': self.tr("Aggregate function"),
+ 'persistentEditor': True
+ }, {
+ 'name': 'delimiter',
+ 'type': QVariant.String,
+ 'header': self.tr("Delimiter")
+ }, {
+ 'name': 'name',
+ 'type': QVariant.String,
+ 'header': self.tr("Output field name")
+ }, {
+ 'name': 'type',
+ 'type': QVariant.Type,
+ 'header': self.tr("Type"),
+ 'persistentEditor': True
+ }, {
+ 'name': 'length',
+ 'type': QVariant.Int,
+ 'header': self.tr("Length")
+ }, {
+ 'name': 'precision',
+ 'type': QVariant.Int,
+ 'header': self.tr("Precision")
+ }]
+
+ def newField(self, field=None):
+ if field is None:
+ return {
+ 'input': '',
+ 'aggregate': '',
+ 'delimiter': '',
+ 'name': '',
+ 'type': QVariant.Invalid,
+ 'length': 0,
+ 'precision': 0,
+ }
+
+ default_aggregate = ''
+ if field.type() in (QVariant.Int,
+ QVariant.Double,
+ QVariant.LongLong):
+ default_aggregate = 'sum'
+ if field.type() == QVariant.DateTime:
+ default_aggregate = ''
+ if field.type() == QVariant.String:
+ default_aggregate = 'concatenate'
+
+ return {
+ 'input': QgsExpression.quotedColumnRef(field.name()),
+ 'aggregate': default_aggregate,
+ 'delimiter': ',',
+ 'name': field.name(),
+ 'type': field.type(),
+ 'length': field.length(),
+ 'precision': field.precision(),
+ }
+
+
+class AggregateDelegate(QStyledItemDelegate):
+
+ def __init__(self, parent=None):
+ super(AggregateDelegate, self).__init__(parent)
+
+ def createEditor(self, parent, option, index):
+ editor = QComboBox(parent)
+ for function in AGGREGATES:
+ editor.addItem(function, function)
+ return editor
+
+ def setEditorData(self, editor, index):
+ if not editor:
+ return
+ value = index.model().data(index, Qt.EditRole)
+ editor.setCurrentIndex(editor.findData(value))
+
+ def setModelData(self, editor, model, index):
+ if not editor:
+ return
+ value = editor.currentData()
+ if value is None:
+ value = QVariant.Invalid
+ model.setData(index, value)
+
+
+class AggregatesPanel(FieldsMappingPanel):
+
+ def configure(self):
+ self.model = AggregatesModel()
+ self.fieldsView.setModel(self.model)
+ self.model.rowsInserted.connect(self.on_model_rowsInserted)
+
+ self.setDelegate('input', ExpressionDelegate(self))
+ self.setDelegate('aggregate', AggregateDelegate(self))
+ self.setDelegate('type', FieldTypeDelegate(self))
+
+
+class AggregatesWidgetWrapper(FieldsMappingWidgetWrapper):
+
+ def createWidget(self, parentLayerParameterName='INPUT'):
+ self._parentLayerParameter = parentLayerParameterName
+ return AggregatesPanel()
diff --git a/python/plugins/processing/algs/qgis/ui/FieldsMappingPanel.py b/python/plugins/processing/algs/qgis/ui/FieldsMappingPanel.py
index 1956b6c282e7..0c3b1417c7ef 100644
--- a/python/plugins/processing/algs/qgis/ui/FieldsMappingPanel.py
+++ b/python/plugins/processing/algs/qgis/ui/FieldsMappingPanel.py
@@ -27,11 +27,9 @@
__revision__ = '$Format:%H$'
import os
-
from collections import OrderedDict
from qgis.PyQt import uic
-
from qgis.PyQt.QtCore import (
QItemSelectionModel,
QAbstractTableModel,
@@ -40,7 +38,6 @@
Qt,
pyqtSlot,
)
-from qgis.PyQt.QtGui import QBrush
from qgis.PyQt.QtWidgets import (
QComboBox,
QHeaderView,
@@ -65,6 +62,7 @@
from processing.gui.wrappers import WidgetWrapper, DIALOG_STANDARD, DIALOG_MODELER
from processing.tools import dataobjects
+
pluginPath = os.path.dirname(__file__)
WIDGET, BASE = uic.loadUiType(
os.path.join(pluginPath, 'fieldsmappingpanelbase.ui'))
@@ -83,7 +81,6 @@ class FieldsMappingModel(QAbstractTableModel):
def __init__(self, parent=None):
super(FieldsMappingModel, self).__init__(parent)
self._mapping = []
- self._errors = []
self._layer = None
self.configure()
diff --git a/python/plugins/processing/tests/testdata/expected/aggregate_all.gfs b/python/plugins/processing/tests/testdata/expected/aggregate_all.gfs
new file mode 100644
index 000000000000..4d68b54e4986
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/aggregate_all.gfs
@@ -0,0 +1,38 @@
+
+
+ aggregate_all
+ aggregate_all
+
+ 6
+ EPSG:4326
+
+ 1
+ -1.00000
+ 9.16296
+ -3.00000
+ 6.08868
+
+
+ fids
+ fids
+ String
+ 79
+
+
+ name
+ name
+ String
+ 27
+
+
+ intval
+ intval
+ Integer
+
+
+ floatval
+ floatval
+ Real
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/aggregate_all.gml b/python/plugins/processing/tests/testdata/expected/aggregate_all.gml
new file mode 100644
index 000000000000..37a6c1187fc5
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/aggregate_all.gml
@@ -0,0 +1,22 @@
+
+
+
+
+ -1-3
+ 9.1629558541266826.088675623800385
+
+
+
+
+ 8.16295585412668,2.73877159309021 8.16295585412668,3.73877159309021 9.16295585412668,3.73877159309021 9.16295585412668,2.73877159309021 8.16295585412668,2.738771593090216.24145873320538,-0.054510556621882 7.24145873320538,-1.05451055662188 6.0,-1.05451055662188 6,-3 2,-1 -1,-1 -1,3 3,3 3,2 6,1 6.0,-0.295969289827257 6.24145873320538,-0.0545105566218824.17255278310941,4.82264875239923 4.17255278310941,5.82264875239923 5.17255278310941,5.82264875239923 5.17255278310941,4.82264875239923 4.17255278310941,4.822648752399232.44337811900192,4.42360844529751 2.44337811900192,5.0 2,5 2,6 2.62072936660269,6.0 2.62072936660269,6.08867562380038 3.62072936660269,6.08867562380038 3.62072936660269,5.08867562380038 3.44337811900192,5.08867562380038 3.44337811900192,4.42360844529751 2.44337811900192,4.42360844529751
+ polys.0,polys.1,polys.2,polys.3,polys.4,polys.5,polys.6,polys.7,polys.9,polys.8
+ aa,dd,bb,,aa,bb,bb,cc,dd,bb
+ 127
+ -11138.1515193333
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/aggregate_field.gfs b/python/plugins/processing/tests/testdata/expected/aggregate_field.gfs
new file mode 100644
index 000000000000..51ce108576cc
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/aggregate_field.gfs
@@ -0,0 +1,38 @@
+
+
+ aggregate_field
+ aggregate_field
+
+ 6
+ EPSG:4326
+
+ 5
+ -1.00000
+ 9.16296
+ -3.00000
+ 6.08868
+
+
+ fids
+ fids
+ String
+ 31
+
+
+ name
+ name
+ String
+ 2
+
+
+ intval
+ intval
+ Integer
+
+
+ floatval
+ floatval
+ Real
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/aggregate_field.gml b/python/plugins/processing/tests/testdata/expected/aggregate_field.gml
new file mode 100644
index 000000000000..9a37f0823f2e
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/aggregate_field.gml
@@ -0,0 +1,56 @@
+
+
+
+
+ -1-3
+ 9.1629558541266826.088675623800385
+
+
+
+
+ 3,2 6,1 6,-3 2,-1 -1,-1 -1,3 3,3 3,2
+ polys.0,polys.4
+ aa
+ 2
+ 23.726728
+
+
+
+
+ 6.24145873320538,-0.054510556621882 7.24145873320538,-1.05451055662188 5.24145873320538,-1.05451055662188 6.24145873320538,-0.054510556621882
+ polys.1,polys.9
+ dd
+ 0
+ 0
+
+
+
+
+ 4.17255278310941,4.82264875239923 4.17255278310941,5.82264875239923 5.17255278310941,5.82264875239923 5.17255278310941,4.82264875239923 4.17255278310941,4.822648752399232.44337811900192,4.42360844529751 2.44337811900192,5.0 2,5 2,6 2.62072936660269,6.0 2.62072936660269,6.08867562380038 3.62072936660269,6.08867562380038 3.62072936660269,5.08867562380038 3.44337811900192,5.08867562380038 3.44337811900192,4.42360844529751 2.44337811900192,4.42360844529751
+ polys.2,polys.5,polys.6,polys.8
+ bb
+ 5
+ 0.123
+
+
+
+
+ polys.3
+ 120
+ -100291.43213
+
+
+
+
+ 8.16295585412668,2.73877159309021 8.16295585412668,3.73877159309021 9.16295585412668,3.73877159309021 9.16295585412668,2.73877159309021 8.16295585412668,2.73877159309021
+ polys.7
+ cc
+ 0
+ 0.123
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/aggregate_lines.gfs b/python/plugins/processing/tests/testdata/expected/aggregate_lines.gfs
new file mode 100644
index 000000000000..d11631b4421a
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/aggregate_lines.gfs
@@ -0,0 +1,22 @@
+
+
+ aggregate_lines
+ aggregate_lines
+
+ 5
+ EPSG:4326
+
+ 1
+ -1.00000
+ 11.00000
+ -3.00000
+ 5.00000
+
+
+ fids
+ fids
+ String
+ 55
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/aggregate_lines.gml b/python/plugins/processing/tests/testdata/expected/aggregate_lines.gml
new file mode 100644
index 000000000000..a112bba196d8
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/aggregate_lines.gml
@@ -0,0 +1,19 @@
+
+
+
+
+ -1-3
+ 115
+
+
+
+
+ -1,-1 1,-17,-3 10,-36,2 9,2 9,3 11,56,-3 10,13,1 5,12,0 2,2 3,2 3,3
+ lines.0,lines.1,lines.2,lines.3,lines.4,lines.5,lines.6
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/aggregate_points.gfs b/python/plugins/processing/tests/testdata/expected/aggregate_points.gfs
new file mode 100644
index 000000000000..bc00d0ed2d68
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/aggregate_points.gfs
@@ -0,0 +1,33 @@
+
+
+ aggregate_points
+ aggregate_points
+
+ 4
+ EPSG:4326
+
+ 3
+ 0.00000
+ 8.00000
+ -5.00000
+ 3.00000
+
+
+ fids
+ fids
+ String
+ 44
+
+
+ ids
+ ids
+ String
+ 9
+
+
+ id2
+ id2
+ Integer
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/aggregate_points.gml b/python/plugins/processing/tests/testdata/expected/aggregate_points.gml
new file mode 100644
index 000000000000..b875968433ff
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/aggregate_points.gml
@@ -0,0 +1,37 @@
+
+
+
+
+ 0-5
+ 83
+
+
+
+
+ 1,15,2
+ points.0,points.3
+ 1,4
+ 2
+
+
+
+
+ 3,34,1
+ points.1,points.4
+ 2,5
+ 1
+
+
+
+
+ 0,-50,-12,27,-18,-1
+ points.2,points.5,points.6,points.7,points.8
+ 3,6,7,8,9
+ 0
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/aggregate_two_fields.gfs b/python/plugins/processing/tests/testdata/expected/aggregate_two_fields.gfs
new file mode 100644
index 000000000000..f6bd169e6448
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/aggregate_two_fields.gfs
@@ -0,0 +1,38 @@
+
+
+ aggregate_two_fields
+ aggregate_two_fields
+
+ 6
+ EPSG:4326
+
+ 6
+ -1.00000
+ 9.16296
+ -3.00000
+ 6.08868
+
+
+ fids
+ fids
+ String
+ 23
+
+
+ name
+ name
+ String
+ 2
+
+
+ intval
+ intval
+ Integer
+
+
+ floatval
+ floatval
+ Real
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/aggregate_two_fields.gml b/python/plugins/processing/tests/testdata/expected/aggregate_two_fields.gml
new file mode 100644
index 000000000000..4159a48786ee
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/aggregate_two_fields.gml
@@ -0,0 +1,63 @@
+
+
+
+
+ -1-3
+ 9.1629558541266826.088675623800385
+
+
+
+
+ 3,2 6,1 6,-3 2,-1 -1,-1 -1,3 3,3 3,2
+ polys.0,polys.4
+ aa
+ 1
+ 23.726728
+
+
+
+
+ 6.24145873320538,-0.054510556621882 7.24145873320538,-1.05451055662188 5.24145873320538,-1.05451055662188 6.24145873320538,-0.054510556621882
+ polys.1,polys.9
+ dd
+ 0
+
+
+
+
+ 4.17255278310941,4.82264875239923 4.17255278310941,5.82264875239923 5.17255278310941,5.82264875239923 5.17255278310941,4.82264875239923 4.17255278310941,4.822648752399232.44337811900192,4.42360844529751 2.44337811900192,5.0 2,5 2,6 3,6 3.0,5.42360844529751 3.44337811900192,5.42360844529751 3.44337811900192,4.42360844529751 2.44337811900192,4.42360844529751
+ polys.2,polys.5,polys.6
+ bb
+ 1
+ 0.123
+
+
+
+
+ polys.3
+ 120
+ -100291.43213
+
+
+
+
+ 8.16295585412668,2.73877159309021 8.16295585412668,3.73877159309021 9.16295585412668,3.73877159309021 9.16295585412668,2.73877159309021 8.16295585412668,2.73877159309021
+ polys.7
+ cc
+ 0.123
+
+
+
+
+ 2.62072936660269,5.08867562380038 2.62072936660269,6.08867562380038 3.62072936660269,6.08867562380038 3.62072936660269,5.08867562380038 2.62072936660269,5.08867562380038
+ polys.8
+ bb
+ 2
+ 0.123
+
+
+
diff --git a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml
index 9cdf58451b0e..ca90ca0ca179 100644
--- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml
+++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml
@@ -16,6 +16,204 @@ tests:
geometry:
precision: 7
+ - name: Aggregate all
+ algorithm: qgis:aggregate
+ params:
+ INPUT:
+ name: dissolve_polys.gml
+ type: vector
+ GROUP_BY: 'NULL'
+ AGGREGATES:
+ [{
+ input: 'fid',
+ aggregate: 'concatenate',
+ delimiter: ',',
+ name: 'fids',
+ type: 10,
+ length: 255,
+ precision: 0
+ }, {
+ input: 'name',
+ aggregate: 'concatenate',
+ delimiter: ',',
+ name: 'name',
+ type: 10,
+ length: 255,
+ precision: 0
+ }, {
+ input: 'intval',
+ aggregate: 'sum',
+ delimiter: '',
+ name: 'intval',
+ type: 2,
+ length: 0,
+ precision: 0
+ }, {
+ aggregate: 'mean',
+ input: 'floatval',
+ type: 6,
+ delimiter: '',
+ name: 'floatval',
+ length: 0,
+ precision: 0
+ }]
+ results:
+ OUTPUT:
+ name: expected/aggregate_all.gml
+ type: vector
+
+ - name: Aggregate using field
+ algorithm: qgis:aggregate
+ params:
+ INPUT:
+ name: dissolve_polys.gml
+ type: vector
+ GROUP_BY: '"name"'
+ AGGREGATES:
+ [{
+ input: 'fid',
+ aggregate: 'concatenate',
+ delimiter: ',',
+ name: 'fids',
+ type: 10,
+ length: 50,
+ precision: 0
+ }, {
+ input: 'name',
+ aggregate: 'first_value',
+ delimiter: ',',
+ name: 'name',
+ type: 10,
+ length: 2,
+ precision: 0
+ }, {
+ input: 'intval',
+ aggregate: 'sum',
+ delimiter: '',
+ name: 'intval',
+ type: 2,
+ length: 0,
+ precision: 0
+ }, {
+ input: 'floatval',
+ aggregate: 'mean',
+ delimiter: '',
+ name: 'floatval',
+ type: 6,
+ length: 0,
+ precision: 0
+ }]
+ results:
+ OUTPUT:
+ name: expected/aggregate_field.gml
+ type: vector
+
+ - algorithm: qgis:aggregate
+ name: Aggregate using two fields
+ params:
+ INPUT:
+ name: dissolve_polys.gml
+ type: vector
+ GROUP_BY: array("intval", "name")
+ AGGREGATES:
+ [{
+ input: 'fid',
+ aggregate: 'concatenate',
+ delimiter: ',',
+ name: 'fids',
+ type: 10,
+ length: 80,
+ precision: 0
+ }, {
+ input: 'name',
+ aggregate: 'first_value',
+ delimiter: ',',
+ name: 'name',
+ type: 10,
+ length: 2,
+ precision: 0
+ }, {
+ input: 'intval',
+ aggregate: 'first_value',
+ delimiter: '',
+ name: 'intval',
+ type: 2,
+ length: 0,
+ precision: 0
+ }, {
+ input: 'floatval',
+ aggregate: 'mean',
+ delimiter: '',
+ name: 'floatval',
+ type: 6,
+ length: 0,
+ precision: 0
+ }]
+ results:
+ OUTPUT:
+ name: expected/aggregate_two_fields.gml
+ type: vector
+
+ - name: Aggregate points
+ algorithm: qgis:aggregate
+ params:
+ INPUT:
+ name: points.gml
+ type: vector
+ GROUP_BY: '"id2"'
+ AGGREGATES:
+ [{
+ input: 'fid',
+ aggregate: 'concatenate',
+ delimiter: ',',
+ name: 'fids',
+ type: 10,
+ length: 50,
+ precision: 0
+ }, {
+ input: 'to_string("id")',
+ aggregate: 'concatenate',
+ delimiter: ',',
+ name: 'ids',
+ type: 10,
+ length: 50,
+ precision: 0
+ }, {
+ input: 'id2',
+ aggregate: 'first_value',
+ delimiter: '',
+ name: 'id2',
+ type: 6,
+ length: 0,
+ precision: 0
+ }]
+ results:
+ OUTPUT:
+ name: expected/aggregate_points.gml
+ type: vector
+
+ - name: Aggregate lines
+ algorithm: qgis:aggregate
+ params:
+ INPUT:
+ name: lines.gml
+ type: vector
+ GROUP_BY: 'NULL'
+ AGGREGATES:
+ [{
+ input: 'fid',
+ aggregate: 'concatenate',
+ delimiter: ',',
+ name: 'fids',
+ type: 10,
+ length: 255,
+ precision: 0
+ }]
+ results:
+ OUTPUT:
+ name: expected/aggregate_lines.gml
+ type: vector
+
- name: Delete Holes
algorithm: qgis:deleteholes
params: