Branch data Line data Source code
1 : : /***************************************************************************
2 : : qgsalgorithmaggregate.h
3 : : ---------------------------------
4 : : begin : June 2020
5 : : copyright : (C) 2020 by Nyall Dawson
6 : : email : nyall dot dawson at gmail dot com
7 : : ***************************************************************************/
8 : :
9 : : /***************************************************************************
10 : : * *
11 : : * This program is free software; you can redistribute it and/or modify *
12 : : * it under the terms of the GNU General Public License as published by *
13 : : * the Free Software Foundation; either version 2 of the License, or *
14 : : * (at your option) any later version. *
15 : : * *
16 : : ***************************************************************************/
17 : :
18 : : #include "qgsalgorithmaggregate.h"
19 : : #include "qgsprocessingparameteraggregate.h"
20 : : #include "qgsexpressioncontextutils.h"
21 : :
22 : : ///@cond PRIVATE
23 : :
24 : 0 : QString QgsAggregateAlgorithm::name() const
25 : : {
26 : 0 : return QStringLiteral( "aggregate" );
27 : : }
28 : :
29 : 0 : QString QgsAggregateAlgorithm::displayName() const
30 : : {
31 : 0 : return QObject::tr( "Aggregate" );
32 : : }
33 : :
34 : 0 : QString QgsAggregateAlgorithm::shortHelpString() const
35 : : {
36 : 0 : return QObject::tr( "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.\n\n"
37 : : "It is possible to group all source features together using constant value in group by parameter, example: NULL.\n\n"
38 : : "It is also possible to group features using multiple fields using Array function, example: Array(\"Field1\", \"Field2\").\n\n"
39 : : "Geometries (if present) are combined into one multipart geometry for each group.\n\n"
40 : : "Output attributes are computed depending on each given aggregate definition." );
41 : : }
42 : :
43 : 0 : QStringList QgsAggregateAlgorithm::tags() const
44 : : {
45 : 0 : return QObject::tr( "attributes,sum,mean,collect,dissolve,statistics" ).split( ',' );
46 : 0 : }
47 : :
48 : 0 : QString QgsAggregateAlgorithm::group() const
49 : : {
50 : 0 : return QObject::tr( "Vector geometry" );
51 : : }
52 : :
53 : 0 : QString QgsAggregateAlgorithm::groupId() const
54 : : {
55 : 0 : return QStringLiteral( "vectorgeometry" );
56 : : }
57 : :
58 : 0 : QgsAggregateAlgorithm *QgsAggregateAlgorithm::createInstance() const
59 : : {
60 : 0 : return new QgsAggregateAlgorithm();
61 : 0 : }
62 : :
63 : 0 : void QgsAggregateAlgorithm::initAlgorithm( const QVariantMap & )
64 : : {
65 : 0 : addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "INPUT" ), QObject::tr( "Input layer" ), QList<int>() << QgsProcessing::TypeVector ) );
66 : 0 : addParameter( new QgsProcessingParameterExpression( QStringLiteral( "GROUP_BY" ), QObject::tr( "Group by expression (NULL to group all features)" ), QStringLiteral( "NULL" ), QStringLiteral( "INPUT" ) ) );
67 : 0 : addParameter( new QgsProcessingParameterAggregate( QStringLiteral( "AGGREGATES" ), QObject::tr( "Aggregates" ), QStringLiteral( "INPUT" ) ) );
68 : 0 : addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "OUTPUT" ), QObject::tr( "Aggregated" ) ) );
69 : 0 : }
70 : :
71 : 0 : bool QgsAggregateAlgorithm::prepareAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback * )
72 : : {
73 : 0 : mSource.reset( parameterAsSource( parameters, QStringLiteral( "INPUT" ), context ) );
74 : 0 : if ( !mSource )
75 : 0 : throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "INPUT" ) ) );
76 : :
77 : 0 : mGroupBy = parameterAsExpression( parameters, QStringLiteral( "GROUP_BY" ), context );
78 : :
79 : 0 : mDa.setSourceCrs( mSource->sourceCrs(), context.transformContext() );
80 : 0 : mDa.setEllipsoid( context.ellipsoid() );
81 : :
82 : 0 : mGroupByExpression = createExpression( mGroupBy, context );
83 : 0 : mGeometryExpression = createExpression( QStringLiteral( "collect($geometry, %1)" ).arg( mGroupBy ), context );
84 : :
85 : 0 : const QVariantList aggregates = parameters.value( QStringLiteral( "AGGREGATES" ) ).toList();
86 : 0 : int currentAttributeIndex = 0;
87 : 0 : for ( const QVariant &aggregate : aggregates )
88 : : {
89 : 0 : const QVariantMap aggregateDef = aggregate.toMap();
90 : :
91 : 0 : const QString name = aggregateDef.value( QStringLiteral( "name" ) ).toString();
92 : 0 : if ( name.isEmpty() )
93 : 0 : throw QgsProcessingException( QObject::tr( "Field name cannot be empty" ) );
94 : :
95 : 0 : const QVariant::Type type = static_cast< QVariant::Type >( aggregateDef.value( QStringLiteral( "type" ) ).toInt() );
96 : :
97 : 0 : const int length = aggregateDef.value( QStringLiteral( "length" ), 0 ).toInt();
98 : 0 : const int precision = aggregateDef.value( QStringLiteral( "precision" ), 0 ).toInt();
99 : :
100 : 0 : mFields.append( QgsField( name, type, QString(), length, precision ) );
101 : :
102 : :
103 : 0 : const QString aggregateType = aggregateDef.value( QStringLiteral( "aggregate" ) ).toString();
104 : 0 : const QString source = aggregateDef.value( QStringLiteral( "input" ) ).toString();
105 : 0 : const QString delimiter = aggregateDef.value( QStringLiteral( "delimiter" ) ).toString();
106 : :
107 : 0 : QString expression;
108 : 0 : if ( aggregateType == QLatin1String( "first_value" ) )
109 : : {
110 : 0 : expression = source;
111 : 0 : }
112 : 0 : else if ( aggregateType == QLatin1String( "last_value" ) )
113 : : {
114 : 0 : expression = source;
115 : 0 : mAttributesRequireLastFeature << currentAttributeIndex;
116 : 0 : }
117 : 0 : else if ( aggregateType == QLatin1String( "concatenate" ) || aggregateType == QLatin1String( "concatenate_unique" ) )
118 : : {
119 : 0 : expression = QStringLiteral( "%1(%2, %3, %4, \'%5\')" ).arg( aggregateType,
120 : : source,
121 : 0 : mGroupBy,
122 : 0 : QStringLiteral( "TRUE" ),
123 : : delimiter );
124 : 0 : }
125 : : else
126 : : {
127 : 0 : expression = QStringLiteral( "%1(%2, %3)" ).arg( aggregateType, source, mGroupBy );
128 : : }
129 : 0 : mExpressions.append( createExpression( expression, context ) );
130 : 0 : currentAttributeIndex++;
131 : 0 : }
132 : :
133 : : return true;
134 : 0 : }
135 : :
136 : 0 : QVariantMap QgsAggregateAlgorithm::processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
137 : : {
138 : 0 : QgsExpressionContext expressionContext = createExpressionContext( parameters, context, mSource.get() );
139 : 0 : mGroupByExpression.prepare( &expressionContext );
140 : :
141 : : // Group features in memory layers
142 : 0 : const long long count = mSource->featureCount();
143 : 0 : double progressStep = count > 0 ? 50.0 / count : 1;
144 : 0 : long long current = 0;
145 : :
146 : 0 : QHash< QVariantList, Group > groups;
147 : 0 : QVector< QVariantList > keys; // We need deterministic order for the tests
148 : 0 : QgsFeature feature;
149 : :
150 : 0 : std::vector< std::unique_ptr< QgsFeatureSink > > groupSinks;
151 : :
152 : 0 : QgsFeatureIterator it = mSource->getFeatures( QgsFeatureRequest() );
153 : 0 : while ( it.nextFeature( feature ) )
154 : : {
155 : 0 : expressionContext.setFeature( feature );
156 : 0 : const QVariant groupByValue = mGroupByExpression.evaluate( &expressionContext );
157 : 0 : if ( mGroupByExpression.hasEvalError() )
158 : : {
159 : 0 : throw QgsProcessingException( QObject::tr( "Evaluation error in group by expression \"%1\": %2" ).arg( mGroupByExpression.expression(),
160 : 0 : mGroupByExpression.evalErrorString() ) );
161 : : }
162 : :
163 : : // upgrade group by value to a list, so that we get correct behavior with the QHash
164 : 0 : const QVariantList key = groupByValue.type() == QVariant::List ? groupByValue.toList() : ( QVariantList() << groupByValue );
165 : :
166 : 0 : auto groupIt = groups.find( key );
167 : 0 : if ( groupIt == groups.end() )
168 : : {
169 : 0 : QString id = QStringLiteral( "memory:" );
170 : 0 : std::unique_ptr< QgsFeatureSink > sink( QgsProcessingUtils::createFeatureSink( id,
171 : 0 : context,
172 : 0 : mSource->fields(),
173 : 0 : mSource->wkbType(),
174 : 0 : mSource->sourceCrs() ) );
175 : :
176 : 0 : sink->addFeature( feature, QgsFeatureSink::FastInsert );
177 : :
178 : 0 : QgsMapLayer *layer = QgsProcessingUtils::mapLayerFromString( id, context );
179 : :
180 : 0 : Group group;
181 : 0 : group.sink = sink.get();
182 : : //store ownership of sink in groupSinks, so that these get deleted automatically if an exception is raised later..
183 : 0 : groupSinks.emplace_back( std::move( sink ) );
184 : 0 : group.layer = layer;
185 : 0 : group.firstFeature = feature;
186 : 0 : group.lastFeature = feature;
187 : 0 : groups[key] = group;
188 : 0 : keys.append( key );
189 : 0 : }
190 : : else
191 : : {
192 : 0 : groupIt->sink->addFeature( feature, QgsFeatureSink::FastInsert );
193 : 0 : groupIt->lastFeature = feature;
194 : : }
195 : :
196 : 0 : current++;
197 : 0 : feedback->setProgress( current * progressStep );
198 : 0 : if ( feedback->isCanceled() )
199 : 0 : break;
200 : 0 : }
201 : :
202 : : // early cleanup
203 : 0 : groupSinks.clear();
204 : :
205 : 0 : QString destId;
206 : 0 : std::unique_ptr< QgsFeatureSink > sink( parameterAsSink( parameters, QStringLiteral( "OUTPUT" ), context, destId, mFields, QgsWkbTypes::multiType( mSource->wkbType() ), mSource->sourceCrs() ) );
207 : 0 : if ( !sink )
208 : 0 : throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "OUTPUT" ) ) );
209 : :
210 : : // Calculate aggregates on memory layers
211 : 0 : if ( !keys.empty() )
212 : 0 : progressStep = 50.0 / keys.size();
213 : :
214 : 0 : current = 0;
215 : 0 : for ( const QVariantList &key : keys )
216 : : {
217 : 0 : Group &group = groups[ key ];
218 : :
219 : 0 : QgsExpressionContext exprContext = createExpressionContext( parameters, context );
220 : 0 : exprContext.appendScope( QgsExpressionContextUtils::layerScope( group.layer ) );
221 : 0 : exprContext.setFeature( group.firstFeature );
222 : :
223 : 0 : QgsGeometry geometry = mGeometryExpression.evaluate( &exprContext ).value< QgsGeometry >();
224 : 0 : if ( mGeometryExpression.hasEvalError() )
225 : : {
226 : 0 : throw QgsProcessingException( QObject::tr( "Evaluation error in geometry expression \"%1\": %2" ).arg( mGeometryExpression.expression(),
227 : 0 : mGeometryExpression.evalErrorString() ) );
228 : : }
229 : :
230 : 0 : if ( !geometry.isNull() && !geometry.isEmpty() )
231 : : {
232 : 0 : geometry = QgsGeometry::unaryUnion( geometry.asGeometryCollection() );
233 : 0 : if ( geometry.isEmpty() )
234 : : {
235 : 0 : QStringList keyString;
236 : 0 : for ( const QVariant &v : key )
237 : 0 : keyString << v.toString();
238 : :
239 : 0 : throw QgsProcessingException( QObject::tr( "Impossible to combine geometries for %1 = %2" ).arg( mGroupBy, keyString.join( ',' ) ) );
240 : 0 : }
241 : 0 : }
242 : :
243 : 0 : QgsAttributes attributes;
244 : 0 : attributes.reserve( mExpressions.size() );
245 : 0 : int currentAttributeIndex = 0;
246 : 0 : for ( auto it = mExpressions.begin(); it != mExpressions.end(); ++it )
247 : : {
248 : 0 : exprContext.setFeature( mAttributesRequireLastFeature.contains( currentAttributeIndex ) ? group.lastFeature : group.firstFeature );
249 : 0 : if ( it->isValid() )
250 : : {
251 : 0 : const QVariant value = it->evaluate( &exprContext );
252 : 0 : if ( it->hasEvalError() )
253 : : {
254 : 0 : throw QgsProcessingException( QObject::tr( "Evaluation error in expression \"%1\": %2" ).arg( it->expression(), it->evalErrorString() ) );
255 : : }
256 : 0 : attributes.append( value );
257 : 0 : }
258 : : else
259 : : {
260 : 0 : attributes.append( QVariant() );
261 : : }
262 : 0 : currentAttributeIndex++;
263 : 0 : }
264 : :
265 : : // Write output feature
266 : 0 : QgsFeature outFeat;
267 : 0 : outFeat.setGeometry( geometry );
268 : 0 : outFeat.setAttributes( attributes );
269 : 0 : sink->addFeature( outFeat, QgsFeatureSink::FastInsert );
270 : :
271 : 0 : current++;
272 : 0 : feedback->setProgress( 50 + current * progressStep );
273 : 0 : if ( feedback->isCanceled() )
274 : 0 : break;
275 : 0 : }
276 : :
277 : 0 : QVariantMap results;
278 : 0 : results.insert( QStringLiteral( "OUTPUT" ), destId );
279 : 0 : return results;
280 : 0 : }
281 : :
282 : 0 : bool QgsAggregateAlgorithm::supportInPlaceEdit( const QgsMapLayer *layer ) const
283 : : {
284 : : Q_UNUSED( layer )
285 : 0 : return false;
286 : : }
287 : :
288 : 0 : QgsExpression QgsAggregateAlgorithm::createExpression( const QString &expressionString, QgsProcessingContext &context ) const
289 : : {
290 : 0 : QgsExpression expr( expressionString );
291 : 0 : expr.setGeomCalculator( &mDa );
292 : 0 : expr.setDistanceUnits( context.distanceUnit() );
293 : 0 : expr.setAreaUnits( context.areaUnit() );
294 : 0 : if ( expr.hasParserError() )
295 : : {
296 : 0 : throw QgsProcessingException(
297 : 0 : QObject::tr( "Parser error in expression \"%1\": %2" ).arg( expressionString, expr.parserErrorString() ) );
298 : : }
299 : 0 : return expr;
300 : 0 : }
301 : :
302 : : ///@endcond
|