Repository: SciTools/cartopy

Issue:
Low-level handling of degenerate polygons
`Projection._project_polygon()` is rather unhelpfully converting degenerate polygons (such as those produced by `matplotlib.contourf`) into global extent.
e.g.
`Polygon([(20, 20), (20, 20), (20, 20), (20, 20)])`
`Polygon([(20, 20), (20, 20), (20, 20.1), (20, 20)])`

Whilst it's easy to filter these out in the MPL integration layer, ideally the low level re-projection code should be able to deal with them.

Infinite (project?) loop
The following code never terminates:

```
import matplotlib.pyplot as plt
import numpy as np

import cartopy.crs as ccrs


def sample_data(shape=(73, 145)):
    """Returns lons, lats and data of some interesting data on a regular grid."""
    nlats, nlons = shape 
    lats = np.linspace(-np.pi/2, np.pi/2, nlats)
    lons = np.linspace(0, 2*np.pi, nlons)
    lons, lats = np.meshgrid(lons, lats)
    wave = 0.75*(np.sin(2*lats)**8) * np.cos(4*lons)
    mean = 0.5*np.cos(2*lats) * ((np.sin(2*lats))**2 + 2)

    lats = np.rad2deg(lats)
    lons = np.rad2deg(lons)
    data = wave + mean

    return lons, lats, data

def main():
    ax = plt.axes(projection=ccrs.NorthPolarStereo())

    ax.contourf(*sample_data(), nlev=2, transform=ccrs.PlateCarree())
    ax.coastlines()
    plt.show()


if __name__ == '__main__':
    main()
```



Discussion:
Actually, it turns out that many of the most common clipping algorithms don't handle degenerate polygons (d3 suffers from this problem too: https://github.com/mbostock/d3/issues/2025). Essentially, the cutting operation can be seen as an extension of a clip (you keep both sides instead of just one side), so picking an algorithm which handles degeneracies is critical. The truth is though, we could just as well pre-process degenerate "polygons" and either remove them entirely, or convert them to lines.

#101 does not close this issue.

I tested this, it terminates within a few seconds and displays an empty graph.  Shouldn't the issue be closed?

> Shouldn't the issue be closed?

Well, it fixed the issue in question, but clearly #403 suggests that all cases haven't been covered. Let's work from that ticket, as it is a tangible and easy to reproduce case.


Resolving patch:
diff --git a/lib/cartopy/crs.py b/lib/cartopy/crs.py
--- a/lib/cartopy/crs.py
+++ b/lib/cartopy/crs.py
@@ -180,9 +180,10 @@ def _project_line_string(self, geometry, src_crs):
     def _project_linear_ring(self, linear_ring, src_crs):
         """
         Projects the given LinearRing from the src_crs into this CRS and
-        returns the resultant LinearRing or MultiLineString.
+        returns a list of LinearRings and a single MultiLineString.
 
         """
+        debug = False
         # 1) Resolve the initial lines into projected segments
         # 1abc
         # def23ghi
@@ -190,6 +191,10 @@ def _project_linear_ring(self, linear_ring, src_crs):
         multi_line_string = cartopy.trace.project_linear(linear_ring,
                                                          src_crs, self)
 
+        # Threshold for whether a point is close enough to be the same
+        # point as another.
+        threshold = max(np.abs(self.x_limits + self.y_limits)) * 1e-5
+
         # 2) Simplify the segments where appropriate.
         if len(multi_line_string) > 1:
             # Stitch together segments which are close to continuous.
@@ -202,13 +207,18 @@ def _project_linear_ring(self, linear_ring, src_crs):
             # 2) The cut ends of segments are too close to reliably
             # place into an order along the boundary.
 
-            # Threshold for whether a point is close enough to be the same
-            # point as another.
-            threshold = max(np.abs(self.x_limits + self.y_limits)) * 1e-5
-
             line_strings = list(multi_line_string)
             any_modified = False
             i = 0
+            if debug:
+                first_coord = np.array([ls.coords[0] for ls in line_strings])
+                last_coord = np.array([ls.coords[-1] for ls in line_strings])
+                print('Distance matrix:')
+                np.set_printoptions(precision=2)
+                x = first_coord[:, np.newaxis, :]
+                y = last_coord[np.newaxis, :, :]
+                print(np.abs(x - y).max(axis=-1))
+
             while i < len(line_strings):
                 modified = False
                 j = 0
@@ -216,6 +226,8 @@ def _project_linear_ring(self, linear_ring, src_crs):
                     if i != j and np.allclose(line_strings[i].coords[0],
                                               line_strings[j].coords[-1],
                                               atol=threshold):
+                        if debug:
+                            print('Joining together {} and {}.'.format(i, j))
                         last_coords = list(line_strings[j].coords)
                         first_coords = list(line_strings[i].coords)[1:]
                         combo = sgeom.LineString(last_coords + first_coords)
@@ -233,16 +245,22 @@ def _project_linear_ring(self, linear_ring, src_crs):
             if any_modified:
                 multi_line_string = sgeom.MultiLineString(line_strings)
 
-        # 3) Check for a single resulting ring.
-        if (len(multi_line_string) == 1 and
-                len(multi_line_string[0].coords) > 3 and
-                np.allclose(multi_line_string[0].coords[0],
-                            multi_line_string[0].coords[-1])):
-            result_geometry = LinearRing(multi_line_string[0].coords[:-1])
-        else:
-            result_geometry = multi_line_string
+        # 3) Check for rings that have been created by the projection stage.
+        rings = []
+        line_strings = []
+        for line in multi_line_string:
+            if len(line.coords) > 3 and np.allclose(line.coords[0],
+                                                    line.coords[-1],
+                                                    atol=threshold):
+                result_geometry = LinearRing(line.coords[:-1])
+                rings.append(result_geometry)
+            else:
+                line_strings.append(line)
+        # If we found any rings, then we should re-create the multi-line str.
+        if rings:
+            multi_line_string = sgeom.MultiLineString(line_strings)
 
-        return result_geometry
+        return rings, multi_line_string
 
     def _project_multipoint(self, geometry, src_crs):
         geoms = []
@@ -288,18 +306,17 @@ def _project_polygon(self, polygon, src_crs):
             is_ccw = True
         else:
             is_ccw = polygon.exterior.is_ccw
-
         # Project the polygon exterior/interior rings.
         # Each source ring will result in either a ring, or one or more
         # lines.
         rings = []
         multi_lines = []
         for src_ring in [polygon.exterior] + list(polygon.interiors):
-            geometry = self._project_linear_ring(src_ring, src_crs)
-            if geometry.geom_type == 'LinearRing':
-                rings.append(geometry)
-            else:
-                multi_lines.append(geometry)
+            p_rings, p_mline = self._project_linear_ring(src_ring, src_crs)
+            if p_rings:
+                rings.extend(p_rings)
+            if len(p_mline) > 0:
+                multi_lines.append(p_mline)
 
         # Convert any lines to rings by attaching them to the boundary.
         if multi_lines:
@@ -316,6 +333,9 @@ def _attach_lines_to_boundary(self, multi_line_strings, is_ccw):
         lines and boundary.
 
         """
+        debug = False
+        debug_plot_edges = False
+
         # Accumulate all the boundary and segment end points, along with
         # their distance along the boundary.
         edge_things = []
@@ -338,43 +358,87 @@ def boundary_distance(xy):
         # Record the positions of all the segment ends
         for i, line_string in enumerate(line_strings):
             first_dist = boundary_distance(line_string.coords[0])
-            thing = _Thing(first_dist, False,
-                           (i, 'first', line_string.coords[0]))
+            thing = _BoundaryPoint(first_dist, False,
+                                   (i, 'first', line_string.coords[0]))
             edge_things.append(thing)
             last_dist = boundary_distance(line_string.coords[-1])
-            thing = _Thing(last_dist, False,
-                           (i, 'last', line_string.coords[-1]))
+            thing = _BoundaryPoint(last_dist, False,
+                                   (i, 'last', line_string.coords[-1]))
             edge_things.append(thing)
 
         # Record the positions of all the boundary vertices
         for xy in list(boundary.coords)[:-1]:
             point = sgeom.Point(*xy)
             dist = boundary.project(point)
-            thing = _Thing(dist, True, point)
+            thing = _BoundaryPoint(dist, True, point)
             edge_things.append(thing)
 
+        if debug_plot_edges:
+            import matplotlib.pyplot as plt
+            current_fig = plt.gcf()
+            fig = plt.figure()
+            # Reset the current figure so we don't upset anything.
+            plt.figure(current_fig.number)
+            ax = fig.add_subplot(1, 1, 1)
+
         # Order everything as if walking around the boundary.
         # NB. We make line end-points take precedence over boundary points
         # to ensure that end-points are still found and followed when they
         # coincide.
         edge_things.sort(key=lambda thing: (thing.distance, thing.kind))
-        debug = 0
+        remaining_ls = dict(enumerate(line_strings))
+
+        prev_thing = None
+        for edge_thing in edge_things[:]:
+            if (prev_thing is not None and
+                    not edge_thing.kind and
+                    not prev_thing.kind and
+                    edge_thing.data[0] == prev_thing.data[0]):
+                j = edge_thing.data[0]
+                # Insert a edge boundary point in between this geometry.
+                mid_dist = (edge_thing.distance + prev_thing.distance) * 0.5
+                mid_point = boundary.interpolate(mid_dist)
+                new_thing = _BoundaryPoint(mid_dist, True, mid_point)
+                if debug:
+                    print('Artificially insert boundary: {}'.format(new_thing))
+                ind = edge_things.index(edge_thing)
+                edge_things.insert(ind, new_thing)
+                prev_thing = None
+            else:
+                prev_thing = edge_thing
+
         if debug:
             print()
             print('Edge things')
             for thing in edge_things:
                 print('   ', thing)
+        if debug_plot_edges:
+            for thing in edge_things:
+                if isinstance(thing.data, sgeom.Point):
+                    ax.plot(*thing.data.xy, marker='o')
+                else:
+                    ax.plot(*thing.data[2], marker='o')
+                    ls = line_strings[thing.data[0]]
+                    coords = np.array(ls.coords)
+                    ax.plot(coords[:, 0], coords[:, 1])
+                    ax.text(coords[0, 0], coords[0, 1], thing.data[0])
+                    ax.text(coords[-1, 0], coords[-1, 1],
+                            '{}.'.format(thing.data[0]))
+
+        processed_ls = []
+        while remaining_ls:
+            # Rename line_string to current_ls
+            i, current_ls = remaining_ls.popitem()
 
-        to_do = {i: line_string for i, line_string in enumerate(line_strings)}
-        done = []
-        while to_do:
-            i, line_string = to_do.popitem()
             if debug:
                 import sys
                 sys.stdout.write('+')
                 sys.stdout.flush()
                 print()
-                print('Processing: %s, %s' % (i, line_string))
+                print('Processing: %s, %s' % (i, current_ls))
+            # We only want to consider boundary-points, the starts-and-ends of
+            # all other line-strings, or the start-point of the current
+            # line-string.
             filter_fn = lambda t: (t.kind or
                                    t.data[0] != i or
                                    t.data[1] != 'last')
@@ -382,56 +446,68 @@ def boundary_distance(xy):
 
             added_linestring = set()
             while True:
-                # Find the distance of the last point
-                d_last = boundary_distance(line_string.coords[-1])
+                # Find out how far around this linestring's last
+                # point is on the boundary. We will use this to find
+                # the next point on the boundary.
+                d_last = boundary_distance(current_ls.coords[-1])
                 if debug:
-                    print('   d_last:', d_last)
-                next_thing = _find_gt(edge_things, d_last)
+                    print('   d_last: {!r}'.format(d_last))
+                next_thing = _find_first_gt(edge_things, d_last)
+                # Remove this boundary point from the edge.
+                edge_things.remove(next_thing)
                 if debug:
                     print('   next_thing:', next_thing)
                 if next_thing.kind:
+                    # We've just got a boundary point, add it, and keep going.
                     if debug:
                         print('   adding boundary point')
                     boundary_point = next_thing.data
-                    combined_coords = (list(line_string.coords) +
+                    combined_coords = (list(current_ls.coords) +
                                        [(boundary_point.x, boundary_point.y)])
-                    line_string = sgeom.LineString(combined_coords)
-                    # XXX
-                    # edge_things.remove(next_thing)
-                elif next_thing.data[0] == i:
+                    current_ls = sgeom.LineString(combined_coords)
+
+                elif next_thing.data[0] == i and next_thing.data[1] == 'first':
+                    # We've gone all the way around and are now back at the
+                    # first boundary thing.
                     if debug:
                         print('   close loop')
-                    done.append(line_string)
+                    processed_ls.append(current_ls)
+                    if debug_plot_edges:
+                        coords = np.array(current_ls.coords)
+                        ax.plot(coords[:, 0], coords[:, 1], color='black',
+                                linestyle='--')
                     break
                 else:
                     if debug:
                         print('   adding line')
                     j = next_thing.data[0]
                     line_to_append = line_strings[j]
-                    # XXX pelson: I think this if statement can be removed
-                    if j in to_do:
-                        del to_do[j]
+                    if j in remaining_ls:
+                        remaining_ls.pop(j)
                     coords_to_append = list(line_to_append.coords)
+
                     if next_thing.data[1] == 'last':
                         coords_to_append = coords_to_append[::-1]
-                    line_string = sgeom.LineString((list(line_string.coords) +
-                                                    coords_to_append))
+                    # Build up the linestring.
+                    current_ls = sgeom.LineString((list(current_ls.coords) +
+                                                   coords_to_append))
 
                     # Catch getting stuck in an infinite loop by checking that
-                    # linestring only added once
+                    # linestring only added once.
                     if j not in added_linestring:
                         added_linestring.add(j)
                     else:
+                        if debug_plot_edges:
+                            plt.show()
                         raise RuntimeError('Unidentified problem with '
                                            'geometry, linestring being '
-                                           're-added')
+                                           're-added. Please raise an issue.')
 
         # filter out any non-valid linear rings
-        done = [linear_ring for linear_ring in done if
-                len(linear_ring.coords) > 2]
+        processed_ls = [linear_ring for linear_ring in processed_ls if
+                        len(linear_ring.coords) > 2]
 
-        # XXX Is the last point in each ring actually the same as the first?
-        linear_rings = [LinearRing(line) for line in done]
+        linear_rings = [LinearRing(line) for line in processed_ls]
 
         if debug:
             print('   DONE')
@@ -1455,22 +1531,37 @@ def y_limits(self):
         return self._ylim
 
 
-class _Thing(object):
+class _BoundaryPoint(object):
     def __init__(self, distance, kind, data):
+        """
+        A representation for a geometric object which is
+        connected to the boundary.
+
+        Parameters
+        ==========
+        distance - float
+            The distance along the boundary that this object
+            can be found.
+        kind - bool
+            Whether this object represents a point from the pre-computed
+            boundary.
+        data - point or namedtuple
+            The actual data that this boundary object represents.
+        """
         self.distance = distance
         self.kind = kind
         self.data = data
 
     def __repr__(self):
-        return '_Thing(%r, %r, %s)' % (self.distance, self.kind, self.data)
+        return '_BoundaryPoint(%r, %r, %s)' % (self.distance, self.kind,
+                                               self.data)
 
 
-def _find_gt(a, x):
+def _find_first_gt(a, x):
     for v in a:
-        # TODO: Fix the problem of co-incident boundary & line points
-        # if v.distance >= x:
         if v.distance > x:
             return v
+    # We've gone all the way around, so pick the first point again.
     return a[0]
 
 
diff --git a/lib/cartopy/mpl/feature_artist.py b/lib/cartopy/mpl/feature_artist.py
--- a/lib/cartopy/mpl/feature_artist.py
+++ b/lib/cartopy/mpl/feature_artist.py
@@ -28,7 +28,7 @@
 import matplotlib.artist
 import matplotlib.collections
 
-import cartopy.mpl.patch
+import cartopy.mpl.patch as cpatch
 
 
 class FeatureArtist(matplotlib.artist.Artist):
@@ -118,8 +118,7 @@ def draw(self, renderer, *args, **kwargs):
                         geom, feature_crs)
                 else:
                     projected_geom = geom
-                geom_paths = cartopy.mpl.patch.geos_to_path(
-                    projected_geom)
+                geom_paths = cpatch.geos_to_path(projected_geom)
                 mapping[key] = geom_paths
             paths.extend(geom_paths)
 
diff --git a/lib/cartopy/mpl/patch.py b/lib/cartopy/mpl/patch.py
--- a/lib/cartopy/mpl/patch.py
+++ b/lib/cartopy/mpl/patch.py
@@ -76,7 +76,8 @@ def poly_codes(poly):
             codes = np.ones(len(poly.xy[0])) * Path.LINETO
             codes[0] = Path.MOVETO
             return codes
-
+        if shape.is_empty:
+            return []
         vertices = np.concatenate([np.array(shape.exterior.xy)] +
                                   [np.array(ring.xy) for ring in
                                    shape.interiors], 1).T


Test patch:
diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_goode_wrap.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_goode_wrap.png
Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_goode_wrap.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_goode_wrap.png differ
diff --git a/lib/cartopy/tests/test_linear_ring.py b/lib/cartopy/tests/test_linear_ring.py
--- a/lib/cartopy/tests/test_linear_ring.py
+++ b/lib/cartopy/tests/test_linear_ring.py
@@ -19,7 +19,8 @@
 
 import unittest
 
-from shapely import geometry
+import shapely.geometry as sgeom
+from shapely.geometry.polygon import LinearRing
 import numpy as np
 
 import cartopy.crs as ccrs
@@ -29,23 +30,20 @@ class TestBoundary(unittest.TestCase):
     def test_cuts(self):
         # Check that fragments do not start or end with one of the
         # original ... ?
-        linear_ring = geometry.polygon.LinearRing([(-10, 30),
-                                                   (10, 60),
-                                                   (10, 50)])
+        linear_ring = LinearRing([(-10, 30), (10, 60), (10, 50)])
         projection = ccrs.Robinson(170.5)
-        multi_line_string = projection.project_geometry(linear_ring)
-        # from cartopy.tests.mpl import show
-        # show(projection, multi_line_string)
+        rings, multi_line_string = projection.project_geometry(linear_ring)
 
         # The original ring should have been split into multiple pieces.
         self.assertGreater(len(multi_line_string), 1)
+        self.assertFalse(rings)
 
         def assert_intersection_with_boundary(segment_coords):
             # Double the length of the segment.
             start = segment_coords[0]
             end = segment_coords[1]
             end = [end[i] + 2 * (end[i] - start[i]) for i in (0, 1)]
-            extended_segment = geometry.LineString([start, end])
+            extended_segment = sgeom.LineString([start, end])
             # And see if it crosses the boundary.
             intersection = extended_segment.intersection(projection.boundary)
             self.assertFalse(intersection.is_empty,
@@ -80,39 +78,32 @@ def test_out_of_bounds(self):
 
         # Try all four combinations of valid/NaN vs valid/NaN.
         for coords, expected_n_lines in rings:
-            linear_ring = geometry.polygon.LinearRing(coords)
-            projected = projection.project_geometry(linear_ring)
+            linear_ring = LinearRing(coords)
+            rings, mlinestr = projection.project_geometry(linear_ring)
             if expected_n_lines == -1:
-                self.assertIsInstance(projected, geometry.polygon.LinearRing)
+                self.assertTrue(rings)
+                self.assertFalse(mlinestr)
             else:
-                self.assertEqual(len(projected), expected_n_lines)
+                self.assertEqual(len(mlinestr), expected_n_lines)
                 if expected_n_lines == 0:
-                    self.assertTrue(projected.is_empty)
+                    self.assertTrue(mlinestr.is_empty)
 
 
 class TestMisc(unittest.TestCase):
-    def test_misc(self):
-        projection = ccrs.TransverseMercator(central_longitude=-90)
-        linear_ring = geometry.polygon.LinearRing([(-10, 30),
-                                                   (10, 60),
-                                                   (10, 50)])
-        multi_line_string = projection.project_geometry(linear_ring)
-        # from cartopy.tests.mpl import show
-        # show(projection, multi_line_string)
-        # XXX not a test...
-
     def test_small(self):
         # What happens when a small (i.e. < threshold) feature crosses the
         # boundary?
         projection = ccrs.Mercator()
-        linear_ring = geometry.polygon.LinearRing([
+        linear_ring = LinearRing([
             (-179.9173693847652942, -16.5017831356493616),
             (-180.0000000000000000, -16.0671326636424396),
             (-179.7933201090486079, -16.0208822567412312),
         ])
-        multi_line_string = projection.project_geometry(linear_ring)
-        # there should be one, and only one, returned line:
-        assert isinstance(multi_line_string, geometry.polygon.LinearRing)
+        rings, multi_line_string = projection.project_geometry(linear_ring)
+        # There should be one, and only one, returned ring.
+        self.assertIsInstance(multi_line_string, sgeom.MultiLineString)
+        self.assertEqual(len(multi_line_string), 0)
+        self.assertEqual(len(rings), 1)
 
         # from cartopy.tests.mpl import show
         # show(projection, multi_line_string)
@@ -128,11 +119,11 @@ def test_three_points(self):
                   (0.000727869825138, -45.0),
                   (0.0, -45.000105851567454),
                   (0.0, -45.0)]
-        linear_ring = geometry.polygon.LinearRing(coords)
+        linear_ring = LinearRing(coords)
         src_proj = ccrs.PlateCarree()
         target_proj = ccrs.PlateCarree(180.0)
         try:
-            result = target_proj.project_geometry(linear_ring, src_proj)
+            _ = target_proj.project_geometry(linear_ring, src_proj)
         except ValueError:
             self.fail("Failed to project LinearRing.")
 
@@ -157,14 +148,17 @@ def test_stitch(self):
                   (10, -73)]
         src_proj = ccrs.PlateCarree()
         target_proj = ccrs.Stereographic(80)
-        linear_ring = geometry.polygon.LinearRing(coords)
-        result = target_proj.project_geometry(linear_ring, src_proj)
-        self.assertEqual(len(result), 1)
+
+        linear_ring = LinearRing(coords)
+        rings, mlinestr = target_proj.project_geometry(linear_ring, src_proj)
+        self.assertEqual(len(mlinestr), 1)
+        self.assertEqual(len(rings), 0)
 
         # Check the stitch works in either direction.
-        linear_ring = geometry.polygon.LinearRing(coords[::-1])
-        result = target_proj.project_geometry(linear_ring, src_proj)
-        self.assertEqual(len(result), 1)
+        linear_ring = LinearRing(coords[::-1])
+        rings, mlinestr = target_proj.project_geometry(linear_ring, src_proj)
+        self.assertEqual(len(mlinestr), 1)
+        self.assertEqual(len(rings), 0)
 
     def test_at_boundary(self):
         # Check that a polygon is split and recombined correctly
@@ -179,24 +173,25 @@ def test_at_boundary(self):
              [178.438, -83.333],
              [178.333, -83.312],
              [177.956, -83.888],
-             [180.,  -84.086],
+             [180., -84.086],
              [180.833, -84.318],
              [183., -86.],
              [183., -78.],
              [177.5, -79.912]])
-        tring = geometry.polygon.LinearRing(exterior)
+        tring = LinearRing(exterior)
 
         tcrs = ccrs.PlateCarree()
         scrs = ccrs.PlateCarree()
 
-        r = tcrs._project_linear_ring(tring, scrs)
+        rings, mlinestr = tcrs._project_linear_ring(tring, scrs)
 
         # Number of linearstrings
-        self.assertEqual(len(r), 4)
+        self.assertEqual(len(mlinestr), 4)
+        self.assertFalse(rings)
 
         # Test area of smallest Polygon that contains all the points in the
         # geometry.
-        self.assertAlmostEqual(r.convex_hull.area, 2347.75623076)
+        self.assertAlmostEqual(mlinestr.convex_hull.area, 2347.75623076)
 
 
 if __name__ == '__main__':
diff --git a/lib/cartopy/tests/test_polygon.py b/lib/cartopy/tests/test_polygon.py
--- a/lib/cartopy/tests/test_polygon.py
+++ b/lib/cartopy/tests/test_polygon.py
@@ -105,11 +105,7 @@ def test_former_infloop_case(self):
         # check the result is non-empty
         self.assertFalse(multi_polygon.is_empty)
 
-    def test_catch_infinite_loop(self):
-        # Known bug resulting in an infinite loop occurring, ensure exception
-        # is raised.  If this multilinestring does not result in an inf. loop
-        # (raised exception), then you've fixed a bug and this test can be
-        # updated.
+    def test_project_previous_infinite_loop(self):
         mstring1 = shapely.wkt.loads(
             'MULTILINESTRING ('
             '(-179.9999990464349651 -80.2000000000000171, '
@@ -139,8 +135,7 @@ def test_catch_infinite_loop(self):
         multi_line_strings = [mstring1, mstring2]
 
         src = ccrs.PlateCarree()
-        with self.assertRaises(RuntimeError):
-            src._attach_lines_to_boundary(multi_line_strings, True)
+        src._attach_lines_to_boundary(multi_line_strings, True)
 
     def test_3pt_poly(self):
         projection = ccrs.OSGB()
@@ -168,7 +163,9 @@ def test_self_intersecting_1(self):
         projected = target.project_geometry(geom, source)
         # Before handling self intersecting interiors, the area would be
         # approximately 13262233761329.
-        self.assertTrue(2.2e9 < projected.area < 2.3e9)
+        area = projected.area
+        self.assertTrue(2.2e9 < area < 2.3e9,
+                        msg='Got area {}, expecting ~2.2e9'.format(area))
 
     def test_self_intersecting_2(self):
         # Geometry comes from a matplotlib contourf (see #509)
@@ -182,6 +179,22 @@ def test_self_intersecting_2(self):
         # approximately 64808.
         self.assertTrue(7.9 < projected.area < 8.1)
 
+    def test_tiny_point_between_boundary_points(self):
+        # Geometry comes from #259.
+        target = ccrs.Orthographic(0, -75)
+        source = ccrs.PlateCarree()
+        wkt = 'POLYGON ((132 -40, 133 -6, 125.3 1, 115 -6, 132 -40))'
+        geom = shapely.wkt.loads(wkt)
+
+        target = ccrs.Orthographic(central_latitude=90., central_longitude=0)
+        source = ccrs.PlateCarree()
+        projected = target.project_geometry(geom, source)
+        area = projected.area
+        # Before fixing, this geometry used to fill the whole disk. Approx
+        # 1.2e14.
+        self.assertTrue(2.4e9 < area < 2.5e9,
+                        msg='Got area {}, expecting ~2.4e9'.format(area))
+
 
 class TestQuality(unittest.TestCase):
     def setUp(self):