diff --git a/python/plugins/processing/gui/TestTools.py b/python/plugins/processing/gui/TestTools.py
index ca54882ef47..d224a7dc74d 100644
--- a/python/plugins/processing/gui/TestTools.py
+++ b/python/plugins/processing/gui/TestTools.py
@@ -231,7 +231,7 @@ def createTest(text):
params[param.name()] = float(token)
elif isinstance(param, QgsProcessingParameterEnum):
params[param.name()] = int(token)
- else:
+ elif token:
if token[0] == '"':
token = token[1:]
if token[-1] == '"':
diff --git a/python/plugins/processing/tests/testdata/expected/collect_all.gfs b/python/plugins/processing/tests/testdata/expected/collect_all.gfs
new file mode 100644
index 00000000000..118efeffe0d
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/collect_all.gfs
@@ -0,0 +1,32 @@
+
+
+ collect_all
+ collect_all
+
+ 6
+ EPSG:4326
+
+ 1
+ -1.00000
+ 9.16296
+ -3.00000
+ 6.08868
+
+
+ name
+ name
+ String
+ 2
+
+
+ intval
+ intval
+ Integer
+
+
+ floatval
+ floatval
+ Real
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/collect_all.gml b/python/plugins/processing/tests/testdata/expected/collect_all.gml
new file mode 100644
index 00000000000..964d4008f30
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/collect_all.gml
@@ -0,0 +1,22 @@
+
+
+
+
+ -1-3
+ 9.1629558541266826.088675623800385
+
+
+
+
+
+ -1,-1 -1,3 3,3 3,2 2,2 2,-1 -1,-16.24145873320538,-0.054510556621882 7.24145873320538,-1.05451055662188 5.24145873320538,-1.05451055662188 6.24145873320538,-0.0545105566218822,5 2,6 3,6 3,5 2,53,2 6,1 6,-3 2,-1 2,2 3,22.44337811900192,4.42360844529751 2.44337811900192,5.42360844529751 3.44337811900192,5.42360844529751 3.44337811900192,4.42360844529751 2.44337811900192,4.423608445297514.17255278310941,4.82264875239923 4.17255278310941,5.82264875239923 5.17255278310941,5.82264875239923 5.17255278310941,4.82264875239923 4.17255278310941,4.822648752399238.16295585412668,2.73877159309021 8.16295585412668,3.73877159309021 9.16295585412668,3.73877159309021 9.16295585412668,2.73877159309021 8.16295585412668,2.738771593090212.62072936660269,5.08867562380038 2.62072936660269,6.08867562380038 3.62072936660269,6.08867562380038 3.62072936660269,5.08867562380038 2.62072936660269,5.08867562380038
+ aa
+ 1
+ 44.123456
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/collect_one_field.gfs b/python/plugins/processing/tests/testdata/expected/collect_one_field.gfs
new file mode 100644
index 00000000000..a8dcd157ff8
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/collect_one_field.gfs
@@ -0,0 +1,32 @@
+
+
+ collect_one_field
+ collect_one_field
+
+ 6
+ EPSG:4326
+
+ 5
+ -1.00000
+ 9.16296
+ -3.00000
+ 6.08868
+
+
+ name
+ name
+ String
+ 2
+
+
+ intval
+ intval
+ Integer
+
+
+ floatval
+ floatval
+ Real
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/collect_one_field.gml b/python/plugins/processing/tests/testdata/expected/collect_one_field.gml
new file mode 100644
index 00000000000..51dd681198d
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/collect_one_field.gml
@@ -0,0 +1,50 @@
+
+
+
+
+ -1-3
+ 9.1629558541266826.088675623800385
+
+
+
+
+
+ -1,-1 -1,3 3,3 3,2 2,2 2,-1 -1,-13,2 6,1 6,-3 2,-1 2,2 3,2
+ aa
+ 1
+ 44.123456
+
+
+
+
+ 6.24145873320538,-0.054510556621882 7.24145873320538,-1.05451055662188 5.24145873320538,-1.05451055662188 6.24145873320538,-0.054510556621882
+ dd
+ 0
+
+
+
+
+ 2,5 2,6 3,6 3,5 2,52.44337811900192,4.42360844529751 2.44337811900192,5.42360844529751 3.44337811900192,5.42360844529751 3.44337811900192,4.42360844529751 2.44337811900192,4.423608445297514.17255278310941,4.82264875239923 4.17255278310941,5.82264875239923 5.17255278310941,5.82264875239923 5.17255278310941,4.82264875239923 4.17255278310941,4.822648752399232.62072936660269,5.08867562380038 2.62072936660269,6.08867562380038 3.62072936660269,6.08867562380038 3.62072936660269,5.08867562380038 2.62072936660269,5.08867562380038
+ bb
+ 1
+ 0.123
+
+
+
+
+ 8.16295585412668,2.73877159309021 8.16295585412668,3.73877159309021 9.16295585412668,3.73877159309021 9.16295585412668,2.73877159309021 8.16295585412668,2.73877159309021
+ cc
+ 0.123
+
+
+
+
+ 120
+ -100291.43213
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/collect_two_fields.gfs b/python/plugins/processing/tests/testdata/expected/collect_two_fields.gfs
new file mode 100644
index 00000000000..8ad47d9b9ab
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/collect_two_fields.gfs
@@ -0,0 +1,32 @@
+
+
+ collect_two_fields
+ collect_two_fields
+
+ 6
+ EPSG:4326
+
+ 6
+ -1.00000
+ 9.16296
+ -3.00000
+ 6.08868
+
+
+ name
+ name
+ String
+ 2
+
+
+ intval
+ intval
+ Integer
+
+
+ floatval
+ floatval
+ Real
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/collect_two_fields.gml b/python/plugins/processing/tests/testdata/expected/collect_two_fields.gml
new file mode 100644
index 00000000000..546ada415b7
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/collect_two_fields.gml
@@ -0,0 +1,58 @@
+
+
+
+
+ -1-3
+ 9.1629558541266826.088675623800385
+
+
+
+
+
+ 2.62072936660269,5.08867562380038 2.62072936660269,6.08867562380038 3.62072936660269,6.08867562380038 3.62072936660269,5.08867562380038 2.62072936660269,5.08867562380038
+ bb
+ 2
+ 0.123
+
+
+
+
+ -1,-1 -1,3 3,3 3,2 2,2 2,-1 -1,-13,2 6,1 6,-3 2,-1 2,2 3,2
+ aa
+ 1
+ 44.123456
+
+
+
+
+ 8.16295585412668,2.73877159309021 8.16295585412668,3.73877159309021 9.16295585412668,3.73877159309021 9.16295585412668,2.73877159309021 8.16295585412668,2.73877159309021
+ cc
+ 0.123
+
+
+
+
+ 6.24145873320538,-0.054510556621882 7.24145873320538,-1.05451055662188 5.24145873320538,-1.05451055662188 6.24145873320538,-0.054510556621882
+ dd
+ 0
+
+
+
+
+ 120
+ -100291.43213
+
+
+
+
+ 2,5 2,6 3,6 3,5 2,52.44337811900192,4.42360844529751 2.44337811900192,5.42360844529751 3.44337811900192,5.42360844529751 3.44337811900192,4.42360844529751 2.44337811900192,4.423608445297514.17255278310941,4.82264875239923 4.17255278310941,5.82264875239923 5.17255278310941,5.82264875239923 5.17255278310941,4.82264875239923 4.17255278310941,4.82264875239923
+ bb
+ 1
+ 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 02fa6a9675d..6d3f36318c6 100644
--- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml
+++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml
@@ -3437,3 +3437,42 @@ tests:
OUTPUT:
name: expected/promote_multipart_already_multi.gml
type: vector
+
+ - algorithm: native:collect
+ name: Test (native:collect)
+ params:
+ INPUT:
+ name: dissolve_polys.gml
+ type: vector
+ results:
+ OUTPUT:
+ name: expected/collect_all.gml
+ type: vector
+
+ - algorithm: native:collect
+ name: Test (native:collect)
+ params:
+ FIELD:
+ - name
+ INPUT:
+ name: dissolve_polys.gml
+ type: vector
+ results:
+ OUTPUT:
+ name: expected/collect_one_field.gml
+ type: vector
+
+ - algorithm: native:collect
+ name: Test (native:collect)
+ params:
+ FIELD:
+ - intval
+ - name
+ INPUT:
+ name: dissolve_polys.gml
+ type: vector
+ results:
+ OUTPUT:
+ name: expected/collect_two_fields.gml
+ type: vector
+
diff --git a/src/core/processing/qgsnativealgorithms.cpp b/src/core/processing/qgsnativealgorithms.cpp
index b16b921e41a..cb473b0b027 100644
--- a/src/core/processing/qgsnativealgorithms.cpp
+++ b/src/core/processing/qgsnativealgorithms.cpp
@@ -25,6 +25,8 @@
#include "qgsgeometryengine.h"
#include "qgswkbtypes.h"
+#include
+
///@cond PRIVATE
QgsNativeAlgorithms::QgsNativeAlgorithms( QObject *parent )
@@ -62,6 +64,7 @@ void QgsNativeAlgorithms::loadAlgorithms()
addAlgorithm( new QgsCentroidAlgorithm() );
addAlgorithm( new QgsClipAlgorithm() );
addAlgorithm( new QgsDissolveAlgorithm() );
+ addAlgorithm( new QgsCollectAlgorithm() );
addAlgorithm( new QgsExtractByAttributeAlgorithm() );
addAlgorithm( new QgsExtractByExpressionAlgorithm() );
addAlgorithm( new QgsMultipartToSinglepartAlgorithm() );
@@ -243,7 +246,8 @@ QgsDissolveAlgorithm *QgsDissolveAlgorithm::createInstance() const
return new QgsDissolveAlgorithm();
}
-QVariantMap QgsDissolveAlgorithm::processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
+QVariantMap QgsCollectorAlgorithm::processCollection( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback,
+ std::function& )> collector, int maxQueueLength )
{
std::unique_ptr< QgsFeatureSource > source( parameterAsSource( parameters, QStringLiteral( "INPUT" ), context ) );
if ( !source )
@@ -289,10 +293,10 @@ QVariantMap QgsDissolveAlgorithm::processAlgorithm( const QVariantMap ¶meter
if ( f.hasGeometry() && f.geometry() )
{
geomQueue.append( f.geometry() );
- if ( geomQueue.length() > 10000 )
+ if ( maxQueueLength > 0 && geomQueue.length() > maxQueueLength )
{
// queue too long, combine it
- QgsGeometry tempOutputGeometry = QgsGeometry::unaryUnion( geomQueue );
+ QgsGeometry tempOutputGeometry = collector( geomQueue );
geomQueue.clear();
geomQueue << tempOutputGeometry;
}
@@ -302,7 +306,7 @@ QVariantMap QgsDissolveAlgorithm::processAlgorithm( const QVariantMap ¶meter
current++;
}
- outputFeature.setGeometry( QgsGeometry::unaryUnion( geomQueue ) );
+ outputFeature.setGeometry( collector( geomQueue ) );
sink->addFeature( outputFeature, QgsFeatureSink::FastInsert );
}
else
@@ -355,7 +359,7 @@ QVariantMap QgsDissolveAlgorithm::processAlgorithm( const QVariantMap ¶meter
QgsFeature outputFeature;
if ( geometryHash.contains( attrIt.key() ) )
{
- QgsGeometry geom = QgsGeometry::unaryUnion( geometryHash.value( attrIt.key() ) );
+ QgsGeometry geom = collector( geometryHash.value( attrIt.key() ) );
if ( !geom.isMultipart() )
{
geom.convertToMultiType();
@@ -375,6 +379,23 @@ QVariantMap QgsDissolveAlgorithm::processAlgorithm( const QVariantMap ¶meter
return outputs;
}
+QVariantMap QgsDissolveAlgorithm::processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
+{
+ return processCollection( parameters, context, feedback, []( const QList< QgsGeometry > &parts )->QgsGeometry
+ {
+ return QgsGeometry::unaryUnion( parts );
+ }, 10000 );
+}
+
+QVariantMap QgsCollectAlgorithm::processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
+{
+ return processCollection( parameters, context, feedback, []( const QList< QgsGeometry > &parts )->QgsGeometry
+ {
+ return QgsGeometry::collectGeometry( parts );
+ } );
+}
+
+
void QgsClipAlgorithm::initAlgorithm( const QVariantMap & )
{
addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "INPUT" ), QObject::tr( "Input layer" ) ) );
@@ -1289,6 +1310,31 @@ QgsFeature QgsPromoteToMultipartAlgorithm::processFeature( const QgsFeature &fea
}
return f;
}
+
+
+void QgsCollectAlgorithm::initAlgorithm( const QVariantMap & )
+{
+ addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "INPUT" ), QObject::tr( "Input layer" ) ) );
+ addParameter( new QgsProcessingParameterField( QStringLiteral( "FIELD" ), QObject::tr( "Unique ID fields" ), QVariant(),
+ QStringLiteral( "INPUT" ), QgsProcessingParameterField::Any, true, true ) );
+
+ addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "OUTPUT" ), QObject::tr( "Collected" ) ) );
+}
+
+QString QgsCollectAlgorithm::shortHelpString() const
+{
+ return QObject::tr( "This algorithm takes a vector layer and collects its geometries into new multipart geometries. One or more attributes can "
+ "be specified to collect only geometries belonging to the same class (having the same value for the specified attributes), alternatively "
+ "all geometries can be collected.\n\n"
+ "All output geometries will be converted to multi geometries, even those with just a single part. "
+ "This algorithm does not dissolve overlapping geometries - they will be collected together without modifying the shape of each geometry part." );
+}
+
+QgsCollectAlgorithm *QgsCollectAlgorithm::createInstance() const
+{
+ return new QgsCollectAlgorithm();
+}
+
///@endcond
diff --git a/src/core/processing/qgsnativealgorithms.h b/src/core/processing/qgsnativealgorithms.h
index ea0111413da..1a6d86f3c82 100644
--- a/src/core/processing/qgsnativealgorithms.h
+++ b/src/core/processing/qgsnativealgorithms.h
@@ -131,10 +131,21 @@ class QgsBufferAlgorithm : public QgsProcessingAlgorithm
};
+/**
+ * Base class for dissolve/collect type algorithms.
+ */
+class QgsCollectorAlgorithm : public QgsProcessingAlgorithm
+{
+ protected:
+
+ QVariantMap processCollection( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback,
+ std::function& )> collector, int maxQueueLength = 0 );
+};
+
/**
* Native dissolve algorithm.
*/
-class QgsDissolveAlgorithm : public QgsProcessingAlgorithm
+class QgsDissolveAlgorithm : public QgsCollectorAlgorithm
{
public:
@@ -155,6 +166,30 @@ class QgsDissolveAlgorithm : public QgsProcessingAlgorithm
};
+/**
+ * Native collect geometries algorithm.
+ */
+class QgsCollectAlgorithm : public QgsCollectorAlgorithm
+{
+
+ public:
+
+ QgsCollectAlgorithm() = default;
+ void initAlgorithm( const QVariantMap &configuration = QVariantMap() ) override;
+ QString name() const override { return QStringLiteral( "collect" ); }
+ QString displayName() const override { return QObject::tr( "Collect geometries" ); }
+ virtual QStringList tags() const override { return QObject::tr( "union,combine,collect,multipart,parts,single" ).split( ',' ); }
+ QString group() const override { return QObject::tr( "Vector geometry" ); }
+ QString shortHelpString() const override;
+ QgsCollectAlgorithm *createInstance() const override SIP_FACTORY;
+
+ protected:
+
+ virtual QVariantMap processAlgorithm( const QVariantMap ¶meters,
+ QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override;
+
+};
+
/**
* Native extract by attribute algorithm.
*/