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