diff --git a/lib/cartopy/mpl/geoaxes.py b/lib/cartopy/mpl/geoaxes.py index 0f43aebb2..4f73e8032 100644 --- a/lib/cartopy/mpl/geoaxes.py +++ b/lib/cartopy/mpl/geoaxes.py @@ -1793,7 +1793,7 @@ def pcolormesh(self, *args, **kwargs): """ # Add in an argument checker to handle Matplotlib's potential # interpolation when coordinate wraps are involved - args = self._wrap_args(*args, **kwargs) + args, kwargs = self._wrap_args(*args, **kwargs) result = matplotlib.axes.Axes.pcolormesh(self, *args, **kwargs) # Wrap the quadrilaterals if necessary result = self._wrap_quadmesh(result, **kwargs) @@ -1815,8 +1815,11 @@ def _wrap_args(self, *args, **kwargs): if not (kwargs.get('shading', default_shading) in ('nearest', 'auto') and len(args) == 3 and getattr(kwargs.get('transform'), '_wrappable', False)): - return args + return args, kwargs + # We have changed the shading from nearest/auto to flat + # due to the addition of an extra coordinate + kwargs['shading'] = 'flat' X = np.asanyarray(args[0]) Y = np.asanyarray(args[1]) nrows, ncols = np.asanyarray(args[2]).shape @@ -1852,7 +1855,7 @@ def _interp_grid(X, wrap=0): X = _interp_grid(X.T, wrap=xwrap).T Y = _interp_grid(Y.T).T - return (X, Y, args[2]) + return (X, Y, args[2]), kwargs def _wrap_quadmesh(self, collection, **kwargs): """ @@ -1868,8 +1871,13 @@ def _wrap_quadmesh(self, collection, **kwargs): # Get the quadmesh data coordinates coords = collection._coordinates Ny, Nx, _ = coords.shape + if kwargs.get('shading') == 'gouraud': + # Gouraud shading has the same shape for coords and data + data_shape = Ny, Nx + else: + data_shape = Ny - 1, Nx - 1 # data array - C = collection.get_array().reshape((Ny - 1, Nx - 1)) + C = collection.get_array().reshape(data_shape) transformed_pts = self.projection.transform_points( t, coords[..., 0], coords[..., 1]) @@ -1898,6 +1906,23 @@ def _wrap_quadmesh(self, collection, **kwargs): # No wrapping needed return collection + # Wrapping with gouraud shading is error-prone. We will do our best, + # but pcolor does not handle gouraud shading, so there needs to be + # another way to handle the wrapped cells. + if kwargs.get('shading') == 'gouraud': + warnings.warn("Handling wrapped coordinates with gouraud " + "shading is likely to introduce artifacts. " + "It is recommended to remove the wrap manually " + "before calling pcolormesh.") + # With gouraud shading, we actually want an (Ny, Nx) shaped mask + gmask = np.zeros(data_shape, dtype=bool) + # If any of the cells were wrapped, apply it to all 4 corners + gmask[:-1, :-1] |= mask + gmask[1:, :-1] |= mask + gmask[1:, 1:] |= mask + gmask[:-1, 1:] |= mask + mask = gmask + # We have quadrilaterals that cross the wrap boundary # Now, we need to update the original collection with # a mask over those cells and use pcolor to draw those @@ -1978,7 +2003,11 @@ def pcolor(self, *args, **kwargs): """ # Add in an argument checker to handle Matplotlib's potential # interpolation when coordinate wraps are involved - args = self._wrap_args(*args, **kwargs) + args, kwargs = self._wrap_args(*args, **kwargs) + if matplotlib.__version__ < "3.3": + # MPL 3.3 introduced the shading option, and it isn't + # handled before that for pcolor calls. + kwargs.pop('shading', None) result = matplotlib.axes.Axes.pcolor(self, *args, **kwargs) # Update the datalim for this pcolor. diff --git a/lib/cartopy/mpl/gridliner.py b/lib/cartopy/mpl/gridliner.py index 986eb7732..16caae1dc 100644 --- a/lib/cartopy/mpl/gridliner.py +++ b/lib/cartopy/mpl/gridliner.py @@ -796,8 +796,6 @@ def update_artist(artist, renderer): # Cache a few things so they aren't re-calculated in the loops. crs_transform = self._crs_transform().transform inverse_data_transform = self.axes.transData.inverted().transform_point - if self.x_inline or self.y_inline: - pc_transform = PlateCarree() for xylabel, lines, line_ticks, formatter, label_style in ( ('x', lon_lines, lon_ticks, @@ -899,7 +897,7 @@ def update_artist(artist, renderer): # Initial text specs x0, y0 = pt0 if x_inline or y_inline: - kw = {'rotation': 0, 'transform': pc_transform, + kw = {'rotation': 0, 'transform': self.crs, 'ha': 'center', 'va': 'center'} loc = 'inline' else: diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/streamplot.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/streamplot.png index 2f72d9518..6911b512b 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/streamplot.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/streamplot.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/xticks_cylindrical.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/xticks_cylindrical.png index e213b16cd..e7dac31bf 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/xticks_cylindrical.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/xticks_cylindrical.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/xticks_no_transform.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/xticks_no_transform.png index 8ed5bf8cb..50b447625 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/xticks_no_transform.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/xticks_no_transform.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/xyticks.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/xyticks.png index 3c0af1f61..e2520af1c 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/xyticks.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/xyticks.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/yticks_cylindrical.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/yticks_cylindrical.png index 248a4071e..15e8399ca 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/yticks_cylindrical.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/yticks_cylindrical.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/yticks_no_transform.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/yticks_no_transform.png index 6b0574149..622ae8be2 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/yticks_no_transform.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/yticks_no_transform.png differ diff --git a/lib/cartopy/tests/mpl/test_mpl_integration.py b/lib/cartopy/tests/mpl/test_mpl_integration.py index 67ae34144..3315d0b7b 100644 --- a/lib/cartopy/tests/mpl/test_mpl_integration.py +++ b/lib/cartopy/tests/mpl/test_mpl_integration.py @@ -583,7 +583,7 @@ def test_pcolormesh_diagonal_wrap(): # and the bottom edge on the other gets wrapped properly xs = [[160, 170], [190, 200]] ys = [[-10, -10], [10, 10]] - zs = [[0, 1], [0, 1]] + zs = [[0]] ax = plt.axes(projection=ccrs.PlateCarree()) mesh = ax.pcolormesh(xs, ys, zs) @@ -652,6 +652,31 @@ def test_pcolormesh_wrap_set_array(): coll.set_array(Z.ravel()) +@pytest.mark.parametrize('shading, input_size, expected', [ + pytest.param('auto', 3, 4, id='auto same size'), + pytest.param('auto', 4, 4, id='auto input larger'), + pytest.param('nearest', 3, 4, id='nearest same size'), + pytest.param('nearest', 4, 4, id='nearest input larger'), + pytest.param('flat', 4, 4, id='flat input larger'), + pytest.param('gouraud', 3, 3, id='gouraud same size') +]) +def test_pcolormesh_shading(shading, input_size, expected): + # Testing that the coordinates are all broadcast as expected with + # the various shading options + # The data shape is (3, 3) and we are changing the input shape + # based upon that + ax = plt.axes(projection=ccrs.PlateCarree()) + + x = np.arange(input_size) + y = np.arange(input_size) + d = np.zeros((3, 3)) + + coll = ax.pcolormesh(x, y, d, shading=shading) + # We can use coll.get_coordinates() once MPL >= 3.5 is required + # For now, we use the private variable for testing + assert coll._coordinates.shape == (expected, expected, 2) + + @pytest.mark.natural_earth @ImageTesting(['quiver_plate_carree']) def test_quiver_plate_carree(): @@ -830,8 +855,10 @@ def test_barbs_1d_transformed(): @pytest.mark.natural_earth -@ImageTesting(['streamplot'], style='mpl20', - tolerance=42 if MPL_VERSION < parse_version('3.2') else 0.54) +@ImageTesting( + ['streamplot'], style='mpl20', + tolerance=(42 if MPL_VERSION.release[:2] < (3, 2) else + 9.77 if MPL_VERSION.release[:2] < (3, 5) else 0.5)) def test_streamplot(): x = np.arange(-60, 42.5, 2.5) y = np.arange(30, 72.5, 2.5) diff --git a/lib/cartopy/tests/mpl/test_pseudo_color.py b/lib/cartopy/tests/mpl/test_pseudo_color.py index 4518af985..c3a822f29 100644 --- a/lib/cartopy/tests/mpl/test_pseudo_color.py +++ b/lib/cartopy/tests/mpl/test_pseudo_color.py @@ -14,7 +14,7 @@ def test_pcolormesh_partially_masked(): - data = np.ma.masked_all((40, 30)) + data = np.ma.masked_all((39, 29)) data[0:100] = 10 # Check that a partially masked data array does trigger a pcolor call. @@ -27,7 +27,7 @@ def test_pcolormesh_partially_masked(): def test_pcolormesh_invisible(): - data = np.zeros((3, 3)) + data = np.zeros((2, 2)) # Check that a fully invisible mesh doesn't fail. with mock.patch('cartopy.mpl.geoaxes.GeoAxes.pcolor') as pcolor: diff --git a/lib/cartopy/tests/mpl/test_ticks.py b/lib/cartopy/tests/mpl/test_ticks.py index d79d55f5c..2eb6c470e 100644 --- a/lib/cartopy/tests/mpl/test_ticks.py +++ b/lib/cartopy/tests/mpl/test_ticks.py @@ -11,12 +11,8 @@ from cartopy.tests.mpl import ImageTesting -ticks_tolerance = 7 - - @pytest.mark.natural_earth -@ImageTesting(['xticks_no_transform'], - tolerance=ticks_tolerance) +@ImageTesting(['xticks_no_transform']) def test_set_xticks_no_transform(): ax = plt.axes(projection=ccrs.PlateCarree()) ax.coastlines('110m') @@ -26,12 +22,9 @@ def test_set_xticks_no_transform(): @pytest.mark.natural_earth -@ImageTesting(['xticks_cylindrical'], - tolerance=ticks_tolerance) +@ImageTesting(['xticks_cylindrical']) def test_set_xticks_cylindrical(): - ax = plt.axes(projection=ccrs.Mercator( - min_latitude=-85., - max_latitude=85.)) + ax = plt.axes(projection=ccrs.Mercator(min_latitude=-85, max_latitude=85)) ax.coastlines('110m') ax.xaxis.set_major_formatter(LongitudeFormatter(degree_symbol='')) ax.set_xticks([-180, -90, 0, 90, 180], crs=ccrs.PlateCarree()) @@ -48,27 +41,24 @@ def test_set_xticks_non_cylindrical(): @pytest.mark.natural_earth -@ImageTesting(['yticks_no_transform'], - tolerance=ticks_tolerance) +@ImageTesting(['yticks_no_transform']) def test_set_yticks_no_transform(): ax = plt.axes(projection=ccrs.PlateCarree()) ax.coastlines('110m') ax.yaxis.set_major_formatter(LatitudeFormatter(degree_symbol='')) ax.set_yticks([-60, -30, 0, 30, 60]) - ax.set_yticks([-75, -45, 15, 45, 75], minor=True) + ax.set_yticks([-75, -45, -15, 15, 45, 75], minor=True) @pytest.mark.natural_earth -@ImageTesting(['yticks_cylindrical'], - tolerance=ticks_tolerance) +@ImageTesting(['yticks_cylindrical']) def test_set_yticks_cylindrical(): - ax = plt.axes(projection=ccrs.Mercator( - min_latitude=-85., - max_latitude=85.)) + ax = plt.axes(projection=ccrs.Mercator(min_latitude=-85, max_latitude=85)) ax.coastlines('110m') ax.yaxis.set_major_formatter(LatitudeFormatter(degree_symbol='')) ax.set_yticks([-60, -30, 0, 30, 60], crs=ccrs.PlateCarree()) - ax.set_yticks([-75, -45, 15, 45, 75], minor=True, crs=ccrs.PlateCarree()) + ax.set_yticks([-75, -45, -15, 15, 45, 75], minor=True, + crs=ccrs.PlateCarree()) def test_set_yticks_non_cylindrical(): @@ -76,12 +66,13 @@ def test_set_yticks_non_cylindrical(): with pytest.raises(RuntimeError): ax.set_yticks([-60, -30, 0, 30, 60], crs=ccrs.Geodetic()) with pytest.raises(RuntimeError): - ax.set_yticks([-75, -45, 15, 45, 75], minor=True, crs=ccrs.Geodetic()) + ax.set_yticks([-75, -45, -15, 15, 45, 75], minor=True, + crs=ccrs.Geodetic()) plt.close() @pytest.mark.natural_earth -@ImageTesting(['xyticks'], tolerance=ticks_tolerance) +@ImageTesting(['xyticks']) def test_set_xyticks(): fig = plt.figure(figsize=(10, 10)) projections = (ccrs.PlateCarree(), diff --git a/lib/cartopy/tests/test_crs.py b/lib/cartopy/tests/test_crs.py index 25bf934f0..2d01015db 100644 --- a/lib/cartopy/tests/test_crs.py +++ b/lib/cartopy/tests/test_crs.py @@ -6,6 +6,8 @@ import copy from io import BytesIO +import os +from pathlib import Path import pickle import numpy as np @@ -46,6 +48,20 @@ def test_osni(self, approx): 3) def _check_osgb(self, osgb): + precision = 1 + + if os.environ.get('PROJ_NETWORK') != 'ON': + grid_name = 'uk_os_OSTN15_NTv2_OSGBtoETRS.tif' + available = ( + Path(pyproj.datadir.get_data_dir(), grid_name).exists() or + Path(pyproj.datadir.get_user_data_dir(), grid_name).exists() + ) + if not available: + import warnings + warnings.warn(f'{grid_name} is unavailable; ' + 'testing OSGB at reduced precision') + precision = -1 + ll = ccrs.Geodetic() # results obtained by streetmap.co.uk. @@ -53,12 +69,10 @@ def _check_osgb(self, osgb): east, north = np.array([295132.1, 63512.6], dtype=np.double) # note the handling of precision here... - assert_arr_almost_eq(np.array(osgb.transform_point(lon, lat, ll)), - np.array([east, north]), - 1) - assert_arr_almost_eq(ll.transform_point(east, north, osgb), - [lon, lat], - 2) + assert_almost_equal(osgb.transform_point(lon, lat, ll), [east, north], + decimal=precision) + assert_almost_equal(ll.transform_point(east, north, osgb), [lon, lat], + decimal=2) r_lon, r_lat = ll.transform_point(east, north, osgb) r_inverted = np.array(osgb.transform_point(r_lon, r_lat, ll)) diff --git a/setup.py b/setup.py index c04029448..6517b5e41 100644 --- a/setup.py +++ b/setup.py @@ -26,11 +26,11 @@ import fnmatch import os +import shutil import subprocess import warnings from collections import defaultdict -from distutils.spawn import find_executable -from distutils.sysconfig import get_config_var +from sysconfig import get_config_var from setuptools import Command, Extension, convert_path, setup @@ -142,7 +142,7 @@ def find_package_tree(root_path, root_package): # Proj def find_proj_version_by_program(conda=None): - proj = find_executable('proj') + proj = shutil.which('proj') if proj is None: print( 'Proj {} must be installed.'.format( diff --git a/tools/cartopy_feature_download.py b/tools/cartopy_feature_download.py index 83cc91311..eea15059b 100755 --- a/tools/cartopy_feature_download.py +++ b/tools/cartopy_feature_download.py @@ -64,6 +64,7 @@ ('cultural', 'admin_0_pacific_groupings', '110m'), ('cultural', 'admin_1_states_provinces', '110m'), ('cultural', 'admin_1_states_provinces_lines', '110m'), + ('cultural', 'admin_1_states_provinces_lakes', ALL_SCALES), ), }