Repository: SciTools/cartopy

Issue:
imshow() extrapolates RGBA data to fill the region
### Description
<!-- Please provide a general introduction to the issue/proposal. -->

Apologies for #1569. imshow() extrapolates RGBA data to fill the region.



<!--
If you are reporting a bug, attach the *entire* traceback from Python.

If you are proposing an enhancement/new feature, provide links to related articles, reference examples, etc.

If you are asking a question, please ask on StackOverflow and use the cartopy tag. All cartopy
questions on StackOverflow can be found at https://stackoverflow.com/questions/tagged/cartopy
-->

#### Code to reproduce

```python
import matplotlib.pyplot as plt
import numpy as np
import cartopy.crs as ccrs
import matplotlib.cm as cm
import matplotlib.colors as colors

z = np.random.rand(100, 100)

cmap = cm.get_cmap()
norm = colors.Normalize(vmin = 0, vmax = 1)

plt_crs = ccrs.LambertAzimuthalEqualArea()

latlon_crs = ccrs.PlateCarree()

fig = plt.figure(figsize = (5, 5))

ax = fig.add_subplot(1, 1, 1, projection = plt_crs)

ax.set_extent([-30, -20, 60, 70], crs = latlon_crs)

ax.gridlines()

ax.imshow(cm.ScalarMappable(norm = norm, cmap = cmap).to_rgba(z),
          extent = [-26, -24, 64, 66], transform = latlon_crs)

```
produces this:
![t1](https://user-images.githubusercontent.com/9297904/82512693-cebafe80-9ade-11ea-9f8a-3acb75871acd.png)

A hack like this corrects the problem:

```python
z1 = cm.ScalarMappable(norm = norm, cmap = cmap).to_rgba(z)
z1[0,:,3] = 0
z1[-1,:,3] = 0
z1[:,0,3] = 0
z1[:,-1,3] = 0

ax.imshow(z1, extent = [-26, -24, 64, 66], transform = latlon_crs)
```

![t2](https://user-images.githubusercontent.com/9297904/82512814-2eb1a500-9adf-11ea-8055-88eaa91fc060.png)


<details>
 <summary>Full environment definition</summary>

<!-- fill in the following information as appropriate -->

### Cartopy version

0.18.0

</details>



Discussion:
That hack hides all of the edge data though. A better way is
```python
ax.imshow(z, norm = norm, cmap = cmap,
          extent = [-26, -24, 64, 66], transform = latlon_crs)
```
which works as expected. My guess is there is some transform logic that expects MxN matrices and not MxNx4.
True, but the hack is required for my non-MWE because I use matplotlib's `LightSource.shade()`, which returns a RGBA array.

I have time to fix this if no changes are needed for the next month or so.

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
@@ -1294,16 +1294,23 @@ def imshow(self, img, *args, **kwargs):
             kwargs['alpha'] = alpha
 
             # As a workaround to a matplotlib limitation, turn any images
-            # which are RGB with a mask into RGBA images with an alpha
-            # channel.
+            # which are RGB(A) with a mask into unmasked RGBA images with alpha
+            # put into the A channel.
             if (isinstance(img, np.ma.MaskedArray) and
-                    img.shape[2:3] == (3, ) and
+                    len(img.shape) > 2 and
                     img.mask is not False):
-                old_img = img
+                # if we don't pop alpha, imshow will apply (erroneously?) a
+                # 1D alpha to the RGBA array
+                # kwargs['alpha'] is guaranteed to be either 1D, 2D, or None
+                alpha = kwargs.pop('alpha')
+                old_img = img[:, :, 0:3]
                 img = np.zeros(img.shape[:2] + (4, ), dtype=img.dtype)
                 img[:, :, 0:3] = old_img
                 # Put an alpha channel in if the image was masked.
-                img[:, :, 3] = ~ np.any(old_img.mask, axis=2)
+                if not np.any(alpha):
+                    alpha = 1
+                img[:, :, 3] = np.ma.filled(alpha, fill_value=0) * \
+                    (~np.any(old_img.mask, axis=2))
                 if img.dtype.kind == 'u':
                     img[:, :, 3] *= 255
 


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
@@ -9,6 +9,8 @@
 
 import numpy as np
 import matplotlib.pyplot as plt
+import matplotlib.cm as cm
+import matplotlib.colors as colors
 from PIL import Image
 import pytest
 import shapely.geometry as sgeom
@@ -150,6 +152,33 @@ def test_imshow_wrapping():
     assert ax.get_xlim() == (-180, 180)
 
 
+def test_imshow_rgba():
+    # tests that the alpha of a RGBA array passed to imshow is set to 0
+    # instead of masked
+    z = np.ones((100, 100))*0.5
+    cmap = cm.get_cmap()
+    norm = colors.Normalize(vmin=0, vmax=1)
+    z1 = cmap(norm(z))
+    plt_crs = ccrs.LambertAzimuthalEqualArea()
+    latlon_crs = ccrs.PlateCarree()
+    ax = plt.axes(projection=plt_crs)
+    ax.set_extent([-30, -20, 60, 70], crs=latlon_crs)
+    img = ax.imshow(z1, extent=[-26, -24, 64, 66], transform=latlon_crs)
+    assert sum(img.get_array().data[:, 0, 3]) == 0
+
+
+def test_imshow_rgb():
+    # tests that the alpha of a RGB array passed to imshow is set to 0
+    # instead of masked
+    z = np.ones((100, 100, 3))*0.5
+    plt_crs = ccrs.LambertAzimuthalEqualArea()
+    latlon_crs = ccrs.PlateCarree()
+    ax = plt.axes(projection=plt_crs)
+    ax.set_extent([-30, -20, 60, 70], crs=latlon_crs)
+    img = ax.imshow(z, extent=[-26, -24, 64, 66], transform=latlon_crs)
+    assert sum(img.get_array().data[:, 0, 3]) == 0
+
+
 @pytest.mark.xfail((5, 0, 0) <= ccrs.PROJ4_VERSION < (5, 1, 0),
                    reason='Proj Orthographic projection is buggy.',
                    strict=True)