From 54438c3b15971c826f222b4bdd66b929a2a1140e Mon Sep 17 00:00:00 2001 From: Jacky Volpes Date: Fri, 19 Jul 2024 16:34:40 +0200 Subject: [PATCH] Processing: Add Fix Geometry algorithm and test: Missing Vertex Porting missing vertex fix from geometry checker to processings --- src/analysis/CMakeLists.txt | 1 + .../qgsalgorithmfixgeometrymissingvertex.cpp | 256 ++++++++++++++++++ .../qgsalgorithmfixgeometrymissingvertex.h | 55 ++++ .../processing/qgsnativealgorithms.cpp | 2 + .../qgsgeometrymissingvertexcheck.h | 1 + .../analysis/testqgsprocessingfixgeometry.cpp | 51 ++++ .../geometry_fix/add_missing_vertex.gpkg | Bin 0 -> 98304 bytes 7 files changed, 366 insertions(+) create mode 100644 src/analysis/processing/qgsalgorithmfixgeometrymissingvertex.cpp create mode 100644 src/analysis/processing/qgsalgorithmfixgeometrymissingvertex.h create mode 100644 tests/testdata/geometry_fix/add_missing_vertex.gpkg diff --git a/src/analysis/CMakeLists.txt b/src/analysis/CMakeLists.txt index c8065d81675d6..a77dabf1f01e7 100644 --- a/src/analysis/CMakeLists.txt +++ b/src/analysis/CMakeLists.txt @@ -66,6 +66,7 @@ set(QGIS_ANALYSIS_SRCS processing/qgsalgorithmcheckgeometryhole.cpp processing/qgsalgorithmfixgeometryhole.cpp processing/qgsalgorithmcheckgeometrymissingvertex.cpp + processing/qgsalgorithmfixgeometrymissingvertex.cpp processing/qgsalgorithmcheckgeometryarea.cpp processing/qgsalgorithmfixgeometryarea.cpp processing/qgsalgorithmclip.cpp diff --git a/src/analysis/processing/qgsalgorithmfixgeometrymissingvertex.cpp b/src/analysis/processing/qgsalgorithmfixgeometrymissingvertex.cpp new file mode 100644 index 0000000000000..be6e447114338 --- /dev/null +++ b/src/analysis/processing/qgsalgorithmfixgeometrymissingvertex.cpp @@ -0,0 +1,256 @@ +/*************************************************************************** + qgsalgorithmfixgeometrymissingvertex.cpp + --------------------- + begin : June 2024 + copyright : (C) 2024 by Jacky Volpes + email : jacky dot volpes at oslandia 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 "qgsalgorithmfixgeometrymissingvertex.h" +#include "qgsgeometrymissingvertexcheck.h" +#include "qgsvectordataproviderfeaturepool.h" +#include "qgsgeometrycheckerror.h" +#include "qgsvectorfilewriter.h" + +///@cond PRIVATE + +auto QgsFixGeometryMissingVertexAlgorithm::name() const -> QString +{ + return QStringLiteral( "fixgeometrymissingvertex" ); +} + +auto QgsFixGeometryMissingVertexAlgorithm::displayName() const -> QString +{ + return QObject::tr( "Fix geometry (Missing Vertex)" ); +} + +auto QgsFixGeometryMissingVertexAlgorithm::tags() const -> QStringList +{ + return QObject::tr( "fix,missing,vertex,polygons" ).split( ',' ); +} + +auto QgsFixGeometryMissingVertexAlgorithm::group() const -> QString +{ + return QObject::tr( "Fix geometry" ); +} + +auto QgsFixGeometryMissingVertexAlgorithm::groupId() const -> QString +{ + return QStringLiteral( "fixgeometry" ); +} + +auto QgsFixGeometryMissingVertexAlgorithm::shortHelpString() const -> QString +{ + return QObject::tr( "This algorithm adds the missing vertices along polygons junctions, " + "based on an error layer from the check missing vertex algorithm." ); +} + +auto QgsFixGeometryMissingVertexAlgorithm::createInstance() const -> QgsFixGeometryMissingVertexAlgorithm * +{ + return new QgsFixGeometryMissingVertexAlgorithm(); +} + +void QgsFixGeometryMissingVertexAlgorithm::initAlgorithm( const QVariantMap &configuration ) +{ + Q_UNUSED( configuration ) + + // Inputs + addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "INPUT" ), QObject::tr( "Input layer" ), + QList< int >() << static_cast( Qgis::ProcessingSourceType::VectorPolygon ) ) + ); + addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "ERRORS" ), QObject::tr( "Errors layer" ), + QList< int >() << static_cast( Qgis::ProcessingSourceType::VectorPoint ) ) + ); + + addParameter( new QgsProcessingParameterField( + QStringLiteral( "UNIQUE_ID" ), QObject::tr( "Field of original feature unique identifier" ), + QStringLiteral( "id" ), QStringLiteral( "ERRORS" ) ) + ); + addParameter( new QgsProcessingParameterField( + QStringLiteral( "PART_IDX" ), QObject::tr( "Field of part index" ), + QStringLiteral( "gc_partidx" ), QStringLiteral( "ERRORS" ), + Qgis::ProcessingFieldParameterDataType::Numeric ) + ); + addParameter( new QgsProcessingParameterField( + QStringLiteral( "RING_IDX" ), QObject::tr( "Field of ring index" ), + QStringLiteral( "gc_ringidx" ), QStringLiteral( "ERRORS" ), + Qgis::ProcessingFieldParameterDataType::Numeric ) + ); + addParameter( new QgsProcessingParameterField( + QStringLiteral( "VERTEX_IDX" ), QObject::tr( "Field of vertex index" ), + QStringLiteral( "gc_vertidx" ), QStringLiteral( "ERRORS" ), + Qgis::ProcessingFieldParameterDataType::Numeric ) + ); + + // Outputs + addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "OUTPUT" ), QObject::tr( "Output layer" ), Qgis::ProcessingSourceType::VectorPolygon ) ); + addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "REPORT" ), QObject::tr( "Report layer" ), Qgis::ProcessingSourceType::VectorPoint ) ); + + std::unique_ptr< QgsProcessingParameterNumber > tolerance = std::make_unique< QgsProcessingParameterNumber >( QStringLiteral( "TOLERANCE" ), + QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13 ); + tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced ); + addParameter( tolerance.release() ); +} + +auto QgsFixGeometryMissingVertexAlgorithm::processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) -> QVariantMap +{ + const std::unique_ptr< QgsProcessingFeatureSource > input( parameterAsSource( parameters, QStringLiteral( "INPUT" ), context ) ); + if ( !input ) + throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "INPUT" ) ) ); + + const std::unique_ptr< QgsProcessingFeatureSource > errors( parameterAsSource( parameters, QStringLiteral( "ERRORS" ), context ) ); + if ( !errors ) + throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "ERRORS" ) ) ); + + QgsProcessingMultiStepFeedback multiStepFeedback( 2, feedback ); + + const QString featIdFieldName = parameterAsString( parameters, QStringLiteral( "UNIQUE_ID" ), context ); + const QString partIdxFieldName = parameterAsString( parameters, QStringLiteral( "PART_IDX" ), context ); + const QString ringIdxFieldName = parameterAsString( parameters, QStringLiteral( "RING_IDX" ), context ); + const QString vertexIdxFieldName = parameterAsString( parameters, QStringLiteral( "VERTEX_IDX" ), context ); + + // Verify that input fields exists + if ( errors->fields().indexFromName( featIdFieldName ) == -1 ) + throw QgsProcessingException( QObject::tr( "Field %1 does not exist in errors layer." ).arg( featIdFieldName ) ); + if ( errors->fields().indexFromName( partIdxFieldName ) == -1 ) + throw QgsProcessingException( QObject::tr( "Field %1 does not exist in errors layer." ).arg( partIdxFieldName ) ); + if ( errors->fields().indexFromName( ringIdxFieldName ) == -1 ) + throw QgsProcessingException( QObject::tr( "Field %1 does not exist in errors layer." ).arg( ringIdxFieldName ) ); + if ( errors->fields().indexFromName( vertexIdxFieldName ) == -1 ) + throw QgsProcessingException( QObject::tr( "Field %1 does not exist in errors layer." ).arg( vertexIdxFieldName ) ); + int inputIdFieldIndex = input->fields().indexFromName( featIdFieldName ); + if ( inputIdFieldIndex == -1 ) + throw QgsProcessingException( QObject::tr( "Field %1 does not exist in input layer." ).arg( featIdFieldName ) ); + + QgsField inputFeatIdField = input->fields().at( inputIdFieldIndex ); + if ( inputFeatIdField.type() != errors->fields().at( errors->fields().indexFromName( featIdFieldName ) ).type() ) + throw QgsProcessingException( QObject::tr( "Field %1 does not have the same type than in errors layer." ).arg( featIdFieldName ) ); + + QString dest_output; + const std::unique_ptr< QgsFeatureSink > sink_output( parameterAsSink( parameters, QStringLiteral( "OUTPUT" ), context, dest_output, input->fields(), input->wkbType(), input->sourceCrs() ) ); + if ( !sink_output ) + throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "OUTPUT" ) ) ); + + QString dest_report; + QgsFields reportFields = errors->fields(); + reportFields.append( QgsField( QStringLiteral( "report" ), QMetaType::QString ) ); + reportFields.append( QgsField( QStringLiteral( "error_fixed" ), QMetaType::Bool ) ); + const std::unique_ptr< QgsFeatureSink > sink_report( parameterAsSink( parameters, QStringLiteral( "REPORT" ), context, dest_report, reportFields, errors->wkbType(), errors->sourceCrs() ) ); + if ( !sink_report ) + throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "REPORT" ) ) ); + + const QgsProject *project = QgsProject::instance(); + std::unique_ptr checkContext = std::make_unique( mTolerance, input->sourceCrs(), project->transformContext(), project ); + QStringList messages; + + const QgsGeometryMissingVertexCheck check( checkContext.get(), QVariantMap() ); + + QgsVectorLayer *fixedLayer = input->materialize( QgsFeatureRequest() ); + std::unique_ptr featurePool = std::make_unique( fixedLayer, false ); + QMap featurePools; + featurePools.insert( fixedLayer->id(), featurePool.get() ); + + QgsFeature errorFeature, inputFeature, testDuplicateIdFeature; + QgsFeatureIterator errorFeaturesIt = errors->getFeatures(); + QList changesList; + QgsFeature reportFeature; + reportFeature.setFields( reportFields ); + long long progression = 0; + long long totalProgression = errors->featureCount(); + multiStepFeedback.setCurrentStep( 1 ); + multiStepFeedback.setProgressText( QObject::tr( "Fixing errors..." ) ); + while ( errorFeaturesIt.nextFeature( errorFeature ) ) + { + progression++; + multiStepFeedback.setProgress( static_cast( progression ) / totalProgression * 100 ); + reportFeature.setGeometry( errorFeature.geometry() ); + + QVariant attr = errorFeature.attribute( featIdFieldName ); + if ( !attr.isValid() || attr.isNull() ) + throw QgsProcessingException( QObject::tr( "NULL or invalid value found in unique field %1" ).arg( featIdFieldName ) ); + + QString idValue = errorFeature.attribute( featIdFieldName ).toString(); + if ( inputFeatIdField.type() == QMetaType::QString ) + idValue = "'" + idValue + "'"; + + QgsFeatureIterator it = fixedLayer->getFeatures( QgsFeatureRequest().setFilterExpression( "\"" + featIdFieldName + "\" = " + idValue ) ); + if ( !it.nextFeature( inputFeature ) || !inputFeature.isValid() ) + reportFeature.setAttributes( errorFeature.attributes() << QObject::tr( "Source feature not found or invalid" ) << false ); + + else if ( it.nextFeature( testDuplicateIdFeature ) ) + throw QgsProcessingException( QObject::tr( "More than one feature found in input layer with value %1 in unique field %2" ).arg( idValue ).arg( featIdFieldName ) ); + + else if ( inputFeature.geometry().isNull() ) + reportFeature.setAttributes( errorFeature.attributes() << QObject::tr( "Feature geometry is null" ) << false ); + + else + { + QgsGeometryCheckError checkError = QgsGeometryCheckError( + &check, + QgsGeometryCheckerUtils::LayerFeature( featurePool.get(), inputFeature, checkContext.get(), false ), + errorFeature.geometry().asPoint(), + QgsVertexId( + errorFeature.attribute( partIdxFieldName ).toInt(), + errorFeature.attribute( ringIdxFieldName ).toInt(), + errorFeature.attribute( vertexIdxFieldName ).toInt() + ) + ); + for ( auto changes : changesList ) + checkError.handleChanges( changes ); + + QgsGeometryCheck::Changes changes; + check.fixError( featurePools, &checkError, QgsGeometryMissingVertexCheck::ResolutionMethod::AddMissingVertex, QMap(), changes ); + changesList << changes; + reportFeature.setAttributes( errorFeature.attributes() << checkError.resolutionMessage() << ( checkError.status() == QgsGeometryCheckError::StatusFixed ) ); + } + + if ( !sink_report->addFeature( reportFeature, QgsFeatureSink::FastInsert ) ) + throw QgsProcessingException( writeFeatureError( sink_report.get(), parameters, QStringLiteral( "REPORT" ) ) ); + + } + multiStepFeedback.setProgress( 100 ); + + progression = 0; + totalProgression = fixedLayer->featureCount(); + multiStepFeedback.setCurrentStep( 2 ); + multiStepFeedback.setProgressText( QObject::tr( "Exporting fixed layer..." ) ); + QgsFeature fixedFeature; + QgsFeatureIterator fixedFeaturesIt = fixedLayer->getFeatures(); + while ( fixedFeaturesIt.nextFeature( fixedFeature ) ) + { + progression++; + multiStepFeedback.setProgress( static_cast( progression ) / totalProgression * 100 ); + if ( !sink_output->addFeature( fixedFeature, QgsFeatureSink::FastInsert ) ) + throw QgsProcessingException( writeFeatureError( sink_output.get(), parameters, QStringLiteral( "OUTPUT" ) ) ); + } + multiStepFeedback.setProgress( 100 ); + + QVariantMap outputs; + outputs.insert( QStringLiteral( "OUTPUT" ), dest_output ); + outputs.insert( QStringLiteral( "REPORT" ), dest_report ); + + return outputs; +} + +auto QgsFixGeometryMissingVertexAlgorithm::prepareAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback * ) -> bool +{ + mTolerance = parameterAsInt( parameters, QStringLiteral( "TOLERANCE" ), context ); + + return true; +} + +auto QgsFixGeometryMissingVertexAlgorithm::flags() const -> Qgis::ProcessingAlgorithmFlags +{ + return QgsProcessingAlgorithm::flags() | Qgis::ProcessingAlgorithmFlag::NoThreading; +} + +///@endcond diff --git a/src/analysis/processing/qgsalgorithmfixgeometrymissingvertex.h b/src/analysis/processing/qgsalgorithmfixgeometrymissingvertex.h new file mode 100644 index 0000000000000..ac9a44176315d --- /dev/null +++ b/src/analysis/processing/qgsalgorithmfixgeometrymissingvertex.h @@ -0,0 +1,55 @@ +/*************************************************************************** + qgsalgorithmfixgeometrymissingvertex.h + --------------------- + begin : June 2024 + copyright : (C) 2024 by Jacky Volpes + email : jacky dot volpes at oslandia 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 QGSALGORITHMFIXGEOMETRYMISSINGVERTEX_H +#define QGSALGORITHMFIXGEOMETRYMISSINGVERTEX_H + +#define SIP_NO_FILE + +#include "qgis_sip.h" +#include "qgsprocessingalgorithm.h" + +///@cond PRIVATE + +class QgsFixGeometryMissingVertexAlgorithm : public QgsProcessingAlgorithm +{ + public: + + QgsFixGeometryMissingVertexAlgorithm() = 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; + Qgis::ProcessingAlgorithmFlags flags() const override; + QgsFixGeometryMissingVertexAlgorithm *createInstance() const override SIP_FACTORY; + + protected: + + bool prepareAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; + QVariantMap processAlgorithm( const QVariantMap ¶meters, + QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; + private: + + int mTolerance{8}; +}; + +///@endcond PRIVATE + +#endif // QGSALGORITHMFIXGEOMETRYMISSINGVERTEX_H diff --git a/src/analysis/processing/qgsnativealgorithms.cpp b/src/analysis/processing/qgsnativealgorithms.cpp index 6a8f4ae2b5d24..eae4ba9aa9702 100644 --- a/src/analysis/processing/qgsnativealgorithms.cpp +++ b/src/analysis/processing/qgsnativealgorithms.cpp @@ -47,6 +47,7 @@ #include "qgsalgorithmcheckgeometryarea.h" #include "qgsalgorithmfixgeometryarea.h" #include "qgsalgorithmfixgeometryhole.h" +#include "qgsalgorithmfixgeometrymissingvertex.h" #include "qgsalgorithmcheckgeometryhole.h" #include "qgsalgorithmcheckgeometrymissingvertex.h" #include "qgsalgorithmclip.h" @@ -584,6 +585,7 @@ void QgsNativeAlgorithms::loadAlgorithms() addAlgorithm( new QgsFixGeometryAngleAlgorithm() ); addAlgorithm( new QgsFixGeometryAreaAlgorithm() ); addAlgorithm( new QgsFixGeometryHoleAlgorithm() ); + addAlgorithm( new QgsFixGeometryMissingVertexAlgorithm() ); } ///@endcond diff --git a/src/analysis/vector/geometry_checker/qgsgeometrymissingvertexcheck.h b/src/analysis/vector/geometry_checker/qgsgeometrymissingvertexcheck.h index f2bec3da879d4..cd8f2a6e0c2bf 100644 --- a/src/analysis/vector/geometry_checker/qgsgeometrymissingvertexcheck.h +++ b/src/analysis/vector/geometry_checker/qgsgeometrymissingvertexcheck.h @@ -86,6 +86,7 @@ class ANALYSIS_EXPORT QgsGeometryMissingVertexCheckError : public QgsGeometryChe */ class ANALYSIS_EXPORT QgsGeometryMissingVertexCheck : public QgsGeometryCheck { + Q_GADGET Q_DECLARE_TR_FUNCTIONS( QgsGeometryMissingVertexCheck ) public: diff --git a/tests/src/analysis/testqgsprocessingfixgeometry.cpp b/tests/src/analysis/testqgsprocessingfixgeometry.cpp index 711b5ba83df81..42bb50b933860 100644 --- a/tests/src/analysis/testqgsprocessingfixgeometry.cpp +++ b/tests/src/analysis/testqgsprocessingfixgeometry.cpp @@ -39,6 +39,7 @@ class TestQgsProcessingFixGeometry: public QgsTest void fixAreaAlg(); void fixHoleAlg(); + void fixMissingVertexAlg(); private: const QDir mDataDir{ QDir( TEST_DATA_DIR ).absoluteFilePath( QStringLiteral( "geometry_fix" ) ) }; @@ -319,5 +320,55 @@ void TestQgsProcessingFixGeometry::fixHoleAlg() } } +void TestQgsProcessingFixGeometry::fixMissingVertexAlg() +{ + const QDir testDataDir( QDir( TEST_DATA_DIR ).absoluteFilePath( "geometry_checker" ) ); + QgsVectorLayer sourceLayer = QgsVectorLayer( testDataDir.absoluteFilePath( "missing_vertex.gpkg|layername=missing_vertex" ), QStringLiteral( "polygons" ), QStringLiteral( "ogr" ) ); + QgsVectorLayer errorsLayer = QgsVectorLayer( mDataDir.absoluteFilePath( "add_missing_vertex.gpkg|layername=errors_layer" ), QStringLiteral( "" ), QStringLiteral( "ogr" ) ); + QVERIFY( sourceLayer.isValid() ); + QVERIFY( errorsLayer.isValid() ); + const auto reportList = QStringList() + << QStringLiteral( "Add missing vertex" ) + << QStringLiteral( "Add missing vertex" ) + << QStringLiteral( "Add missing vertex" ) + << QStringLiteral( "Add missing vertex" ) + << QStringLiteral( "Add missing vertex" ); + + const std::unique_ptr< QgsProcessingAlgorithm > alg( + QgsApplication::processingRegistry()->createAlgorithmById( QStringLiteral( "native:fixgeometrymissingvertex" ) ) + ); + QVERIFY( alg != nullptr ); + + QVariantMap parameters; + parameters.insert( QStringLiteral( "INPUT" ), QVariant::fromValue( &sourceLayer ) ); + parameters.insert( QStringLiteral( "UNIQUE_ID" ), "id" ); + parameters.insert( QStringLiteral( "ERRORS" ), QVariant::fromValue( &errorsLayer ) ); + parameters.insert( QStringLiteral( "OUTPUT" ), QgsProcessing::TEMPORARY_OUTPUT ); + parameters.insert( QStringLiteral( "REPORT" ), QgsProcessing::TEMPORARY_OUTPUT ); + + bool ok = false; + QgsProcessingFeedback feedback; + std::unique_ptr< QgsProcessingContext > context = std::make_unique< QgsProcessingContext >(); + + QVariantMap results; + results = alg->run( parameters, *context, &feedback, &ok ); + QVERIFY( ok ); + + const std::unique_ptr outputLayer( qobject_cast< QgsVectorLayer * >( context->getMapLayer( results.value( QStringLiteral( "OUTPUT" ) ).toString() ) ) ); + const std::unique_ptr reportLayer( qobject_cast< QgsVectorLayer * >( context->getMapLayer( results.value( QStringLiteral( "REPORT" ) ).toString() ) ) ); + QVERIFY( reportLayer->isValid() ); + QVERIFY( outputLayer->isValid() ); + + QCOMPARE( outputLayer->featureCount(), 6 ); + QCOMPARE( reportLayer->featureCount(), reportList.count() ); + int idx = 1; + for ( QString expectedReport : reportList ) + { + const QgsFeature reportFeature = reportLayer->getFeature( idx ); + QCOMPARE( reportFeature.attribute( "report" ), expectedReport ); + idx++; + } +} + QGSTEST_MAIN( TestQgsProcessingFixGeometry ) #include "testqgsprocessingfixgeometry.moc" diff --git a/tests/testdata/geometry_fix/add_missing_vertex.gpkg b/tests/testdata/geometry_fix/add_missing_vertex.gpkg new file mode 100644 index 0000000000000000000000000000000000000000..3073f43f403de653eaa3d8181fdeff6b1e255378 GIT binary patch literal 98304 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 literal 0 HcmV?d00001