Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed #29770 -- Added LinearRing.is_counterclockwise property. #10403

Merged
merged 1 commit into from Oct 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 4 additions & 15 deletions django/contrib/gis/db/backends/oracle/adapter.py
Expand Up @@ -25,11 +25,13 @@ def __init__(self, geom):

def _fix_polygon(self, poly):
"""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))

for i in range(1, len(poly)):
if not self._isClockwise(poly[i]):
if poly[i].is_counterclockwise:
poly[i] = list(reversed(poly[i]))

return poly
Expand All @@ -42,16 +44,3 @@ def _fix_geometry_collection(self, coll):
for i, geom in enumerate(coll):
if isinstance(geom, Polygon):
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
24 changes: 22 additions & 2 deletions django/contrib/gis/geos/coordseq.py
Expand Up @@ -3,12 +3,12 @@
by GEOSGeometry to house the actual coordinates of the Point,
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.base import GEOSBase
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


Expand Down Expand Up @@ -194,3 +194,23 @@ def tuple(self):
if n == 1:
return get_point(0)
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
felixxm marked this conversation as resolved.
Show resolved Hide resolved
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
8 changes: 8 additions & 0 deletions django/contrib/gis/geos/linestring.py
Expand Up @@ -176,3 +176,11 @@ def z(self):
class LinearRing(LineString):
_minlength = 4
_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
3 changes: 2 additions & 1 deletion django/contrib/gis/geos/prototypes/__init__.py
Expand Up @@ -6,7 +6,8 @@

from django.contrib.gis.geos.prototypes.coordseq import ( # NOQA
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
create_collection, create_empty_polygon, create_linearring,
Expand Down
4 changes: 3 additions & 1 deletion django/contrib/gis/geos/prototypes/coordseq.py
@@ -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.prototypes.errcheck import (
Expand Down Expand Up @@ -89,3 +89,5 @@ def errcheck(result, func, cargs):
# These routines return size & dimensions.
cs_getsize = CsInt('GEOSCoordSeq_getSize')
cs_getdims = CsInt('GEOSCoordSeq_getDimensions')

cs_is_ccw = GEOSFuncFactory('GEOSCoordSeq_isCCW', restype=c_int, argtypes=[CS_PTR, POINTER(c_byte)])
6 changes: 6 additions & 0 deletions docs/ref/contrib/gis/geos.txt
Expand Up @@ -730,6 +730,12 @@ Other Properties & Methods
Notice that ``(0, 0)`` is the first and last coordinate -- if they were not
equal, an error would be raised.

.. attribute:: is_counterclockwise

.. versionadded:: 3.1

Returns whether this ``LinearRing`` is counterclockwise.

``Polygon``
-----------

Expand Down
2 changes: 2 additions & 0 deletions docs/releases/3.1.txt
Expand Up @@ -61,6 +61,8 @@ Minor features

* :lookup:`relate` lookup is now supported on MariaDB.

* Added the :attr:`.LinearRing.is_counterclockwise` property.

:mod:`django.contrib.messages`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
28 changes: 27 additions & 1 deletion tests/gis_tests/geos_tests/test_geos.py
Expand Up @@ -5,7 +5,7 @@
import random
from binascii import a2b_hex
from io import BytesIO
from unittest import mock
from unittest import mock, skipIf

from django.contrib.gis import gdal
from django.contrib.gis.geos import (
Expand Down Expand Up @@ -360,6 +360,32 @@ def test_linestring_reverse(self):
line.reverse()
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):
"Testing MultiLineString objects."
prev = fromstr('POINT(0 0)')
Expand Down