Fixed #29770 -- Added LinearRing.is_counterclockwise property.

This commit is contained in:
Sergey Fedoseev 2019-10-17 13:17:42 +05:00 committed by Mariusz Felisiak
parent 24e540fbd7
commit 6bbf9a20e2
8 changed files with 74 additions and 20 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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)])

View File

@ -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``
----------- -----------

View File

@ -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`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -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)')