Skip to content

Commit 4979891

Browse files
committed
By default, validity check should treat ring self intersections as invalid
We use the OGC definition of validity to ensure consistent results with PostGIS, GDAL, etc Fixes #16418, fixes #21336 (cherry picked from commit 4e04d02)
1 parent 4a63882 commit 4979891

13 files changed

+213
-17
lines changed

python/core/auto_generated/geometry/qgsgeometry.sip.in

+2-4
Original file line numberDiff line numberDiff line change
@@ -1707,14 +1707,12 @@ True if the location available from :py:func:`where` is valid.
17071707
ValidatorGeos,
17081708
};
17091709

1710-
void validateGeometry( QVector<QgsGeometry::Error> &errors /Out/, ValidationMethod method = ValidatorQgisInternal ) const;
1710+
void validateGeometry( QVector<QgsGeometry::Error> &errors /Out/, ValidationMethod method = ValidatorQgisInternal, QgsGeometry::ValidityFlags flags = 0 ) const;
17111711
%Docstring
17121712
Validates geometry and produces a list of geometry errors.
17131713
The ``method`` argument dictates which validator to utilize.
17141714

1715-
.. note::
1716-
1717-
Available in Python bindings since QGIS 1.6
1715+
The ``flags`` parameter indicates optional flags which control the type of validity checking performed.
17181716

17191717
.. versionadded:: 1.5
17201718
%End
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/ poly_ring_self_intersection.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>200</gml:X><gml:Y>200</gml:Y></gml:coord>
10+
<gml:coord><gml:X>400</gml:X><gml:Y>400</gml:Y></gml:coord>
11+
</gml:Box>
12+
</gml:boundedBy>
13+
14+
<gml:featureMember>
15+
<ogr:poly_ring_self_intersection fid="poly_ring_self_intersection.0">
16+
<ogr:geometryProperty><gml:Polygon srsName="EPSG:28356"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>200,400 400,400 400,200 300,200 350,250 250,250 300,200 200,200 200,400</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
17+
</ogr:poly_ring_self_intersection>
18+
</gml:featureMember>
19+
</ogr:FeatureCollection>
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="poly_ring_self_intersection" type="ogr:poly_ring_self_intersection_Type" substitutionGroup="gml:_Feature"/>
14+
<xs:complexType name="poly_ring_self_intersection_Type">
15+
<xs:complexContent>
16+
<xs:extension base="gml:AbstractFeatureType">
17+
<xs:sequence>
18+
<xs:element name="geometryProperty" type="gml:PolygonPropertyType" nillable="true" minOccurs="0" maxOccurs="1"/>
19+
</xs:sequence>
20+
</xs:extension>
21+
</xs:complexContent>
22+
</xs:complexType>
23+
</xs:schema>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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/ poly_ring_self_intersection_error.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>300</gml:X><gml:Y>200</gml:Y></gml:coord>
10+
<gml:coord><gml:X>300</gml:X><gml:Y>200</gml:Y></gml:coord>
11+
</gml:Box>
12+
</gml:boundedBy>
13+
14+
<gml:featureMember>
15+
<ogr:poly_ring_self_intersection_error fid="poly_ring_self_intersection_error.0">
16+
<ogr:geometryProperty><gml:Point srsName="EPSG:28356"><gml:coordinates>300,200</gml:coordinates></gml:Point></ogr:geometryProperty>
17+
<ogr:message>Ring self-intersection</ogr:message>
18+
</ogr:poly_ring_self_intersection_error>
19+
</gml:featureMember>
20+
</ogr:FeatureCollection>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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="poly_ring_self_intersection_error" type="ogr:poly_ring_self_intersection_error_Type" substitutionGroup="gml:_Feature"/>
14+
<xs:complexType name="poly_ring_self_intersection_error_Type">
15+
<xs:complexContent>
16+
<xs:extension base="gml:AbstractFeatureType">
17+
<xs:sequence>
18+
<xs:element name="geometryProperty" type="gml:PointPropertyType" nillable="true" minOccurs="0" maxOccurs="1"/>
19+
<xs:element name="message" nillable="true" minOccurs="0" maxOccurs="1">
20+
<xs:simpleType>
21+
<xs:restriction base="xs:string">
22+
<xs:maxLength value="255"/>
23+
</xs:restriction>
24+
</xs:simpleType>
25+
</xs:element>
26+
</xs:sequence>
27+
</xs:extension>
28+
</xs:complexContent>
29+
</xs:complexType>
30+
</xs:schema>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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/ poly_ring_self_intersection_invalid.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>200</gml:X><gml:Y>200</gml:Y></gml:coord>
10+
<gml:coord><gml:X>400</gml:X><gml:Y>400</gml:Y></gml:coord>
11+
</gml:Box>
12+
</gml:boundedBy>
13+
14+
<gml:featureMember>
15+
<ogr:poly_ring_self_intersection_invalid fid="poly_ring_self_intersection.0">
16+
<ogr:geometryProperty><gml:Polygon srsName="EPSG:28356"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>200,400 400,400 400,200 300,200 350,250 250,250 300,200 200,200 200,400</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
17+
<ogr:_errors>Ring self-intersection</ogr:_errors>
18+
</ogr:poly_ring_self_intersection_invalid>
19+
</gml:featureMember>
20+
</ogr:FeatureCollection>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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="poly_ring_self_intersection_invalid" type="ogr:poly_ring_self_intersection_invalid_Type" substitutionGroup="gml:_Feature"/>
14+
<xs:complexType name="poly_ring_self_intersection_invalid_Type">
15+
<xs:complexContent>
16+
<xs:extension base="gml:AbstractFeatureType">
17+
<xs:sequence>
18+
<xs:element name="geometryProperty" type="gml:PolygonPropertyType" nillable="true" minOccurs="0" maxOccurs="1"/>
19+
<xs:element name="_errors" nillable="true" minOccurs="0" maxOccurs="1">
20+
<xs:simpleType>
21+
<xs:restriction base="xs:string">
22+
<xs:maxLength value="255"/>
23+
</xs:restriction>
24+
</xs:simpleType>
25+
</xs:element>
26+
</xs:sequence>
27+
</xs:extension>
28+
</xs:complexContent>
29+
</xs:complexType>
30+
</xs:schema>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
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/ poly_ring_self_intersection_valid.xsd"
5+
xmlns:ogr="http://ogr.maptools.org/"
6+
xmlns:gml="http://www.opengis.net/gml">
7+
<gml:boundedBy><gml:null>missing</gml:null></gml:boundedBy>
8+
9+
</ogr:FeatureCollection>
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="poly_ring_self_intersection_valid" type="ogr:poly_ring_self_intersection_valid_Type" substitutionGroup="gml:_Feature"/>
14+
<xs:complexType name="poly_ring_self_intersection_valid_Type">
15+
<xs:complexContent>
16+
<xs:extension base="gml:AbstractFeatureType">
17+
<xs:sequence>
18+
<xs:element name="geometryProperty" type="gml:PolygonPropertyType" 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/qgis_algorithm_tests.yaml

+18
Original file line numberDiff line numberDiff line change
@@ -3678,6 +3678,24 @@ tests:
36783678
name: expected/valid.gml
36793679
type: vector
36803680

3681+
- algorithm: qgis:checkvalidity
3682+
name: Check validity polygon ring self intersection
3683+
params:
3684+
INPUT_LAYER:
3685+
name: custom/poly_ring_self_intersection.gml|layername=poly_ring_self_intersection
3686+
type: vector
3687+
METHOD: 2
3688+
results:
3689+
ERROR_OUTPUT:
3690+
name: expected/poly_ring_self_intersection_error.gml
3691+
type: vector
3692+
INVALID_OUTPUT:
3693+
name: expected/poly_ring_self_intersection_invalid.gml
3694+
type: vector
3695+
VALID_OUTPUT:
3696+
name: expected/poly_ring_self_intersection_valid.gml
3697+
type: vector
3698+
36813699
- algorithm: qgis:polygonize
36823700
name: Polygonize
36833701
params:

src/core/geometry/qgsgeometry.cpp

+2-2
Original file line numberDiff line numberDiff line change
@@ -2457,7 +2457,7 @@ QgsGeometry QgsGeometry::forceRHR() const
24572457
}
24582458

24592459

2460-
void QgsGeometry::validateGeometry( QVector<QgsGeometry::Error> &errors, ValidationMethod method ) const
2460+
void QgsGeometry::validateGeometry( QVector<QgsGeometry::Error> &errors, const ValidationMethod method, const QgsGeometry::ValidityFlags flags ) const
24612461
{
24622462
errors.clear();
24632463
if ( !d->geometry )
@@ -2480,7 +2480,7 @@ void QgsGeometry::validateGeometry( QVector<QgsGeometry::Error> &errors, Validat
24802480
QgsGeos geos( d->geometry.get() );
24812481
QString error;
24822482
QgsGeometry errorLoc;
2483-
if ( !geos.isValid( &error, true, &errorLoc ) )
2483+
if ( !geos.isValid( &error, flags & FlagAllowSelfTouchingHoles, &errorLoc ) )
24842484
{
24852485
if ( errorLoc.isNull() )
24862486
{

src/core/geometry/qgsgeometry.h

+4-2
Original file line numberDiff line numberDiff line change
@@ -1755,10 +1755,12 @@ class CORE_EXPORT QgsGeometry
17551755
/**
17561756
* Validates geometry and produces a list of geometry errors.
17571757
* The \a method argument dictates which validator to utilize.
1758-
* \note Available in Python bindings since QGIS 1.6
1758+
*
1759+
* The \a flags parameter indicates optional flags which control the type of validity checking performed.
1760+
*
17591761
* \since QGIS 1.5
17601762
*/
1761-
void validateGeometry( QVector<QgsGeometry::Error> &errors SIP_OUT, ValidationMethod method = ValidatorQgisInternal ) const;
1763+
void validateGeometry( QVector<QgsGeometry::Error> &errors SIP_OUT, ValidationMethod method = ValidatorQgisInternal, QgsGeometry::ValidityFlags flags = nullptr ) const;
17621764

17631765
/**
17641766
* Compute the unary union on a list of \a geometries. May be faster than an iterative union on a set of geometries.

tests/src/python/test_qgsgeometry.py

+13-9
Original file line numberDiff line numberDiff line change
@@ -5001,23 +5001,27 @@ def testIsGeosValid(self):
50015001

50025002
def testValidateGeometry(self):
50035003
tests = [
5004-
["", []],
5005-
["Point (100 100)", [], []],
5006-
["MultiPoint (100 100, 100 200)", [], []],
5007-
["LINESTRING (0 0, 0 100, 100 100)", [], []],
5008-
["POLYGON((-1 -1, 4 0, 4 2, 0 2, -1 -1))", [], []],
5009-
["MULTIPOLYGON(Polygon((-1 -1, 4 0, 4 2, 0 2, -1 -1)),Polygon((100 100, 200 100, 200 200, 100 200, 100 100)))", [], []],
5010-
['MultiPolygon (((159865.14786298031685874 6768656.31838363595306873, 159858.97975336571107619 6769211.44824895076453686, 160486.07089751763851382 6769211.44824895076453686, 160481.95882444124436006 6768658.37442017439752817, 160163.27316101978067309 6768658.37442017439752817, 160222.89822062765597366 6769116.87056819349527359, 160132.43261294672265649 6769120.98264127038419247, 160163.27316101978067309 6768658.37442017439752817, 159865.14786298031685874 6768656.31838363595306873)))', [], []],
5011-
['Polygon((0 3, 3 0, 3 3, 0 0, 0 3))', [QgsGeometry.Error('Self-intersection', QgsPointXY(1.5, 1.5))], [QgsGeometry.Error('segments 0 and 2 of line 0 intersect at 1.5, 1.5', QgsPointXY(1.5, 1.5))]],
5004+
["", [], [], []],
5005+
["Point (100 100)", [], [], []],
5006+
["MultiPoint (100 100, 100 200)", [], [], []],
5007+
["LINESTRING (0 0, 0 100, 100 100)", [], [], []],
5008+
["POLYGON((-1 -1, 4 0, 4 2, 0 2, -1 -1))", [], [], []],
5009+
["MULTIPOLYGON(Polygon((-1 -1, 4 0, 4 2, 0 2, -1 -1)),Polygon((100 100, 200 100, 200 200, 100 200, 100 100)))", [], [], []],
5010+
['POLYGON ((200 400, 400 400, 400 200, 300 200, 350 250, 250 250, 300 200, 200 200, 200 400))', [QgsGeometry.Error('Ring self-intersection', QgsPointXY(300, 200))], [], []],
5011+
['MultiPolygon (((159865.14786298031685874 6768656.31838363595306873, 159858.97975336571107619 6769211.44824895076453686, 160486.07089751763851382 6769211.44824895076453686, 160481.95882444124436006 6768658.37442017439752817, 160163.27316101978067309 6768658.37442017439752817, 160222.89822062765597366 6769116.87056819349527359, 160132.43261294672265649 6769120.98264127038419247, 160163.27316101978067309 6768658.37442017439752817, 159865.14786298031685874 6768656.31838363595306873)))', [QgsGeometry.Error('Ring self-intersection', QgsPointXY(160163.27316101978067309, 6768658.37442017439752817))], [], []],
5012+
['Polygon((0 3, 3 0, 3 3, 0 0, 0 3))', [QgsGeometry.Error('Self-intersection', QgsPointXY(1.5, 1.5))], [QgsGeometry.Error('Self-intersection', QgsPointXY(1.5, 1.5))], [QgsGeometry.Error('segments 0 and 2 of line 0 intersect at 1.5, 1.5', QgsPointXY(1.5, 1.5))]],
50125013
]
50135014
for t in tests:
50145015
g1 = QgsGeometry.fromWkt(t[0])
50155016
res = g1.validateGeometry(QgsGeometry.ValidatorGeos)
50165017
self.assertEqual(res, t[1],
50175018
"mismatch for {}, expected:\n{}\nGot:\n{}\n".format(t[0], t[1], res[0].where() if res else ''))
5018-
res = g1.validateGeometry(QgsGeometry.ValidatorQgisInternal)
5019+
res = g1.validateGeometry(QgsGeometry.ValidatorGeos, QgsGeometry.FlagAllowSelfTouchingHoles)
50195020
self.assertEqual(res, t[2],
50205021
"mismatch for {}, expected:\n{}\nGot:\n{}\n".format(t[0], t[2], res[0].where() if res else ''))
5022+
res = g1.validateGeometry(QgsGeometry.ValidatorQgisInternal)
5023+
self.assertEqual(res, t[3],
5024+
"mismatch for {}, expected:\n{}\nGot:\n{}\n".format(t[0], t[3], res[0].where() if res else ''))
50215025

50225026
def renderGeometry(self, geom, use_pen, as_polygon=False, as_painter_path=False):
50235027
image = QImage(200, 200, QImage.Format_RGB32)

0 commit comments

Comments
 (0)