Skip to content

Commit 32ba5bf

Browse files
authored
Merge pull request #5791 from nyalldawson/geom_snapper_vertices
Fix geometry snapper sometimes creates unwanted overlapping segments when snapping line layers
2 parents f180ea4 + 5a81870 commit 32ba5bf

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+866
-20
lines changed

python/analysis/vector/qgsgeometrysnapper.sip

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ class QgsGeometrySnapper : QObject
2828
{
2929
PreferNodes,
3030
PreferClosest,
31+
PreferNodesNoExtraVertices,
32+
PreferClosestNoExtraVertices,
3133
EndPointPreferNodes,
3234
EndPointPreferClosest,
3335
EndPointToEndPoint,

python/core/geometry/qgsabstractgeometry.sip

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,29 @@ Returns the centroid of the geometry
436436
:rtype: QgsAbstractGeometry
437437
%End
438438

439+
virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ) = 0;
440+
%Docstring
441+
Removes duplicate nodes from the geometry, wherever removing the nodes does not result in a
442+
degenerate geometry.
443+
444+
The ``epsilon`` parameter specifies the tolerance for coordinates when determining that
445+
vertices are identical.
446+
447+
By default, z values are not considered when detecting duplicate nodes. E.g. two nodes
448+
with the same x and y coordinate but different z values will still be considered
449+
duplicate and one will be removed. If ``useZValues`` is true, then the z values are
450+
also tested and nodes with the same x and y but different z will be maintained.
451+
452+
Note that duplicate nodes are not tested between different parts of a multipart geometry. E.g.
453+
a multipoint geometry with overlapping points will not be changed by this method.
454+
455+
The function will return true if nodes were removed, or false if no duplicate nodes
456+
were found.
457+
458+
.. versionadded:: 3.0
459+
:rtype: bool
460+
%End
461+
439462
virtual double vertexAngle( QgsVertexId vertex ) const = 0;
440463
%Docstring
441464
Returns approximate angle at a vertex. This is usually the average angle between adjacent

python/core/geometry/qgscircularstring.sip

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ class QgsCircularString: QgsCurve
8383

8484
virtual QgsCircularString *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const /Factory/;
8585

86+
virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false );
87+
88+
8689
virtual void draw( QPainter &p ) const;
8790

8891
virtual void transform( const QgsCoordinateTransform &ct, QgsCoordinateTransform::TransformDirection d = QgsCoordinateTransform::ForwardTransform,

python/core/geometry/qgscompoundcurve.sip

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ class QgsCompoundCurve: QgsCurve
7979

8080
virtual QgsCompoundCurve *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const /Factory/;
8181

82+
virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false );
83+
8284

8385
int nCurves() const;
8486
%Docstring

python/core/geometry/qgscurvepolygon.sip

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ class QgsCurvePolygon: QgsSurface
6767

6868
virtual QgsCurvePolygon *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const /Factory/;
6969

70+
virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false );
71+
7072

7173
int numInteriorRings() const;
7274
%Docstring

python/core/geometry/qgsgeometry.sip

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@ Returns true if WKB of the geometry is of WKBMulti* type
501501
\param afterVertex Receives index of the vertex after the closest segment. The vertex
502502
before the closest segment is always afterVertex - 1
503503
\param leftOf Out: Returns if the point lies on the left of left side of the geometry ( < 0 means left, > 0 means right, 0 indicates
504-
that the test was unsuccesful, e.g. for a point exactly on the line)
504+
that the test was unsuccessful, e.g. for a point exactly on the line)
505505
\param epsilon epsilon for segment snapping
506506
:return: The squared Cartesian distance is also returned in sqrDist, negative number on error
507507
:rtype: float
@@ -688,6 +688,29 @@ Returns true if WKB of the geometry is of WKBMulti* type
688688
:rtype: QgsGeometry
689689
%End
690690

691+
bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false );
692+
%Docstring
693+
Removes duplicate nodes from the geometry, wherever removing the nodes does not result in a
694+
degenerate geometry.
695+
696+
The ``epsilon`` parameter specifies the tolerance for coordinates when determining that
697+
vertices are identical.
698+
699+
By default, z values are not considered when detecting duplicate nodes. E.g. two nodes
700+
with the same x and y coordinate but different z values will still be considered
701+
duplicate and one will be removed. If ``useZValues`` is true, then the z values are
702+
also tested and nodes with the same x and y but different z will be maintained.
703+
704+
Note that duplicate nodes are not tested between different parts of a multipart geometry. E.g.
705+
a multipoint geometry with overlapping points will not be changed by this method.
706+
707+
The function will return true if nodes were removed, or false if no duplicate nodes
708+
were found.
709+
710+
.. versionadded:: 3.0
711+
:rtype: bool
712+
%End
713+
691714
bool intersects( const QgsRectangle &r ) const;
692715
%Docstring
693716
Tests for intersection with a rectangle (uses GEOS)

python/core/geometry/qgsgeometrycollection.sip

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ class QgsGeometryCollection: QgsAbstractGeometry
5252
virtual void clear();
5353

5454
virtual QgsGeometryCollection *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const /Factory/;
55+
56+
virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false );
57+
5558
virtual QgsAbstractGeometry *boundary() const /Factory/;
5659

5760
virtual void adjacentVertices( QgsVertexId vertex, QgsVertexId &previousVertex /Out/, QgsVertexId &nextVertex /Out/ ) const;

python/core/geometry/qgslinestring.sip

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ Closes the line string by appending the first point to the end of the line, if i
179179

180180
virtual QgsLineString *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const /Factory/;
181181

182+
virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false );
183+
182184

183185
virtual bool fromWkb( QgsConstWkbPtr &wkb );
184186

python/core/geometry/qgspoint.sip

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,9 @@ class QgsPoint: QgsAbstractGeometry
339339
virtual QgsPoint *clone() const /Factory/;
340340

341341
virtual QgsPoint *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const /Factory/;
342+
343+
virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false );
344+
342345
virtual void clear();
343346

344347
virtual bool fromWkb( QgsConstWkbPtr &wkb );

python/plugins/processing/algs/qgis/CheckValidity.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ def icon(self):
6868
def group(self):
6969
return self.tr('Vector geometry')
7070

71+
def tags(self):
72+
return self.tr('valid,invalid,detect').split(',')
73+
7174
def __init__(self):
7275
super().__init__()
7376

@@ -79,7 +82,7 @@ def initAlgorithm(self, config=None):
7982
self.addParameter(QgsProcessingParameterFeatureSource(self.INPUT_LAYER,
8083
self.tr('Input layer')))
8184
self.addParameter(QgsProcessingParameterEnum(self.METHOD,
82-
self.tr('Method'), self.methods))
85+
self.tr('Method'), self.methods, defaultValue=2))
8386
self.parameterDefinition(self.METHOD).setMetadata({
8487
'widget_wrapper': {
8588
'class': 'processing.gui.wrappers.EnumWidgetWrapper',

python/plugins/processing/algs/qgis/SnapGeometries.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,10 @@ def initAlgorithm(self, config=None):
6161
self.addParameter(QgsProcessingParameterNumber(self.TOLERANCE, self.tr('Tolerance (layer units)'), type=QgsProcessingParameterNumber.Double,
6262
minValue=0.00000001, maxValue=9999999999, defaultValue=10.0))
6363

64-
self.modes = [self.tr('Prefer aligning nodes'),
65-
self.tr('Prefer closest point'),
64+
self.modes = [self.tr('Prefer aligning nodes, insert extra vertices where required'),
65+
self.tr('Prefer closest point, insert extra vertices where required'),
66+
self.tr('Prefer aligning nodes, don\'t insert new vertices'),
67+
self.tr('Prefer closest point, don\'t insert new vertices'),
6668
self.tr('Move end points only, prefer aligning nodes'),
6769
self.tr('Move end points only, prefer closest point'),
6870
self.tr('Snap end points to end points only')]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<ogr:FeatureCollection
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://ogr.maptools.org/ line_duplicate_nodes.xsd"
5+
xmlns:ogr="http://ogr.maptools.org/"
6+
xmlns:gml="http://www.opengis.net/gml">
7+
<gml:boundedBy>
8+
<gml:Box>
9+
<gml:coord><gml:X>2</gml:X><gml:Y>0</gml:Y></gml:coord>
10+
<gml:coord><gml:X>3</gml:X><gml:Y>3</gml:Y></gml:coord>
11+
</gml:Box>
12+
</gml:boundedBy>
13+
14+
<gml:featureMember>
15+
<ogr:line_duplicate_nodes fid="lines.2">
16+
<ogr:geometryProperty><gml:LineString srsName="EPSG:4326"><gml:coordinates>2,0 2,2 3,2 3,3 3,3</gml:coordinates></gml:LineString></ogr:geometryProperty>
17+
</ogr:line_duplicate_nodes>
18+
</gml:featureMember>
19+
</ogr:FeatureCollection>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<xs:schema targetNamespace="http://ogr.maptools.org/" xmlns:ogr="http://ogr.maptools.org/" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:gml="http://www.opengis.net/gml" elementFormDefault="qualified" version="1.0">
3+
<xs:import namespace="http://www.opengis.net/gml" schemaLocation="http://schemas.opengis.net/gml/2.1.2/feature.xsd"/>
4+
<xs:element name="FeatureCollection" type="ogr:FeatureCollectionType" substitutionGroup="gml:_FeatureCollection"/>
5+
<xs:complexType name="FeatureCollectionType">
6+
<xs:complexContent>
7+
<xs:extension base="gml:AbstractFeatureCollectionType">
8+
<xs:attribute name="lockId" type="xs:string" use="optional"/>
9+
<xs:attribute name="scope" type="xs:string" use="optional"/>
10+
</xs:extension>
11+
</xs:complexContent>
12+
</xs:complexType>
13+
<xs:element name="line_duplicate_nodes" type="ogr:line_duplicate_nodes_Type" substitutionGroup="gml:_Feature"/>
14+
<xs:complexType name="line_duplicate_nodes_Type">
15+
<xs:complexContent>
16+
<xs:extension base="gml:AbstractFeatureType">
17+
<xs:sequence>
18+
<xs:element name="geometryProperty" type="gml:LineStringPropertyType" nillable="true" minOccurs="0" maxOccurs="1"/>
19+
</xs:sequence>
20+
</xs:extension>
21+
</xs:complexContent>
22+
</xs:complexType>
23+
</xs:schema>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<ogr:FeatureCollection
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://ogr.maptools.org/ removed_duplicated_nodes_line.xsd"
5+
xmlns:ogr="http://ogr.maptools.org/"
6+
xmlns:gml="http://www.opengis.net/gml">
7+
<gml:boundedBy>
8+
<gml:Box>
9+
<gml:coord><gml:X>2</gml:X><gml:Y>0</gml:Y></gml:coord>
10+
<gml:coord><gml:X>3</gml:X><gml:Y>3</gml:Y></gml:coord>
11+
</gml:Box>
12+
</gml:boundedBy>
13+
14+
<gml:featureMember>
15+
<ogr:removed_duplicated_nodes_line fid="lines.2">
16+
<ogr:geometryProperty><gml:LineString srsName="EPSG:4326"><gml:coordinates>2,0 2,2 3,2 3,3</gml:coordinates></gml:LineString></ogr:geometryProperty>
17+
</ogr:removed_duplicated_nodes_line>
18+
</gml:featureMember>
19+
</ogr:FeatureCollection>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<xs:schema targetNamespace="http://ogr.maptools.org/" xmlns:ogr="http://ogr.maptools.org/" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:gml="http://www.opengis.net/gml" elementFormDefault="qualified" version="1.0">
3+
<xs:import namespace="http://www.opengis.net/gml" schemaLocation="http://schemas.opengis.net/gml/2.1.2/feature.xsd"/>
4+
<xs:element name="FeatureCollection" type="ogr:FeatureCollectionType" substitutionGroup="gml:_FeatureCollection"/>
5+
<xs:complexType name="FeatureCollectionType">
6+
<xs:complexContent>
7+
<xs:extension base="gml:AbstractFeatureCollectionType">
8+
<xs:attribute name="lockId" type="xs:string" use="optional"/>
9+
<xs:attribute name="scope" type="xs:string" use="optional"/>
10+
</xs:extension>
11+
</xs:complexContent>
12+
</xs:complexType>
13+
<xs:element name="removed_duplicated_nodes_line" type="ogr:removed_duplicated_nodes_line_Type" substitutionGroup="gml:_Feature"/>
14+
<xs:complexType name="removed_duplicated_nodes_line_Type">
15+
<xs:complexContent>
16+
<xs:extension base="gml:AbstractFeatureType">
17+
<xs:sequence>
18+
<xs:element name="geometryProperty" type="gml:LineStringPropertyType" nillable="true" minOccurs="0" maxOccurs="1"/>
19+
</xs:sequence>
20+
</xs:extension>
21+
</xs:complexContent>
22+
</xs:complexType>
23+
</xs:schema>

python/plugins/processing/tests/testdata/expected/snap_lines_to_lines.gml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
</gml:featureMember>
2424
<gml:featureMember>
2525
<ogr:snap_lines_to_lines fid="lines.2">
26-
<ogr:geometryProperty><gml:LineString srsName="EPSG:4326"><gml:coordinates>2,0 2,2 2,2 3,2</gml:coordinates></gml:LineString></ogr:geometryProperty>
26+
<ogr:geometryProperty><gml:LineString srsName="EPSG:4326"><gml:coordinates>2,0 2,2 3,2</gml:coordinates></gml:LineString></ogr:geometryProperty>
2727
</ogr:snap_lines_to_lines>
2828
</gml:featureMember>
2929
<gml:featureMember>

python/plugins/processing/tests/testdata/expected/snap_polys_to_polys.gml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
<gml:featureMember>
1515
<ogr:snap_polys_to_polys fid="polys.0">
16-
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>-1,-1 -1,3 3,3 3,2 2,2 2,2 2,2 2,-1 -1,-1</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
16+
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>-1,-1 -1,3 3,3 3,2 2,2 2,-1 -1,-1</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
1717
<ogr:name>aaaaa</ogr:name>
1818
<ogr:intval>33</ogr:intval>
1919
<ogr:floatval>44.123456</ogr:floatval>

python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4582,3 +4582,15 @@ tests:
45824582
name: expected/difference.gml
45834583
type: vector
45844584

4585+
- algorithm: native:removeduplicatenodes
4586+
name: Remove duplicate nodes lines
4587+
params:
4588+
INPUT:
4589+
name: custom/line_duplicate_nodes.gml
4590+
type: vector
4591+
TOLERANCE: 1.0e-06
4592+
USE_Z_VALUE: false
4593+
results:
4594+
OUTPUT:
4595+
name: expected/removed_duplicated_nodes_line.gml
4596+
type: vector

src/analysis/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ SET(QGIS_ANALYSIS_SRCS
5151
processing/qgsalgorithmpackage.cpp
5252
processing/qgsalgorithmpromotetomultipart.cpp
5353
processing/qgsalgorithmrasterlayeruniquevalues.cpp
54+
processing/qgsalgorithmremoveduplicatenodes
5455
processing/qgsalgorithmremovenullgeometry.cpp
5556
processing/qgsalgorithmrenamelayer.cpp
5657
processing/qgsalgorithmsaveselectedfeatures.cpp
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/***************************************************************************
2+
qgsalgorithmremoveduplicatenodes.cpp
3+
---------------------
4+
begin : November 2017
5+
copyright : (C) 2017 by Nyall Dawson
6+
email : nyall dot dawson at gmail dot com
7+
***************************************************************************/
8+
9+
/***************************************************************************
10+
* *
11+
* This program is free software; you can redistribute it and/or modify *
12+
* it under the terms of the GNU General Public License as published by *
13+
* the Free Software Foundation; either version 2 of the License, or *
14+
* (at your option) any later version. *
15+
* *
16+
***************************************************************************/
17+
18+
#include "qgsalgorithmremoveduplicatenodes.h"
19+
20+
///@cond PRIVATE
21+
22+
QString QgsAlgorithmRemoveDuplicateNodes::name() const
23+
{
24+
return QStringLiteral( "removeduplicatenodes" );
25+
}
26+
27+
QString QgsAlgorithmRemoveDuplicateNodes::displayName() const
28+
{
29+
return QObject::tr( "Remove duplicate nodes" );
30+
}
31+
32+
QStringList QgsAlgorithmRemoveDuplicateNodes::tags() const
33+
{
34+
return QObject::tr( "points,valid,overlapping" ).split( ',' );
35+
}
36+
37+
QString QgsAlgorithmRemoveDuplicateNodes::group() const
38+
{
39+
return QObject::tr( "Vector geometry" );
40+
}
41+
42+
QString QgsAlgorithmRemoveDuplicateNodes::outputName() const
43+
{
44+
return QObject::tr( "Cleaned" );
45+
}
46+
47+
QString QgsAlgorithmRemoveDuplicateNodes::shortHelpString() const
48+
{
49+
return QObject::tr( "This algorithm removes duplicate nodes from features, wherever removing the nodes does "
50+
"not result in a degenerate geometry.\n\n"
51+
"The tolerance parameter specifies the tolerance for coordinates when determining whether "
52+
"vertices are identical.\n\n"
53+
"By default, z values are not considered when detecting duplicate nodes. E.g. two nodes "
54+
"with the same x and y coordinate but different z values will still be considered "
55+
"duplicate and one will be removed. If the Use Z Value parameter is true, then the z values are "
56+
"also tested and nodes with the same x and y but different z will be maintained.\n\n"
57+
"Note that duplicate nodes are not tested between different parts of a multipart geometry. E.g. "
58+
"a multipoint geometry with overlapping points will not be changed by this method." );
59+
}
60+
61+
QgsAlgorithmRemoveDuplicateNodes *QgsAlgorithmRemoveDuplicateNodes::createInstance() const
62+
{
63+
return new QgsAlgorithmRemoveDuplicateNodes();
64+
}
65+
66+
void QgsAlgorithmRemoveDuplicateNodes::initParameters( const QVariantMap & )
67+
{
68+
addParameter( new QgsProcessingParameterNumber( QStringLiteral( "TOLERANCE" ),
69+
QObject::tr( "Tolerance" ), QgsProcessingParameterNumber::Double,
70+
0.000001, false, 0, 10000000.0 ) );
71+
addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "USE_Z_VALUE" ),
72+
QObject::tr( "Use Z Value" ), false ) );
73+
}
74+
75+
bool QgsAlgorithmRemoveDuplicateNodes::prepareAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback * )
76+
{
77+
mTolerance = parameterAsDouble( parameters, QStringLiteral( "TOLERANCE" ), context );
78+
mUseZValues = parameterAsBool( parameters, QStringLiteral( "USE_Z_VALUE" ), context );
79+
return true;
80+
}
81+
82+
QgsFeature QgsAlgorithmRemoveDuplicateNodes::processFeature( const QgsFeature &feature, QgsProcessingContext &, QgsProcessingFeedback * )
83+
{
84+
QgsFeature f = feature;
85+
if ( f.hasGeometry() )
86+
{
87+
QgsGeometry geometry = f.geometry();
88+
geometry.removeDuplicateNodes( mTolerance, mUseZValues );
89+
f.setGeometry( geometry );
90+
}
91+
return f;
92+
}
93+
94+
///@endcond
95+
96+

0 commit comments

Comments
 (0)