Repository: Toblerity/Shapely
Pull request #304 (2015-08-20 16:27:10)

Relate pattern
See #296.


Patch:
diff --git a/CHANGES.txt b/CHANGES.txt
index 4a635c0..93f3772 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,10 +1,6 @@
 Changes
 =======
 
-1.5.10 (2015-??-??)
--------------------
-- Monkey patch affinity module by absolute reference (#299).
-
 1.5.9 (2015-05-27)
 ------------------
 - Fix for 64 bit speedups compatibility (#274).
diff --git a/docs/manual.rst b/docs/manual.rst
index e8956da..07971b8 100644
--- a/docs/manual.rst
+++ b/docs/manual.rst
@@ -1235,6 +1235,37 @@ elements.
   >>> Point(0, 0).relate(LineString([(0, 0), (1, 1)]))
   'F0FFFF102'
 
+.. method:: object.relate_pattern(other, pattern)
+
+    Returns True if the DE-9IM string code for the relationship between the
+    geometries satisfies the pattern, otherwise False.
+
+The :meth:`relate_pattern` compares the DE-9IM code string for two geometries
+against a specified pattern. If the string matches the pattern then ``True`` is
+returned, otherwise ``False``. The pattern specified can be an exact match
+(``0``, ``1`` or ``2``), a boolean match (``T`` or ``F``), or a wildcard
+(``*``). For example, the pattern for the `within` predicate is ``T*****FF*``.
+
+.. code-block:: pycon
+
+  >> point = Point(0.5, 0.5)
+  >> square = Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])
+  >> square.relate_pattern(point, 'T*****FF*')
+  True
+  >> point.within(square)
+  True
+
+Note that the order or the geometries is significant, as demonstrated below.
+In this example the square contains the point, but the point does not contain
+the square.
+
+.. code-block:: pycon
+
+  >>> point.relate(square)
+  '0FFFFF212'
+  >>> square.relate(point)
+  '0F2FF1FF2'
+
 Further discussion of the DE-9IM matrix is beyond the scope of this manual. See
 [4]_ and http://pypi.python.org/pypi/de9im.
 
diff --git a/shapely/__init__.py b/shapely/__init__.py
index 63d18fb..85ee20b 100644
--- a/shapely/__init__.py
+++ b/shapely/__init__.py
@@ -1,1 +1,1 @@
-__version__ = "1.5.10"
+__version__ = "1.6rc1"
diff --git a/shapely/ctypes_declarations.py b/shapely/ctypes_declarations.py
index 9baa480..ac38fc5 100644
--- a/shapely/ctypes_declarations.py
+++ b/shapely/ctypes_declarations.py
@@ -298,12 +298,16 @@ def prototype(lgeos, geos_version):
     Dimensionally Extended 9 Intersection Model related
     '''
 
-    lgeos.GEOSRelatePattern.restype = c_char
-    lgeos.GEOSRelatePattern.argtypes = [c_void_p, c_void_p, c_char_p]
-
     lgeos.GEOSRelate.restype = allocated_c_char_p
     lgeos.GEOSRelate.argtypes = [c_void_p, c_void_p]
 
+    lgeos.GEOSRelatePattern.restype = c_byte
+    lgeos.GEOSRelatePattern.argtypes = [c_void_p, c_void_p, c_char_p]
+
+    if geos_version >= (3, 3, 0):
+        lgeos.GEOSRelatePatternMatch.restype = c_byte
+        lgeos.GEOSRelatePatternMatch.argtypes = [c_char_p, c_char_p]
+
     '''
     Prepared Geometry Binary predicates
     Return 2 on exception, 1 on true, 0 on false
diff --git a/shapely/geometry/base.py b/shapely/geometry/base.py
index bd9a81d..d9450bd 100644
--- a/shapely/geometry/base.py
+++ b/shapely/geometry/base.py
@@ -537,7 +537,7 @@ class BaseGeometry(object):
 
     @delegated
     def simplify(self, tolerance, preserve_topology=True):
-        """Returns a simplified geometry produced by the Douglas-Peucker
+        """Returns a simplified geometry produced by the Douglas-Puecker
         algorithm
 
         Coordinates of the simplified geometry will be no more than the
@@ -672,6 +672,12 @@ class BaseGeometry(object):
         specified decimal place"""
         return self.equals_exact(other, 0.5 * 10**(-decimal))
 
+    def relate_pattern(self, other, pattern):
+        """Returns True if the DE-9IM string code for the relationship between
+        the geometries satisfies the pattern, else False"""
+        pattern = c_char_p(pattern.encode('ascii'))
+        return bool(self.impl['relate_pattern'](self, other, pattern))
+
     # Linear referencing
     # ------------------
 
diff --git a/shapely/geos.py b/shapely/geos.py
index 27b4eaa..f2749b7 100644
--- a/shapely/geos.py
+++ b/shapely/geos.py
@@ -468,6 +468,7 @@ class LGEOS300(LGEOSBase):
                 self.GEOSOverlaps,
                 self.GEOSEquals,
                 self.GEOSEqualsExact,
+                self.GEOSRelatePattern,
                 self.GEOSisEmpty,
                 self.GEOSisValid,
                 self.GEOSisSimple,
@@ -503,6 +504,7 @@ class LGEOS300(LGEOSBase):
         self.methods['symmetric_difference'] = self.GEOSSymDifference
         self.methods['union'] = self.GEOSUnion
         self.methods['intersection'] = self.GEOSIntersection
+        self.methods['relate_pattern'] = self.GEOSRelatePattern
         self.methods['simplify'] = self.GEOSSimplify
         self.methods['topology_preserve_simplify'] = \
             self.GEOSTopologyPreserveSimplify
@@ -553,6 +555,7 @@ class LGEOS310(LGEOSBase):
                 self.GEOSPreparedContainsProperly,
                 self.GEOSPreparedCovers,
                 self.GEOSPreparedIntersects,
+                self.GEOSRelatePattern,
                 self.GEOSisEmpty,
                 self.GEOSisValid,
                 self.GEOSisSimple,
@@ -601,6 +604,7 @@ class LGEOS310(LGEOSBase):
             self.GEOSPreparedContainsProperly
         self.methods['prepared_overlaps'] = self.GEOSPreparedOverlaps
         self.methods['prepared_covers'] = self.GEOSPreparedCovers
+        self.methods['relate_pattern'] = self.GEOSRelatePattern
         self.methods['simplify'] = self.GEOSSimplify
         self.methods['topology_preserve_simplify'] = \
             self.GEOSTopologyPreserveSimplify
diff --git a/shapely/impl.py b/shapely/impl.py
index 6d8c134..828e0ae 100644
--- a/shapely/impl.py
+++ b/shapely/impl.py
@@ -108,6 +108,7 @@ IMPL300 = {
     'within': (BinaryPredicate, 'within'),
     'covers': (BinaryPredicate, 'covers'),
     'equals_exact': (BinaryPredicate, 'equals_exact'),
+    'relate_pattern': (BinaryPredicate, 'relate_pattern'),
 
     # First pure Python implementation
     'is_ccw': (cga.is_ccw_impl, 'is_ccw'),
diff --git a/shapely/speedups/__init__.py b/shapely/speedups/__init__.py
index 2e84a1f..60d9d0b 100644
--- a/shapely/speedups/__init__.py
+++ b/shapely/speedups/__init__.py
@@ -58,5 +58,5 @@ def disable():
     coords.CoordinateSequence.__iter__ = _orig['CoordinateSequence.__iter__']
     linestring.geos_linestring_from_py = _orig['geos_linestring_from_py']
     polygon.geos_linearring_from_py = _orig['geos_linearring_from_py']
-    shapely.affinity.affine_transform = _orig['affine_transform']
+    affinity.affine_transform = _orig['affine_transform']
     _orig.clear()


Test patch:
diff --git a/tests/test_predicates.py b/tests/test_predicates.py
index 3062eac..c3c1cb4 100644
--- a/tests/test_predicates.py
+++ b/tests/test_predicates.py
@@ -2,8 +2,8 @@
 """
 from . import unittest
 from shapely.geometry import Point, Polygon
-from shapely.geos import TopologicalError
-
+from shapely.geos import TopologicalError, PredicateError
+import pytest
 
 class PredicatesTestCase(unittest.TestCase):
 
@@ -41,6 +41,25 @@ class PredicatesTestCase(unittest.TestCase):
               (399, 450), (339, 207)]
         self.assertRaises(TopologicalError, Polygon(p1).within, Polygon(p2))
 
+    def test_relate_pattern(self):
+
+        # a pair of partially overlapping polygons, and a nearby point
+        g1 = Polygon([(0, 0), (0, 1), (3, 1), (3, 0), (0, 0)])
+        g2 = Polygon([(1, -1), (1, 2), (2, 2), (2, -1), (1, -1)])
+        g3 = Point(5, 5)
+
+        assert(g1.relate(g2) == '212101212')
+        assert(g1.relate_pattern(g2, '212101212'))
+        assert(g1.relate_pattern(g2, '*********'))
+        assert(g1.relate_pattern(g2, '2********'))
+        assert(g1.relate_pattern(g2, 'T********'))
+        assert(not g1.relate_pattern(g2, '112101212'))
+        assert(not g1.relate_pattern(g2, '1********'))
+        assert(g1.relate_pattern(g3, 'FF2FF10F2'))
+
+        # an invalid pattern should raise an exception
+        with pytest.raises(PredicateError):
+            g1.relate_pattern(g2, 'fail')
 
 def test_suite():
     return unittest.TestLoader().loadTestsFromTestCase(PredicatesTestCase)