From e12621ce2adbc5473e68d7316ec9cbbc05cacbcf Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sun, 3 Dec 2017 14:54:53 +1000 Subject: [PATCH] Add API method to remove duplicate nodes from geometries Removes duplicate nodes from the geometry, wherever removing the nodes does not result in a degenerate geometry. By default, z values are not considered when detecting duplicate nodes. E.g. two nodes with the same x and y coordinate but different z values will still be considered duplicate and one will be removed. If useZValues is true, then the z values are also tested and nodes with the same x and y but different z will be maintained. Note that duplicate nodes are not tested between different parts of a multipart geometry. E.g. a multipoint geometry with overlapping points will not be changed by this method. The function will return true if nodes were removed, or false if no duplicate nodes were found. Includes unit tests and a processing algorithm which exposes this functionality. --- python/core/geometry/qgsabstractgeometry.sip | 23 +++ python/core/geometry/qgscircularstring.sip | 3 + python/core/geometry/qgscompoundcurve.sip | 2 + python/core/geometry/qgscurvepolygon.sip | 2 + python/core/geometry/qgsgeometry.sip | 23 +++ .../core/geometry/qgsgeometrycollection.sip | 3 + python/core/geometry/qgslinestring.sip | 2 + python/core/geometry/qgspoint.sip | 3 + .../testdata/custom/line_duplicate_nodes.gml | 19 ++ .../testdata/custom/line_duplicate_nodes.xsd | 23 +++ .../removed_duplicated_nodes_line.gml | 19 ++ .../removed_duplicated_nodes_line.xsd | 23 +++ .../tests/testdata/qgis_algorithm_tests.yaml | 12 ++ src/analysis/CMakeLists.txt | 1 + .../qgsalgorithmremoveduplicatenodes.cpp | 96 ++++++++++ .../qgsalgorithmremoveduplicatenodes.h | 62 +++++++ .../processing/qgsnativealgorithms.cpp | 2 + src/analysis/vector/qgsgeometrysnapper.h | 1 + src/core/geometry/qgsabstractgeometry.h | 22 +++ src/core/geometry/qgscircularstring.cpp | 51 ++++++ src/core/geometry/qgscircularstring.h | 2 + src/core/geometry/qgscompoundcurve.cpp | 29 +++ src/core/geometry/qgscompoundcurve.h | 1 + src/core/geometry/qgscurvepolygon.cpp | 31 ++++ src/core/geometry/qgscurvepolygon.h | 1 + src/core/geometry/qgsgeometry.cpp | 9 + src/core/geometry/qgsgeometry.h | 22 +++ src/core/geometry/qgsgeometrycollection.cpp | 10 + src/core/geometry/qgsgeometrycollection.h | 3 +- src/core/geometry/qgslinestring.cpp | 40 ++++ src/core/geometry/qgslinestring.h | 1 + src/core/geometry/qgspoint.cpp | 5 + src/core/geometry/qgspoint.h | 3 +- tests/src/core/testqgsgeometry.cpp | 173 ++++++++++++++++++ 34 files changed, 720 insertions(+), 2 deletions(-) create mode 100644 python/plugins/processing/tests/testdata/custom/line_duplicate_nodes.gml create mode 100644 python/plugins/processing/tests/testdata/custom/line_duplicate_nodes.xsd create mode 100644 python/plugins/processing/tests/testdata/expected/removed_duplicated_nodes_line.gml create mode 100644 python/plugins/processing/tests/testdata/expected/removed_duplicated_nodes_line.xsd create mode 100644 src/analysis/processing/qgsalgorithmremoveduplicatenodes.cpp create mode 100644 src/analysis/processing/qgsalgorithmremoveduplicatenodes.h diff --git a/python/core/geometry/qgsabstractgeometry.sip b/python/core/geometry/qgsabstractgeometry.sip index ea9c2de8e4b..0d2878dc196 100644 --- a/python/core/geometry/qgsabstractgeometry.sip +++ b/python/core/geometry/qgsabstractgeometry.sip @@ -436,6 +436,29 @@ Returns the centroid of the geometry :rtype: QgsAbstractGeometry %End + virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ) = 0; +%Docstring + Removes duplicate nodes from the geometry, wherever removing the nodes does not result in a + degenerate geometry. + + The ``epsilon`` parameter specifies the tolerance for coordinates when determining that + vertices are identical. + + By default, z values are not considered when detecting duplicate nodes. E.g. two nodes + with the same x and y coordinate but different z values will still be considered + duplicate and one will be removed. If ``useZValues`` is true, then the z values are + also tested and nodes with the same x and y but different z will be maintained. + + Note that duplicate nodes are not tested between different parts of a multipart geometry. E.g. + a multipoint geometry with overlapping points will not be changed by this method. + + The function will return true if nodes were removed, or false if no duplicate nodes + were found. + +.. versionadded:: 3.0 + :rtype: bool +%End + virtual double vertexAngle( QgsVertexId vertex ) const = 0; %Docstring Returns approximate angle at a vertex. This is usually the average angle between adjacent diff --git a/python/core/geometry/qgscircularstring.sip b/python/core/geometry/qgscircularstring.sip index 7095f9b02f4..6260c75fb39 100644 --- a/python/core/geometry/qgscircularstring.sip +++ b/python/core/geometry/qgscircularstring.sip @@ -83,6 +83,9 @@ class QgsCircularString: QgsCurve virtual QgsCircularString *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const /Factory/; + virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ); + + virtual void draw( QPainter &p ) const; virtual void transform( const QgsCoordinateTransform &ct, QgsCoordinateTransform::TransformDirection d = QgsCoordinateTransform::ForwardTransform, diff --git a/python/core/geometry/qgscompoundcurve.sip b/python/core/geometry/qgscompoundcurve.sip index e41eb12f989..cbca2ba881b 100644 --- a/python/core/geometry/qgscompoundcurve.sip +++ b/python/core/geometry/qgscompoundcurve.sip @@ -79,6 +79,8 @@ class QgsCompoundCurve: QgsCurve virtual QgsCompoundCurve *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const /Factory/; + virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ); + int nCurves() const; %Docstring diff --git a/python/core/geometry/qgscurvepolygon.sip b/python/core/geometry/qgscurvepolygon.sip index a7438896ed2..3489a0b79dc 100644 --- a/python/core/geometry/qgscurvepolygon.sip +++ b/python/core/geometry/qgscurvepolygon.sip @@ -67,6 +67,8 @@ class QgsCurvePolygon: QgsSurface virtual QgsCurvePolygon *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const /Factory/; + virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ); + int numInteriorRings() const; %Docstring diff --git a/python/core/geometry/qgsgeometry.sip b/python/core/geometry/qgsgeometry.sip index 7d977c59d80..f9a9d91e53b 100644 --- a/python/core/geometry/qgsgeometry.sip +++ b/python/core/geometry/qgsgeometry.sip @@ -688,6 +688,29 @@ Returns true if WKB of the geometry is of WKBMulti* type :rtype: QgsGeometry %End + bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ); +%Docstring + Removes duplicate nodes from the geometry, wherever removing the nodes does not result in a + degenerate geometry. + + The ``epsilon`` parameter specifies the tolerance for coordinates when determining that + vertices are identical. + + By default, z values are not considered when detecting duplicate nodes. E.g. two nodes + with the same x and y coordinate but different z values will still be considered + duplicate and one will be removed. If ``useZValues`` is true, then the z values are + also tested and nodes with the same x and y but different z will be maintained. + + Note that duplicate nodes are not tested between different parts of a multipart geometry. E.g. + a multipoint geometry with overlapping points will not be changed by this method. + + The function will return true if nodes were removed, or false if no duplicate nodes + were found. + +.. versionadded:: 3.0 + :rtype: bool +%End + bool intersects( const QgsRectangle &r ) const; %Docstring Tests for intersection with a rectangle (uses GEOS) diff --git a/python/core/geometry/qgsgeometrycollection.sip b/python/core/geometry/qgsgeometrycollection.sip index eb929e2331c..95adfdc3209 100644 --- a/python/core/geometry/qgsgeometrycollection.sip +++ b/python/core/geometry/qgsgeometrycollection.sip @@ -52,6 +52,9 @@ class QgsGeometryCollection: QgsAbstractGeometry virtual void clear(); virtual QgsGeometryCollection *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const /Factory/; + + virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ); + virtual QgsAbstractGeometry *boundary() const /Factory/; virtual void adjacentVertices( QgsVertexId vertex, QgsVertexId &previousVertex /Out/, QgsVertexId &nextVertex /Out/ ) const; diff --git a/python/core/geometry/qgslinestring.sip b/python/core/geometry/qgslinestring.sip index aecfb9a9eb4..149f69bace9 100644 --- a/python/core/geometry/qgslinestring.sip +++ b/python/core/geometry/qgslinestring.sip @@ -179,6 +179,8 @@ Closes the line string by appending the first point to the end of the line, if i virtual QgsLineString *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const /Factory/; + virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ); + virtual bool fromWkb( QgsConstWkbPtr &wkb ); diff --git a/python/core/geometry/qgspoint.sip b/python/core/geometry/qgspoint.sip index da2d1d4f333..d5f355b76d7 100644 --- a/python/core/geometry/qgspoint.sip +++ b/python/core/geometry/qgspoint.sip @@ -339,6 +339,9 @@ class QgsPoint: QgsAbstractGeometry virtual QgsPoint *clone() const /Factory/; virtual QgsPoint *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const /Factory/; + + virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ); + virtual void clear(); virtual bool fromWkb( QgsConstWkbPtr &wkb ); diff --git a/python/plugins/processing/tests/testdata/custom/line_duplicate_nodes.gml b/python/plugins/processing/tests/testdata/custom/line_duplicate_nodes.gml new file mode 100644 index 00000000000..392ab31d15f --- /dev/null +++ b/python/plugins/processing/tests/testdata/custom/line_duplicate_nodes.gml @@ -0,0 +1,19 @@ + + + + + 20 + 33 + + + + + + 2,0 2,2 3,2 3,3 3,3 + + + diff --git a/python/plugins/processing/tests/testdata/custom/line_duplicate_nodes.xsd b/python/plugins/processing/tests/testdata/custom/line_duplicate_nodes.xsd new file mode 100644 index 00000000000..edd56fb3fc6 --- /dev/null +++ b/python/plugins/processing/tests/testdata/custom/line_duplicate_nodes.xsd @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/expected/removed_duplicated_nodes_line.gml b/python/plugins/processing/tests/testdata/expected/removed_duplicated_nodes_line.gml new file mode 100644 index 00000000000..7f2082edec2 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/removed_duplicated_nodes_line.gml @@ -0,0 +1,19 @@ + + + + + 20 + 33 + + + + + + 2,0 2,2 3,2 3,3 + + + diff --git a/python/plugins/processing/tests/testdata/expected/removed_duplicated_nodes_line.xsd b/python/plugins/processing/tests/testdata/expected/removed_duplicated_nodes_line.xsd new file mode 100644 index 00000000000..41750902364 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/removed_duplicated_nodes_line.xsd @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml index f984036a829..6a7274172f1 100755 --- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml +++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml @@ -4582,3 +4582,15 @@ tests: name: expected/difference.gml type: vector + - algorithm: native:removeduplicatenodes + name: Remove duplicate nodes lines + params: + INPUT: + name: custom/line_duplicate_nodes.gml + type: vector + TOLERANCE: 1.0e-06 + USE_Z_VALUE: false + results: + OUTPUT: + name: expected/removed_duplicated_nodes_line.gml + type: vector diff --git a/src/analysis/CMakeLists.txt b/src/analysis/CMakeLists.txt index cc85d137cec..94795cd9ad1 100644 --- a/src/analysis/CMakeLists.txt +++ b/src/analysis/CMakeLists.txt @@ -51,6 +51,7 @@ SET(QGIS_ANALYSIS_SRCS processing/qgsalgorithmpackage.cpp processing/qgsalgorithmpromotetomultipart.cpp processing/qgsalgorithmrasterlayeruniquevalues.cpp + processing/qgsalgorithmremoveduplicatenodes processing/qgsalgorithmremovenullgeometry.cpp processing/qgsalgorithmrenamelayer.cpp processing/qgsalgorithmsaveselectedfeatures.cpp diff --git a/src/analysis/processing/qgsalgorithmremoveduplicatenodes.cpp b/src/analysis/processing/qgsalgorithmremoveduplicatenodes.cpp new file mode 100644 index 00000000000..057adcbcad0 --- /dev/null +++ b/src/analysis/processing/qgsalgorithmremoveduplicatenodes.cpp @@ -0,0 +1,96 @@ +/*************************************************************************** + qgsalgorithmremoveduplicatenodes.cpp + --------------------- + begin : November 2017 + copyright : (C) 2017 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 "qgsalgorithmremoveduplicatenodes.h" + +///@cond PRIVATE + +QString QgsAlgorithmRemoveDuplicateNodes::name() const +{ + return QStringLiteral( "removeduplicatenodes" ); +} + +QString QgsAlgorithmRemoveDuplicateNodes::displayName() const +{ + return QObject::tr( "Remove duplicate nodes" ); +} + +QStringList QgsAlgorithmRemoveDuplicateNodes::tags() const +{ + return QObject::tr( "points,valid,overlapping" ).split( ',' ); +} + +QString QgsAlgorithmRemoveDuplicateNodes::group() const +{ + return QObject::tr( "Vector geometry" ); +} + +QString QgsAlgorithmRemoveDuplicateNodes::outputName() const +{ + return QObject::tr( "Cleaned" ); +} + +QString QgsAlgorithmRemoveDuplicateNodes::shortHelpString() const +{ + return QObject::tr( "This algorithm removes duplicate nodes from features, wherever removing the nodes does " + "not result in a degenerate geometry.\n\n" + "The tolerance parameter specifies the tolerance for coordinates when determining whether " + "vertices are identical.\n\n" + "By default, z values are not considered when detecting duplicate nodes. E.g. two nodes " + "with the same x and y coordinate but different z values will still be considered " + "duplicate and one will be removed. If the Use Z Value parameter is true, then the z values are " + "also tested and nodes with the same x and y but different z will be maintained.\n\n" + "Note that duplicate nodes are not tested between different parts of a multipart geometry. E.g. " + "a multipoint geometry with overlapping points will not be changed by this method." ); +} + +QgsAlgorithmRemoveDuplicateNodes *QgsAlgorithmRemoveDuplicateNodes::createInstance() const +{ + return new QgsAlgorithmRemoveDuplicateNodes(); +} + +void QgsAlgorithmRemoveDuplicateNodes::initParameters( const QVariantMap & ) +{ + addParameter( new QgsProcessingParameterNumber( QStringLiteral( "TOLERANCE" ), + QObject::tr( "Tolerance" ), QgsProcessingParameterNumber::Double, + 0.000001, false, 0, 10000000.0 ) ); + addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "USE_Z_VALUE" ), + QObject::tr( "Use Z Value" ), false ) ); +} + +bool QgsAlgorithmRemoveDuplicateNodes::prepareAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback * ) +{ + mTolerance = parameterAsDouble( parameters, QStringLiteral( "TOLERANCE" ), context ); + mUseZValues = parameterAsBool( parameters, QStringLiteral( "USE_Z_VALUE" ), context ); + return true; +} + +QgsFeature QgsAlgorithmRemoveDuplicateNodes::processFeature( const QgsFeature &feature, QgsProcessingContext &, QgsProcessingFeedback * ) +{ + QgsFeature f = feature; + if ( f.hasGeometry() ) + { + QgsGeometry geometry = f.geometry(); + geometry.removeDuplicateNodes( mTolerance, mUseZValues ); + f.setGeometry( geometry ); + } + return f; +} + +///@endcond + + diff --git a/src/analysis/processing/qgsalgorithmremoveduplicatenodes.h b/src/analysis/processing/qgsalgorithmremoveduplicatenodes.h new file mode 100644 index 00000000000..6b2ddc6e2ed --- /dev/null +++ b/src/analysis/processing/qgsalgorithmremoveduplicatenodes.h @@ -0,0 +1,62 @@ +/*************************************************************************** + qgsalgorithmremoveduplicatenodes.h + --------------------- + begin : November 2017 + copyright : (C) 2017 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 QGSALGORITHMREMOVEDUPLICATENODES_H +#define QGSALGORITHMREMOVEDUPLICATENODES_H + +#define SIP_NO_FILE + +#include "qgis.h" +#include "qgsprocessingalgorithm.h" + +///@cond PRIVATE + +/** + * Native remove duplicate nodes algorithm. + */ +class QgsAlgorithmRemoveDuplicateNodes : public QgsProcessingFeatureBasedAlgorithm +{ + + public: + + QgsAlgorithmRemoveDuplicateNodes() = default; + QString name() const override; + QString displayName() const override; + virtual QStringList tags() const override; + QString group() const override; + QString shortHelpString() const override; + QgsAlgorithmRemoveDuplicateNodes *createInstance() const override SIP_FACTORY; + void initParameters( const QVariantMap &configuration = QVariantMap() ) override; + + protected: + QString outputName() const override; + bool prepareAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; + QgsFeature processFeature( const QgsFeature &feature, QgsProcessingContext &, QgsProcessingFeedback *feedback ) override; + + private: + + double mTolerance = 1.0; + bool mUseZValues = false; + +}; + + +///@endcond PRIVATE + +#endif // QGSALGORITHMSIMPLIFY_H + + diff --git a/src/analysis/processing/qgsnativealgorithms.cpp b/src/analysis/processing/qgsnativealgorithms.cpp index 3df827502eb..74e1f4d38ce 100644 --- a/src/analysis/processing/qgsnativealgorithms.cpp +++ b/src/analysis/processing/qgsnativealgorithms.cpp @@ -48,6 +48,7 @@ #include "qgsalgorithmpackage.h" #include "qgsalgorithmpromotetomultipart.h" #include "qgsalgorithmrasterlayeruniquevalues.h" +#include "qgsalgorithmremoveduplicatenodes.h" #include "qgsalgorithmremovenullgeometry.h" #include "qgsalgorithmrenamelayer.h" #include "qgsalgorithmsaveselectedfeatures.h" @@ -128,6 +129,7 @@ void QgsNativeAlgorithms::loadAlgorithms() addAlgorithm( new QgsPackageAlgorithm() ); addAlgorithm( new QgsPromoteToMultipartAlgorithm() ); addAlgorithm( new QgsRasterLayerUniqueValuesReportAlgorithm() ); + addAlgorithm( new QgsAlgorithmRemoveDuplicateNodes() ); addAlgorithm( new QgsRemoveNullGeometryAlgorithm() ); addAlgorithm( new QgsRenameLayerAlgorithm() ); addAlgorithm( new QgsSaveSelectedFeatures() ); diff --git a/src/analysis/vector/qgsgeometrysnapper.h b/src/analysis/vector/qgsgeometrysnapper.h index 750261a1caa..cba97513748 100644 --- a/src/analysis/vector/qgsgeometrysnapper.h +++ b/src/analysis/vector/qgsgeometrysnapper.h @@ -159,6 +159,7 @@ class ANALYSIS_EXPORT QgsInternalGeometrySnapper QgsGeometrySnapper::SnapMode mMode = QgsGeometrySnapper::PreferNodes; QgsSpatialIndex mProcessedIndex; QgsGeometryMap mProcessedGeometries; + }; #ifndef SIP_RUN diff --git a/src/core/geometry/qgsabstractgeometry.h b/src/core/geometry/qgsabstractgeometry.h index e5bed32161d..95d43934d40 100644 --- a/src/core/geometry/qgsabstractgeometry.h +++ b/src/core/geometry/qgsabstractgeometry.h @@ -443,6 +443,28 @@ class CORE_EXPORT QgsAbstractGeometry */ virtual QgsAbstractGeometry *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const = 0 SIP_FACTORY; + /** + * Removes duplicate nodes from the geometry, wherever removing the nodes does not result in a + * degenerate geometry. + * + * The \a epsilon parameter specifies the tolerance for coordinates when determining that + * vertices are identical. + * + * By default, z values are not considered when detecting duplicate nodes. E.g. two nodes + * with the same x and y coordinate but different z values will still be considered + * duplicate and one will be removed. If \a useZValues is true, then the z values are + * also tested and nodes with the same x and y but different z will be maintained. + * + * Note that duplicate nodes are not tested between different parts of a multipart geometry. E.g. + * a multipoint geometry with overlapping points will not be changed by this method. + * + * The function will return true if nodes were removed, or false if no duplicate nodes + * were found. + * + * \since QGIS 3.0 + */ + virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ) = 0; + /** * Returns approximate angle at a vertex. This is usually the average angle between adjacent * segments, and can be pictured as the orientation of a line following the curvature of the diff --git a/src/core/geometry/qgscircularstring.cpp b/src/core/geometry/qgscircularstring.cpp index a5a46784395..54a95da82fc 100644 --- a/src/core/geometry/qgscircularstring.cpp +++ b/src/core/geometry/qgscircularstring.cpp @@ -403,6 +403,57 @@ QgsCircularString *QgsCircularString::snappedToGrid( double hSpacing, double vSp return nullptr; } +bool QgsCircularString::removeDuplicateNodes( double epsilon, bool useZValues ) +{ + if ( mX.count() <= 3 ) + return false; // don't create degenerate lines + bool result = false; + double prevX = mX.at( 0 ); + double prevY = mY.at( 0 ); + bool hasZ = is3D(); + bool useZ = hasZ && useZValues; + double prevZ = useZ ? mZ.at( 0 ) : 0; + int i = 1; + int remaining = mX.count(); + // we have to consider points in pairs, since a segment can validly have the same start and + // end if it has a different curve point + while ( i + 1 < remaining ) + { + double currentCurveX = mX.at( i ); + double currentCurveY = mY.at( i ); + double currentX = mX.at( i + 1 ); + double currentY = mY.at( i + 1 ); + double currentZ = useZ ? mZ.at( i + 1 ) : 0; + if ( qgsDoubleNear( currentCurveX, prevX, epsilon ) && + qgsDoubleNear( currentCurveY, prevY, epsilon ) && + qgsDoubleNear( currentX, prevX, epsilon ) && + qgsDoubleNear( currentY, prevY, epsilon ) && + ( !useZ || qgsDoubleNear( currentZ, prevZ, epsilon ) ) ) + { + result = true; + // remove point + mX.removeAt( i ); + mX.removeAt( i ); + mY.removeAt( i ); + mY.removeAt( i ); + if ( hasZ ) + { + mZ.removeAt( i ); + mZ.removeAt( i ); + } + remaining -= 2; + } + else + { + prevX = currentX; + prevY = currentY; + prevZ = currentZ; + i += 2; + } + } + return result; +} + int QgsCircularString::numPoints() const { return std::min( mX.size(), mY.size() ); diff --git a/src/core/geometry/qgscircularstring.h b/src/core/geometry/qgscircularstring.h index 36500559d1a..428b698e982 100644 --- a/src/core/geometry/qgscircularstring.h +++ b/src/core/geometry/qgscircularstring.h @@ -73,6 +73,8 @@ class CORE_EXPORT QgsCircularString: public QgsCurve QgsPoint endPoint() const override; QgsLineString *curveToLine( double tolerance = M_PI_2 / 90, SegmentationToleranceType toleranceType = MaximumAngle ) const override SIP_FACTORY; QgsCircularString *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const override SIP_FACTORY; + bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ) override; + void draw( QPainter &p ) const override; void transform( const QgsCoordinateTransform &ct, QgsCoordinateTransform::TransformDirection d = QgsCoordinateTransform::ForwardTransform, bool transformZ = false ) override; diff --git a/src/core/geometry/qgscompoundcurve.cpp b/src/core/geometry/qgscompoundcurve.cpp index 52e221ed3cf..4d8da63dfcb 100644 --- a/src/core/geometry/qgscompoundcurve.cpp +++ b/src/core/geometry/qgscompoundcurve.cpp @@ -408,6 +408,35 @@ QgsCompoundCurve *QgsCompoundCurve::snappedToGrid( double hSpacing, double vSpac return result.release(); } +bool QgsCompoundCurve::removeDuplicateNodes( double epsilon, bool useZValues ) +{ + bool result = false; + const QVector< QgsCurve * > curves = mCurves; + int i = 0; + QgsPoint lastEnd; + for ( QgsCurve *curve : curves ) + { + result = result || curve->removeDuplicateNodes( epsilon, useZValues ); + if ( curve->numPoints() == 0 || qgsDoubleNear( curve->length(), 0.0, epsilon ) ) + { + // empty curve, remove it + delete mCurves.takeAt( i ); + result = true; + } + else + { + // ensure this line starts exactly where previous line ended + if ( i > 0 ) + { + curve->moveVertex( QgsVertexId( -1, -1, 0 ), lastEnd ); + } + lastEnd = curve->vertexAt( QgsVertexId( -1, -1, curve->numPoints() - 1 ) ); + } + i++; + } + return result; +} + const QgsCurve *QgsCompoundCurve::curveAt( int i ) const { if ( i < 0 || i >= mCurves.size() ) diff --git a/src/core/geometry/qgscompoundcurve.h b/src/core/geometry/qgscompoundcurve.h index 7f232d5ae35..2ea1ea94106 100644 --- a/src/core/geometry/qgscompoundcurve.h +++ b/src/core/geometry/qgscompoundcurve.h @@ -69,6 +69,7 @@ class CORE_EXPORT QgsCompoundCurve: public QgsCurve QgsLineString *curveToLine( double tolerance = M_PI_2 / 90, SegmentationToleranceType toleranceType = MaximumAngle ) const override SIP_FACTORY; QgsCompoundCurve *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const override SIP_FACTORY; + bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ) override; /** * Returns the number of curves in the geometry. diff --git a/src/core/geometry/qgscurvepolygon.cpp b/src/core/geometry/qgscurvepolygon.cpp index a8222adf043..bd76e5b463b 100644 --- a/src/core/geometry/qgscurvepolygon.cpp +++ b/src/core/geometry/qgscurvepolygon.cpp @@ -534,6 +534,37 @@ QgsCurvePolygon *QgsCurvePolygon::snappedToGrid( double hSpacing, double vSpacin } +bool QgsCurvePolygon::removeDuplicateNodes( double epsilon, bool useZValues ) +{ + bool result = false; + auto cleanRing = [this, &result, epsilon, useZValues ]( QgsCurve * ring )->bool + { + if ( ring->numPoints() <= 4 ) + return false; + + if ( ring->removeDuplicateNodes( epsilon, useZValues ) ) + { + QgsPoint startPoint; + QgsVertexId::VertexType type; + ring->pointAt( 0, startPoint, type ); + // ensure ring is properly closed - if we removed the final node, it may no longer be properly closed + ring->moveVertex( QgsVertexId( -1, -1, ring->numPoints() - 1 ), startPoint ); + return true; + } + + return false; + }; + if ( mExteriorRing ) + { + result = cleanRing( mExteriorRing.get() ); + } + for ( QgsCurve *ring : qgis::as_const( mInteriorRings ) ) + { + result = result || cleanRing( ring ); + } + return result; +} + QgsPolygon *QgsCurvePolygon::toPolygon( double tolerance, SegmentationToleranceType toleranceType ) const { std::unique_ptr< QgsPolygon > poly( new QgsPolygon() ); diff --git a/src/core/geometry/qgscurvepolygon.h b/src/core/geometry/qgscurvepolygon.h index e4562b57dc8..005ec1c39f4 100644 --- a/src/core/geometry/qgscurvepolygon.h +++ b/src/core/geometry/qgscurvepolygon.h @@ -63,6 +63,7 @@ class CORE_EXPORT QgsCurvePolygon: public QgsSurface QgsPolygon *surfaceToPolygon() const override SIP_FACTORY; QgsAbstractGeometry *boundary() const override SIP_FACTORY; QgsCurvePolygon *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const override SIP_FACTORY; + bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ) override; //curve polygon interface int numInteriorRings() const; diff --git a/src/core/geometry/qgsgeometry.cpp b/src/core/geometry/qgsgeometry.cpp index 6897fe713ab..14010b78e11 100644 --- a/src/core/geometry/qgsgeometry.cpp +++ b/src/core/geometry/qgsgeometry.cpp @@ -1076,6 +1076,15 @@ QgsGeometry QgsGeometry::snappedToGrid( double hSpacing, double vSpacing, double return QgsGeometry( d->geometry->snappedToGrid( hSpacing, vSpacing, dSpacing, mSpacing ) ); } +bool QgsGeometry::removeDuplicateNodes( double epsilon, bool useZValues ) +{ + if ( !d->geometry ) + return false; + + detach(); + return d->geometry->removeDuplicateNodes( epsilon, useZValues ); +} + bool QgsGeometry::intersects( const QgsRectangle &r ) const { QgsGeometry g = fromRect( r ); diff --git a/src/core/geometry/qgsgeometry.h b/src/core/geometry/qgsgeometry.h index 29a3de6fa7a..e1ca2a1ac5f 100644 --- a/src/core/geometry/qgsgeometry.h +++ b/src/core/geometry/qgsgeometry.h @@ -747,6 +747,28 @@ class CORE_EXPORT QgsGeometry */ QgsGeometry snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const; + /** + * Removes duplicate nodes from the geometry, wherever removing the nodes does not result in a + * degenerate geometry. + * + * The \a epsilon parameter specifies the tolerance for coordinates when determining that + * vertices are identical. + * + * By default, z values are not considered when detecting duplicate nodes. E.g. two nodes + * with the same x and y coordinate but different z values will still be considered + * duplicate and one will be removed. If \a useZValues is true, then the z values are + * also tested and nodes with the same x and y but different z will be maintained. + * + * Note that duplicate nodes are not tested between different parts of a multipart geometry. E.g. + * a multipoint geometry with overlapping points will not be changed by this method. + * + * The function will return true if nodes were removed, or false if no duplicate nodes + * were found. + * + * \since QGIS 3.0 + */ + bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ); + //! Tests for intersection with a rectangle (uses GEOS) bool intersects( const QgsRectangle &r ) const; diff --git a/src/core/geometry/qgsgeometrycollection.cpp b/src/core/geometry/qgsgeometrycollection.cpp index 51548f1268d..08a76110969 100644 --- a/src/core/geometry/qgsgeometrycollection.cpp +++ b/src/core/geometry/qgsgeometrycollection.cpp @@ -102,6 +102,16 @@ QgsGeometryCollection *QgsGeometryCollection::snappedToGrid( double hSpacing, do return result.release(); } +bool QgsGeometryCollection::removeDuplicateNodes( double epsilon, bool useZValues ) +{ + bool result = false; + for ( QgsAbstractGeometry *geom : qgis::as_const( mGeometries ) ) + { + result = result || geom->removeDuplicateNodes( epsilon, useZValues ); + } + return result; +} + QgsAbstractGeometry *QgsGeometryCollection::boundary() const { return nullptr; diff --git a/src/core/geometry/qgsgeometrycollection.h b/src/core/geometry/qgsgeometrycollection.h index fe52d9bacdc..3b05f8240c1 100644 --- a/src/core/geometry/qgsgeometrycollection.h +++ b/src/core/geometry/qgsgeometrycollection.h @@ -64,7 +64,8 @@ class CORE_EXPORT QgsGeometryCollection: public QgsAbstractGeometry int dimension() const override; QString geometryType() const override; void clear() override; - virtual QgsGeometryCollection *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const override SIP_FACTORY; + QgsGeometryCollection *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const override SIP_FACTORY; + bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ) override; QgsAbstractGeometry *boundary() const override SIP_FACTORY; void adjacentVertices( QgsVertexId vertex, QgsVertexId &previousVertex SIP_OUT, QgsVertexId &nextVertex SIP_OUT ) const override; int vertexNumberFromVertexId( QgsVertexId id ) const override; diff --git a/src/core/geometry/qgslinestring.cpp b/src/core/geometry/qgslinestring.cpp index 98eb6fd4cbf..8ee48175c8b 100644 --- a/src/core/geometry/qgslinestring.cpp +++ b/src/core/geometry/qgslinestring.cpp @@ -202,6 +202,46 @@ QgsLineString *QgsLineString::snappedToGrid( double hSpacing, double vSpacing, d return nullptr; } +bool QgsLineString::removeDuplicateNodes( double epsilon, bool useZValues ) +{ + if ( mX.count() <= 2 ) + return false; // don't create degenerate lines + bool result = false; + double prevX = mX.at( 0 ); + double prevY = mY.at( 0 ); + bool hasZ = is3D(); + bool useZ = hasZ && useZValues; + double prevZ = useZ ? mZ.at( 0 ) : 0; + int i = 1; + int remaining = mX.count(); + while ( i < remaining ) + { + double currentX = mX.at( i ); + double currentY = mY.at( i ); + double currentZ = useZ ? mZ.at( i ) : 0; + if ( qgsDoubleNear( currentX, prevX, epsilon ) && + qgsDoubleNear( currentY, prevY, epsilon ) && + ( !useZ || qgsDoubleNear( currentZ, prevZ, epsilon ) ) ) + { + result = true; + // remove point + mX.removeAt( i ); + mY.removeAt( i ); + if ( hasZ ) + mZ.removeAt( i ); + remaining--; + } + else + { + prevX = currentX; + prevY = currentY; + prevZ = currentZ; + i++; + } + } + return result; +} + bool QgsLineString::fromWkb( QgsConstWkbPtr &wkbPtr ) { if ( !wkbPtr ) diff --git a/src/core/geometry/qgslinestring.h b/src/core/geometry/qgslinestring.h index ee43b8ca4e7..bddcc6ec0d5 100644 --- a/src/core/geometry/qgslinestring.h +++ b/src/core/geometry/qgslinestring.h @@ -180,6 +180,7 @@ class CORE_EXPORT QgsLineString: public QgsCurve void clear() override; bool isEmpty() const override; QgsLineString *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const override SIP_FACTORY; + bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ) override; bool fromWkb( QgsConstWkbPtr &wkb ) override; bool fromWkt( const QString &wkt ) override; diff --git a/src/core/geometry/qgspoint.cpp b/src/core/geometry/qgspoint.cpp index ff8296206ce..d55774528c6 100644 --- a/src/core/geometry/qgspoint.cpp +++ b/src/core/geometry/qgspoint.cpp @@ -137,6 +137,11 @@ QgsPoint *QgsPoint::snappedToGrid( double hSpacing, double vSpacing, double dSpa return new QgsPoint( mWkbType, x, y, z, m ); } +bool QgsPoint::removeDuplicateNodes( double, bool ) +{ + return false; +} + bool QgsPoint::fromWkb( QgsConstWkbPtr &wkbPtr ) { QgsWkbTypes::Type type = wkbPtr.readHeader(); diff --git a/src/core/geometry/qgspoint.h b/src/core/geometry/qgspoint.h index cc896e2855c..27e8d0b6e24 100644 --- a/src/core/geometry/qgspoint.h +++ b/src/core/geometry/qgspoint.h @@ -391,7 +391,8 @@ class CORE_EXPORT QgsPoint: public QgsAbstractGeometry QString geometryType() const override; int dimension() const override; QgsPoint *clone() const override SIP_FACTORY; - virtual QgsPoint *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const override SIP_FACTORY; + QgsPoint *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const override SIP_FACTORY; + bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ) override; void clear() override; bool fromWkb( QgsConstWkbPtr &wkb ) override; bool fromWkt( const QString &wkt ) override; diff --git a/tests/src/core/testqgsgeometry.cpp b/tests/src/core/testqgsgeometry.cpp index 83a4cfc685b..d6e31d18881 100644 --- a/tests/src/core/testqgsgeometry.cpp +++ b/tests/src/core/testqgsgeometry.cpp @@ -1053,6 +1053,12 @@ void TestQgsGeometry::point() QCOMPARE( QgsPoint( 1, 2 ).segmentLength( QgsVertexId( -1, 0, 1 ) ), 0.0 ); QCOMPARE( QgsPoint( 1, 2 ).segmentLength( QgsVertexId( -1, 0, -1 ) ), 0.0 ); QCOMPARE( QgsPoint( 1, 2 ).segmentLength( QgsVertexId( 0, 0, 0 ) ), 0.0 ); + + // remove duplicate points + QgsPoint p = QgsPoint( 1, 2 ); + QVERIFY( !p.removeDuplicateNodes() ); + QCOMPARE( p.x(), 1.0 ); + QCOMPARE( p.y(), 2.0 ); } void TestQgsGeometry::circularString() @@ -2364,6 +2370,48 @@ void TestQgsGeometry::circularString() QCOMPARE( curveLine2.segmentLength( QgsVertexId( 0, 0, 1 ) ), 0.0 ); QGSCOMPARENEAR( curveLine2.segmentLength( QgsVertexId( 0, 0, 2 ) ), 31.4159, 0.001 ); QCOMPARE( curveLine2.segmentLength( QgsVertexId( 0, 0, 3 ) ), 0.0 ); + + //removeDuplicateNodes + QgsCircularString nodeLine; + QVERIFY( !nodeLine.removeDuplicateNodes() ); + nodeLine.setPoints( QgsPointSequence() << QgsPoint( 11, 2 ) << QgsPoint( 11, 12 ) << QgsPoint( 111, 12 ) ); + QVERIFY( !nodeLine.removeDuplicateNodes() ); + QCOMPARE( nodeLine.asWkt(), QStringLiteral( "CircularString (11 2, 11 12, 111 12)" ) ); + nodeLine.setPoints( QgsPointSequence() << QgsPoint( 11, 2 ) << QgsPoint( 11, 12 ) << QgsPoint( 11, 2 ) ); + QVERIFY( !nodeLine.removeDuplicateNodes() ); + QCOMPARE( nodeLine.asWkt(), QStringLiteral( "CircularString (11 2, 11 12, 11 2)" ) ); + nodeLine.setPoints( QgsPointSequence() << QgsPoint( 11, 2 ) << QgsPoint( 10, 3 ) << QgsPoint( 11.01, 1.99 ) << QgsPoint( 9, 3 ) + << QgsPoint( 11, 2 ) ); + QVERIFY( !nodeLine.removeDuplicateNodes( 0.02 ) ); + QCOMPARE( nodeLine.asWkt( 2 ), QStringLiteral( "CircularString (11 2, 10 3, 11.01 1.99, 9 3, 11 2)" ) ); + nodeLine.setPoints( QgsPointSequence() << QgsPoint( 11, 2 ) << QgsPoint( 11.01, 1.99 ) << QgsPoint( 11.02, 2.01 ) + << QgsPoint( 11, 12 ) << QgsPoint( 111, 12 ) << QgsPoint( 111.01, 11.99 ) ); + QVERIFY( !nodeLine.removeDuplicateNodes() ); + QCOMPARE( nodeLine.asWkt( 2 ), QStringLiteral( "CircularString (11 2, 11.01 1.99, 11.02 2.01, 11 12, 111 12, 111.01 11.99)" ) ); + QVERIFY( nodeLine.removeDuplicateNodes( 0.02 ) ); + QCOMPARE( nodeLine.asWkt( 2 ), QStringLiteral( "CircularString (11 2, 11 12, 111 12, 111.01 11.99)" ) ); + + // don't create degenerate lines + nodeLine.setPoints( QgsPointSequence() << QgsPoint( 11, 2 ) ); + QVERIFY( !nodeLine.removeDuplicateNodes( 0.02 ) ); + QCOMPARE( nodeLine.asWkt( 2 ), QStringLiteral( "CircularString (11 2)" ) ); + nodeLine.setPoints( QgsPointSequence() << QgsPoint( 11, 2 ) << QgsPoint( 11.01, 1.99 ) ); + QVERIFY( !nodeLine.removeDuplicateNodes( 0.02 ) ); + QCOMPARE( nodeLine.asWkt( 2 ), QStringLiteral( "CircularString (11 2, 11.01 1.99)" ) ); + nodeLine.setPoints( QgsPointSequence() << QgsPoint( 11, 2 ) << QgsPoint( 11.01, 1.99 ) << QgsPoint( 11, 2 ) ); + QVERIFY( !nodeLine.removeDuplicateNodes( 0.02 ) ); + QCOMPARE( nodeLine.asWkt( 2 ), QStringLiteral( "CircularString (11 2, 11.01 1.99, 11 2)" ) ); + + // with z + nodeLine.setPoints( QgsPointSequence() << QgsPoint( 11, 2, 1 ) << QgsPoint( 11.01, 1.99, 2 ) << QgsPoint( 11.02, 2.01, 3 ) + << QgsPoint( 11, 12, 4 ) << QgsPoint( 111, 12, 5 ) ); + QVERIFY( nodeLine.removeDuplicateNodes( 0.02 ) ); + QCOMPARE( nodeLine.asWkt( 2 ), QStringLiteral( "CircularStringZ (11 2 1, 11 12 4, 111 12 5)" ) ); + nodeLine.setPoints( QgsPointSequence() << QgsPoint( 11, 2, 1 ) << QgsPoint( 11.01, 1.99, 2 ) << QgsPoint( 11.02, 2.01, 3 ) + << QgsPoint( 11, 12, 4 ) << QgsPoint( 111, 12, 5 ) ); + QVERIFY( !nodeLine.removeDuplicateNodes( 0.02, true ) ); + QCOMPARE( nodeLine.asWkt( 2 ), QStringLiteral( "CircularStringZ (11 2 1, 11.01 1.99 2, 11.02 2.01 3, 11 12 4, 111 12 5)" ) ); + } @@ -4162,6 +4210,38 @@ void TestQgsGeometry::lineString() QCOMPARE( vertexLine3.segmentLength( QgsVertexId( -1, 0, 0 ) ), 10.0 ); QCOMPARE( vertexLine3.segmentLength( QgsVertexId( 1, 0, 1 ) ), 100.0 ); QCOMPARE( vertexLine3.segmentLength( QgsVertexId( 1, 1, 1 ) ), 100.0 ); + + //removeDuplicateNodes + QgsLineString nodeLine; + QVERIFY( !nodeLine.removeDuplicateNodes() ); + nodeLine.setPoints( QgsPointSequence() << QgsPoint( 11, 2 ) << QgsPoint( 11, 12 ) << QgsPoint( 111, 12 ) ); + QVERIFY( !nodeLine.removeDuplicateNodes() ); + QCOMPARE( nodeLine.asWkt(), QStringLiteral( "LineString (11 2, 11 12, 111 12)" ) ); + nodeLine.setPoints( QgsPointSequence() << QgsPoint( 11, 2 ) << QgsPoint( 11.01, 1.99 ) << QgsPoint( 11.02, 2.01 ) + << QgsPoint( 11, 12 ) << QgsPoint( 111, 12 ) << QgsPoint( 111.01, 11.99 ) ); + QVERIFY( nodeLine.removeDuplicateNodes( 0.02 ) ); + QCOMPARE( nodeLine.asWkt(), QStringLiteral( "LineString (11 2, 11 12, 111 12)" ) ); + nodeLine.setPoints( QgsPointSequence() << QgsPoint( 11, 2 ) << QgsPoint( 11.01, 1.99 ) << QgsPoint( 11.02, 2.01 ) + << QgsPoint( 11, 12 ) << QgsPoint( 111, 12 ) << QgsPoint( 111.01, 11.99 ) ); + QVERIFY( !nodeLine.removeDuplicateNodes() ); + QCOMPARE( nodeLine.asWkt( 2 ), QStringLiteral( "LineString (11 2, 11.01 1.99, 11.02 2.01, 11 12, 111 12, 111.01 11.99)" ) ); + // don't create degenerate lines + nodeLine.setPoints( QgsPointSequence() << QgsPoint( 11, 2 ) ); + QVERIFY( !nodeLine.removeDuplicateNodes( 0.02 ) ); + QCOMPARE( nodeLine.asWkt( 2 ), QStringLiteral( "LineString (11 2)" ) ); + nodeLine.setPoints( QgsPointSequence() << QgsPoint( 11, 2 ) << QgsPoint( 11.01, 1.99 ) ); + QVERIFY( !nodeLine.removeDuplicateNodes( 0.02 ) ); + QCOMPARE( nodeLine.asWkt( 2 ), QStringLiteral( "LineString (11 2, 11.01 1.99)" ) ); + // with z + nodeLine.setPoints( QgsPointSequence() << QgsPoint( 11, 2, 1 ) << QgsPoint( 11.01, 1.99, 2 ) << QgsPoint( 11.02, 2.01, 3 ) + << QgsPoint( 11, 12, 4 ) << QgsPoint( 111, 12, 5 ) << QgsPoint( 111.01, 11.99, 6 ) ); + QVERIFY( nodeLine.removeDuplicateNodes( 0.02 ) ); + QCOMPARE( nodeLine.asWkt(), QStringLiteral( "LineStringZ (11 2 1, 11 12 4, 111 12 5)" ) ); + nodeLine.setPoints( QgsPointSequence() << QgsPoint( 11, 2, 1 ) << QgsPoint( 11.01, 1.99, 2 ) << QgsPoint( 11.02, 2.01, 3 ) + << QgsPoint( 11, 12, 4 ) << QgsPoint( 111, 12, 5 ) << QgsPoint( 111.01, 11.99, 6 ) ); + QVERIFY( !nodeLine.removeDuplicateNodes( 0.02, true ) ); + QCOMPARE( nodeLine.asWkt( 2 ), QStringLiteral( "LineStringZ (11 2 1, 11.01 1.99 2, 11.02 2.01 3, 11 12 4, 111 12 5, 111.01 11.99 6)" ) ); + } void TestQgsGeometry::polygon() @@ -5881,6 +5961,30 @@ void TestQgsGeometry::polygon() QCOMPARE( p30.segmentLength( QgsVertexId( 0, 1, 4 ) ), 0.0 ); QCOMPARE( p30.segmentLength( QgsVertexId( 1, 0, 1 ) ), 100.0 ); QCOMPARE( p30.segmentLength( QgsVertexId( 1, 1, 1 ) ), 2.0 ); + + //removeDuplicateNodes + QgsPolygon nodePolygon; + QgsLineString nodeLine; + QVERIFY( !nodePolygon.removeDuplicateNodes() ); + nodeLine.setPoints( QgsPointSequence() << QgsPoint( 11, 2 ) << QgsPoint( 11, 12 ) << QgsPoint( 11, 22 ) << QgsPoint( 11, 2 ) ); + nodePolygon.setExteriorRing( nodeLine.clone() ); + QVERIFY( !nodePolygon.removeDuplicateNodes() ); + QCOMPARE( nodePolygon.asWkt(), QStringLiteral( "Polygon ((11 2, 11 12, 11 22, 11 2))" ) ); + nodeLine.setPoints( QgsPointSequence() << QgsPoint( 11, 2 ) << QgsPoint( 11.01, 1.99 ) << QgsPoint( 11.02, 2.01 ) + << QgsPoint( 11, 12 ) << QgsPoint( 11, 22 ) << QgsPoint( 11.01, 21.99 ) << QgsPoint( 10.99, 1.99 ) << QgsPoint( 11, 2 ) ); + nodePolygon.setExteriorRing( nodeLine.clone() ); + QVERIFY( nodePolygon.removeDuplicateNodes( 0.02 ) ); + QCOMPARE( nodePolygon.asWkt( 2 ), QStringLiteral( "Polygon ((11 2, 11 12, 11 22, 11 2))" ) ); + nodeLine.setPoints( QgsPointSequence() << QgsPoint( 11, 2 ) << QgsPoint( 11.01, 1.99 ) << QgsPoint( 11.02, 2.01 ) + << QgsPoint( 11, 12 ) << QgsPoint( 11, 22 ) << QgsPoint( 11.01, 21.99 ) << QgsPoint( 10.99, 1.99 ) << QgsPoint( 11, 2 ) ); + nodePolygon.setExteriorRing( nodeLine.clone() ); + QVERIFY( !nodePolygon.removeDuplicateNodes() ); + QCOMPARE( nodePolygon.asWkt( 2 ), QStringLiteral( "Polygon ((11 2, 11.01 1.99, 11.02 2.01, 11 12, 11 22, 11.01 21.99, 10.99 1.99, 11 2))" ) ); + // don't create degenerate rings + nodeLine.setPoints( QgsPointSequence() << QgsPoint( 11, 2 ) << QgsPoint( 11.01, 2.01 ) << QgsPoint( 11, 2.01 ) << QgsPoint( 11, 2 ) ); + nodePolygon.addInteriorRing( nodeLine.clone() ); + QVERIFY( nodePolygon.removeDuplicateNodes( 0.02 ) ); + QCOMPARE( nodePolygon.asWkt( 2 ), QStringLiteral( "Polygon ((11 2, 11 12, 11 22, 11 2),(11 2, 11.01 2.01, 11 2.01, 11 2))" ) ); } void TestQgsGeometry::triangle() @@ -10523,6 +10627,52 @@ void TestQgsGeometry::compoundCurve() QCOMPARE( slc1.segmentLength( QgsVertexId( 0, 0, 3 ) ), 0.0 ); QCOMPARE( slc1.segmentLength( QgsVertexId( 0, 0, 4 ) ), 9.0 ); QCOMPARE( slc1.segmentLength( QgsVertexId( 0, 0, 5 ) ), 0.0 ); + + //removeDuplicateNodes + QgsCompoundCurve nodeCurve; + QgsCircularString nodeLine; + QVERIFY( !nodeCurve.removeDuplicateNodes() ); + nodeLine.setPoints( QgsPointSequence() << QgsPoint( 11, 2 ) << QgsPoint( 11, 12 ) << QgsPoint( 111, 12 ) ); + nodeCurve.addCurve( nodeLine.clone() ); + QVERIFY( !nodeCurve.removeDuplicateNodes() ); + QCOMPARE( nodeCurve.asWkt(), QStringLiteral( "CompoundCurve (CircularString (11 2, 11 12, 111 12))" ) ); + nodeLine.setPoints( QgsPointSequence() << QgsPoint( 11, 2 ) << QgsPoint( 11, 12 ) << QgsPoint( 11, 2 ) ); + nodeCurve.clear(); + nodeCurve.addCurve( nodeLine.clone() ); + QVERIFY( !nodeCurve.removeDuplicateNodes() ); + QCOMPARE( nodeCurve.asWkt(), QStringLiteral( "CompoundCurve (CircularString (11 2, 11 12, 11 2))" ) ); + nodeLine.setPoints( QgsPointSequence() << QgsPoint( 11, 2 ) << QgsPoint( 10, 3 ) << QgsPoint( 11.01, 1.99 ) << QgsPoint( 9, 3 ) + << QgsPoint( 11, 2 ) ); + nodeCurve.clear(); + nodeCurve.addCurve( nodeLine.clone() ); + QVERIFY( !nodeCurve.removeDuplicateNodes( 0.02 ) ); + QCOMPARE( nodeCurve.asWkt( 2 ), QStringLiteral( "CompoundCurve (CircularString (11 2, 10 3, 11.01 1.99, 9 3, 11 2))" ) ); + nodeLine.setPoints( QgsPointSequence() << QgsPoint( 11, 2 ) << QgsPoint( 11.01, 1.99 ) << QgsPoint( 11.02, 2.01 ) + << QgsPoint( 11, 12 ) << QgsPoint( 111, 12 ) << QgsPoint( 111.01, 11.99 ) ); + nodeCurve.clear(); + nodeCurve.addCurve( nodeLine.clone() ); + QVERIFY( !nodeCurve.removeDuplicateNodes() ); + QCOMPARE( nodeCurve.asWkt( 2 ), QStringLiteral( "CompoundCurve (CircularString (11 2, 11.01 1.99, 11.02 2.01, 11 12, 111 12, 111.01 11.99))" ) ); + QVERIFY( nodeCurve.removeDuplicateNodes( 0.02 ) ); + QCOMPARE( nodeCurve.asWkt( 2 ), QStringLiteral( "CompoundCurve (CircularString (11 2, 11 12, 111 12, 111.01 11.99))" ) ); + + // with tiny segment + QgsLineString linePart; + linePart.setPoints( QgsPointSequence() << QgsPoint( 111.01, 11.99 ) << QgsPoint( 111, 12 ) ); + nodeCurve.addCurve( linePart.clone() ); + QVERIFY( !nodeCurve.removeDuplicateNodes() ); + QCOMPARE( nodeCurve.asWkt( 2 ), QStringLiteral( "CompoundCurve (CircularString (11 2, 11 12, 111 12, 111.01 11.99),(111.01 11.99, 111 12))" ) ); + QVERIFY( nodeCurve.removeDuplicateNodes( 0.02 ) ); + QCOMPARE( nodeCurve.asWkt( 2 ), QStringLiteral( "CompoundCurve (CircularString (11 2, 11 12, 111 12, 111.01 11.99))" ) ); + + // ensure continuity + nodeCurve.clear(); + linePart.setPoints( QgsPointSequence() << QgsPoint( 1, 1 ) << QgsPoint( 111.01, 11.99 ) << QgsPoint( 111, 12 ) ); + nodeCurve.addCurve( linePart.clone() ); + linePart.setPoints( QgsPointSequence() << QgsPoint( 111, 12 ) << QgsPoint( 31, 33 ) ); + nodeCurve.addCurve( linePart.clone() ); + QVERIFY( nodeCurve.removeDuplicateNodes( 0.02 ) ); + QCOMPARE( nodeCurve.asWkt( 2 ), QStringLiteral( "CompoundCurve ((1 1, 111.01 11.99),(111.01 11.99, 31 33))" ) ); } void TestQgsGeometry::multiPoint() @@ -10994,6 +11144,14 @@ void TestQgsGeometry::multiPoint() QCOMPARE( c22.vertexNumberFromVertexId( QgsVertexId( 1, 0, 0 ) ), 1 ); QCOMPARE( c22.vertexNumberFromVertexId( QgsVertexId( 1, 0, 1 ) ), -1 ); QCOMPARE( c22.vertexNumberFromVertexId( QgsVertexId( -1, 0, 0 ) ), -1 ); + + QgsMultiPoint mp; + // multipoints should not be affected by removeDuplicatePoints + QVERIFY( !mp.removeDuplicateNodes() ); + mp.addGeometry( new QgsPoint( QgsWkbTypes::PointZM, 10, 1, 4, 8 ) ); + mp.addGeometry( new QgsPoint( QgsWkbTypes::PointZM, 10, 1, 4, 8 ) ); + QVERIFY( !mp.removeDuplicateNodes() ); + QCOMPARE( mp.numGeometries(), 2 ); } void TestQgsGeometry::multiLineString() @@ -14855,6 +15013,21 @@ void TestQgsGeometry::geometryCollection() QCOMPARE( c32.vertexNumberFromVertexId( QgsVertexId( 3, 1, 3 ) ), 17 ); QCOMPARE( c32.vertexNumberFromVertexId( QgsVertexId( 3, 1, 4 ) ), -1 ); QCOMPARE( c32.vertexNumberFromVertexId( QgsVertexId( 3, 2, 0 ) ), -1 ); + + + //removeDuplicateNodes + QgsGeometryCollection gcNodes; + QgsLineString nodeLine; + QVERIFY( !gcNodes.removeDuplicateNodes() ); + nodeLine.setPoints( QgsPointSequence() << QgsPoint( 11, 2 ) << QgsPoint( 11, 12 ) << QgsPoint( 111, 12 ) ); + gcNodes.addGeometry( nodeLine.clone() ); + QVERIFY( !gcNodes.removeDuplicateNodes( 0.02 ) ); + QCOMPARE( gcNodes.asWkt(), QStringLiteral( "GeometryCollection (LineString (11 2, 11 12, 111 12))" ) ); + nodeLine.setPoints( QgsPointSequence() << QgsPoint( 11, 2 ) << QgsPoint( 11.01, 1.99 ) << QgsPoint( 11.02, 2.01 ) + << QgsPoint( 11, 12 ) << QgsPoint( 111, 12 ) << QgsPoint( 111.01, 11.99 ) ); + gcNodes.addGeometry( nodeLine.clone() ); + QVERIFY( gcNodes.removeDuplicateNodes( 0.02 ) ); + QCOMPARE( gcNodes.asWkt( 2 ), QStringLiteral( "GeometryCollection (LineString (11 2, 11 12, 111 12),LineString (11 2, 11 12, 111 12))" ) ); } void TestQgsGeometry::fromQgsPointXY()