Repository: Toblerity/Shapely
Pull request #733 (2019-06-16 14:33:19)

Add support of MultiPolygon and GeometryCollection to 'orient' function
Fix for https://github.com/Toblerity/Shapely/issues/635.

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

Coverage increased (+0.05%) to 80.836% when pulling **7d79fd35b10f2327544a7c0983001105053445c3 on drnextgis:gh635-orient** into **322daf7463f069312f71508902e6aaf9df915d94 on Toblerity:master**.

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

Coverage increased (+0.05%) to 80.836% when pulling **7d79fd35b10f2327544a7c0983001105053445c3 on drnextgis:gh635-orient** into **322daf7463f069312f71508902e6aaf9df915d94 on Toblerity:master**.


Patch:
diff --git a/shapely/ops.py b/shapely/ops.py
index dd669b3..718c811 100644
--- a/shapely/ops.py
+++ b/shapely/ops.py
@@ -11,9 +11,10 @@ else:
 from ctypes import byref, c_void_p, c_double
 
 from shapely.geos import lgeos
-from shapely.geometry.base import geom_factory, BaseGeometry
+from shapely.geometry.base import geom_factory, BaseGeometry, BaseMultipartGeometry
 from shapely.geometry import asShape, asLineString, asMultiLineString, Point, MultiPoint,\
                              LineString, MultiLineString, Polygon, GeometryCollection
+from shapely.geometry.polygon import orient as orient_
 from shapely.algorithms.polylabel import polylabel
 
 
@@ -567,3 +568,18 @@ def clip_by_rect(geom, xmin, ymin, xmax, ymax):
         return geom
     result = geom_factory(lgeos.methods['clip_by_rect'](geom._geom, xmin, ymin, xmax, ymax))
     return result
+
+
+def orient(geom, sign=1.0):
+    if isinstance(geom, BaseMultipartGeometry):
+        return geom.__class__(
+            list(
+                map(
+                    lambda geom: orient(geom, sign),
+                    geom.geoms,
+                )
+            )
+        )
+    if isinstance(geom, (Polygon,)):
+        return orient_(geom, sign)
+    return geom


Test patch:
diff --git a/tests/test_orient.py b/tests/test_orient.py
new file mode 100644
index 0000000..ff76534
--- /dev/null
+++ b/tests/test_orient.py
@@ -0,0 +1,82 @@
+from . import unittest
+from shapely.geometry import (
+    Point,
+    MultiPoint,
+    LineString,
+    MultiLineString,
+    LinearRing,
+    Polygon,
+    MultiPolygon,
+    GeometryCollection,
+)
+
+from shapely.ops import orient
+
+
+class OrientTestCase(unittest.TestCase):
+    def test_point(self):
+        point = Point(0, 0)
+        assert orient(point, 1) == point
+        assert orient(point, -1) == point
+
+    def test_multipoint(self):
+        multipoint = MultiPoint([(0, 0), (1, 1)])
+        assert orient(multipoint, 1) == multipoint
+        assert orient(multipoint, -1) == multipoint
+
+    def test_linestring(self):
+        linestring = LineString(((0, 0), (1, 1)))
+        assert orient(linestring, 1) == linestring
+        assert orient(linestring, -1) == linestring
+
+    def test_multilinestring(self):
+        multilinestring = MultiLineString(
+            [[(0, 0), (1, 1)], [(1, 0), (0, 1)]]
+        )
+        assert orient(multilinestring, 1) == multilinestring
+        assert orient(multilinestring, -1) == multilinestring
+
+    def test_linearring(self):
+        linearring = LinearRing([(0, 0), (0, 1), (1, 0)])
+        assert orient(linearring, 1) == linearring
+        assert orient(linearring, -1) == linearring
+
+    def test_polygon(self):
+        polygon = Polygon([(0, 0), (0, 1), (1, 0)])
+        polygon_reversed = Polygon(
+            polygon.exterior.coords[::-1]
+        )
+        assert (orient(polygon, 1)) == polygon_reversed
+        assert (orient(polygon, -1)) == polygon
+
+    def test_multipolygon(self):
+        polygon1 = Polygon([(0, 0), (0, 1), (1, 0)])
+        polygon2 = Polygon([(1, 0), (2, 0), (2, 1)])
+        polygon1_reversed = Polygon(
+            polygon1.exterior.coords[::-1]
+        )
+        polygon2_reversed = Polygon(
+            polygon2.exterior.coords[::-1]
+        )
+        multipolygon = MultiPolygon([polygon1, polygon2])
+        assert not polygon1.exterior.is_ccw
+        assert polygon2.exterior.is_ccw
+        assert orient(multipolygon, 1) == MultiPolygon(
+            [polygon1_reversed, polygon2]
+        )
+        assert orient(multipolygon, -1) == MultiPolygon(
+            [polygon1, polygon2_reversed]
+        )
+
+    def test_geometrycollection(self):
+        polygon = Polygon([(0, 0), (0, 1), (1, 0)])
+        polygon_reversed = Polygon(
+            polygon.exterior.coords[::-1]
+        )
+        collection = GeometryCollection([polygon])
+        assert orient(collection, 1) == GeometryCollection(
+            [polygon_reversed]
+        )
+        assert orient(collection, -1) == GeometryCollection(
+            [polygon]
+        )