Fixed #33136 -- Added GEOSGeometry.make_valid() method.

This commit is contained in:
Claude Paroz 2021-09-24 10:15:23 +02:00 committed by Mariusz Felisiak
parent fb05ca420d
commit 4ffada3609
6 changed files with 45 additions and 5 deletions

View File

@ -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.base import GEOSBase
from django.contrib.gis.geos.coordseq import GEOSCoordSeq from django.contrib.gis.geos.coordseq import GEOSCoordSeq
from django.contrib.gis.geos.error import GEOSException 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.mutable_list import ListMixin
from django.contrib.gis.geos.prepared import PreparedGeometry from django.contrib.gis.geos.prepared import PreparedGeometry
from django.contrib.gis.geos.prototypes.io import ( from django.contrib.gis.geos.prototypes.io import (
@ -219,6 +219,15 @@ class GEOSGeometryBase(GEOSBase):
"Convert this Geometry to normal form (or canonical form)." "Convert this Geometry to normal form (or canonical form)."
capi.geos_normalize(self.ptr) 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 #### # #### Unary predicates ####
@property @property
def empty(self): def empty(self):

View File

@ -12,9 +12,9 @@ from django.contrib.gis.geos.prototypes.coordseq import ( # NOQA
from django.contrib.gis.geos.prototypes.geom import ( # NOQA from django.contrib.gis.geos.prototypes.geom import ( # NOQA
create_collection, create_empty_polygon, create_linearring, create_collection, create_empty_polygon, create_linearring,
create_linestring, create_point, create_polygon, destroy_geom, geom_clone, create_linestring, create_point, create_polygon, destroy_geom, geom_clone,
geos_get_srid, geos_normalize, geos_set_srid, geos_type, geos_typeid, geos_get_srid, geos_makevalid, geos_normalize, geos_set_srid, geos_type,
get_dims, get_extring, get_geomn, get_intring, get_nrings, get_num_coords, geos_typeid, get_dims, get_extring, get_geomn, get_intring, get_nrings,
get_num_geoms, get_num_coords, get_num_geoms,
) )
from django.contrib.gis.geos.prototypes.misc import * # NOQA from django.contrib.gis.geos.prototypes.misc import * # NOQA
from django.contrib.gis.geos.prototypes.predicates import ( # NOQA from django.contrib.gis.geos.prototypes.predicates import ( # NOQA

View File

@ -44,6 +44,7 @@ class StringFromGeom(GEOSFuncFactory):
# ### ctypes prototypes ### # ### ctypes prototypes ###
# The GEOS geometry type, typeid, num_coordinates and number of geometries # The GEOS geometry type, typeid, num_coordinates and number of geometries
geos_makevalid = GeomOutput('GEOSMakeValid', argtypes=[GEOM_PTR])
geos_normalize = IntFromGeom('GEOSNormalize') geos_normalize = IntFromGeom('GEOSNormalize')
geos_type = StringFromGeom('GEOSGeomType') geos_type = StringFromGeom('GEOSGeomType')
geos_typeid = IntFromGeom('GEOSGeomTypeId') geos_typeid = IntFromGeom('GEOSGeomTypeId')

View File

@ -655,6 +655,16 @@ Other Properties & Methods
doesn't impose any constraints on the geometry's SRID if called with a doesn't impose any constraints on the geometry's SRID if called with a
:class:`~django.contrib.gis.gdal.CoordTransform` object. :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() .. method:: GEOSGeometry.normalize()
Converts this geometry to canonical form:: Converts this geometry to canonical form::

View File

@ -53,7 +53,8 @@ Minor features
:mod:`django.contrib.gis` :mod:`django.contrib.gis`
~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~
* ... * The new :meth:`.GEOSGeometry.make_valid()` method allows converting invalid
geometries to valid ones.
:mod:`django.contrib.messages` :mod:`django.contrib.messages`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -1429,6 +1429,25 @@ class GEOSTest(SimpleTestCase, TestDataMixin):
self.assertIsNone(g.normalize()) self.assertIsNone(g.normalize())
self.assertTrue(g.equals_exact(MultiPoint(Point(2, 2), Point(1, 1), Point(0, 0)))) 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): def test_empty_point(self):
p = Point(srid=4326) p = Point(srid=4326)
self.assertEqual(p.ogr.ewkt, p.ewkt) self.assertEqual(p.ogr.ewkt, p.ewkt)