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" ).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 : :
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( "concatenate" ) || aggregateType == QLatin1String( "concatenate_unique" ) )
113 : : {
114 : 0 : expression = QStringLiteral( "%1(%2, %3, %4, \'%5\')" ).arg( aggregateType,
115 : : source,
116 : 0 : mGroupBy,
117 : 0 : QStringLiteral( "TRUE" ),
118 : : delimiter );
119 : 0 : }
120 : : else
121 : : {
122 : 0 : expression = QStringLiteral( "%1(%2, %3)" ).arg( aggregateType, source, mGroupBy );
123 : : }
124 : 0 : mExpressions.append( createExpression( expression, context ) );
125 : 0 : }
126 : :
127 : : return true;
128 : 0 : }
129 : :
130 : 0 : QVariantMap QgsAggregateAlgorithm::processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
131 : : {
132 : 0 : QgsExpressionContext expressionContext = createExpressionContext( parameters, context, mSource.get() );
133 : 0 : mGroupByExpression.prepare( &expressionContext );
134 : :
135 : : // Group features in memory layers
136 : 0 : const long long count = mSource->featureCount();
137 : 0 : double progressStep = count > 0 ? 50.0 / count : 1;
138 : 0 : long long current = 0;
139 : :
140 : 0 : QHash< QVariantList, Group > groups;
141 : 0 : QVector< QVariantList > keys; // We need deterministic order for the tests
142 : 0 : QgsFeature feature;
143 : :
144 : 0 : std::vector< std::unique_ptr< QgsFeatureSink > > groupSinks;
145 : :
146 : 0 : QgsFeatureIterator it = mSource->getFeatures( QgsFeatureRequest() );
147 : 0 : while ( it.nextFeature( feature ) )
148 : : {
149 : 0 : expressionContext.setFeature( feature );
150 : 0 : const QVariant groupByValue = mGroupByExpression.evaluate( &expressionContext );
151 : 0 : if ( mGroupByExpression.hasEvalError() )
152 : : {
153 : 0 : throw QgsProcessingException( QObject::tr( "Evaluation error in group by expression \"%1\": %2" ).arg( mGroupByExpression.expression(),
154 : 0 : mGroupByExpression.evalErrorString() ) );
155 : : }
156 : :
157 : : // upgrade group by value to a list, so that we get correct behavior with the QHash
158 : 0 : const QVariantList key = groupByValue.type() == QVariant::List ? groupByValue.toList() : ( QVariantList() << groupByValue );
159 : :
160 : 0 : auto groupIt = groups.constFind( key );
161 : 0 : if ( groupIt == groups.constEnd() )
162 : : {
163 : 0 : QString id = QStringLiteral( "memory:" );
164 : 0 : std::unique_ptr< QgsFeatureSink > sink( QgsProcessingUtils::createFeatureSink( id,
165 : 0 : context,
166 : 0 : mSource->fields(),
167 : 0 : mSource->wkbType(),
168 : 0 : mSource->sourceCrs() ) );
169 : :
170 : 0 : sink->addFeature( feature, QgsFeatureSink::FastInsert );
171 : :
172 : 0 : QgsMapLayer *layer = QgsProcessingUtils::mapLayerFromString( id, context );
173 : :
174 : 0 : Group group;
175 : 0 : group.sink = sink.get();
176 : : //store ownership of sink in groupSinks, so that these get deleted automatically if an exception is raised later..
177 : 0 : groupSinks.emplace_back( std::move( sink ) );
178 : 0 : group.layer = layer;
179 : 0 : group.feature = feature;
180 : 0 : groups[key] = group;
181 : 0 : keys.append( key );
182 : 0 : }
183 : : else
184 : : {
185 : 0 : groupIt->sink->addFeature( feature, QgsFeatureSink::FastInsert );
186 : : }
187 : :
188 : 0 : current++;
189 : 0 : feedback->setProgress( current * progressStep );
190 : 0 : if ( feedback->isCanceled() )
191 : 0 : break;
192 : 0 : }
193 : :
194 : : // early cleanup
195 : 0 : groupSinks.clear();
196 : :
197 : 0 : QString destId;
198 : 0 : std::unique_ptr< QgsFeatureSink > sink( parameterAsSink( parameters, QStringLiteral( "OUTPUT" ), context, destId, mFields, QgsWkbTypes::multiType( mSource->wkbType() ), mSource->sourceCrs() ) );
199 : 0 : if ( !sink )
200 : 0 : throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "OUTPUT" ) ) );
201 : :
202 : : // Calculate aggregates on memory layers
203 : 0 : if ( !keys.empty() )
204 : 0 : progressStep = 50.0 / keys.size();
205 : :
206 : 0 : current = 0;
207 : 0 : for ( const QVariantList &key : keys )
208 : : {
209 : 0 : Group &group = groups[ key ];
210 : :
211 : 0 : QgsExpressionContext exprContext = createExpressionContext( parameters, context );
212 : 0 : exprContext.appendScope( QgsExpressionContextUtils::layerScope( group.layer ) );
213 : 0 : exprContext.setFeature( group.feature );
214 : :
215 : 0 : QgsGeometry geometry = mGeometryExpression.evaluate( &exprContext ).value< QgsGeometry >();
216 : 0 : if ( mGeometryExpression.hasEvalError() )
217 : : {
218 : 0 : throw QgsProcessingException( QObject::tr( "Evaluation error in geometry expression \"%1\": %2" ).arg( mGeometryExpression.expression(),
219 : 0 : mGeometryExpression.evalErrorString() ) );
220 : : }
221 : :
222 : 0 : if ( !geometry.isNull() && !geometry.isEmpty() )
223 : : {
224 : 0 : geometry = QgsGeometry::unaryUnion( geometry.asGeometryCollection() );
225 : 0 : if ( geometry.isEmpty() )
226 : : {
227 : 0 : QStringList keyString;
228 : 0 : for ( const QVariant &v : key )
229 : 0 : keyString << v.toString();
230 : :
231 : 0 : throw QgsProcessingException( QObject::tr( "Impossible to combine geometries for %1 = %2" ).arg( mGroupBy, keyString.join( ',' ) ) );
232 : 0 : }
233 : 0 : }
234 : :
235 : 0 : QgsAttributes attributes;
236 : 0 : attributes.reserve( mExpressions.size() );
237 : 0 : for ( auto it = mExpressions.begin(); it != mExpressions.end(); ++it )
238 : : {
239 : 0 : if ( it->isValid() )
240 : : {
241 : 0 : const QVariant value = it->evaluate( &exprContext );
242 : 0 : if ( it->hasEvalError() )
243 : : {
244 : 0 : throw QgsProcessingException( QObject::tr( "Evaluation error in expression \"%1\": %2" ).arg( it->expression(), it->evalErrorString() ) );
245 : : }
246 : 0 : attributes.append( value );
247 : 0 : }
248 : : else
249 : : {
250 : 0 : attributes.append( QVariant() );
251 : : }
252 : 0 : }
253 : :
254 : : // Write output feature
255 : 0 : QgsFeature outFeat;
256 : 0 : outFeat.setGeometry( geometry );
257 : 0 : outFeat.setAttributes( attributes );
258 : 0 : sink->addFeature( outFeat, QgsFeatureSink::FastInsert );
259 : :
260 : 0 : current++;
261 : 0 : feedback->setProgress( 50 + current * progressStep );
262 : 0 : if ( feedback->isCanceled() )
263 : 0 : break;
264 : 0 : }
265 : :
266 : 0 : QVariantMap results;
267 : 0 : results.insert( QStringLiteral( "OUTPUT" ), destId );
268 : 0 : return results;
269 : 0 : }
270 : :
271 : 0 : bool QgsAggregateAlgorithm::supportInPlaceEdit( const QgsMapLayer *layer ) const
272 : : {
273 : : Q_UNUSED( layer )
274 : 0 : return false;
275 : : }
276 : :
277 : 0 : QgsExpression QgsAggregateAlgorithm::createExpression( const QString &expressionString, QgsProcessingContext &context ) const
278 : : {
279 : 0 : QgsExpression expr( expressionString );
280 : 0 : expr.setGeomCalculator( &mDa );
281 : 0 : expr.setDistanceUnits( context.distanceUnit() );
282 : 0 : expr.setAreaUnits( context.areaUnit() );
283 : 0 : if ( expr.hasParserError() )
284 : : {
285 : 0 : throw QgsProcessingException(
286 : 0 : QObject::tr( "Parser error in expression \"%1\": %2" ).arg( expressionString, expr.parserErrorString() ) );
287 : : }
288 : 0 : return expr;
289 : 0 : }
290 : :
291 : : ///@endcond
|