mirror of https://github.com/django/django.git
Fixed #29770 -- Added LinearRing.is_counterclockwise property.
This commit is contained in:
parent
24e540fbd7
commit
6bbf9a20e2
|
@ -25,11 +25,13 @@ class OracleSpatialAdapter(WKTAdapter):
|
||||||
|
|
||||||
def _fix_polygon(self, poly):
|
def _fix_polygon(self, poly):
|
||||||
"""Fix single polygon orientation as described in __init__()."""
|
"""Fix single polygon orientation as described in __init__()."""
|
||||||
if self._isClockwise(poly.exterior_ring):
|
if poly.empty:
|
||||||
|
return poly
|
||||||
|
if not poly.exterior_ring.is_counterclockwise:
|
||||||
poly.exterior_ring = list(reversed(poly.exterior_ring))
|
poly.exterior_ring = list(reversed(poly.exterior_ring))
|
||||||
|
|
||||||
for i in range(1, len(poly)):
|
for i in range(1, len(poly)):
|
||||||
if not self._isClockwise(poly[i]):
|
if poly[i].is_counterclockwise:
|
||||||
poly[i] = list(reversed(poly[i]))
|
poly[i] = list(reversed(poly[i]))
|
||||||
|
|
||||||
return poly
|
return poly
|
||||||
|
@ -42,16 +44,3 @@ class OracleSpatialAdapter(WKTAdapter):
|
||||||
for i, geom in enumerate(coll):
|
for i, geom in enumerate(coll):
|
||||||
if isinstance(geom, Polygon):
|
if isinstance(geom, Polygon):
|
||||||
coll[i] = self._fix_polygon(geom)
|
coll[i] = self._fix_polygon(geom)
|
||||||
|
|
||||||
def _isClockwise(self, coords):
|
|
||||||
"""
|
|
||||||
A modified shoelace algorithm to determine polygon orientation.
|
|
||||||
See https://en.wikipedia.org/wiki/Shoelace_formula.
|
|
||||||
"""
|
|
||||||
n = len(coords)
|
|
||||||
area = 0.0
|
|
||||||
for i in range(n):
|
|
||||||
j = (i + 1) % n
|
|
||||||
area += coords[i][0] * coords[j][1]
|
|
||||||
area -= coords[j][0] * coords[i][1]
|
|
||||||
return area < 0.0
|
|
||||||
|
|
|
@ -3,12 +3,12 @@
|
||||||
by GEOSGeometry to house the actual coordinates of the Point,
|
by GEOSGeometry to house the actual coordinates of the Point,
|
||||||
LineString, and LinearRing geometries.
|
LineString, and LinearRing geometries.
|
||||||
"""
|
"""
|
||||||
from ctypes import byref, c_double, c_uint
|
from ctypes import byref, c_byte, c_double, c_uint
|
||||||
|
|
||||||
from django.contrib.gis.geos import prototypes as capi
|
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.error import GEOSException
|
from django.contrib.gis.geos.error import GEOSException
|
||||||
from django.contrib.gis.geos.libgeos import CS_PTR
|
from django.contrib.gis.geos.libgeos import CS_PTR, geos_version_tuple
|
||||||
from django.contrib.gis.shortcuts import numpy
|
from django.contrib.gis.shortcuts import numpy
|
||||||
|
|
||||||
|
|
||||||
|
@ -194,3 +194,23 @@ class GEOSCoordSeq(GEOSBase):
|
||||||
if n == 1:
|
if n == 1:
|
||||||
return get_point(0)
|
return get_point(0)
|
||||||
return tuple(get_point(i) for i in range(n))
|
return tuple(get_point(i) for i in range(n))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_counterclockwise(self):
|
||||||
|
"""Return whether this coordinate sequence is counterclockwise."""
|
||||||
|
if geos_version_tuple() < (3, 7):
|
||||||
|
# A modified shoelace algorithm to determine polygon orientation.
|
||||||
|
# See https://en.wikipedia.org/wiki/Shoelace_formula.
|
||||||
|
area = 0.0
|
||||||
|
n = len(self)
|
||||||
|
for i in range(n):
|
||||||
|
j = (i + 1) % n
|
||||||
|
area += self[i][0] * self[j][1]
|
||||||
|
area -= self[j][0] * self[i][1]
|
||||||
|
return area > 0.0
|
||||||
|
ret = c_byte()
|
||||||
|
if not capi.cs_is_ccw(self.ptr, byref(ret)):
|
||||||
|
raise GEOSException(
|
||||||
|
'Error encountered in GEOS C function "%s".' % capi.cs_is_ccw.func_name
|
||||||
|
)
|
||||||
|
return ret.value == 1
|
||||||
|
|
|
@ -176,3 +176,11 @@ class LineString(LinearGeometryMixin, GEOSGeometry):
|
||||||
class LinearRing(LineString):
|
class LinearRing(LineString):
|
||||||
_minlength = 4
|
_minlength = 4
|
||||||
_init_func = capi.create_linearring
|
_init_func = capi.create_linearring
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_counterclockwise(self):
|
||||||
|
if self.empty:
|
||||||
|
raise ValueError(
|
||||||
|
'Orientation of an empty LinearRing cannot be determined.'
|
||||||
|
)
|
||||||
|
return self._cs.is_counterclockwise
|
||||||
|
|
|
@ -6,7 +6,8 @@
|
||||||
|
|
||||||
from django.contrib.gis.geos.prototypes.coordseq import ( # NOQA
|
from django.contrib.gis.geos.prototypes.coordseq import ( # NOQA
|
||||||
create_cs, cs_clone, cs_getdims, cs_getordinate, cs_getsize, cs_getx,
|
create_cs, cs_clone, cs_getdims, cs_getordinate, cs_getsize, cs_getx,
|
||||||
cs_gety, cs_getz, cs_setordinate, cs_setx, cs_sety, cs_setz, get_cs,
|
cs_gety, cs_getz, cs_is_ccw, cs_setordinate, cs_setx, cs_sety, cs_setz,
|
||||||
|
get_cs,
|
||||||
)
|
)
|
||||||
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,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from ctypes import POINTER, c_double, c_int, c_uint
|
from ctypes import POINTER, c_byte, c_double, c_int, c_uint
|
||||||
|
|
||||||
from django.contrib.gis.geos.libgeos import CS_PTR, GEOM_PTR, GEOSFuncFactory
|
from django.contrib.gis.geos.libgeos import CS_PTR, GEOM_PTR, GEOSFuncFactory
|
||||||
from django.contrib.gis.geos.prototypes.errcheck import (
|
from django.contrib.gis.geos.prototypes.errcheck import (
|
||||||
|
@ -89,3 +89,5 @@ cs_setz = CsOperation('GEOSCoordSeq_setZ')
|
||||||
# These routines return size & dimensions.
|
# These routines return size & dimensions.
|
||||||
cs_getsize = CsInt('GEOSCoordSeq_getSize')
|
cs_getsize = CsInt('GEOSCoordSeq_getSize')
|
||||||
cs_getdims = CsInt('GEOSCoordSeq_getDimensions')
|
cs_getdims = CsInt('GEOSCoordSeq_getDimensions')
|
||||||
|
|
||||||
|
cs_is_ccw = GEOSFuncFactory('GEOSCoordSeq_isCCW', restype=c_int, argtypes=[CS_PTR, POINTER(c_byte)])
|
||||||
|
|
|
@ -730,6 +730,12 @@ Other Properties & Methods
|
||||||
Notice that ``(0, 0)`` is the first and last coordinate -- if they were not
|
Notice that ``(0, 0)`` is the first and last coordinate -- if they were not
|
||||||
equal, an error would be raised.
|
equal, an error would be raised.
|
||||||
|
|
||||||
|
.. attribute:: is_counterclockwise
|
||||||
|
|
||||||
|
.. versionadded:: 3.1
|
||||||
|
|
||||||
|
Returns whether this ``LinearRing`` is counterclockwise.
|
||||||
|
|
||||||
``Polygon``
|
``Polygon``
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
|
|
|
@ -61,6 +61,8 @@ Minor features
|
||||||
|
|
||||||
* :lookup:`relate` lookup is now supported on MariaDB.
|
* :lookup:`relate` lookup is now supported on MariaDB.
|
||||||
|
|
||||||
|
* Added the :attr:`.LinearRing.is_counterclockwise` property.
|
||||||
|
|
||||||
:mod:`django.contrib.messages`
|
:mod:`django.contrib.messages`
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import pickle
|
||||||
import random
|
import random
|
||||||
from binascii import a2b_hex
|
from binascii import a2b_hex
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from unittest import mock
|
from unittest import mock, skipIf
|
||||||
|
|
||||||
from django.contrib.gis import gdal
|
from django.contrib.gis import gdal
|
||||||
from django.contrib.gis.geos import (
|
from django.contrib.gis.geos import (
|
||||||
|
@ -360,6 +360,32 @@ class GEOSTest(SimpleTestCase, TestDataMixin):
|
||||||
line.reverse()
|
line.reverse()
|
||||||
self.assertEqual(line.ewkt, 'SRID=4326;LINESTRING (151.2607 -33.887, 144.963 -37.8143)')
|
self.assertEqual(line.ewkt, 'SRID=4326;LINESTRING (151.2607 -33.887, 144.963 -37.8143)')
|
||||||
|
|
||||||
|
def _test_is_counterclockwise(self):
|
||||||
|
lr = LinearRing((0, 0), (1, 0), (0, 1), (0, 0))
|
||||||
|
self.assertIs(lr.is_counterclockwise, True)
|
||||||
|
lr.reverse()
|
||||||
|
self.assertIs(lr.is_counterclockwise, False)
|
||||||
|
msg = 'Orientation of an empty LinearRing cannot be determined.'
|
||||||
|
with self.assertRaisesMessage(ValueError, msg):
|
||||||
|
LinearRing().is_counterclockwise
|
||||||
|
|
||||||
|
@skipIf(geos_version_tuple() < (3, 7), 'GEOS >= 3.7.0 is required')
|
||||||
|
def test_is_counterclockwise(self):
|
||||||
|
self._test_is_counterclockwise()
|
||||||
|
|
||||||
|
@skipIf(geos_version_tuple() < (3, 7), 'GEOS >= 3.7.0 is required')
|
||||||
|
def test_is_counterclockwise_geos_error(self):
|
||||||
|
with mock.patch('django.contrib.gis.geos.prototypes.cs_is_ccw') as mocked:
|
||||||
|
mocked.return_value = 0
|
||||||
|
mocked.func_name = 'GEOSCoordSeq_isCCW'
|
||||||
|
msg = 'Error encountered in GEOS C function "GEOSCoordSeq_isCCW".'
|
||||||
|
with self.assertRaisesMessage(GEOSException, msg):
|
||||||
|
LinearRing((0, 0), (1, 0), (0, 1), (0, 0)).is_counterclockwise
|
||||||
|
|
||||||
|
@mock.patch('django.contrib.gis.geos.libgeos.geos_version', lambda: b'3.6.9')
|
||||||
|
def test_is_counterclockwise_fallback(self):
|
||||||
|
self._test_is_counterclockwise()
|
||||||
|
|
||||||
def test_multilinestring(self):
|
def test_multilinestring(self):
|
||||||
"Testing MultiLineString objects."
|
"Testing MultiLineString objects."
|
||||||
prev = fromstr('POINT(0 0)')
|
prev = fromstr('POINT(0 0)')
|
||||||
|
|
Loading…
Reference in New Issue