diff --git a/python/core/auto_generated/geometry/qgsgeometry.sip.in b/python/core/auto_generated/geometry/qgsgeometry.sip.in index b0427711dacf..fe868f7dc8c4 100644 --- a/python/core/auto_generated/geometry/qgsgeometry.sip.in +++ b/python/core/auto_generated/geometry/qgsgeometry.sip.in @@ -1853,6 +1853,20 @@ This method requires a QGIS build based on GEOS 3.12 or later. .. seealso:: :py:func:`validateCoverage` +.. versionadded:: 3.36 +%End + + QgsGeometry unionCoverage() const; +%Docstring +Optimized union algorithm for polygonal inputs that are correctly noded and do not overlap. +It may generate an error (returning a null geometry) for inputs that do not satisfy this constraint, +however this is not guaranteed. + +The input geometry is the polygonal coverage to union, stored in a geometry collection. +All members must be POLYGON or MULTIPOLYGON. + +.. seealso:: :py:func:`validateCoverage` + .. versionadded:: 3.36 %End diff --git a/src/analysis/CMakeLists.txt b/src/analysis/CMakeLists.txt index c6df6db81e30..5c4bb11a4a3a 100644 --- a/src/analysis/CMakeLists.txt +++ b/src/analysis/CMakeLists.txt @@ -67,6 +67,7 @@ set(QGIS_ANALYSIS_SRCS processing/qgsalgorithmconverttocurves.cpp processing/qgsalgorithmconvexhull.cpp processing/qgsalgorithmcoveragesimplify.cpp + processing/qgsalgorithmcoverageunion.cpp processing/qgsalgorithmcoveragevalidate.cpp processing/qgsalgorithmcreatedirectory.cpp processing/qgsalgorithmdbscanclustering.cpp diff --git a/src/analysis/processing/qgsalgorithmcoveragesimplify.cpp b/src/analysis/processing/qgsalgorithmcoveragesimplify.cpp index c17ce9a5f68c..5a827a5f3134 100644 --- a/src/analysis/processing/qgsalgorithmcoveragesimplify.cpp +++ b/src/analysis/processing/qgsalgorithmcoveragesimplify.cpp @@ -40,12 +40,12 @@ QStringList QgsCoverageSimplifyAlgorithm::tags() const QString QgsCoverageSimplifyAlgorithm::group() const { - return QObject::tr( "Vector geometry" ); + return QObject::tr( "Vector coverage" ); } QString QgsCoverageSimplifyAlgorithm::groupId() const { - return QStringLiteral( "vectorgeometry" ); + return QStringLiteral( "vectorcoverage" ); } void QgsCoverageSimplifyAlgorithm::initAlgorithm( const QVariantMap & ) @@ -130,7 +130,7 @@ QVariantMap QgsCoverageSimplifyAlgorithm::processAlgorithm( const QVariantMap &p std::unique_ptr< QgsAbstractGeometry > simplified; try { - simplified.reset( geos.simplifyCoverageVW( tolerance, preserveBoundary, &error ) ); + simplified = geos.simplifyCoverageVW( tolerance, preserveBoundary, &error ); } catch ( QgsNotSupportedException &e ) { diff --git a/src/analysis/processing/qgsalgorithmcoverageunion.cpp b/src/analysis/processing/qgsalgorithmcoverageunion.cpp new file mode 100644 index 000000000000..4f00112b0268 --- /dev/null +++ b/src/analysis/processing/qgsalgorithmcoverageunion.cpp @@ -0,0 +1,139 @@ +/*************************************************************************** + qgsalgorithmcoverageunion.cpp + --------------------- + begin : October 2023 + copyright : (C) 2023 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + + +#include "qgsalgorithmcoverageunion.h" +#include "qgsgeometrycollection.h" +#include "qgsgeos.h" + + +///@cond PRIVATE + +QString QgsCoverageUnionAlgorithm::name() const +{ + return QStringLiteral( "coverageunion" ); +} + +QString QgsCoverageUnionAlgorithm::displayName() const +{ + return QObject::tr( "Dissolve coverage" ); +} + +QStringList QgsCoverageUnionAlgorithm::tags() const +{ + return QObject::tr( "union,merge,topological,boundary" ).split( ',' ); +} + +QString QgsCoverageUnionAlgorithm::group() const +{ + return QObject::tr( "Vector coverage" ); +} + +QString QgsCoverageUnionAlgorithm::groupId() const +{ + return QStringLiteral( "vectorcoverage" ); +} + +void QgsCoverageUnionAlgorithm::initAlgorithm( const QVariantMap & ) +{ + addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "INPUT" ), QObject::tr( "Input layer" ), QList< int >() << QgsProcessing::TypeVectorPolygon ) ); + addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "OUTPUT" ), QObject::tr( "Dissolved" ), QgsProcessing::TypeVectorPolygon ) ); +} + +QString QgsCoverageUnionAlgorithm::shortHelpString() const +{ + return QObject::tr( "" ); +} + +QgsCoverageUnionAlgorithm *QgsCoverageUnionAlgorithm::createInstance() const +{ + return new QgsCoverageUnionAlgorithm(); +} + +QVariantMap QgsCoverageUnionAlgorithm::processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) +{ + std::unique_ptr< QgsProcessingFeatureSource > source( parameterAsSource( parameters, QStringLiteral( "INPUT" ), context ) ); + if ( !source ) + throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "INPUT" ) ) ); + + QString sinkId; + std::unique_ptr< QgsFeatureSink > sink( parameterAsSink( parameters, QStringLiteral( "OUTPUT" ), context, sinkId, source->fields(), Qgis::WkbType::MultiPolygon, source->sourceCrs() ) ); + if ( !sink ) + throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "OUTPUT" ) ) ); + + // we have to build up a list of geometries in advance + QgsGeometryCollection collection; + + const long count = source->featureCount(); + if ( count > 0 ) + { + collection.reserve( count ); + } + + const double step = count > 0 ? 100.0 / count : 1; + int current = 0; + + feedback->pushInfo( QObject::tr( "Collecting features" ) ); + + QgsFeature inFeature; + QgsFeatureRequest req; + req.setNoAttributes(); + QgsFeatureIterator features = source->getFeatures( req ); + while ( features.nextFeature( inFeature ) ) + { + if ( feedback->isCanceled() ) + { + break; + } + + if ( inFeature.hasGeometry() ) + { + collection.addGeometry( inFeature.geometry().constGet()->clone() ); + } + + feedback->setProgress( current * step * 0.2 ); + current++; + } + + feedback->pushInfo( QObject::tr( "Dissolving coverage" ) ); + + QgsGeos geos( &collection ); + QString error; + std::unique_ptr< QgsAbstractGeometry > dissolved = geos.unionCoverage( &error ); + + if ( !dissolved ) + { + if ( !error.isEmpty() ) + throw QgsProcessingException( error ); + else + throw QgsProcessingException( QObject::tr( "No geometry was returned for dissolved coverage" ) ); + } + + feedback->setProgress( 95 ); + + feedback->pushInfo( QObject::tr( "Storing output" ) ); + QgsFeature outFeature( source->fields() ); + outFeature.setGeometry( std::move( dissolved ) ); + if ( !sink->addFeature( outFeature, QgsFeatureSink::FastInsert ) ) + throw QgsProcessingException( writeFeatureError( sink.get(), parameters, QStringLiteral( "OUTPUT" ) ) ); + + QVariantMap outputs; + outputs.insert( QStringLiteral( "OUTPUT" ), sinkId ); + return outputs; +} + +///@endcond diff --git a/src/analysis/processing/qgsalgorithmcoverageunion.h b/src/analysis/processing/qgsalgorithmcoverageunion.h new file mode 100644 index 000000000000..dcb5c87dbccb --- /dev/null +++ b/src/analysis/processing/qgsalgorithmcoverageunion.h @@ -0,0 +1,55 @@ +/*************************************************************************** + qgsalgorithmcoverageunion.h + --------------------- + begin : October 2023 + copyright : (C) 2023 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSALGORITHMCOVERAGEUNION_H +#define QGSALGORITHMCOVERAGEUNION_H + +#define SIP_NO_FILE + +#include "qgis_sip.h" +#include "qgsprocessingalgorithm.h" + +///@cond PRIVATE + +/** + * Native coverage union algorithm. + */ +class QgsCoverageUnionAlgorithm : public QgsProcessingAlgorithm +{ + public: + QgsCoverageUnionAlgorithm() = default; + void initAlgorithm( const QVariantMap &configuration = QVariantMap() ) override; + QString name() const override; + QString displayName() const override; + QStringList tags() const override; + QString group() const override; + QString groupId() const override; + QString shortHelpString() const override; + QgsCoverageUnionAlgorithm *createInstance() const override SIP_FACTORY; + + protected: + + QVariantMap processAlgorithm( const QVariantMap ¶meters, + QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; +}; + + +///@endcond PRIVATE + +#endif // QGSALGORITHMCOVERAGEUNION_H + + diff --git a/src/analysis/processing/qgsalgorithmcoveragevalidate.cpp b/src/analysis/processing/qgsalgorithmcoveragevalidate.cpp index 61bb42e5b46f..e362e40f4989 100644 --- a/src/analysis/processing/qgsalgorithmcoveragevalidate.cpp +++ b/src/analysis/processing/qgsalgorithmcoveragevalidate.cpp @@ -41,12 +41,12 @@ QStringList QgsCoverageValidateAlgorithm::tags() const QString QgsCoverageValidateAlgorithm::group() const { - return QObject::tr( "Vector geometry" ); + return QObject::tr( "Vector coverage" ); } QString QgsCoverageValidateAlgorithm::groupId() const { - return QStringLiteral( "vectorgeometry" ); + return QStringLiteral( "vectorcoverage" ); } void QgsCoverageValidateAlgorithm::initAlgorithm( const QVariantMap & ) diff --git a/src/analysis/processing/qgsnativealgorithms.cpp b/src/analysis/processing/qgsnativealgorithms.cpp index 5f159682e8fa..179b15a3ba8b 100644 --- a/src/analysis/processing/qgsnativealgorithms.cpp +++ b/src/analysis/processing/qgsnativealgorithms.cpp @@ -48,6 +48,7 @@ #include "qgsalgorithmconverttocurves.h" #include "qgsalgorithmconvexhull.h" #include "qgsalgorithmcoveragesimplify.h" +#include "qgsalgorithmcoverageunion.h" #include "qgsalgorithmcoveragevalidate.h" #include "qgsalgorithmcreatedirectory.h" #include "qgsalgorithmdbscanclustering.h" @@ -316,6 +317,7 @@ void QgsNativeAlgorithms::loadAlgorithms() addAlgorithm( new QgsConvertToCurvesAlgorithm() ); addAlgorithm( new QgsConvexHullAlgorithm() ); addAlgorithm( new QgsCoverageSimplifyAlgorithm() ); + addAlgorithm( new QgsCoverageUnionAlgorithm() ); addAlgorithm( new QgsCoverageValidateAlgorithm() ); addAlgorithm( new QgsCreateDirectoryAlgorithm() ); addAlgorithm( new QgsDbscanClusteringAlgorithm() ); diff --git a/src/core/geometry/qgsgeometry.cpp b/src/core/geometry/qgsgeometry.cpp index 15d9f919bd34..ab42cb263d41 100644 --- a/src/core/geometry/qgsgeometry.cpp +++ b/src/core/geometry/qgsgeometry.cpp @@ -2638,6 +2638,20 @@ QgsGeometry QgsGeometry::delaunayTriangulation( double tolerance, bool edgesOnly return result; } +QgsGeometry QgsGeometry::unionCoverage() const +{ + if ( !d->geometry ) + { + return QgsGeometry(); + } + + QgsGeos geos( d->geometry.get() ); + mLastError.clear(); + const QgsGeometry result = QgsGeometry( geos.unionCoverage( &mLastError ) ); + result.mLastError = mLastError; + return result; +} + Qgis::CoverageValidityResult QgsGeometry::validateCoverage( double gapWidth, QgsGeometry *invalidEdges ) const { if ( !d->geometry ) diff --git a/src/core/geometry/qgsgeometry.h b/src/core/geometry/qgsgeometry.h index ef445d9e478c..4c8b8d0a5e6f 100644 --- a/src/core/geometry/qgsgeometry.h +++ b/src/core/geometry/qgsgeometry.h @@ -1874,6 +1874,19 @@ class CORE_EXPORT QgsGeometry */ QgsGeometry simplifyCoverageVW( double tolerance, bool preserveBoundary ) const SIP_THROW( QgsNotSupportedException ); + /** + * Optimized union algorithm for polygonal inputs that are correctly noded and do not overlap. + * It may generate an error (returning a null geometry) for inputs that do not satisfy this constraint, + * however this is not guaranteed. + * + * The input geometry is the polygonal coverage to union, stored in a geometry collection. + * All members must be POLYGON or MULTIPOLYGON. + * + * \see validateCoverage() + * \since QGIS 3.36 + */ + QgsGeometry unionCoverage() const; + /** * Returns a (Multi)LineString representing the fully noded version of a collection of linestrings. * diff --git a/src/core/geometry/qgsgeos.cpp b/src/core/geometry/qgsgeos.cpp index 91ecb79bfad0..9cc7dde80271 100644 --- a/src/core/geometry/qgsgeos.cpp +++ b/src/core/geometry/qgsgeos.cpp @@ -2087,7 +2087,7 @@ Qgis::CoverageValidityResult QgsGeos::validateCoverage( double gapWidth, std::un #endif } -QgsAbstractGeometry *QgsGeos::simplifyCoverageVW( double tolerance, bool preserveBoundary, QString *errorMsg ) const +std::unique_ptr QgsGeos::simplifyCoverageVW( double tolerance, bool preserveBoundary, QString *errorMsg ) const { #if GEOS_VERSION_MAJOR==3 && GEOS_VERSION_MINOR<12 ( void )tolerance; @@ -2106,12 +2106,30 @@ QgsAbstractGeometry *QgsGeos::simplifyCoverageVW( double tolerance, bool preserv { geos::unique_ptr simplified( GEOSCoverageSimplifyVW_r( geosinit()->ctxt, mGeos.get(), tolerance, preserveBoundary ? 1 : 0 ) ); std::unique_ptr< QgsAbstractGeometry > simplifiedGeom = fromGeos( simplified.get() ); - return simplifiedGeom.release(); + return simplifiedGeom; } CATCH_GEOS_WITH_ERRMSG( nullptr ) #endif } +std::unique_ptr QgsGeos::unionCoverage( QString *errorMsg ) const +{ + if ( !mGeos ) + { + if ( errorMsg ) + *errorMsg = QStringLiteral( "Input geometry was not set" ); + return nullptr; + } + + try + { + geos::unique_ptr unioned( GEOSCoverageUnion_r( geosinit()->ctxt, mGeos.get() ) ); + std::unique_ptr< QgsAbstractGeometry > result = fromGeos( unioned.get() ); + return result; + } + CATCH_GEOS_WITH_ERRMSG( nullptr ) +} + bool QgsGeos::isValid( QString *errorMsg, const bool allowSelfTouchingHoles, QgsGeometry *errorLoc ) const { if ( !mGeos ) diff --git a/src/core/geometry/qgsgeos.h b/src/core/geometry/qgsgeos.h index 38cf7ba6cc62..736e8ee09fb8 100644 --- a/src/core/geometry/qgsgeos.h +++ b/src/core/geometry/qgsgeos.h @@ -616,7 +616,20 @@ class CORE_EXPORT QgsGeos: public QgsGeometryEngine * \see validateCoverage() * \since QGIS 3.36 */ - QgsAbstractGeometry *simplifyCoverageVW( double tolerance, bool preserveBoundary, QString *errorMsg = nullptr ) const; + std::unique_ptr< QgsAbstractGeometry > simplifyCoverageVW( double tolerance, bool preserveBoundary, QString *errorMsg = nullptr ) const; + + /** + * Optimized union algorithm for polygonal inputs that are correctly noded and do not overlap. + * It may generate an error (returns NULLPTR) for inputs that do not satisfy this constraint, + * however this is not guaranteed. + * + * The input geometry is the polygonal coverage to union, stored in a geometry collection. + * All members must be POLYGON or MULTIPOLYGON. + * + * \see validateCoverage() + * \since QGIS 3.36 + */ + std::unique_ptr< QgsAbstractGeometry > unionCoverage( QString *errorMsg = nullptr ) const; /** * Create a geometry from a GEOSGeometry