Repository: SciTools/cartopy

Issue:
mpl interface: make it possible to add filled polygons (coastlines)



Discussion:

Resolving patch:
diff --git a/lib/cartopy/io/shapereader.py b/lib/cartopy/io/shapereader.py
--- a/lib/cartopy/io/shapereader.py
+++ b/lib/cartopy/io/shapereader.py
@@ -217,31 +217,6 @@ def natural_earth(resolution='110m', category='physical', name='coastline', data
     return shapefiles[0]
 
 
-_geometry_to_path_cache = {}
-"""
-Caches a map between (id(geometries), hash(axes.projection)) to projected paths.
-This provides a significant boost when producing multiple maps of the same projection.
-"""
-def mpl_axes_plot(axes, geometries, facecolor='none', **kwargs):
-    """Plot lines on the given axes, given the geometries in lon/lat."""
-    # TODO: This interface should be exposed nicely on the geoaxes itself.
-    import matplotlib.collections as mcollections
-    import cartopy.mpl_integration.patch as patch
-
-    oid = (id(geometries), hash(axes.projection))
-    if oid in _geometry_to_path_cache:
-        paths = _geometry_to_path_cache[oid]
-    else:
-        paths = []
-        for geom in geometries:
-            paths.extend(patch.geos_to_path(axes.projection.project_geometry(geom)))
-        _geometry_to_path_cache[oid] = paths
-    axes.add_collection(mcollections.PathCollection(paths, facecolor=facecolor, 
-                                                    transform=axes.transData,
-                                                    **kwargs), 
-                        autolim=False)
-
-
 if __name__ == '__main__':
     coastlines = natural_earth(resolution='110m', category='physical', name='coastline')
     for record in Reader(coastlines).records():
diff --git a/lib/cartopy/mpl_integration/geoaxes.py b/lib/cartopy/mpl_integration/geoaxes.py
--- a/lib/cartopy/mpl_integration/geoaxes.py
+++ b/lib/cartopy/mpl_integration/geoaxes.py
@@ -51,6 +51,20 @@
 
 """
 
+_GEOMETRY_TO_PATH_CACHE = {}
+"""
+Caches a map between (id(geometries), hash(crs)) to projected paths.
+This provides a significant boost when producing multiple maps of the same projection.
+"""
+
+_NATURAL_EARTH_GEOM_CACHE = {}
+"""
+Caches a mapping between (name, category, resolution) and a tuple of the resulting geometries.
+Provides a significant performance benefit (when combined with object id caching 
+in GeoAxes.add_geometries) when producing multiple maps of the same projection.
+"""
+
+
 # XXX call this InterCRSTransform
 class InterProjectionTransform(mtransforms.Transform):
     """Transforms coordinates from the source_projection to the target_projection."""
@@ -202,14 +216,7 @@ def format_coord(self, x, y):
 
         return u'%.4g, %.4g (%f\u00b0%s, %f\u00b0%s)' % (x, y, abs(lat), ns, abs(lon), ew)
 
-    _coastlines_cache = {}
-    """
-    Caches a mapping between "resolution" and a tuple of the resulting geometries.
-    Provides a significant performance benefit (when combined with object id caching 
-    in shapereader.mpl_axes_plot) when producing multiple maps of the same projection.
-    """
-    
-    def coastlines(self, resolution='110m', **kwargs):
+    def coastlines(self, resolution='110m', color='black', **kwargs):
         """
         Adds coastal **outlines** to the current axes from the Natural Earth
         "coastline" shapefile collection.
@@ -226,36 +233,72 @@ def coastlines(self, resolution='110m', **kwargs):
             This means, if very high resolution coastlines are being used, performance is
             likely to be severely effected. This should be resolved transparently by v0.5.
 
+        """
+        kwargs = kwargs.copy()
+        kwargs.setdefault('edgecolor', color)
+        kwargs.setdefault('facecolor', 'none')
+        return self.natural_earth_shp(name='coastline',
+                                      resolution=resolution,
+                                      category='physical', **kwargs)
+        
+    def natural_earth_shp(self, name='land', resolution='110m', category='physical', 
+                          **kwargs):
+        """
+        Adds the geometries from the specified Natural Earth shapefile to the Axes as a 
+        :class:`~matplotlib.collections.PathCollection`.
+        
+        ``**kwargs`` are passed through to the :class:`~matplotlib.collections.PathCollection`
+        constructor.
+        
+        Returns the created :class:`~matplotlib.collections.PathCollection`.
+        
+        .. note::
+
+            Currently no clipping is done on the geometries before adding them to the axes.
+            This means, if very high resolution geometries are being used, performance is
+            likely to be severely effected. This should be resolved transparently by v0.5.
+        
         """
         import cartopy.io.shapereader as shapereader
 
-        if resolution not in self._coastlines_cache: 
+        kwargs.setdefault('edgecolor', 'face')
+        kwargs.setdefault('facecolor', _colors['land'])
+
+        key = (name, category, resolution) 
+
+        if key not in _NATURAL_EARTH_GEOM_CACHE: 
             coastline_path = shapereader.natural_earth(resolution=resolution,
-                                                       category='physical',
-                                                       name='coastline')
+                                                       category=category,
+                                                       name=name)
             geoms = tuple(shapereader.Reader(coastline_path).geometries())
             # put the geoms in the cache
-            self._coastlines_cache[resolution] = geoms 
+            _NATURAL_EARTH_GEOM_CACHE[key] = geoms 
         else:
-            geoms = self._coastlines_cache[resolution]
+            geoms = _NATURAL_EARTH_GEOM_CACHE[key]
         
-        shapereader.mpl_axes_plot(self, geoms, **kwargs)
-
-    # TODO: expose an interface similar to ax.add_image for shapely things, 
-    # and another for adding paths/patches (consider the land shapefile and gshhs as the primary usecases).
-#    def _coastlines_land(self, facecolor=colors['land'], **kwargs):
-#        import cartopy.io.shapereader as shapereader
-#
-#        land_path = shapereader.natural_earth(resolution='110m',
-#                                               category='physical',
-#                                               name='land')
-#
-#        paths = []
-#        for geom in shapereader.Reader(land_path).geometries():
-#
-#            paths.extend(patch.geos_to_path(self.projection.project_geometry(geom)))
-#        self.add_collection(mcollections.PathCollection(paths, facecolor=facecolor, **kwargs), autolim=False)
-#
+        return self.add_geometries(geoms, ccrs.Geodetic(), 
+                                   **kwargs)
+        
+    def add_geometries(self, geoms, crs, **collection_kwargs):
+        """
+        Add the given shapely geometries (in the given crs) to the axes as 
+        a :class:`~matplotlib.collections.PathCollection`.
+        
+        """
+        oid = (id(geoms), hash(crs), hash(self.projection))
+        if oid in _GEOMETRY_TO_PATH_CACHE:
+            paths = _GEOMETRY_TO_PATH_CACHE[oid]
+        else:
+            paths = []
+            for geom in geoms:
+                paths.extend(patch.geos_to_path(self.projection.project_geometry(geom, crs)))
+            _GEOMETRY_TO_PATH_CACHE[oid] = paths
+            
+        c = mcollections.PathCollection(paths, transform=self.projection, **collection_kwargs)
+        self.add_collection(c, autolim=False)
+        
+        return c
+                
 #    def gshhs(self, outline_color='k', land_fill='green', ocean_fill='None', resolution='coarse', domain=None):
 #        import cartopy.gshhs as gshhs
 #        from matplotlib.collections import PatchCollection
@@ -282,8 +325,7 @@ def coastlines(self, resolution='110m', **kwargs):
 #                                     facecolor=land_fill,
 #                                     zorder=2,
 #                                     )
-#        # XXX Should it update the limits??? (AND HOW???)
-#        self.add_collection(collection)
+#        self.add_collection(collection, autolim=False)
 
     def get_extent(self, crs=None):
         """


Test patch:
diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/natural_earth_interface.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/natural_earth_interface.png
new file mode 100644
Binary files /dev/null and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/natural_earth_interface.png differ
diff --git a/lib/cartopy/tests/mpl/test_caching.py b/lib/cartopy/tests/mpl/test_caching.py
--- a/lib/cartopy/tests/mpl/test_caching.py
+++ b/lib/cartopy/tests/mpl/test_caching.py
@@ -108,9 +108,9 @@ def test_shapefile_transform_cache():
     project_geometry_counter = CallCounter(ax.projection, 'project_geometry')
     
     with project_geometry_counter:
-        c = cartopy.io.shapereader.mpl_axes_plot(ax, geoms)
-        c = cartopy.io.shapereader.mpl_axes_plot(ax, geoms)
-        c = cartopy.io.shapereader.mpl_axes_plot(ax, geoms[:]) # <- caching will not work for this case
+        c = ax.add_geometries(geoms, ccrs.Geodetic())
+        c = ax.add_geometries(geoms, ccrs.Geodetic())
+        c = ax.add_geometries(geoms[:], ccrs.Geodetic()) # <- caching will not work for this case
     
     # before the performance enhancement, the count would have been n_calls * n_geom,
     # but should now be just n_geom * n_unique_id_geoms_calls (i.e. in this case 2 * n_calls))
diff --git a/lib/cartopy/tests/mpl/test_mpl_integration.py b/lib/cartopy/tests/mpl/test_mpl_integration.py
--- a/lib/cartopy/tests/mpl/test_mpl_integration.py
+++ b/lib/cartopy/tests/mpl/test_mpl_integration.py
@@ -173,11 +173,16 @@ def test_cursor_values():
     r = ax.format_coord(x, y)
     assert_equal(r.encode('ascii', 'ignore'), '1.606e+07, 2.363e+06 (22.095524N, 173.709136E)')
 
-#
-#@image_comparison(baseline_images=['image_transform'])
-#def test_image_transforms():
-#    plt.subplot(131, projection=ccrs.PlateCarree())
-#
+
+@image_comparison(baseline_images=['natural_earth_interface'])
+def test_axes_natural_earth_interface():
+    rob = ccrs.Robinson()
+    
+    ax = plt.axes(projection=rob)
+    
+    ax.natural_earth_shp('rivers-lake-centerlines', edgecolor='black', facecolor='none')
+    ax.natural_earth_shp('lakes', facecolor='blue')
+    
 
 if __name__=='__main__':
     import nose
diff --git a/lib/cartopy/tests/mpl/test_shapely_to_mpl.py b/lib/cartopy/tests/mpl/test_shapely_to_mpl.py
--- a/lib/cartopy/tests/mpl/test_shapely_to_mpl.py
+++ b/lib/cartopy/tests/mpl/test_shapely_to_mpl.py
@@ -128,7 +128,7 @@ def test_contour_interiors():
     cs = plt.contourf(lons, lats, data, numlev, transform=ccrs.Geodetic())
     
     ax.coastlines()    
-    
+
 
 if __name__=='__main__':
     import nose