Skip to content

Commit 2a326ef

Browse files
committed
[FEATURE] support aggregation of geometry
This feature adds a 'collect' aggregation method resulting in a single multipart geometry from a list of geometries. This is exposed in the expression engine via the existing aggregate() function, as well as a new collect() function.
1 parent 5d38dcb commit 2a326ef

12 files changed

+176
-4
lines changed

python/core/geometry/qgsgeometry.sip

+2
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ class QgsGeometry
8080
static QgsGeometry fromMultiPolygon( const QgsMultiPolygon& multipoly );
8181
/** Creates a new geometry from a QgsRectangle */
8282
static QgsGeometry fromRect( const QgsRectangle& rect );
83+
/** Creates a new multipart geometry from a list of QgsGeometry objects*/
84+
static QgsGeometry collectGeometry( const QList< QgsGeometry >& geometries );
8385

8486
/**
8587
* Set the geometry, feeding in a geometry in GEOS format.

python/core/qgsaggregatecalculator.sip

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class QgsAggregateCalculator
3838
StringMinimumLength, //!< Minimum length of string (string fields only)
3939
StringMaximumLength, //!< Maximum length of string (string fields only)
4040
StringConcatenate, //! Concatenate values with a joining string (string fields only). Specify the delimiter using setDelimiter().
41+
GeometryCollect, //! Create a multipart geometry from aggregated geometries
4142
};
4243

4344
//! A bundle of parameters controlling aggregate calculation

resources/function_help/json/aggregate

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "Returns an aggregate value calculated using features from another layer.",
55
"arguments": [
66
{"arg":"layer", "description":"a string, representing either a layer name or layer ID"},
7-
{"arg":"aggregate", "description":"a string corresponding to the aggregate to calculate. Valid options are:<br /><ul><li>count</li><li>count_distinct</li><li>count_missing</li><li>min</li><li>max</li><li>sum</li><li>mean</li><li>median</li><li>stdev</li><li>stdevsample</li><li>range</li><li>minority</li><li>majority</li><li>q1: first quartile</li><li>q3: third quartile</li><li>iqr: inter quartile range</li><li>min_length: minimum string length</li><li>max_length: maximum string length</li><li>concatenate: join strings with a concatenator</li></ul>"},
7+
{"arg":"aggregate", "description":"a string corresponding to the aggregate to calculate. Valid options are:<br /><ul><li>count</li><li>count_distinct</li><li>count_missing</li><li>min</li><li>max</li><li>sum</li><li>mean</li><li>median</li><li>stdev</li><li>stdevsample</li><li>range</li><li>minority</li><li>majority</li><li>q1: first quartile</li><li>q3: third quartile</li><li>iqr: inter quartile range</li><li>min_length: minimum string length</li><li>max_length: maximum string length</li><li>concatenate: join strings with a concatenator</li><li>collect: create an aggregated multipart geometry</li></ul>"},
88
{"arg":"calculation", "description":"sub expression or field name to aggregate"},
99
{"arg":"filter", "optional":true, "description":"optional filter expression to limit the features used for calculating the aggregate"},
1010
{"arg":"concatenator", "optional":true, "description":"optional string to use to join values for 'concatenate' aggregate"}

resources/function_help/json/collect

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "collect",
3+
"type": "function",
4+
"description": "Returns the multipart geometry of aggregated geometries from an expression",
5+
"arguments": [
6+
{"arg":"expression", "description":"geometry expression to aggregate"},
7+
{"arg":"group_by", "optional":true, "description":"optional expression to use to group aggregate calculations"},
8+
{"arg":"filter", "optional":true, "description":"optional expression to use to filter features used to calculate aggregate"}
9+
],
10+
"examples": [
11+
{ "expression":"collect( $geometry )", "returns":"multipart geometry of aggregated geometries"}
12+
]
13+
}

src/core/geometry/qgsgeometry.cpp

+21
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,27 @@ QgsGeometry QgsGeometry::fromRect( const QgsRectangle& rect )
239239
return fromPolygon( polygon );
240240
}
241241

242+
QgsGeometry QgsGeometry::collectGeometry( const QList< QgsGeometry >& geometries )
243+
{
244+
QgsGeometry collected;
245+
246+
QList< QgsGeometry >::const_iterator git = geometries.constBegin();
247+
for ( ; git != geometries.constEnd(); ++git )
248+
{
249+
if ( collected.isEmpty() )
250+
{
251+
collected = QgsGeometry( *git );
252+
collected.convertToMultiType();
253+
}
254+
else
255+
{
256+
QgsGeometry part = QgsGeometry( *git );
257+
collected.addPart( &part );
258+
}
259+
}
260+
return collected;
261+
}
262+
242263
void QgsGeometry::fromWkb( unsigned char *wkb, int length )
243264
{
244265
detach( false );

src/core/geometry/qgsgeometry.h

+2
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ class CORE_EXPORT QgsGeometry
132132
static QgsGeometry fromMultiPolygon( const QgsMultiPolygon& multipoly );
133133
/** Creates a new geometry from a QgsRectangle */
134134
static QgsGeometry fromRect( const QgsRectangle& rect );
135+
/** Creates a new multipart geometry from a list of QgsGeometry objects*/
136+
static QgsGeometry collectGeometry( const QList< QgsGeometry >& geometries );
135137

136138
/**
137139
* Set the geometry, feeding in a geometry in GEOS format.

src/core/qgsaggregatecalculator.cpp

+40
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "qgsfeature.h"
2020
#include "qgsfeaturerequest.h"
2121
#include "qgsfeatureiterator.h"
22+
#include "qgsgeometry.h"
2223
#include "qgsvectorlayer.h"
2324

2425

@@ -162,6 +163,8 @@ QgsAggregateCalculator::Aggregate QgsAggregateCalculator::stringToAggregate( con
162163
return StringMaximumLength;
163164
else if ( normalized == "concatenate" )
164165
return StringConcatenate;
166+
else if ( normalized == "collect" )
167+
return GeometryCollect;
165168

166169
if ( ok )
167170
*ok = false;
@@ -206,6 +209,20 @@ QVariant QgsAggregateCalculator::calculate( QgsAggregateCalculator::Aggregate ag
206209
return calculateDateTimeAggregate( fit, attr, expression, context, stat );
207210
}
208211

212+
case QVariant::UserType:
213+
{
214+
if ( aggregate == GeometryCollect )
215+
{
216+
if ( ok )
217+
*ok = true;
218+
return calculateGeometryAggregate( fit, expression, context );
219+
}
220+
else
221+
{
222+
return QVariant();
223+
}
224+
}
225+
209226
default:
210227
{
211228
// treat as string
@@ -275,6 +292,7 @@ QgsStatisticalSummary::Statistic QgsAggregateCalculator::numericStatFromAggregat
275292
case StringMinimumLength:
276293
case StringMaximumLength:
277294
case StringConcatenate:
295+
case GeometryCollect:
278296
{
279297
if ( ok )
280298
*ok = false;
@@ -321,6 +339,7 @@ QgsStringStatisticalSummary::Statistic QgsAggregateCalculator::stringStatFromAgg
321339
case ThirdQuartile:
322340
case InterQuartileRange:
323341
case StringConcatenate:
342+
case GeometryCollect:
324343
{
325344
if ( ok )
326345
*ok = false;
@@ -366,6 +385,7 @@ QgsDateTimeStatisticalSummary::Statistic QgsAggregateCalculator::dateTimeStatFro
366385
case StringMinimumLength:
367386
case StringMaximumLength:
368387
case StringConcatenate:
388+
case GeometryCollect:
369389
{
370390
if ( ok )
371391
*ok = false;
@@ -430,6 +450,26 @@ QVariant QgsAggregateCalculator::calculateStringAggregate( QgsFeatureIterator& f
430450
return s.statistic( stat );
431451
}
432452

453+
QVariant QgsAggregateCalculator::calculateGeometryAggregate( QgsFeatureIterator& fit, QgsExpression* expression, QgsExpressionContext* context )
454+
{
455+
Q_ASSERT( expression );
456+
457+
QgsFeature f;
458+
QList< QgsGeometry > geometries;
459+
while ( fit.nextFeature( f ) )
460+
{
461+
Q_ASSERT( context );
462+
context->setFeature( f );
463+
QVariant v = expression->evaluate( context );
464+
if ( v.canConvert<QgsGeometry>() )
465+
{
466+
geometries << v.value<QgsGeometry>();
467+
}
468+
}
469+
470+
return QVariant::fromValue( QgsGeometry::collectGeometry( geometries ) );
471+
}
472+
433473
QVariant QgsAggregateCalculator::concatenateStrings( QgsFeatureIterator& fit, int attr, QgsExpression* expression,
434474
QgsExpressionContext* context, const QString& delimiter )
435475
{

src/core/qgsaggregatecalculator.h

+2
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class CORE_EXPORT QgsAggregateCalculator
6464
StringMinimumLength, //!< Minimum length of string (string fields only)
6565
StringMaximumLength, //!< Maximum length of string (string fields only)
6666
StringConcatenate, //! Concatenate values with a joining string (string fields only). Specify the delimiter using setDelimiter().
67+
GeometryCollect //! Create a multipart geometry from aggregated geometries
6768
};
6869

6970
//! A bundle of parameters controlling aggregate calculation
@@ -160,6 +161,7 @@ class CORE_EXPORT QgsAggregateCalculator
160161

161162
static QVariant calculateDateTimeAggregate( QgsFeatureIterator& fit, int attr, QgsExpression* expression,
162163
QgsExpressionContext* context, QgsDateTimeStatisticalSummary::Statistic stat );
164+
static QVariant calculateGeometryAggregate( QgsFeatureIterator& fit, QgsExpression* expression, QgsExpressionContext* context );
163165

164166
static QVariant calculate( Aggregate aggregate, QgsFeatureIterator& fit, QVariant::Type resultType,
165167
int attr, QgsExpression* expression,

src/core/qgsexpression.cpp

+8-2
Original file line numberDiff line numberDiff line change
@@ -945,6 +945,11 @@ static QVariant fcnAggregateMaxLength( const QVariantList& values, const QgsExpr
945945
return fcnAggregateGeneric( QgsAggregateCalculator::StringMaximumLength, values, QgsAggregateCalculator::AggregateParameters(), context, parent );
946946
}
947947

948+
static QVariant fcnAggregateCollectGeometry( const QVariantList& values, const QgsExpressionContext* context, QgsExpression *parent )
949+
{
950+
return fcnAggregateGeneric( QgsAggregateCalculator::GeometryCollect, values, QgsAggregateCalculator::AggregateParameters(), context, parent );
951+
}
952+
948953
static QVariant fcnAggregateStringConcat( const QVariantList& values, const QgsExpressionContext* context, QgsExpression *parent )
949954
{
950955
QgsAggregateCalculator::AggregateParameters parameters;
@@ -3150,7 +3155,7 @@ const QStringList& QgsExpression::BuiltinFunctions()
31503155
<< "aggregate" << "relation_aggregate" << "count" << "count_distinct"
31513156
<< "count_missing" << "minimum" << "maximum" << "sum" << "mean"
31523157
<< "median" << "stdev" << "range" << "minority" << "majority"
3153-
<< "q1" << "q3" << "iqr" << "min_length" << "max_length" << "concatenate"
3158+
<< "q1" << "q3" << "iqr" << "min_length" << "max_length" << "collect" << "concatenate"
31543159
<< "attribute" << "var" << "layer_property"
31553160
<< "$id" << "$scale" << "_specialcol_";
31563161
}
@@ -3214,7 +3219,7 @@ const QList<QgsExpression::Function*>& QgsExpression::Functions()
32143219
<< new StaticFunction( "coalesce", -1, fcnCoalesce, "Conditionals", QString(), false, QStringList(), false, QStringList(), true )
32153220
<< new StaticFunction( "if", 3, fcnIf, "Conditionals", QString(), False, QStringList(), true )
32163221
<< new StaticFunction( "aggregate", ParameterList() << Parameter( "layer" ) << Parameter( "aggregate" ) << Parameter( "expression" )
3217-
<< Parameter( "filter", true ) << Parameter( "concatenator", true ), fcnAggregate, "Aggregates", QString(), False, QStringList(), true )
3222+
<< Parameter( "filter", true ) << Parameter( "concatenator", true ), fcnAggregate, "Aggregates", QString(), false, QStringList(), true )
32183223
<< new StaticFunction( "relation_aggregate", ParameterList() << Parameter( "relation" ) << Parameter( "aggregate" ) << Parameter( "expression" ) << Parameter( "concatenator", true ),
32193224
fcnAggregateRelation, "Aggregates", QString(), False, QStringList( QgsFeatureRequest::AllAttributes ), true )
32203225

@@ -3235,6 +3240,7 @@ const QList<QgsExpression::Function*>& QgsExpression::Functions()
32353240
<< new StaticFunction( "iqr", aggParams, fcnAggregateIQR, "Aggregates", QString(), False, QStringList(), true )
32363241
<< new StaticFunction( "min_length", aggParams, fcnAggregateMinLength, "Aggregates", QString(), False, QStringList(), true )
32373242
<< new StaticFunction( "max_length", aggParams, fcnAggregateMaxLength, "Aggregates", QString(), False, QStringList(), true )
3243+
<< new StaticFunction( "collect", aggParams, fcnAggregateCollectGeometry, "Aggregates", QString(), False, QStringList(), true )
32383244
<< new StaticFunction( "concatenate", aggParams << Parameter( "concatenator", true ), fcnAggregateStringConcat, "Aggregates", QString(), False, QStringList(), true )
32393245

32403246
<< new StaticFunction( "regexp_match", 2, fcnRegexpMatch, "Conditionals" )

tests/src/core/testqgsexpression.cpp

+10
Original file line numberDiff line numberDiff line change
@@ -116,26 +116,32 @@ class TestQgsExpression: public QObject
116116
mAggregatesLayer = new QgsVectorLayer( "Point?field=col1:integer&field=col2:string&field=col3:integer", "aggregate_layer", "memory" );
117117
QVERIFY( mAggregatesLayer->isValid() );
118118
QgsFeature af1( mAggregatesLayer->dataProvider()->fields(), 1 );
119+
af1.setGeometry( QgsGeometry::fromPoint( QgsPoint( 0, 0 ) ) );
119120
af1.setAttribute( "col1", 4 );
120121
af1.setAttribute( "col2", "test" );
121122
af1.setAttribute( "col3", 2 );
122123
QgsFeature af2( mAggregatesLayer->dataProvider()->fields(), 2 );
124+
af2.setGeometry( QgsGeometry::fromPoint( QgsPoint( 1, 0 ) ) );
123125
af2.setAttribute( "col1", 2 );
124126
af2.setAttribute( "col2", QVariant( QVariant::String ) );
125127
af2.setAttribute( "col3", 1 );
126128
QgsFeature af3( mAggregatesLayer->dataProvider()->fields(), 3 );
129+
af3.setGeometry( QgsGeometry::fromPoint( QgsPoint( 2, 0 ) ) );
127130
af3.setAttribute( "col1", 3 );
128131
af3.setAttribute( "col2", "test333" );
129132
af3.setAttribute( "col3", 2 );
130133
QgsFeature af4( mAggregatesLayer->dataProvider()->fields(), 4 );
134+
af4.setGeometry( QgsGeometry::fromPoint( QgsPoint( 3, 0 ) ) );
131135
af4.setAttribute( "col1", 2 );
132136
af4.setAttribute( "col2", "test4" );
133137
af4.setAttribute( "col3", 2 );
134138
QgsFeature af5( mAggregatesLayer->dataProvider()->fields(), 5 );
139+
af5.setGeometry( QgsGeometry::fromPoint( QgsPoint( 4, 0 ) ) );
135140
af5.setAttribute( "col1", 5 );
136141
af5.setAttribute( "col2", QVariant( QVariant::String ) );
137142
af5.setAttribute( "col3", 3 );
138143
QgsFeature af6( mAggregatesLayer->dataProvider()->fields(), 6 );
144+
af6.setGeometry( QgsGeometry::fromPoint( QgsPoint( 5, 0 ) ) );
139145
af6.setAttribute( "col1", 8 );
140146
af6.setAttribute( "col2", "test4" );
141147
af6.setAttribute( "col3", 3 );
@@ -1213,6 +1219,8 @@ class TestQgsExpression: public QObject
12131219
QTest::newRow( "string aggregate 2" ) << "aggregate('test','min_length',\"col2\")" << false << QVariant( 5 );
12141220
QTest::newRow( "string concatenate" ) << "aggregate('test','concatenate',\"col2\",concatenator:=' , ')" << false << QVariant( "test1 , test2 , test3 , test4" );
12151221

1222+
QTest::newRow( "geometry collect" ) << "geom_to_wkt(aggregate('aggregate_layer','collect',$geometry))" << false << QVariant( QString( "MultiPoint ((0 0),(1 0),(2 0),(3 0),(4 0),(5 0))" ) );
1223+
12161224
QTest::newRow( "sub expression" ) << "aggregate('test','sum',\"col1\" * 2)" << false << QVariant( 65 * 2 );
12171225
QTest::newRow( "bad sub expression" ) << "aggregate('test','sum',\"xcvxcv\" * 2)" << true << QVariant();
12181226

@@ -1289,6 +1297,8 @@ class TestQgsExpression: public QObject
12891297
QTest::newRow( "max_length" ) << "max_length(\"col2\")" << false << QVariant( 7 );
12901298
QTest::newRow( "concatenate" ) << "concatenate(\"col2\",concatenator:=',')" << false << QVariant( "test,,test333,test4,,test4" );
12911299

1300+
QTest::newRow( "geometry collect" ) << "geom_to_wkt(collect($geometry))" << false << QVariant( QString( "MultiPoint ((0 0),(1 0),(2 0),(3 0),(4 0),(5 0))" ) );
1301+
12921302
QTest::newRow( "bad expression" ) << "sum(\"xcvxcvcol1\")" << true << QVariant();
12931303
QTest::newRow( "aggregate named" ) << "sum(expression:=\"col1\")" << false << QVariant( 24.0 );
12941304
QTest::newRow( "string aggregate on int" ) << "max_length(\"col1\")" << true << QVariant();

tests/src/python/test_qgsaggregatecalculator.py

+37-1
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,16 @@
2121
QgsInterval,
2222
QgsExpressionContext,
2323
QgsExpressionContextScope,
24+
QgsGeometry,
2425
NULL
2526
)
2627
from qgis.PyQt.QtCore import QDateTime, QDate, QTime
2728
from qgis.testing import unittest, start_app
2829

30+
from utilities import(
31+
compareWkt
32+
)
33+
2934
start_app()
3035

3136

@@ -55,6 +60,31 @@ def testParameters(self):
5560
self.assertEqual(a.filter(), 'string filter')
5661
self.assertEqual(a.delimiter(), 'delim')
5762

63+
def testGeometry(self):
64+
""" Test calculation of aggregates on geometry expressions """
65+
66+
layer = QgsVectorLayer("Point?",
67+
"layer", "memory")
68+
pr = layer.dataProvider()
69+
70+
# must be same length:
71+
geometry_values = [QgsGeometry.fromWkt("Point ( 0 0 )"), QgsGeometry.fromWkt("Point ( 1 1 )"), QgsGeometry.fromWkt("Point ( 2 2 )")]
72+
73+
features = []
74+
for i in range(len(geometry_values)):
75+
f = QgsFeature()
76+
f.setGeometry(geometry_values[i])
77+
features.append(f)
78+
self.assertTrue(pr.addFeatures(features))
79+
80+
agg = QgsAggregateCalculator(layer)
81+
82+
val, ok = agg.calculate(QgsAggregateCalculator.GeometryCollect, '$geometry')
83+
self.assertTrue(ok)
84+
expwkt = "MultiPoint ((0 0), (1 1), (2 2))"
85+
wkt = val.exportToWkt()
86+
self.assertTrue(compareWkt(expwkt, wkt), "Expected:\n%s\nGot:\n%s\n" % (expwkt, wkt))
87+
5888
def testNumeric(self):
5989
""" Test calculation of aggregates on numeric fields"""
6090

@@ -324,6 +354,11 @@ def testExpression(self):
324354
self.assertTrue(ok)
325355
self.assertEqual(val, '8 oranges')
326356

357+
# geometry
358+
val, ok = agg.calculate(QgsAggregateCalculator.GeometryCollect, "make_point( coalesce(fldint,0), 2 )")
359+
self.assertTrue(ok)
360+
self.assertTrue(val.exportToWkt(), 'MultiPoint((4 2, 2 2, 3 2, 2 2,5 2, 0 2,8 2))')
361+
327362
# try a bad expression
328363
val, ok = agg.calculate(QgsAggregateCalculator.Max, "not_a_field || ' oranges'")
329364
self.assertFalse(ok)
@@ -372,7 +407,8 @@ def testStringToAggregate(self):
372407
[QgsAggregateCalculator.InterQuartileRange, 'iqr'],
373408
[QgsAggregateCalculator.StringMinimumLength, 'min_length'],
374409
[QgsAggregateCalculator.StringMaximumLength, 'max_length'],
375-
[QgsAggregateCalculator.StringConcatenate, 'concatenate']]
410+
[QgsAggregateCalculator.StringConcatenate, 'concatenate'],
411+
[QgsAggregateCalculator.GeometryCollect, 'collect']]
376412

377413
for t in tests:
378414
agg, ok = QgsAggregateCalculator.stringToAggregate(t[1])

tests/src/python/test_qgsgeometry.py

+39
Original file line numberDiff line numberDiff line change
@@ -1346,6 +1346,45 @@ def testBoundingBox(self):
13461346
line = QgsGeometry.fromPolyline(points)
13471347
assert line.boundingBox().isNull()
13481348

1349+
def testCollectGeometry(self):
1350+
# collect points
1351+
geometries = [QgsGeometry.fromPoint(QgsPoint(0, 0)), QgsGeometry.fromPoint(QgsPoint(1, 1))]
1352+
geometry = QgsGeometry.collectGeometry(geometries)
1353+
expwkt = "MultiPoint ((0 0), (1 1))"
1354+
wkt = geometry.exportToWkt()
1355+
assert compareWkt(expwkt, wkt), "Expected:\n%s\nGot:\n%s\n" % (expwkt, wkt)
1356+
1357+
# collect lines
1358+
points = [
1359+
[QgsPoint(0, 0), QgsPoint(1, 0)],
1360+
[QgsPoint(2, 0), QgsPoint(3, 0)]
1361+
]
1362+
geometries = [QgsGeometry.fromPolyline(points[0]), QgsGeometry.fromPolyline(points[1])]
1363+
geometry = QgsGeometry.collectGeometry(geometries)
1364+
expwkt = "MultiLineString ((0 0, 1 0), (2 0, 3 0))"
1365+
wkt = geometry.exportToWkt()
1366+
assert compareWkt(expwkt, wkt), "Expected:\n%s\nGot:\n%s\n" % (expwkt, wkt)
1367+
1368+
# collect polygons
1369+
points = [
1370+
[[QgsPoint(0, 0), QgsPoint(1, 0), QgsPoint(1, 1), QgsPoint(0, 1), QgsPoint(0, 0)]],
1371+
[[QgsPoint(2, 0), QgsPoint(3, 0), QgsPoint(3, 1), QgsPoint(2, 1), QgsPoint(2, 0)]]
1372+
]
1373+
geometries = [QgsGeometry.fromPolygon(points[0]), QgsGeometry.fromPolygon(points[1])]
1374+
geometry = QgsGeometry.collectGeometry(geometries)
1375+
expwkt = "MultiPolygon (((0 0, 1 0, 1 1, 0 1, 0 0)),((2 0, 3 0, 3 1, 2 1, 2 0)))"
1376+
wkt = geometry.exportToWkt()
1377+
assert compareWkt(expwkt, wkt), "Expected:\n%s\nGot:\n%s\n" % (expwkt, wkt)
1378+
1379+
# test empty list
1380+
geometries = []
1381+
geometry = QgsGeometry.collectGeometry(geometries)
1382+
assert geometry.isEmpty(), "Expected geometry to be empty"
1383+
1384+
# check that the resulting geometry is multi
1385+
geometry = QgsGeometry.collectGeometry([QgsGeometry.fromWkt('Point (0 0)')])
1386+
assert geometry.isMultipart(), "Expected collected geometry to be multipart"
1387+
13491388
def testAddPart(self):
13501389
# add a part to a multipoint
13511390
points = [QgsPoint(0, 0), QgsPoint(1, 0)]

0 commit comments

Comments
 (0)