Repository: Toblerity/Shapely
Pull request #1008 (2020-10-25 20:18:50)

Substring: negative values and direction issues + more robust testing
I set out to try some performance enhancements out on `ops.substring` and ran into some behavior issues. Here are two examples that capture the issues I found: 

```python
from shapely.geometry import LineString
from shapely.ops import substring

ls = LineString([(i, i) for i in range(1000)])
print(substring(ls, -3, 3))
print(substring(ls, -1.1, 0.002, True))

# LINESTRING (996.8786796564403 996.8786796564403, 0 0, 1 1, 2 2, 2.121320343559642 2.121320343559642)
# LINESTRING (0 0, 0 0, 1 1, 1.998000000000014 1.998000000000014)
```
Just as a reminder from the docs of what behavior we're expecting here: 

> Negative distance values are taken as measured in the reverse direction from the end of the geometry. Out-of-range index values are handled by clamping them to the valid range of values.
>
> If the start distance equals the end distance, a point is being returned.
>
> If the normalized arg is True, the distance will be interpreted as a fraction of the geometry’s length.


Two things bother me about the first example. First and most importantly, it returned the wrong cut. It got the start and end correct, but the vertices in the middle are missing. Instead there are the vertices from the beginning of the LineString to the start of the cut. The second issue is the order. 

In the second example, the only issue is that the first vertex is duplicated. Probably not a big deal, but I didn't like it. So I fixed it. 

The test suit would have clued us in to the problem with the duplicate points if it wasn't using `.equals` to compare results. This returns true for reversed LineStrings and ones with more points than the other as long as those points are along the line. Since order matters in this case, I changed the tests to compare wkt instead. The failed tests are also nicer to read like this because the string diff is shown. 

I also added some more tests that cover the issues in the first example. 

Edits and comments are welcome! And take your time, I know things are busy around here these days. : )

Is this a good branch to point to?

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

Coverage increased (+0.03%) to 83.411% when pulling **f02e0d50f0087a096e89b874ae4157a549bd891e on Jeremiah-England:substrings** into **eae4dcbedb0756f658f9982825d5fa23b0061b2a on Toblerity:master**.


Patch:
diff --git a/docs/conf.py b/docs/conf.py
index 52e7ae9..dd3ec47 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -25,7 +25,7 @@ import shapely
 
 # For pyplots in code/, load functions here first, so they are visible
 from shapely import geometry, affinity, wkt, wkb
-from shapely.ops import cascaded_union
+from shapely.ops import unary_union
 
 # -- General configuration -----------------------------------------------------
 
diff --git a/docs/manual.rst b/docs/manual.rst
index 68a94f0..239ac66 100644
--- a/docs/manual.rst
+++ b/docs/manual.rst
@@ -137,7 +137,7 @@ described in a following section of the manual.
 Coordinate Systems
 ------------------
 
-Even though the Earth is not flat – and for that matter not exactly spherical –
+Even though the Earth is not flat – and for that matter not exactly spherical –
 there are many analytic problems that can be approached by transforming Earth
 features to a Cartesian plane, applying tried and true algorithms, and then
 transforming the results back to geographic coordinates.  This practice is as
@@ -2220,10 +2220,8 @@ efficient than accumulating with :meth:`union`.
 
   .. note::
 
-     In 1.2.16 :func:`shapely.ops.cascaded_union` was transparently superseded by
-     :func:`shapely.ops.unary_union` if GEOS 3.3+ is used. The unary union
-     function can operate on different geometry types, not only polygons as is
-     the case for the older cascaded union.
+     In 1.8.0 :func:`shapely.ops.cascaded_union` is deprecated, as it was
+     superseded by :func:`shapely.ops.unary_union`.
 
 Delaunay triangulation
 ----------------------
@@ -2358,7 +2356,7 @@ Shared paths
 ------------
 
 The :func:`~shapely.ops.shared_paths` function in `shapely.ops` finds the shared
-paths between two lineal geometries.
+paths between two linear geometries.
 
 .. function:: shapely.ops.shared_paths(geom1, geom2)
 
@@ -2416,27 +2414,53 @@ The :func:`~shapely.ops.split` function in `shapely.ops` splits a geometry by an
 Substring
 ---------
 
-The :func:`~shapely.ops.substring` function in `shapely.ops` returns a line segment between specified distances along a linear geometry.
+The :func:`~shapely.ops.substring` function in :mod:`shapely.ops` returns a line segment
+between specified distances along a `LineString`.
 
 .. function:: shapely.ops.substring(geom, start_dist, end_dist[, normalized=False])
 
-    Negative distance values are taken as measured in the reverse
-    direction from the end of the geometry. Out-of-range index
-    values are handled by clamping them to the valid range of values.
+  Return the `LineString` between `start_dist` and `end_dist` or a `Point`
+  if they are at the same location
 
-    If the start distance equals the end distance, a point is being returned.
+  Negative distance values are taken as measured in the reverse
+  direction from the end of the geometry. Out-of-range index
+  values are handled by clamping them to the valid range of values.
 
-    If the normalized arg is True, the distance will be interpreted as a
-    fraction of the geometry's length.
+  If the start distance equals the end distance, a point is being returned.
 
-   `New in version 1.7.0`
+  If the start distance is actually past the end distance, then the
+  reversed substring is returned such that the start distance is
+  at the first coordinate.
 
-.. code-block:: pycon
+  If the normalized arg is ``True``, the distance will be interpreted as a
+  fraction of the geometry's length
 
-  >>> line = LineString(([0, 0], [2, 0]))
-  >>> result = substring(line, 0.5, 0.6)
-  >>> result.wkt
-  'LINESTRING (0.5 0, 0.6 0)'
+  `New in version 1.7.0`
+
+  Here are some examples that return `LineString` geometries.
+
+  .. code-block:: pycon
+
+    >>> from shapely.geometry import LineString
+    >>> from shapely.ops import substring
+    >>> ls = LineString((i, 0) for i in range(6))
+    >>> ls.wkt
+    'LINESTRING (0 0, 1 0, 2 0, 3 0, 4 0, 5 0)'
+    >>> substring(ls, start_dist=1, end_dist=3).wkt
+    'LINESTRING (1 0, 2 0, 3 0)'
+    >>> substring(ls, start_dist=3, end_dist=1).wkt
+    'LINESTRING (3 0, 2 0, 1 0)'
+    >>> substring(ls, start_dist=1, end_dist=-3).wkt
+    'LINESTRING (1 0, 2 0)'
+    >>> substring(ls, start_dist=0.2, end_dist=-0.6, normalized=True).wkt
+    'LINESTRING (1 0, 2 0)'
+
+  And here is an example that returns a `Point`.
+
+  .. code-block:: pycon
+
+    >>> substring(ls, start_dist=2.5, end_dist=-2.5)
+    'POINT (2.5 0)'
 
 Prepared Geometry Operations
 ----------------------------
diff --git a/shapely/geometry/base.py b/shapely/geometry/base.py
index c6ad4fa..ea8ee68 100644
--- a/shapely/geometry/base.py
+++ b/shapely/geometry/base.py
@@ -80,7 +80,7 @@ def geom_factory(g, parent=None):
         [geom_type],
         )
     ob.__class__ = getattr(mod, geom_type)
-    ob._geom = g
+    ob._set_geom(g)
     ob.__p__ = parent
     if lgeos.methods['has_z'](g):
         ob._ndim = 3
@@ -181,6 +181,13 @@ class BaseGeometry(object):
     _lgeos = lgeos
 
     def empty(self, val=EMPTY):
+        warn(
+            "The 'empty()' method is deprecated and will be removed in "
+            "Shapely 2.0",
+            ShapelyDeprecationWarning, stacklevel=2)
+        self._empty(val=val)
+
+    def _empty(self, val=EMPTY):
         if not self._other_owned and self.__geom__ and self.__geom__ != EMPTY:
             try:
                 self._lgeos.GEOSGeom_destroy(self.__geom__)
@@ -198,7 +205,7 @@ class BaseGeometry(object):
         return self.__bool__()
 
     def __del__(self):
-        self.empty(val=None)
+        self._empty(val=None)
         self.__p__ = None
 
     def __str__(self):
@@ -209,7 +216,7 @@ class BaseGeometry(object):
         return (self.__class__, (), self.wkb)
 
     def __setstate__(self, state):
-        self.empty()
+        self._empty()
         self.__geom__ = deserialize_wkb(state)
         self._is_empty = False
         if lgeos.methods['has_z'](self.__geom__):
@@ -223,7 +230,14 @@ class BaseGeometry(object):
 
     @_geom.setter
     def _geom(self, val):
-        self.empty()
+        warn(
+            "Setting the '_geom' attribute directly is deprecated, and will "
+            "no longer be possible in Shapely 2.0.",
+            ShapelyDeprecationWarning, stacklevel=2)
+        self._set_geom(val)
+
+    def _set_geom(self, val):
+        self._empty()
         self._is_empty = val in [EMPTY, None]
         self.__geom__ = val
 
@@ -944,7 +958,7 @@ class GeometrySequence(object):
     def _get_geom_item(self, i):
         g = self.shape_factory()
         g._other_owned = True
-        g._geom = lgeos.GEOSGetGeometryN(self._geom, i)
+        g._set_geom(lgeos.GEOSGetGeometryN(self._geom, i))
         g._ndim = self._ndim
         g.__p__ = self
         return g
diff --git a/shapely/geometry/collection.py b/shapely/geometry/collection.py
index 3c93247..65810b5 100644
--- a/shapely/geometry/collection.py
+++ b/shapely/geometry/collection.py
@@ -40,7 +40,9 @@ class GeometryCollection(BaseMultipartGeometry):
         if not geoms:
             pass
         else:
-            self._geom, self._ndim = geos_geometrycollection_from_py(geoms)
+            geom, n = geos_geometrycollection_from_py(geoms)
+            self._set_geom(geom)
+            self._ndim = n
 
     @property
     def __geo_interface__(self):
diff --git a/shapely/geometry/linestring.py b/shapely/geometry/linestring.py
index fcac541..9815e74 100644
--- a/shapely/geometry/linestring.py
+++ b/shapely/geometry/linestring.py
@@ -44,7 +44,9 @@ class LineString(BaseGeometry):
         if coordinates is not None:
             ret = geos_linestring_from_py(coordinates)
             if ret is not None:
-                self._geom, self._ndim = ret
+                geom, n = ret
+                self._set_geom(geom)
+                self._ndim = n
 
     @property
     def __geo_interface__(self):
@@ -110,10 +112,12 @@ class LineString(BaseGeometry):
             "Setting the 'coords' to mutate a Geometry in place is deprecated,"
             " and will not be possible any more in Shapely 2.0",
             ShapelyDeprecationWarning, stacklevel=2)
-        self.empty()
+        self._empty()
         ret = geos_linestring_from_py(coordinates)
         if ret is not None:
-            self._geom, self._ndim = ret
+            geom, n = ret
+            self._set_geom(geom)
+            self._ndim = n
 
     coords = property(BaseGeometry._get_coords, _set_coords)
 
diff --git a/shapely/geometry/multilinestring.py b/shapely/geometry/multilinestring.py
index 18071ae..94f5e2d 100644
--- a/shapely/geometry/multilinestring.py
+++ b/shapely/geometry/multilinestring.py
@@ -46,7 +46,9 @@ class MultiLineString(BaseMultipartGeometry):
             # allow creation of empty multilinestrings, to support unpickling
             pass
         else:
-            self._geom, self._ndim = geos_multilinestring_from_py(lines)
+            geom, n = geos_multilinestring_from_py(lines)
+            self._set_geom(geom)
+            self._ndim = n
 
     def shape_factory(self, *args):
         return linestring.LineString(*args)
diff --git a/shapely/geometry/multipoint.py b/shapely/geometry/multipoint.py
index 08025bf..bf13f85 100644
--- a/shapely/geometry/multipoint.py
+++ b/shapely/geometry/multipoint.py
@@ -52,7 +52,9 @@ class MultiPoint(BaseMultipartGeometry):
             # allow creation of empty multipoints, to support unpickling
             pass
         else:
-            self._geom, self._ndim = geos_multipoint_from_py(points)
+            geom, n = geos_multipoint_from_py(points)
+            self._set_geom(geom)
+            self._ndim = n
 
     def shape_factory(self, *args):
         return point.Point(*args)
diff --git a/shapely/geometry/multipolygon.py b/shapely/geometry/multipolygon.py
index efa68cb..d83beb3 100644
--- a/shapely/geometry/multipolygon.py
+++ b/shapely/geometry/multipolygon.py
@@ -55,11 +55,13 @@ class MultiPolygon(BaseMultipartGeometry):
 
         if not polygons:
             # allow creation of empty multipolygons, to support unpickling
-            pass
+            return
         elif context_type == 'polygons':
-            self._geom, self._ndim = geos_multipolygon_from_polygons(polygons)
+            geom, n = geos_multipolygon_from_polygons(polygons)
         elif context_type == 'geojson':
-            self._geom, self._ndim = geos_multipolygon_from_py(polygons)
+            geom, n = geos_multipolygon_from_py(polygons)
+        self._set_geom(geom)
+        self._ndim = n
 
     def shape_factory(self, *args):
         return polygon.Polygon(*args)
diff --git a/shapely/geometry/point.py b/shapely/geometry/point.py
index 3378705..ffcad22 100644
--- a/shapely/geometry/point.py
+++ b/shapely/geometry/point.py
@@ -47,13 +47,15 @@ class Point(BaseGeometry):
         BaseGeometry.__init__(self)
         if len(args) > 0:
             if len(args) == 1:
-                self._geom, self._ndim = geos_point_from_py(args[0])
+                geom, n = geos_point_from_py(args[0])
             elif len(args) > 3:
                 raise TypeError(
                     "Point() takes at most 3 arguments ({} given)".format(len(args))
                 )
             else:
-                self._geom, self._ndim = geos_point_from_py(tuple(args))
+                geom, n = geos_point_from_py(tuple(args))
+            self._set_geom(geom)
+            self._ndim = n
 
     # Coordinate getters and setters
 
@@ -155,13 +157,15 @@ class Point(BaseGeometry):
             "Setting the 'coords' to mutate a Geometry in place is deprecated,"
             " and will not be possible any more in Shapely 2.0",
             ShapelyDeprecationWarning, stacklevel=2)
-        self.empty()
+        self._empty()
         if len(args) == 1:
-            self._geom, self._ndim = geos_point_from_py(args[0])
+            geom, n = geos_point_from_py(args[0])
         elif len(args) > 3:
             raise TypeError("Point() takes at most 3 arguments ({} given)".format(len(args)))
         else:
-            self._geom, self._ndim = geos_point_from_py(tuple(args))
+            geom, n = geos_point_from_py(tuple(args))
+        self._set_geom(geom)
+        self._ndim = n
 
     coords = property(BaseGeometry._get_coords, _set_coords)
 
diff --git a/shapely/geometry/polygon.py b/shapely/geometry/polygon.py
index 7463d2b..031993f 100644
--- a/shapely/geometry/polygon.py
+++ b/shapely/geometry/polygon.py
@@ -52,7 +52,9 @@ class LinearRing(LineString):
         if coordinates is not None:
             ret = geos_linearring_from_py(coordinates)
             if ret is not None:
-                self._geom, self._ndim = ret
+                geom, n = ret
+                self._set_geom(geom)
+                self._ndim = n
 
     @property
     def __geo_interface__(self):
@@ -70,10 +72,12 @@ class LinearRing(LineString):
             "Setting the 'coords' to mutate a Geometry in place is deprecated,"
             " and will not be possible any more in Shapely 2.0",
             ShapelyDeprecationWarning, stacklevel=2)
-        self.empty()
+        self._empty()
         ret = geos_linearring_from_py(coordinates)
         if ret is not None:
-            self._geom, self._ndim = ret
+            geom, n = ret
+            self._set_geom(geom)
+            self._ndim = n
 
     coords = property(_get_coords, _set_coords)
 
@@ -196,7 +200,7 @@ class InteriorRingSequence(object):
         if i not in self.__rings__:
             g = lgeos.GEOSGetInteriorRingN(self._geom, i)
             ring = LinearRing()
-            ring._geom = g
+            ring._set_geom(g)
             ring.__p__ = self
             ring._other_owned = True
             ring._ndim = self._ndim
@@ -249,9 +253,11 @@ class Polygon(BaseGeometry):
         if shell is not None:
             ret = geos_polygon_from_py(shell, holes)
             if ret is not None:
-                self._geom, self._ndim = ret
+                geom, n = ret
+                self._set_geom(geom)
+                self._ndim = n
             else:
-                self.empty()
+                self._empty()
 
     @property
     def exterior(self):
@@ -260,7 +266,7 @@ class Polygon(BaseGeometry):
         elif self._exterior is None or self._exterior() is None:
             g = lgeos.GEOSGetExteriorRing(self._geom)
             ring = LinearRing()
-            ring._geom = g
+            ring._set_geom(g)
             ring.__p__ = self
             ring._other_owned = True
             ring._ndim = self._ndim
diff --git a/shapely/ops.py b/shapely/ops.py
index f86dadf..7c50c7f 100644
--- a/shapely/ops.py
+++ b/shapely/ops.py
@@ -2,7 +2,9 @@
 """
 
 from ctypes import byref, c_void_p, c_double
+from warnings import warn
 
+from shapely.errors import ShapelyDeprecationWarning
 from shapely.prepared import prep
 from shapely.geos import lgeos
 from shapely.geometry.base import geom_factory, BaseGeometry, BaseMultipartGeometry
@@ -119,8 +121,13 @@ class CollectionOperator(object):
     def cascaded_union(self, geoms):
         """Returns the union of a sequence of geometries
 
-        This method was superseded by :meth:`unary_union`.
+        This function is deprecated, as it was superseded by
+        :meth:`unary_union`.
         """
+        warn(
+            "The 'cascaded_union()' function is deprecated. "
+            "Use 'unary_union()' instead.",
+            ShapelyDeprecationWarning, stacklevel=2)
         try:
             L = len(geoms)
             if isinstance(geoms, BaseMultipartGeometry):
@@ -553,17 +560,66 @@ split = SplitOp.split
 
 
 def substring(geom, start_dist, end_dist, normalized=False):
-    """Return a line segment between specified distances along a linear geometry.
+    """Return a line segment between specified distances along a LineString
 
     Negative distance values are taken as measured in the reverse
     direction from the end of the geometry. Out-of-range index
     values are handled by clamping them to the valid range of values.
-    If the start distances equals the end distance, a point is being returned.
-    If the normalized arg is True, the distance will be interpreted as a
-    fraction of the geometry's length.
+
+    If the start distance equals the end distance, a Point is returned.
+
+    If the start distance is actually beyond the end distance, then the
+    reversed substring is returned such that the start distance is
+    at the first coordinate.
+
+    Parameters
+    ----------
+    geom : LineString
+        The geometry to get a substring of.
+    start_dist : float
+        The distance along `geom` of the start of the substring.
+    end_dist : float
+        The distance along `geom` of the end of the substring.
+    normalized : bool, False
+        Whether the distance parameters are interpreted as a
+        fraction of the geometry's length.
+
+    Returns
+    -------
+    Union[Point, LineString]
+        The substring between `start_dist` and `end_dist` or a Point
+        if they are at the same location.
+
+    Raises
+    ------
+    TypeError
+        If `geom` is not a LineString.
+
+    Examples
+    --------
+    >>> from shapely.geometry import LineString
+    >>> from shapely.ops import substring
+    >>> ls = LineString((i, 0) for i in range(6))
+    >>> ls.wkt
+    'LINESTRING (0 0, 1 0, 2 0, 3 0, 4 0, 5 0)'
+    >>> substring(ls, start_dist=1, end_dist=3).wkt
+    'LINESTRING (1 0, 2 0, 3 0)'
+    >>> substring(ls, start_dist=3, end_dist=1).wkt
+    'LINESTRING (3 0, 2 0, 1 0)'
+    >>> substring(ls, start_dist=1, end_dist=-3).wkt
+    'LINESTRING (1 0, 2 0)'
+    >>> substring(ls, start_dist=0.2, end_dist=-0.6, normalized=True).wkt
+    'LINESTRING (1 0, 2 0)'
+
+    Returning a `Point` when `start_dist` and `end_dist` are at the
+    same location.
+
+    >>> substring(ls, 2.5, -2.5).wkt
+    'POINT (2.5 0)'
     """
 
-    assert(isinstance(geom, LineString))
+    if not isinstance(geom, LineString):
+        raise TypeError("Can only calculate a substring of LineString geometries. A %s was provided." % geom.type)
 
     # Filter out cases in which to return a point
     if start_dist == end_dist:
@@ -577,36 +633,52 @@ def substring(geom, start_dist, end_dist, normalized=False):
     elif normalized and -start_dist >= 1 and -end_dist >= 1:
         return geom.interpolate(0, normalized)
 
-    start_point = geom.interpolate(start_dist, normalized)
-    end_point = geom.interpolate(end_dist, normalized)
-
-    min_dist = min(start_dist, end_dist)
-    max_dist = max(start_dist, end_dist)
     if normalized:
-        min_dist *= geom.length
-        max_dist *= geom.length
+        start_dist *= geom.length
+        end_dist *= geom.length
 
-    if start_dist < end_dist:
-        vertex_list = [(start_point.x, start_point.y)]
-    else:
+    # Filter out cases where distances meet at a middle point from opposite ends.
+    if start_dist < 0 < end_dist and abs(start_dist) + end_dist == geom.length:
+        return geom.interpolate(end_dist)
+    elif end_dist < 0 < start_dist and abs(end_dist) + start_dist == geom.length:
+        return geom.interpolate(start_dist)
+
+    start_point = geom.interpolate(start_dist)
+    end_point = geom.interpolate(end_dist)
+
+    if start_dist < 0:
+        start_dist = geom.length + start_dist  # Values may still be negative,
+    if end_dist < 0:                           # but only in the out-of-range
+        end_dist = geom.length + end_dist      # sense, not the wrap-around sense.
+
+    reverse = start_dist > end_dist
+    if reverse:
+        start_dist, end_dist = end_dist, start_dist
+
+    if start_dist < 0:
+        start_dist = 0  # to avoid duplicating the first vertex
+
+    if reverse:
         vertex_list = [(end_point.x, end_point.y)]
+    else:
+        vertex_list = [(start_point.x, start_point.y)]
 
     coords = list(geom.coords)
     current_distance = 0
     for p1, p2 in zip(coords, coords[1:]):
-        if min_dist < current_distance < max_dist:
+        if start_dist < current_distance < end_dist:
             vertex_list.append(p1)
-        elif current_distance >= max_dist:
+        elif current_distance >= end_dist:
             break
 
         current_distance += ((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2) ** 0.5
 
-    if start_dist < end_dist:
-        vertex_list.append((end_point.x, end_point.y))
-    else:
+    if reverse:
         vertex_list.append((start_point.x, start_point.y))
         # reverse direction result
         vertex_list = reversed(vertex_list)
+    else:
+        vertex_list.append((end_point.x, end_point.y))
 
     return LineString(vertex_list)
 


Test patch:
diff --git a/tests/test_emptiness.py b/tests/test_emptiness.py
index 10bfe72..348266d 100644
--- a/tests/test_emptiness.py
+++ b/tests/test_emptiness.py
@@ -1,4 +1,4 @@
-from . import unittest
+from . import unittest, shapely20_deprecated
 
 from shapely.errors import ShapelyDeprecationWarning
 from shapely.geometry.base import BaseGeometry, EmptyGeometry
@@ -22,12 +22,14 @@ class EmptinessTestCase(unittest.TestCase):
         g = BaseGeometry()
         self.assertTrue(g._is_empty)
 
+    @shapely20_deprecated
     def test_emptying_point(self):
         p = sgeom.Point(0, 0)
         self.assertFalse(p._is_empty)
         p.empty()
         self.assertTrue(p._is_empty)
 
+    @shapely20_deprecated
     def test_none_geom(self):
         p = BaseGeometry()
         p._geom = None
diff --git a/tests/test_split.py b/tests/test_split.py
index 58f759a..3eaa1c8 100644
--- a/tests/test_split.py
+++ b/tests/test_split.py
@@ -3,7 +3,7 @@ from shapely.ops import split
 from . import unittest
 
 from shapely.geometry import Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon, GeometryCollection
-from shapely.ops import cascaded_union, linemerge
+from shapely.ops import linemerge, unary_union
 
 class TestSplitGeometry(unittest.TestCase):
 	# helper class for testing below
@@ -16,7 +16,7 @@ class TestSplitGeometry(unittest.TestCase):
 			if s.geoms[0].type == 'LineString':
 				self.assertTrue(linemerge(s).simplify(0.000001).equals(geom))
 			elif s.geoms[0].type == 'Polygon':
-				union = cascaded_union(s).simplify(0.000001)
+				union = unary_union(s).simplify(0.000001)
 				self.assertTrue(union.equals(geom))
 				self.assertEqual(union.area, geom.area)
 			else:
diff --git a/tests/test_substring.py b/tests/test_substring.py
index acc5c7b..4d734ca 100644
--- a/tests/test_substring.py
+++ b/tests/test_substring.py
@@ -12,6 +12,7 @@ class SubstringTestCase(unittest.TestCase):
         self.point = Point(1, 1)
         self.line1 = LineString(([0, 0], [2, 0]))
         self.line2 = LineString(([3, 0], [3, 6], [4.5, 6]))
+        self.line3 = LineString(((0, i) for i in range(5)))
 
     @unittest.skipIf(geos_version < (3, 2, 0), 'GEOS 3.2.0 required')
     def test_return_startpoint(self):
@@ -34,44 +35,77 @@ class SubstringTestCase(unittest.TestCase):
         self.assertTrue(substring(self.line1, 0.5, 0.5, True).equals(Point(1, 0)))
         self.assertTrue(substring(self.line1, -0.5, -0.5, True).equals(Point(1, 0)))
 
+        # Coming from opposite ends
+        self.assertTrue(substring(self.line1, 1.5, -0.5).equals(Point(1.5, 0)))
+        self.assertTrue(substring(self.line1, -0.5, 1.5).equals(Point(1.5, 0)))
+        self.assertTrue(substring(self.line1, -0.7, 0.3, True).equals(Point(0.6, 0)))
+        self.assertTrue(substring(self.line1, 0.3, -0.7, True).equals(Point(0.6, 0)))
+
     @unittest.skipIf(geos_version < (3, 2, 0), 'GEOS 3.2.0 required')
     def test_return_startsubstring(self):
-        self.assertTrue(substring(self.line1, -500, 0.6).equals(LineString(([0, 0], [0.6, 0]))))
-        self.assertTrue(substring(self.line1, -1.1, 0.6, True).equals(LineString(([0, 0], [1.2, 0]))))
+        self.assertEqual(substring(self.line1, -500, 0.6).wkt, LineString(([0, 0], [0.6, 0])).wkt)
+        self.assertEqual(substring(self.line1, -1.1, 0.6, True).wkt, LineString(([0, 0], [1.2, 0])).wkt)
+
+    @unittest.skipIf(geos_version < (3, 2, 0), 'GEOS 3.2.0 required')
+    def test_return_startsubstring_reversed(self):
+        # not normalized
+        self.assertEqual(substring(self.line1, -1, -500).wkt, LineString(([1, 0], [0, 0])).wkt)
+        self.assertEqual(substring(self.line3, 3.5, 0).wkt, LineString(([0, 3.5], [0, 3], [0, 2], [0, 1], [0, 0])).wkt)
+        self.assertEqual(substring(self.line3, -1.5, -500).wkt, LineString(([0, 2.5], [0, 2], [0, 1], [0, 0])).wkt)
+        # normalized
+        self.assertEqual(substring(self.line1, -0.5, -1.1, True).wkt, LineString(([1.0, 0], [0, 0])).wkt)
+        self.assertEqual(substring(self.line3, 0.5, 0, True).wkt, LineString(([0, 2.0], [0, 1], [0, 0])).wkt)
+        self.assertEqual(substring(self.line3, -0.5, -1.1, True).wkt, LineString(([0, 2.0], [0, 1], [0, 0])).wkt)
 
     @unittest.skipIf(geos_version < (3, 2, 0), 'GEOS 3.2.0 required')
     def test_return_endsubstring(self):
-        self.assertTrue(substring(self.line1, 0.6, 500).equals(LineString(([0.6, 0], [2, 0]))))
-        self.assertTrue(substring(self.line1, 0.6, 1.1, True).equals(LineString(([1.2, 0], [2, 0]))))
+        self.assertEqual(substring(self.line1, 0.6, 500).wkt, LineString(([0.6, 0], [2, 0])).wkt)
+        self.assertEqual(substring(self.line1, 0.6, 1.1, True).wkt, LineString(([1.2, 0], [2, 0])).wkt)
 
     @unittest.skipIf(geos_version < (3, 2, 0), 'GEOS 3.2.0 required')
-    def test_return_midsubstring(self):
-        self.assertTrue(substring(self.line1, 0.5, 0.6).equals(LineString(([0.5, 0], [0.6, 0]))))
-        self.assertTrue(substring(self.line1, 0.6, 0.5).equals(LineString(([0.6, 0], [0.5, 0]))))
-        self.assertTrue(substring(self.line1, -0.5, -0.6).equals(LineString(([1.5, 0], [1.4, 0]))))
-        self.assertTrue(substring(self.line1, -0.6, -0.5).equals(LineString(([1.4, 0], [1.5, 0]))))
-        self.assertTrue(substring(self.line1, 0.5, 0.6, True).equals(LineString(([1, 0], [1.2, 0]))))
-        self.assertTrue(substring(self.line1, 0.6, 0.5, True).equals(LineString(([1.2, 0], [1, 0]))))
-        self.assertTrue(substring(self.line1, -0.5, -0.6, True).equals(LineString(([1, 0], [0.8, 0]))))
-        self.assertTrue(substring(self.line1, -0.6, -0.5, True).equals(LineString(([0.8, 0], [1, 0]))))
+    def test_return_endsubstring_reversed(self):
+        # not normalized
+        self.assertEqual(substring(self.line1, 500, -1).wkt, LineString(([2, 0], [1, 0])).wkt)
+        self.assertEqual(substring(self.line3, 4, 2.5).wkt, LineString(([0, 4], [0, 3], [0, 2.5])).wkt)
+        self.assertEqual(substring(self.line3, 500, -1.5).wkt, LineString(([0, 4], [0, 3], [0, 2.5])).wkt)
+        # normalized
+        self.assertEqual(substring(self.line1, 1.1, -0.5, True).wkt, LineString(([2, 0], [1.0, 0])).wkt)
+        self.assertEqual(substring(self.line3, 1, 0.5, True).wkt, LineString(([0, 4], [0, 3], [0, 2.0])).wkt)
+        self.assertEqual(substring(self.line3, 1.1, -0.5, True).wkt, LineString(([0, 4], [0, 3], [0, 2.0])).wkt)
 
     @unittest.skipIf(geos_version < (3, 2, 0), 'GEOS 3.2.0 required')
     def test_return_midsubstring(self):
-        self.assertTrue(substring(self.line1, 0.5, 0.6).equals(LineString(([0.5, 0], [0.6, 0]))))
-        self.assertTrue(substring(self.line1, 0.6, 0.5).equals(LineString(([0.6, 0], [0.5, 0]))))
-        self.assertTrue(substring(self.line1, -0.5, -0.6).equals(LineString(([1.5, 0], [1.4, 0]))))
-        self.assertTrue(substring(self.line1, -0.6, -0.5).equals(LineString(([1.4, 0], [1.5, 0]))))
-        self.assertTrue(substring(self.line1, 0.5, 0.6, True).equals(LineString(([1, 0], [1.2, 0]))))
-        self.assertTrue(substring(self.line1, 0.6, 0.5, True).equals(LineString(([1.2, 0], [1, 0]))))
-        self.assertTrue(substring(self.line1, -0.5, -0.6, True).equals(LineString(([1, 0], [0.8, 0]))))
-        self.assertTrue(substring(self.line1, -0.6, -0.5, True).equals(LineString(([0.8, 0], [1, 0]))))
+        self.assertEqual(substring(self.line1, 0.5, 0.6).wkt, LineString(([0.5, 0], [0.6, 0])).wkt)
+        self.assertEqual(substring(self.line1, -0.6, -0.5).wkt, LineString(([1.4, 0], [1.5, 0])).wkt)
+        self.assertEqual(substring(self.line1, 0.5, 0.6, True).wkt, LineString(([1, 0], [1.2, 0])).wkt)
+        self.assertEqual(substring(self.line1, -0.6, -0.5, True).wkt, LineString(([0.8, 0], [1, 0])).wkt)
+
+    @unittest.skipIf(geos_version < (3, 2, 0), 'GEOS 3.2.0 required')
+    def test_return_midsubstring_reversed(self):
+        self.assertEqual(substring(self.line1, 0.6, 0.5).wkt, LineString(([0.6, 0], [0.5, 0])).wkt)
+        self.assertEqual(substring(self.line1, -0.5, -0.6).wkt, LineString(([1.5, 0], [1.4, 0])).wkt)
+        self.assertEqual(substring(self.line1, 0.6, 0.5, True).wkt, LineString(([1.2, 0], [1, 0])).wkt)
+        self.assertEqual(substring(self.line1, -0.5, -0.6, True).wkt, LineString(([1, 0], [0.8, 0])).wkt)
+
+        # with vertices
+        # not normalized
+        self.assertEqual(substring(self.line3, 3.5, 2.5).wkt, LineString(([0, 3.5], [0, 3], [0, 2.5])).wkt)    # (+, +)
+        self.assertEqual(substring(self.line3, -0.5, -1.5).wkt, LineString(([0, 3.5], [0, 3], [0, 2.5])).wkt)  # (-, -)
+        self.assertEqual(substring(self.line3, 3.5, -1.5).wkt, LineString(([0, 3.5], [0, 3], [0, 2.5])).wkt)   # (+, -)
+        self.assertEqual(substring(self.line3, -0.5, 2.5).wkt, LineString(([0, 3.5], [0, 3], [0, 2.5])).wkt)   # (-, +)
+
+        # normalized
+        self.assertEqual(substring(self.line3, 0.875, 0.625, True).wkt, LineString(([0, 3.5], [0, 3], [0, 2.5])).wkt)
+        self.assertEqual(substring(self.line3, -0.125, -0.375, True).wkt, LineString(([0, 3.5], [0, 3], [0, 2.5])).wkt)
+        self.assertEqual(substring(self.line3, 0.875, -0.375, True).wkt, LineString(([0, 3.5], [0, 3], [0, 2.5])).wkt)
+        self.assertEqual(substring(self.line3, -0.125, 0.625, True).wkt, LineString(([0, 3.5], [0, 3], [0, 2.5])).wkt)
 
     @unittest.skipIf(geos_version < (3, 2, 0), 'GEOS 3.2.0 required')
     def test_return_substring_with_vertices(self):
-        self.assertTrue(substring(self.line2, 1, 7).equals(LineString(([3, 1], [3, 6], [4, 6]))))
-        self.assertTrue(substring(self.line2, 0.2, 0.9, True).equals(LineString(([3, 1.5], [3, 6], [3.75, 6]))))
-        self.assertTrue(substring(self.line2, 0, 0.9, True).equals(LineString(([3, 0], [3, 6], [3.75, 6]))))
-        self.assertTrue(substring(self.line2, 0.2, 1, True).equals(LineString(([3, 1.5], [3, 6], [4.5, 6]))))
+        self.assertEqual(substring(self.line2, 1, 7).wkt, LineString(([3, 1], [3, 6], [4, 6])).wkt)
+        self.assertEqual(substring(self.line2, 0.2, 0.9, True).wkt, LineString(([3, 1.5], [3, 6], [3.75, 6])).wkt)
+        self.assertEqual(substring(self.line2, 0, 0.9, True).wkt, LineString(([3, 0], [3, 6], [3.75, 6])).wkt)
+        self.assertEqual(substring(self.line2, 0.2, 1, True).wkt, LineString(([3, 1.5], [3, 6], [4.5, 6])).wkt)
 
     @unittest.skipIf(geos_version < (3, 2, 0), 'GEOS 3.2.0 required')
     def test_return_substring_issue682(self):
@@ -83,5 +117,9 @@ class SubstringTestCase(unittest.TestCase):
         cut_line = substring(line, 0.7, 0.8, normalized=True)
         assert len(cut_line.coords) == 53
 
+    @unittest.skipIf(geos_version < (3, 2, 0), 'GEOS 3.2.0 required')
+    def test_raise_type_error(self):
+        self.assertRaises(TypeError, substring, Point(0, 0), 0, 0)
+
 
 data_issue_848 = '{"type": "LineString", "coordinates": [[-87.71314, 41.96793], [-87.71312, 41.96667], [-87.71311, 41.96643], [-87.7131, 41.96635], [-87.71309, 41.9663], [-87.71303, 41.96621], [-87.71298, 41.96615], [-87.71292, 41.96611], [-87.7128, 41.96607], [-87.71268, 41.96605], [-87.71255, 41.96605], [-87.7124, 41.96605], [-87.71219, 41.96605], [-87.71173, 41.96606], [-87.71108, 41.96607], [-87.71027, 41.96607], [-87.70884, 41.96609], [-87.70763, 41.96611], [-87.70645, 41.96612], [-87.70399, 41.96613], [-87.70267, 41.96614], [-87.70166, 41.96615], [-87.70075, 41.96615], [-87.69954, 41.96615], [-87.69873, 41.96616], [-87.69789, 41.96618], [-87.69675, 41.9662], [-87.69502, 41.96621], [-87.69411, 41.96621], [-87.69145, 41.96623], [-87.69026, 41.96624], [-87.68946, 41.96625], [-87.6885, 41.96625], [-87.68718, 41.96628], [-87.68545, 41.96631], [-87.68399, 41.96632], [-87.68271, 41.96635], [-87.68159, 41.96636], [-87.68034, 41.96638], [-87.67863, 41.96641], [-87.67766, 41.96642], [-87.67741, 41.96641], [-87.67722, 41.9664], [-87.67695, 41.96638], [-87.67665, 41.96632], [-87.67638, 41.96623], [-87.67613, 41.96612], [-87.67589, 41.96596], [-87.6757, 41.96579], [-87.67557, 41.96565], [-87.67544, 41.96547], [-87.67539, 41.96536], [-87.6753, 41.96519], [-87.67524, 41.96503], [-87.67523, 41.96491], [-87.67522, 41.96477], [-87.67521, 41.96457], [-87.6752, 41.96434], [-87.67519, 41.96371], [-87.67517, 41.96175], [-87.67513, 41.96077], [-87.67505, 41.95798], [-87.67501, 41.95666], [-87.67497, 41.95513], [-87.67496, 41.95452], [-87.67491, 41.95392], [-87.67487, 41.95302], [-87.67485, 41.95202], [-87.67484, 41.95101], [-87.67479, 41.94959], [-87.67476, 41.94859], [-87.67474, 41.94703], [-87.67468, 41.94596], [-87.67466, 41.94513], [-87.67463, 41.94494], [-87.67457, 41.94474], [-87.6745, 41.94455], [-87.67442, 41.94438], [-87.6743, 41.94424], [-87.67419, 41.94414], [-87.67405, 41.94404], [-87.67386, 41.94393], [-87.67367, 41.94386], [-87.67348, 41.9438], [-87.67334, 41.94376], [-87.67311, 41.94373], [-87.67289, 41.9437], [-87.67263, 41.94369], [-87.67234, 41.94369], [-87.6715, 41.9437], [-87.67088, 41.94371], [-87.66938, 41.94373], [-87.66749, 41.94377], [-87.66585, 41.94378], [-87.66508, 41.94379], [-87.66361, 41.94381], [-87.6591, 41.94391], [-87.65767, 41.94391], [-87.65608, 41.94393], [-87.6555, 41.94394], [-87.65521, 41.94394], [-87.65503, 41.94393], [-87.65488, 41.9439], [-87.6547, 41.94386], [-87.65454, 41.9438], [-87.65441, 41.94375], [-87.65425, 41.94364], [-87.6541, 41.94351], [-87.654, 41.94342], [-87.65392, 41.94331], [-87.65382, 41.94319], [-87.65375, 41.94306], [-87.65367, 41.94292], [-87.65361, 41.9428], [-87.65355, 41.94269], [-87.65351, 41.94257], [-87.65347, 41.94238], [-87.65345, 41.94218], [-87.65338, 41.93975], [-87.65337, 41.93939], [-87.65337, 41.93893], [-87.65336, 41.93865], [-87.65333, 41.93763], [-87.65331, 41.93717], [-87.65328, 41.93627], [-87.65327, 41.93603], [-87.65323, 41.93532], [-87.65322, 41.93491], [-87.6532, 41.93445], [-87.65314, 41.93312], [-87.65313, 41.93273], [-87.6531, 41.93218], [-87.65307, 41.93151], [-87.65305, 41.9309], [-87.65302, 41.9303], [-87.65299, 41.92951], [-87.65296, 41.9287], [-87.65295, 41.92842], [-87.65294, 41.92768], [-87.65292, 41.92715], [-87.65289, 41.92599], [-87.65288, 41.92537], [-87.65287, 41.92505], [-87.65282, 41.92352], [-87.65276, 41.92172], [-87.65274, 41.92113], [-87.65264, 41.91822], [-87.65264, 41.91808], [-87.65262, 41.91763], [-87.65261, 41.91718], [-87.65255, 41.91563], [-87.6525, 41.91406], [-87.65242, 41.91377], [-87.65234, 41.91362], [-87.65223, 41.91351], [-87.65208, 41.91339], [-87.65183, 41.91322], [-87.65093, 41.9126], [-87.65017, 41.91203], [-87.64985, 41.9118], [-87.64971, 41.91171], [-87.64957, 41.91164], [-87.64948, 41.9116], [-87.64939, 41.91158], [-87.6492, 41.91153], [-87.649, 41.9115], [-87.64883, 41.9115], [-87.64863, 41.9115], [-87.64792, 41.91151], [-87.64781, 41.9115], [-87.64768, 41.91146], [-87.64756, 41.91139], [-87.64745, 41.91122], [-87.6474, 41.91112], [-87.64739, 41.91101], [-87.64738, 41.91086], [-87.64736, 41.91071], [-87.64734, 41.91061], [-87.64728, 41.91051], [-87.64718, 41.91044], [-87.64709, 41.9104], [-87.64697, 41.91036], [-87.64682, 41.91034], [-87.64664, 41.91033], [-87.64646, 41.91033], [-87.6458, 41.91034], [-87.64523, 41.91034], [-87.64348, 41.91036], [-87.64255, 41.91039], [-87.641, 41.9104], [-87.64038, 41.9104], [-87.63975, 41.9104], [-87.6393, 41.91041], [-87.63814, 41.91042], [-87.63798, 41.91041], [-87.63787, 41.91039], [-87.63771, 41.91034], [-87.63757, 41.91027], [-87.63746, 41.91021], [-87.63736, 41.91011], [-87.6373, 41.90999], [-87.63727, 41.90986], [-87.63726, 41.90973], [-87.63725, 41.90951], [-87.63723, 41.90874], [-87.63718, 41.90758], [-87.63713, 41.90607], [-87.63711, 41.90543], [-87.63702, 41.90381], [-87.63702, 41.90368], [-87.63701, 41.90334], [-87.63699, 41.90322], [-87.63694, 41.90312], [-87.63688, 41.90299], [-87.63682, 41.90292], [-87.63671, 41.90279], [-87.63659, 41.90265], [-87.63653, 41.90255], [-87.63649, 41.90245], [-87.63646, 41.90235], [-87.63647, 41.90221], [-87.63647, 41.90211], [-87.6365, 41.90202], [-87.63653, 41.9019], [-87.63659, 41.90177], [-87.63666, 41.90156], [-87.63669, 41.90143], [-87.6367, 41.90131], [-87.6367, 41.90119], [-87.63664, 41.90029], [-87.63664, 41.90008], [-87.63662, 41.89975], [-87.63658, 41.89892], [-87.63657, 41.89867], [-87.63654, 41.89761], [-87.63654, 41.89738], [-87.63649, 41.89726], [-87.63641, 41.89715], [-87.63634, 41.89708], [-87.63623, 41.89699], [-87.63595, 41.89677], [-87.63583, 41.89667], [-87.63574, 41.89654], [-87.63569, 41.89645], [-87.63568, 41.89633], [-87.63565, 41.89542], [-87.63563, 41.89434], [-87.6356, 41.89327], [-87.63558, 41.89261], [-87.63554, 41.89147], [-87.63553, 41.89051], [-87.63548, 41.8903], [-87.6354, 41.89021], [-87.63533, 41.89012], [-87.63524, 41.89007], [-87.63508, 41.89001], [-87.63493, 41.88997], [-87.63475, 41.88994], [-87.63462, 41.88991], [-87.63447, 41.88989], [-87.63436, 41.88984], [-87.63425, 41.88979], [-87.63414, 41.8897], [-87.63407, 41.88962], [-87.63402, 41.88952], [-87.63399, 41.88943], [-87.63397, 41.88897], [-87.63396, 41.88707], [-87.63391, 41.88572], [-87.63389, 41.88441], [-87.63385, 41.8827], [-87.63384, 41.88144], [-87.63378, 41.88014], [-87.63374, 41.87872], [-87.63369, 41.87726], [-87.63369, 41.87706], [-87.63365, 41.87695], [-87.63359, 41.87691], [-87.63353, 41.87688], [-87.63345, 41.87686], [-87.63338, 41.87685], [-87.63263, 41.87685], [-87.63173, 41.87686], [-87.62925, 41.87689], [-87.62821, 41.87691], [-87.62757, 41.87693], [-87.6265, 41.87696], [-87.62635, 41.87696], [-87.62603, 41.87697], [-87.62605, 41.87831], [-87.6261, 41.87951], [-87.62616, 41.88203], [-87.62619, 41.88322], [-87.62622, 41.88443], [-87.62626, 41.88534], [-87.62625, 41.88552], [-87.62625, 41.88557], [-87.62627, 41.88562], [-87.6263, 41.88566], [-87.62635, 41.88569], [-87.62642, 41.88572], [-87.6265, 41.88573], [-87.62655, 41.88574], [-87.62661, 41.88574], [-87.62683, 41.88574], [-87.62784, 41.88574], [-87.62887, 41.88574], [-87.62948, 41.88574], [-87.62982, 41.88574], [-87.62992, 41.88574], [-87.63011, 41.88574], [-87.6302, 41.88574], [-87.63089, 41.88574], [-87.63204, 41.88574], [-87.63285, 41.88573], [-87.63391, 41.88572], [-87.63396, 41.88707], [-87.63397, 41.88897], [-87.63399, 41.88943], [-87.63402, 41.88952], [-87.63407, 41.88962], [-87.63414, 41.8897], [-87.63425, 41.88979], [-87.63436, 41.88984], [-87.63447, 41.88989], [-87.63462, 41.88991], [-87.63475, 41.88994], [-87.63493, 41.88997], [-87.63508, 41.89001], [-87.63524, 41.89007], [-87.63533, 41.89012], [-87.6354, 41.89021], [-87.63548, 41.8903], [-87.63553, 41.89051], [-87.63554, 41.89147], [-87.63558, 41.89261], [-87.6356, 41.89327], [-87.63563, 41.89434], [-87.63565, 41.89542], [-87.63568, 41.89633], [-87.63569, 41.89645], [-87.63574, 41.89654], [-87.63583, 41.89667], [-87.63595, 41.89677], [-87.63623, 41.89699], [-87.63634, 41.89708], [-87.63641, 41.89715], [-87.63649, 41.89726], [-87.63654, 41.89738], [-87.63654, 41.89761], [-87.63657, 41.89867], [-87.63658, 41.89892], [-87.63662, 41.89975], [-87.63664, 41.90008], [-87.63664, 41.90029], [-87.6367, 41.90119], [-87.6367, 41.90131], [-87.63669, 41.90143], [-87.63666, 41.90156], [-87.63659, 41.90177], [-87.63653, 41.9019], [-87.6365, 41.90202], [-87.63647, 41.90211], [-87.63647, 41.90221], [-87.63646, 41.90235], [-87.63649, 41.90245], [-87.63653, 41.90255], [-87.63659, 41.90265], [-87.63671, 41.90279], [-87.63682, 41.90292], [-87.63688, 41.90299], [-87.63694, 41.90312], [-87.63699, 41.90322], [-87.63701, 41.90334], [-87.63702, 41.90368], [-87.63702, 41.90381], [-87.63711, 41.90543], [-87.63713, 41.90607], [-87.63718, 41.90758], [-87.63723, 41.90874], [-87.63725, 41.90951], [-87.63726, 41.90973], [-87.63727, 41.90986], [-87.6373, 41.90999], [-87.63736, 41.91011], [-87.63746, 41.91021], [-87.63757, 41.91027], [-87.63771, 41.91034], [-87.63787, 41.91039], [-87.63798, 41.91041], [-87.63814, 41.91042], [-87.6393, 41.91041], [-87.63975, 41.9104], [-87.64038, 41.9104], [-87.641, 41.9104], [-87.64255, 41.91039], [-87.64348, 41.91036], [-87.64523, 41.91034], [-87.6458, 41.91034], [-87.64646, 41.91033], [-87.64664, 41.91033], [-87.64682, 41.91034], [-87.64697, 41.91036], [-87.64709, 41.9104], [-87.64718, 41.91044], [-87.64728, 41.91051], [-87.64734, 41.91061], [-87.64736, 41.91071], [-87.64738, 41.91086], [-87.64739, 41.91101], [-87.6474, 41.91112], [-87.64745, 41.91122], [-87.64756, 41.91139], [-87.64768, 41.91146], [-87.64781, 41.9115], [-87.64792, 41.91151], [-87.64863, 41.9115], [-87.64883, 41.9115], [-87.649, 41.9115], [-87.6492, 41.91153], [-87.64939, 41.91158], [-87.64948, 41.9116], [-87.64957, 41.91164], [-87.64971, 41.91171], [-87.64985, 41.9118], [-87.65017, 41.91203], [-87.65093, 41.9126], [-87.65183, 41.91322], [-87.65208, 41.91339], [-87.65223, 41.91351], [-87.65234, 41.91362], [-87.65242, 41.91377], [-87.6525, 41.91406], [-87.65255, 41.91563], [-87.65261, 41.91718], [-87.65262, 41.91763], [-87.65264, 41.91808], [-87.65264, 41.91822], [-87.65274, 41.92113], [-87.65276, 41.92172], [-87.65282, 41.92352], [-87.65287, 41.92505], [-87.65288, 41.92537], [-87.65289, 41.92599], [-87.65292, 41.92715], [-87.65294, 41.92768], [-87.65295, 41.92842], [-87.65296, 41.9287], [-87.65299, 41.92951], [-87.65302, 41.9303], [-87.65305, 41.9309], [-87.65307, 41.93151], [-87.6531, 41.93218], [-87.65313, 41.93273], [-87.65314, 41.93312], [-87.6532, 41.93445], [-87.65322, 41.93491], [-87.65323, 41.93532], [-87.65327, 41.93603], [-87.65328, 41.93627], [-87.65331, 41.93717], [-87.65333, 41.93763], [-87.65336, 41.93865], [-87.65337, 41.93893], [-87.65337, 41.93939], [-87.65338, 41.93975], [-87.65345, 41.94218], [-87.65347, 41.94238], [-87.65351, 41.94257], [-87.65355, 41.94269], [-87.65361, 41.9428], [-87.65367, 41.94292], [-87.65375, 41.94306], [-87.65382, 41.94319], [-87.65392, 41.94331], [-87.654, 41.94342], [-87.6541, 41.94351], [-87.65425, 41.94364], [-87.65441, 41.94375], [-87.65454, 41.9438], [-87.6547, 41.94386], [-87.65488, 41.9439], [-87.65503, 41.94393], [-87.65521, 41.94394], [-87.6555, 41.94394], [-87.65608, 41.94393], [-87.65767, 41.94391], [-87.6591, 41.94391], [-87.66361, 41.94381], [-87.66508, 41.94379], [-87.66585, 41.94378], [-87.66749, 41.94377], [-87.66938, 41.94373], [-87.67088, 41.94371], [-87.6715, 41.9437], [-87.67234, 41.94369], [-87.67263, 41.94369], [-87.67289, 41.9437], [-87.67311, 41.94373], [-87.67334, 41.94376], [-87.67348, 41.9438], [-87.67367, 41.94386], [-87.67386, 41.94393], [-87.67405, 41.94404], [-87.67419, 41.94414], [-87.6743, 41.94424], [-87.67442, 41.94438], [-87.6745, 41.94455], [-87.67457, 41.94474], [-87.67463, 41.94494], [-87.67466, 41.94513], [-87.67468, 41.94596], [-87.67474, 41.94703], [-87.67476, 41.94859], [-87.67479, 41.94959], [-87.67484, 41.95101], [-87.67485, 41.95202], [-87.67487, 41.95302], [-87.67491, 41.95392], [-87.67496, 41.95452], [-87.67497, 41.95513], [-87.67501, 41.95666], [-87.67505, 41.95798], [-87.67513, 41.96077], [-87.67517, 41.96175], [-87.67519, 41.96371], [-87.6752, 41.96434], [-87.67521, 41.96457], [-87.67522, 41.96477], [-87.67523, 41.96491], [-87.67524, 41.96503], [-87.6753, 41.96519], [-87.67539, 41.96536], [-87.67544, 41.96547], [-87.67557, 41.96565], [-87.6757, 41.96579], [-87.67589, 41.96596], [-87.67613, 41.96612], [-87.67638, 41.96623], [-87.67665, 41.96632], [-87.67695, 41.96638], [-87.67722, 41.9664], [-87.67741, 41.96641], [-87.67766, 41.96642], [-87.67863, 41.96641], [-87.68034, 41.96638], [-87.68159, 41.96636], [-87.68271, 41.96635], [-87.68399, 41.96632], [-87.68545, 41.96631], [-87.68718, 41.96628], [-87.6885, 41.96625], [-87.68946, 41.96625], [-87.69026, 41.96624], [-87.69145, 41.96623], [-87.69411, 41.96621], [-87.69502, 41.96621], [-87.69675, 41.9662], [-87.69789, 41.96618], [-87.69873, 41.96616], [-87.69954, 41.96615], [-87.70075, 41.96615], [-87.70166, 41.96615], [-87.70267, 41.96614], [-87.70399, 41.96613], [-87.70645, 41.96612], [-87.70763, 41.96611], [-87.70884, 41.96609], [-87.71027, 41.96607], [-87.71108, 41.96607], [-87.71173, 41.96606], [-87.71219, 41.96605], [-87.7124, 41.96605], [-87.71255, 41.96605], [-87.71268, 41.96605], [-87.7128, 41.96607], [-87.71292, 41.96611], [-87.71298, 41.96615], [-87.71303, 41.96621], [-87.71309, 41.9663], [-87.7131, 41.96635], [-87.71311, 41.96643], [-87.71312, 41.96667], [-87.71314, 41.96793]]}'
diff --git a/tests/test_union.py b/tests/test_union.py
index bd3e6a4..4482fc2 100644
--- a/tests/test_union.py
+++ b/tests/test_union.py
@@ -1,7 +1,9 @@
 from . import unittest
+import pytest
 import random
 from itertools import islice
 from functools import partial
+from shapely.errors import ShapelyDeprecationWarning
 from shapely.geos import geos_version
 from shapely.geometry import Point, MultiPolygon
 from shapely.ops import cascaded_union, unary_union
@@ -27,6 +29,7 @@ def halton(base):
 class UnionTestCase(unittest.TestCase):
 
     def test_cascaded_union(self):
+        # cascaded_union is deprecated, as it was superseded by unary_union
 
         # Use a partial function to make 100 points uniformly distributed
         # in a 40x40 box centered on 0,0.
@@ -39,7 +42,8 @@ class UnionTestCase(unittest.TestCase):
 
         # Perform a cascaded union of the polygon spots, dissolving them
         # into a collection of polygon patches
-        u = cascaded_union(spots)
+        with pytest.warns(ShapelyDeprecationWarning, match="is deprecated"):
+            u = cascaded_union(spots)
         self.assertTrue(u.geom_type in ('Polygon', 'MultiPolygon'))
 
     def setUp(self):