Repository: SciTools/cartopy

Issue:
problem projecting shapely geometry
Unable to project certain geometries which are considered `valid` by shapely.  The result of this, is that certain files which exhibit certain features (not yet identified), result in cartopy getting stuck in an infinite loop when trying to contour plot.

```
import matplotlib.pyplot as plt
import matplotlib.patches as mpatch
from matplotlib.path import Path
import cartopy.crs as ccrs
import cartopy
import cartopy.mpl.patch
import numpy as np


codes = np.array(
  [ 1,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,
    2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,
    2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,
  79,  1,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2], dtype=np.uint8)

verts = np.array(
  [[ 177.5       ,  -79.91257282],
  [ 178.33332825,  -79.94626848],
  [ 178.76980034,  -80.        ],
  [ 179.16665649,  -80.05090491],
  [ 180.        ,  -80.1341655 ],
  [ 180.83332825,  -80.18303477],
  [ 181.66665649,  -80.24403151],
  [ 182.5       ,  -80.27413206],
  [ 183.33332825,  -80.43097599],
  [ 184.16665649,  -80.49045702],
  [ 184.38492079,  -80.55555725],
  [ 184.16665649,  -80.61479558],
  [ 183.33332825,  -80.72903092],
  [ 182.5       ,  -80.89439627],
  [ 181.88733292,  -81.1111145 ],
  [ 182.5       ,  -81.32193767],
  [ 183.1470951 ,  -81.66666412],
  [ 183.33332825,  -82.07478295],
  [ 184.16665649,  -81.94521875],
  [ 184.64650567,  -82.22222137],
  [ 185.        ,  -82.68186229],
  [ 185.83332825,  -82.54804157],
  [ 186.58600609,  -82.77777863],
  [ 186.66665649,  -82.83954736],
  [ 187.5       ,  -83.18075545],
  [ 188.33332825,  -83.28405989],
  [ 188.74266377,  -83.33333588],
  [ 188.33332825,  -83.59752943],
  [ 187.5       ,  -83.45866268],
  [ 186.66665649,  -83.58507857],
  [ 185.83332825,  -83.62652996],
  [ 185.        ,  -83.77986131],
  [ 184.16665649,  -83.61819378],
  [ 183.33332825,  -83.75258955],
  [ 182.5       ,  -83.56468976],
  [ 181.66665649,  -83.49493477],
  [ 180.83332825,  -83.57002013],
  [ 180.        ,  -83.62037747],
  [ 179.16665649,  -83.54838551],
  [ 178.4380705 ,  -83.33333588],
  [ 178.33332825,  -83.31271285],
  [ 177.5       ,  -83.30272666],
  [ 177.34790921,  -83.33333588],
  [ 177.5       ,  -83.47133501],
  [ 177.95698433,  -83.8888855 ],
  [ 178.33332825,  -83.98447198],
  [ 179.16665649,  -84.12857505],
  [ 180.        ,  -84.08646885],
  [ 180.83332825,  -84.31844593],
  [ 200.        ,  -86.        ],
  [ 200.        ,  -76.        ],
  [ 177.5       ,  -79.91257282],
  [ 179.16665649,  -83.96388918],
  [ 178.7488463 ,  -83.8888855 ],
  [ 179.16665649,  -83.7823362 ],
  [ 180.        ,  -83.80228931],
  [ 180.83332825,  -83.72426266],
  [ 181.66665649,  -83.6545546 ],
  [ 182.5       ,  -83.83379012],
  [ 182.72829154,  -83.8888855 ],
  [ 182.5       ,  -84.07004817],
  [ 181.66665649,  -84.2399593 ],
  [ 180.83332825,  -84.11065765],
  [ 180.        ,  -83.95260785],
  [ 179.16665649,  -83.96388918]])


def main():
    path = Path(verts, codes)
    fig = plt.figure()
    ax = fig.add_subplot(111)
    patch = mpatch.PathPatch(path, facecolor='orange', lw=2)
    ax.add_patch(patch)
    ax.set_xlim(160, 280)
    ax.set_ylim(-74, -86)
    plt.show()

    src = ccrs.PlateCarree()
    tgt = ccrs.PlateCarree()

    geoms = cartopy.mpl.patch.path_to_geos(path)
    print 'GEOMS:', geoms

    for g in geoms:
        print 'valid: ', g.is_valid
        print 'counter clockwise: ', g.exterior.is_ccw
        if not g.is_valid:
            break
        print tgt.project_geometry(g, src) 
    print '\ndone'


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

Have narrowed this problem down to the few vertices defined above.
We get stuck in an infinite loop!



Discussion:
![figure_1](https://f.cloud.github.com/assets/2071643/442812/08c8e416-b167-11e2-86fd-63e39316d7f4.png)

# PathPatch # Plot of the unprojected geometry

![1](https://f.cloud.github.com/assets/2071643/443620/6b7ae3fc-b185-11e2-8318-229ac81c2935.png)

# EXTERIOR  # Linestrings (from multilinestring)

![2](https://f.cloud.github.com/assets/2071643/443623/85c492f8-b185-11e2-8ce3-76d93778900e.png)

# EXTERIOR # Simplified

![3](https://f.cloud.github.com/assets/2071643/443626/a0e5914a-b185-11e2-92b5-9aadc8f74dbb.png)

# INTERIOR #  Linestrings (from multilinestring) == Simplified

(both cross boundary so cannot be simplified)

``` Python
_Thing(366.3796225986569, False, (0, 'first', (179.9999992052617, -83.6203774013431)))
_Thing(366.3796225986569, False, (0, 'first', (179.9999992052617, -83.6203774013431)))
_Thing(365.91353110984437, False, (0, 'last', (179.9999992052617, -84.08646889015561)))
_Thing(365.91353110984437, False, (0, 'last', (179.9999992052617, -84.08646889015561)))
_Thing(370.52215624954414, False, (1, 'first', (179.99999284735168, -79.47784375045588)))
_Thing(370.52215624954414, False, (1, 'first', (179.99999284735168, -79.47784375045588)))
_Thing(369.8658345794035, False, (1, 'last', (179.9999992052617, -80.1341654205965)))
_Thing(369.8658345794035, False, (1, 'last', (179.9999992052617, -80.1341654205965)))
_Thing(1070.1341655466053, False, (2, 'first', (-179.9999992053873, -80.13416554660536)))
_Thing(1070.1341655466053, False, (2, 'first', (-179.9999992053873, -80.13416554660536)))
_Thing(1069.477840433711, False, (2, 'last', (-179.9999880791821, -79.47784043371101)))
_Thing(1069.477840433711, False, (2, 'last', (-179.9999880791821, -79.47784043371101)))
_Thing(1073.802289235588, False, (3, 'first', (-179.9999992053873, -83.802289235588)))
_Thing(1073.802289235588, False, (3, 'first', (-179.9999992053873, -83.802289235588)))
_Thing(1073.952608000728, False, (3, 'last', (-179.9999992053873, -83.95260800072806)))
_Thing(1073.952608000728, False, (3, 'last', (-179.9999992053873, -83.95260800072806)))
_Thing(366.04739213924125, False, (4, 'first', (179.9999992052617, -83.95260786075873)))
_Thing(366.04739213924125, False, (4, 'first', (179.9999992052617, -83.95260786075873)))
_Thing(366.19771070902874, False, (4, 'last', (179.9999992052617, -83.80228929097125)))
_Thing(366.19771070902874, False, (4, 'last', (179.9999992052617, -83.80228929097125)))
_Thing(0.0, True, POINT (-180.0000000000000000 -90.0000000000000000))
_Thing(0.0, True, POINT (-180.0000000000000000 -90.0000000000000000))
_Thing(360.0, True, POINT (180.0000000000000000 -90.0000000000000000))
_Thing(360.0, True, POINT (180.0000000000000000 -90.0000000000000000))
_Thing(540.0, True, POINT (180.0000000000000000 90.0000000000000000))
_Thing(540.0, True, POINT (180.0000000000000000 90.0000000000000000))
_Thing(900.0, True, POINT (-180.0000000000000000 90.0000000000000000))
_Thing(900.0, True, POINT (-180.0000000000000000 90.0000000000000000))
```

Start-End points of all linestrings and also boundary points (there are 5 line strings corresponding to the images above)

``` Python
_Thing(0.0, True, POINT (-180.0000000000000000 -90.0000000000000000))
_Thing(0.0, True, POINT (-180.0000000000000000 -90.0000000000000000))
_Thing(360.0, True, POINT (180.0000000000000000 -90.0000000000000000))
_Thing(360.0, True, POINT (180.0000000000000000 -90.0000000000000000))
_Thing(365.91353110984437, False, (0, 'last', (179.9999992052617, -84.08646889015561)))
_Thing(365.91353110984437, False, (0, 'last', (179.9999992052617, -84.08646889015561)))
_Thing(366.04739213924125, False, (4, 'first', (179.9999992052617, -83.95260786075873)))
_Thing(366.04739213924125, False, (4, 'first', (179.9999992052617, -83.95260786075873)))
_Thing(366.19771070902874, False, (4, 'last', (179.9999992052617, -83.80228929097125)))
_Thing(366.19771070902874, False, (4, 'last', (179.9999992052617, -83.80228929097125)))
_Thing(366.3796225986569, False, (0, 'first', (179.9999992052617, -83.6203774013431)))
_Thing(366.3796225986569, False, (0, 'first', (179.9999992052617, -83.6203774013431)))
_Thing(369.8658345794035, False, (1, 'last', (179.9999992052617, -80.1341654205965)))
_Thing(369.8658345794035, False, (1, 'last', (179.9999992052617, -80.1341654205965)))
_Thing(370.52215624954414, False, (1, 'first', (179.99999284735168, -79.47784375045588)))
_Thing(370.52215624954414, False, (1, 'first', (179.99999284735168, -79.47784375045588)))
_Thing(540.0, True, POINT (180.0000000000000000 90.0000000000000000))
_Thing(540.0, True, POINT (180.0000000000000000 90.0000000000000000))
_Thing(900.0, True, POINT (-180.0000000000000000 90.0000000000000000))
_Thing(900.0, True, POINT (-180.0000000000000000 90.0000000000000000))
_Thing(1069.477840433711, False, (2, 'last', (-179.9999880791821, -79.47784043371101)))
_Thing(1069.477840433711, False, (2, 'last', (-179.9999880791821, -79.47784043371101)))
_Thing(1070.1341655466053, False, (2, 'first', (-179.9999992053873, -80.13416554660536)))
_Thing(1070.1341655466053, False, (2, 'first', (-179.9999992053873, -80.13416554660536)))
_Thing(1073.802289235588, False, (3, 'first', (-179.9999992053873, -83.802289235588)))
_Thing(1073.802289235588, False, (3, 'first', (-179.9999992053873, -83.802289235588)))
_Thing(1073.952608000728, False, (3, 'last', (-179.9999992053873, -83.95260800072806)))
_Thing(1073.952608000728, False, (3, 'last', (-179.9999992053873, -83.95260800072806)))
```

As above, only sorted:

``` Python
        # 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.
```

![4_1](https://f.cloud.github.com/assets/2071643/443641/3d9f44f8-b187-11e2-9105-4d7c8d9be80d.png)
![4_2](https://f.cloud.github.com/assets/2071643/443642/3da1e5aa-b187-11e2-9b18-160e0c9565a9.png)
![4_3](https://f.cloud.github.com/assets/2071643/443643/3de2a3ba-b187-11e2-8fd5-83340050e2d5.png)

# Combined linestring plotting - possible issue along boundary

# What's with all the shouting? ;-)

:)

![identified](https://f.cloud.github.com/assets/2071643/444091/f4ff2b06-b199-11e2-86cb-72cd46becb1d.png)
Shifting coordinates identifies where problem resides

![identified_wint](https://f.cloud.github.com/assets/2071643/444116/d2be3f0e-b19a-11e2-855f-d43679d7fa07.png)
with internal linestrings

![identified_notsimpl](https://f.cloud.github.com/assets/2071643/444459/d1703838-b1a3-11e2-84fd-50e0d081fb72.png)
unsimplified linestring

two linestring were combined when they should not have been
combining the start of one linestring to the end of another as it is within a tolerance, breaks the link between its partner linestring which crosses the boundary.

```
if len(multi_line_string) > 1:
```

The stitching condition is given by:

```
                    if i != j and np.allclose(line_strings[i].coords[0],
                                              line_strings[j].coords[-1],
                                              atol=self.threshold):
```

The use of `self.threshold` is causing the problem - it shouldn't be used here, it should only be used to control how "bendy" a projected line has to be before subdividing. (For a PlateCarree plot `self.threshold` is 0.5!) The current version of the line projecting algorithm locates the boundary with much higher precision than `self.threshold` (although previous versions didn't, which is probably why this problem exists).

I realise this thanks @rhattersley, whats not clear is how we might define a "suitable" threshold? any suggestions?

I'm not sure I'm comfortable with defining it as a fraction of the projections threshold (though it should be tied to the order of magnitude suitable for that projection), I am also uneasy about an absolute threshold for all projections (for example the default of allclose 1e-8) as some may be in meters and others in degrees for example.

I dont see how we can determine a realistic number for precision on the projected points.

Given the projected precision is effectively limited by the precision of the floating point numbers involved, I suspect the tolerance should derived from the maximum `x_limits`/`y_limits` values:

```
atol=max(np.abs(self.x_limits + self.y_limits)) * 1e-5
```

(Although in real life you wouldn't want to compute all of that in-line.)


Resolving patch:
diff --git a/lib/cartopy/crs.py b/lib/cartopy/crs.py
--- a/lib/cartopy/crs.py
+++ b/lib/cartopy/crs.py
@@ -191,6 +191,11 @@ def _project_linear_ring(self, linear_ring, src_crs):
             #   jkl41abc
             # 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
@@ -200,7 +205,7 @@ def _project_linear_ring(self, linear_ring, src_crs):
                 while j < len(line_strings):
                     if i != j and np.allclose(line_strings[i].coords[0],
                                               line_strings[j].coords[-1],
-                                              atol=self.threshold):
+                                              atol=threshold):
                         last_coords = list(line_strings[j].coords)
                         first_coords = list(line_strings[i].coords)[1:]
                         combo = sgeom.LineString(last_coords + first_coords)


Test patch:
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,6 +19,7 @@
 import unittest
 
 from shapely import geometry
+import numpy as np
 
 import cartopy.crs as ccrs
 
@@ -164,6 +165,38 @@ def test_stitch(self):
         result = target_proj.project_geometry(linear_ring, src_proj)
         self.assertEqual(len(result), 1)
 
+    def test_at_boundary(self):
+        # Check that a polygon is split and recombined correctly
+        # as a result of being on the boundary, determined by tolerance.
+
+        exterior = np.array(
+            [[177.5, -79.912],
+             [178.333, -79.946],
+             [181.666, -83.494],
+             [180.833, -83.570],
+             [180., -83.620],
+             [178.438, -83.333],
+             [178.333, -83.312],
+             [177.956, -83.888],
+             [180.,  -84.086],
+             [180.833, -84.318],
+             [183., -86.],
+             [183., -78.],
+             [177.5, -79.912]])
+        tring = geometry.polygon.LinearRing(exterior)
+
+        tcrs = ccrs.PlateCarree()
+        scrs = ccrs.PlateCarree()
+
+        r = tcrs._project_linear_ring(tring, scrs)
+
+        # Number of linearstrings
+        self.assertEqual(len(r), 4)
+
+        # Test area of smallest Polygon that contains all the points in the
+        # geometry.
+        self.assertAlmostEqual(r.convex_hull.area, 2347.75619258)
+
 
 if __name__ == '__main__':
     unittest.main()