Skip to content

Commit ddfeb9e

Browse files
committed
Fixed #26455 -- Allowed filtering and repairing invalid geometries.
The IsValid and MakeValid database functions were added to the GIS module, and a isvalid lookup was added to filter geometries by validity. Conflicts: django/contrib/gis/db/backends/mysql/operations.py django/contrib/gis/db/backends/spatialite/operations.py docs/ref/contrib/gis/functions.txt
1 parent f9fe942 commit ddfeb9e

File tree

14 files changed

+129
-16
lines changed

14 files changed

+129
-16
lines changed

django/contrib/gis/db/backends/base/features.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ def supports_dwithin_lookup(self):
6363
def supports_relate_lookup(self):
6464
return 'relate' in self.connection.ops.gis_operators
6565

66+
@property
67+
def supports_isvalid_lookup(self):
68+
return 'isvalid' in self.connection.ops.gis_operators
69+
6670
# For each of those methods, the class will have a property named
6771
# `has_<name>_method` (defined in __init__) which accesses connection.ops
6872
# to determine GIS method availability.

django/contrib/gis/db/backends/base/operations.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,10 @@ class BaseSpatialOperations(object):
5858
unsupported_functions = {
5959
'Area', 'AsGeoJSON', 'AsGML', 'AsKML', 'AsSVG',
6060
'BoundingCircle', 'Centroid', 'Difference', 'Distance', 'Envelope',
61-
'ForceRHR', 'GeoHash', 'Intersection', 'Length', 'MemSize', 'NumGeometries',
62-
'NumPoints', 'Perimeter', 'PointOnSurface', 'Reverse', 'Scale',
63-
'SnapToGrid', 'SymDifference', 'Transform', 'Translate',
64-
'Union',
61+
'ForceRHR', 'GeoHash', 'Intersection', 'IsValid', 'Length', 'MakeValid',
62+
'MemSize', 'NumGeometries', 'NumPoints', 'Perimeter', 'PointOnSurface',
63+
'Reverse', 'Scale', 'SnapToGrid', 'SymDifference', 'Transform',
64+
'Translate', 'Union',
6565
}
6666

6767
# Serialization

django/contrib/gis/db/backends/mysql/operations.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def unsupported_functions(self):
4949
unsupported = {
5050
'AsGeoJSON', 'AsGML', 'AsKML', 'AsSVG', 'BoundingCircle',
5151
'Difference', 'ForceRHR', 'GeoHash', 'Intersection', 'MemSize',
52+
'IsValid', 'MakeValid',
5253
'Perimeter', 'PointOnSurface', 'Reverse', 'Scale', 'SnapToGrid',
5354
'SymDifference', 'Transform', 'Translate',
5455
}

django/contrib/gis/db/backends/oracle/operations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations):
128128
unsupported_functions = {
129129
'AsGeoJSON', 'AsGML', 'AsKML', 'AsSVG',
130130
'BoundingCircle', 'Envelope',
131-
'ForceRHR', 'GeoHash', 'MemSize', 'Scale',
131+
'ForceRHR', 'GeoHash', 'IsValid', 'MakeValid', 'MemSize', 'Scale',
132132
'SnapToGrid', 'Translate',
133133
}
134134

django/contrib/gis/db/backends/postgis/operations.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
8080
'disjoint': PostGISOperator(func='ST_Disjoint'),
8181
'equals': PostGISOperator(func='ST_Equals'),
8282
'intersects': PostGISOperator(func='ST_Intersects', geography=True),
83+
'isvalid': PostGISOperator(func='ST_IsValid'),
8384
'overlaps': PostGISOperator(func='ST_Overlaps'),
8485
'relate': PostGISOperator(func='ST_Relate'),
8586
'touches': PostGISOperator(func='ST_Touches'),
@@ -119,11 +120,13 @@ def __init__(self, connection):
119120
self.geojson = prefix + 'AsGeoJson'
120121
self.gml = prefix + 'AsGML'
121122
self.intersection = prefix + 'Intersection'
123+
self.isvalid = prefix + 'IsValid'
122124
self.kml = prefix + 'AsKML'
123125
self.length = prefix + 'Length'
124126
self.length3d = prefix + '3DLength'
125127
self.length_spheroid = prefix + 'length_spheroid'
126128
self.makeline = prefix + 'MakeLine'
129+
self.makevalid = prefix + 'MakeValid'
127130
self.mem_size = prefix + 'mem_size'
128131
self.num_geom = prefix + 'NumGeometries'
129132
self.num_points = prefix + 'npoints'

django/contrib/gis/db/backends/spatialite/operations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def function_names(self):
9292

9393
@cached_property
9494
def unsupported_functions(self):
95-
unsupported = {'BoundingCircle', 'ForceRHR', 'GeoHash', 'MemSize'}
95+
unsupported = {'BoundingCircle', 'ForceRHR', 'GeoHash', 'MemSize', 'IsValid', 'MakeValid',}
9696
if not self.gml:
9797
unsupported.add('AsGML')
9898
if not self.kml:

django/contrib/gis/db/models/fields.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ def get_prep_value(self, value):
225225
returning to the caller.
226226
"""
227227
value = super(GeometryField, self).get_prep_value(value)
228-
if isinstance(value, Expression):
228+
if isinstance(value, (Expression, bool)):
229229
return value
230230
elif isinstance(value, (tuple, list)):
231231
geom = value[0]

django/contrib/gis/db/models/functions.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
Area as AreaMeasure, Distance as DistanceMeasure,
77
)
88
from django.core.exceptions import FieldError
9-
from django.db.models import FloatField, IntegerField, TextField
9+
from django.db.models import BooleanField, FloatField, IntegerField, TextField
1010
from django.db.models.expressions import Func, Value
1111
from django.utils import six
1212

@@ -281,6 +281,10 @@ class Intersection(OracleToleranceMixin, GeoFuncWithGeoParam):
281281
pass
282282

283283

284+
class IsValid(GeoFunc):
285+
output_field_class = BooleanField
286+
287+
284288
class Length(DistanceResultMixin, OracleToleranceMixin, GeoFunc):
285289
output_field_class = FloatField
286290

@@ -320,6 +324,10 @@ def as_sqlite(self, compiler, connection):
320324
return super(Length, self).as_sql(compiler, connection)
321325

322326

327+
class MakeValid(GeoFunc):
328+
pass
329+
330+
323331
class MemSize(GeoFunc):
324332
output_field_class = IntegerField
325333

django/contrib/gis/db/models/lookups.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from django.core.exceptions import FieldDoesNotExist
66
from django.db.models.constants import LOOKUP_SEP
77
from django.db.models.expressions import Col, Expression
8-
from django.db.models.lookups import Lookup
8+
from django.db.models.lookups import BuiltinLookup, Lookup
99
from django.utils import six
1010

1111
gis_lookups = {}
@@ -265,6 +265,19 @@ class IntersectsLookup(GISLookup):
265265
gis_lookups['intersects'] = IntersectsLookup
266266

267267

268+
class IsValidLookup(BuiltinLookup):
269+
lookup_name = 'isvalid'
270+
271+
def as_sql(self, compiler, connection):
272+
gis_op = connection.ops.gis_operators[self.lookup_name]
273+
sql, params = self.process_lhs(compiler, connection)
274+
sql = '%(func)s(%(lhs)s)' % {'func': gis_op.func, 'lhs': sql}
275+
if not self.rhs:
276+
sql = 'NOT ' + sql
277+
return sql, params
278+
gis_lookups['isvalid'] = IsValidLookup
279+
280+
268281
class OverlapsLookup(GISLookup):
269282
lookup_name = 'overlaps'
270283
gis_lookups['overlaps'] = OverlapsLookup

docs/ref/contrib/gis/functions.txt

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,12 @@ Function's summary:
2525
================== ======================= ====================== =================== ================== =====================
2626
Measurement Relationships Operations Editors Output format Miscellaneous
2727
================== ======================= ====================== =================== ================== =====================
28-
:class:`Area` :class:`BoundingCircle` :class:`Difference` :class:`ForceRHR` :class:`AsGeoJSON` :class:`MemSize`
29-
:class:`Distance` :class:`Centroid` :class:`Intersection` :class:`Reverse` :class:`AsGML` :class:`NumGeometries`
30-
:class:`Length` :class:`Envelope` :class:`SymDifference` :class:`Scale` :class:`AsKML` :class:`NumPoints`
31-
:class:`Perimeter` :class:`PointOnSurface` :class:`Union` :class:`SnapToGrid` :class:`AsSVG`
32-
33-
:class:`Transform` :class:`GeoHash`
34-
28+
:class:`Area` :class:`BoundingCircle` :class:`Difference` :class:`ForceRHR` :class:`AsGeoJSON` :class:`IsValid`
29+
:class:`Distance` :class:`Centroid` :class:`Intersection` :class:`MakeValid` :class:`AsGML` :class:`MemSize`
30+
:class:`Length` :class:`Envelope` :class:`SymDifference` :class:`Reverse` :class:`AsKML` :class:`NumGeometries`
31+
:class:`Perimeter` :class:`PointOnSurface` :class:`Union` :class:`Scale` :class:`AsSVG` :class:`NumPoints`
32+
:class:`SnapToGrid` :class:`GeoHash`
33+
:class:`Transform`
3534
:class:`Translate`
3635
================== ======================= ====================== =================== ================== =====================
3736

@@ -279,6 +278,22 @@ __ https://en.wikipedia.org/wiki/Geohash
279278
Accepts two geographic fields or expressions and returns the geometric
280279
intersection between them.
281280

281+
.. versionchanged:: 1.10
282+
283+
MySQL support was added.
284+
285+
``IsValid``
286+
================
287+
288+
.. versionadded:: 1.10
289+
290+
.. class:: IsValid(expr)
291+
292+
*Availability*: PostGIS
293+
294+
Accepts a geographic field or expression and tests if the value is well formed.
295+
Returns ``True`` if its value is a valid geometry and ``False`` otherwise.
296+
282297
``Length``
283298
==========
284299

@@ -296,6 +311,20 @@ specify if the calculation should be based on a simple sphere (less
296311
accurate, less resource-intensive) or on a spheroid (more accurate, more
297312
resource-intensive) with the ``spheroid`` keyword argument.
298313

314+
``MakeValid``
315+
================
316+
317+
.. versionadded:: 1.10
318+
319+
.. class:: MakeValid(expr)
320+
321+
*Availability*: PostGIS
322+
323+
Accepts a geographic field or expression and attempts to convert the value into
324+
a valid geometry without losing any of the input vertices. Geometries that are
325+
already valid are returned without changes. Simple polygons might become a
326+
multipolygon and the result might be of lower dimension than the input.
327+
299328
``MemSize``
300329
===========
301330

0 commit comments

Comments
 (0)