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()