From 4b5d81b3703e8b97701b6cfc089fe9bf9e9ce3f5 Mon Sep 17 00:00:00 2001 From: "arnaud.morvan@camptocamp.com" Date: Thu, 4 May 2017 13:38:14 +0200 Subject: [PATCH] [processing] Add Aggregate algorithm --- python/plugins/processing/algs/help/qgis.yaml | 11 + .../plugins/processing/algs/qgis/Aggregate.py | 269 ++++++++++++++++++ .../algs/qgis/QGISAlgorithmProvider.py | 2 + .../algs/qgis/ui/AggregatesPanel.py | 184 ++++++++++++ .../algs/qgis/ui/FieldsMappingPanel.py | 5 +- .../tests/testdata/expected/aggregate_all.gfs | 38 +++ .../tests/testdata/expected/aggregate_all.gml | 22 ++ .../testdata/expected/aggregate_field.gfs | 38 +++ .../testdata/expected/aggregate_field.gml | 56 ++++ .../testdata/expected/aggregate_lines.gfs | 22 ++ .../testdata/expected/aggregate_lines.gml | 19 ++ .../testdata/expected/aggregate_points.gfs | 33 +++ .../testdata/expected/aggregate_points.gml | 37 +++ .../expected/aggregate_two_fields.gfs | 38 +++ .../expected/aggregate_two_fields.gml | 63 ++++ .../tests/testdata/qgis_algorithm_tests.yaml | 198 +++++++++++++ 16 files changed, 1031 insertions(+), 4 deletions(-) create mode 100644 python/plugins/processing/algs/qgis/Aggregate.py create mode 100644 python/plugins/processing/algs/qgis/ui/AggregatesPanel.py create mode 100644 python/plugins/processing/tests/testdata/expected/aggregate_all.gfs create mode 100644 python/plugins/processing/tests/testdata/expected/aggregate_all.gml create mode 100644 python/plugins/processing/tests/testdata/expected/aggregate_field.gfs create mode 100644 python/plugins/processing/tests/testdata/expected/aggregate_field.gml create mode 100644 python/plugins/processing/tests/testdata/expected/aggregate_lines.gfs create mode 100644 python/plugins/processing/tests/testdata/expected/aggregate_lines.gml create mode 100644 python/plugins/processing/tests/testdata/expected/aggregate_points.gfs create mode 100644 python/plugins/processing/tests/testdata/expected/aggregate_points.gml create mode 100644 python/plugins/processing/tests/testdata/expected/aggregate_two_fields.gfs create mode 100644 python/plugins/processing/tests/testdata/expected/aggregate_two_fields.gml 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 2a97476caf14..898ea7f1ef67 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: