|
| 1 | +# -*- coding: utf-8 -*- |
| 2 | + |
| 3 | +""" |
| 4 | +*************************************************************************** |
| 5 | + Aggregate.py |
| 6 | + --------------------- |
| 7 | + Date : February 2017 |
| 8 | + Copyright : (C) 2017 by Arnaud Morvan |
| 9 | + Email : arnaud dot morvan at camptocamp dot com |
| 10 | +*************************************************************************** |
| 11 | +* * |
| 12 | +* This program is free software; you can redistribute it and/or modify * |
| 13 | +* it under the terms of the GNU General Public License as published by * |
| 14 | +* the Free Software Foundation; either version 2 of the License, or * |
| 15 | +* (at your option) any later version. * |
| 16 | +* * |
| 17 | +*************************************************************************** |
| 18 | +""" |
| 19 | + |
| 20 | +__author__ = 'Arnaud Morvan' |
| 21 | +__date__ = 'February 2017' |
| 22 | +__copyright__ = '(C) 2017, Arnaud Morvan' |
| 23 | + |
| 24 | +# This will get replaced with a git SHA1 when you do a git archive |
| 25 | + |
| 26 | +__revision__ = '$Format:%H$' |
| 27 | + |
| 28 | +from qgis.core import ( |
| 29 | + QgsDistanceArea, |
| 30 | + QgsExpression, |
| 31 | + QgsExpressionContextUtils, |
| 32 | + QgsFeature, |
| 33 | + QgsFeatureSink, |
| 34 | + QgsField, |
| 35 | + QgsFields, |
| 36 | + QgsGeometry, |
| 37 | + QgsProcessingParameterDefinition, |
| 38 | + QgsProcessingParameterExpression, |
| 39 | + QgsProcessingParameterFeatureSink, |
| 40 | + QgsProcessingParameterFeatureSource, |
| 41 | + QgsProcessingException, |
| 42 | + QgsProcessingUtils, |
| 43 | + QgsWkbTypes, |
| 44 | +) |
| 45 | + |
| 46 | +from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm |
| 47 | + |
| 48 | + |
| 49 | +class Aggregate(QgisAlgorithm): |
| 50 | + |
| 51 | + INPUT = 'INPUT' |
| 52 | + GROUP_BY = 'GROUP_BY' |
| 53 | + AGGREGATES = 'AGGREGATES' |
| 54 | + DISSOLVE = 'DISSOLVE' |
| 55 | + OUTPUT = 'OUTPUT' |
| 56 | + |
| 57 | + def group(self): |
| 58 | + return self.tr('Vector geometry tools') |
| 59 | + |
| 60 | + def name(self): |
| 61 | + return 'aggregate' |
| 62 | + |
| 63 | + def displayName(self): |
| 64 | + return self.tr('Aggregate') |
| 65 | + |
| 66 | + def initAlgorithm(self, config=None): |
| 67 | + self.addParameter(QgsProcessingParameterFeatureSource(self.INPUT, |
| 68 | + self.tr('Input layer'))) |
| 69 | + self.addParameter(QgsProcessingParameterExpression(self.GROUP_BY, |
| 70 | + self.tr('Group by expression (NULL to group all features)'), |
| 71 | + defaultValue='NULL', |
| 72 | + optional=False, |
| 73 | + parentLayerParameterName=self.INPUT)) |
| 74 | + |
| 75 | + class ParameterAggregates(QgsProcessingParameterDefinition): |
| 76 | + |
| 77 | + def __init__(self, name, description, parentLayerParameterName='INPUT'): |
| 78 | + super().__init__(name, description) |
| 79 | + self._parentLayerParameter = parentLayerParameterName |
| 80 | + |
| 81 | + def type(self): |
| 82 | + return 'aggregates' |
| 83 | + |
| 84 | + def checkValueIsAcceptable(self, value, context=None): |
| 85 | + if not isinstance(value, list): |
| 86 | + return False |
| 87 | + for field_def in value: |
| 88 | + if not isinstance(field_def, dict): |
| 89 | + return False |
| 90 | + if not field_def.get('input', False): |
| 91 | + return False |
| 92 | + if not field_def.get('aggregate', False): |
| 93 | + return False |
| 94 | + if not field_def.get('name', False): |
| 95 | + return False |
| 96 | + if not field_def.get('type', False): |
| 97 | + return False |
| 98 | + return True |
| 99 | + |
| 100 | + def valueAsPythonString(self, value, context): |
| 101 | + return str(value) |
| 102 | + |
| 103 | + def asScriptCode(self): |
| 104 | + raise NotImplementedError() |
| 105 | + |
| 106 | + @classmethod |
| 107 | + def fromScriptCode(cls, name, description, isOptional, definition): |
| 108 | + raise NotImplementedError() |
| 109 | + |
| 110 | + def parentLayerParameter(self): |
| 111 | + return self._parentLayerParameter |
| 112 | + |
| 113 | + self.addParameter(ParameterAggregates(self.AGGREGATES, |
| 114 | + description=self.tr('Aggregates'))) |
| 115 | + self.parameterDefinition(self.AGGREGATES).setMetadata({ |
| 116 | + 'widget_wrapper': 'processing.algs.qgis.ui.AggregatesPanel.AggregatesWidgetWrapper' |
| 117 | + }) |
| 118 | + |
| 119 | + self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT, |
| 120 | + self.tr('Aggregated'))) |
| 121 | + |
| 122 | + def parameterAsAggregates(self, parameters, name, context): |
| 123 | + return parameters[name] |
| 124 | + |
| 125 | + def prepareAlgorithm(self, parameters, context, feedback): |
| 126 | + source = self.parameterAsSource(parameters, self.INPUT, context) |
| 127 | + group_by = self.parameterAsExpression(parameters, self.GROUP_BY, context) |
| 128 | + aggregates = self.parameterAsAggregates(parameters, self.AGGREGATES, context) |
| 129 | + |
| 130 | + da = QgsDistanceArea() |
| 131 | + da.setSourceCrs(source.sourceCrs()) |
| 132 | + da.setEllipsoid(context.project().ellipsoid()) |
| 133 | + |
| 134 | + self.source = source |
| 135 | + self.group_by = group_by |
| 136 | + self.group_by_expr = self.createExpression(group_by, da, context) |
| 137 | + self.geometry_expr = self.createExpression('collect($geometry, {})'.format(group_by), da, context) |
| 138 | + |
| 139 | + self.fields = QgsFields() |
| 140 | + self.fields_expr = [] |
| 141 | + for field_def in aggregates: |
| 142 | + self.fields.append(QgsField(name=field_def['name'], |
| 143 | + type=field_def['type'], |
| 144 | + typeName="", |
| 145 | + len=field_def['length'], |
| 146 | + prec=field_def['precision'])) |
| 147 | + aggregate = field_def['aggregate'] |
| 148 | + if aggregate == 'first_value': |
| 149 | + expression = field_def['input'] |
| 150 | + elif aggregate == 'concatenate': |
| 151 | + expression = ('{}({}, {}, {}, \'{}\')' |
| 152 | + .format(field_def['aggregate'], |
| 153 | + field_def['input'], |
| 154 | + group_by, |
| 155 | + 'TRUE', |
| 156 | + field_def['delimiter'])) |
| 157 | + else: |
| 158 | + expression = '{}({}, {})'.format(field_def['aggregate'], |
| 159 | + field_def['input'], |
| 160 | + group_by) |
| 161 | + expr = self.createExpression(expression, da, context) |
| 162 | + self.fields_expr.append(expr) |
| 163 | + return True |
| 164 | + |
| 165 | + def processAlgorithm(self, parameters, context, feedback): |
| 166 | + expr_context = self.createExpressionContext(parameters, context) |
| 167 | + self.group_by_expr.prepare(expr_context) |
| 168 | + |
| 169 | + # Group features in memory layers |
| 170 | + source = self.source |
| 171 | + count = self.source.featureCount() |
| 172 | + if count: |
| 173 | + progress_step = 50.0 / count |
| 174 | + current = 0 |
| 175 | + groups = {} |
| 176 | + keys = [] # We need deterministic order for the tests |
| 177 | + feature = QgsFeature() |
| 178 | + for feature in self.source.getFeatures(): |
| 179 | + expr_context.setFeature(feature) |
| 180 | + group_by_value = self.evaluateExpression(self.group_by_expr, expr_context) |
| 181 | + |
| 182 | + # Get an hashable key for the dict |
| 183 | + key = group_by_value |
| 184 | + if isinstance(key, list): |
| 185 | + key = tuple(key) |
| 186 | + |
| 187 | + group = groups.get(key, None) |
| 188 | + if group is None: |
| 189 | + sink, id = QgsProcessingUtils.createFeatureSink( |
| 190 | + 'memory:', |
| 191 | + context, |
| 192 | + source.fields(), |
| 193 | + source.wkbType(), |
| 194 | + source.sourceCrs()) |
| 195 | + layer = QgsProcessingUtils.mapLayerFromString(id, context) |
| 196 | + group = { |
| 197 | + 'sink': sink, |
| 198 | + 'layer': layer, |
| 199 | + 'feature': feature |
| 200 | + } |
| 201 | + groups[key] = group |
| 202 | + keys.append(key) |
| 203 | + |
| 204 | + group['sink'].addFeature(feature, QgsFeatureSink.FastInsert) |
| 205 | + |
| 206 | + current += 1 |
| 207 | + feedback.setProgress(int(current * progress_step)) |
| 208 | + if feedback.isCanceled(): |
| 209 | + return |
| 210 | + |
| 211 | + (sink, dest_id) = self.parameterAsSink(parameters, |
| 212 | + self.OUTPUT, |
| 213 | + context, |
| 214 | + self.fields, |
| 215 | + QgsWkbTypes.multiType(source.wkbType()), |
| 216 | + source.sourceCrs()) |
| 217 | + |
| 218 | + # Calculate aggregates on memory layers |
| 219 | + if len(keys): |
| 220 | + progress_step = 50.0 / len(keys) |
| 221 | + for current, key in enumerate(keys): |
| 222 | + group = groups[key] |
| 223 | + expr_context = self.createExpressionContext(parameters, context) |
| 224 | + expr_context.appendScope(QgsExpressionContextUtils.layerScope(group['layer'])) |
| 225 | + expr_context.setFeature(group['feature']) |
| 226 | + |
| 227 | + geometry = self.evaluateExpression(self.geometry_expr, expr_context) |
| 228 | + if geometry is not None and not geometry.isEmpty(): |
| 229 | + geometry = QgsGeometry.unaryUnion(geometry.asGeometryCollection()) |
| 230 | + if geometry.isEmpty(): |
| 231 | + raise QgsProcessingException( |
| 232 | + 'Impossible to combine geometries for {} = {}' |
| 233 | + .format(self.group_by, group_by_value)) |
| 234 | + |
| 235 | + attrs = [] |
| 236 | + for fields_expr in self.fields_expr: |
| 237 | + attrs.append(self.evaluateExpression(fields_expr, expr_context)) |
| 238 | + |
| 239 | + # Write output feature |
| 240 | + outFeat = QgsFeature() |
| 241 | + if geometry is not None: |
| 242 | + outFeat.setGeometry(geometry) |
| 243 | + outFeat.setAttributes(attrs) |
| 244 | + sink.addFeature(outFeat, QgsFeatureSink.FastInsert) |
| 245 | + |
| 246 | + feedback.setProgress(50 + int(current * progress_step)) |
| 247 | + if feedback.isCanceled(): |
| 248 | + return |
| 249 | + |
| 250 | + return {self.OUTPUT: dest_id} |
| 251 | + |
| 252 | + def createExpression(self, text, da, context): |
| 253 | + expr = QgsExpression(text) |
| 254 | + expr.setGeomCalculator(da) |
| 255 | + expr.setDistanceUnits(context.project().distanceUnits()) |
| 256 | + expr.setAreaUnits(context.project().areaUnits()) |
| 257 | + if expr.hasParserError(): |
| 258 | + raise QgsProcessingException( |
| 259 | + self.tr(u'Parser error in expression "{}": {}') |
| 260 | + .format(text, expr.parserErrorString())) |
| 261 | + return expr |
| 262 | + |
| 263 | + def evaluateExpression(self, expr, context): |
| 264 | + value = expr.evaluate(context) |
| 265 | + if expr.hasEvalError(): |
| 266 | + raise QgsProcessingException( |
| 267 | + self.tr(u'Evaluation error in expression "{}": {}') |
| 268 | + .format(expr.expression(), expr.evalErrorString())) |
| 269 | + return value |
0 commit comments