Repository: Toblerity/Shapely
Pull request #732 (2019-06-15 06:00:30)

Construct Polygon from Points
Fix for https://github.com/Toblerity/Shapely/issues/706

coveralls: 
[![Coverage Status](https://coveralls.io/builds/24012708/badge)](https://coveralls.io/builds/24012708)

Coverage decreased (-0.7%) to 80.085% when pulling **933e43e5e89176e000aeb08cfae75c8d8cdb0819 on drnextgis:gh706-polygon-from-points** into **57286ee5a34c9ab15e60030c73ea33d40e62828a on Toblerity:master**.


Patch:
diff --git a/shapely/geometry/polygon.py b/shapely/geometry/polygon.py
index 24a02b0..b1ba4e5 100644
--- a/shapely/geometry/polygon.py
+++ b/shapely/geometry/polygon.py
@@ -14,6 +14,7 @@ from shapely.algorithms.cga import signed_area
 from shapely.geos import lgeos
 from shapely.geometry.base import BaseGeometry, geos_geom_from_py
 from shapely.geometry.linestring import LineString, LineStringAdapter
+from shapely.geometry.point import Point
 from shapely.geometry.proxy import PolygonProxy
 
 __all__ = ['Polygon', 'asPolygon', 'LinearRing', 'asLinearRing']
@@ -32,7 +33,8 @@ class LinearRing(LineString):
         Parameters
         ----------
         coordinates : sequence
-            A sequence of (x, y [,z]) numeric coordinate pairs or triples
+            A sequence of (x, y [,z]) numeric coordinate pairs or triples.
+            Also can be a sequence of Point objects.
 
         Rings are implicitly closed. There is no need to specific a final
         coordinate pair identical to the first.
@@ -219,7 +221,8 @@ class Polygon(BaseGeometry):
         Parameters
         ----------
         shell : sequence
-            A sequence of (x, y [,z]) numeric coordinate pairs or triples
+            A sequence of (x, y [,z]) numeric coordinate pairs or triples.
+            Also can be a sequence of Point objects.
         holes : sequence
             A sequence of objects which satisfy the same requirements as the
             shell parameters above
@@ -245,7 +248,7 @@ class Polygon(BaseGeometry):
     @property
     def exterior(self):
         if self.is_empty:
-            return None
+            return LinearRing()
         elif self._exterior is None or self._exterior() is None:
             g = lgeos.GEOSGetExteriorRing(self._geom)
             ring = LinearRing()
@@ -311,7 +314,7 @@ class Polygon(BaseGeometry):
 
     @property
     def __geo_interface__(self):
-        if not self.exterior:
+        if self.exterior == LinearRing():
             coords = []
         else:
             coords = [tuple(self.exterior.coords)]
@@ -426,14 +429,24 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
     if m == 0:
         return None
 
-    n = len(ob[0])
+    def _coords(o):
+        if isinstance(o, Point):
+            return o.coords[0]
+        else:
+            return o
+
+    n = len(_coords(ob[0]))
     if m < 3:
         raise ValueError(
             "A LinearRing must have at least 3 coordinate tuples")
     assert (n == 2 or n == 3)
 
     # Add closing coordinates if not provided
-    if m == 3 or ob[0][0] != ob[-1][0] or ob[0][1] != ob[-1][1]:
+    if (
+        m == 3
+        or _coords(ob[0])[0] != _coords(ob[-1])[0]
+        or _coords(ob[0])[1] != _coords(ob[-1])[1]
+    ):
         M = m + 1
     else:
         M = m
@@ -450,7 +463,7 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
 
     # add to coordinate sequence
     for i in range(m):
-        coords = ob[i]
+        coords = _coords(ob[i])
         # Because of a bug in the GEOS C API,
         # always set X before Y
         lgeos.GEOSCoordSeq_setX(cs, i, coords[0])
@@ -463,7 +476,7 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
 
     # Add closing coordinates to sequence?
     if M > m:
-        coords = ob[0]
+        coords = _coords(ob[0])
         # Because of a bug in the GEOS C API,
         # always set X before Y
         lgeos.GEOSCoordSeq_setX(cs, M-1, coords[0])
diff --git a/shapely/speedups/_speedups.pyx b/shapely/speedups/_speedups.pyx
index 29f1c90..60d0492 100644
--- a/shapely/speedups/_speedups.pyx
+++ b/shapely/speedups/_speedups.pyx
@@ -324,14 +324,24 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
         if m == 0:
             return None
 
-        n = len(ob[0])
+        def _coords(o):
+            if isinstance(o, Point):
+                return o.coords[0]
+            else:
+                return o
+
+        n = len(_coords(ob[0]))
         if m < 3:
             raise ValueError(
                 "A LinearRing must have at least 3 coordinate tuples")
         assert (n == 2 or n == 3)
 
         # Add closing coordinates if not provided
-        if m == 3 or ob[0][0] != ob[-1][0] or ob[0][1] != ob[-1][1]:
+        if (
+            m == 3
+            or _coords(ob[0])[0] != _coords(ob[-1])[0]
+            or _coords(ob[0])[1] != _coords(ob[-1])[1]
+        ):
             M = m + 1
         else:
             M = m
@@ -348,7 +358,7 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
         
         # add to coordinate sequence
         for i in xrange(m):
-            coords = ob[i]
+            coords = _coords(ob[i])
             dx = coords[0]
             dy = coords[1]
             dz = 0
@@ -364,7 +374,7 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
 
         # Add closing coordinates to sequence?
         if M > m:
-            coords = ob[0]
+            coords = _coords(ob[0])
             dx = coords[0]
             dy = coords[1]
             dz = 0


Test patch:
diff --git a/tests/test_polygon.py b/tests/test_polygon.py
index 057a3d9..2bd76f7 100644
--- a/tests/test_polygon.py
+++ b/tests/test_polygon.py
@@ -21,6 +21,9 @@ class PolygonTestCase(unittest.TestCase):
         self.assertEqual(ring.coords[0], ring.coords[-1])
         self.assertTrue(ring.is_ring)
 
+        # Ring from sequence of Points
+        self.assertEqual(LinearRing((map(Point, coords))), ring)
+
         # Coordinate modification
         ring.coords = ((0.0, 0.0), (0.0, 2.0), (2.0, 2.0), (2.0, 0.0))
         self.assertEqual(
@@ -257,6 +260,10 @@ class PolygonTestCase(unittest.TestCase):
             Polygon(coords),
             Polygon.from_bounds(xmin, ymin, xmax, ymax))
 
+    def test_empty_polygon_exterior(self):
+        p = Polygon()
+        assert p.exterior == LinearRing()
+
 
 def test_suite():
     return unittest.TestLoader().loadTestsFromTestCase(PolygonTestCase)