Skip to content

Commit

Permalink
Fixed #33136 -- Added GEOSGeometry.make_valid() method.
Browse files Browse the repository at this point in the history
  • Loading branch information
claudep authored and felixxm committed Sep 27, 2021
1 parent fb05ca4 commit 4ffada3
Show file tree
Hide file tree
Showing 6 changed files with 45 additions and 5 deletions.
11 changes: 10 additions & 1 deletion django/contrib/gis/geos/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from django.contrib.gis.geos.base import GEOSBase
from django.contrib.gis.geos.coordseq import GEOSCoordSeq
from django.contrib.gis.geos.error import GEOSException
from django.contrib.gis.geos.libgeos import GEOM_PTR
from django.contrib.gis.geos.libgeos import GEOM_PTR, geos_version_tuple
from django.contrib.gis.geos.mutable_list import ListMixin
from django.contrib.gis.geos.prepared import PreparedGeometry
from django.contrib.gis.geos.prototypes.io import (
Expand Down Expand Up @@ -219,6 +219,15 @@ def normalize(self):
"Convert this Geometry to normal form (or canonical form)."
capi.geos_normalize(self.ptr)

def make_valid(self):
"""
Attempt to create a valid representation of a given invalid geometry
without losing any of the input vertices.
"""
if geos_version_tuple() < (3, 8):
raise GEOSException('GEOSGeometry.make_valid() requires GEOS >= 3.8.0.')
return GEOSGeometry(capi.geos_makevalid(self.ptr), srid=self.srid)

# #### Unary predicates ####
@property
def empty(self):
Expand Down
6 changes: 3 additions & 3 deletions django/contrib/gis/geos/prototypes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
from django.contrib.gis.geos.prototypes.geom import ( # NOQA
create_collection, create_empty_polygon, create_linearring,
create_linestring, create_point, create_polygon, destroy_geom, geom_clone,
geos_get_srid, geos_normalize, geos_set_srid, geos_type, geos_typeid,
get_dims, get_extring, get_geomn, get_intring, get_nrings, get_num_coords,
get_num_geoms,
geos_get_srid, geos_makevalid, geos_normalize, geos_set_srid, geos_type,
geos_typeid, get_dims, get_extring, get_geomn, get_intring, get_nrings,
get_num_coords, get_num_geoms,
)
from django.contrib.gis.geos.prototypes.misc import * # NOQA
from django.contrib.gis.geos.prototypes.predicates import ( # NOQA
Expand Down
1 change: 1 addition & 0 deletions django/contrib/gis/geos/prototypes/geom.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class StringFromGeom(GEOSFuncFactory):
# ### ctypes prototypes ###

# The GEOS geometry type, typeid, num_coordinates and number of geometries
geos_makevalid = GeomOutput('GEOSMakeValid', argtypes=[GEOM_PTR])
geos_normalize = IntFromGeom('GEOSNormalize')
geos_type = StringFromGeom('GEOSGeomType')
geos_typeid = IntFromGeom('GEOSGeomTypeId')
Expand Down
10 changes: 10 additions & 0 deletions docs/ref/contrib/gis/geos.txt
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,16 @@ Other Properties & Methods
doesn't impose any constraints on the geometry's SRID if called with a
:class:`~django.contrib.gis.gdal.CoordTransform` object.

.. method:: GEOSGeometry.make_valid()

.. versionadded:: 4.1

Returns a valid :class:`GEOSGeometry` equivalent, trying not to lose any of
the input vertices. If the geometry is already valid, it is returned
untouched. This is similar to the
:class:`~django.contrib.gis.db.models.functions.MakeValid` database
function. Requires GEOS 3.8.

.. method:: GEOSGeometry.normalize()

Converts this geometry to canonical form::
Expand Down
3 changes: 2 additions & 1 deletion docs/releases/4.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ Minor features
:mod:`django.contrib.gis`
~~~~~~~~~~~~~~~~~~~~~~~~~

* ...
* The new :meth:`.GEOSGeometry.make_valid()` method allows converting invalid
geometries to valid ones.

:mod:`django.contrib.messages`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
19 changes: 19 additions & 0 deletions tests/gis_tests/geos_tests/test_geos.py
Original file line number Diff line number Diff line change
Expand Up @@ -1429,6 +1429,25 @@ def test_normalize(self):
self.assertIsNone(g.normalize())
self.assertTrue(g.equals_exact(MultiPoint(Point(2, 2), Point(1, 1), Point(0, 0))))

@skipIf(geos_version_tuple() < (3, 8), 'GEOS >= 3.8.0 is required')
def test_make_valid(self):
poly = GEOSGeometry('POLYGON((0 0, 0 23, 23 0, 23 23, 0 0))')
self.assertIs(poly.valid, False)
valid_poly = poly.make_valid()
self.assertIs(valid_poly.valid, True)
self.assertNotEqual(valid_poly, poly)

valid_poly2 = valid_poly.make_valid()
self.assertIs(valid_poly2.valid, True)
self.assertEqual(valid_poly, valid_poly2)

@mock.patch('django.contrib.gis.geos.libgeos.geos_version', lambda: b'3.7.3')
def test_make_valid_geos_version(self):
msg = 'GEOSGeometry.make_valid() requires GEOS >= 3.8.0.'
poly = GEOSGeometry('POLYGON((0 0, 0 23, 23 0, 23 23, 0 0))')
with self.assertRaisesMessage(GEOSException, msg):
poly.make_valid()

def test_empty_point(self):
p = Point(srid=4326)
self.assertEqual(p.ogr.ewkt, p.ewkt)
Expand Down

0 comments on commit 4ffada3

Please sign in to comment.