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. */