summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSergey Fedoseev <fedoseev.sergey@gmail.com>2019-10-17 13:17:42 +0500
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2019-10-25 14:28:26 +0200
commit6bbf9a20e2c5865e01b537e8cd34dfca06621a4a (patch)
treea3e3ba68a165301704829ea18cb5a3156bba5670
parent24e540fbd71bd2b0843e751bde61ad0052a811b3 (diff)
Fixed #29770 -- Added LinearRing.is_counterclockwise property.
-rw-r--r--django/contrib/gis/db/backends/oracle/adapter.py19
-rw-r--r--django/contrib/gis/geos/coordseq.py24
-rw-r--r--django/contrib/gis/geos/linestring.py8
-rw-r--r--django/contrib/gis/geos/prototypes/__init__.py3
-rw-r--r--django/contrib/gis/geos/prototypes/coordseq.py4
-rw-r--r--docs/ref/contrib/gis/geos.txt6
-rw-r--r--docs/releases/3.1.txt2
-rw-r--r--tests/gis_tests/geos_tests/test_geos.py28
8 files changed, 74 insertions, 20 deletions
diff --git a/django/contrib/gis/db/backends/oracle/adapter.py b/django/contrib/gis/db/backends/oracle/adapter.py
index 0c491fdc8d..6b19349ed2 100644
--- a/django/contrib/gis/db/backends/oracle/adapter.py
+++ b/django/contrib/gis/db/backends/oracle/adapter.py
@@ -25,11 +25,13 @@ class OracleSpatialAdapter(WKTAdapter):
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
@@ -42,16 +44,3 @@ class OracleSpatialAdapter(WKTAdapter):
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
diff --git a/django/contrib/gis/geos/coordseq.py b/django/contrib/gis/geos/coordseq.py
index 6ea0fe9be3..0a71a2be4a 100644
--- a/django/contrib/gis/geos/coordseq.py
+++ b/django/contrib/gis/geos/coordseq.py
@@ -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
@@ -194,3 +194,23 @@ class GEOSCoordSeq(GEOSBase):
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
+ 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
diff --git a/django/contrib/gis/geos/linestring.py b/django/contrib/gis/geos/linestring.py
index 630aa0a63d..85f29319e4 100644
--- a/django/contrib/gis/geos/linestring.py
+++ b/django/contrib/gis/geos/linestring.py
@@ -176,3 +176,11 @@ class LineString(LinearGeometryMixin, GEOSGeometry):
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
diff --git a/django/contrib/gis/geos/prototypes/__init__.py b/django/contrib/gis/geos/prototypes/__init__.py
index 18eae1d31a..844ae4cda7 100644
--- a/django/contrib/gis/geos/prototypes/__init__.py
+++ b/django/contrib/gis/geos/prototypes/__init__.py
@@ -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,
diff --git a/django/contrib/gis/geos/prototypes/coordseq.py b/django/contrib/gis/geos/prototypes/coordseq.py
index d1a5ed6043..aab5d3e75e 100644
--- a/django/contrib/gis/geos/prototypes/coordseq.py
+++ b/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 (
@@ -89,3 +89,5 @@ cs_setz = CsOperation('GEOSCoordSeq_setZ')
# 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)])
diff --git a/docs/ref/contrib/gis/geos.txt b/docs/ref/contrib/gis/geos.txt
index fda1b5cc3d..536c42f74e 100644
--- a/docs/ref/contrib/gis/geos.txt
+++ b/docs/ref/contrib/gis/geos.txt
@@ -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``
-----------
diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt
index 70297b5311..0a08ce3cdf 100644
--- a/docs/releases/3.1.txt
+++ b/docs/releases/3.1.txt
@@ -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`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/tests/gis_tests/geos_tests/test_geos.py b/tests/gis_tests/geos_tests/test_geos.py
index a74eabb314..c2aca95178 100644
--- a/tests/gis_tests/geos_tests/test_geos.py
+++ b/tests/gis_tests/geos_tests/test_geos.py
@@ -5,7 +5,7 @@ import pickle
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 (
@@ -360,6 +360,32 @@ class GEOSTest(SimpleTestCase, TestDataMixin):
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)')