Repository: Toblerity/Shapely
Pull request #365 (2016-02-18 00:08:45)

Implemented GEOSSharedPaths.
This PR implements the GEOSSharedPaths operation, which arrived in GEOS 3.3.x.

The function finds shared parts between two lineal geometries.

I've implemented this as `shapely.ops.shared_paths` rather than `LineString.shared_paths`. An argument could be made for either I guess.


Patch:
diff --git a/docs/manual.rst b/docs/manual.rst
index 7aa5ad4..902368f 100644
--- a/docs/manual.rst
+++ b/docs/manual.rst
@@ -2080,6 +2080,35 @@ one geometry to the vertices in a second geometry with a given tolerance.
   >>> result.wkt
   'LINESTRING (0 0, 1 1, 2 1, 2.6 0.5)'
 
+Shared paths
+------------
+
+The :func:`~shapely.ops.shared_paths` function in `shapely.ops` finds the shared
+paths between two lineal geometries.
+
+.. function:: shapely.ops.shared_paths(geom1, geom2)
+
+   Finds the shared paths between `geom1` and `geom2`, where both geometries
+   are `LineStrings`.
+   
+   A `GeometryCollection` is returned with two elements. The first element is a
+   `MultiLineString` containing shared paths with the same direction for both
+   inputs. The second element is a MultiLineString containing shared paths with
+   the opposite direction for the two inputs.
+   
+   `New in version 1.6.0`
+
+.. code-block:: pycon
+
+  >>> from shapely.ops import shared_paths
+  >>> g1 = LineString([(0, 0), (10, 0), (10, 5), (20, 5)])
+  >>> g2 = LineString([(5, 0), (30, 0), (30, 5), (0, 5)])
+  >>> forward, backward = shared_paths(g1, g2)
+  >>> forward.wkt
+  'MULTILINESTRING ((5 0, 10 0))'
+  >>> backward.wkt
+  'MULTILINESTRING ((10 5, 20 5))'
+
 Prepared Geometry Operations
 ----------------------------
 
diff --git a/shapely/ctypes_declarations.py b/shapely/ctypes_declarations.py
index ac38fc5..e5f1261 100644
--- a/shapely/ctypes_declarations.py
+++ b/shapely/ctypes_declarations.py
@@ -501,6 +501,9 @@ def prototype(lgeos, geos_version):
         lgeos.GEOSSnap.restype = c_void_p
         lgeos.GEOSSnap.argtypes = [c_void_p, c_void_p, c_double]
 
+        lgeos.GEOSSharedPaths.restype = c_void_p
+        lgeos.GEOSSharedPaths.argtypes = [c_void_p, c_void_p]
+
     if geos_version >= (3, 4, 0):
         lgeos.GEOSNearestPoints.restype = c_void_p
         lgeos.GEOSNearestPoints.argtypes = [c_void_p, c_void_p]
diff --git a/shapely/geos.py b/shapely/geos.py
index c767d72..42afcea 100644
--- a/shapely/geos.py
+++ b/shapely/geos.py
@@ -683,6 +683,7 @@ class LGEOS330(LGEOS320):
         self.methods['is_closed'] = self.GEOSisClosed
         self.methods['cascaded_union'] = self.methods['unary_union']
         self.methods['snap'] = self.GEOSSnap
+        self.methods['shared_paths'] = self.GEOSSharedPaths
 
 
 class LGEOS340(LGEOS330):
diff --git a/shapely/ops.py b/shapely/ops.py
index b1aff95..c27b9d4 100644
--- a/shapely/ops.py
+++ b/shapely/ops.py
@@ -12,7 +12,8 @@ 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 import asShape, asLineString, asMultiLineString, Point
+from shapely.geometry import asShape, asLineString, asMultiLineString, Point, \
+                             LineString
 
 __all__ = ['cascaded_union', 'linemerge', 'operator', 'polygonize',
            'polygonize_full', 'transform', 'unary_union', 'triangulate']
@@ -301,3 +302,25 @@ def snap(g1, g2, tolerance):
     'LINESTRING (0 0, 1 1, 2 1, 2.6 0.5)'
     """
     return(geom_factory(lgeos.methods['snap'](g1._geom, g2._geom, tolerance)))
+
+def shared_paths(g1, g2):
+    """Find paths shared between the two given lineal geometries
+
+    Returns a GeometryCollection with two elements:
+     - First element is a MultiLineString containing shared paths with the
+       same direction for both inputs.
+     - Second element is a MultiLineString containing shared paths with the
+       opposite direction for the two inputs.
+
+    Parameters
+    ----------
+    g1 : geometry
+        The first geometry
+    g2 : geometry
+        The second geometry
+    """
+    if not isinstance(g1, LineString):
+        raise TypeError("First geometry must be a LineString")
+    if not isinstance(g2, LineString):
+        raise TypeError("Second geometry must be a LineString")
+    return(geom_factory(lgeos.methods['shared_paths'](g1._geom, g2._geom)))


Test patch:
diff --git a/tests/test_shared_paths.py b/tests/test_shared_paths.py
new file mode 100644
index 0000000..7ab2d54
--- /dev/null
+++ b/tests/test_shared_paths.py
@@ -0,0 +1,50 @@
+from . import unittest
+
+from shapely.geometry import Point, LineString, Polygon, MultiLineString, \
+                             GeometryCollection
+from shapely.geos import geos_version
+from shapely.ops import shared_paths
+
+@unittest.skipIf(geos_version < (3, 3, 0), 'GEOS 3.3.0 required')
+class SharedPaths(unittest.TestCase):
+    def test_shared_paths_forward(self):
+        g1 = LineString([(0, 0), (10, 0), (10, 5), (20, 5)])
+        g2 = LineString([(5, 0), (15, 0)])
+        result = shared_paths(g1, g2)
+        
+        self.assertTrue(isinstance(result, GeometryCollection))
+        self.assertTrue(len(result) == 2)
+        a, b = result
+        self.assertTrue(isinstance(a, MultiLineString))
+        self.assertTrue(len(a) == 1)
+        self.assertEqual(a[0].coords[:], [(5, 0), (10, 0)])
+        self.assertTrue(b.is_empty)
+
+    def test_shared_paths_forward(self):
+        g1 = LineString([(0, 0), (10, 0), (10, 5), (20, 5)])
+        g2 = LineString([(15, 0), (5, 0)])
+        result = shared_paths(g1, g2)
+        
+        self.assertTrue(isinstance(result, GeometryCollection))
+        self.assertTrue(len(result) == 2)
+        a, b = result
+        self.assertTrue(isinstance(b, MultiLineString))
+        self.assertTrue(len(b) == 1)
+        self.assertEqual(b[0].coords[:], [(5, 0), (10, 0)])
+        self.assertTrue(a.is_empty)
+    
+    def test_wrong_type(self):
+        g1 = Point(0, 0)
+        g2 = LineString([(5, 0), (15, 0)])
+        
+        with self.assertRaises(TypeError):
+            result = shared_paths(g1, g2)
+            
+        with self.assertRaises(TypeError):
+            result = shared_paths(g2, g1)
+
+def test_suite():
+    return unittest.TestLoader().loadTestsFromTestCase(SharedPaths)
+
+if __name__ == '__main__':
+    unittest.main()