Repository: SciTools/cartopy

Issue:
Missing contours in stereographic projection
I'm having a real problem plotting a data set and I want to see if the problem can be reproduced. I'm trying to plot some anomaly data in a `NorthPolarStereo` projection, but the negative filled contours fail to show up. It is troubling me because I have another script which plots the same data and it works! This shows the errors for me:

```
import numpy as np
import matplotlib.pyplot as plt
import cartopy.crs as ccrs


data = np.load('data.npy')
lons = np.load('lons.npy')
lats = np.load('lats.npy')

clevs = [-180, -140, -100, -60, -40, -20, 20, 40, 60, 100, 140, 180]

# plot the data on a Robinson projection
plt.figure()
ax = plt.axes(projection=ccrs.Robinson())
ax.set_extent([0, 360, 30, 90], crs=ccrs.PlateCarree())
ax.coastlines()
fill = ax.contourf(lons, lats, data, clevs, transform=ccrs.PlateCarree(),
                   extend='both')
ax.set_title('correct')

# same code except for the projection of the axes is NorthPolarStereo
plt.figure()
ax = plt.axes(projection=ccrs.NorthPolarStereo())
ax.set_extent([0, 360, 30, 90], crs=ccrs.PlateCarree())
ax.coastlines()
fill = ax.contourf(lons, lats, data, clevs, transform=ccrs.PlateCarree(),
                   extend='both')
ax.set_title('incorrect')

# now the same but add 0 to the contour levels
clevs = [-180, -140, -100, -60, -40, -20, 0, 20, 40, 60, 100, 140, 180]
plt.figure()
ax = plt.axes(projection=ccrs.NorthPolarStereo())
ax.set_extent([0, 360, 30, 90], crs=ccrs.PlateCarree())
ax.coastlines()
fill = ax.contourf(lons, lats, data, clevs, transform=ccrs.PlateCarree(),
                   extend='both')
ax.set_title('seems to be fine!')

plt.show()
```

I'd like to know if anyone can reproduce this, data is at http://www.atm.ox.ac.uk/user/dawson/uploads/data/

![robinson_trim](https://f.cloud.github.com/assets/390602/302224/ab24fcaa-95ff-11e2-9ebe-0e4941b51ff3.png)

![npstereo_bad_trim](https://f.cloud.github.com/assets/390602/302226/b23921ba-95ff-11e2-8e31-af5c711ac425.png)

![npstereo_ok_trim](https://f.cloud.github.com/assets/390602/302228/b596a22e-95ff-11e2-83b4-542bb215cfb1.png)



Discussion:
I figured out the cause of this is the cyclic point in the longitude dimension. In my examples longitude spans 0-360 inclusive. If I change the last longitude point to be 359.999 then the plot is as expected for the `NorthPolarStereo` projection... is this a bug?

Confirmed. I'm trying to isolate the polygon in question now, but in the meantime I notice your longitudes are in the range `(0, 360)`. Technically `PlateCarree` (as defined in Cartopy) should be in the range `(-180, 180)`. If you change your data slightly:

```
lons -= 180

...
fill = ax.contourf(lons, lats, data, clevs,
                   transform=ccrs.PlateCarree(central_longitude=180))
```

The plot works as expected. I will continue to look into the underling problem to try to find the offending polygon.

Thanks for making it nice and easy to replicate.

OK thanks. Worth noting that this works fine for contour lines, only filled contours appear to be broken.

Hi @ajdawson - sorry its taken so long to track down (a combination of Easter & impending deadlines has got in the way!). As you can see, I've reduced one of the problems from this issue into a simpler case. I believe mpl is (perfectly validly, but unexpectedly) producing non ccw contours - I suggest we address #261 in the hope that it resolves this more complex case (or leads to further problems which themselves can be simplified).

Thanks again for reporting this.


Resolving patch:
diff --git a/lib/cartopy/mpl/geoaxes.py b/lib/cartopy/mpl/geoaxes.py
--- a/lib/cartopy/mpl/geoaxes.py
+++ b/lib/cartopy/mpl/geoaxes.py
@@ -152,11 +152,17 @@ def transform_path_non_affine(self, src_path):
             return mpath.Path(self.transform(src_path.vertices))
 
         transformed_geoms = []
-        for geom in patch.path_to_geos(src_path):
-            transformed_geoms.append(
-                self.target_projection.project_geometry(geom,
-                                                        self.source_projection)
-            )
+        # Check whether this transform has the "force_path_ccw" attribute set.
+        # This is a cartopy extension to the Transform API to allow finer
+        # control of Path orientation handling (Path ordering is not important
+        # in matplotlib, but is in Cartopy).
+        geoms = patch.path_to_geos(src_path,
+                                   getattr(self, 'force_path_ccw', False))
+
+        for geom in geoms:
+            proj_geom = self.target_projection.project_geometry(
+                geom, self.source_projection)
+            transformed_geoms.append(proj_geom)
 
         if not transformed_geoms:
             result = mpath.Path(np.empty([0, 2]))
@@ -872,9 +878,17 @@ def contourf(self, *args, **kwargs):
                              ' Spherical contouring is not supported - '
                              ' consider using PlateCarree/RotatedPole.')
         if isinstance(t, ccrs.Projection):
-            kwargs['transform'] = t._as_mpl_transform(self)
+            kwargs['transform'] = t = t._as_mpl_transform(self)
         else:
             kwargs['transform'] = t
+
+        # Set flag to indicate correcting orientation of paths if not ccw
+        if isinstance(t, mtransforms.Transform):
+            for sub_trans, _ in t._iter_break_from_left_to_right():
+                if isinstance(sub_trans, InterProjectionTransform):
+                    if not hasattr(sub_trans, 'force_path_ccw'):
+                        sub_trans.force_path_ccw = True
+
         return matplotlib.axes.Axes.contourf(self, *args, **kwargs)
 
     # mpl 1.2.0rc2 compatibility. To be removed once 1.2 is released
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
@@ -25,10 +25,10 @@
    http://toblerity.github.com/shapely/manual.html#geometric-objects
 
 """
-
 import numpy as np
 import matplotlib.path
 from matplotlib.path import Path
+import shapely
 from shapely.geometry.collection import GeometryCollection
 from shapely.geometry.linestring import LineString
 from shapely.geometry.point import Point
@@ -130,7 +130,7 @@ def path_segments(path, transform=None, remove_nans=False, clip=None,
     return vertices[:-1, :], codes[:-1]
 
 
-def path_to_geos(path):
+def path_to_geos(path, force_ccw=False):
     """
     Creates a list of Shapely geometric objects from a
     :class:`matplotlib.path.Path`.
@@ -140,6 +140,12 @@ def path_to_geos(path):
     * path
         A :class:`matplotlib.path.Path` instance.
 
+    Kwargs:
+
+    * force_ccw
+        Boolean flag determining whether the path can be inverted to enforce
+        ccw.
+
     Returns:
         A list of :class:`shapely.geometry.polygon.Polygon`,
         :class:`shapely.geometry.linestring.LineString` and/or
@@ -166,7 +172,6 @@ def path_to_geos(path):
         # XXX A path can be given which does not end with close poly, in that
         # situation, we have to guess?
         # XXX Implement a point
-
         if (path_verts.shape[0] > 2 and
                 (path_codes[-1] == Path.CLOSEPOLY or
                  all(path_verts[0, :] == path_verts[-1, :]))):
@@ -198,6 +203,11 @@ def path_to_geos(path):
             geom = Polygon(external_geom.exterior, internal_polys)
         else:
             geom = external_geom
+
+        # Correctly orientate the polygon (ccw)
+        if force_ccw and not geom.exterior.is_ccw:
+            geom = shapely.geometry.polygon.orient(geom)
+
         geom_collection.append(geom)
 
     # If the geom_collection only contains LineStrings combine them


Test patch: