From 4ffada36097ceccfd6f0d358217ac3c40d2a729c Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 24 Sep 2021 10:15:23 +0200 Subject: [PATCH] Fixed #33136 -- Added GEOSGeometry.make_valid() method. --- django/contrib/gis/geos/geometry.py | 11 ++++++++++- .../contrib/gis/geos/prototypes/__init__.py | 6 +++--- django/contrib/gis/geos/prototypes/geom.py | 1 + docs/ref/contrib/gis/geos.txt | 10 ++++++++++ docs/releases/4.1.txt | 3 ++- tests/gis_tests/geos_tests/test_geos.py | 19 +++++++++++++++++++ 6 files changed, 45 insertions(+), 5 deletions(-) diff --git a/django/contrib/gis/geos/geometry.py b/django/contrib/gis/geos/geometry.py index bdb3868ac5..36f6895224 100644 --- a/django/contrib/gis/geos/geometry.py +++ b/django/contrib/gis/geos/geometry.py @@ -11,7 +11,7 @@ from django.contrib.gis.geos import prototypes as capi 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 ( @@ -219,6 +219,15 @@ class GEOSGeometryBase(GEOSBase): "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): diff --git a/django/contrib/gis/geos/prototypes/__init__.py b/django/contrib/gis/geos/prototypes/__init__.py index 844ae4cda7..a79533fa14 100644 --- a/django/contrib/gis/geos/prototypes/__init__.py +++ b/django/contrib/gis/geos/prototypes/__init__.py @@ -12,9 +12,9 @@ from django.contrib.gis.geos.prototypes.coordseq import ( # NOQA 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 diff --git a/django/contrib/gis/geos/prototypes/geom.py b/django/contrib/gis/geos/prototypes/geom.py index a84f710900..4cd34504db 100644 --- a/django/contrib/gis/geos/prototypes/geom.py +++ b/django/contrib/gis/geos/prototypes/geom.py @@ -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') diff --git a/docs/ref/contrib/gis/geos.txt b/docs/ref/contrib/gis/geos.txt index 84de1094fc..e8a88014cc 100644 --- a/docs/ref/contrib/gis/geos.txt +++ b/docs/ref/contrib/gis/geos.txt @@ -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:: diff --git a/docs/releases/4.1.txt b/docs/releases/4.1.txt index 28dd574af5..8401103767 100644 --- a/docs/releases/4.1.txt +++ b/docs/releases/4.1.txt @@ -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` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/gis_tests/geos_tests/test_geos.py b/tests/gis_tests/geos_tests/test_geos.py index ab809b6630..27208e005d 100644 --- a/tests/gis_tests/geos_tests/test_geos.py +++ b/tests/gis_tests/geos_tests/test_geos.py @@ -1429,6 +1429,25 @@ class GEOSTest(SimpleTestCase, TestDataMixin): 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)