Repository: SciTools/cartopy

Issue:
central_longitude=0.0 and central_longtitude=0 give different results
I would like to project some simple data using a PlateCarree projection. When I set `central_longitude=0` everything works as expected, but if I specify `central_longitude=0.0` their are major problems, as shown in the image below.

```
import cartopy.crs as ccrs
interval = 30

# set the longitudes and latitudes to span 0-360 and 90 to -90.
lat = np.arange(90, -90-interval, -interval)
lon = np.arange(0, 360+interval, interval)
longrid, latgrid = np.meshgrid(lon, lat)

# make a grid of data that representats the function sin(lon)*cos(lat). Note that the value
# represents the center of a pixel.
data = np.sin(np.deg2rad(longrid)) * np.cos(np.deg2rad(latgrid))

# set the extent of the data coordinates: the boundaries are extended by half a pixel in
# all directions since the function uses gridline coordinates.
extent=(lon[0]-interval/2, lon[-1]+interval/2, lat[-1]-interval/2, lat[0]+interval/2)

# plot using a PlateCarree projection
fig = plt.figure()
ax1 = fig.add_subplot(121, projection=ccrs.PlateCarree(central_longitude=0))
ax1.imshow(data, transform=ccrs.PlateCarree(), extent=extent, origin='upper')
ax1.gridlines()
ax1.set_title('central_longitude=0')

ax2 = fig.add_subplot(122, projection=ccrs.PlateCarree(central_longitude=0.0))
ax2.imshow(data, transform=ccrs.PlateCarree(), extent=extent, origin='upper')
ax2.gridlines()
ax2.set_title('central_longitude=0.0')
```
![test](https://user-images.githubusercontent.com/8697285/72143487-12666480-3397-11ea-8e60-1e1a9d5b8678.png)



Discussion:
I can reproduce. It looks like an issue with the `imshow` wrapping to me.
The problem is not just with `central_longitude=0`, but rather that the imshow transform is the same/different than the axis projection.
`axis_proj = ccrs.PlateCarree(0)`
`data_proj = ccrs.PlateCarree(0.)`

if `axis_proj == data_proj` then there is a shortcut path that imshow takes and simply plots the image on the current axes rather than warping/wrapping the image.
https://github.com/SciTools/cartopy/blob/da56f88528e21866c9370fe656ba5cdfc100a6d8/lib/cartopy/mpl/geoaxes.py#L1237-L1240
Your first axis you have different projections (one with `0`, the other default of `0.`), but in the second axis you have the same projection and the shortcut happens in imshow.

In your second case, the xlim of the axis is (-15, 375) instead of (-180, 180) which would match the extent of the axis.

It definitely seems like a bad feature (bug) to me.

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
@@ -1,4 +1,4 @@
-# (C) British Crown Copyright 2011 - 2019, Met Office
+# (C) British Crown Copyright 2011 - 2020, Met Office
 #
 # This file is part of cartopy.
 #
@@ -1223,20 +1223,30 @@ def imshow(self, img, *args, **kwargs):
             Default is ``'lower'``.
 
         """
-        transform = kwargs.pop('transform', None)
         if 'update_datalim' in kwargs:
             raise ValueError('The update_datalim keyword has been removed in '
                              'imshow. To hold the data and view limits see '
                              'GeoAxes.hold_limits.')
 
+        transform = kwargs.pop('transform', None)
+        extent = kwargs.get('extent', None)
         kwargs.setdefault('origin', 'lower')
 
         same_projection = (isinstance(transform, ccrs.Projection) and
                            self.projection == transform)
-
-        if transform is None or transform == self.transData or same_projection:
-            if isinstance(transform, ccrs.Projection):
-                transform = transform._as_mpl_transform(self)
+        # Only take the shortcut path if the image is within the current
+        # bounds (+/- threshold) of the projection
+        x0, x1 = self.projection.x_limits
+        y0, y1 = self.projection.y_limits
+        eps = self.projection.threshold
+        inside_bounds = (extent is None or
+                         (x0 - eps <= extent[0] <= x1 + eps and
+                          x0 - eps <= extent[1] <= x1 + eps and
+                          y0 - eps <= extent[2] <= y1 + eps and
+                          y0 - eps <= extent[3] <= y1 + eps))
+
+        if (transform is None or transform == self.transData or
+                same_projection and inside_bounds):
             result = matplotlib.axes.Axes.imshow(self, img, *args, **kwargs)
         else:
             extent = kwargs.pop('extent', None)


Test patch:
diff --git a/lib/cartopy/tests/mpl/test_images.py b/lib/cartopy/tests/mpl/test_images.py
--- a/lib/cartopy/tests/mpl/test_images.py
+++ b/lib/cartopy/tests/mpl/test_images.py
@@ -1,4 +1,4 @@
-# (C) British Crown Copyright 2011 - 2019, Met Office
+# (C) British Crown Copyright 2011 - 2020, Met Office
 #
 # This file is part of cartopy.
 #
@@ -151,6 +151,16 @@ def test_imshow_projected():
     ax.imshow(img, extent=img_extent, origin='upper', transform=source_proj)
 
 
+def test_imshow_wrapping():
+    ax = plt.axes(projection=ccrs.PlateCarree(central_longitude=0.0))
+    # Set the extent outside of the current projection domain to ensure
+    # it is wrapped back to the (-180, 180) extent of the projection
+    ax.imshow(np.random.random((10, 10)), transform=ccrs.PlateCarree(),
+              extent=(0, 360, -90, 90))
+
+    assert ax.get_xlim() == (-180, 180)
+
+
 @pytest.mark.xfail((5, 0, 0) <= ccrs.PROJ4_VERSION < (5, 1, 0),
                    reason='Proj Orthographic projection is buggy.',
                    strict=True)