diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index bdd1d87c5..f6426ac64 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -9,3 +9,6 @@ c00e0adae1b1e8695e173f4482369face82206ee # style: adopt isort to enforce import ordering (#2150) 030a78b0f5cd6a88b04dd2fbe78f12b4b3e5e60d + +# style: adopt ruff formatter (#2540) +a44e1d66406b69a3fb11d6c2b2d91c8cedb65013 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ee88a35af..65cb035e3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,13 +31,11 @@ repos: - id: codespell types_or: [python, markdown, rst] additional_dependencies: [tomli] - - repo: https://github.com/aio-libs/sort-all - rev: "v1.3.0" - hooks: - - id: sort-all - types: [file, python] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.11.4' + rev: 'v0.11.11' hooks: - - id: ruff - args: [--fix] + # Run the linter. + - id: ruff-check + args: [ --fix ] + # Run the formatter. + - id: ruff-format diff --git a/benchmarks/cases/gridliner.py b/benchmarks/cases/gridliner.py index 4c5c4d485..1e9070442 100644 --- a/benchmarks/cases/gridliner.py +++ b/benchmarks/cases/gridliner.py @@ -18,8 +18,9 @@ class Gridliner: def setup(self, draw_labels, inline): self.proj = ccrs.PlateCarree() fig, ax = plt.subplots(subplot_kw=dict(projection=self.proj)) - ax.gridlines(draw_labels=draw_labels, x_inline=inline, y_inline=inline, - auto_inline=False) + ax.gridlines( + draw_labels=draw_labels, x_inline=inline, y_inline=inline, auto_inline=False + ) self.figure = fig def time_gridlines(self, draw_labels, inline): diff --git a/benchmarks/cases/project_linear.py b/benchmarks/cases/project_linear.py index a486c0e9d..dcf838141 100644 --- a/benchmarks/cases/project_linear.py +++ b/benchmarks/cases/project_linear.py @@ -9,15 +9,15 @@ class Suite: params = [ - ('PlateCarree', 'NorthPolarStereo', 'Robinson', - 'InterruptedGoodeHomolosine'), + ('PlateCarree', 'NorthPolarStereo', 'Robinson', 'InterruptedGoodeHomolosine'), ('110m', '50m'), ] param_names = ['projection', 'resolution'] def setup(self, projection, resolution): shpfilename = shpreader.natural_earth( - resolution=resolution, category='physical', name='ocean') + resolution=resolution, category='physical', name='ocean' + ) reader = shpreader.Reader(shpfilename) oceans = list(reader.geometries()) self.geoms = oceans[0] diff --git a/docs/make_projection.py b/docs/make_projection.py index 3ccf018d5..4b7f49a4f 100644 --- a/docs/make_projection.py +++ b/docs/make_projection.py @@ -19,8 +19,10 @@ ccrs.RotatedPole: {'pole_longitude': 177.5, 'pole_latitude': 37.5}, ccrs.AzimuthalEquidistant: {'central_latitude': 90}, ccrs.NearsidePerspective: { - 'central_longitude': -3.53, 'central_latitude': 50.72, - 'satellite_height': 10.0e6}, + 'central_longitude': -3.53, + 'central_latitude': 50.72, + 'satellite_height': 10.0e6, + }, ccrs.OSGB: {'approx': False}, ccrs.OSNI: {'approx': False}, ccrs.TransverseMercator: {'approx': False}, @@ -35,8 +37,11 @@ def plate_carree_plot(): for i in range(0, nplots): central_longitude = 0 if i == 0 else 180 ax = fig.add_subplot( - nplots, 1, i+1, - projection=ccrs.PlateCarree(central_longitude=central_longitude)) + nplots, + 1, + i + 1, + projection=ccrs.PlateCarree(central_longitude=central_longitude), + ) ax.coastlines(resolution='110m') ax.gridlines() @@ -44,15 +49,20 @@ def plate_carree_plot(): def igh_plot(): fig = plt.figure(figsize=(6.9228, 6)) - ax1 = fig.add_subplot(2, 1, 1, - projection=ccrs.InterruptedGoodeHomolosine( - emphasis='land')) + ax1 = fig.add_subplot( + 2, 1, 1, projection=ccrs.InterruptedGoodeHomolosine(emphasis='land') + ) ax1.coastlines(resolution='110m') ax1.gridlines() - ax2 = fig.add_subplot(2, 1, 2, - projection=ccrs.InterruptedGoodeHomolosine( - central_longitude=-160, emphasis='ocean')) + ax2 = fig.add_subplot( + 2, + 1, + 2, + projection=ccrs.InterruptedGoodeHomolosine( + central_longitude=-160, emphasis='ocean' + ), + ) ax2.coastlines(resolution='110m') ax2.gridlines() @@ -63,9 +73,9 @@ def utm_plot(): fig = plt.figure(figsize=(10, 3)) for i in range(0, nplots): - ax = fig.add_subplot(1, nplots, i+1, - projection=ccrs.UTM(zone=i+1, - southern_hemisphere=True)) + ax = fig.add_subplot( + 1, nplots, i + 1, projection=ccrs.UTM(zone=i + 1, southern_hemisphere=True) + ) ax.coastlines(resolution='110m') ax.gridlines() @@ -77,33 +87,56 @@ def utm_plot(): } -COASTLINE_RESOLUTION = {ccrs.OSNI: '10m', - ccrs.OSGB: '50m', - ccrs.EuroPP: '50m', - ccrs.LambertZoneII: '10m'} +COASTLINE_RESOLUTION = { + ccrs.OSNI: '10m', + ccrs.OSGB: '50m', + ccrs.EuroPP: '50m', + ccrs.LambertZoneII: '10m', +} -PRJ_SORT_ORDER = {'PlateCarree': 1, - 'Mercator': 2, 'Mollweide': 2, 'Robinson': 2, - 'TransverseMercator': 2, 'ObliqueMercator': 2, - 'LambertCylindrical': 2, - 'LambertConformal': 2, 'EquidistantConic': 2, - 'Stereographic': 2, 'Miller': 2, - 'Orthographic': 2, 'UTM': 2, 'AlbersEqualArea': 2, - 'AzimuthalEquidistant': 2, 'Sinusoidal': 2, - 'InterruptedGoodeHomolosine': 3, 'RotatedPole': 3, - 'OSGB': 4, 'LambertZoneII': 4.1, 'EuroPP': 5, - 'Geostationary': 6, 'NearsidePerspective': 7, - 'EckertI': 8.1, 'EckertII': 8.2, 'EckertIII': 8.3, - 'EckertIV': 8.4, 'EckertV': 8.5, 'EckertVI': 8.6, - 'Spilhaus': 9} +PRJ_SORT_ORDER = { + 'PlateCarree': 1, + 'Mercator': 2, + 'Mollweide': 2, + 'Robinson': 2, + 'TransverseMercator': 2, + 'ObliqueMercator': 2, + 'LambertCylindrical': 2, + 'LambertConformal': 2, + 'EquidistantConic': 2, + 'Stereographic': 2, + 'Miller': 2, + 'Orthographic': 2, + 'UTM': 2, + 'AlbersEqualArea': 2, + 'AzimuthalEquidistant': 2, + 'Sinusoidal': 2, + 'InterruptedGoodeHomolosine': 3, + 'RotatedPole': 3, + 'OSGB': 4, + 'LambertZoneII': 4.1, + 'EuroPP': 5, + 'Geostationary': 6, + 'NearsidePerspective': 7, + 'EckertI': 8.1, + 'EckertII': 8.2, + 'EckertIII': 8.3, + 'EckertIV': 8.4, + 'EckertV': 8.5, + 'EckertVI': 8.6, + 'Spilhaus': 9, +} def find_projections(): for obj_name, o in vars(ccrs).copy().items(): - if isinstance(o, type) and issubclass(o, ccrs.Projection) and \ - not obj_name.startswith('_') and obj_name not in ['Projection']: - + if ( + isinstance(o, type) + and issubclass(o, ccrs.Projection) + and not obj_name.startswith('_') + and obj_name not in ['Projection'] + ): yield o @@ -112,12 +145,11 @@ def create_instance(prj_cls, instance_args): # Format instance arguments into strings instance_params = ',\n '.join( - f'{k}={v}' - for k, v in sorted(instance_args.items())) + f'{k}={v}' for k, v in sorted(instance_args.items()) + ) if instance_params: - instance_params = '\n ' \ - + instance_params + instance_params = '\n ' + instance_params instance_creation_code = f'{name}({instance_params})' @@ -147,8 +179,7 @@ def create_instance(prj_cls, instance_args): table.write(textwrap.dedent(header)) def prj_class_sorter(cls): - return (PRJ_SORT_ORDER.get(cls.__name__, 100), - cls.__name__) + return (PRJ_SORT_ORDER.get(cls.__name__, 100), cls.__name__) for prj in sorted(find_projections(), key=prj_class_sorter): name = prj.__name__ @@ -164,8 +195,7 @@ def prj_class_sorter(cls): prj_inst, instance_repr = create_instance(prj, instance_args) - aspect = (np.diff(prj_inst.x_limits) / - np.diff(prj_inst.y_limits))[0] + aspect = (np.diff(prj_inst.x_limits) / np.diff(prj_inst.y_limits))[0] width = 3 * aspect width = f'{width:.4f}'.rstrip('0').rstrip('.') @@ -183,16 +213,17 @@ def prj_class_sorter(cls): ax.gridlines() - """).format(width=width, - proj_constructor=instance_repr, - coastline_resolution=COASTLINE_RESOLUTION.get(prj, - '110m')) + """).format( + width=width, + proj_constructor=instance_repr, + coastline_resolution=COASTLINE_RESOLUTION.get(prj, '110m'), + ) else: func = MULTI_PLOT_CASES[prj] lines = inspect.getsourcelines(func) - func_code = "".join(lines[0][1:]) + func_code = ''.join(lines[0][1:]) code = textwrap.dedent(""" .. plot:: diff --git a/docs/source/conf.py b/docs/source/conf.py index f4ab27101..5b5fffd0d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -37,18 +37,18 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.coverage', - 'sphinx.ext.viewcode', - 'sphinx.ext.extlinks', - 'sphinx.ext.autosummary', - 'matplotlib.sphinxext.plot_directive', - 'matplotlib.sphinxext.roles', - 'sphinx_gallery.gen_gallery', - 'sphinx.ext.napoleon' - ] + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode', + 'sphinx.ext.extlinks', + 'sphinx.ext.autosummary', + 'matplotlib.sphinxext.plot_directive', + 'matplotlib.sphinxext.roles', + 'sphinx_gallery.gen_gallery', + 'sphinx.ext.napoleon', +] # generate autosummary even if no references autosummary_generate = True @@ -69,8 +69,10 @@ # General information about the project. project = 'cartopy' -copyright = ('2011 - 2018 British Crown Copyright, ' - f'2018 - {datetime.now().year} Cartopy contributors') +copyright = ( + '2011 - 2018 British Crown Copyright, ' + f'2018 - {datetime.now().year} Cartopy contributors' +) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -82,18 +84,22 @@ release = cartopy.__version__ -subsection_order = ExplicitOrder(['../../examples/lines_and_polygons', - '../../examples/scalar_data', - '../../examples/vector_data', - '../../examples/web_services', - '../../examples/gridlines_and_labels', - '../../examples/miscellanea']) +subsection_order = ExplicitOrder( + [ + '../../examples/lines_and_polygons', + '../../examples/scalar_data', + '../../examples/vector_data', + '../../examples/web_services', + '../../examples/gridlines_and_labels', + '../../examples/miscellanea', + ] +) # Sphinx gallery configuration sphinx_gallery_conf = { 'capture_repr': (), 'examples_dirs': ['../../examples'], - "expected_failing_examples": [ + 'expected_failing_examples': [ # NASA wmts servers frequently return bad content metadata # uncomment these to fix the doc build failures # '../../examples/web_services/reprojected_wmts.py', @@ -166,8 +172,8 @@ # further. For a list of options available for each theme, see the # documentation. html_theme_options = { - "external_links": [], - "github_url": "https://github.com/SciTools/cartopy", + 'external_links': [], + 'github_url': 'https://github.com/SciTools/cartopy', } # The name for this set of Sphinx documents. If None, it defaults to @@ -179,12 +185,12 @@ # The name of an image file (relative to this directory) to place at the top # of the sidebar. -html_logo = "_static/cartopy.png" +html_logo = '_static/cartopy.png' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -html_favicon = "_static/favicon.ico" +html_favicon = '_static/favicon.ico' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -243,23 +249,33 @@ # -- Options for LaTeX output -------------------------------------------------- latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # 'preamble': '', + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'cartopy.tex', 'Cartopy Introduction', - 'Philip Elson, Richard Hattersley', 'manual', False), - ('introductory_examples/index', 'cartopy_examples.tex', 'Cartopy examples', - 'Philip Elson, Richard Hattersley', 'manual', True) + ( + 'index', + 'cartopy.tex', + 'Cartopy Introduction', + 'Philip Elson, Richard Hattersley', + 'manual', + False, + ), + ( + 'introductory_examples/index', + 'cartopy_examples.tex', + 'Cartopy examples', + 'Philip Elson, Richard Hattersley', + 'manual', + True, + ), ] # The name of an image file (relative to this directory) to place at the top of @@ -288,8 +304,13 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'cartopy', 'cartopy Documentation', - ['Philip Elson, Richard Hattersley'], 1) + ( + 'index', + 'cartopy', + 'cartopy Documentation', + ['Philip Elson, Richard Hattersley'], + 1, + ) ] # If true, show URL addresses after external links. @@ -302,10 +323,15 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'cartopy', 'cartopy Documentation', - 'Philip Elson, Richard Hattersley', 'cartopy', - 'One line description of project.', - 'Miscellaneous'), + ( + 'index', + 'cartopy', + 'cartopy Documentation', + 'Philip Elson, Richard Hattersley', + 'cartopy', + 'One line description of project.', + 'Miscellaneous', + ), ] # Documents to append as an appendix to all manuals. @@ -371,26 +397,27 @@ ############ extlinks extension ############ -extlinks = {'issues': ('https://github.com/SciTools/cartopy/labels/%s', - 'issues labeled with %s'), - 'issue': ('https://github.com/SciTools/cartopy/issues/%s', - 'Issue #%s'), - 'pull': ('https://github.com/SciTools/cartopy/pull/%s', 'PR #%s'), - } +extlinks = { + 'issues': ( + 'https://github.com/SciTools/cartopy/labels/%s', + 'issues labeled with %s', + ), + 'issue': ('https://github.com/SciTools/cartopy/issues/%s', 'Issue #%s'), + 'pull': ('https://github.com/SciTools/cartopy/pull/%s', 'PR #%s'), +} ############ plot directive ############## plot_html_show_formats = False # plot_rcparams = {'figure.autolayout': True} -plot_rcparams = {'figure.subplot.bottom': 0.04, - 'figure.subplot.top': 0.96, - 'figure.subplot.left': 0.04, - 'figure.subplot.right': 0.96} -plot_formats = ['png', - ('thumb.png', 20), - 'pdf' - ] +plot_rcparams = { + 'figure.subplot.bottom': 0.04, + 'figure.subplot.top': 0.96, + 'figure.subplot.left': 0.04, + 'figure.subplot.right': 0.96, +} +plot_formats = ['png', ('thumb.png', 20), 'pdf'] ############ autodoc config ############## diff --git a/examples/gridlines_and_labels/gridliner.py b/examples/gridlines_and_labels/gridliner.py index 112400ea1..7d90c41c5 100755 --- a/examples/gridlines_and_labels/gridliner.py +++ b/examples/gridlines_and_labels/gridliner.py @@ -17,6 +17,7 @@ In the third example, labels are drawn only on the left and bottom sides. """ + import matplotlib.pyplot as plt import cartopy.crs as ccrs @@ -24,7 +25,6 @@ def main(): - rotated_crs = ccrs.RotatedPole(pole_longitude=120.0, pole_latitude=70.0) ax0 = plt.axes(projection=rotated_crs) ax0.set_extent([-6, 1, 47.5, 51.5], crs=ccrs.PlateCarree()) diff --git a/examples/gridlines_and_labels/map_latitudes.py b/examples/gridlines_and_labels/map_latitudes.py index 5e5f8d8a5..6d20b3052 100644 --- a/examples/gridlines_and_labels/map_latitudes.py +++ b/examples/gridlines_and_labels/map_latitudes.py @@ -18,6 +18,7 @@ PC_PROJ = ccrs.PlateCarree() + def map_and_latitudes(map_proj): """ Create a map with the given projection, together with a rectangular axes diff --git a/examples/gridlines_and_labels/tick_labels.py b/examples/gridlines_and_labels/tick_labels.py index 4ad95ce45..9a23202a7 100644 --- a/examples/gridlines_and_labels/tick_labels.py +++ b/examples/gridlines_and_labels/tick_labels.py @@ -6,6 +6,7 @@ projections using special tick formatters. """ + import matplotlib.pyplot as plt import cartopy.crs as ccrs @@ -16,8 +17,7 @@ def main(): fig = plt.figure(figsize=(8, 10)) # Label axes of a Plate Carree projection with a central longitude of 180: - ax1 = fig.add_subplot(2, 1, 1, - projection=ccrs.PlateCarree(central_longitude=180)) + ax1 = fig.add_subplot(2, 1, 1, projection=ccrs.PlateCarree(central_longitude=180)) ax1.set_global() ax1.coastlines() ax1.set_xticks([0, 60, 120, 180, 240, 300, 360], crs=ccrs.PlateCarree()) @@ -34,11 +34,10 @@ def main(): ax2.coastlines() ax2.set_xticks([-180, -120, -60, 0, 60, 120, 180], crs=ccrs.PlateCarree()) ax2.set_yticks([-78.5, -60, -25.5, 25.5, 60, 80], crs=ccrs.PlateCarree()) - lon_formatter = LongitudeFormatter(number_format='.1f', - degree_symbol='', - dateline_direction_label=True) - lat_formatter = LatitudeFormatter(number_format='.1f', - degree_symbol='') + lon_formatter = LongitudeFormatter( + number_format='.1f', degree_symbol='', dateline_direction_label=True + ) + lat_formatter = LatitudeFormatter(number_format='.1f', degree_symbol='') ax2.xaxis.set_major_formatter(lon_formatter) ax2.yaxis.set_major_formatter(lat_formatter) diff --git a/examples/lines_and_polygons/always_circular_stereo.py b/examples/lines_and_polygons/always_circular_stereo.py index 9b59074f0..005653c43 100644 --- a/examples/lines_and_polygons/always_circular_stereo.py +++ b/examples/lines_and_polygons/always_circular_stereo.py @@ -10,6 +10,7 @@ always be a circle. """ + import matplotlib.path as mpath import matplotlib.pyplot as plt import numpy as np @@ -21,10 +22,10 @@ def main(): fig = plt.figure(figsize=[10, 5]) ax1 = fig.add_subplot(1, 2, 1, projection=ccrs.SouthPolarStereo()) - ax2 = fig.add_subplot(1, 2, 2, projection=ccrs.SouthPolarStereo(), - sharex=ax1, sharey=ax1) - fig.subplots_adjust(bottom=0.05, top=0.95, - left=0.04, right=0.95, wspace=0.02) + ax2 = fig.add_subplot( + 1, 2, 2, projection=ccrs.SouthPolarStereo(), sharex=ax1, sharey=ax1 + ) + fig.subplots_adjust(bottom=0.05, top=0.95, left=0.04, right=0.95, wspace=0.02) # Limit the map to -60 degrees latitude and below. ax1.set_extent([-180, 180, -90, -60], ccrs.PlateCarree()) @@ -41,7 +42,7 @@ def main(): # Compute a circle in axes coordinates, which we can use as a boundary # for the map. We can pan/zoom as much as we like - the boundary will be # permanently circular. - theta = np.linspace(0, 2*np.pi, 100) + theta = np.linspace(0, 2 * np.pi, 100) center, radius = [0.5, 0.5], 0.5 verts = np.vstack([np.sin(theta), np.cos(theta)]).T circle = mpath.Path(verts * radius + center) diff --git a/examples/lines_and_polygons/effects_of_the_ellipse.py b/examples/lines_and_polygons/effects_of_the_ellipse.py index 7d1ad089e..676a016de 100644 --- a/examples/lines_and_polygons/effects_of_the_ellipse.py +++ b/examples/lines_and_polygons/effects_of_the_ellipse.py @@ -14,6 +14,7 @@ coastlines are shifted as a result of referencing the incorrect ellipse. """ + from matplotlib.lines import Line2D as Line from matplotlib.patheffects import Stroke import matplotlib.pyplot as plt @@ -36,10 +37,11 @@ def transform_fn_factory(target_crs, source_crs): that the resulting geometry would make any sense. """ + def transform_fn(x, y, z=None): - new_coords = target_crs.transform_points(source_crs, - np.asanyarray(x), - np.asanyarray(y)) + new_coords = target_crs.transform_points( + source_crs, np.asanyarray(x), np.asanyarray(y) + ) return new_coords[:, 0], new_coords[:, 1], new_coords[:, 2] return transform_fn @@ -53,12 +55,12 @@ def main(): # Define the coordinate system of the data we have from Natural Earth and # acquire the 1:10m physical coastline shapefile. geodetic = ccrs.Geodetic(globe=ccrs.Globe(datum='WGS84')) - dataset = cfeature.NaturalEarthFeature(category='physical', - name='coastline', - scale='10m') + dataset = cfeature.NaturalEarthFeature( + category='physical', name='coastline', scale='10m' + ) # Create an image tiler instance, and use its CRS for the GeoAxes. - tiler = GoogleTiles(style="satellite") + tiler = GoogleTiles(style='satellite') fig = plt.figure() ax = fig.add_subplot(1, 1, 1, projection=tiler.crs) ax.set_title('The effect of incorrectly referencing the Solomon Islands') @@ -74,10 +76,12 @@ def main(): # Transform the geodetic coordinates of the coastlines into the two # projections of differing ellipses. - wgs84_geoms = [geom_transform(transform_fn_factory(wgs84, geodetic), - geom) for geom in geoms] - sphere_geoms = [geom_transform(transform_fn_factory(sphere, geodetic), - geom) for geom in geoms] + wgs84_geoms = [ + geom_transform(transform_fn_factory(wgs84, geodetic), geom) for geom in geoms + ] + sphere_geoms = [ + geom_transform(transform_fn_factory(sphere, geodetic), geom) for geom in geoms + ] # Using these differently referenced geometries, assume that they are # both referenced to WGS84. @@ -85,16 +89,17 @@ def main(): ax.add_geometries(sphere_geoms, wgs84, edgecolor='gray', facecolor='none') # Create a legend for the coastlines. - legend_artists = [Line([0], [0], color=color, linewidth=3) - for color in ('white', 'gray')] + legend_artists = [ + Line([0], [0], color=color, linewidth=3) for color in ('white', 'gray') + ] legend_texts = ['Correct ellipse\n(WGS84)', 'Incorrect ellipse\n(sphere)'] - legend = ax.legend(legend_artists, legend_texts, fancybox=True, - loc='lower left', framealpha=0.75) + legend = ax.legend( + legend_artists, legend_texts, fancybox=True, loc='lower left', framealpha=0.75 + ) legend.legendPatch.set_facecolor('wheat') # Create an inset GeoAxes showing the location of the Solomon Islands. - sub_ax = fig.add_axes([0.7, 0.625, 0.2, 0.2], - projection=ccrs.PlateCarree()) + sub_ax = fig.add_axes([0.7, 0.625, 0.2, 0.2], projection=ccrs.PlateCarree()) sub_ax.set_extent([110, 180, -50, 10], geodetic) # Make a nice border around the inset axes. @@ -105,8 +110,13 @@ def main(): sub_ax.add_feature(cfeature.LAND) sub_ax.coastlines() extent_box = sgeom.box(extent[0], extent[2], extent[1], extent[3]) - sub_ax.add_geometries([extent_box], ccrs.PlateCarree(), facecolor='none', - edgecolor='blue', linewidth=2) + sub_ax.add_geometries( + [extent_box], + ccrs.PlateCarree(), + facecolor='none', + edgecolor='blue', + linewidth=2, + ) plt.show() diff --git a/examples/lines_and_polygons/feature_creation.py b/examples/lines_and_polygons/feature_creation.py index 80d02bc0c..3956a817c 100644 --- a/examples/lines_and_polygons/feature_creation.py +++ b/examples/lines_and_polygons/feature_creation.py @@ -13,6 +13,7 @@ pre-defined :data:`cartopy.feature.STATES` constant. """ + from matplotlib.offsetbox import AnchoredText import matplotlib.pyplot as plt @@ -33,7 +34,8 @@ def main(): category='cultural', name='admin_1_states_provinces_lines', scale='50m', - facecolor='none') + facecolor='none', + ) SOURCE = 'Natural Earth' LICENSE = 'public domain' @@ -46,9 +48,12 @@ def main(): # Add a text annotation for the license information to the # the bottom right corner. - text = AnchoredText('\u00A9 {}; license: {}' - ''.format(SOURCE, LICENSE), - loc=4, prop={'size': 12}, frameon=True) + text = AnchoredText( + '\u00a9 {}; license: {}'.format(SOURCE, LICENSE), + loc=4, + prop={'size': 12}, + frameon=True, + ) ax.add_artist(text) plt.show() diff --git a/examples/lines_and_polygons/features.py b/examples/lines_and_polygons/features.py index 69808e779..4386ec2b8 100644 --- a/examples/lines_and_polygons/features.py +++ b/examples/lines_and_polygons/features.py @@ -6,6 +6,7 @@ in cartopy. """ + import matplotlib.pyplot as plt import cartopy.crs as ccrs diff --git a/examples/lines_and_polygons/global_map.py b/examples/lines_and_polygons/global_map.py index 826783ca8..31ac8ba1c 100644 --- a/examples/lines_and_polygons/global_map.py +++ b/examples/lines_and_polygons/global_map.py @@ -6,6 +6,7 @@ between two locations. """ + import matplotlib.pyplot as plt import cartopy.crs as ccrs diff --git a/examples/lines_and_polygons/hurricane_katrina.py b/examples/lines_and_polygons/hurricane_katrina.py index 88547f5db..b28a07523 100644 --- a/examples/lines_and_polygons/hurricane_katrina.py +++ b/examples/lines_and_polygons/hurricane_katrina.py @@ -6,6 +6,7 @@ have been significantly impacted by Hurricane Katrina. """ + import matplotlib.patches as mpatches import matplotlib.pyplot as plt import shapely.geometry as sgeom @@ -27,12 +28,12 @@ def sample_data(): -79.6, -80.1, -80.3, -81.3, -82.0, -82.6, -83.3, -84.0, -84.7, -85.3, -85.9, -86.7, -87.7, -88.6, -89.2, -89.6, -89.6, -89.6, -89.6, -89.6, -89.1, -88.6, -88.0, -87.0, - -85.3, -82.9] + -85.3, -82.9] # fmt: skip lats = [23.1, 23.4, 23.8, 24.5, 25.4, 26.0, 26.1, 26.2, 26.2, 26.0, 25.9, 25.4, 25.1, 24.9, 24.6, 24.4, 24.4, 24.5, 24.8, 25.2, 25.7, 26.3, 27.2, 28.2, 29.3, 29.5, 30.2, 31.1, 32.6, 34.1, - 35.6, 37.0, 38.6, 40.1] + 35.6, 37.0, 38.6, 40.1] # fmt: skip return lons, lats @@ -41,20 +42,19 @@ def main(): fig = plt.figure() # to get the effect of having just the states without a map "background" # turn off the background patch and axes frame - ax = fig.add_axes([0, 0, 1, 1], projection=ccrs.LambertConformal(), - frameon=False) + ax = fig.add_axes([0, 0, 1, 1], projection=ccrs.LambertConformal(), frameon=False) ax.patch.set_visible(False) ax.set_extent([-125, -66.5, 20, 50], ccrs.Geodetic()) shapename = 'admin_1_states_provinces_lakes' - states_shp = shpreader.natural_earth(resolution='110m', - category='cultural', name=shapename) + states_shp = shpreader.natural_earth( + resolution='110m', category='cultural', name=shapename + ) lons, lats = sample_data() - ax.set_title('US States which intersect the track of ' - 'Hurricane Katrina (2005)') + ax.set_title('US States which intersect the track of Hurricane Katrina (2005)') # turn the lons and lats into a shapely LineString track = sgeom.LineString(zip(lons, lats)) @@ -74,20 +74,28 @@ def colorize_state(geometry): ax.add_geometries( shpreader.Reader(states_shp).geometries(), ccrs.PlateCarree(), - styler=colorize_state) + styler=colorize_state, + ) - ax.add_geometries([track_buffer], ccrs.PlateCarree(), - facecolor='#C8A2C8', alpha=0.5) - ax.add_geometries([track], ccrs.PlateCarree(), - facecolor='none', edgecolor='k') + ax.add_geometries( + [track_buffer], ccrs.PlateCarree(), facecolor='#C8A2C8', alpha=0.5 + ) + ax.add_geometries([track], ccrs.PlateCarree(), facecolor='none', edgecolor='k') # make two proxy artists to add to a legend - direct_hit = mpatches.Rectangle((0, 0), 1, 1, facecolor="red") - within_2_deg = mpatches.Rectangle((0, 0), 1, 1, facecolor="#FF7E00") - labels = ['State directly intersects\nwith track', - 'State is within \n2 degrees of track'] - ax.legend([direct_hit, within_2_deg], labels, - loc='lower left', bbox_to_anchor=(0.025, -0.1), fancybox=True) + direct_hit = mpatches.Rectangle((0, 0), 1, 1, facecolor='red') + within_2_deg = mpatches.Rectangle((0, 0), 1, 1, facecolor='#FF7E00') + labels = [ + 'State directly intersects\nwith track', + 'State is within \n2 degrees of track', + ] + ax.legend( + [direct_hit, within_2_deg], + labels, + loc='lower left', + bbox_to_anchor=(0.025, -0.1), + fancybox=True, + ) plt.show() diff --git a/examples/lines_and_polygons/nightshade.py b/examples/lines_and_polygons/nightshade.py index 1dfacfa71..184bbda27 100644 --- a/examples/lines_and_polygons/nightshade.py +++ b/examples/lines_and_polygons/nightshade.py @@ -5,6 +5,7 @@ Draws a polygon where there is no sunlight for the given datetime. """ + import datetime import matplotlib.pyplot as plt diff --git a/examples/lines_and_polygons/ocean_bathymetry.py b/examples/lines_and_polygons/ocean_bathymetry.py index 2210aeafe..ae0973fda 100644 --- a/examples/lines_and_polygons/ocean_bathymetry.py +++ b/examples/lines_and_polygons/ocean_bathymetry.py @@ -11,6 +11,7 @@ instead of the specialized `cartopy.feature.NaturalEarthFeature` interface. """ + from glob import glob import matplotlib @@ -29,9 +30,10 @@ def load_bathymetry(zip_file_url): import zipfile import requests + r = requests.get(zip_file_url) z = zipfile.ZipFile(io.BytesIO(r.content)) - z.extractall("ne_10m_bathymetry_all/") + z.extractall('ne_10m_bathymetry_all/') # Read shapefiles, sorted by depth shp_dict = {} @@ -49,17 +51,18 @@ def load_bathymetry(zip_file_url): return depths, shp_dict -if __name__ == "__main__": +if __name__ == '__main__': # Load data (14.8 MB file) depths_str, shp_dict = load_bathymetry( - 'https://naturalearth.s3.amazonaws.com/' + - '10m_physical/ne_10m_bathymetry_all.zip') + 'https://naturalearth.s3.amazonaws.com/' + + '10m_physical/ne_10m_bathymetry_all.zip' + ) # Construct a discrete colormap with colors corresponding to each depth depths = depths_str.astype(int) N = len(depths) nudge = 0.01 # shift bin edge slightly to include data - boundaries = [min(depths)] + sorted(depths+nudge) # low to high + boundaries = [min(depths)] + sorted(depths + nudge) # low to high norm = matplotlib.colors.BoundaryNorm(boundaries, N) blues_cm = matplotlib.colormaps['Blues_r'].resampled(N) colors_depths = blues_cm(norm(depths)) @@ -71,9 +74,11 @@ def load_bathymetry(zip_file_url): # Iterate and plot feature for each depth level for i, depth_str in enumerate(depths_str): - ax.add_geometries(shp_dict[depth_str].geometries(), - crs=ccrs.PlateCarree(), - color=colors_depths[i]) + ax.add_geometries( + shp_dict[depth_str].geometries(), + crs=ccrs.PlateCarree(), + color=colors_depths[i], + ) # Add standard features ax.add_feature(cfeature.LAND, color='grey') @@ -85,12 +90,14 @@ def load_bathymetry(zip_file_url): axi = fig.add_axes([0.85, 0.1, 0.025, 0.8]) ax.add_feature(cfeature.BORDERS, linestyle=':') sm = plt.cm.ScalarMappable(cmap=blues_cm, norm=norm) - fig.colorbar(mappable=sm, - cax=axi, - spacing='proportional', - extend='min', - ticks=depths, - label='Depth (m)') + fig.colorbar( + mappable=sm, + cax=axi, + spacing='proportional', + extend='min', + ticks=depths, + label='Depth (m)', + ) # Convert vector bathymetries to raster (saves a lot of disk space) # while leaving labels as vectors diff --git a/examples/lines_and_polygons/rotated_pole.py b/examples/lines_and_polygons/rotated_pole.py index 2a24c01c1..af327d3bd 100644 --- a/examples/lines_and_polygons/rotated_pole.py +++ b/examples/lines_and_polygons/rotated_pole.py @@ -9,6 +9,7 @@ that including the pole in the polygon has. """ + import matplotlib.pyplot as plt import cartopy.crs as ccrs diff --git a/examples/lines_and_polygons/tissot.py b/examples/lines_and_polygons/tissot.py index 3a9eb27d5..5ca5baad9 100644 --- a/examples/lines_and_polygons/tissot.py +++ b/examples/lines_and_polygons/tissot.py @@ -5,6 +5,7 @@ Visualize Tissot's indicatrix on a map. """ + import matplotlib.pyplot as plt import cartopy.crs as ccrs diff --git a/examples/miscellanea/animate_surface.py b/examples/miscellanea/animate_surface.py index 28a8355a7..48c3862c0 100644 --- a/examples/miscellanea/animate_surface.py +++ b/examples/miscellanea/animate_surface.py @@ -5,6 +5,7 @@ This example demonstrates how to animate gridded data using `~cartopy.mpl.geoaxes.GeoAxes.pcolormesh()`. """ + from matplotlib.animation import FuncAnimation import matplotlib.pyplot as plt import numpy as np @@ -21,8 +22,15 @@ xs, ys = np.meshgrid(2 * x + 180, x) zs = xs + ys vmin, vmax = np.min(zs), np.max(zs) -mesh = ax.pcolormesh(xs, ys, np.zeros_like(zs), transform=ccrs.PlateCarree(), - shading='auto', vmin=vmin, vmax=vmax) +mesh = ax.pcolormesh( + xs, + ys, + np.zeros_like(zs), + transform=ccrs.PlateCarree(), + shading='auto', + vmin=vmin, + vmax=vmax, +) n = 10 @@ -34,7 +42,6 @@ def update_mesh(t): ts = [i / n for i in range(n)] # Go back to the start to make it a smooth repeat ts += ts[::-1] -ani = FuncAnimation(fig, update_mesh, frames=ts, - interval=100) +ani = FuncAnimation(fig, update_mesh, frames=ts, interval=100) plt.show() diff --git a/examples/miscellanea/axes_grid_basic.py b/examples/miscellanea/axes_grid_basic.py index ab89a4ad4..f16bb5c0a 100644 --- a/examples/miscellanea/axes_grid_basic.py +++ b/examples/miscellanea/axes_grid_basic.py @@ -11,6 +11,7 @@ is not used, and instead a standard procedure of creating grid lines is used. Then some fake data is plotted. """ + import matplotlib.pyplot as plt from mpl_toolkits.axes_grid1 import AxesGrid import numpy as np @@ -34,7 +35,7 @@ def sample_data_3d(shape): data = wave + mean times = np.linspace(-1, 1, ntimes) - new_shape = data.shape + (ntimes, ) + new_shape = data.shape + (ntimes,) data = np.rollaxis(data.repeat(ntimes).reshape(new_shape), -1) data *= times[:, np.newaxis, np.newaxis] @@ -43,20 +44,23 @@ def sample_data_3d(shape): def main(): projection = ccrs.PlateCarree() - axes_class = (GeoAxes, - dict(projection=projection)) + axes_class = (GeoAxes, dict(projection=projection)) lons, lats, times, data = sample_data_3d((6, 73, 145)) fig = plt.figure() - axgr = AxesGrid(fig, 111, axes_class=axes_class, - nrows_ncols=(3, 2), - axes_pad=0.6, - cbar_location='right', - cbar_mode='single', - cbar_pad=0.2, - cbar_size='3%', - label_mode='keep') + axgr = AxesGrid( + fig, + 111, + axes_class=axes_class, + nrows_ncols=(3, 2), + axes_pad=0.6, + cbar_location='right', + cbar_mode='single', + cbar_pad=0.2, + cbar_size='3%', + label_mode='keep', + ) for i, ax in enumerate(axgr): ax.coastlines() @@ -67,9 +71,7 @@ def main(): ax.xaxis.set_major_formatter(lon_formatter) ax.yaxis.set_major_formatter(lat_formatter) - p = ax.contourf(lons, lats, data[i, ...], - transform=projection, - cmap='RdBu') + p = ax.contourf(lons, lats, data[i, ...], transform=projection, cmap='RdBu') axgr.cbar_axes[0].colorbar(p) diff --git a/examples/miscellanea/eccentric_ellipse.py b/examples/miscellanea/eccentric_ellipse.py index 14f410f87..e91432fc4 100644 --- a/examples/miscellanea/eccentric_ellipse.py +++ b/examples/miscellanea/eccentric_ellipse.py @@ -10,6 +10,7 @@ matching eccentricity. """ + from io import BytesIO from urllib.request import urlopen @@ -43,16 +44,15 @@ def vesta_image(): # The image is extremely high-resolution, which takes a long time to # plot. Sub-sampling reduces the time taken to plot while not # significantly altering the integrity of the result. - smaller_image = raw_image.resize([raw_image.size[0] // 10, - raw_image.size[1] // 10]) + smaller_image = raw_image.resize([raw_image.size[0] // 10, raw_image.size[1] // 10]) img = np.asarray(smaller_image) # We define the semimajor and semiminor axes, but must also tell the # globe not to use the WGS84 ellipse, which is its default behaviour. - img_globe = ccrs.Globe(semimajor_axis=285000., semiminor_axis=229000., - ellipse=None) + img_globe = ccrs.Globe( + semimajor_axis=285000.0, semiminor_axis=229000.0, ellipse=None + ) img_proj = ccrs.PlateCarree(globe=img_globe) - img_extent = (-180, 180, - -90, 90) + img_extent = (-180, 180, -90, 90) return img, img_globe, img_proj, img_extent @@ -63,8 +63,12 @@ def main(): fig = plt.figure() ax = fig.add_subplot(1, 1, 1, projection=projection) ax.imshow(img, transform=crs, extent=extent) - fig.text(.075, .012, "Image credit: NASA/JPL-Caltech/UCLA/MPS/DLR/IDA/PSI", - bbox={'facecolor': 'w', 'edgecolor': 'k'}) + fig.text( + 0.075, + 0.012, + 'Image credit: NASA/JPL-Caltech/UCLA/MPS/DLR/IDA/PSI', + bbox={'facecolor': 'w', 'edgecolor': 'k'}, + ) plt.show() diff --git a/examples/miscellanea/favicon.py b/examples/miscellanea/favicon.py index ca9bd49e2..cdd0358b8 100644 --- a/examples/miscellanea/favicon.py +++ b/examples/miscellanea/favicon.py @@ -5,6 +5,7 @@ The actual code to generate cartopy's favicon. """ + from matplotlib.font_manager import FontProperties import matplotlib.patches import matplotlib.pyplot as plt @@ -23,13 +24,18 @@ def main(): # Generate a matplotlib path representing the character "C". fp = FontProperties(family='DejaVu Sans', weight='bold') - logo_path = matplotlib.textpath.TextPath((-4.5e7, -3.7e7), - 'C', size=103250000, prop=fp) + logo_path = matplotlib.textpath.TextPath( + (-4.5e7, -3.7e7), 'C', size=103250000, prop=fp + ) # Add the path as a patch, drawing black outlines around the text. - patch = matplotlib.patches.PathPatch(logo_path, facecolor='white', - edgecolor='black', linewidth=10, - transform=ccrs.SouthPolarStereo()) + patch = matplotlib.patches.PathPatch( + logo_path, + facecolor='white', + edgecolor='black', + linewidth=10, + transform=ccrs.SouthPolarStereo(), + ) ax.add_patch(patch) plt.show() diff --git a/examples/miscellanea/logo.py b/examples/miscellanea/logo.py index 62bdaf3f3..8696b88b1 100644 --- a/examples/miscellanea/logo.py +++ b/examples/miscellanea/logo.py @@ -5,6 +5,7 @@ The actual code to produce cartopy's logo. """ + from matplotlib.font_manager import FontProperties import matplotlib.patches import matplotlib.pyplot as plt @@ -23,8 +24,9 @@ def main(): # generate a matplotlib path representing the word "cartopy" fp = FontProperties(family='DejaVu Sans', weight='bold') - logo_path = matplotlib.textpath.TextPath((-171.01406, -39.33125), 'cartopy', - size=80, prop=fp) + logo_path = matplotlib.textpath.TextPath( + (-171.01406, -39.33125), 'cartopy', size=80, prop=fp + ) # scale the letters up to sensible longitude and latitude sizes transform = matplotlib.transforms.Affine2D().scale(1, 2).translate(0, 35) @@ -32,13 +34,12 @@ def main(): # add a background image im = ax.stock_img() # Apply the scale transform and then the map coordinate transform - plate_carree_transform = (transform + - ccrs.PlateCarree()._as_mpl_transform(ax)) + plate_carree_transform = transform + ccrs.PlateCarree()._as_mpl_transform(ax) # add the path as a patch, drawing black outlines around the text - patch = matplotlib.patches.PathPatch(logo_path, - facecolor='none', edgecolor='black', - transform=plate_carree_transform) + patch = matplotlib.patches.PathPatch( + logo_path, facecolor='none', edgecolor='black', transform=plate_carree_transform + ) im.set_clip_path(patch) ax.add_patch(patch) diff --git a/examples/miscellanea/star_shaped_boundary.py b/examples/miscellanea/star_shaped_boundary.py index 341959567..bcef2c38d 100644 --- a/examples/miscellanea/star_shaped_boundary.py +++ b/examples/miscellanea/star_shaped_boundary.py @@ -10,6 +10,7 @@ star shaped boundary. """ + import matplotlib.path as mpath import matplotlib.pyplot as plt @@ -23,8 +24,7 @@ def main(): # Construct a star in longitudes and latitudes. star_path = mpath.Path.unit_regular_star(5, 0.5) - star_path = mpath.Path(star_path.vertices.copy() * 80, - star_path.codes.copy()) + star_path = mpath.Path(star_path.vertices.copy() * 80, star_path.codes.copy()) # Use the star as the boundary. ax.set_boundary(star_path, transform=ccrs.PlateCarree()) diff --git a/examples/miscellanea/tube_stations.py b/examples/miscellanea/tube_stations.py index 5ff551301..202f8cbe4 100644 --- a/examples/miscellanea/tube_stations.py +++ b/examples/miscellanea/tube_stations.py @@ -6,6 +6,7 @@ resolution background imagery provided by OpenStreetMap. """ + from matplotlib.path import Path import matplotlib.pyplot as plt import numpy as np @@ -22,18 +23,33 @@ def tube_locations(): Source: https://www.doogal.co.uk/london_stations.php """ - return np.array([[531738., 180890.], [532379., 179734.], - [531096., 181642.], [530234., 180492.], - [531688., 181150.], [530242., 180982.], - [531940., 179144.], [530406., 180380.], - [529012., 180283.], [530553., 181488.], - [531165., 179489.], [529987., 180812.], - [532347., 180962.], [529102., 181227.], - [529612., 180625.], [531566., 180025.], - [529629., 179503.], [532105., 181261.], - [530995., 180810.], [529774., 181354.], - [528941., 179131.], [531050., 179933.], - [530240., 179718.]]) + return np.array( + [ + [531738.0, 180890.0], + [532379.0, 179734.0], + [531096.0, 181642.0], + [530234.0, 180492.0], + [531688.0, 181150.0], + [530242.0, 180982.0], + [531940.0, 179144.0], + [530406.0, 180380.0], + [529012.0, 180283.0], + [530553.0, 181488.0], + [531165.0, 179489.0], + [529987.0, 180812.0], + [532347.0, 180962.0], + [529102.0, 181227.0], + [529612.0, 180625.0], + [531566.0, 180025.0], + [529629.0, 179503.0], + [532105.0, 181261.0], + [530995.0, 180810.0], + [529774.0, 181354.0], + [528941.0, 179131.0], + [531050.0, 179933.0], + [530240.0, 179718.0], + ] + ) def main(): @@ -47,8 +63,9 @@ def main(): # suitable for a London Underground logo. theta = np.linspace(0, 2 * np.pi, 100) circle_verts = np.vstack([np.sin(theta), np.cos(theta)]).T - concentric_circle = Path.make_compound_path(Path(circle_verts[::-1]), - Path(circle_verts * 0.6)) + concentric_circle = Path.make_compound_path( + Path(circle_verts[::-1]), Path(circle_verts * 0.6) + ) rectangle = Path([[-1.1, -0.2], [1, -0.2], [1, 0.3], [-1.1, 0.3]]) @@ -58,10 +75,24 @@ def main(): # Plot the locations twice, first with the red concentric circles, # then with the blue rectangle. xs, ys = tube_locations().T - ax.plot(xs, ys, transform=ccrs.OSGB(approx=False), - marker=concentric_circle, color='red', markersize=9, linestyle='') - ax.plot(xs, ys, transform=ccrs.OSGB(approx=False), - marker=rectangle, color='blue', markersize=11, linestyle='') + ax.plot( + xs, + ys, + transform=ccrs.OSGB(approx=False), + marker=concentric_circle, + color='red', + markersize=9, + linestyle='', + ) + ax.plot( + xs, + ys, + transform=ccrs.OSGB(approx=False), + marker=rectangle, + color='blue', + markersize=11, + linestyle='', + ) ax.set_title('London underground locations') plt.show() diff --git a/examples/miscellanea/un_flag.py b/examples/miscellanea/un_flag.py index 53bac7f46..f9b8f9efc 100644 --- a/examples/miscellanea/un_flag.py +++ b/examples/miscellanea/un_flag.py @@ -6,6 +6,7 @@ Equidistant projection to reproduce the UN flag. """ + from matplotlib.patches import PathPatch import matplotlib.path import matplotlib.pyplot as plt @@ -65,7 +66,7 @@ def olive_path(): 595, 342, 349, 355, 357, 346, 326, 309, 303, 297, 291, 290, 297, 304, 310, 321, 327, 343, 321, 305, 292, 286, 278, 270, 276, 281, 287, 306, 328, 342, 595, 379, 369, 355, 343, 333, 326, 318, 328, - 340, 349, 366, 373, 379, 595]]).T + 340, 349, 366, 373, 379, 595]]).T # fmt: skip olives_codes = np.array([1, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 79, 1, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, @@ -80,7 +81,7 @@ def olive_path(): 4, 4, 4, 4, 4, 4, 79, 1, 4, 4, 4, 4, 4, 4, 4, 4, 4, 2, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 79, 1, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, - 4, 4, 79], dtype=np.uint8) + 4, 4, 79], dtype=np.uint8) # fmt: skip return matplotlib.path.Path(olives_verts, olives_codes) @@ -91,9 +92,12 @@ def main(): # We're drawing a flag with a 3:5 aspect ratio. fig = plt.figure(figsize=[7.5, 4.5], facecolor=blue) # Put a blue background on the figure. - blue_background = PathPatch(matplotlib.path.Path.unit_rectangle(), - transform=fig.transFigure, color=blue, - zorder=-1) + blue_background = PathPatch( + matplotlib.path.Path.unit_rectangle(), + transform=fig.transFigure, + color=blue, + zorder=-1, + ) fig.patches.append(blue_background) # Set up the Azimuthal Equidistant and Plate Carree projections @@ -122,8 +126,7 @@ def main(): ax.set_boundary(circular_path) if filled_land: - ax.add_feature( - cfeature.LAND, facecolor='white', edgecolor='none') + ax.add_feature(cfeature.LAND, facecolor='white', edgecolor='none') else: ax.stock_img() @@ -148,10 +151,18 @@ def main(): olive2_axes_bbox = Bbox([[0.55, 0.15], [0.275, 0.75]]) olive2_trans = BboxTransform(olives_bbox, olive2_axes_bbox) - olive1 = PathPatch(olive_leaf, facecolor='white', edgecolor='none', - transform=olive1_trans + fig.transFigure) - olive2 = PathPatch(olive_leaf, facecolor='white', edgecolor='none', - transform=olive2_trans + fig.transFigure) + olive1 = PathPatch( + olive_leaf, + facecolor='white', + edgecolor='none', + transform=olive1_trans + fig.transFigure, + ) + olive2 = PathPatch( + olive_leaf, + facecolor='white', + edgecolor='none', + transform=olive2_trans + fig.transFigure, + ) fig.patches.append(olive1) fig.patches.append(olive2) diff --git a/examples/miscellanea/utm_all_zones.py b/examples/miscellanea/utm_all_zones.py index 56c51095d..45cf55943 100644 --- a/examples/miscellanea/utm_all_zones.py +++ b/examples/miscellanea/utm_all_zones.py @@ -10,6 +10,7 @@ Then we add coastlines, gridlines and the number of the zone. Finally we add a supertitle and display the figure. """ + import matplotlib.pyplot as plt import cartopy.crs as ccrs @@ -24,11 +25,13 @@ def main(): # Loop through each zone in the list for zone in zones: - # Add GeoAxes object with specific UTM zone projection to the figure - ax = fig.add_subplot(1, len(zones), zone, - projection=ccrs.UTM(zone=zone, - southern_hemisphere=True)) + ax = fig.add_subplot( + 1, + len(zones), + zone, + projection=ccrs.UTM(zone=zone, southern_hemisphere=True), + ) # Add coastlines, gridlines and zone number for the subplot ax.coastlines(resolution='110m') @@ -36,7 +39,7 @@ def main(): ax.set_title(zone) # Add a supertitle for the figure - fig.suptitle("UTM Projection - Zones") + fig.suptitle('UTM Projection - Zones') # Display the figure plt.show() diff --git a/examples/scalar_data/aurora_forecast.py b/examples/scalar_data/aurora_forecast.py index 03c99363e..b6444c796 100644 --- a/examples/scalar_data/aurora_forecast.py +++ b/examples/scalar_data/aurora_forecast.py @@ -12,6 +12,7 @@ data spaced equally in degrees from 0 to 359 and -90 to 90. """ + from datetime import datetime import json from urllib.request import urlopen @@ -45,9 +46,11 @@ def aurora_forecast(): """ # GitHub gist to download the example data from - url = ('https://gist.githubusercontent.com/lgolston/594c030876c0614d3' - '6d13d03e4f115b6/raw/342ff751419204594180e88d69b3986dbd4fea4a/' - 'ovation_aurora_latest.json') + url = ( + 'https://gist.githubusercontent.com/lgolston/594c030876c0614d3' + '6d13d03e4f115b6/raw/342ff751419204594180e88d69b3986dbd4fea4a/' + 'ovation_aurora_latest.json' + ) # To plot the current forecast instead, uncomment the following line # url = 'https://services.swpc.noaa.gov/json/ovation_aurora_latest.json' @@ -67,21 +70,20 @@ def aurora_forecast(): def aurora_cmap(): """Return a colormap with aurora like colors""" - stops = {'red': [(0.00, 0.1725, 0.1725), - (0.50, 0.1725, 0.1725), - (1.00, 0.8353, 0.8353)], - - 'green': [(0.00, 0.9294, 0.9294), - (0.50, 0.9294, 0.9294), - (1.00, 0.8235, 0.8235)], - - 'blue': [(0.00, 0.3843, 0.3843), - (0.50, 0.3843, 0.3843), - (1.00, 0.6549, 0.6549)], - - 'alpha': [(0.00, 0.0, 0.0), - (0.50, 1.0, 1.0), - (1.00, 1.0, 1.0)]} + stops = { + 'red': [(0.00, 0.1725, 0.1725), (0.50, 0.1725, 0.1725), (1.00, 0.8353, 0.8353)], + 'green': [ + (0.00, 0.9294, 0.9294), + (0.50, 0.9294, 0.9294), + (1.00, 0.8235, 0.8235), + ], + 'blue': [ + (0.00, 0.3843, 0.3843), + (0.50, 0.3843, 0.3843), + (1.00, 0.6549, 0.6549), + ], + 'alpha': [(0.00, 0.0, 0.0), (0.50, 1.0, 1.0), (1.00, 1.0, 1.0)], + } return LinearSegmentedColormap('aurora', stops) @@ -106,9 +108,16 @@ def main(): ax.stock_img() ax.gridlines() ax.add_feature(Nightshade(dt)) - ax.imshow(img, vmin=0, vmax=100, transform=crs, - extent=extent, origin=origin, zorder=2, - cmap=aurora_cmap()) + ax.imshow( + img, + vmin=0, + vmax=100, + transform=crs, + extent=extent, + origin=origin, + zorder=2, + cmap=aurora_cmap(), + ) plt.show() diff --git a/examples/scalar_data/contour_labels.py b/examples/scalar_data/contour_labels.py index 43ab13dbc..1ae79ab7f 100644 --- a/examples/scalar_data/contour_labels.py +++ b/examples/scalar_data/contour_labels.py @@ -5,6 +5,7 @@ An example of adding contour labels to matplotlib contours. """ + import matplotlib.pyplot as plt import cartopy.crs as ccrs @@ -45,9 +46,9 @@ def main(): filled_c = ax.contourf(x, y, z, transform=ccrs.PlateCarree()) # And black line contours (or set colors='none' for invisible lines). - line_c = ax.contour(x, y, z, levels=filled_c.levels, - colors='black', - transform=ccrs.PlateCarree()) + line_c = ax.contour( + x, y, z, levels=filled_c.levels, colors='black', transform=ccrs.PlateCarree() + ) # Add a colorbar for the filled contour. fig.colorbar(filled_c, orientation='horizontal') diff --git a/examples/scalar_data/contour_transforms.py b/examples/scalar_data/contour_transforms.py index ecaa696e3..cdd4d6426 100644 --- a/examples/scalar_data/contour_transforms.py +++ b/examples/scalar_data/contour_transforms.py @@ -11,6 +11,7 @@ have a negative impact on the wrapped coordinates as one can see in the second axes that the data does not extend to the full global extent. """ + import matplotlib.pyplot as plt import cartopy.crs as ccrs @@ -35,7 +36,6 @@ def sample_data(shape=(73, 145)): def main(): - # Use the same sample data as the waves example, but make it # more dependent on y for more interesting contours. x, y, z = sample_data((20, 40)) @@ -44,23 +44,29 @@ def main(): # Setup a global EckertIII map with faint coastlines. fig = plt.figure() ax1 = fig.add_subplot(2, 1, 1, projection=ccrs.EckertIII()) - ax1.set_title("transform_first=False") + ax1.set_title('transform_first=False') ax2 = fig.add_subplot(2, 1, 2, projection=ccrs.EckertIII()) - ax2.set_title("transform_first=True") + ax2.set_title('transform_first=True') for ax, transform_first in zip([ax1, ax2], [False, True]): ax.set_global() ax.coastlines('110m', alpha=0.1) # Add colourful filled contours. - filled_c = ax.contourf(x, y, z, transform=ccrs.PlateCarree(), - transform_first=transform_first) + filled_c = ax.contourf( + x, y, z, transform=ccrs.PlateCarree(), transform_first=transform_first + ) # And black line contours. - ax.contour(x, y, z, levels=filled_c.levels, - colors=['black'], - transform=ccrs.PlateCarree(), - transform_first=transform_first) + ax.contour( + x, + y, + z, + levels=filled_c.levels, + colors=['black'], + transform=ccrs.PlateCarree(), + transform_first=transform_first, + ) plt.show() diff --git a/examples/scalar_data/eyja_volcano.py b/examples/scalar_data/eyja_volcano.py index 7127f87e2..aff952385 100644 --- a/examples/scalar_data/eyja_volcano.py +++ b/examples/scalar_data/eyja_volcano.py @@ -7,6 +7,7 @@ into a single image and displayed in the cartopy GeoAxes. """ + import matplotlib.pyplot as plt from matplotlib.transforms import offset_copy @@ -16,7 +17,7 @@ def main(): # Create a background image tile. - google_terrain = cimgt.GoogleTiles(style="satellite") + google_terrain = cimgt.GoogleTiles(style='satellite') fig = plt.figure() @@ -30,8 +31,15 @@ def main(): ax.add_image(google_terrain, 8) # Add a marker for the Eyjafjallajökull volcano. - ax.plot(-19.613333, 63.62, marker='o', color='red', markersize=12, - alpha=0.7, transform=ccrs.Geodetic()) + ax.plot( + -19.613333, + 63.62, + marker='o', + color='red', + markersize=12, + alpha=0.7, + transform=ccrs.Geodetic(), + ) # Use the cartopy interface to create a matplotlib transform object # for the Geodetic coordinate system. We will use this along with @@ -41,10 +49,15 @@ def main(): text_transform = offset_copy(geodetic_transform, units='dots', x=-25) # Add text 25 pixels to the left of the volcano. - ax.text(-19.613333, 63.62, 'Eyjafjallajökull', - verticalalignment='center', horizontalalignment='right', - transform=text_transform, - bbox=dict(facecolor='sandybrown', alpha=0.5, boxstyle='round')) + ax.text( + -19.613333, + 63.62, + 'Eyjafjallajökull', + verticalalignment='center', + horizontalalignment='right', + transform=text_transform, + bbox=dict(facecolor='sandybrown', alpha=0.5, boxstyle='round'), + ) plt.show() diff --git a/examples/scalar_data/geometry_data.py b/examples/scalar_data/geometry_data.py index b9227ec0c..7e97e8e1d 100644 --- a/examples/scalar_data/geometry_data.py +++ b/examples/scalar_data/geometry_data.py @@ -6,6 +6,7 @@ functionality is available since Cartopy 0.23. """ + import matplotlib.colors as mcolors import matplotlib.pyplot as plt @@ -15,9 +16,9 @@ def main(): # Load Natural Earth's country shapefiles. - shpfilename = shpreader.natural_earth(resolution='110m', - category='cultural', - name='admin_0_countries') + shpfilename = shpreader.natural_earth( + resolution='110m', category='cultural', name='admin_0_countries' + ) reader = shpreader.Reader(shpfilename) countries = reader.records() @@ -34,9 +35,13 @@ def main(): ax = fig.add_subplot(projection=ccrs.EckertVI()) # Plot the geometries coloured according to population estimate. - art = ax.add_geometries(geometries, crs=ccrs.PlateCarree(), - array=population_estimates, cmap='YlGnBu', - norm=mcolors.LogNorm(vmin=1e6)) + art = ax.add_geometries( + geometries, + crs=ccrs.PlateCarree(), + array=population_estimates, + cmap='YlGnBu', + norm=mcolors.LogNorm(vmin=1e6), + ) cbar = fig.colorbar(art, orientation='horizontal', extend='min') cbar.set_label('Number of people') fig.suptitle('Country Population Estimates', fontsize='x-large') diff --git a/examples/scalar_data/geostationary.py b/examples/scalar_data/geostationary.py index fddffa78b..645d6c817 100644 --- a/examples/scalar_data/geostationary.py +++ b/examples/scalar_data/geostationary.py @@ -11,6 +11,7 @@ cartopy into a global Miller map. """ + from io import BytesIO from urllib.request import urlopen @@ -36,8 +37,7 @@ def geos_image(): The origin of the image to be passed through to matplotlib's imshow. """ - url = ('https://gist.github.com/pelson/5871263/raw/' - 'EIDA50_201211061300_clip2.png') + url = 'https://gist.github.com/pelson/5871263/raw/EIDA50_201211061300_clip2.png' img_handle = BytesIO(urlopen(url).read()) img = plt.imread(img_handle) img_proj = ccrs.Geostationary(satellite_height=35786000) diff --git a/examples/scalar_data/raster_reprojections.py b/examples/scalar_data/raster_reprojections.py index 6e0bfbdcf..63744608e 100644 --- a/examples/scalar_data/raster_reprojections.py +++ b/examples/scalar_data/raster_reprojections.py @@ -27,7 +27,7 @@ def main(): # Define the origin and extent of the image following matplotlib's # convention `(left, right, bottom, top)`. These are referenced to # a rectangular coordinate system. - img_origin = "lower" + img_origin = 'lower' img_extent = (-87.6, -86.4, 41.4, 42.0) img_proj = ccrs.PlateCarree() @@ -35,7 +35,7 @@ def main(): extent=img_extent, origin=img_origin, transform=img_proj, - cmap="spring", + cmap='spring', ) # Define extent and projection for the map @@ -46,22 +46,22 @@ def main(): nrows=1, ncols=2, figsize=(12, 5), - subplot_kw={"projection": map_proj}, + subplot_kw={'projection': map_proj}, sharex=True, sharey=True, - layout="constrained", + layout='constrained', ) # Adding the raster *before* setting the map extent ax = axs[0] - ax.set_title("\u2717 Adding the raster\nBEFORE setting the map extent") + ax.set_title('\u2717 Adding the raster\nBEFORE setting the map extent') ax.imshow(img, **imshow_kwargs) ax.set_extent(map_extent, crs=img_proj) # Adding the raster *after* setting the map extent ax = axs[1] - ax.set_title("\u2713 Adding the raster\nAFTER setting the map extent") + ax.set_title('\u2713 Adding the raster\nAFTER setting the map extent') ax.set_extent(map_extent, crs=img_proj) ax.imshow(img, **imshow_kwargs) @@ -81,10 +81,10 @@ def main(): width, height, transform=img_proj, - edgecolor="black", - facecolor="None", + edgecolor='black', + facecolor='None', linewidth=3, - label="Raster data bounds", + label='Raster data bounds', ) ) @@ -94,5 +94,5 @@ def main(): plt.show() -if __name__ == "__main__": +if __name__ == '__main__': main() diff --git a/examples/scalar_data/waves.py b/examples/scalar_data/waves.py index f95fc39eb..f60a71029 100644 --- a/examples/scalar_data/waves.py +++ b/examples/scalar_data/waves.py @@ -5,6 +5,7 @@ An example of contourf on manufactured data. """ + import matplotlib.pyplot as plt import numpy as np @@ -33,9 +34,7 @@ def main(): lons, lats, data = sample_data() - ax.contourf(lons, lats, data, - transform=ccrs.PlateCarree(), - cmap='nipy_spectral') + ax.contourf(lons, lats, data, transform=ccrs.PlateCarree(), cmap='nipy_spectral') ax.coastlines() ax.set_global() plt.show() diff --git a/examples/scalar_data/wrapping_global.py b/examples/scalar_data/wrapping_global.py index 1655166ed..9cd2a1a41 100644 --- a/examples/scalar_data/wrapping_global.py +++ b/examples/scalar_data/wrapping_global.py @@ -15,6 +15,7 @@ to the wrap point. """ + import matplotlib.pyplot as plt import numpy as np @@ -23,53 +24,42 @@ def main(): - # data with longitude centers from 0 to 360 nlon = 24 nlat = 12 # 7.5, 22.5, ..., 337.5, 352.5 - dlon = 360//nlon - lon = np.linspace(dlon/2., 360.-dlon/2., nlon) -# -82.5, -67.5, ..., 67.5, 82.5 - dlat = 180//nlat - lat = np.linspace(-90.+dlat/2., 90.-dlat/2., nlat) + dlon = 360 // nlon + lon = np.linspace(dlon / 2.0, 360.0 - dlon / 2.0, nlon) + # -82.5, -67.5, ..., 67.5, 82.5 + dlat = 180 // nlat + lat = np.linspace(-90.0 + dlat / 2.0, 90.0 - dlat / 2.0, nlat) # 0, 1, ..., 10, 11, 11, 10, ..., 1, 0 - data = np.concatenate((np.arange(nlon // 2), - np.arange(nlon // 2)[::-1])) + data = np.concatenate((np.arange(nlon // 2), np.arange(nlon // 2)[::-1])) data = np.tile(data, nlat).reshape((nlat, nlon)) fig = plt.figure() # plot with central longitude 180 - ax1 = fig.add_subplot(2, 2, 1, - projection=ccrs.Robinson(central_longitude=180)) - ax1.set_title("1d longitudes, central longitude=180", - fontsize='small') + ax1 = fig.add_subplot(2, 2, 1, projection=ccrs.Robinson(central_longitude=180)) + ax1.set_title('1d longitudes, central longitude=180', fontsize='small') ax1.set_global() - ax1.contourf(lon, lat, data, - transform=ccrs.PlateCarree(), cmap='RdBu') + ax1.contourf(lon, lat, data, transform=ccrs.PlateCarree(), cmap='RdBu') ax1.coastlines() # plot with central longitude 0 - ax2 = fig.add_subplot(2, 2, 2, - projection=ccrs.Robinson(central_longitude=0)) - ax2.set_title("1d longitudes, central longitude=0", - fontsize='small') + ax2 = fig.add_subplot(2, 2, 2, projection=ccrs.Robinson(central_longitude=0)) + ax2.set_title('1d longitudes, central longitude=0', fontsize='small') ax2.set_global() - ax2.contourf(lon, lat, data, - transform=ccrs.PlateCarree(), cmap='RdBu') + ax2.contourf(lon, lat, data, transform=ccrs.PlateCarree(), cmap='RdBu') ax2.coastlines() # add cyclic points to data and longitudes # latitudes are unchanged in 1-dimension cdata, clon, clat = cutil.add_cyclic(data, lon, lat) - ax3 = fig.add_subplot(2, 2, 3, - projection=ccrs.Robinson(central_longitude=180)) - ax3.set_title("Cyclic 1d longitudes, central longitude=180", - fontsize='small') + ax3 = fig.add_subplot(2, 2, 3, projection=ccrs.Robinson(central_longitude=180)) + ax3.set_title('Cyclic 1d longitudes, central longitude=180', fontsize='small') ax3.set_global() - ax3.contourf(clon, clat, cdata, - transform=ccrs.PlateCarree(), cmap='RdBu') + ax3.contourf(clon, clat, cdata, transform=ccrs.PlateCarree(), cmap='RdBu') ax3.coastlines() # add_cyclic also works with 2-dimensional data @@ -77,13 +67,10 @@ def main(): # ensure the dimensions of the returned arrays are all the same shape. lon2d, lat2d = np.meshgrid(lon, lat) cdata, clon2d, clat2d = cutil.add_cyclic(data, lon2d, lat2d) - ax4 = fig.add_subplot(2, 2, 4, - projection=ccrs.Robinson(central_longitude=0)) - ax4.set_title("Cyclic 2d longitudes, central longitude=0", - fontsize='small') + ax4 = fig.add_subplot(2, 2, 4, projection=ccrs.Robinson(central_longitude=0)) + ax4.set_title('Cyclic 2d longitudes, central longitude=0', fontsize='small') ax4.set_global() - ax4.contourf(clon2d, clat2d, cdata, - transform=ccrs.PlateCarree(), cmap='RdBu') + ax4.contourf(clon2d, clat2d, cdata, transform=ccrs.PlateCarree(), cmap='RdBu') ax4.coastlines() plt.show() diff --git a/examples/vector_data/arrows.py b/examples/vector_data/arrows.py index 6d58a872f..bbc8fcf5b 100644 --- a/examples/vector_data/arrows.py +++ b/examples/vector_data/arrows.py @@ -5,6 +5,7 @@ Plotting arrows. """ + import matplotlib.pyplot as plt import numpy as np diff --git a/examples/vector_data/barbs.py b/examples/vector_data/barbs.py index a65434b83..b49211831 100644 --- a/examples/vector_data/barbs.py +++ b/examples/vector_data/barbs.py @@ -5,6 +5,7 @@ Plotting barbs. """ + import matplotlib.pyplot as plt import cartopy.crs as ccrs @@ -40,9 +41,16 @@ def main(): ax.coastlines() x, y, u, v, vector_crs = sample_data(shape=(10, 14)) - ax.barbs(x, y, u, v, length=5, - sizes=dict(emptybarb=0.25, spacing=0.2, height=0.5), - linewidth=0.95, transform=vector_crs) + ax.barbs( + x, + y, + u, + v, + length=5, + sizes=dict(emptybarb=0.25, spacing=0.2, height=0.5), + linewidth=0.95, + transform=vector_crs, + ) plt.show() diff --git a/examples/vector_data/regridding_arrows.py b/examples/vector_data/regridding_arrows.py index 18b6ee732..b3c82d012 100644 --- a/examples/vector_data/regridding_arrows.py +++ b/examples/vector_data/regridding_arrows.py @@ -9,6 +9,7 @@ if the data is dense or warped. """ + import matplotlib.pyplot as plt import numpy as np diff --git a/examples/vector_data/streamplot.py b/examples/vector_data/streamplot.py index 3207a7c27..dcab220af 100644 --- a/examples/vector_data/streamplot.py +++ b/examples/vector_data/streamplot.py @@ -5,6 +5,7 @@ Generating a vector-based streamplot. """ + import matplotlib.pyplot as plt import numpy as np @@ -39,8 +40,9 @@ def main(): x, y, u, v, vector_crs = sample_data(shape=(80, 100)) magnitude = np.hypot(u, v) - ax.streamplot(x, y, u, v, transform=vector_crs, - linewidth=2, density=2, color=magnitude) + ax.streamplot( + x, y, u, v, transform=vector_crs, linewidth=2, density=2, color=magnitude + ) plt.show() diff --git a/examples/web_services/image_tiles.py b/examples/web_services/image_tiles.py index ce1176990..b6576ce57 100644 --- a/examples/web_services/image_tiles.py +++ b/examples/web_services/image_tiles.py @@ -6,6 +6,7 @@ providing web service can be accessed. """ + import matplotlib.pyplot as plt import cartopy.crs as ccrs @@ -13,7 +14,7 @@ def main(): - tiler = GoogleTiles(style="satellite") + tiler = GoogleTiles(style='satellite') mercator = tiler.crs fig = plt.figure() diff --git a/examples/web_services/reprojected_wmts.py b/examples/web_services/reprojected_wmts.py index 0fa1daefb..477269eca 100644 --- a/examples/web_services/reprojected_wmts.py +++ b/examples/web_services/reprojected_wmts.py @@ -16,6 +16,7 @@ and Atmospheric Administration (NOAA). """ + import matplotlib.pyplot as plt import cartopy.crs as ccrs diff --git a/examples/web_services/wms.py b/examples/web_services/wms.py index 4de888b50..0d430722d 100644 --- a/examples/web_services/wms.py +++ b/examples/web_services/wms.py @@ -6,6 +6,7 @@ supported by an OGC web services Web Map Service (WMS) aware axes. """ + import matplotlib.pyplot as plt import cartopy.crs as ccrs @@ -16,8 +17,7 @@ def main(): ax = fig.add_subplot(1, 1, 1, projection=ccrs.InterruptedGoodeHomolosine()) ax.coastlines() - ax.add_wms(wms='http://vmap0.tiles.osgeo.org/wms/vmap0', - layers=['basic']) + ax.add_wms(wms='http://vmap0.tiles.osgeo.org/wms/vmap0', layers=['basic']) plt.show() diff --git a/examples/web_services/wmts.py b/examples/web_services/wmts.py index 1f31721c2..4074a80d3 100644 --- a/examples/web_services/wmts.py +++ b/examples/web_services/wmts.py @@ -14,6 +14,7 @@ and Atmospheric Administration (NOAA). """ + import matplotlib.pyplot as plt import cartopy.crs as ccrs diff --git a/examples/web_services/wmts_time.py b/examples/web_services/wmts_time.py index 7dc966584..161202de1 100644 --- a/examples/web_services/wmts_time.py +++ b/examples/web_services/wmts_time.py @@ -12,6 +12,7 @@ the left, with the MODIS false color 'snow RGB' shown on the right. """ + from matplotlib import patheffects import matplotlib.pyplot as plt from owslib.wmts import WebMapTileService @@ -25,8 +26,10 @@ def main(): wmts = WebMapTileService(url) # Layers for MODIS true color and snow RGB - layers = ['MODIS_Terra_SurfaceReflectance_Bands143', - 'MODIS_Terra_CorrectedReflectance_Bands367'] + layers = [ + 'MODIS_Terra_SurfaceReflectance_Bands143', + 'MODIS_Terra_CorrectedReflectance_Bands367', + ] date_str = '2016-02-05' @@ -44,10 +47,15 @@ def main(): ax.set_xlim((x0, x1)) ax.set_ylim((y0, y1)) ax.add_wmts(wmts, layer, wmts_kwargs={'time': date_str}) - txt = ax.text(4.7, 43.2, wmts[layer].title, fontsize=18, color='wheat', - transform=geodetic_crs) - txt.set_path_effects([patheffects.withStroke(linewidth=5, - foreground='black')]) + txt = ax.text( + 4.7, + 43.2, + wmts[layer].title, + fontsize=18, + color='wheat', + transform=geodetic_crs, + ) + txt.set_path_effects([patheffects.withStroke(linewidth=5, foreground='black')]) plt.show() diff --git a/lib/cartopy/__init__.py b/lib/cartopy/__init__.py index a6234acde..62b29a7b3 100644 --- a/lib/cartopy/__init__.py +++ b/lib/cartopy/__init__.py @@ -16,16 +16,16 @@ # the XDG guidelines found at # https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html _writable_dir = Path.home() / '.local' / 'share' -_data_dir = Path(os.environ.get("XDG_DATA_HOME", _writable_dir)) / 'cartopy' +_data_dir = Path(os.environ.get('XDG_DATA_HOME', _writable_dir)) / 'cartopy' _cache_dir = Path(tempfile.gettempdir()) / 'cartopy_cache_dir' -config = {'pre_existing_data_dir': Path(os.environ.get('CARTOPY_DATA_DIR', - '')), - 'data_dir': _data_dir, - 'cache_dir': _cache_dir, - 'repo_data_dir': Path(__file__).parent / 'data', - 'downloaders': {}, - } +config = { + 'pre_existing_data_dir': Path(os.environ.get('CARTOPY_DATA_DIR', '')), + 'data_dir': _data_dir, + 'cache_dir': _cache_dir, + 'repo_data_dir': Path(__file__).parent / 'data', + 'downloaders': {}, +} """ The config dictionary stores global configuration values for cartopy. @@ -88,6 +88,7 @@ # otherwise, fail gracefully. try: from cartopy.siteconfig import update_config as _update_config + _update_config(config) except ImportError: pass @@ -97,6 +98,7 @@ # function, otherwise, fail gracefully. try: from cartopy_userconfig import update_config as _update_config + _update_config(config) except ImportError: pass diff --git a/lib/cartopy/_epsg.py b/lib/cartopy/_epsg.py index d966ca7ce..5b2618d4b 100644 --- a/lib/cartopy/_epsg.py +++ b/lib/cartopy/_epsg.py @@ -6,6 +6,7 @@ Provide support for converting EPSG codes to Projection instances. """ + from pyproj.crs import CRS as _CRS import cartopy.crs as ccrs @@ -17,7 +18,7 @@ def __init__(self, code): if not crs.is_projected: raise ValueError('EPSG code does not define a projection') if not crs.area_of_use: - raise ValueError("Area of use not defined.") + raise ValueError('Area of use not defined.') self.epsg_code = code super().__init__(crs.to_wkt()) @@ -26,4 +27,4 @@ def __repr__(self): return f'_EPSGProjection({self.epsg_code})' def __reduce__(self): - return self.__class__, (self.epsg_code, ) + return self.__class__, (self.epsg_code,) diff --git a/lib/cartopy/crs.py b/lib/cartopy/crs.py index d614afc44..8fb976644 100644 --- a/lib/cartopy/crs.py +++ b/lib/cartopy/crs.py @@ -60,8 +60,7 @@ def _safe_pj_transform(src_crs, tgt_crs, x, y, z=None, trap=True): # see https://github.com/numpy/numpy/pull/10615 # and https://github.com/SciTools/cartopy/pull/2194 warnings.filterwarnings( - "ignore", - message="Conversion of an array with ndim > 0" + 'ignore', message='Conversion of an array with ndim > 0' ) return transformer.transform(x, y, z, errcheck=trap) @@ -72,10 +71,17 @@ class Globe: """ - def __init__(self, datum=None, ellipse='WGS84', - semimajor_axis=None, semiminor_axis=None, - flattening=None, inverse_flattening=None, - towgs84=None, nadgrids=None): + def __init__( + self, + datum=None, + ellipse='WGS84', + semimajor_axis=None, + semiminor_axis=None, + flattening=None, + inverse_flattening=None, + towgs84=None, + nadgrids=None, + ): """ Parameters ---------- @@ -120,7 +126,7 @@ def to_proj4_params(self): ['f', self.flattening], ['rf', self.inverse_flattening], ['towgs84', self.towgs84], - ['nadgrids', self.nadgrids] + ['nadgrids', self.nadgrids], ) return OrderedDict((k, v) for k, v in proj4_params if v is not None) @@ -160,9 +166,9 @@ def __init__(self, proj4_params, globe=None): pass # handle PROJ JSON if ( - isinstance(proj4_params, dict) and - "proj" not in proj4_params and - "init" not in proj4_params + isinstance(proj4_params, dict) + and 'proj' not in proj4_params + and 'init' not in proj4_params ): proj4_params = json.dumps(proj4_params) @@ -172,14 +178,15 @@ def __init__(self, proj4_params, globe=None): if self._handles_ellipses: globe = Globe() else: - globe = Globe(semimajor_axis=WGS84_SEMIMAJOR_AXIS, - ellipse=None) + globe = Globe(semimajor_axis=WGS84_SEMIMAJOR_AXIS, ellipse=None) if globe is not None and not self._handles_ellipses: a = globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS b = globe.semiminor_axis or a if a != b or globe.ellipse is not None: - warnings.warn(f'The {self.__class__.__name__!r} projection ' - 'does not handle elliptical globes.') + warnings.warn( + f'The {self.__class__.__name__!r} projection ' + 'does not handle elliptical globes.' + ) self.globe = globe if isinstance(proj4_params, str): self._proj4_params = {} @@ -264,14 +271,12 @@ def _as_mpl_transform(self, axes=None): # lazy import mpl.geoaxes (and therefore matplotlib) as mpl # is only an optional dependency import cartopy.mpl.geoaxes as geoaxes + if not isinstance(axes, geoaxes.GeoAxes): raise ValueError( 'Axes should be an instance of GeoAxes, got %s' % type(axes) ) - return ( - geoaxes.InterProjectionTransform(self, axes.projection) + - axes.transData - ) + return geoaxes.InterProjectionTransform(self, axes.projection) + axes.transData @property def proj4_params(self): @@ -285,35 +290,33 @@ def as_geocentric(self): """ return CRS( { - "$schema": ( - "https://proj.org/schemas/v0.2/projjson.schema.json" - ), - "type": "GeodeticCRS", - "name": "unknown", - "datum": self.datum.to_json_dict(), - "coordinate_system": { - "subtype": "Cartesian", - "axis": [ + '$schema': ('https://proj.org/schemas/v0.2/projjson.schema.json'), + 'type': 'GeodeticCRS', + 'name': 'unknown', + 'datum': self.datum.to_json_dict(), + 'coordinate_system': { + 'subtype': 'Cartesian', + 'axis': [ { - "name": "Geocentric X", - "abbreviation": "X", - "direction": "geocentricX", - "unit": "metre" + 'name': 'Geocentric X', + 'abbreviation': 'X', + 'direction': 'geocentricX', + 'unit': 'metre', }, { - "name": "Geocentric Y", - "abbreviation": "Y", - "direction": "geocentricY", - "unit": "metre" + 'name': 'Geocentric Y', + 'abbreviation': 'Y', + 'direction': 'geocentricY', + 'unit': 'metre', }, { - "name": "Geocentric Z", - "abbreviation": "Z", - "direction": "geocentricZ", - "unit": "metre" - } - ] - } + 'name': 'Geocentric Z', + 'abbreviation': 'Z', + 'direction': 'geocentricZ', + 'unit': 'metre', + }, + ], + }, } ) @@ -354,7 +357,10 @@ def transform_point(self, x, y, src_crs, trap=True): """ result = self.transform_points( - src_crs, np.array([x]), np.array([y]), trap=trap, + src_crs, + np.array([x]), + np.array([y]), + trap=trap, ).reshape((1, 3)) return result[0, 0], result[0, 1] @@ -389,7 +395,7 @@ def transform_points(self, src_crs, x, y, z=None, trap=False): Array of shape ``x.shape + (3, )`` in this coordinate system. """ - result_shape = tuple(x.shape[i] for i in range(x.ndim)) + (3, ) + result_shape = tuple(x.shape[i] for i in range(x.ndim)) + (3,) if z is None: if x.ndim > 2 or y.ndim > 2: @@ -401,36 +407,35 @@ def transform_points(self, src_crs, x, y, z=None, trap=False): raise ValueError('x and y arrays must have the same length') else: if x.ndim > 2 or y.ndim > 2 or z.ndim > 2: - raise ValueError('x, y and z arrays must be 1 or 2 ' - 'dimensional') + raise ValueError('x, y and z arrays must be 1 or 2 dimensional') elif x.ndim != 1 or y.ndim != 1 or z.ndim != 1: x, y, z = x.flatten(), y.flatten(), z.flatten() if not x.shape[0] == y.shape[0] == z.shape[0]: - raise ValueError('x, y, and z arrays must have the same ' - 'length') + raise ValueError('x, y, and z arrays must have the same length') npts = x.shape[0] result = np.empty([npts, 3], dtype=np.double) if npts: if self == src_crs and ( - isinstance(src_crs, _CylindricalProjection) or - self.is_geodetic()): + isinstance(src_crs, _CylindricalProjection) or self.is_geodetic() + ): # convert from [0,360] to [-180,180] x = np.array(x, copy=True) to_180 = (x > 180) | (x < -180) - x[to_180] = (((x[to_180] + 180) % 360) - 180) + x[to_180] = ((x[to_180] + 180) % 360) - 180 try: - result[:, 0], result[:, 1], result[:, 2] = \ - _safe_pj_transform(src_crs, self, x, y, z, trap=trap) + result[:, 0], result[:, 1], result[:, 2] = _safe_pj_transform( + src_crs, self, x, y, z, trap=trap + ) except ProjError as err: msg = str(err).lower() if ( - "latitude" in msg or - "longitude" in msg or - "outside of projection domain" in msg or - "tolerance condition error" in msg + 'latitude' in msg + or 'longitude' in msg + or 'outside of projection domain' in msg + or 'tolerance condition error' in msg ): result[:] = np.nan else: @@ -500,7 +505,7 @@ def transform_vectors(self, src_proj, x, y, u, v): # a small distance away from the base point of the vector (near # the poles the point may have to be in the opposite direction # to be valid). - factor = 360000. + factor = 360000.0 delta = (src_proj.x_limits[1] - src_proj.x_limits[0]) / factor x_perturbations = delta * np.cos(vector_angles) y_perturbations = delta * np.sin(vector_angles) @@ -520,7 +525,8 @@ def transform_vectors(self, src_proj, x, y, u, v): eps = 1e-9 invalid_x = np.logical_or( source_x + x_perturbations < src_proj.x_limits[0] - eps, - source_x + x_perturbations > src_proj.x_limits[1] + eps) + source_x + x_perturbations > src_proj.x_limits[1] + eps, + ) if invalid_x.any(): x_perturbations[invalid_x] *= -1 y_perturbations[invalid_x] *= -1 @@ -530,7 +536,8 @@ def transform_vectors(self, src_proj, x, y, u, v): # applied. invalid_y = np.logical_or( source_y + y_perturbations < src_proj.y_limits[0] - eps, - source_y + y_perturbations > src_proj.y_limits[1] + eps) + source_y + y_perturbations > src_proj.y_limits[1] + eps, + ) if invalid_y.any(): x_perturbations[invalid_y] *= -1 y_perturbations[invalid_y] *= -1 @@ -542,21 +549,25 @@ def transform_vectors(self, src_proj, x, y, u, v): # domain of the projection, and issue a warning if there are. problem_points = np.logical_or( source_x + x_perturbations < src_proj.x_limits[0] - eps, - source_x + x_perturbations > src_proj.x_limits[1] + eps) + source_x + x_perturbations > src_proj.x_limits[1] + eps, + ) if problem_points.any(): - warnings.warn('Some vectors at source domain corners ' - 'may not have been transformed correctly') + warnings.warn( + 'Some vectors at source domain corners ' + 'may not have been transformed correctly' + ) # 4: Transform this set of points to the projection coordinates and # find the angle between the base point and the perturbed point # in the projection coordinates (reversing the direction at any # points where the original was reversed in step 3). - proj_xyz = self.transform_points(src_proj, - source_x + x_perturbations, - source_y + y_perturbations) + proj_xyz = self.transform_points( + src_proj, source_x + x_perturbations, source_y + y_perturbations + ) target_x_perturbed = proj_xyz[..., 0] target_y_perturbed = proj_xyz[..., 1] - projected_angles = np.arctan2(target_y_perturbed - target_y, - target_x_perturbed - target_x) + projected_angles = np.arctan2( + target_y_perturbed - target_y, target_x_perturbed - target_x + ) if reversed_vectors.any(): projected_angles[reversed_vectors] += np.pi # 5: Form the projected vector components, preserving the magnitude @@ -627,8 +638,9 @@ class RotatedGeodetic(CRS): """ - def __init__(self, pole_longitude, pole_latitude, - central_rotated_longitude=0.0, globe=None): + def __init__( + self, pole_longitude, pole_latitude, central_rotated_longitude=0.0, globe=None + ): """ Parameters ---------- @@ -643,12 +655,17 @@ def __init__(self, pole_longitude, pole_latitude, """ globe = globe or Globe(datum='WGS84') - proj4_params = [('proj', 'ob_tran'), ('o_proj', 'latlon'), - ('o_lon_p', central_rotated_longitude), - ('o_lat_p', pole_latitude), - ('lon_0', 180 + pole_longitude), - ('to_meter', math.radians(1) * ( - globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS))] + proj4_params = [ + ('proj', 'ob_tran'), + ('o_proj', 'latlon'), + ('o_lon_p', central_rotated_longitude), + ('o_lat_p', pole_latitude), + ('lon_0', 180 + pole_longitude), + ( + 'to_meter', + math.radians(1) * (globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS), + ), + ] super().__init__(proj4_params, globe=globe) @@ -668,7 +685,7 @@ class Projection(CRS, metaclass=ABCMeta): 'MultiPoint': '_project_multipoint', 'MultiLineString': '_project_multiline', 'MultiPolygon': '_project_multipolygon', - 'GeometryCollection': '_project_geometry_collection' + 'GeometryCollection': '_project_geometry_collection', } # Whether or not this projection can handle wrapped coordinates _wrappable = False @@ -688,14 +705,12 @@ def __init__(self, *args, **kwargs): y1 = self.area_of_use.north lons = np.array([x0, x0, x1, x1]) lats = np.array([y0, y1, y1, y0]) - points = self.transform_points( - PlateCarree().as_geodetic(), lons, lats - ) + points = self.transform_points(PlateCarree().as_geodetic(), lons, lats) x = points[:, 0] y = points[:, 1] self.bounds = (x.min(), x.max(), y.min(), y.max()) x0, x1, y0, y1 = self.bounds - self.threshold = min(x1 - x0, y1 - y0) / 100. + self.threshold = min(x1 - x0, y1 - y0) / 100.0 elif self.is_geographic: # If the projection is geographic without an area of use, assume # the bounds are the full globe. @@ -706,8 +721,7 @@ def boundary(self): if self.bounds is None: raise NotImplementedError x0, x1, y0, y1 = self.bounds - return sgeom.LineString([(x0, y0), (x0, y1), (x1, y1), (x1, y0), - (x0, y0)]) + return sgeom.LineString([(x0, y0), (x0, y1), (x1, y1), (x1, y0), (x0, y0)]) @property def x_limits(self): @@ -774,6 +788,7 @@ def _determine_longitude_bounds(self, central_longitude): def _repr_html_(self): from html import escape + try: # As matplotlib is not a core cartopy dependency, don't error # if it's not available. @@ -784,8 +799,7 @@ def _repr_html_(self): return None # Produce a visual repr of the Projection instance. - fig, ax = plt.subplots(figsize=(5, 3), - subplot_kw={'projection': self}) + fig, ax = plt.subplots(figsize=(5, 3), subplot_kw={'projection': self}) ax.set_global() ax.coastlines('auto') ax.gridlines() @@ -799,6 +813,7 @@ def _repr_html_(self): def _as_mpl_axes(self): import cartopy.mpl.geoaxes as geoaxes + return geoaxes.GeoAxes, {'projection': self} def project_geometry(self, geometry, src_crs=None): @@ -824,8 +839,10 @@ def project_geometry(self, geometry, src_crs=None): if src_crs is None: src_crs = self.as_geodetic() elif not isinstance(src_crs, CRS): - raise TypeError('Source CRS must be an instance of CRS' - ' or one of its subclasses, or None.') + raise TypeError( + 'Source CRS must be an instance of CRS' + ' or one of its subclasses, or None.' + ) geom_type = geometry.geom_type method_name = self._method_map.get(geom_type) if not method_name: @@ -850,8 +867,7 @@ def _project_linear_ring(self, linear_ring, src_crs): # 1abc # def23ghi # jkl41 - multi_line_string = cartopy.trace.project_linear(linear_ring, - src_crs, self) + multi_line_string = cartopy.trace.project_linear(linear_ring, src_crs, self) # Threshold for whether a point is close enough to be the same # point as another. @@ -885,9 +901,11 @@ def _project_linear_ring(self, linear_ring, src_crs): modified = False j = 0 while j < len(line_strings): - if i != j and np.allclose(line_strings[i].coords[0], - line_strings[j].coords[-1], - atol=threshold): + if i != j and np.allclose( + line_strings[i].coords[0], + line_strings[j].coords[-1], + atol=threshold, + ): if debug: print(f'Joining together {i} and {j}.') last_coords = list(line_strings[j].coords) @@ -911,9 +929,9 @@ def _project_linear_ring(self, linear_ring, src_crs): rings = [] line_strings = [] for line in multi_line_string.geoms: - if len(line.coords) > 3 and np.allclose(line.coords[0], - line.coords[-1], - atol=threshold): + if len(line.coords) > 3 and np.allclose( + line.coords[0], line.coords[-1], atol=threshold + ): result_geometry = sgeom.LinearRing(line.coords[:-1]) rings.append(result_geometry) else: @@ -948,8 +966,8 @@ def _project_multipolygon(self, geometry, src_crs): def _project_geometry_collection(self, geometry, src_crs): return sgeom.GeometryCollection( - [self.project_geometry(geom, src_crs) for geom in geometry.geoms]) - + [self.project_geometry(geom, src_crs) for geom in geometry.geoms] + ) def _project_polygon(self, polygon, src_crs): """ @@ -1016,12 +1034,14 @@ def boundary_distance(xy): # Record the positions of all the segment ends for i, line_string in enumerate(line_strings): first_dist = boundary_distance(line_string.coords[0]) - thing = _BoundaryPoint(first_dist, False, - (i, 'first', line_string.coords[0])) + thing = _BoundaryPoint( + first_dist, False, (i, 'first', line_string.coords[0]) + ) edge_things.append(thing) last_dist = boundary_distance(line_string.coords[-1]) - thing = _BoundaryPoint(last_dist, False, - (i, 'last', line_string.coords[-1])) + thing = _BoundaryPoint( + last_dist, False, (i, 'last', line_string.coords[-1]) + ) edge_things.append(thing) # Record the positions of all the boundary vertices @@ -1033,6 +1053,7 @@ def boundary_distance(xy): if debug_plot_edges: import matplotlib.pyplot as plt + current_fig = plt.gcf() fig = plt.figure() # Reset the current figure so we don't upset anything. @@ -1048,10 +1069,12 @@ def boundary_distance(xy): prev_thing = None for edge_thing in edge_things[:]: - if (prev_thing is not None and - not edge_thing.kind and - not prev_thing.kind and - edge_thing.data[0] == prev_thing.data[0]): + if ( + prev_thing is not None + and not edge_thing.kind + and not prev_thing.kind + and edge_thing.data[0] == prev_thing.data[0] + ): j = edge_thing.data[0] # Insert a edge boundary point in between this geometry. mid_dist = (edge_thing.distance + prev_thing.distance) * 0.5 @@ -1080,8 +1103,7 @@ def boundary_distance(xy): coords = np.array(ls.coords) ax.plot(coords[:, 0], coords[:, 1]) ax.text(coords[0, 0], coords[0, 1], thing.data[0]) - ax.text(coords[-1, 0], coords[-1, 1], - f'{thing.data[0]}.') + ax.text(coords[-1, 0], coords[-1, 1], f'{thing.data[0]}.') def filter_last(t): return t.kind or t.data[1] == 'first' @@ -1095,6 +1117,7 @@ def filter_last(t): if debug: import sys + sys.stdout.write('+') sys.stdout.flush() print() @@ -1118,8 +1141,9 @@ def filter_last(t): if debug: print(' adding boundary point') boundary_point = next_thing.data - combined_coords = (list(current_ls.coords) + - [(boundary_point.x, boundary_point.y)]) + combined_coords = list(current_ls.coords) + [ + (boundary_point.x, boundary_point.y) + ] current_ls = sgeom.LineString(combined_coords) elif next_thing.data[0] == i: @@ -1130,8 +1154,9 @@ def filter_last(t): processed_ls.append(current_ls) if debug_plot_edges: coords = np.array(current_ls.coords) - ax.plot(coords[:, 0], coords[:, 1], color='black', - linestyle='--') + ax.plot( + coords[:, 0], coords[:, 1], color='black', linestyle='--' + ) break else: if debug: @@ -1143,8 +1168,9 @@ def filter_last(t): coords_to_append = list(line_to_append.coords) # Build up the linestring. - current_ls = sgeom.LineString(list(current_ls.coords) + - coords_to_append) + current_ls = sgeom.LineString( + list(current_ls.coords) + coords_to_append + ) # Catch getting stuck in an infinite loop by checking that # linestring only added once. @@ -1153,9 +1179,11 @@ def filter_last(t): else: if debug_plot_edges: plt.show() - raise RuntimeError('Unidentified problem with ' - 'geometry, linestring being ' - 're-added. Please raise an issue.') + raise RuntimeError( + 'Unidentified problem with ' + 'geometry, linestring being ' + 're-added. Please raise an issue.' + ) # filter out any non-valid linear rings def makes_valid_ring(line_string): @@ -1172,7 +1200,8 @@ def makes_valid_ring(line_string): linear_rings = [ sgeom.LinearRing(line_string) for line_string in processed_ls - if makes_valid_ring(line_string)] + if makes_valid_ring(line_string) + ] if debug: print(' DONE') @@ -1205,8 +1234,7 @@ def _rings_to_multi_polygon(self, rings, is_ccw): # that from #509 or #537. holes.append(interior_ring) interior_rings.remove(interior_ring) - polygon_bits.append((exterior_ring.coords, - [ring.coords for ring in holes])) + polygon_bits.append((exterior_ring.coords, [ring.coords for ring in holes])) # Any left over "interior" rings need "inverting" with respect # to the boundary. @@ -1288,11 +1316,15 @@ def quick_vertices_transform(self, vertices, src_crs): x = vertices[:, 0] y = vertices[:, 1] # Extend the limits a tiny amount to allow for precision mistakes - epsilon = 1.e-10 + epsilon = 1.0e-10 x_limits = (self.x_limits[0] - epsilon, self.x_limits[1] + epsilon) y_limits = (self.y_limits[0] - epsilon, self.y_limits[1] + epsilon) - if (x.min() >= x_limits[0] and x.max() <= x_limits[1] and - y.min() >= y_limits[0] and y.max() <= y_limits[1]): + if ( + x.min() >= x_limits[0] + and x.max() <= x_limits[1] + and y.min() >= y_limits[0] + and y.max() <= y_limits[1] + ): return vertices @@ -1302,6 +1334,7 @@ class _RectangularProjection(Projection, metaclass=ABCMeta): is symmetric about the origin. """ + _wrappable = True def __init__(self, proj4_params, half_width, half_height, globe=None): @@ -1329,6 +1362,7 @@ class _CylindricalProjection(_RectangularProjection, metaclass=ABCMeta): want to allow x values to wrap around. """ + _wrappable = True @@ -1349,10 +1383,15 @@ def _ellipse_boundary(semimajor=2, semiminor=1, easting=0, northing=0, n=201): class PlateCarree(_CylindricalProjection): def __init__(self, central_longitude=0.0, globe=None): globe = globe or Globe(semimajor_axis=WGS84_SEMIMAJOR_AXIS) - proj4_params = [('proj', 'eqc'), ('lon_0', central_longitude), - ('to_meter', math.radians(1) * ( - globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS)), - ('vto_meter', 1)] + proj4_params = [ + ('proj', 'eqc'), + ('lon_0', central_longitude), + ( + 'to_meter', + math.radians(1) * (globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS), + ), + ('vto_meter', 1), + ] x_max = 180 y_max = 90 # Set the threshold around 0.5 if the x max is 180. @@ -1388,16 +1427,19 @@ def _bbox_and_offset(self, other_plate_carree): lon_0_offset = other_lon_0 - self_lon_0 lon_lower_bound_0 = self.x_limits[0] - lon_lower_bound_1 = (other_plate_carree.x_limits[0] + lon_0_offset) + lon_lower_bound_1 = other_plate_carree.x_limits[0] + lon_0_offset if lon_lower_bound_1 < self.x_limits[0]: lon_lower_bound_1 += np.diff(self.x_limits)[0] lon_lower_bound_0, lon_lower_bound_1 = sorted( - [lon_lower_bound_0, lon_lower_bound_1]) + [lon_lower_bound_0, lon_lower_bound_1] + ) - bbox = [[lon_lower_bound_0, lon_lower_bound_1], - [lon_lower_bound_1, lon_lower_bound_0]] + bbox = [ + [lon_lower_bound_0, lon_lower_bound_1], + [lon_lower_bound_1, lon_lower_bound_0], + ] bbox[1][1] += self.x_limits[1] - self.x_limits[0] @@ -1415,9 +1457,11 @@ def quick_vertices_transform(self, vertices, src_crs): xs, ys = vertices[:, 0], vertices[:, 1] - potential = (self_params == src_params and - self.y_limits[0] <= ys.min() and - self.y_limits[1] >= ys.max()) + potential = ( + self_params == src_params + and self.y_limits[0] <= ys.min() + and self.y_limits[1] >= ys.max() + ) if potential: mod = np.diff(src_crs.x_limits)[0] bboxes, proj_offset = self._bbox_and_offset(src_crs) @@ -1428,8 +1472,9 @@ def quick_vertices_transform(self, vertices, src_crs): # this range, we're not going to transform it quickly. for i in [-1, 0, 1, 2]: offset = mod * i - proj_offset - if ((poly[0] + offset) <= x_lim[0] and - (poly[1] + offset) >= x_lim[1]): + if (poly[0] + offset) <= x_lim[0] and ( + poly[1] + offset + ) >= x_lim[1]: return_value = vertices + [[-offset, 0]] break if return_value is not None: @@ -1443,11 +1488,19 @@ class TransverseMercator(Projection): A Transverse Mercator projection. """ + _wrappable = True - def __init__(self, central_longitude=0.0, central_latitude=0.0, - false_easting=0.0, false_northing=0.0, - scale_factor=1.0, globe=None, approx=False): + def __init__( + self, + central_longitude=0.0, + central_latitude=0.0, + false_easting=0.0, + false_northing=0.0, + scale_factor=1.0, + globe=None, + approx=False, + ): """ Parameters ---------- @@ -1473,10 +1526,15 @@ def __init__(self, central_longitude=0.0, central_latitude=0.0, will change to False in the next release. """ - proj4_params = [('proj', 'tmerc'), ('lon_0', central_longitude), - ('lat_0', central_latitude), ('k', scale_factor), - ('x_0', false_easting), ('y_0', false_northing), - ('units', 'm')] + proj4_params = [ + ('proj', 'tmerc'), + ('lon_0', central_longitude), + ('lat_0', central_latitude), + ('k', scale_factor), + ('x_0', false_easting), + ('y_0', false_northing), + ('units', 'm'), + ] if approx: proj4_params += [('approx', None)] super().__init__(proj4_params, globe=globe) @@ -1487,9 +1545,7 @@ def __init__(self, central_longitude=0.0, central_latitude=0.0, def boundary(self): x0, x1 = self.x_limits y0, y1 = self.y_limits - return sgeom.LinearRing([(x0, y0), (x0, y1), - (x1, y1), (x1, y0), - (x0, y0)]) + return sgeom.LinearRing([(x0, y0), (x0, y1), (x1, y1), (x1, y0), (x0, y0)]) @property def x_limits(self): @@ -1502,11 +1558,15 @@ def y_limits(self): class OSGB(TransverseMercator): def __init__(self, approx=False): - super().__init__(central_longitude=-2, central_latitude=49, - scale_factor=0.9996012717, - false_easting=400000, false_northing=-100000, - globe=Globe(datum='OSGB36', ellipse='airy'), - approx=approx) + super().__init__( + central_longitude=-2, + central_latitude=49, + scale_factor=0.9996012717, + false_easting=400000, + false_northing=-100000, + globe=Globe(datum='OSGB36', ellipse='airy'), + approx=approx, + ) @property def boundary(self): @@ -1525,12 +1585,16 @@ def y_limits(self): class OSNI(TransverseMercator): def __init__(self, approx=False): - globe = Globe(semimajor_axis=6377340.189, - semiminor_axis=6356034.447938534) - super().__init__(central_longitude=-8, central_latitude=53.5, - scale_factor=1.000035, - false_easting=200000, false_northing=250000, - globe=globe, approx=approx) + globe = Globe(semimajor_axis=6377340.189, semiminor_axis=6356034.447938534) + super().__init__( + central_longitude=-8, + central_latitude=53.5, + scale_factor=1.000035, + false_easting=200000, + false_northing=250000, + globe=globe, + approx=approx, + ) @property def boundary(self): @@ -1567,9 +1631,7 @@ def __init__(self, zone, southern_hemisphere=False, globe=None): globe is created. """ - proj4_params = [('proj', 'utm'), - ('units', 'm'), - ('zone', zone)] + proj4_params = [('proj', 'utm'), ('units', 'm'), ('zone', zone)] if southern_hemisphere: proj4_params.append(('south', None)) super().__init__(proj4_params, globe=globe) @@ -1579,9 +1641,7 @@ def __init__(self, zone, southern_hemisphere=False, globe=None): def boundary(self): x0, x1 = self.x_limits y0, y1 = self.y_limits - return sgeom.LinearRing([(x0, y0), (x0, y1), - (x1, y1), (x1, y0), - (x0, y0)]) + return sgeom.LinearRing([(x0, y0), (x0, y1), (x1, y1), (x1, y0), (x0, y0)]) @property def x_limits(self): @@ -1622,12 +1682,20 @@ class Mercator(Projection): A Mercator projection. """ + _wrappable = True - def __init__(self, central_longitude=0.0, - min_latitude=-80.0, max_latitude=84.0, - globe=None, latitude_true_scale=None, - false_easting=0.0, false_northing=0.0, scale_factor=None): + def __init__( + self, + central_longitude=0.0, + min_latitude=-80.0, + max_latitude=84.0, + globe=None, + latitude_true_scale=None, + false_easting=0.0, + false_northing=0.0, + scale_factor=None, + ): """ Parameters ---------- @@ -1655,11 +1723,13 @@ def __init__(self, central_longitude=0.0, Only one of ``latitude_true_scale`` and ``scale_factor`` should be included. """ - proj4_params = [('proj', 'merc'), - ('lon_0', central_longitude), - ('x_0', false_easting), - ('y_0', false_northing), - ('units', 'm')] + proj4_params = [ + ('proj', 'merc'), + ('lon_0', central_longitude), + ('x_0', false_easting), + ('y_0', false_northing), + ('units', 'm'), + ] # If it's None, we don't pass it to Proj4, in which case its default # of 0.0 will be used. @@ -1668,8 +1738,10 @@ def __init__(self, central_longitude=0.0, if scale_factor is not None: if latitude_true_scale is not None: - raise ValueError('It does not make sense to provide both ' - '"scale_factor" and "latitude_true_scale". ') + raise ValueError( + 'It does not make sense to provide both ' + '"scale_factor" and "latitude_true_scale". ' + ) else: proj4_params.append(('k_0', scale_factor)) @@ -1680,19 +1752,25 @@ def __init__(self, central_longitude=0.0, self._x_limits = self._y_limits = None # Calculate limits. minlon, maxlon = self._determine_longitude_bounds(central_longitude) - limits = self.transform_points(self.as_geodetic(), - np.array([minlon, maxlon]), - np.array([min_latitude, max_latitude])) + limits = self.transform_points( + self.as_geodetic(), + np.array([minlon, maxlon]), + np.array([min_latitude, max_latitude]), + ) self._x_limits = tuple(limits[..., 0]) self._y_limits = tuple(limits[..., 1]) - self.threshold = min(np.diff(self.x_limits)[0] / 720, - np.diff(self.y_limits)[0] / 360) + self.threshold = min( + np.diff(self.x_limits)[0] / 720, np.diff(self.y_limits)[0] / 360 + ) def __eq__(self, other): res = super().__eq__(other) - if hasattr(other, "_y_limits") and hasattr(other, "_x_limits"): - res = res and self._y_limits == other._y_limits and \ - self._x_limits == other._x_limits + if hasattr(other, '_y_limits') and hasattr(other, '_x_limits'): + res = ( + res + and self._y_limits == other._y_limits + and self._x_limits == other._x_limits + ) return res def __ne__(self, other): @@ -1705,9 +1783,7 @@ def __hash__(self): def boundary(self): x0, x1 = self.x_limits y0, y1 = self.y_limits - return sgeom.LinearRing([(x0, y0), (x0, y1), - (x1, y1), (x1, y0), - (x0, y0)]) + return sgeom.LinearRing([(x0, y0), (x0, y1), (x1, y1), (x1, y0), (x0, y0)]) @property def x_limits(self): @@ -1719,12 +1795,16 @@ def y_limits(self): # Define a specific instance of a Mercator projection, the Google mercator. -Mercator.GOOGLE = Mercator(min_latitude=-85.0511287798066, - max_latitude=85.0511287798066, - globe=Globe(ellipse=None, - semimajor_axis=WGS84_SEMIMAJOR_AXIS, - semiminor_axis=WGS84_SEMIMAJOR_AXIS, - nadgrids='@null')) +Mercator.GOOGLE = Mercator( + min_latitude=-85.0511287798066, + max_latitude=85.0511287798066, + globe=Globe( + ellipse=None, + semimajor_axis=WGS84_SEMIMAJOR_AXIS, + semiminor_axis=WGS84_SEMIMAJOR_AXIS, + nadgrids='@null', + ), +) # Deprecated form GOOGLE_MERCATOR = Mercator.GOOGLE @@ -1732,9 +1812,14 @@ def y_limits(self): class LambertCylindrical(_RectangularProjection): def __init__(self, central_longitude=0.0, globe=None): globe = globe or Globe(semimajor_axis=WGS84_SEMIMAJOR_AXIS) - proj4_params = [('proj', 'cea'), ('lon_0', central_longitude), - ('to_meter', math.radians(1) * ( - globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS))] + proj4_params = [ + ('proj', 'cea'), + ('lon_0', central_longitude), + ( + 'to_meter', + math.radians(1) * (globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS), + ), + ] super().__init__(proj4_params, 180, math.degrees(1), globe=globe) @@ -1744,10 +1829,16 @@ class LambertConformal(Projection): """ - def __init__(self, central_longitude=-96.0, central_latitude=39.0, - false_easting=0.0, false_northing=0.0, - standard_parallels=(33, 45), - globe=None, cutoff=-30): + def __init__( + self, + central_longitude=-96.0, + central_latitude=39.0, + false_easting=0.0, + false_northing=0.0, + standard_parallels=(33, 45), + globe=None, + cutoff=-30, + ): """ Parameters ---------- @@ -1771,17 +1862,21 @@ def __init__(self, central_longitude=-96.0, central_latitude=39.0, A value of 0 will draw half the globe. Defaults to -30. """ - proj4_params = [('proj', 'lcc'), - ('lon_0', central_longitude), - ('lat_0', central_latitude), - ('x_0', false_easting), - ('y_0', false_northing)] + proj4_params = [ + ('proj', 'lcc'), + ('lon_0', central_longitude), + ('lat_0', central_latitude), + ('x_0', false_easting), + ('y_0', false_northing), + ] n_parallels = len(standard_parallels) if not 1 <= n_parallels <= 2: - raise ValueError('1 or 2 standard parallels must be specified. ' - f'Got {n_parallels} ({standard_parallels})') + raise ValueError( + '1 or 2 standard parallels must be specified. ' + f'Got {n_parallels} ({standard_parallels})' + ) proj4_params.append(('lat_1', standard_parallels[0])) if n_parallels == 2: @@ -1811,11 +1906,13 @@ def __init__(self, central_longitude=-96.0, central_latitude=39.0, lats[0] = lats[-1] = plat if plat == 90: # Ensure clockwise - lons[1:-1] = np.linspace(central_longitude + 180 - 0.001, - central_longitude - 180 + 0.001, n) + lons[1:-1] = np.linspace( + central_longitude + 180 - 0.001, central_longitude - 180 + 0.001, n + ) else: - lons[1:-1] = np.linspace(central_longitude - 180 + 0.001, - central_longitude + 180 - 0.001, n) + lons[1:-1] = np.linspace( + central_longitude - 180 + 0.001, central_longitude + 180 - 0.001, n + ) points = self.transform_points(self.as_geodetic(), lons, lats) @@ -1829,7 +1926,7 @@ def __init__(self, central_longitude=-96.0, central_latitude=39.0, def __eq__(self, other): res = super().__eq__(other) - if hasattr(other, "cutoff"): + if hasattr(other, 'cutoff'): res = res and self.cutoff == other.cutoff return res @@ -1858,6 +1955,7 @@ class LambertZoneII(Projection): legacy projection that covers hexagonal France and Corsica. """ + def __init__(self): crs = pyproj.CRS.from_epsg(27572) super().__init__(crs.to_wkt()) @@ -1871,11 +1969,17 @@ class LambertAzimuthalEqualArea(Projection): A Lambert Azimuthal Equal-Area projection. """ + _wrappable = True - def __init__(self, central_longitude=0.0, central_latitude=0.0, - false_easting=0.0, false_northing=0.0, - globe=None): + def __init__( + self, + central_longitude=0.0, + central_latitude=0.0, + false_easting=0.0, + false_northing=0.0, + globe=None, + ): """ Parameters ---------- @@ -1892,11 +1996,13 @@ def __init__(self, central_longitude=0.0, central_latitude=0.0, created. """ - proj4_params = [('proj', 'laea'), - ('lon_0', central_longitude), - ('lat_0', central_latitude), - ('x_0', false_easting), - ('y_0', false_northing)] + proj4_params = [ + ('proj', 'laea'), + ('lon_0', central_longitude), + ('lat_0', central_latitude), + ('x_0', false_easting), + ('y_0', false_northing), + ] super().__init__(proj4_params, globe=globe) @@ -1909,8 +2015,9 @@ def __init__(self, central_longitude=0.0, central_latitude=0.0, lat = -central_latitude + sign * 0.01 x, max_y = self.transform_point(lon, lat, self.as_geodetic()) - coords = _ellipse_boundary(a * 1.9999, max_y - false_northing, - false_easting, false_northing, 61) + coords = _ellipse_boundary( + a * 1.9999, max_y - false_northing, false_easting, false_northing, 61 + ) self._boundary = sgeom.polygon.LinearRing(coords.T) mins = np.min(coords, axis=1) maxs = np.max(coords, axis=1) @@ -1943,8 +2050,7 @@ def __init__(self, central_longitude=0.0, globe=None): proj4_params = [('proj', 'mill'), ('lon_0', central_longitude)] # See Snyder, 1987. Eqs (11-1) and (11-2) substituting maximums of # (lambda-lambda0)=180 and phi=90 to get limits. - super().__init__(proj4_params, a * np.pi, a * 2.303412543376391, - globe=globe) + super().__init__(proj4_params, a * np.pi, a * 2.303412543376391, globe=globe) class RotatedPole(_CylindricalProjection): @@ -1964,8 +2070,13 @@ class RotatedPole(_CylindricalProjection): """ - def __init__(self, pole_longitude=0.0, pole_latitude=90.0, - central_rotated_longitude=0.0, globe=None): + def __init__( + self, + pole_longitude=0.0, + pole_latitude=90.0, + central_rotated_longitude=0.0, + globe=None, + ): """ Parameters ---------- @@ -1981,22 +2092,29 @@ def __init__(self, pole_longitude=0.0, pole_latitude=90.0, """ globe = globe or Globe(semimajor_axis=WGS84_SEMIMAJOR_AXIS) - proj4_params = [('proj', 'ob_tran'), ('o_proj', 'latlon'), - ('o_lon_p', central_rotated_longitude), - ('o_lat_p', pole_latitude), - ('lon_0', 180 + pole_longitude), - ('to_meter', math.radians(1) * ( - globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS))] + proj4_params = [ + ('proj', 'ob_tran'), + ('o_proj', 'latlon'), + ('o_lon_p', central_rotated_longitude), + ('o_lat_p', pole_latitude), + ('lon_0', 180 + pole_longitude), + ( + 'to_meter', + math.radians(1) * (globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS), + ), + ] super().__init__(proj4_params, 180, 90, globe=globe) class Gnomonic(Projection): _handles_ellipses = False - def __init__(self, central_latitude=0.0, - central_longitude=0.0, globe=None): - proj4_params = [('proj', 'gnom'), ('lat_0', central_latitude), - ('lon_0', central_longitude)] + def __init__(self, central_latitude=0.0, central_longitude=0.0, globe=None): + proj4_params = [ + ('proj', 'gnom'), + ('lat_0', central_latitude), + ('lon_0', central_longitude), + ] super().__init__(proj4_params, globe=globe) self._max = 5e7 self.threshold = 1e5 @@ -2017,27 +2135,41 @@ def y_limits(self): class Stereographic(Projection): _wrappable = True - def __init__(self, central_latitude=0.0, central_longitude=0.0, - false_easting=0.0, false_northing=0.0, - true_scale_latitude=None, - scale_factor=None, globe=None): - proj4_params = [('proj', 'stere'), ('lat_0', central_latitude), - ('lon_0', central_longitude), - ('x_0', false_easting), ('y_0', false_northing)] + def __init__( + self, + central_latitude=0.0, + central_longitude=0.0, + false_easting=0.0, + false_northing=0.0, + true_scale_latitude=None, + scale_factor=None, + globe=None, + ): + proj4_params = [ + ('proj', 'stere'), + ('lat_0', central_latitude), + ('lon_0', central_longitude), + ('x_0', false_easting), + ('y_0', false_northing), + ] if true_scale_latitude is not None: - if central_latitude not in (-90., 90.): - warnings.warn('"true_scale_latitude" parameter is only used ' - 'for polar stereographic projections. Consider ' - 'the use of "scale_factor" instead.', - stacklevel=2) + if central_latitude not in (-90.0, 90.0): + warnings.warn( + '"true_scale_latitude" parameter is only used ' + 'for polar stereographic projections. Consider ' + 'the use of "scale_factor" instead.', + stacklevel=2, + ) proj4_params.append(('lat_ts', true_scale_latitude)) if scale_factor is not None: if true_scale_latitude is not None: - raise ValueError('It does not make sense to provide both ' - '"scale_factor" and "true_scale_latitude". ' - 'Ignoring "scale_factor".') + raise ValueError( + 'It does not make sense to provide both ' + '"scale_factor" and "true_scale_latitude". ' + 'Ignoring "scale_factor".' + ) else: proj4_params.append(('k_0', scale_factor)) @@ -2052,12 +2184,17 @@ def __init__(self, central_latitude=0.0, central_longitude=0.0, # should even be linear. x_axis_offset = 5e7 / WGS84_SEMIMAJOR_AXIS y_axis_offset = 5e7 / WGS84_SEMIMINOR_AXIS - self._x_limits = (-a * x_axis_offset + false_easting, - a * x_axis_offset + false_easting) - self._y_limits = (-b * y_axis_offset + false_northing, - b * y_axis_offset + false_northing) - coords = _ellipse_boundary(self._x_limits[1], self._y_limits[1], - false_easting, false_northing, 91) + self._x_limits = ( + -a * x_axis_offset + false_easting, + a * x_axis_offset + false_easting, + ) + self._y_limits = ( + -b * y_axis_offset + false_northing, + b * y_axis_offset + false_northing, + ) + coords = _ellipse_boundary( + self._x_limits[1], self._y_limits[1], false_easting, false_northing, 91 + ) self._boundary = sgeom.LinearRing(coords.T) self.threshold = np.diff(self._x_limits)[0] * 1e-3 @@ -2075,37 +2212,43 @@ def y_limits(self): class NorthPolarStereo(Stereographic): - def __init__(self, central_longitude=0.0, true_scale_latitude=None, - globe=None): + def __init__(self, central_longitude=0.0, true_scale_latitude=None, globe=None): super().__init__( central_latitude=90, central_longitude=central_longitude, true_scale_latitude=true_scale_latitude, # None is +90 - globe=globe) + globe=globe, + ) class SouthPolarStereo(Stereographic): - def __init__(self, central_longitude=0.0, true_scale_latitude=None, - globe=None): + def __init__(self, central_longitude=0.0, true_scale_latitude=None, globe=None): super().__init__( central_latitude=-90, central_longitude=central_longitude, true_scale_latitude=true_scale_latitude, # None is -90 - globe=globe) + globe=globe, + ) class Orthographic(Projection): _handles_ellipses = False - def __init__(self, central_longitude=0.0, central_latitude=0.0, - azimuth=0.0, globe=None): - proj4_params = [('proj', 'ortho'), ('lon_0', central_longitude), - ('lat_0', central_latitude), ('alpha', azimuth)] + def __init__( + self, central_longitude=0.0, central_latitude=0.0, azimuth=0.0, globe=None + ): + proj4_params = [ + ('proj', 'ortho'), + ('lon_0', central_longitude), + ('lat_0', central_latitude), + ('alpha', azimuth), + ] if pyproj.__proj_version__ < '9.5.0' and azimuth != 0.0: warnings.warn( 'Setting azimuth is not supported with PROJ versions < 9.5.0. ' 'Assuming azimuth=0. ' - 'Current PROJ version: %s' % pyproj.__proj_version__) + 'Current PROJ version: %s' % pyproj.__proj_version__ + ) super().__init__(proj4_params, globe=globe) # TODO: Let the globe return the semimajor axis always. @@ -2137,8 +2280,14 @@ def y_limits(self): class _WarpedRectangularProjection(Projection, metaclass=ABCMeta): _wrappable = True - def __init__(self, proj4_params, central_longitude, - false_easting=None, false_northing=None, globe=None): + def __init__( + self, + proj4_params, + central_longitude, + false_easting=None, + false_northing=None, + globe=None, + ): if false_easting is not None: proj4_params += [('x_0', false_easting)] if false_northing is not None: @@ -2152,8 +2301,8 @@ def __init__(self, proj4_params, central_longitude, lat = np.empty(2 * n + 1) lon[:n] = minlon lat[:n] = np.linspace(-90, 90, n) - lon[n:2 * n] = maxlon - lat[n:2 * n] = np.linspace(90, -90, n) + lon[n : 2 * n] = maxlon + lat[n : 2 * n] = np.linspace(90, -90, n) lon[-1] = minlon lat[-1] = -90 points = self.transform_points(self.as_geodetic(), lon, lat) @@ -2190,8 +2339,9 @@ class Aitoff(_WarpedRectangularProjection): _handles_ellipses = False - def __init__(self, central_longitude=0, false_easting=None, - false_northing=None, globe=None): + def __init__( + self, central_longitude=0, false_easting=None, false_northing=None, globe=None + ): """ Parameters ---------- @@ -2208,12 +2358,14 @@ def __init__(self, central_longitude=0, false_easting=None, This projection does not handle elliptical globes. """ - proj_params = [('proj', 'aitoff'), - ('lon_0', central_longitude)] - super().__init__(proj_params, central_longitude, - false_easting=false_easting, - false_northing=false_northing, - globe=globe) + proj_params = [('proj', 'aitoff'), ('lon_0', central_longitude)] + super().__init__( + proj_params, + central_longitude, + false_easting=false_easting, + false_northing=false_northing, + globe=globe, + ) self.threshold = 1e5 @@ -2228,8 +2380,9 @@ class _Eckert(_WarpedRectangularProjection, metaclass=ABCMeta): _handles_ellipses = False - def __init__(self, central_longitude=0, false_easting=None, - false_northing=None, globe=None): + def __init__( + self, central_longitude=0, false_easting=None, false_northing=None, globe=None + ): """ Parameters ---------- @@ -2246,12 +2399,14 @@ def __init__(self, central_longitude=0, false_easting=None, This projection does not handle elliptical globes. """ - proj4_params = [('proj', self._proj_name), - ('lon_0', central_longitude)] - super().__init__(proj4_params, central_longitude, - false_easting=false_easting, - false_northing=false_northing, - globe=globe) + proj4_params = [('proj', self._proj_name), ('lon_0', central_longitude)] + super().__init__( + proj4_params, + central_longitude, + false_easting=false_easting, + false_northing=false_northing, + globe=globe, + ) self.threshold = 1e5 @@ -2263,6 +2418,7 @@ class EckertI(_Eckert): and parallels are straight lines. Its equal-area pair is :class:`EckertII`. """ + _proj_name = 'eck1' @@ -2275,6 +2431,7 @@ class EckertII(_Eckert): parallels is :class:`EckertI`. """ + _proj_name = 'eck2' @@ -2287,6 +2444,7 @@ class EckertIII(_Eckert): semicircles on the edges. Its equal-area pair is :class:`EckertIV`. """ + _proj_name = 'eck3' @@ -2302,6 +2460,7 @@ class EckertIV(_Eckert): It is commonly used for world maps. """ + _proj_name = 'eck4' @@ -2314,6 +2473,7 @@ class EckertV(_Eckert): equal-area pair is :class:`EckertVI`. """ + _proj_name = 'eck5' @@ -2328,6 +2488,7 @@ class EckertVI(_Eckert): It is commonly used for world maps. """ + _proj_name = 'eck6' @@ -2353,8 +2514,9 @@ class EqualEarth(_WarpedRectangularProjection): """ - def __init__(self, central_longitude=0, false_easting=None, - false_northing=None, globe=None): + def __init__( + self, central_longitude=0, false_easting=None, false_northing=None, globe=None + ): """ Parameters ---------- @@ -2369,10 +2531,13 @@ def __init__(self, central_longitude=0, false_easting=None, """ proj_params = [('proj', 'eqearth'), ('lon_0', central_longitude)] - super().__init__(proj_params, central_longitude, - false_easting=false_easting, - false_northing=false_northing, - globe=globe) + super().__init__( + proj_params, + central_longitude, + false_easting=false_easting, + false_northing=false_northing, + globe=globe, + ) self.threshold = 1e5 @@ -2389,8 +2554,9 @@ class Hammer(_WarpedRectangularProjection): _handles_ellipses = False - def __init__(self, central_longitude=0, false_easting=None, - false_northing=None, globe=None): + def __init__( + self, central_longitude=0, false_easting=None, false_northing=None, globe=None + ): """ Parameters ---------- @@ -2407,12 +2573,14 @@ def __init__(self, central_longitude=0, false_easting=None, This projection does not handle elliptical globes. """ - proj4_params = [('proj', 'hammer'), - ('lon_0', central_longitude)] - super().__init__(proj4_params, central_longitude, - false_easting=false_easting, - false_northing=false_northing, - globe=globe) + proj4_params = [('proj', 'hammer'), ('lon_0', central_longitude)] + super().__init__( + proj4_params, + central_longitude, + false_easting=false_easting, + false_northing=false_northing, + globe=globe, + ) self.threshold = 1e5 @@ -2431,8 +2599,9 @@ class Mollweide(_WarpedRectangularProjection): _handles_ellipses = False - def __init__(self, central_longitude=0, globe=None, - false_easting=None, false_northing=None): + def __init__( + self, central_longitude=0, globe=None, false_easting=None, false_northing=None + ): """ Parameters ---------- @@ -2450,10 +2619,13 @@ def __init__(self, central_longitude=0, globe=None, """ proj4_params = [('proj', 'moll'), ('lon_0', central_longitude)] - super().__init__(proj4_params, central_longitude, - false_easting=false_easting, - false_northing=false_northing, - globe=globe) + super().__init__( + proj4_params, + central_longitude, + false_easting=false_easting, + false_northing=false_northing, + globe=globe, + ) self.threshold = 1e5 @@ -2471,8 +2643,9 @@ class Robinson(_WarpedRectangularProjection): _handles_ellipses = False - def __init__(self, central_longitude=0, globe=None, - false_easting=None, false_northing=None): + def __init__( + self, central_longitude=0, globe=None, false_easting=None, false_northing=None + ): """ Parameters ---------- @@ -2490,10 +2663,13 @@ def __init__(self, central_longitude=0, globe=None, """ proj4_params = [('proj', 'robin'), ('lon_0', central_longitude)] - super().__init__(proj4_params, central_longitude, - false_easting=false_easting, - false_northing=false_northing, - globe=globe) + super().__init__( + proj4_params, + central_longitude, + false_easting=false_easting, + false_northing=false_northing, + globe=globe, + ) self.threshold = 1e4 def transform_point(self, x, y, src_crs, trap=True): @@ -2563,6 +2739,7 @@ class InterruptedGoodeHomolosine(Projection): A central_longitude value of -160 is recommended for the oceanic view. """ + _wrappable = True def __init__(self, central_longitude=0, globe=None, emphasis='land'): @@ -2586,7 +2763,7 @@ def __init__(self, central_longitude=0, globe=None, emphasis='land'): super().__init__(proj4_params, globe=globe) else: - msg = '`emphasis` needs to be either \'land\' or \'ocean\'' + msg = "`emphasis` needs to be either 'land' or 'ocean'" raise ValueError(msg) minlon, maxlon = self._determine_longitude_bounds(central_longitude) @@ -2601,39 +2778,39 @@ def __init__(self, central_longitude=0, globe=None, emphasis='land'): top_interrupted_lons = (-90.0, 60.0) bottom_interrupted_lons = (90.0, -60.0) lons = np.empty( - (2 + 2 * len(top_interrupted_lons + bottom_interrupted_lons)) * n + - 1) + (2 + 2 * len(top_interrupted_lons + bottom_interrupted_lons)) * n + 1 + ) lats = np.empty( - (2 + 2 * len(top_interrupted_lons + bottom_interrupted_lons)) * n + - 1) + (2 + 2 * len(top_interrupted_lons + bottom_interrupted_lons)) * n + 1 + ) end = 0 # Left boundary - lons[end:end + n] = minlon - lats[end:end + n] = np.linspace(-90, 90, n) + lons[end : end + n] = minlon + lats[end : end + n] = np.linspace(-90, 90, n) end += n # Top boundary for lon in top_interrupted_lons: - lons[end:end + n] = lon - epsilon + central_longitude - lats[end:end + n] = np.linspace(90, 0, n) + lons[end : end + n] = lon - epsilon + central_longitude + lats[end : end + n] = np.linspace(90, 0, n) end += n - lons[end:end + n] = lon + epsilon + central_longitude - lats[end:end + n] = np.linspace(0, 90, n) + lons[end : end + n] = lon + epsilon + central_longitude + lats[end : end + n] = np.linspace(0, 90, n) end += n # Right boundary - lons[end:end + n] = maxlon - lats[end:end + n] = np.linspace(90, -90, n) + lons[end : end + n] = maxlon + lats[end : end + n] = np.linspace(90, -90, n) end += n # Bottom boundary for lon in bottom_interrupted_lons: - lons[end:end + n] = lon + epsilon + central_longitude - lats[end:end + n] = np.linspace(-90, 0, n) + lons[end : end + n] = lon + epsilon + central_longitude + lats[end : end + n] = np.linspace(-90, 0, n) end += n - lons[end:end + n] = lon - epsilon + central_longitude - lats[end:end + n] = np.linspace(0, -90, n) + lons[end : end + n] = lon - epsilon + central_longitude + lats[end : end + n] = np.linspace(0, -90, n) end += n # Close loop @@ -2664,14 +2841,26 @@ def y_limits(self): class _Satellite(Projection): - def __init__(self, projection, satellite_height=35785831, - central_longitude=0.0, central_latitude=0.0, - false_easting=0, false_northing=0, globe=None, - sweep_axis=None): - proj4_params = [('proj', projection), ('lon_0', central_longitude), - ('lat_0', central_latitude), ('h', satellite_height), - ('x_0', false_easting), ('y_0', false_northing), - ('units', 'm')] + def __init__( + self, + projection, + satellite_height=35785831, + central_longitude=0.0, + central_latitude=0.0, + false_easting=0, + false_northing=0, + globe=None, + sweep_axis=None, + ): + proj4_params = [ + ('proj', projection), + ('lon_0', central_longitude), + ('lat_0', central_latitude), + ('h', satellite_height), + ('x_0', false_easting), + ('y_0', false_northing), + ('units', 'm'), + ] if sweep_axis: proj4_params.append(('sweep', sweep_axis)) super().__init__(proj4_params, globe=globe) @@ -2709,9 +2898,15 @@ class Geostationary(_Satellite): """ - def __init__(self, central_longitude=0.0, satellite_height=35785831, - false_easting=0, false_northing=0, globe=None, - sweep_axis='y'): + def __init__( + self, + central_longitude=0.0, + satellite_height=35785831, + false_easting=0, + false_northing=0, + globe=None, + sweep_axis='y', + ): """ Parameters ---------- @@ -2740,7 +2935,8 @@ def __init__(self, central_longitude=0.0, satellite_height=35785831, false_easting=false_easting, false_northing=false_northing, globe=globe, - sweep_axis=sweep_axis) + sweep_axis=sweep_axis, + ) # TODO: Let the globe return the semimajor axis always. a = float(self.ellipsoid.semi_major_metre or WGS84_SEMIMAJOR_AXIS) @@ -2780,15 +2976,19 @@ def __init__(self, central_longitude=0.0, satellite_height=35785831, # sin_c = x0 / np.hypot(x0, sat_dist - y0) # tan_c = x0 / (sat_dist - y0) # A bit of algebra combines these to give directly: - sin_c = r / np.sqrt(sat_dist ** 2 - a ** 2 + r ** 2) - tan_c = r / np.sqrt(sat_dist ** 2 - a ** 2) + sin_c = r / np.sqrt(sat_dist**2 - a**2 + r**2) + tan_c = r / np.sqrt(sat_dist**2 - a**2) # Using Napier's rules for right spherical triangles R2 and R6, # (See https://en.wikipedia.org/wiki/Spherical_trigonometry), we can # solve for arc angles b and a, which are our x and y scanning angles, # respectively. - coords = np.vstack([np.arctan(np.cos(angleA) * tan_c), # R6 - np.arcsin(np.sin(angleA) * sin_c)]) # R2 + coords = np.vstack( + [ + np.arctan(np.cos(angleA) * tan_c), # R6 + np.arcsin(np.sin(angleA) * sin_c), + ] + ) # R2 # Need to multiply scanning angles by satellite height to get to the # actual native coordinates for the projection. @@ -2809,9 +3009,15 @@ class NearsidePerspective(_Satellite): _handles_ellipses = False - def __init__(self, central_longitude=0.0, central_latitude=0.0, - satellite_height=35785831, - false_easting=0, false_northing=0, globe=None): + def __init__( + self, + central_longitude=0.0, + central_latitude=0.0, + satellite_height=35785831, + false_easting=0, + false_northing=0, + globe=None, + ): """ Parameters ---------- @@ -2840,15 +3046,15 @@ def __init__(self, central_longitude=0.0, central_latitude=0.0, central_latitude=central_latitude, false_easting=false_easting, false_northing=false_northing, - globe=globe) + globe=globe, + ) # TODO: Let the globe return the semimajor axis always. a = self.ellipsoid.semi_major_metre or WGS84_SEMIMAJOR_AXIS h = float(satellite_height) max_x = a * np.sqrt(h / (2 * a + h)) - coords = _ellipse_boundary(max_x, max_x, - false_easting, false_northing, 61) + coords = _ellipse_boundary(max_x, max_x, false_easting, false_northing, 61) self._set_boundary(coords) @@ -2861,9 +3067,15 @@ class AlbersEqualArea(Projection): """ - def __init__(self, central_longitude=0.0, central_latitude=0.0, - false_easting=0.0, false_northing=0.0, - standard_parallels=(20.0, 50.0), globe=None): + def __init__( + self, + central_longitude=0.0, + central_latitude=0.0, + false_easting=0.0, + false_northing=0.0, + standard_parallels=(20.0, 50.0), + globe=None, + ): """ Parameters ---------- @@ -2882,11 +3094,13 @@ def __init__(self, central_longitude=0.0, central_latitude=0.0, created. """ - proj4_params = [('proj', 'aea'), - ('lon_0', central_longitude), - ('lat_0', central_latitude), - ('x_0', false_easting), - ('y_0', false_northing)] + proj4_params = [ + ('proj', 'aea'), + ('lon_0', central_longitude), + ('lat_0', central_latitude), + ('x_0', false_easting), + ('y_0', false_northing), + ] if standard_parallels is not None: try: proj4_params.append(('lat_1', standard_parallels[0])) @@ -2942,11 +3156,17 @@ class AzimuthalEquidistant(Projection): This projection provides accurate angles about and distances through the central position. Other angles, distances, or areas may be distorted. """ + _wrappable = True - def __init__(self, central_longitude=0.0, central_latitude=0.0, - false_easting=0.0, false_northing=0.0, - globe=None): + def __init__( + self, + central_longitude=0.0, + central_latitude=0.0, + false_easting=0.0, + false_northing=0.0, + globe=None, + ): """ Parameters ---------- @@ -2965,17 +3185,22 @@ def __init__(self, central_longitude=0.0, central_latitude=0.0, globe is created. """ - proj4_params = [('proj', 'aeqd'), ('lon_0', central_longitude), - ('lat_0', central_latitude), - ('x_0', false_easting), ('y_0', false_northing)] + proj4_params = [ + ('proj', 'aeqd'), + ('lon_0', central_longitude), + ('lat_0', central_latitude), + ('x_0', false_easting), + ('y_0', false_northing), + ] super().__init__(proj4_params, globe=globe) # TODO: Let the globe return the semimajor axis always. a = float(self.ellipsoid.semi_major_metre or WGS84_SEMIMAJOR_AXIS) b = float(self.ellipsoid.semi_minor_metre or a) - coords = _ellipse_boundary(a * np.pi, b * np.pi, - false_easting, false_northing, 61) + coords = _ellipse_boundary( + a * np.pi, b * np.pi, false_easting, false_northing, 61 + ) self._boundary = sgeom.LinearRing(coords.T) mins = np.min(coords, axis=1) maxs = np.max(coords, axis=1) @@ -3005,8 +3230,9 @@ class Sinusoidal(Projection): """ - def __init__(self, central_longitude=0.0, false_easting=0.0, - false_northing=0.0, globe=None): + def __init__( + self, central_longitude=0.0, false_easting=0.0, false_northing=0.0, globe=None + ): """ Parameters ---------- @@ -3021,10 +3247,12 @@ def __init__(self, central_longitude=0.0, false_easting=0.0, created. """ - proj4_params = [('proj', 'sinu'), - ('lon_0', central_longitude), - ('x_0', false_easting), - ('y_0', false_northing)] + proj4_params = [ + ('proj', 'sinu'), + ('lon_0', central_longitude), + ('x_0', false_easting), + ('y_0', false_northing), + ] super().__init__(proj4_params, globe=globe) # Obtain boundary points @@ -3035,8 +3263,8 @@ def __init__(self, central_longitude=0.0, false_easting=0.0, lat = np.empty(2 * n + 1) lon[:n] = minlon lat[:n] = np.linspace(-90, 90, n) - lon[n:2 * n] = maxlon - lat[n:2 * n] = np.linspace(90, -90, n) + lon[n : 2 * n] = maxlon + lat[n : 2 * n] = np.linspace(90, -90, n) lon[-1] = minlon lat[-1] = -90 points = self.transform_points(self.as_geodetic(), lon, lat) @@ -3063,9 +3291,9 @@ def y_limits(self): # MODIS data products use a Sinusoidal projection of a spherical Earth # https://modis-land.gsfc.nasa.gov/GCTP.html -Sinusoidal.MODIS = Sinusoidal(globe=Globe(ellipse=None, - semimajor_axis=6371007.181, - semiminor_axis=6371007.181)) +Sinusoidal.MODIS = Sinusoidal( + globe=Globe(ellipse=None, semimajor_axis=6371007.181, semiminor_axis=6371007.181) +) class EquidistantConic(Projection): @@ -3076,9 +3304,15 @@ class EquidistantConic(Projection): meridians and along one or two specified standard parallels. """ - def __init__(self, central_longitude=0.0, central_latitude=0.0, - false_easting=0.0, false_northing=0.0, - standard_parallels=(20.0, 50.0), globe=None): + def __init__( + self, + central_longitude=0.0, + central_latitude=0.0, + false_easting=0.0, + false_northing=0.0, + standard_parallels=(20.0, 50.0), + globe=None, + ): """ Parameters ---------- @@ -3097,11 +3331,13 @@ def __init__(self, central_longitude=0.0, central_latitude=0.0, created. """ - proj4_params = [('proj', 'eqdc'), - ('lon_0', central_longitude), - ('lat_0', central_latitude), - ('x_0', false_easting), - ('y_0', false_northing)] + proj4_params = [ + ('proj', 'eqdc'), + ('lon_0', central_longitude), + ('lat_0', central_latitude), + ('x_0', false_easting), + ('y_0', false_northing), + ] if standard_parallels is not None: try: proj4_params.append(('lat_1', standard_parallels[0])) @@ -3155,11 +3391,19 @@ class ObliqueMercator(Projection): An Oblique Mercator projection. """ + _wrappable = True - def __init__(self, central_longitude=0.0, central_latitude=0.0, - false_easting=0.0, false_northing=0.0, - scale_factor=1.0, azimuth=0.0, globe=None): + def __init__( + self, + central_longitude=0.0, + central_latitude=0.0, + false_easting=0.0, + false_northing=0.0, + scale_factor=1.0, + azimuth=0.0, + globe=None, + ): """ Parameters ---------- @@ -3192,10 +3436,16 @@ def __init__(self, central_longitude=0.0, central_latitude=0.0, # Exactly 90 causes coastline 'folding'. azimuth -= 1e-3 - proj4_params = [('proj', 'omerc'), ('lonc', central_longitude), - ('lat_0', central_latitude), ('k', scale_factor), - ('x_0', false_easting), ('y_0', false_northing), - ('alpha', azimuth), ('units', 'm')] + proj4_params = [ + ('proj', 'omerc'), + ('lonc', central_longitude), + ('lat_0', central_latitude), + ('k', scale_factor), + ('x_0', false_easting), + ('y_0', false_northing), + ('alpha', azimuth), + ('units', 'm'), + ] super().__init__(proj4_params, globe=globe) @@ -3216,9 +3466,7 @@ def __init__(self, central_longitude=0.0, central_latitude=0.0, def boundary(self): x0, x1 = self.x_limits y0, y1 = self.y_limits - return sgeom.LinearRing([(x0, y0), (x0, y1), - (x1, y1), (x1, y0), - (x0, y0)]) + return sgeom.LinearRing([(x0, y0), (x0, y1), (x1, y1), (x1, y0), (x0, y0)]) @property def x_limits(self): @@ -3228,6 +3476,7 @@ def x_limits(self): def y_limits(self): return self._y_limits + class Spilhaus(Projection): """ Spilhaus World Ocean Map in a Square. @@ -3237,6 +3486,7 @@ class Spilhaus(Projection): (115°E and 30°N) and Argentina (65°W and 30°S). See https://storymaps.arcgis.com/stories/756bcae18d304a1eac140f19f4d5cb3d """ + def __init__(self, rotation=45, false_easting=0.0, false_northing=0.0, globe=None): """ Parameters @@ -3249,21 +3499,23 @@ def __init__(self, rotation=45, false_easting=0.0, false_northing=0.0, globe=Non Y offset from the planar origin in metres. Defaults to 0.0. """ - proj4_params = [('proj', 'spilhaus'), - ('rot',rotation), - ('x_0', false_easting), - ('y_0', false_northing)] + proj4_params = [ + ('proj', 'spilhaus'), + ('rot', rotation), + ('x_0', false_easting), + ('y_0', false_northing), + ] super().__init__(proj4_params, globe=globe) # The boundary on https://epsg.io/54099 are wrong # The following bounds are calculated based on - #[-65.00000012, -29.99999981] + # [-65.00000012, -29.99999981] # and [115.00000024, 30.00000036] self.bounds = [ -11802684.083372328, 11802683.949222516, -11801129.925928915, - 11801129.925928915 + 11801129.925928915, ] @@ -3315,4 +3567,5 @@ def epsg(code): """ import cartopy._epsg + return cartopy._epsg._EPSGProjection(code) diff --git a/lib/cartopy/feature/__init__.py b/lib/cartopy/feature/__init__.py index f021b6b16..9aa51d720 100644 --- a/lib/cartopy/feature/__init__.py +++ b/lib/cartopy/feature/__init__.py @@ -23,9 +23,11 @@ import cartopy.io.shapereader as shapereader -COLORS = {'land': np.array((240, 240, 220)) / 256., - 'land_alt1': np.array((220, 220, 220)) / 256., - 'water': np.array((152, 183, 226)) / 256.} +COLORS = { + 'land': np.array((240, 240, 220)) / 256.0, + 'land_alt1': np.array((220, 220, 220)) / 256.0, + 'water': np.array((152, 183, 226)) / 256.0, +} """ A dictionary of colors useful for drawing Features. @@ -107,10 +109,12 @@ def intersecting_geometries(self, extent): # shapely 2.0 returns tuple of NaNs instead of None for empty geometry # -> check for both if extent is not None and not np.isnan(extent[0]): - extent_geom = sgeom.box(extent[0], extent[2], - extent[1], extent[3]) - return (geom for geom in self.geometries() if - geom is not None and extent_geom.intersects(geom)) + extent_geom = sgeom.box(extent[0], extent[2], extent[1], extent[3]) + return ( + geom + for geom in self.geometries() + if geom is not None and extent_geom.intersects(geom) + ) else: return self.geometries() @@ -288,9 +292,9 @@ def geometries(self): """ key = (self.name, self.category, self.scale) if key not in _NATURAL_EARTH_GEOM_CACHE: - path = shapereader.natural_earth(resolution=self.scale, - category=self.category, - name=self.name) + path = shapereader.natural_earth( + resolution=self.scale, category=self.category, name=self.name + ) reader = shapereader.Reader(path) if reader.crs is not None: self._crs = reader.crs @@ -323,8 +327,7 @@ def with_scale(self, new_scale): respectively. """ - return NaturalEarthFeature(self.category, self.name, new_scale, - **self.kwargs) + return NaturalEarthFeature(self.category, self.name, new_scale, **self.kwargs) class GSHHSFeature(Feature): @@ -364,9 +367,21 @@ class GSHHSFeature(Feature): def __init__(self, scale='auto', levels=None, **kwargs): super().__init__(cartopy.crs.PlateCarree(), **kwargs) - if scale not in ('auto', 'a', 'coarse', 'c', 'low', 'l', - 'intermediate', 'i', 'high', 'h', 'full', 'f'): - raise ValueError(f"Unknown GSHHS scale {scale!r}.") + if scale not in ( + 'auto', + 'a', + 'coarse', + 'c', + 'low', + 'l', + 'intermediate', + 'i', + 'high', + 'h', + 'full', + 'f', + ): + raise ValueError(f'Unknown GSHHS scale {scale!r}.') self._scale = scale if levels is None: @@ -374,7 +389,7 @@ def __init__(self, scale='auto', levels=None, **kwargs): self._levels = set(levels) unknown_levels = self._levels.difference([1, 2, 3, 4, 5, 6]) if unknown_levels: - raise ValueError(f"Unknown GSHHS levels {unknown_levels!r}.") + raise ValueError(f'Unknown GSHHS levels {unknown_levels!r}.') # Default kwargs self._kwargs.setdefault('edgecolor', 'black') @@ -391,11 +406,13 @@ def _scale_from_extent(self, extent): if extent is not None: # Upper limit on extent in degrees. - scale_limits = (('c', 20.0), - ('l', 10.0), - ('i', 2.0), - ('h', 0.5), - ('f', 0.1)) + scale_limits = ( + ('c', 20.0), + ('l', 10.0), + ('i', 2.0), + ('h', 0.5), + ('f', 0.1), + ) width = abs(extent[1] - extent[0]) height = abs(extent[3] - extent[2]) @@ -417,8 +434,7 @@ def intersecting_geometries(self, extent): scale = self._scale[0] if extent is not None: - extent_geom = sgeom.box(extent[0], extent[2], - extent[1], extent[3]) + extent_geom = sgeom.box(extent[0], extent[2], extent[1], extent[3]) for level in self._levels: geoms = GSHHSFeature._geometries_cache.get((scale, level)) if geoms is None: @@ -466,7 +482,8 @@ def __init__(self, wfs, features, **kwargs): except ImportError as e: raise ImportError( 'WFSFeature requires additional dependencies. If installed ' - 'via pip, try `pip install cartopy[ows]`.\n') from e + 'via pip, try `pip install cartopy[ows]`.\n' + ) from e self.source = WFSGeometrySource(wfs, features) crs = self.source.default_projection() @@ -477,9 +494,9 @@ def __init__(self, wfs, features, **kwargs): def geometries(self): min_x, min_y, max_x, max_y = self.crs.boundary.bounds - geoms = self.source.fetch_geometries(self.crs, - extent=(min_x, max_x, - min_y, max_y)) + geoms = self.source.fetch_geometries( + self.crs, extent=(min_x, max_x, min_y, max_y) + ) return iter(geoms) def intersecting_geometries(self, extent): @@ -494,42 +511,64 @@ def intersecting_geometries(self, extent): BORDERS = NaturalEarthFeature( - 'cultural', 'admin_0_boundary_lines_land', - auto_scaler, edgecolor='black', facecolor='never') + 'cultural', + 'admin_0_boundary_lines_land', + auto_scaler, + edgecolor='black', + facecolor='never', +) """Automatically scaled country boundaries.""" STATES = NaturalEarthFeature( - 'cultural', 'admin_1_states_provinces_lakes', - auto_scaler, edgecolor='black', facecolor='none') + 'cultural', + 'admin_1_states_provinces_lakes', + auto_scaler, + edgecolor='black', + facecolor='none', +) """Automatically scaled state and province boundaries.""" COASTLINE = NaturalEarthFeature( - 'physical', 'coastline', auto_scaler, - edgecolor='black', facecolor='never') + 'physical', 'coastline', auto_scaler, edgecolor='black', facecolor='never' +) """Automatically scaled coastline, including major islands.""" LAKES = NaturalEarthFeature( - 'physical', 'lakes', auto_scaler, - edgecolor='none', facecolor=COLORS['water']) + 'physical', 'lakes', auto_scaler, edgecolor='none', facecolor=COLORS['water'] +) """Automatically scaled natural and artificial lakes.""" LAND = NaturalEarthFeature( - 'physical', 'land', auto_scaler, - edgecolor='none', facecolor=COLORS['land'], zorder=-1) + 'physical', + 'land', + auto_scaler, + edgecolor='none', + facecolor=COLORS['land'], + zorder=-1, +) """Automatically scaled land polygons, including major islands.""" OCEAN = NaturalEarthFeature( - 'physical', 'ocean', auto_scaler, - edgecolor='none', facecolor=COLORS['water'], zorder=-1) + 'physical', + 'ocean', + auto_scaler, + edgecolor='none', + facecolor=COLORS['water'], + zorder=-1, +) """Automatically scaled ocean polygons.""" RIVERS = NaturalEarthFeature( - 'physical', 'rivers_lake_centerlines', auto_scaler, - edgecolor=COLORS['water'], facecolor='never') + 'physical', + 'rivers_lake_centerlines', + auto_scaler, + edgecolor=COLORS['water'], + facecolor='never', +) """Automatically scaled single-line drainages, including lake centerlines.""" diff --git a/lib/cartopy/feature/download/__main__.py b/lib/cartopy/feature/download/__main__.py index 264058711..ed990abaf 100755 --- a/lib/cartopy/feature/download/__main__.py +++ b/lib/cartopy/feature/download/__main__.py @@ -24,8 +24,10 @@ ALL_SCALES = ('110m', '50m', '10m') # See https://github.com/SciTools/cartopy/pull/1833 -URL_TEMPLATE = ('https://naturalearth.s3.amazonaws.com/{resolution}_' - '{category}/ne_{resolution}_{name}.zip') +URL_TEMPLATE = ( + 'https://naturalearth.s3.amazonaws.com/{resolution}_' + '{category}/ne_{resolution}_{name}.zip' +) SHP_NE_SPEC = ('shapefiles', 'natural_earth') FEATURE_DEFN_GROUPS = { @@ -42,16 +44,14 @@ ('physical', 'geography_regions_points', ALL_SCALES), ('physical', 'geography_marine_polys', ALL_SCALES), ('physical', 'glaciated_areas', ALL_SCALES), - ('physical', 'antarctic_ice_shelves_polys', ('50m', '10m')) + ('physical', 'antarctic_ice_shelves_polys', ('50m', '10m')), ), 'cultural': ( ('cultural', 'admin_0_countries', ALL_SCALES), ('cultural', 'admin_0_countries_lakes', ALL_SCALES), ('cultural', 'admin_0_sovereignty', ALL_SCALES), ('cultural', 'admin_0_boundary_lines_land', ALL_SCALES), - ('cultural', 'urban_areas', ('50m', '10m')), - ('cultural', 'roads', '10m'), ('cultural', 'roads_north_america', '10m'), ('cultural', 'railroads', '10m'), @@ -75,10 +75,10 @@ def download_features(group_names, dry_run=True): if isinstance(feature_defns, Feature): feature = feature_defns level = list(feature._levels)[0] - downloader = Downloader.from_config(('shapefiles', 'gshhs', - feature._scale, level)) - format_dict = {'config': config, 'scale': feature._scale, - 'level': level} + downloader = Downloader.from_config( + ('shapefiles', 'gshhs', feature._scale, level) + ) + format_dict = {'config': config, 'scale': feature._scale, 'level': level} if dry_run: print(f'URL: {downloader.url(format_dict)}') else: @@ -90,39 +90,55 @@ def download_features(group_names, dry_run=True): if not isinstance(scales, tuple): scales = (scales,) for scale in scales: - downloader = Downloader.from_config(('shapefiles', - 'natural_earth', - scale, category, - name)) + downloader = Downloader.from_config( + ('shapefiles', 'natural_earth', scale, category, name) + ) feature = NaturalEarthFeature(category, name, scale) - format_dict = {'config': config, 'category': category, - 'name': name, 'resolution': scale} + format_dict = { + 'config': config, + 'category': category, + 'name': name, + 'resolution': scale, + } if dry_run: print(f'URL: {downloader.url(format_dict)}') else: downloader.path(format_dict) geoms = list(feature.geometries()) - print('Feature {}, {}, {} length: {}' - ''.format(category, name, scale, len(geoms))) + print( + 'Feature {}, {}, {} length: {}'.format( + category, name, scale, len(geoms) + ) + ) def main(): parser = argparse.ArgumentParser(description='Download feature datasets.') - parser.add_argument('group_names', nargs='+', - choices=FEATURE_DEFN_GROUPS, - metavar='GROUP_NAME', - help='Feature group name: %(choices)s') - parser.add_argument('--output', '-o', - help='save datasets in the specified directory ' - '(default: user cache directory)') - parser.add_argument('--dry-run', - help='just print the URLs to download', - action='store_true') - parser.add_argument('--ignore-repo-data', action='store_true', - help='ignore existing repo data when downloading') - parser.add_argument('--no-warn', - action='store_true', - help='ignore cartopy "DownloadWarning" warnings') + parser.add_argument( + 'group_names', + nargs='+', + choices=FEATURE_DEFN_GROUPS, + metavar='GROUP_NAME', + help='Feature group name: %(choices)s', + ) + parser.add_argument( + '--output', + '-o', + help='save datasets in the specified directory (default: user cache directory)', + ) + parser.add_argument( + '--dry-run', help='just print the URLs to download', action='store_true' + ) + parser.add_argument( + '--ignore-repo-data', + action='store_true', + help='ignore existing repo data when downloading', + ) + parser.add_argument( + '--no-warn', + action='store_true', + help='ignore cartopy "DownloadWarning" warnings', + ) args = parser.parse_args() if args.output: @@ -134,6 +150,7 @@ def main(): config['repo_data_dir'] = config['data_dir'] if args.no_warn: import warnings + warnings.filterwarnings('ignore', category=DownloadWarning) # Enforce use of stable AWS endpoint, regardless of cartopy version. diff --git a/lib/cartopy/feature/nightshade.py b/lib/cartopy/feature/nightshade.py index 49c3c44f0..044e2fa9f 100644 --- a/lib/cartopy/feature/nightshade.py +++ b/lib/cartopy/feature/nightshade.py @@ -13,8 +13,9 @@ class Nightshade(ShapelyFeature): - def __init__(self, date=None, delta=0.1, refraction=-0.83, - color="k", alpha=0.5, **kwargs): + def __init__( + self, date=None, delta=0.1, refraction=-0.83, color='k', alpha=0.5, **kwargs + ): """ Shade the darkside of the Earth, accounting for refraction. @@ -42,8 +43,7 @@ def __init__(self, date=None, delta=0.1, refraction=-0.83, # make sure date is UTC, or naive with respect to time zones if date.utcoffset(): - raise ValueError( - f'datetime instance must be UTC, not {date.tzname()}') + raise ValueError(f'datetime instance must be UTC, not {date.tzname()}') # Returns the Greenwich hour angle, # need longitude (opposite direction) @@ -56,9 +56,11 @@ def __init__(self, date=None, delta=0.1, refraction=-0.83, pole_lat = 90 + lat central_lon = 0 - rotated_pole = ccrs.RotatedPole(pole_latitude=pole_lat, - pole_longitude=pole_lon, - central_rotated_longitude=central_lon) + rotated_pole = ccrs.RotatedPole( + pole_latitude=pole_lat, + pole_longitude=pole_lon, + central_rotated_longitude=central_lon, + ) npts = int(180 / delta) x = np.empty(npts * 2) @@ -79,8 +81,9 @@ def __init__(self, date=None, delta=0.1, refraction=-0.83, # We need to clip the input to arccos to [-1, 1] due to floating # point precision and arccos creating nans for values outside # of the domain - arccos_tmp = np.clip(np.sin(np.deg2rad(refraction)) / - np.cos(np.deg2rad(y)), -1, 1) + arccos_tmp = np.clip( + np.sin(np.deg2rad(refraction)) / np.cos(np.deg2rad(y)), -1, 1 + ) omega0 = np.rad2deg(np.arccos(arccos_tmp)) # Fill the longitude values from the offset for midnight. @@ -94,8 +97,7 @@ def __init__(self, date=None, delta=0.1, refraction=-0.83, kwargs.setdefault('alpha', alpha) geom = sgeom.Polygon(np.column_stack((x, y))) - return super().__init__( - [geom], rotated_pole, **kwargs) + return super().__init__([geom], rotated_pole, **kwargs) def _julian_day(date): @@ -132,8 +134,7 @@ def _julian_day(date): B = 2 - year // 100 + (year // 100) // 4 C = ((second / 60 + minute) / 60 + hour) / 24 - JD = (int(365.25 * (year + 4716)) + int(30.6001 * (month + 1)) + - day + B - 1524.5 + C) + JD = int(365.25 * (year + 4716)) + int(30.6001 * (month + 1)) + day + B - 1524.5 + C return JD @@ -170,30 +171,37 @@ def _solar_position(date): M_sun = (357.5277233 + 35999.05034 * T_UT1) % 360 # ecliptic longitude - lambda_ecliptic = (lambda_M_sun + 1.914666471 * np.sin(np.deg2rad(M_sun)) + - 0.019994643 * np.sin(np.deg2rad(2 * M_sun))) + lambda_ecliptic = ( + lambda_M_sun + + 1.914666471 * np.sin(np.deg2rad(M_sun)) + + 0.019994643 * np.sin(np.deg2rad(2 * M_sun)) + ) # obliquity of the ecliptic (epsilon in Vallado's notation) epsilon = 23.439291 - 0.0130042 * T_UT1 # declination of the sun - delta_sun = np.rad2deg(np.arcsin(np.sin(np.deg2rad(epsilon)) * - np.sin(np.deg2rad(lambda_ecliptic)))) + delta_sun = np.rad2deg( + np.arcsin(np.sin(np.deg2rad(epsilon)) * np.sin(np.deg2rad(lambda_ecliptic))) + ) # Greenwich mean sidereal time (seconds) - theta_GMST = (67310.54841 + - (876600 * 3600 + 8640184.812866) * T_UT1 + - 0.093104 * T_UT1**2 - - 6.2e-6 * T_UT1**3) + theta_GMST = ( + 67310.54841 + + (876600 * 3600 + 8640184.812866) * T_UT1 + + 0.093104 * T_UT1**2 + - 6.2e-6 * T_UT1**3 + ) # Convert to degrees theta_GMST = (theta_GMST % 86400) / 240 # Right ascension calculations - numerator = (np.cos(np.deg2rad(epsilon)) * - np.sin(np.deg2rad(lambda_ecliptic)) / - np.cos(np.deg2rad(delta_sun))) - denominator = (np.cos(np.deg2rad(lambda_ecliptic)) / - np.cos(np.deg2rad(delta_sun))) + numerator = ( + np.cos(np.deg2rad(epsilon)) + * np.sin(np.deg2rad(lambda_ecliptic)) + / np.cos(np.deg2rad(delta_sun)) + ) + denominator = np.cos(np.deg2rad(lambda_ecliptic)) / np.cos(np.deg2rad(delta_sun)) alpha_sun = np.rad2deg(np.arctan2(numerator, denominator)) diff --git a/lib/cartopy/geodesic.py b/lib/cartopy/geodesic.py index 0cd8c17e4..bb612bdd0 100644 --- a/lib/cartopy/geodesic.py +++ b/lib/cartopy/geodesic.py @@ -11,6 +11,7 @@ for more background information. """ + import numpy as np import pyproj import shapely.geometry as sgeom @@ -44,8 +45,10 @@ def __init__(self, radius=6378137.0, flattening=1 / 298.257223563): self.flattening = flattening def __str__(self): - return (f'') + return ( + f'' + ) def direct(self, points, azimuths, distances): """ @@ -85,7 +88,7 @@ def direct(self, points, azimuths, distances): sizes = [pts.shape[0], azims.size, dists.size] n_points = max(sizes) if not all(size in [1, n_points] for size in sizes): - raise ValueError("Inputs must have common length n or length one.") + raise ValueError('Inputs must have common length n or length one.') # Broadcast any length 1 arrays to the correct size. if pts.shape[0] == 1: @@ -133,8 +136,7 @@ def inverse(self, points, endpoints): endpoints = np.array(endpoints, dtype=np.float64) if points.ndim > 2 or (points.ndim == 2 and points.shape[1] != 2): - raise ValueError( - f'Expecting input points to be (N, 2), got {points.shape}') + raise ValueError(f'Expecting input points to be (N, 2), got {points.shape}') pts = points.reshape((-1, 2)) epts = endpoints.reshape((-1, 2)) @@ -142,7 +144,7 @@ def inverse(self, points, endpoints): sizes = [pts.shape[0], epts.shape[0]] n_points = max(sizes) if not all(size in [1, n_points] for size in sizes): - raise ValueError("Inputs must have common length n or length one.") + raise ValueError('Inputs must have common length n or length one.') # Broadcast any length 1 arrays to the correct size. if pts.shape[0] == 1: @@ -155,8 +157,9 @@ def inverse(self, points, endpoints): epts = np.empty([n_points, 2], dtype=np.float64) epts[:, :] = orig_pts - start_azims, end_azims, dists = self.geod.inv(pts[:, 0], pts[:, 1], - epts[:, 0], epts[:, 1]) + start_azims, end_azims, dists = self.geod.inv( + pts[:, 0], pts[:, 1], epts[:, 0], epts[:, 1] + ) # Convert back azimuth to forward azimuth. end_azims += np.where(end_azims > 0, -180, 180) return np.column_stack([dists, start_azims, end_azims]) @@ -189,8 +192,9 @@ def circle(self, lon, lat, radius, n_samples=180, endpoint=False): center = np.array([lon, lat]).reshape((1, 2)) radius_m = np.asarray(radius).reshape(1) - azimuths = np.linspace(360., 0., n_samples, - endpoint=endpoint).astype(np.double) + azimuths = np.linspace(360.0, 0.0, n_samples, endpoint=endpoint).astype( + np.double + ) return self.direct(center, azimuths, radius_m)[:, 0:2] @@ -217,15 +221,13 @@ def geometry_length(self, geometry): # Polygon. result = self.geometry_length(geometry.exterior) - elif (hasattr(geometry, 'coords') and - not isinstance(geometry, sgeom.Point)): + elif hasattr(geometry, 'coords') and not isinstance(geometry, sgeom.Point): coords = np.array(geometry.coords) result = self.geometry_length(coords) elif isinstance(geometry, np.ndarray): coords = geometry - distances, _, _ = np.array( - self.inverse(coords[:-1, :], coords[1:, :]).T) + distances, _, _ = np.array(self.inverse(coords[:-1, :], coords[1:, :]).T) result = distances.sum() else: diff --git a/lib/cartopy/img_transform.py b/lib/cartopy/img_transform.py index d0e44b913..e43962fa4 100644 --- a/lib/cartopy/img_transform.py +++ b/lib/cartopy/img_transform.py @@ -12,21 +12,21 @@ try: from pykdtree.kdtree import KDTree as _kdtreeClass + _is_pykdtree = True except ImportError: try: from scipy.spatial import cKDTree as _kdtreeClass except ImportError as e: - raise ImportError("Using image transforms requires either " - "pykdtree or scipy.") from e + raise ImportError( + 'Using image transforms requires either pykdtree or scipy.' + ) from e _is_pykdtree = False import cartopy.crs as ccrs -def mesh_projection(projection, nx, ny, - x_extents=(None, None), - y_extents=(None, None)): +def mesh_projection(projection, nx, ny, x_extents=(None, None), y_extents=(None, None)): """ Return sample points in the given projection which span the entire projection range evenly. @@ -74,10 +74,8 @@ def extent(specified, default, index): # Calculate evenly spaced sample points spanning the # extent - excluding endpoint. - x, xstep = np.linspace(x_lower, x_upper, nx, retstep=True, - endpoint=False) - y, ystep = np.linspace(y_lower, y_upper, ny, retstep=True, - endpoint=False) + x, xstep = np.linspace(x_lower, x_upper, nx, retstep=True, endpoint=False) + y, ystep = np.linspace(y_lower, y_upper, ny, retstep=True, endpoint=False) # Deal with single point corner case and the difference # between np.linspace v1.9 and v1.10+ retstep nan result. @@ -121,9 +119,15 @@ def warp_img(fname, target_proj, source_proj=None, target_res=(400, 200)): raise NotImplementedError('Not yet implemented.') -def warp_array(array, target_proj, source_proj=None, target_res=(400, 200), - source_extent=None, target_extent=None, - mask_extrapolated=False): +def warp_array( + array, + target_proj, + source_proj=None, + target_res=(400, 200), + source_extent=None, + target_extent=None, + mask_extrapolated=False, +): """ Regrid the data array from the source projection to the target projection. @@ -179,20 +183,30 @@ def warp_array(array, target_proj, source_proj=None, target_res=(400, 200), source_proj = ccrs.PlateCarree() ny, nx = array.shape[:2] - source_native_xy = mesh_projection(source_proj, nx, ny, - x_extents=source_x_extents, - y_extents=source_y_extents) + source_native_xy = mesh_projection( + source_proj, nx, ny, x_extents=source_x_extents, y_extents=source_y_extents + ) # XXX Take into account the extents of the original to determine # target_extents? target_native_x, target_native_y, extent = mesh_projection( - target_proj, target_res[0], target_res[1], - x_extents=target_x_extents, y_extents=target_y_extents) - - array = regrid(array, source_native_xy[0], source_native_xy[1], - source_proj, target_proj, - target_native_x, target_native_y, - mask_extrapolated) + target_proj, + target_res[0], + target_res[1], + x_extents=target_x_extents, + y_extents=target_y_extents, + ) + + array = regrid( + array, + source_native_xy[0], + source_native_xy[1], + source_proj, + target_proj, + target_native_x, + target_native_y, + mask_extrapolated, + ) return array, extent @@ -200,29 +214,38 @@ def _determine_bounds(x_coords, y_coords, source_cs): # Returns bounds corresponding to one or two rectangles depending on # transformation between ranges. bounds = dict(x=[]) - half_px = abs(np.diff(x_coords[:2])).max() / 2. + half_px = abs(np.diff(x_coords[:2])).max() / 2.0 - if (((hasattr(source_cs, 'is_geodetic') and - source_cs.is_geodetic()) or - isinstance(source_cs, ccrs.PlateCarree)) and x_coords.max() > 180): + if ( + (hasattr(source_cs, 'is_geodetic') and source_cs.is_geodetic()) + or isinstance(source_cs, ccrs.PlateCarree) + ) and x_coords.max() > 180: if x_coords.min() < 180: bounds['x'].append([x_coords.min() - half_px, 180]) bounds['x'].append([-180, x_coords.max() - 360 + half_px]) else: - bounds['x'].append([x_coords.min() - 180 - half_px, - x_coords.max() - 180 + half_px]) + bounds['x'].append( + [x_coords.min() - 180 - half_px, x_coords.max() - 180 + half_px] + ) else: - bounds['x'].append([x_coords.min() - half_px, - x_coords.max() + half_px]) + bounds['x'].append([x_coords.min() - half_px, x_coords.max() + half_px]) # y_coords are the centers, so adjust a half-pixel out in y too - half_px = abs(np.diff(y_coords, axis=0)).max() / 2. + half_px = abs(np.diff(y_coords, axis=0)).max() / 2.0 bounds['y'] = [y_coords.min() - half_px, y_coords.max() + half_px] return bounds -def regrid(array, source_x_coords, source_y_coords, source_proj, target_proj, - target_x_points, target_y_points, mask_extrapolated=False): +def regrid( + array, + source_x_coords, + source_y_coords, + source_proj, + target_proj, + target_x_points, + target_y_points, + mask_extrapolated=False, +): """ Regrid the data array from the source projection to the target projection. @@ -260,13 +283,13 @@ def regrid(array, source_x_coords, source_y_coords, source_proj, target_proj, """ # Stack our original xyz array, this will also wrap coords when necessary - xyz = source_proj.transform_points(source_proj, - source_x_coords.flatten(), - source_y_coords.flatten()) + xyz = source_proj.transform_points( + source_proj, source_x_coords.flatten(), source_y_coords.flatten() + ) # Transform the target points into the source projection - target_xyz = source_proj.transform_points(target_proj, - target_x_points.flatten(), - target_y_points.flatten()) + target_xyz = source_proj.transform_points( + target_proj, target_x_points.flatten(), target_y_points.flatten() + ) # Find mask of valid points before querying kdtree: scipy >= 1.11 errors # when querying nan points, might as well use for pykdtree too. @@ -277,9 +300,9 @@ def regrid(array, source_x_coords, source_y_coords, source_proj, target_proj, kdtree = _kdtreeClass(xyz) # Use sqr_dists=True because we don't care about distances, # and it saves a sqrt. - _, indices[finite_xyz] = kdtree.query(target_xyz[finite_xyz, :], - k=1, - sqr_dists=True) + _, indices[finite_xyz] = kdtree.query( + target_xyz[finite_xyz, :], k=1, sqr_dists=True + ) else: # Versions of scipy >= v0.16 added the balanced_tree argument, # which caused the KDTree to hang with this input. @@ -304,22 +327,23 @@ def regrid(array, source_x_coords, source_y_coords, source_proj, target_proj, # to the same point to within a fixed fractional offset. # NOTE: This only needs to be done for (pseudo-)cylindrical projections, # or any others which have the concept of wrapping - back_to_target_xyz = target_proj.transform_points(source_proj, - target_xyz[:, 0], - target_xyz[:, 1]) - back_to_target_x = back_to_target_xyz[:, 0].reshape(desired_ny, - desired_nx) - back_to_target_y = back_to_target_xyz[:, 1].reshape(desired_ny, - desired_nx) + back_to_target_xyz = target_proj.transform_points( + source_proj, target_xyz[:, 0], target_xyz[:, 1] + ) + back_to_target_x = back_to_target_xyz[:, 0].reshape(desired_ny, desired_nx) + back_to_target_y = back_to_target_xyz[:, 1].reshape(desired_ny, desired_nx) FRACTIONAL_OFFSET_THRESHOLD = 0.1 # data has moved by 10% of the map x_extent = np.abs(target_proj.x_limits[1] - target_proj.x_limits[0]) y_extent = np.abs(target_proj.y_limits[1] - target_proj.y_limits[0]) - non_self_inverse_points = (((np.abs(target_x_points - back_to_target_x) / - x_extent) > FRACTIONAL_OFFSET_THRESHOLD) | - ((np.abs(target_y_points - back_to_target_y) / - y_extent) > FRACTIONAL_OFFSET_THRESHOLD)) + non_self_inverse_points = ( + (np.abs(target_x_points - back_to_target_x) / x_extent) + > FRACTIONAL_OFFSET_THRESHOLD + ) | ( + (np.abs(target_y_points - back_to_target_y) / y_extent) + > FRACTIONAL_OFFSET_THRESHOLD + ) if np.any(non_self_inverse_points): if not np.ma.isMaskedArray(new_array): new_array = np.ma.array(new_array, mask=False) @@ -329,21 +353,20 @@ def regrid(array, source_x_coords, source_y_coords, source_proj, target_proj, # Transform the target points to the source projection and mask any points # that fall outside the original source domain. if mask_extrapolated: - target_in_source_x = target_xyz[:, 0].reshape(desired_ny, - desired_nx) - target_in_source_y = target_xyz[:, 1].reshape(desired_ny, - desired_nx) + target_in_source_x = target_xyz[:, 0].reshape(desired_ny, desired_nx) + target_in_source_y = target_xyz[:, 1].reshape(desired_ny, desired_nx) - bounds = _determine_bounds(source_x_coords, source_y_coords, - source_proj) + bounds = _determine_bounds(source_x_coords, source_y_coords, source_proj) - outside_source_domain = ((target_in_source_y >= bounds['y'][1]) | - (target_in_source_y <= bounds['y'][0])) + outside_source_domain = (target_in_source_y >= bounds['y'][1]) | ( + target_in_source_y <= bounds['y'][0] + ) tmp_inside = np.zeros_like(outside_source_domain) for bound_x in bounds['x']: - tmp_inside = tmp_inside | ((target_in_source_x <= bound_x[1]) & - (target_in_source_x >= bound_x[0])) + tmp_inside = tmp_inside | ( + (target_in_source_x <= bound_x[1]) & (target_in_source_x >= bound_x[0]) + ) outside_source_domain = outside_source_domain | ~tmp_inside if np.any(outside_source_domain): diff --git a/lib/cartopy/io/__init__.py b/lib/cartopy/io/__init__.py index a2a62cec6..80bc51ad5 100644 --- a/lib/cartopy/io/__init__.py +++ b/lib/cartopy/io/__init__.py @@ -63,6 +63,7 @@ def fh_getter(fh, mode='r', needs_filename=False): class DownloadWarning(Warning): """Issued when a file is being downloaded by a :class:`Downloader`.""" + pass @@ -113,8 +114,9 @@ class Downloader: """ - def __init__(self, url_template, target_path_template, - pre_downloaded_path_template=''): + def __init__( + self, url_template, target_path_template, pre_downloaded_path_template='' + ): self.url_template = url_template self.target_path_template = target_path_template self.pre_downloaded_path_template = pre_downloaded_path_template @@ -152,8 +154,7 @@ def target_path(self, format_dict): expected as a minimum in their ``FORMAT_KEYS`` class attribute. """ - return Path(self._formatter.format(self.target_path_template, - **format_dict)) + return Path(self._formatter.format(self.target_path_template, **format_dict)) def pre_downloaded_path(self, format_dict): """ @@ -169,8 +170,7 @@ def pre_downloaded_path(self, format_dict): expected as a minimum in their ``FORMAT_KEYS`` class attribute. """ - p = self._formatter.format(self.pre_downloaded_path_template, - **format_dict) + p = self._formatter.format(self.pre_downloaded_path_template, **format_dict) return None if p == '' else Path(p) def path(self, format_dict): @@ -294,8 +294,10 @@ def from_config(specification, config_dict=None): # should never really happen, but could if the user does # some strange things like not having any downloaders defined # in the config... - raise ValueError('No generic downloadable item in the config ' - f'dictionary for {specification}') + raise ValueError( + 'No generic downloadable item in the config ' + f'dictionary for {specification}' + ) return result_downloader @@ -384,8 +386,7 @@ def __init__(self, contained_source): self._source = contained_source def fetch_raster(self, projection, extent, target_resolution): - return self._source.fetch_raster(projection, extent, - target_resolution) + return self._source.fetch_raster(projection, extent, target_resolution) def validate_projection(self, projection): return self._source.validate_projection(projection) diff --git a/lib/cartopy/io/img_nest.py b/lib/cartopy/io/img_nest.py index 7c07163e9..8ec699340 100644 --- a/lib/cartopy/io/img_nest.py +++ b/lib/cartopy/io/img_nest.py @@ -160,17 +160,20 @@ def world_file_extent(worldfile_handle, im_shape): pix_size = (float(lines[0]), float(lines[3])) pix_rotation = (float(lines[1]), float(lines[2])) - if pix_rotation != (0., 0.): - raise ValueError('Rotated pixels in world files is not currently ' - 'supported.') + if pix_rotation != (0.0, 0.0): + raise ValueError( + 'Rotated pixels in world files is not currently supported.' + ) ul_corner = (float(lines[4]), float(lines[5])) - min_x, max_x = (ul_corner[0] - pix_size[0] / 2., - ul_corner[0] + pix_size[0] * im_shape[0] - - pix_size[0] / 2.) - min_y, max_y = (ul_corner[1] - pix_size[1] / 2., - ul_corner[1] + pix_size[1] * im_shape[1] - - pix_size[1] / 2.) + min_x, max_x = ( + ul_corner[0] - pix_size[0] / 2.0, + ul_corner[0] + pix_size[0] * im_shape[0] - pix_size[0] / 2.0, + ) + min_y, max_y = ( + ul_corner[1] - pix_size[1] / 2.0, + ul_corner[1] + pix_size[1] * im_shape[1] - pix_size[1] / 2.0, + ) return (min_x, max_x, min_y, max_y), pix_size @@ -196,8 +199,7 @@ def __init__(self, name, crs, images=None): self.crs = crs self.images = images or [] - def scan_dir_for_imgs(self, directory, glob_pattern='*.tif', - img_class=Img): + def scan_dir_for_imgs(self, directory, glob_pattern='*.tif', img_class=Img): """ Search the given directory for the associated world files of the image files. @@ -227,8 +229,7 @@ def scan_dir_for_imgs(self, directory, glob_pattern='*.tif', if fworld.exists(): break else: - raise ValueError( - f'Image file {img!r} has no associated world file') + raise ValueError(f'Image file {img!r} has no associated world file') self.images.append(img_class.from_world_file(img, fworld)) @@ -261,13 +262,15 @@ def __init__(self, name, crs, collections, _ancestry=None): """ # NOTE: all collections must have the same crs. _names = {collection.name for collection in collections} - assert len(_names) == len(collections), \ + assert len(_names) == len(collections), ( 'The collections must have unique names.' + ) self.name = name self.crs = crs - self._collections_by_name = {collection.name: collection - for collection in collections} + self._collections_by_name = { + collection.name: collection for collection in collections + } def sort_func(c): return np.max([image.bbox().area for image in c.images]) @@ -281,8 +284,7 @@ def sort_func(c): if _ancestry is not None: self._ancestry = _ancestry else: - parent_wth_children = zip(self._collections, - self._collections[1:]) + parent_wth_children = zip(self._collections, self._collections[1:]) for parent_collection, collection in parent_wth_children: for parent_image in parent_collection.images: for image in collection.images: @@ -290,7 +292,8 @@ def sort_func(c): # append the child image to the parent's ancestry key = (parent_collection.name, parent_image) self._ancestry.setdefault(key, []).append( - (collection.name, image)) + (collection.name, image) + ) # TODO check that the ancestry is in a good state (i.e. that each # collection has child images) @@ -341,8 +344,7 @@ def image_for_domain(self, target_domain, target_z): # XXX Copied from cartopy.io.img_tiles if target_z not in self._collections_by_name: # TODO: Handle integer depths also? - raise ValueError( - f'{target_z!r} is not one of the possible collections.') + raise ValueError(f'{target_z!r} is not one of the possible collections.') tiles = [] for tile in self.find_images(target_domain, target_z): @@ -353,13 +355,12 @@ def image_for_domain(self, target_domain, target_z): img = np.array(img) - x = np.linspace(extent[0], extent[1], img.shape[1], - endpoint=False) - y = np.linspace(extent[2], extent[3], img.shape[0], - endpoint=False) + x = np.linspace(extent[0], extent[1], img.shape[1], endpoint=False) + y = np.linspace(extent[2], extent[3], img.shape[0], endpoint=False) tiles.append([np.array(img), x, y, origin]) from cartopy.io.img_tiles import _merge_tiles + img, extent, origin = _merge_tiles(tiles) return img, extent, origin @@ -395,25 +396,24 @@ def find_images(self, target_domain, target_z, start_tiles=None): # XXX Copied from cartopy.io.img_tiles if target_z not in self._collections_by_name: # TODO: Handle integer depths also? - raise ValueError( - f'{target_z!r} is not one of the possible collections.') + raise ValueError(f'{target_z!r} is not one of the possible collections.') if start_tiles is None: - start_tiles = ((self._collections[0].name, img) - for img in self._collections[0].images) + start_tiles = ( + (self._collections[0].name, img) for img in self._collections[0].images + ) for start_tile in start_tiles: # recursively drill down to the images at the target zoom domain = start_tile[1].bbox() - if target_domain.intersects(domain) and \ - not target_domain.touches(domain): + if target_domain.intersects(domain) and not target_domain.touches(domain): if start_tile[0] == target_z: yield start_tile else: for tile in self.subtiles(start_tile): - yield from self.find_images(target_domain, - target_z, - start_tiles=[tile]) + yield from self.find_images( + target_domain, target_z, start_tiles=[tile] + ) def subtiles(self, collection_image): """ @@ -470,9 +470,9 @@ def get_image(self, collection_image): return img_data, img.extent, img.origin @classmethod - def from_configuration(cls, name, crs, name_dir_pairs, - glob_pattern='*.tif', - img_class=Img): + def from_configuration( + cls, name, crs, name_dir_pairs, glob_pattern='*.tif', img_class=Img + ): """ Create a :class:`~cartopy.io.img_nest.NestedImageCollection` instance given the list of image collection name and directory path pairs. @@ -519,8 +519,8 @@ def from_configuration(cls, name, crs, name_dir_pairs, collections = [] for collection_name, collection_dir in name_dir_pairs: collection = ImageCollection(collection_name, crs) - collection.scan_dir_for_imgs(collection_dir, - glob_pattern=glob_pattern, - img_class=img_class) + collection.scan_dir_for_imgs( + collection_dir, glob_pattern=glob_pattern, img_class=img_class + ) collections.append(collection) return cls(name, crs, collections) diff --git a/lib/cartopy/io/img_tiles.py b/lib/cartopy/io/img_tiles.py index a9e92af38..bc2b94a91 100644 --- a/lib/cartopy/io/img_tiles.py +++ b/lib/cartopy/io/img_tiles.py @@ -44,10 +44,15 @@ class GoogleWTS(metaclass=ABCMeta): (the default behavior), the tiles are downloaded each time. """ + _MAX_THREADS = 24 - def __init__(self, desired_tile_form='RGB', - user_agent=f'CartoPy/{cartopy.__version__}', cache=False): + def __init__( + self, + desired_tile_form='RGB', + user_agent=f'CartoPy/{cartopy.__version__}', + cache=False, + ): self.imgs = [] self.crs = ccrs.Mercator.GOOGLE self.desired_tile_form = desired_tile_form @@ -60,7 +65,7 @@ def __init__(self, desired_tile_form='RGB', self._default_cache = False if cache is True: self._default_cache = True - self.cache_path = Path(cartopy.config["cache_dir"]) + self.cache_path = Path(cartopy.config['cache_dir']) elif cache is False: self.cache_path = None else: @@ -84,7 +89,8 @@ def fetch_tile(tile): return img, x, y, origin with concurrent.futures.ThreadPoolExecutor( - max_workers=self._MAX_THREADS) as executor: + max_workers=self._MAX_THREADS + ) as executor: futures = [] for tile in self.find_images(target_domain, target_z): futures.append(executor.submit(fetch_tile, tile)) @@ -112,15 +118,16 @@ def _load_cache(self): if self._default_cache: warnings.warn( 'Cartopy created the following directory to cache ' - f'GoogleWTS tiles: {cache_dir}') + f'GoogleWTS tiles: {cache_dir}' + ) self.cache = self.cache.union(set(cache_dir.iterdir())) def _find_images(self, target_domain, target_z, start_tile=(0, 0, 0)): """Target domain is a shapely polygon in native coordinates.""" - assert isinstance(target_z, int) and target_z >= 0, ('target_z must ' - 'be an integer ' - '>=0.') + assert isinstance(target_z, int) and target_z >= 0, ( + 'target_z must be an integer >=0.' + ) # Recursively drill down to the images at the target zoom. x0, x1, y0, y1 = self._tileextent(start_tile) @@ -130,8 +137,9 @@ def _find_images(self, target_domain, target_z, start_tile=(0, 0, 0)): yield start_tile else: for tile in self._subtiles(start_tile): - yield from self._find_images(target_domain, target_z, - start_tile=tile) + yield from self._find_images( + target_domain, target_z, start_tile=tile + ) find_images = _find_images @@ -166,11 +174,13 @@ def tile_bbox(self, x, y, z, y0_at_north_pole=True): """ - n = 2 ** z - assert 0 <= x <= (n - 1), \ + n = 2**z + assert 0 <= x <= (n - 1), ( f"Tile's x index is out of range. Upper limit {n}. Got {x}" - assert 0 <= y <= (n - 1), \ + ) + assert 0 <= y <= (n - 1), ( f"Tile's y index is out of range. Upper limit {n}. Got {y}" + ) x0, x1 = self.crs.x_limits y0, y1 = self.crs.y_limits @@ -205,7 +215,7 @@ def get_image(self, tile): from urllib.request import HTTPError, Request, URLError, urlopen if self.cache_path is not None: - filename = "_".join([str(i) for i in tile]) + ".npy" + filename = '_'.join([str(i) for i in tile]) + '.npy' cached_file = self._cache_dir / filename else: cached_file = None @@ -215,7 +225,7 @@ def get_image(self, tile): else: url = self._image_url(tile) try: - request = Request(url, headers={"User-Agent": self.user_agent}) + request = Request(url, headers={'User-Agent': self.user_agent}) fh = urlopen(request) im_data = io.BytesIO(fh.read()) fh.close() @@ -223,8 +233,9 @@ def get_image(self, tile): except (HTTPError, URLError) as err: print(err) - img = Image.fromarray(np.full((256, 256, 3), (250, 250, 250), - dtype=np.uint8)) + img = Image.fromarray( + np.full((256, 256, 3), (250, 250, 250), dtype=np.uint8) + ) img = img.convert(self.desired_tile_form) if self.cache_path is not None: @@ -235,10 +246,16 @@ def get_image(self, tile): class GoogleTiles(GoogleWTS): - def __init__(self, desired_tile_form='RGB', style="street", - url=('https://mts0.google.com/vt/lyrs={style}' - '@177000000&hl=en&src=api&x={x}&y={y}&z={z}&s=G'), - cache=False): + def __init__( + self, + desired_tile_form='RGB', + style='street', + url=( + 'https://mts0.google.com/vt/lyrs={style}' + '@177000000&hl=en&src=api&x={x}&y={y}&z={z}&s=G' + ), + cache=False, + ): """ Parameters ---------- @@ -253,36 +270,43 @@ def __init__(self, desired_tile_form='RGB', style="street", World_Shaded_Relief/MapServer/tile/{z}/{y}/{x}.jpg'`` """ - styles = ["street", "satellite", "terrain", "only_streets"] + styles = ['street', 'satellite', 'terrain', 'only_streets'] style = style.lower() self.url = url if style not in styles: raise ValueError( - f"Invalid style {style!r}. Valid styles: {', '.join(styles)}") + f'Invalid style {style!r}. Valid styles: {", ".join(styles)}' + ) self.style = style # The 'satellite' and 'terrain' styles require pillow with a jpeg # decoder. - if self.style in ["satellite", "terrain"] and \ - not hasattr(Image.core, "jpeg_decoder") or \ - not Image.core.jpeg_decoder: + if ( + self.style in ['satellite', 'terrain'] + and not hasattr(Image.core, 'jpeg_decoder') + or not Image.core.jpeg_decoder + ): raise ValueError( - f"The {self.style!r} style requires pillow with jpeg decoding " - "support.") - return super().__init__(desired_tile_form=desired_tile_form, - cache=cache) + f'The {self.style!r} style requires pillow with jpeg decoding support.' + ) + return super().__init__(desired_tile_form=desired_tile_form, cache=cache) def _image_url(self, tile): style_dict = { - "street": "m", - "satellite": "s", - "terrain": "t", - "only_streets": "h"} + 'street': 'm', + 'satellite': 's', + 'terrain': 't', + 'only_streets': 'h', + } url = self.url.format( style=style_dict[self.style], - x=tile[0], X=tile[0], - y=tile[1], Y=tile[1], - z=tile[2], Z=tile[2]) + x=tile[0], + X=tile[0], + y=tile[1], + Y=tile[1], + z=tile[2], + Z=tile[2], + ) return url @@ -294,11 +318,15 @@ class MapQuestOSM(GoogleWTS): def _image_url(self, tile): x, y, z = tile url = f'https://otile1.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.jpg' - mqdevurl = ('https://devblog.mapquest.com/2016/06/15/' - 'modernization-of-mapquest-results-in-changes' - '-to-open-tile-access/') - warnings.warn(f'{url} will require a log in and will likely' - f' fail. see {mqdevurl} for more details.') + mqdevurl = ( + 'https://devblog.mapquest.com/2016/06/15/' + 'modernization-of-mapquest-results-in-changes' + '-to-open-tile-access/' + ) + warnings.warn( + f'{url} will require a log in and will likely' + f' fail. see {mqdevurl} for more details.' + ) return url @@ -358,26 +386,24 @@ class StadiaMapsTiles(GoogleWTS): used. If a string, the string is used as the path to the cache. """ - def __init__(self, - apikey, - style="alidade_smooth", - resolution="", - cache=False): - super().__init__(cache=cache, desired_tile_form="RGBA") + def __init__(self, apikey, style='alidade_smooth', resolution='', cache=False): + super().__init__(cache=cache, desired_tile_form='RGBA') self.apikey = apikey self.style = style self.resolution = resolution - if style == "stamen_watercolor": + if style == 'stamen_watercolor': # Known style that has the jpg extension - self.extension = "jpg" + self.extension = 'jpg' else: - self.extension = "png" + self.extension = 'png' def _image_url(self, tile): x, y, z = tile - return ("http://tiles.stadiamaps.com/tiles/" - f"{self.style}/{z}/{x}/{y}{self.resolution}.{self.extension}" - f"?api_key={self.apikey}") + return ( + 'http://tiles.stadiamaps.com/tiles/' + f'{self.style}/{z}/{x}/{y}{self.resolution}.{self.extension}' + f'?api_key={self.apikey}' + ) class Stamen(GoogleWTS): @@ -409,30 +435,30 @@ class Stamen(GoogleWTS): """ - def __init__(self, style='toner', - desired_tile_form=None, cache=False): - warnings.warn("The Stamen styles are no longer served by Stamen and " - "are now served by Stadia Maps. Please use the " - "StadiaMapsTiles class instead.") + def __init__(self, style='toner', desired_tile_form=None, cache=False): + warnings.warn( + 'The Stamen styles are no longer served by Stamen and ' + 'are now served by Stadia Maps. Please use the ' + 'StadiaMapsTiles class instead.' + ) # preset layer configuration layer_config = { - 'terrain': {'extension': 'png', 'opaque': True}, - 'terrain-background': {'extension': 'png', 'opaque': True}, - 'terrain-labels': {'extension': 'png', 'opaque': False}, - 'terrain-lines': {'extension': 'png', 'opaque': False}, - 'toner-background': {'extension': 'png', 'opaque': True}, - 'toner': {'extension': 'png', 'opaque': True}, - 'toner-hybrid': {'extension': 'png', 'opaque': False}, - 'toner-labels': {'extension': 'png', 'opaque': False}, - 'toner-lines': {'extension': 'png', 'opaque': False}, - 'toner-lite': {'extension': 'png', 'opaque': True}, - 'watercolor': {'extension': 'jpg', 'opaque': True}, + 'terrain': {'extension': 'png', 'opaque': True}, + 'terrain-background': {'extension': 'png', 'opaque': True}, + 'terrain-labels': {'extension': 'png', 'opaque': False}, + 'terrain-lines': {'extension': 'png', 'opaque': False}, + 'toner-background': {'extension': 'png', 'opaque': True}, + 'toner': {'extension': 'png', 'opaque': True}, + 'toner-hybrid': {'extension': 'png', 'opaque': False}, + 'toner-labels': {'extension': 'png', 'opaque': False}, + 'toner-lines': {'extension': 'png', 'opaque': False}, + 'toner-lite': {'extension': 'png', 'opaque': True}, + 'watercolor': {'extension': 'jpg', 'opaque': True}, } # get layer information from dict - layer_info = layer_config.get( - style, {'extension': '.png', 'opaque': True}) + layer_info = layer_config.get(style, {'extension': '.png', 'opaque': True}) # use optional desired_tile_form input if available # otherwise, use preset value based on the layer name @@ -442,15 +468,13 @@ def __init__(self, style='toner', else: desired_tile_form = 'RGBA' - super().__init__(desired_tile_form=desired_tile_form, - cache=cache) + super().__init__(desired_tile_form=desired_tile_form, cache=cache) self.style = style self.extension = layer_info['extension'] def _image_url(self, tile): x, y, z = tile - return 'http://tile.stamen.com/' + \ - f'{self.style}/{z}/{x}/{y}.{self.extension}' + return 'http://tile.stamen.com/' + f'{self.style}/{z}/{x}/{y}.{self.extension}' class MapboxTiles(GoogleWTS): @@ -491,8 +515,10 @@ def __init__(self, access_token, map_id, cache=False): def _image_url(self, tile): x, y, z = tile - return (f'https://api.mapbox.com/styles/v1/mapbox/{self.map_id}/tiles' - f'/{z}/{x}/{y}?access_token={self.access_token}') + return ( + f'https://api.mapbox.com/styles/v1/mapbox/{self.map_id}/tiles' + f'/{z}/{x}/{y}?access_token={self.access_token}' + ) class MapboxStyleTiles(GoogleWTS): @@ -532,9 +558,11 @@ def __init__(self, access_token, username, map_id, cache=False): def _image_url(self, tile): x, y, z = tile - return (f'https://api.mapbox.com/styles/v1/{self.username}' - f'/{self.map_id}/tiles/256/{z}/{x}/{y}' - f'?access_token={self.access_token}') + return ( + f'https://api.mapbox.com/styles/v1/{self.username}' + f'/{self.map_id}/tiles/256/{z}/{x}/{y}' + f'?access_token={self.access_token}' + ) class QuadtreeTiles(GoogleWTS): @@ -548,17 +576,19 @@ class QuadtreeTiles(GoogleWTS): """ def _image_url(self, tile): - return ('http://ecn.dynamic.t1.tiles.virtualearth.net/comp/' - f'CompositionHandler/{tile}?mkt=en-' - 'gb&it=A,G,L&shading=hill&n=z') + return ( + 'http://ecn.dynamic.t1.tiles.virtualearth.net/comp/' + f'CompositionHandler/{tile}?mkt=en-' + 'gb&it=A,G,L&shading=hill&n=z' + ) def tms_to_quadkey(self, tms, google=False): - quadKey = "" + quadKey = '' x, y, z = tms # this algorithm works with google tiles, rather than tms, so convert # to those first. if not google: - y = (2 ** z - 1) - y + y = (2**z - 1) - y for i in range(z, 0, -1): digit = 0 mask = 1 << (i - 1) @@ -591,7 +621,7 @@ def quadkey_to_tms(self, quadkey, google=False): raise ValueError(f'Invalid QuadKey digit sequence: {quadkey}') # the algorithm works to google tiles, so convert to tms if not google: - y = (2 ** z - 1) - y + y = (2**z - 1) - y return (x, y, z) def subtiles(self, quadkey): @@ -620,8 +650,9 @@ def find_images(self, target_domain, target_z, start_tile=None): for start_tile in start_tiles: start_tile = self.quadkey_to_tms(start_tile, google=True) - for tile in GoogleWTS.find_images(self, target_domain, target_z, - start_tile=start_tile): + for tile in GoogleWTS.find_images( + self, target_domain, target_z, start_tile=start_tile + ): yield self.tms_to_quadkey(tile, google=True) @@ -638,13 +669,10 @@ class OrdnanceSurvey(GoogleWTS): For the API framework agreement, see https://osdatahub.os.uk/legal/apiTermsConditions. """ + # API Documentation: https://osdatahub.os.uk/docs/wmts/overview - def __init__(self, - apikey, - layer='Road_3857', - desired_tile_form='RGB', - cache=False): + def __init__(self, apikey, layer='Road_3857', desired_tile_form='RGB', cache=False): """ Parameters ---------- @@ -660,29 +688,35 @@ def __init__(self, desired_tile_form: optional Defaults to 'RGB'. """ - super().__init__(desired_tile_form=desired_tile_form, - cache=cache) + super().__init__(desired_tile_form=desired_tile_form, cache=cache) self.apikey = apikey - if layer not in ("Road_3857", "Outdoor_3857", "Light_3857", - "Road", "Outdoor", "Light"): + if layer not in ( + 'Road_3857', + 'Outdoor_3857', + 'Light_3857', + 'Road', + 'Outdoor', + 'Light', + ): raise ValueError(f'Invalid layer {layer}') - elif layer in ("Road", "Outdoor", "Light"): - layer += "_3857" + elif layer in ('Road', 'Outdoor', 'Light'): + layer += '_3857' self.layer = layer def _image_url(self, tile): x, y, z = tile - return f"https://api.os.uk/maps/raster/v1/zxy/" \ - f"{self.layer}/{z}/{x}/{y}.png?key={self.apikey}" + return ( + f'https://api.os.uk/maps/raster/v1/zxy/' + f'{self.layer}/{z}/{x}/{y}.png?key={self.apikey}' + ) def _merge_tiles(tiles): """Return a single image, merging the given images.""" if not tiles: - raise ValueError('A non-empty list of tiles should ' - 'be provided to merge.') + raise ValueError('A non-empty list of tiles should be provided to merge.') xset = [set(x) for i, x, y, _ in tiles] yset = [set(y) for i, x, y, _ in tiles] @@ -735,9 +769,14 @@ def _merge_tiles(tiles): class AzureMapsTiles(GoogleWTS): - - def __init__(self, subscription_key, tileset_id="microsoft.imagery", - api_version="2.0", desired_tile_form='RGB', cache=False): + def __init__( + self, + subscription_key, + tileset_id='microsoft.imagery', + api_version='2.0', + desired_tile_form='RGB', + cache=False, + ): """ Set up a new instance to retrieve tiles from Azure Maps. @@ -767,13 +806,14 @@ def _image_url(self, tile): return ( f'https://atlas.microsoft.com/map/tile?' f'api-version={self.api_version}&tilesetId={self.tileset_id}&' - f'x={x}&y={y}&zoom={z}&subscription-key={self.subscription_key}') + f'x={x}&y={y}&zoom={z}&subscription-key={self.subscription_key}' + ) class LINZMapsTiles(GoogleWTS): - - def __init__(self, apikey, layer_id, api_version="v4", - desired_tile_form='RGB', cache=False): + def __init__( + self, apikey, layer_id, api_version='v4', desired_tile_form='RGB', cache=False + ): """ Set up a new instance to retrieve tiles from The LINZ aka. Land Information New Zealand @@ -803,4 +843,5 @@ def _image_url(self, tile): return ( f'https://tiles-a.koordinates.com/services;' f'key={self.apikey}/tiles/{self.api_version}/' - f'layer={self.layer_id}/EPSG:3857/{z}/{x}/{y}.png') + f'layer={self.layer_id}/EPSG:3857/{z}/{x}/{y}.png' + ) diff --git a/lib/cartopy/io/ogc_clients.py b/lib/cartopy/io/ogc_clients.py index 4e463c0b0..19f6d71fb 100644 --- a/lib/cartopy/io/ogc_clients.py +++ b/lib/cartopy/io/ogc_clients.py @@ -53,10 +53,12 @@ # Hardcode some known EPSG codes for now. # The order given here determines the preferred SRS for WMS retrievals. _CRS_TO_OGC_SRS = collections.OrderedDict( - [(ccrs.PlateCarree(), ['EPSG:4326']), - (ccrs.Mercator.GOOGLE, ['EPSG:3857', 'EPSG:900913']), - (ccrs.OSGB(approx=True), ['EPSG:27700']) - ]) + [ + (ccrs.PlateCarree(), ['EPSG:4326']), + (ccrs.Mercator.GOOGLE, ['EPSG:3857', 'EPSG:900913']), + (ccrs.OSGB(approx=True), ['EPSG:27700']), + ] +) # Standard pixel size of 0.28 mm as defined by WMTS. METERS_PER_PIXEL = 0.28e-3 @@ -70,32 +72,43 @@ 'urn:ogc:def:crs:EPSG::3031': 1, 'urn:ogc:def:crs:EPSG::3413': 1, 'urn:ogc:def:crs:EPSG::3857': 1, - 'urn:ogc:def:crs:EPSG:6.18.3:3857': 1 + 'urn:ogc:def:crs:EPSG:6.18.3:3857': 1, } _URN_TO_CRS = collections.OrderedDict( - [('urn:ogc:def:crs:OGC:1.3:CRS84', ccrs.PlateCarree()), - ('urn:ogc:def:crs:EPSG::4326', ccrs.PlateCarree()), - ('urn:ogc:def:crs:EPSG::900913', ccrs.Mercator.GOOGLE), - ('urn:ogc:def:crs:EPSG::27700', ccrs.OSGB(approx=True)), - ('urn:ogc:def:crs:EPSG::3031', ccrs.Stereographic( - central_latitude=-90, - true_scale_latitude=-71)), - ('urn:ogc:def:crs:EPSG::3413', ccrs.Stereographic( - central_longitude=-45, - central_latitude=90, - true_scale_latitude=70)), - ('urn:ogc:def:crs:EPSG::3857', ccrs.Mercator.GOOGLE), - ('urn:ogc:def:crs:EPSG:6.18.3:3857', ccrs.Mercator.GOOGLE) - ]) + [ + ('urn:ogc:def:crs:OGC:1.3:CRS84', ccrs.PlateCarree()), + ('urn:ogc:def:crs:EPSG::4326', ccrs.PlateCarree()), + ('urn:ogc:def:crs:EPSG::900913', ccrs.Mercator.GOOGLE), + ('urn:ogc:def:crs:EPSG::27700', ccrs.OSGB(approx=True)), + ( + 'urn:ogc:def:crs:EPSG::3031', + ccrs.Stereographic(central_latitude=-90, true_scale_latitude=-71), + ), + ( + 'urn:ogc:def:crs:EPSG::3413', + ccrs.Stereographic( + central_longitude=-45, central_latitude=90, true_scale_latitude=70 + ), + ), + ('urn:ogc:def:crs:EPSG::3857', ccrs.Mercator.GOOGLE), + ('urn:ogc:def:crs:EPSG:6.18.3:3857', ccrs.Mercator.GOOGLE), + ] +) # XML namespace definitions _MAP_SERVER_NS = '{http://mapserver.gis.umn.edu/mapserver}' _GML_NS = '{http://www.opengis.net/gml}' -def _warped_located_image(image, source_projection, source_extent, - output_projection, output_extent, target_resolution): +def _warped_located_image( + image, + source_projection, + source_extent, + output_projection, + output_extent, + target_resolution, +): """ Reproject an Image from one source-projection and extent to another. @@ -113,22 +126,22 @@ def _warped_located_image(image, source_projection, source_extent, # is 'lower'). # Convert to RGBA to keep the color palette in the regrid process # if any - img, extent = warp_array(np.asanyarray(image.convert('RGBA'))[::-1], - source_proj=source_projection, - source_extent=source_extent, - target_proj=output_projection, - target_res=np.asarray(target_resolution, - dtype=int), - target_extent=output_extent, - mask_extrapolated=True) + img, extent = warp_array( + np.asanyarray(image.convert('RGBA'))[::-1], + source_proj=source_projection, + source_extent=source_extent, + target_proj=output_projection, + target_res=np.asarray(target_resolution, dtype=int), + target_extent=output_extent, + mask_extrapolated=True, + ) # Convert arrays with masked RGB(A) values to non-masked RGBA # arrays, setting the alpha channel to zero for masked values. # This avoids unsightly grey boundaries appearing when the # extent is limited (i.e. not global). if np.ma.is_masked(img): - img[:, :, 3] = np.where(np.any(img.mask, axis=2), 0, - img[:, :, 3]) + img[:, :, 3] = np.where(np.any(img.mask, axis=2), 0, img[:, :, 3]) img = img.data # Convert warped image array back to an Image, undoing the @@ -154,16 +167,15 @@ def _target_extents(extent, requested_projection, available_projection): # If the requested area (i.e. target_box) is bigger (or nearly bigger) than # the entire output requested_projection domain, then we erode the request # area to avoid re-projection instabilities near the projection boundary. - buffered_target_box = target_box.buffer(requested_projection.threshold, - resolution=1) + buffered_target_box = target_box.buffer( + requested_projection.threshold, resolution=1 + ) fudge_mode = buffered_target_box.contains(requested_projection.domain) if fudge_mode: - target_box = requested_projection.domain.buffer( - -requested_projection.threshold) + target_box = requested_projection.domain.buffer(-requested_projection.threshold) # Translate the requested area into the server projection. - polys = available_projection.project_geometry(target_box, - requested_projection) + polys = available_projection.project_geometry(target_box, requested_projection) # Return the polygons' rectangular bounds as extent tuples. target_extents = [] @@ -237,8 +249,7 @@ def __init__(self, service, layers, getmap_extra_kwargs=None): raise ValueError('One or more layers must be defined.') for layer in layers: if layer not in service.contents: - raise ValueError(f'The {layer!r} layer does not exist in ' - 'this service.') + raise ValueError(f'The {layer!r} layer does not exist in this service.') #: The OWSLib WebMapService instance. self.service = service @@ -265,8 +276,7 @@ def _native_srs(self, projection): for native_srs in native_srs_list: native_OK = all( - native_srs.lower() in map( - str.lower, contents[layer].crsOptions) + native_srs.lower() in map(str.lower, contents[layer].crsOptions) for layer in self.layers ) if native_OK: @@ -286,31 +296,45 @@ def _fallback_proj_and_srs(self): for srs in srs_list: srs_OK = all( srs.lower() in map(str.lower, contents[layer].crsOptions) - for layer in self.layers) + for layer in self.layers + ) if srs_OK: return proj, srs - raise ValueError('The requested layers are not available in a ' - 'known SRS.') + raise ValueError('The requested layers are not available in a known SRS.') def validate_projection(self, projection): if self._native_srs(projection) is None: self._fallback_proj_and_srs() - def _image_and_extent(self, wms_proj, wms_srs, wms_extent, output_proj, - output_extent, target_resolution): + def _image_and_extent( + self, + wms_proj, + wms_srs, + wms_extent, + output_proj, + output_extent, + target_resolution, + ): min_x, max_x, min_y, max_y = wms_extent - wms_image = self.service.getmap(layers=self.layers, - srs=wms_srs, - bbox=(min_x, min_y, max_x, max_y), - size=target_resolution, - format='image/png', - **self.getmap_extra_kwargs) + wms_image = self.service.getmap( + layers=self.layers, + srs=wms_srs, + bbox=(min_x, min_y, max_x, max_y), + size=target_resolution, + format='image/png', + **self.getmap_extra_kwargs, + ) wms_image = Image.open(io.BytesIO(wms_image.read())) - return _warped_located_image(wms_image, wms_proj, wms_extent, - output_proj, output_extent, - target_resolution) + return _warped_located_image( + wms_image, + wms_proj, + wms_extent, + output_proj, + output_extent, + target_resolution, + ) def fetch_raster(self, projection, extent, target_resolution): target_resolution = [math.ceil(val) for val in target_resolution] @@ -329,10 +353,11 @@ def fetch_raster(self, projection, extent, target_resolution): located_images = [] for wms_extent in wms_extents: - located_images.append(self._image_and_extent(wms_proj, wms_srs, - wms_extent, - projection, extent, - target_resolution)) + located_images.append( + self._image_and_extent( + wms_proj, wms_srs, wms_extent, projection, extent, target_resolution + ) + ) return located_images @@ -379,16 +404,19 @@ def __init__(self, wmts, layer_name, gettile_extra_kwargs=None, cache=False): if WebMapService is None: raise ImportError(_OWSLIB_REQUIRED) - if not (hasattr(wmts, 'tilematrixsets') and - hasattr(wmts, 'contents') and - hasattr(wmts, 'gettile')): + if not ( + hasattr(wmts, 'tilematrixsets') + and hasattr(wmts, 'contents') + and hasattr(wmts, 'gettile') + ): wmts = owslib.wmts.WebMapTileService(wmts) try: layer = wmts.contents[layer_name] except KeyError: raise ValueError( - f'Invalid layer name {layer_name!r} for WMTS at {wmts.url!r}') + f'Invalid layer name {layer_name!r} for WMTS at {wmts.url!r}' + ) #: The OWSLib WebMapTileService instance. self.wmts = wmts @@ -407,7 +435,7 @@ def __init__(self, wmts, layer_name, gettile_extra_kwargs=None, cache=False): self._default_cache = False if cache is True: self._default_cache = True - self.cache_path = Path(cartopy.config["cache_dir"]) + self.cache_path = Path(cartopy.config['cache_dir']) elif cache is False: self.cache_path = None else: @@ -453,9 +481,12 @@ def find_projection(match_projection): break if matrix_set_name is None: # Fail completely. - available_urns = sorted({ - self.wmts.tilematrixsets[name].crs - for name in matrix_set_names}) + available_urns = sorted( + { + self.wmts.tilematrixsets[name].crs + for name in matrix_set_names + } + ) msg = 'Unable to find tile matrix for projection.' msg += f'\n Projection: {target_projection}' msg += '\n Available tile CRS URNs:' @@ -476,8 +507,7 @@ def fetch_raster(self, projection, extent, target_resolution): epsg_num = crs_urn.split(':')[-1] wmts_projection = ccrs.epsg(int(epsg_num)) else: - raise ValueError(f'Unknown coordinate reference system string:' - f' {crs_urn}') + raise ValueError(f'Unknown coordinate reference system string: {crs_urn}') if wmts_projection == projection: wmts_extents = [extent] @@ -500,18 +530,19 @@ def fetch_raster(self, projection, extent, target_resolution): # which is potentially excessive! min_x, max_x, min_y, max_y = wmts_desired_extent if wmts_projection == projection: - max_pixel_span = min((max_x - min_x) / width, - (max_y - min_y) / height) + max_pixel_span = min((max_x - min_x) / width, (max_y - min_y) / height) else: # X/Y orientation is arbitrary, so use a worst-case guess. - max_pixel_span = (min(max_x - min_x, max_y - min_y) / - max(width, height)) + max_pixel_span = min(max_x - min_x, max_y - min_y) / max(width, height) # Fetch a suitable image and its actual extent. wmts_image, wmts_actual_extent = self._wmts_images( - self.wmts, self.layer, matrix_set_name, + self.wmts, + self.layer, + matrix_set_name, extent=wmts_desired_extent, - max_pixel_span=max_pixel_span) + max_pixel_span=max_pixel_span, + ) # Return each (image, extent) as a LocatedImage. if wmts_projection == projection: @@ -520,9 +551,12 @@ def fetch_raster(self, projection, extent, target_resolution): # Reproject the image to the desired projection. located_image = _warped_located_image( wmts_image, - wmts_projection, wmts_actual_extent, - output_projection=projection, output_extent=extent, - target_resolution=target_resolution) + wmts_projection, + wmts_actual_extent, + output_projection=projection, + output_extent=extent, + target_resolution=target_resolution, + ) located_images.append(located_image) @@ -542,14 +576,15 @@ def _load_cache(self): if self._default_cache: warnings.warn( 'Cartopy created the following directory to cache ' - f'WMTSRasterSource tiles: {cache_dir}') + f'WMTSRasterSource tiles: {cache_dir}' + ) self.cache = self.cache.union(set(cache_dir.iterdir())) def _choose_matrix(self, tile_matrices, meters_per_unit, max_pixel_span): # Get the tile matrices in order of increasing resolution. - tile_matrices = sorted(tile_matrices, - key=lambda tm: tm.scaledenominator, - reverse=True) + tile_matrices = sorted( + tile_matrices, key=lambda tm: tm.scaledenominator, reverse=True + ) # Find which tile matrix has the appropriate resolution. max_scale = max_pixel_span * meters_per_unit / METERS_PER_PIXEL @@ -559,14 +594,14 @@ def _choose_matrix(self, tile_matrices, meters_per_unit, max_pixel_span): return tile_matrices[-1] def _tile_span(self, tile_matrix, meters_per_unit): - pixel_span = (tile_matrix.scaledenominator * - (METERS_PER_PIXEL / meters_per_unit)) + pixel_span = tile_matrix.scaledenominator * (METERS_PER_PIXEL / meters_per_unit) tile_span_x = tile_matrix.tilewidth * pixel_span tile_span_y = tile_matrix.tileheight * pixel_span return tile_span_x, tile_span_y - def _select_tiles(self, tile_matrix, tile_matrix_limits, - tile_span_x, tile_span_y, extent): + def _select_tiles( + self, tile_matrix, tile_matrix_limits, tile_span_x, tile_span_y, extent + ): # Convert the requested extent from CRS coordinates to tile # indices. See annex H of the WMTS v1.0.0 spec. # NB. The epsilons get rid of any tiles which only just @@ -593,8 +628,7 @@ def _select_tiles(self, tile_matrix, tile_matrix_limits, max_row = min(max_row, tile_matrix_limits.maxtilerow) return min_col, max_col, min_row, max_row - def _wmts_images(self, wmts, layer, matrix_set_name, extent, - max_pixel_span): + def _wmts_images(self, wmts, layer, matrix_set_name, extent, max_pixel_span): """ Add images from the specified WMTS layer and matrix set to cover the specified extent at an appropriate resolution. @@ -631,38 +665,39 @@ def _wmts_images(self, wmts, layer, matrix_set_name, extent, # dictionary does not contain all information for all projections; # need only 'units' here with warnings.catch_warnings(): - warnings.simplefilter("ignore") + warnings.simplefilter('ignore') crs_dict = tms_crs.to_dict() crs_units = crs_dict.get('units', '') if crs_units != 'm': - raise ValueError('Only unit "m" implemented for' - ' EPSG projections.') + raise ValueError('Only unit "m" implemented for EPSG projections.') meters_per_unit = 1 else: - raise ValueError(f'Unknown coordinate reference system string:' - f' {tile_matrix_set.crs}') - tile_matrix = self._choose_matrix(tile_matrices, meters_per_unit, - max_pixel_span) + raise ValueError( + f'Unknown coordinate reference system string: {tile_matrix_set.crs}' + ) + tile_matrix = self._choose_matrix( + tile_matrices, meters_per_unit, max_pixel_span + ) # Determine which tiles are required to cover the requested extent. - tile_span_x, tile_span_y = self._tile_span(tile_matrix, - meters_per_unit) + tile_span_x, tile_span_y = self._tile_span(tile_matrix, meters_per_unit) tile_matrix_set_links = getattr(layer, 'tilematrixsetlinks', None) if tile_matrix_set_links is None: tile_matrix_limits = None else: tile_matrix_set_link = tile_matrix_set_links[matrix_set_name] tile_matrix_limits = tile_matrix_set_link.tilematrixlimits.get( - tile_matrix.identifier) + tile_matrix.identifier + ) min_col, max_col, min_row, max_row = self._select_tiles( - tile_matrix, tile_matrix_limits, tile_span_x, tile_span_y, extent) + tile_matrix, tile_matrix_limits, tile_span_x, tile_span_y, extent + ) # Find the relevant section of the image cache. tile_matrix_id = tile_matrix.identifier cache_by_wmts = WMTSRasterSource._shared_image_cache cache_by_layer_matrix = cache_by_wmts.setdefault(wmts, {}) - image_cache = cache_by_layer_matrix.setdefault((layer.id, - tile_matrix_id), {}) + image_cache = cache_by_layer_matrix.setdefault((layer.id, tile_matrix_id), {}) # To avoid nasty seams between the individual tiles, we # accumulate the tile images into a single image. @@ -681,7 +716,7 @@ def _wmts_images(self, wmts, layer, matrix_set_name, extent, if img is None: # Try it from disk cache if self.cache_path is not None: - filename = f"{img_key[0]}_{img_key[1]}.npy" + filename = f'{img_key[0]}_{img_key[1]}.npy' cached_file = self._cache_dir / filename else: filename = None @@ -695,11 +730,15 @@ def _wmts_images(self, wmts, layer, matrix_set_name, extent, layer=layer.id, tilematrixset=matrix_set_name, tilematrix=str(tile_matrix_id), - row=str(row), column=str(col), - **self.gettile_extra_kwargs) + row=str(row), + column=str(col), + **self.gettile_extra_kwargs, + ) except owslib.util.ServiceException as exception: - if ('TileOutOfRange' in exception.message and - ignore_out_of_range): + if ( + 'TileOutOfRange' in exception.message + and ignore_out_of_range + ): continue raise exception img = Image.open(io.BytesIO(tile.read())) @@ -722,8 +761,12 @@ def _wmts_images(self, wmts, layer, matrix_set_name, extent, matrix_min_x, matrix_max_y = tile_matrix.topleftcorner min_img_x = matrix_min_x + tile_span_x * min_col max_img_y = matrix_max_y - tile_span_y * min_row - img_extent = (min_img_x, min_img_x + n_cols * tile_span_x, - max_img_y - n_rows * tile_span_y, max_img_y) + img_extent = ( + min_img_x, + min_img_x + n_cols * tile_span_x, + max_img_y - n_rows * tile_span_y, + max_img_y, + ) return big_img, img_extent @@ -770,7 +813,8 @@ def __init__(self, service, features, getfeature_extra_kwargs=None): for feature in features: if feature not in service.contents: raise ValueError( - f'The {feature!r} feature does not exist in this service.') + f'The {feature!r} feature does not exist in this service.' + ) self.service = service self.features = features @@ -786,20 +830,25 @@ def default_projection(self): """ # Using first element in crsOptions (default). if self._default_urn is None: - default_urn = {self.service.contents[feature].crsOptions[0] for - feature in self.features} + default_urn = { + self.service.contents[feature].crsOptions[0] + for feature in self.features + } if len(default_urn) != 1: - ValueError('Failed to find a single common default SRS ' - 'across all features (typenames).') + ValueError( + 'Failed to find a single common default SRS ' + 'across all features (typenames).' + ) else: default_urn = default_urn.pop() if (str(default_urn) not in _URN_TO_CRS) and ( - ":EPSG:" not in str(default_urn) + ':EPSG:' not in str(default_urn) ): raise ValueError( - f"Unknown mapping from SRS/CRS_URN {default_urn!r} to " - "cartopy projection.") + f'Unknown mapping from SRS/CRS_URN {default_urn!r} to ' + 'cartopy projection.' + ) self._default_urn = default_urn if str(self._default_urn) in _URN_TO_CRS: @@ -808,8 +857,9 @@ def default_projection(self): epsg_num = str(self._default_urn).split(':')[-1] return ccrs.epsg(int(epsg_num)) else: - raise ValueError(f'Unknown coordinate reference system:' - f' {str(self._default_urn)}') + raise ValueError( + f'Unknown coordinate reference system: {str(self._default_urn)}' + ) def fetch_geometries(self, projection, extent): """ @@ -833,20 +883,26 @@ def fetch_geometries(self, projection, extent): """ if self.default_projection() != projection: - raise ValueError('Geometries are only available in projection ' - f'{self.default_projection()!r}.') + raise ValueError( + 'Geometries are only available in projection ' + f'{self.default_projection()!r}.' + ) min_x, max_x, min_y, max_y = extent - response = self.service.getfeature(typename=self.features, - bbox=(min_x, min_y, max_x, max_y), - **self.getfeature_extra_kwargs) + response = self.service.getfeature( + typename=self.features, + bbox=(min_x, min_y, max_x, max_y), + **self.getfeature_extra_kwargs, + ) geoms_by_srs = self._to_shapely_geoms(response) if not geoms_by_srs: geoms = [] elif len(geoms_by_srs) > 1: - raise ValueError('Unexpected response from the WFS server. The ' - 'geometries are in multiple SRSs, when only one ' - 'was expected.') + raise ValueError( + 'Unexpected response from the WFS server. The ' + 'geometries are in multiple SRSs, when only one ' + 'was expected.' + ) else: srs, geoms = list(geoms_by_srs.items())[0] # Attempt to verify the SRS associated with the geometries (if any) @@ -857,7 +913,8 @@ def fetch_geometries(self, projection, extent): if geom_proj != projection: raise ValueError( f'The geometries are not in expected projection. ' - f'Expected {projection!r}, got {geom_proj!r}.') + f'Expected {projection!r}, got {geom_proj!r}.' + ) elif ':EPSG:' in srs: epsg_num = srs.split(':')[-1] geom_proj = ccrs.epsg(int(epsg_num)) @@ -865,12 +922,14 @@ def fetch_geometries(self, projection, extent): raise ValueError( f'The EPSG geometries are not in expected ' f' projection. Expected {projection!r}, ' - f' got {geom_proj!r}.') + f' got {geom_proj!r}.' + ) else: warnings.warn( f'Unable to verify matching projections due to ' f'incomplete mappings from SRS identifiers to cartopy ' - f'projections. The geometries have an SRS of {srs!r}.') + f'projections. The geometries have an SRS of {srs!r}.' + ) return geoms def _to_shapely_geoms(self, response): @@ -899,11 +958,9 @@ def _to_shapely_geoms(self, response): # and other servers for node in tree.iter(): snode = str(node) - if ((_MAP_SERVER_NS in snode) or - (self.url and (self.url in snode) - )): + if (_MAP_SERVER_NS in snode) or (self.url and (self.url in snode)): s1 = snode.split()[1] - tag = s1[s1.find('}') + 1:-1] + tag = s1[s1.find('}') + 1 : -1] if ('geom' in tag) or ('Geom' in tag): # Find LinearRing geometries in our msGeometry node. find_str = f'.//{_GML_NS}LinearRing' @@ -925,14 +982,11 @@ def _to_shapely_geoms(self, response): geoms_by_srs = {} for srs, x, y in linear_rings_data: - geoms_by_srs.setdefault(srs, []).append( - sgeom.LinearRing(zip(x, y))) + geoms_by_srs.setdefault(srs, []).append(sgeom.LinearRing(zip(x, y))) for srs, x, y in linestrings_data: - geoms_by_srs.setdefault(srs, []).append( - sgeom.LineString(zip(x, y))) + geoms_by_srs.setdefault(srs, []).append(sgeom.LineString(zip(x, y))) for srs, x, y in points_data: - geoms_by_srs.setdefault(srs, []).append( - sgeom.Point(zip(x, y))) + geoms_by_srs.setdefault(srs, []).append(sgeom.Point(zip(x, y))) return geoms_by_srs def _find_polygon_coords(self, node, find_str): @@ -977,8 +1031,9 @@ def _find_polygon_coords(self, node, find_str): x.append(float(coord.findtext(f'{_GML_NS}X'))) y.append(float(coord.findtext(f'{_GML_NS}Y'))) else: - raise ValueError('Unable to find or parse coordinate values ' - 'from the XML.') + raise ValueError( + 'Unable to find or parse coordinate values from the XML.' + ) data.append((feature_srs, x, y)) return data diff --git a/lib/cartopy/io/shapereader.py b/lib/cartopy/io/shapereader.py index 9460de06f..3a5c284d0 100644 --- a/lib/cartopy/io/shapereader.py +++ b/lib/cartopy/io/shapereader.py @@ -44,11 +44,12 @@ _HAS_FIONA = False try: import fiona + _HAS_FIONA = True except ImportError: pass -__all__ = ["Reader", "Record"] +__all__ = ['Reader', 'Record'] class Record: @@ -147,8 +148,7 @@ def __init__(self, filename, bbox=None, **kwargs): pass self._bbox = bbox if reader.shp is None or reader.shx is None or reader.dbf is None: - raise ValueError("Incomplete shapefile definition " - "in '%s'." % filename) + raise ValueError("Incomplete shapefile definition in '%s'." % filename) self._fields = self._reader.fields @@ -219,7 +219,7 @@ def __init__(self, filename, bbox=None, **kwargs): features = f # Handle feature collections - if hasattr(features, "__geo_interface__"): + if hasattr(features, '__geo_interface__'): fs = features.__geo_interface__ else: fs = features @@ -230,13 +230,16 @@ def __init__(self, filename, bbox=None, **kwargs): features_lst = features for feature in features_lst: - if hasattr(f, "__geo_interface__"): + if hasattr(f, '__geo_interface__'): feature = feature.__geo_interface__ else: feature = feature - d = {'geometry': sgeom.shape(feature['geometry']) - if feature['geometry'] else None} + d = { + 'geometry': sgeom.shape(feature['geometry']) + if feature['geometry'] + else None + } d.update(feature['properties']) self._data.append(d) @@ -270,9 +273,10 @@ def records(self): """ for item in self._data: - yield FionaRecord(item['geometry'], - {key: value for key, value in - item.items() if key != 'geometry'}) + yield FionaRecord( + item['geometry'], + {key: value for key, value in item.items() if key != 'geometry'}, + ) Reader = FionaReader if _HAS_FIONA else BasicReader @@ -314,10 +318,15 @@ def natural_earth(resolution='110m', category='physical', name='coastline'): # get hold of the Downloader (typically a NEShpDownloader instance) # which we can then simply call its path method to get the appropriate # shapefile (it will download if necessary) - ne_downloader = Downloader.from_config(('shapefiles', 'natural_earth', - resolution, category, name)) - format_dict = {'config': config, 'category': category, - 'name': name, 'resolution': resolution} + ne_downloader = Downloader.from_config( + ('shapefiles', 'natural_earth', resolution, category, name) + ) + format_dict = { + 'config': config, + 'category': category, + 'name': name, + 'resolution': resolution, + } return ne_downloader.path(format_dict) @@ -331,22 +340,26 @@ class NEShpDownloader(Downloader): are typically ``category``, ``resolution`` and ``name``. """ + FORMAT_KEYS = ('config', 'resolution', 'category', 'name') # Define the NaturalEarth URL template. Shapefiles are hosted on AWS since # 2021: https://github.com/nvkelso/natural-earth-vector/issues/445 - _NE_URL_TEMPLATE = ('https://naturalearth.s3.amazonaws.com/' - '{resolution}_{category}/ne_{resolution}_{name}.zip') - - def __init__(self, - url_template=_NE_URL_TEMPLATE, - target_path_template=None, - pre_downloaded_path_template='', - ): + _NE_URL_TEMPLATE = ( + 'https://naturalearth.s3.amazonaws.com/' + '{resolution}_{category}/ne_{resolution}_{name}.zip' + ) + + def __init__( + self, + url_template=_NE_URL_TEMPLATE, + target_path_template=None, + pre_downloaded_path_template='', + ): # adds some NE defaults to the __init__ of a Downloader - Downloader.__init__(self, url_template, - target_path_template, - pre_downloaded_path_template) + Downloader.__init__( + self, url_template, target_path_template, pre_downloaded_path_template + ) def zip_file_contents(self, format_dict): """ @@ -355,8 +368,9 @@ def zip_file_contents(self, format_dict): """ for ext in ['.shp', '.dbf', '.shx', '.prj', '.cpg']: - yield ('ne_{resolution}_{name}' - '{extension}'.format(extension=ext, **format_dict)) + yield ( + 'ne_{resolution}_{name}{extension}'.format(extension=ext, **format_dict) + ) def acquire_resource(self, target_path, format_dict): """ @@ -377,7 +391,7 @@ def acquire_resource(self, target_path, format_dict): for member_path in self.zip_file_contents(format_dict): try: - member = zfh.getinfo(member_path.replace("\\", "/")) + member = zfh.getinfo(member_path.replace('\\', '/')) except KeyError: # These extensions are required for shapefiles, so raise # if they are missing, continue on otherwise as the other @@ -409,21 +423,26 @@ def default_downloader(): ne_{resolution}_{name}.shp """ - default_spec = ('shapefiles', 'natural_earth', '{category}', - 'ne_{resolution}_{name}.shp') - ne_path_template = str( - Path('{config[data_dir]}').joinpath(*default_spec)) + default_spec = ( + 'shapefiles', + 'natural_earth', + '{category}', + 'ne_{resolution}_{name}.shp', + ) + ne_path_template = str(Path('{config[data_dir]}').joinpath(*default_spec)) pre_path_template = str( - Path('{config[pre_existing_data_dir]}').joinpath(*default_spec)) - return NEShpDownloader(target_path_template=ne_path_template, - pre_downloaded_path_template=pre_path_template) + Path('{config[pre_existing_data_dir]}').joinpath(*default_spec) + ) + return NEShpDownloader( + target_path_template=ne_path_template, + pre_downloaded_path_template=pre_path_template, + ) # add a generic Natural Earth shapefile downloader to the config dictionary's # 'downloaders' section. _ne_key = ('shapefiles', 'natural_earth') -config['downloaders'].setdefault(_ne_key, - NEShpDownloader.default_downloader()) +config['downloaders'].setdefault(_ne_key, NEShpDownloader.default_downloader()) def gshhs(scale='c', level=1): @@ -435,8 +454,7 @@ def gshhs(scale='c', level=1): # Get hold of the Downloader (typically a GSHHSShpDownloader instance) # and call its path method to get the appropriate shapefile (it will # download it if necessary). - gshhs_downloader = Downloader.from_config(('shapefiles', 'gshhs', - scale, level)) + gshhs_downloader = Downloader.from_config(('shapefiles', 'gshhs', scale, level)) format_dict = {'config': config, 'scale': scale, 'level': level} return gshhs_downloader.path(format_dict) @@ -451,6 +469,7 @@ class GSHHSShpDownloader(Downloader): (a number indicating the type of feature). """ + FORMAT_KEYS = ('config', 'scale', 'level') gshhs_version = '2.3.7' @@ -460,12 +479,15 @@ class GSHHSShpDownloader(Downloader): f'gshhs/latest/gshhg-shp-{gshhs_version}.zip' ) - def __init__(self, - url_template=_GSHHS_URL_TEMPLATE, - target_path_template=None, - pre_downloaded_path_template=''): - super().__init__(url_template, target_path_template, - pre_downloaded_path_template) + def __init__( + self, + url_template=_GSHHS_URL_TEMPLATE, + target_path_template=None, + pre_downloaded_path_template='', + ): + super().__init__( + url_template, target_path_template, pre_downloaded_path_template + ) def zip_file_contents(self, format_dict): """ @@ -474,8 +496,7 @@ def zip_file_contents(self, format_dict): """ for ext in ['.shp', '.dbf', '.shx']: - p = Path('GSHHS_shp', '{scale}', - 'GSHHS_{scale}_L{level}{extension}') + p = Path('GSHHS_shp', '{scale}', 'GSHHS_{scale}_L{level}{extension}') yield str(p).format(extension=ext, **format_dict) def acquire_all_resources(self, format_dict): @@ -518,7 +539,7 @@ def acquire_all_resources(self, format_dict): levels = (1, 2, 3, 4, 5, 6) for scale, level in itertools.product(scales, levels): # the combination c4 does not occur for some reason - if scale == "c" and level == 4: + if scale == 'c' and level == 4: continue modified_format_dict.update({'scale': scale, 'level': level}) target_path = self.target_path(modified_format_dict) @@ -544,8 +565,9 @@ def acquire_resource(self, target_path, format_dict): exist in the ``cartopy.config['repo_data_dir']`` directory. """ - repo_fname_pattern = str(Path('shapefiles') / 'gshhs' - / '{scale}' / 'GSHHS_{scale}_L?.shp') + repo_fname_pattern = str( + Path('shapefiles') / 'gshhs' / '{scale}' / 'GSHHS_{scale}_L?.shp' + ) repo_fname_pattern = repo_fname_pattern.format(**format_dict) repo_fnames = list(config['repo_data_dir'].glob(repo_fname_pattern)) if repo_fnames: @@ -553,8 +575,9 @@ def acquire_resource(self, target_path, format_dict): return repo_fnames[0] self.acquire_all_resources(format_dict) if not target_path.exists(): - raise RuntimeError('Failed to download and extract GSHHS ' - f'shapefile to {target_path!r}.') + raise RuntimeError( + f'Failed to download and extract GSHHS shapefile to {target_path!r}.' + ) return target_path @staticmethod @@ -574,18 +597,18 @@ def default_downloader(): GSHHS_{scale}_L{level}.shp """ - default_spec = ('shapefiles', 'gshhs', '{scale}', - 'GSHHS_{scale}_L{level}.shp') - gshhs_path_template = str( - Path('{config[data_dir]}').joinpath(*default_spec)) + default_spec = ('shapefiles', 'gshhs', '{scale}', 'GSHHS_{scale}_L{level}.shp') + gshhs_path_template = str(Path('{config[data_dir]}').joinpath(*default_spec)) pre_path_tmplt = str( - Path('{config[pre_existing_data_dir]}').joinpath(*default_spec)) - return GSHHSShpDownloader(target_path_template=gshhs_path_template, - pre_downloaded_path_template=pre_path_tmplt) + Path('{config[pre_existing_data_dir]}').joinpath(*default_spec) + ) + return GSHHSShpDownloader( + target_path_template=gshhs_path_template, + pre_downloaded_path_template=pre_path_tmplt, + ) # Add a GSHHS shapefile downloader to the config dictionary's # 'downloaders' section. _gshhs_key = ('shapefiles', 'gshhs') -config['downloaders'].setdefault(_gshhs_key, - GSHHSShpDownloader.default_downloader()) +config['downloaders'].setdefault(_gshhs_key, GSHHSShpDownloader.default_downloader()) diff --git a/lib/cartopy/io/srtm.py b/lib/cartopy/io/srtm.py index 1c88d1de5..4424b9d8d 100644 --- a/lib/cartopy/io/srtm.py +++ b/lib/cartopy/io/srtm.py @@ -59,8 +59,7 @@ def __init__(self, resolution, downloader, max_nx, max_ny): elif resolution == 1: self._shape = (3601, 3601) else: - raise ValueError( - f'Resolution is an unexpected value ({resolution}).') + raise ValueError(f'Resolution is an unexpected value ({resolution}).') self._resolution = resolution #: The CRS of the underlying SRTM data. @@ -71,8 +70,7 @@ def __init__(self, resolution, downloader, max_nx, max_ny): self.downloader = downloader if self.downloader is None: - self.downloader = Downloader.from_config( - ('SRTM', f'SRTM{resolution}')) + self.downloader = Downloader.from_config(('SRTM', f'SRTM{resolution}')) #: A tuple of (max_x_tiles, max_y_tiles). self._max_tiles = (max_nx, max_ny) @@ -86,8 +84,9 @@ def fetch_raster(self, projection, extent, target_resolution): """ if not self.validate_projection(projection): - raise ValueError(f'Unsupported projection for the ' - f'SRTM{self._resolution} source.') + raise ValueError( + f'Unsupported projection for the SRTM{self._resolution} source.' + ) min_x, max_x, min_y, max_y = extent min_x, min_y = np.floor([min_x, min_y]) @@ -97,12 +96,14 @@ def fetch_raster(self, projection, extent, target_resolution): if nx > self._max_tiles[0]: warnings.warn( f'Required SRTM{self._resolution} tile count ({nx}) exceeds ' - f'maximum ({self._max_tiles[0]}). Increase max_nx limit.') + f'maximum ({self._max_tiles[0]}). Increase max_nx limit.' + ) skip = True if ny > self._max_tiles[1]: warnings.warn( f'Required SRTM{self._resolution} tile count ({ny}) exceeds ' - f'maximum ({self._max_tiles[1]}). Increase max_ny limit.') + f'maximum ({self._max_tiles[1]}). Increase max_ny limit.' + ) skip = True if skip: return [] @@ -123,10 +124,8 @@ def srtm_fname(self, lon, lat): x = '%s%03d' % ('E' if lon >= 0 else 'W', abs(int(lon))) y = '%s%02d' % ('N' if lat >= 0 else 'S', abs(int(lat))) - srtm_downloader = Downloader.from_config( - ('SRTM', f'SRTM{self._resolution}')) - params = {'config': config, 'resolution': self._resolution, - 'x': x, 'y': y} + srtm_downloader = Downloader.from_config(('SRTM', f'SRTM{self._resolution}')) + params = {'config': config, 'resolution': self._resolution, 'x': x, 'y': y} # If the URL doesn't exist then we are over sea/north/south of the # limits of the SRTM data and we return None. @@ -150,15 +149,20 @@ def combined(self, lon_min, lat_min, nx, ny): y_img_slice = slice(j * shape[0], (j + 1) * shape[0]) try: - tile_img, _, _ = self.single_tile(bottom_left_ll[0] + i, - bottom_left_ll[1] + j) + tile_img, _, _ = self.single_tile( + bottom_left_ll[0] + i, bottom_left_ll[1] + j + ) except ValueError: img[y_img_slice, x_img_slice] = 0 else: img[y_img_slice, x_img_slice] = tile_img - extent = (bottom_left_ll[0], bottom_left_ll[0] + nx, - bottom_left_ll[1], bottom_left_ll[1] + ny) + extent = ( + bottom_left_ll[0], + bottom_left_ll[0] + nx, + bottom_left_ll[1], + bottom_left_ll[1] + ny, + ) return img, self.crs, extent @@ -193,8 +197,9 @@ def __init__(self, downloader=None, max_nx=3, max_ny=3): producing a taller composite for this RasterSource. """ - super().__init__(resolution=3, downloader=downloader, - max_nx=max_nx, max_ny=max_ny) + super().__init__( + resolution=3, downloader=downloader, max_nx=max_nx, max_ny=max_ny + ) class SRTM1Source(_SRTMSource): @@ -221,8 +226,9 @@ def __init__(self, downloader=None, max_nx=3, max_ny=3): producing a taller composite for this RasterSource. """ - super().__init__(resolution=1, downloader=downloader, - max_nx=max_nx, max_ny=max_ny) + super().__init__( + resolution=1, downloader=downloader, max_nx=max_nx, max_ny=max_ny + ) def add_shading(elevation, azimuth, altitude): @@ -246,9 +252,9 @@ def add_shading(elevation, azimuth, altitude): slope = np.pi / 2 - np.arctan(np.hypot(x, y)) # -x here because of pixel orders in the SRTM tile aspect = np.arctan2(-x, y) - shaded = np.sin(altitude) * np.sin(slope) \ - + np.cos(altitude) * np.cos(slope) \ - * np.cos((azimuth - np.pi / 2) - aspect) + shaded = np.sin(altitude) * np.sin(slope) + np.cos(altitude) * np.cos( + slope + ) * np.cos((azimuth - np.pi / 2) - aspect) return shaded @@ -279,6 +285,7 @@ def read_SRTM(fh): fh, fname = fh_getter(fh, needs_filename=True) if fname.endswith('.zip'): from zipfile import ZipFile + zfh = ZipFile(fh, 'rb') fh = zfh.open(Path(fname).stem, 'r') @@ -288,8 +295,7 @@ def read_SRTM(fh): elif elev.size == 1442401: elev.shape = (1201, 1201) else: - raise ValueError( - f'Shape of SRTM data ({elev.size}) is unexpected.') + raise ValueError(f'Shape of SRTM data ({elev.size}) is unexpected.') fname = Path(fname).name y_dir, y, x_dir, x = fname[0], int(fname[1:3]), fname[3], int(fname[4:7]) @@ -312,10 +318,12 @@ class SRTMDownloader(Downloader): Provide a SRTM download mechanism. """ + FORMAT_KEYS = ('config', 'resolution', 'x', 'y') - _SRTM_BASE_URL = ('https://e4ftl01.cr.usgs.gov/MEASURES/' - 'SRTMGL{resolution}.003/2000.02.11/') + _SRTM_BASE_URL = ( + 'https://e4ftl01.cr.usgs.gov/MEASURES/SRTMGL{resolution}.003/2000.02.11/' + ) _SRTM_LOOKUP_CACHE = Path(__file__).parent / 'srtm.npz' _SRTM_LOOKUP_MASK = np.load(_SRTM_LOOKUP_CACHE)['mask'] """ @@ -324,21 +332,24 @@ class SRTMDownloader(Downloader): """ - def __init__(self, - target_path_template, - pre_downloaded_path_template='', - ): + def __init__( + self, + target_path_template, + pre_downloaded_path_template='', + ): # adds some SRTM defaults to the __init__ of a Downloader # namely, the URL is determined on the fly using the # ``SRTMDownloader._SRTM_LOOKUP_MASK`` array - Downloader.__init__(self, None, - target_path_template, - pre_downloaded_path_template) + Downloader.__init__( + self, None, target_path_template, pre_downloaded_path_template + ) def url(self, format_dict): - warnings.warn('SRTM requires an account set up and log in to access. ' - 'Use of this Downloader is likely to fail with HTTP 401 ' - 'errors.') + warnings.warn( + 'SRTM requires an account set up and log in to access. ' + 'Use of this Downloader is likely to fail with HTTP 401 ' + 'errors.' + ) # override the url method, looking up the url from the # ``SRTMDownloader._SRTM_LOOKUP_MASK`` array @@ -355,8 +366,9 @@ def url(self, format_dict): lon = 360 - lon if SRTMDownloader._SRTM_LOOKUP_MASK[lon, colat]: - return (SRTMDownloader._SRTM_BASE_URL + - '{y}{x}.SRTMGL{resolution}.hgt.zip').format(**format_dict) + return ( + SRTMDownloader._SRTM_BASE_URL + '{y}{x}.SRTMGL{resolution}.hgt.zip' + ).format(**format_dict) else: return None @@ -400,8 +412,10 @@ def _create_srtm_mask(resolution, filename=None): # lazy imports. In most situations, these are not # dependencies of cartopy. from bs4 import BeautifulSoup + if filename is None: from urllib.request import urlopen + url = SRTMDownloader._SRTM_BASE_URL.format(resolution=resolution) with urlopen(url) as f: html = f.read() @@ -439,16 +453,16 @@ def default_downloader(cls): """ default_spec = ('SRTM', 'SRTMGL{resolution}', '{y}{x}.hgt') - target_path_template = str( - Path('{config[data_dir]}').joinpath(*default_spec)) + target_path_template = str(Path('{config[data_dir]}').joinpath(*default_spec)) pre_path_template = str( - Path('{config[pre_existing_data_dir]}').joinpath(*default_spec)) - return cls(target_path_template=target_path_template, - pre_downloaded_path_template=pre_path_template) + Path('{config[pre_existing_data_dir]}').joinpath(*default_spec) + ) + return cls( + target_path_template=target_path_template, + pre_downloaded_path_template=pre_path_template, + ) # add a generic SRTM downloader to the config 'downloaders' section. -config['downloaders'].setdefault(('SRTM', 'SRTM3'), - SRTMDownloader.default_downloader()) -config['downloaders'].setdefault(('SRTM', 'SRTM1'), - SRTMDownloader.default_downloader()) +config['downloaders'].setdefault(('SRTM', 'SRTM3'), SRTMDownloader.default_downloader()) +config['downloaders'].setdefault(('SRTM', 'SRTM1'), SRTMDownloader.default_downloader()) diff --git a/lib/cartopy/mpl/clip_path.py b/lib/cartopy/mpl/clip_path.py index ab8904f19..7226ec730 100644 --- a/lib/cartopy/mpl/clip_path.py +++ b/lib/cartopy/mpl/clip_path.py @@ -8,9 +8,12 @@ import numpy as np -warnings.warn('The clip_path module is deprecated and will be removed ' - 'in a future release with no replacement.', - DeprecationWarning, stacklevel=2) +warnings.warn( + 'The clip_path module is deprecated and will be removed ' + 'in a future release with no replacement.', + DeprecationWarning, + stacklevel=2, +) def intersection_point(p0, p1, p2, p3): @@ -30,13 +33,14 @@ def intersection_point(p0, p1, p2, p3): div = (x_1 - x_2) * (y_3 - y_4) - (y_1 - y_2) * (x_3 - x_4) if div == 0: - raise ValueError('Lines are parallel and cannot ' - 'intersect at any one point.') + raise ValueError('Lines are parallel and cannot intersect at any one point.') - x = ((x_1 * y_2 - y_1 * x_2) * (x_3 - x_4) - (x_1 - x_2) * (x_3 * - y_4 - y_3 * x_4)) / div - y = ((x_1 * y_2 - y_1 * x_2) * (y_3 - y_4) - (y_1 - y_2) * (x_3 * - y_4 - y_3 * x_4)) / div + x = ( + (x_1 * y_2 - y_1 * x_2) * (x_3 - x_4) - (x_1 - x_2) * (x_3 * y_4 - y_3 * x_4) + ) / div + y = ( + (x_1 * y_2 - y_1 * x_2) * (y_3 - y_4) - (y_1 - y_2) * (x_3 * y_4 - y_3 * x_4) + ) / div return x, y @@ -66,7 +70,13 @@ def bbox_to_path(bbox): a :class:`matplotlib.path.Path` instance. """ - verts = np.array([[bbox.x0, bbox.y0], [bbox.x1, bbox.y0], - [bbox.x1, bbox.y1], [bbox.x0, bbox.y1], - [bbox.x0, bbox.y0]]) + verts = np.array( + [ + [bbox.x0, bbox.y0], + [bbox.x1, bbox.y0], + [bbox.x1, bbox.y1], + [bbox.x0, bbox.y1], + [bbox.x0, bbox.y0], + ] + ) return mpath.Path(verts) diff --git a/lib/cartopy/mpl/contour.py b/lib/cartopy/mpl/contour.py index a53c2e6e9..808232e4a 100644 --- a/lib/cartopy/mpl/contour.py +++ b/lib/cartopy/mpl/contour.py @@ -15,6 +15,7 @@ class GeoContourSet(QuadContourSet): A contourset designed to handle things like contour labels. """ + # nb. No __init__ method here - most of the time a GeoContourSet will # come from GeoAxes.contour[f]. These methods morph a ContourSet by # fiddling with instance.__class__. @@ -48,10 +49,8 @@ def clabel(self, *args, **kwargs): # Now that we have the transform, project all of this # collection's paths. - new_paths = [col_to_data.transform_path(path) - for path in paths] - new_paths = [path for path in new_paths - if path.vertices.size >= 1] + new_paths = [col_to_data.transform_path(path) for path in paths] + new_paths = [path for path in new_paths if path.vertices.size >= 1] # The collection will now be referenced in axes projection # coordinates. @@ -66,8 +65,7 @@ def clabel(self, *args, **kwargs): continue # Split the path if it has multiple MOVETO statements. - codes = np.array( - path.codes if path.codes is not None else [0]) + codes = np.array(path.codes if path.codes is not None else [0]) moveto = codes == mpath.Path.MOVETO if moveto.sum() <= 1: # This is only one path, so add it to the collection. diff --git a/lib/cartopy/mpl/feature_artist.py b/lib/cartopy/mpl/feature_artist.py index 04aacd27c..d4021fda3 100644 --- a/lib/cartopy/mpl/feature_artist.py +++ b/lib/cartopy/mpl/feature_artist.py @@ -138,10 +138,12 @@ def set_facecolor(self, c): self._never_fc = True super().set_facecolor('none') - elif (getattr(self, '_never_fc', False) and - (not isinstance(c, str) or c != 'none')): - warnings.warn('facecolor will have no effect as it has been ' - 'defined as "never".') + elif getattr(self, '_never_fc', False) and ( + not isinstance(c, str) or c != 'none' + ): + warnings.warn( + 'facecolor will have no effect as it has been defined as "never".' + ) else: super().set_facecolor(c) @@ -204,15 +206,12 @@ def draw(self, renderer): # cache of transformed geometries. So when the geom-key is # garbage collected so are the transformed geometries. geom_key = _GeomKey(geom) - FeatureArtist._geom_key_to_geometry_cache.setdefault( - geom_key, geom) - mapping = FeatureArtist._geom_key_to_path_cache.setdefault( - geom_key, {}) + FeatureArtist._geom_key_to_geometry_cache.setdefault(geom_key, geom) + mapping = FeatureArtist._geom_key_to_path_cache.setdefault(geom_key, {}) geom_path = mapping.get(key) if geom_path is None: if ax.projection != feature_crs: - projected_geom = ax.projection.project_geometry( - geom, feature_crs) + projected_geom = ax.projection.project_geometry(geom, feature_crs) else: projected_geom = geom @@ -233,7 +232,7 @@ def draw(self, renderer): style = dict(style) # Temporarily replace properties. - orig_style = {k: getattr(self, f"get_{k}")() for k in style} + orig_style = {k: getattr(self, f'get_{k}')() for k in style} self.set(paths=paths, **style) super().draw(renderer) diff --git a/lib/cartopy/mpl/geoaxes.py b/lib/cartopy/mpl/geoaxes.py index b58e594cf..5eeda880e 100644 --- a/lib/cartopy/mpl/geoaxes.py +++ b/lib/cartopy/mpl/geoaxes.py @@ -61,6 +61,7 @@ # CARTOPY_USER_BACKGROUNDS environment variable. _USER_BG_IMGS = {} + # XXX call this InterCRSTransform class InterProjectionTransform(mtransforms.Transform): """ @@ -68,6 +69,7 @@ class InterProjectionTransform(mtransforms.Transform): the ``target_projection``. """ + input_dims = 2 output_dims = 2 is_separable = False @@ -92,15 +94,19 @@ def __init__(self, source_projection, target_projection): mtransforms.Transform.__init__(self) def __repr__(self): - return (f'< {self.__class__.__name__!s} {self.source_projection!s} ' - f'-> {self.target_projection!s} >') + return ( + f'< {self.__class__.__name__!s} {self.source_projection!s} ' + f'-> {self.target_projection!s} >' + ) def __eq__(self, other): if not isinstance(other, self.__class__): result = NotImplemented else: - result = (self.source_projection == other.source_projection and - self.target_projection == other.target_projection) + result = ( + self.source_projection == other.source_projection + and self.target_projection == other.target_projection + ) return result def __ne__(self, other): @@ -123,8 +129,9 @@ def transform_non_affine(self, xy): """ prj = self.target_projection if isinstance(xy, np.ndarray): - return prj.transform_points(self.source_projection, - xy[:, 0], xy[:, 1])[:, 0:2] + return prj.transform_points(self.source_projection, xy[:, 0], xy[:, 1])[ + :, 0:2 + ] else: x, y = xy x, y = prj.transform_point(x, y, self.source_projection) @@ -160,7 +167,8 @@ def transform_path_non_affine(self, src_path): # Allow the vertices to be quickly transformed, if # quick_vertices_transform allows it. new_vertices = self.target_projection.quick_vertices_transform( - src_path.vertices, self.source_projection) + src_path.vertices, self.source_projection + ) if new_vertices is not None: if new_vertices is src_path.vertices: return src_path @@ -172,7 +180,8 @@ def transform_path_non_affine(self, src_path): geom = cpath.path_to_shapely(src_path) transformed_geom = self.target_projection.project_geometry( - geom, self.source_projection) + geom, self.source_projection + ) result = cpath.shapely_to_path(transformed_geom) @@ -194,8 +203,7 @@ def inverted(self): from target to source coordinates. """ - return InterProjectionTransform(self.target_projection, - self.source_projection) + return InterProjectionTransform(self.target_projection, self.source_projection) class _ViewClippedPathPatch(mpatches.PathPatch): @@ -221,7 +229,9 @@ def _adjust_location(self): if self.stale: self.set_path( cpath._ensure_path_closed( - self._original_path.clip_to_bbox(self.axes.viewLim))) + self._original_path.clip_to_bbox(self.axes.viewLim) + ) + ) # Some places in matplotlib's transform stack cache the actual # path so we trigger an update by invalidating the transform. self._trans_wrap.invalidate() @@ -248,7 +258,7 @@ def _adjust_location(self): if self.stale: self._path = cpath._ensure_path_closed( self._original_path.clip_to_bbox(self.axes.viewLim) - ) + ) def get_window_extent(self, renderer=None): # make sure the location is updated so that transforms etc are @@ -265,12 +275,12 @@ def draw(self, renderer): def set_position(self, position): """GeoSpine does not support changing its position.""" - raise NotImplementedError( - 'GeoSpine does not support changing its position.') + raise NotImplementedError('GeoSpine does not support changing its position.') def _add_transform(func): """A decorator that adds and validates the transform keyword argument.""" + @functools.wraps(func) def wrapper(self, *args, **kwargs): transform = kwargs.get('transform', None) @@ -278,17 +288,29 @@ def wrapper(self, *args, **kwargs): transform = self.projection # Raise an error if any of these functions try to use # a spherical source CRS. - non_spherical_funcs = ['contour', 'contourf', 'pcolormesh', 'pcolor', - 'quiver', 'barbs', 'streamplot'] - if (func.__name__ in non_spherical_funcs and - isinstance(transform, ccrs.CRS) and - not isinstance(transform, ccrs.Projection)): - raise ValueError(f'Invalid transform: Spherical {func.__name__} ' - 'is not supported - consider using ' - 'PlateCarree/RotatedPole.') + non_spherical_funcs = [ + 'contour', + 'contourf', + 'pcolormesh', + 'pcolor', + 'quiver', + 'barbs', + 'streamplot', + ] + if ( + func.__name__ in non_spherical_funcs + and isinstance(transform, ccrs.CRS) + and not isinstance(transform, ccrs.Projection) + ): + raise ValueError( + f'Invalid transform: Spherical {func.__name__} ' + 'is not supported - consider using ' + 'PlateCarree/RotatedPole.' + ) kwargs['transform'] = transform return func(self, *args, **kwargs) + return wrapper @@ -305,17 +327,21 @@ def _add_transform_first(func): This should be added after the _add_transform wrapper so that a transform is guaranteed to be present. """ + @functools.wraps(func) def wrapper(self, *args, **kwargs): if kwargs.pop('transform_first', False): if len(args) < 3: # For the fast-path we need X and Y input points - raise ValueError("The X and Y arguments must be provided to " - "use the transform_first=True fast-path.") + raise ValueError( + 'The X and Y arguments must be provided to ' + 'use the transform_first=True fast-path.' + ) x, y, z = (np.array(i) for i in args[:3]) if not (x.ndim == y.ndim == 2): - raise ValueError("The X and Y arguments must be gridded " - "2-dimensional arrays") + raise ValueError( + 'The X and Y arguments must be gridded 2-dimensional arrays' + ) # Remove the transform from the keyword arguments t = kwargs.pop('transform') @@ -333,6 +359,7 @@ def wrapper(self, *args, **kwargs): # Use the new points as the input arguments args = (x, y, z) + args[3:] return func(self, *args, **kwargs) + return wrapper @@ -363,6 +390,7 @@ class GeoAxes(matplotlib.axes.Axes): plt.contourf(x, y, data, transform=cartopy.crs.PlateCarree()) """ + name = 'cartopy.geoaxes' def __init__(self, *args, **kwargs): @@ -375,18 +403,22 @@ def __init__(self, *args, **kwargs): projection : cartopy.crs.Projection The target projection of this Axes. """ - if "map_projection" in kwargs: - warnings.warn("The `map_projection` keyword argument is " - "deprecated, use `projection` to instantiate a " - "GeoAxes instead.") - projection = kwargs.pop("map_projection") + if 'map_projection' in kwargs: + warnings.warn( + 'The `map_projection` keyword argument is ' + 'deprecated, use `projection` to instantiate a ' + 'GeoAxes instead.' + ) + projection = kwargs.pop('map_projection') else: - projection = kwargs.pop("projection") + projection = kwargs.pop('projection') # The :class:`cartopy.crs.Projection` of this GeoAxes. if not isinstance(projection, ccrs.Projection): - raise ValueError("A GeoAxes can only be created with a " - "projection of type cartopy.crs.Projection") + raise ValueError( + 'A GeoAxes can only be created with a ' + 'projection of type cartopy.crs.Projection' + ) self.projection = projection super().__init__(*args, **kwargs) @@ -439,16 +471,20 @@ def hold_limits(self, hold=True): """ with contextlib.ExitStack() as stack: if hold: - stack.callback(self.dataLim.set_points, - self.dataLim.frozen().get_points()) - stack.callback(self.viewLim.set_points, - self.viewLim.frozen().get_points()) - stack.callback(setattr, self, 'ignore_existing_data_limits', - self.ignore_existing_data_limits) - stack.callback(self.set_autoscalex_on, - self.get_autoscalex_on()) - stack.callback(self.set_autoscaley_on, - self.get_autoscaley_on()) + stack.callback( + self.dataLim.set_points, self.dataLim.frozen().get_points() + ) + stack.callback( + self.viewLim.set_points, self.viewLim.frozen().get_points() + ) + stack.callback( + setattr, + self, + 'ignore_existing_data_limits', + self.ignore_existing_data_limits, + ) + stack.callback(self.set_autoscalex_on, self.get_autoscalex_on()) + stack.callback(self.set_autoscaley_on, self.get_autoscaley_on()) yield def _draw_preprocess(self): @@ -500,10 +536,16 @@ def draw(self, renderer=None, **kwargs): if not self._done_img_factory: for factory, factory_args, factory_kwargs in self.img_factories: img, extent, origin = factory.image_for_domain( - self._get_extent_geom(factory.crs), factory_args[0]) - self.imshow(img, extent=extent, origin=origin, - transform=factory.crs, *factory_args[1:], - **factory_kwargs) + self._get_extent_geom(factory.crs), factory_args[0] + ) + self.imshow( + img, + extent=extent, + origin=origin, + transform=factory.crs, + *factory_args[1:], + **factory_kwargs, + ) self._done_img_factory = True return super().draw(renderer=renderer, **kwargs) @@ -515,6 +557,7 @@ def _update_title_position(self, renderer): return from cartopy.mpl.gridliner import Gridliner + gridliners = [a for a in self.artists if isinstance(a, Gridliner)] if not gridliners: return @@ -526,14 +569,13 @@ def _update_title_position(self, renderer): if gl.top_labels or gl.geo_labels: # Make sure Gridliner is populated and up-to-date gl._draw_gridliner(renderer=renderer) - for label in (gl.top_label_artists + - gl.geo_label_artists): + for label in gl.top_label_artists + gl.geo_label_artists: bb = label.get_tightbbox(renderer) top = max(top, bb.ymax) if top < 0: # nothing to do if no label found return - yn = self.transAxes.inverted().transform((0., top))[1] + yn = self.transAxes.inverted().transform((0.0, top))[1] if yn <= 1: # nothing to do if the upper bounds of labels is below # the top of the axes @@ -580,16 +622,15 @@ def format_coord(self, x, y): """ lon, lat = self.projection.as_geodetic().transform_point( - x, y, self.projection, + x, + y, + self.projection, ) ns = 'N' if lat >= 0.0 else 'S' ew = 'E' if lon >= 0.0 else 'W' - return ( - f'{x:.4g}, {y:.4g} ' - f'({abs(lat):f}°{ns}, {abs(lon):f}°{ew})' - ) + return f'{x:.4g}, {y:.4g} ({abs(lat):f}°{ns}, {abs(lon):f}°{ew})' def coastlines(self, resolution='auto', color='black', **kwargs): """ @@ -667,8 +708,7 @@ def tissot(self, rad_km=500, lons=None, lats=None, n_samples=80, **kwargs): circle = geod.circle(lon, lat, rad_km * 1e3, n_samples=n_samples) geoms.append(sgeom.Polygon(circle)) - feature = cartopy.feature.ShapelyFeature(geoms, ccrs.Geodetic(), - **kwargs) + feature = cartopy.feature.ShapelyFeature(geoms, ccrs.Geodetic(), **kwargs) return self.add_feature(feature) def add_feature(self, feature, **kwargs): @@ -747,9 +787,9 @@ def _get_extent_geom(self, crs=None): self.autoscale_view() [x1, y1], [x2, y2] = self.viewLim.get_points() - domain_in_src_proj = sgeom.Polygon([[x1, y1], [x2, y1], - [x2, y2], [x1, y2], - [x1, y1]]) + domain_in_src_proj = sgeom.Polygon( + [[x1, y1], [x2, y1], [x2, y2], [x1, y2], [x1, y1]] + ) # Determine target projection based on requested CRS. if crs is None: @@ -760,17 +800,23 @@ def _get_extent_geom(self, crs=None): # Attempt to select suitable projection for # non-projection CRS. if isinstance(crs, ccrs.RotatedGeodetic): - proj = ccrs.RotatedPole(crs.proj4_params['lon_0'] - 180, - crs.proj4_params['o_lat_p']) - warnings.warn(f'Approximating coordinate system {crs!r} with ' - 'a RotatedPole projection.') + proj = ccrs.RotatedPole( + crs.proj4_params['lon_0'] - 180, crs.proj4_params['o_lat_p'] + ) + warnings.warn( + f'Approximating coordinate system {crs!r} with ' + 'a RotatedPole projection.' + ) elif hasattr(crs, 'is_geodetic') and crs.is_geodetic(): proj = ccrs.PlateCarree(globe=crs.globe) - warnings.warn(f'Approximating coordinate system {crs!r} with ' - 'the PlateCarree projection.') + warnings.warn( + f'Approximating coordinate system {crs!r} with ' + 'the PlateCarree projection.' + ) else: - raise ValueError('Cannot determine extent in' - f' coordinate system {crs!r}') + raise ValueError( + f'Cannot determine extent in coordinate system {crs!r}' + ) # Calculate intersection with boundary and project if necessary. boundary_poly = sgeom.Polygon(self.projection.boundary) @@ -778,10 +824,8 @@ def _get_extent_geom(self, crs=None): # Erode boundary by threshold to avoid transform issues. # This is a workaround for numerical issues at the boundary. eroded_boundary = boundary_poly.buffer(-self.projection.threshold) - geom_in_src_proj = eroded_boundary.intersection( - domain_in_src_proj) - geom_in_crs = proj.project_geometry(geom_in_src_proj, - self.projection) + geom_in_src_proj = eroded_boundary.intersection(domain_in_src_proj) + geom_in_crs = proj.project_geometry(geom_in_src_proj, self.projection) else: geom_in_crs = boundary_poly.intersection(domain_in_src_proj) @@ -804,9 +848,9 @@ def set_extent(self, extents, crs=None): # plt.ylim - allowing users to set None for a minimum and/or # maximum value x1, x2, y1, y2 = extents - domain_in_crs = sgeom.polygon.LineString([[x1, y1], [x2, y1], - [x2, y2], [x1, y2], - [x1, y1]]) + domain_in_crs = sgeom.polygon.LineString( + [[x1, y1], [x2, y1], [x2, y2], [x1, y2], [x1, y1]] + ) projected = None @@ -816,9 +860,9 @@ def set_extent(self, extents, crs=None): # tuple, which causes an unpack error. # This workaround avoids using the projection when the requested # extents are obviously the same as the projection domain. - try_workaround = ((crs is None and - isinstance(self.projection, ccrs.PlateCarree)) or - crs == self.projection) + try_workaround = ( + crs is None and isinstance(self.projection, ccrs.PlateCarree) + ) or crs == self.projection if try_workaround: boundary = self.projection.boundary if boundary.equals(domain_in_crs): @@ -836,7 +880,8 @@ def set_extent(self, extents, crs=None): 'Failed to determine the required bounds in projection ' 'coordinates. Check that the values provided are within the ' f'valid range (x_limits={self.projection.x_limits}, ' - f'y_limits={self.projection.y_limits}).') + f'y_limits={self.projection.y_limits}).' + ) self.set_xlim([x1, x2]) self.set_ylim([y1, y2]) @@ -868,12 +913,16 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True): # Limit the resulting bounds to valid area. if scalex and self.get_autoscalex_on(): bounds = self.get_xbound() - self.set_xbound(max(bounds[0], self.projection.x_limits[0]), - min(bounds[1], self.projection.x_limits[1])) + self.set_xbound( + max(bounds[0], self.projection.x_limits[0]), + min(bounds[1], self.projection.x_limits[1]), + ) if scaley and self.get_autoscaley_on(): bounds = self.get_ybound() - self.set_ybound(max(bounds[0], self.projection.y_limits[0]), - min(bounds[1], self.projection.y_limits[1])) + self.set_ybound( + max(bounds[0], self.projection.y_limits[0]), + min(bounds[1], self.projection.y_limits[1]), + ) def set_xticks(self, ticks, minor=False, crs=None): """ @@ -903,16 +952,15 @@ def set_xticks(self, ticks, minor=False, crs=None): """ # Project ticks if crs differs from axes' projection if crs is not None and crs != self.projection: - if not isinstance(crs, (ccrs._RectangularProjection, - ccrs.Mercator)) or \ - not isinstance(self.projection, - (ccrs._RectangularProjection, - ccrs.Mercator)): - raise RuntimeError('Cannot handle non-rectangular coordinate ' - 'systems.') - proj_xyz = self.projection.transform_points(crs, - np.asarray(ticks), - np.zeros(len(ticks))) + if not isinstance( + crs, (ccrs._RectangularProjection, ccrs.Mercator) + ) or not isinstance( + self.projection, (ccrs._RectangularProjection, ccrs.Mercator) + ): + raise RuntimeError('Cannot handle non-rectangular coordinate systems.') + proj_xyz = self.projection.transform_points( + crs, np.asarray(ticks), np.zeros(len(ticks)) + ) xticks = proj_xyz[..., 0] else: xticks = ticks @@ -950,16 +998,15 @@ def set_yticks(self, ticks, minor=False, crs=None): """ # Project ticks if crs differs from axes' projection if crs is not None and crs != self.projection: - if not isinstance(crs, (ccrs._RectangularProjection, - ccrs.Mercator)) or \ - not isinstance(self.projection, - (ccrs._RectangularProjection, - ccrs.Mercator)): - raise RuntimeError('Cannot handle non-rectangular coordinate ' - 'systems.') - proj_xyz = self.projection.transform_points(crs, - np.zeros(len(ticks)), - np.asarray(ticks)) + if not isinstance( + crs, (ccrs._RectangularProjection, ccrs.Mercator) + ) or not isinstance( + self.projection, (ccrs._RectangularProjection, ccrs.Mercator) + ): + raise RuntimeError('Cannot handle non-rectangular coordinate systems.') + proj_xyz = self.projection.transform_points( + crs, np.zeros(len(ticks)), np.asarray(ticks) + ) yticks = proj_xyz[..., 1] else: yticks = ticks @@ -980,18 +1027,26 @@ def stock_img(self, name='ne_shaded', **kwargs): """ if name == 'ne_shaded': source_proj = ccrs.PlateCarree() - fname = (config["repo_data_dir"] / 'raster' / 'natural_earth' - / '50-natural-earth-1-downsampled.png') - - return self.imshow(imread(fname), origin='upper', - transform=source_proj, - extent=[-180, 180, -90, 90], - **kwargs) + fname = ( + config['repo_data_dir'] + / 'raster' + / 'natural_earth' + / '50-natural-earth-1-downsampled.png' + ) + + return self.imshow( + imread(fname), + origin='upper', + transform=source_proj, + extent=[-180, 180, -90, 90], + **kwargs, + ) else: raise ValueError('Unknown stock image %r.' % name) - def background_img(self, name='ne_shaded', resolution='low', extent=None, - cache=False): + def background_img( + self, name='ne_shaded', resolution='low', extent=None, cache=False + ): """ Add a background image to the map, from a selection of pre-prepared images held in a directory specified by the CARTOPY_USER_BACKGROUNDS @@ -1032,9 +1087,12 @@ def background_img(self, name='ne_shaded', resolution='low', extent=None, # read in the user's background image directory: if len(_USER_BG_IMGS) == 0: self.read_user_background_images() - bgdir = Path(os.getenv( - 'CARTOPY_USER_BACKGROUNDS', - config["repo_data_dir"] / 'raster' / 'natural_earth')) + bgdir = Path( + os.getenv( + 'CARTOPY_USER_BACKGROUNDS', + config['repo_data_dir'] / 'raster' / 'natural_earth', + ) + ) # now get the filename we want to use: try: fname = _USER_BG_IMGS[name][resolution] @@ -1042,7 +1100,8 @@ def background_img(self, name='ne_shaded', resolution='low', extent=None, raise ValueError( f'Image {name!r} and resolution {resolution!r} are not ' f'present in the user background image metadata in directory ' - f'{bgdir!r}') + f'{bgdir!r}' + ) # Now obtain the image data from file or cache: fpath = bgdir / fname if cache: @@ -1066,9 +1125,9 @@ def background_img(self, name='ne_shaded', resolution='low', extent=None, if extent is None: # not specifying an extent, so return all of it: - return self.imshow(img, origin='upper', - transform=source_proj, - extent=[-180, 180, -90, 90]) + return self.imshow( + img, origin='upper', transform=source_proj, extent=[-180, 180, -90, 90] + ) else: # return only a subset of the image: # set up coordinate arrays: @@ -1079,38 +1138,41 @@ def background_img(self, name='ne_shaded', resolution='low', extent=None, lon_pts = (np.arange(img.shape[1]) * d_lon + (d_lon / 2)) - 180 # which points are in range: - lat_in_range = np.logical_and(lat_pts >= extent[2], - lat_pts <= extent[3]) + lat_in_range = np.logical_and(lat_pts >= extent[2], lat_pts <= extent[3]) if extent[0] < 180 and extent[1] > 180: # we have a region crossing the dateline # this is the westerly side of the input image: - lon_in_range1 = np.logical_and(lon_pts >= extent[0], - lon_pts <= 180.0) + lon_in_range1 = np.logical_and(lon_pts >= extent[0], lon_pts <= 180.0) img_subset1 = img[lat_in_range, :, :][:, lon_in_range1, :] # and the eastward half: - lon_in_range2 = lon_pts + 360. <= extent[1] + lon_in_range2 = lon_pts + 360.0 <= extent[1] img_subset2 = img[lat_in_range, :, :][:, lon_in_range2, :] # now join them up: img_subset = np.concatenate((img_subset1, img_subset2), axis=1) # now define the extent for output that matches those points: - ret_extent = [lon_pts[lon_in_range1][0] - d_lon / 2, - lon_pts[lon_in_range2][-1] + d_lon / 2 + 360, - lat_pts[lat_in_range][-1] - d_lat / 2, - lat_pts[lat_in_range][0] + d_lat / 2] + ret_extent = [ + lon_pts[lon_in_range1][0] - d_lon / 2, + lon_pts[lon_in_range2][-1] + d_lon / 2 + 360, + lat_pts[lat_in_range][-1] - d_lat / 2, + lat_pts[lat_in_range][0] + d_lat / 2, + ] else: # not crossing the dateline, so just find the region: - lon_in_range = np.logical_and(lon_pts >= extent[0], - lon_pts <= extent[1]) + lon_in_range = np.logical_and( + lon_pts >= extent[0], lon_pts <= extent[1] + ) img_subset = img[lat_in_range, :, :][:, lon_in_range, :] # now define the extent for output that matches those points: - ret_extent = [lon_pts[lon_in_range][0] - d_lon / 2.0, - lon_pts[lon_in_range][-1] + d_lon / 2.0, - lat_pts[lat_in_range][-1] - d_lat / 2.0, - lat_pts[lat_in_range][0] + d_lat / 2.0] + ret_extent = [ + lon_pts[lon_in_range][0] - d_lon / 2.0, + lon_pts[lon_in_range][-1] + d_lon / 2.0, + lat_pts[lat_in_range][-1] - d_lat / 2.0, + lat_pts[lat_in_range][0] + d_lat / 2.0, + ] - return self.imshow(img_subset, origin='upper', - transform=source_proj, - extent=ret_extent) + return self.imshow( + img_subset, origin='upper', transform=source_proj, extent=ret_extent + ) def read_user_background_images(self, verify=True): """ @@ -1130,9 +1192,12 @@ def read_user_background_images(self, verify=True): lib/cartopy/data/raster/natural_earth/images.json """ - bgdir = Path(os.getenv( - 'CARTOPY_USER_BACKGROUNDS', - config["repo_data_dir"] / 'raster' / 'natural_earth')) + bgdir = Path( + os.getenv( + 'CARTOPY_USER_BACKGROUNDS', + config['repo_data_dir'] / 'raster' / 'natural_earth', + ) + ) json_file = bgdir / 'images.json' with open(json_file) as js_obj: @@ -1153,15 +1218,15 @@ def read_user_background_images(self, verify=True): raise ValueError( f'User background metadata file {json_file!r},' f' image type {img_type!r}, does not specify' - f' metadata item {required!r}') + f' metadata item {required!r}' + ) for resln in _USER_BG_IMGS[img_type]: # the required_info items are not resolutions: if resln not in required_info: img_it_r = _USER_BG_IMGS[img_type][resln] test_file = bgdir / img_it_r if not test_file.is_file(): - raise ValueError( - f'File "{test_file}" not found') + raise ValueError(f'File "{test_file}" not found') def add_raster(self, raster_source, **slippy_image_kwargs): """ @@ -1243,35 +1308,45 @@ def imshow(self, img, *args, **kwargs): """ 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.') + 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') extent = kwargs.get('extent', None) kwargs.setdefault('origin', 'upper') - same_projection = (isinstance(transform, ccrs.Projection) and - self.projection == transform) + same_projection = ( + isinstance(transform, ccrs.Projection) and self.projection == transform + ) # 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): - if "regrid_shape" in kwargs: - warnings.warn("ignoring regrid_shape because it doesn't do anything " - "when working in the same projection. To avoid this " - "warning, remove the 'regrid_shape' keyword argument.") - kwargs.pop("regrid_shape") + 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 + ): + if 'regrid_shape' in kwargs: + warnings.warn( + "ignoring regrid_shape because it doesn't do anything " + 'when working in the same projection. To avoid this ' + "warning, remove the 'regrid_shape' keyword argument." + ) + kwargs.pop('regrid_shape') result = super().imshow(img, *args, **kwargs) else: extent = kwargs.pop('extent', None) @@ -1284,46 +1359,48 @@ def imshow(self, img, *args, **kwargs): kwargs['origin'] = 'lower' if not isinstance(transform, ccrs.Projection): - raise ValueError('Expected a projection subclass. Cannot ' - 'handle a %s in imshow.' % type(transform)) + raise ValueError( + 'Expected a projection subclass. Cannot ' + 'handle a %s in imshow.' % type(transform) + ) target_extent = self.get_extent(self.projection) regrid_shape = kwargs.pop('regrid_shape', 750) - regrid_shape = self._regrid_shape_aspect(regrid_shape, - target_extent) + regrid_shape = self._regrid_shape_aspect(regrid_shape, target_extent) # Lazy import because scipy/pykdtree in img_transform are only # optional dependencies from cartopy.img_transform import warp_array + original_extent = extent - img, extent = warp_array(img, - source_proj=transform, - source_extent=original_extent, - target_proj=self.projection, - target_res=regrid_shape, - target_extent=target_extent, - mask_extrapolated=True, - ) + img, extent = warp_array( + img, + source_proj=transform, + source_extent=original_extent, + target_proj=self.projection, + target_res=regrid_shape, + target_extent=target_extent, + mask_extrapolated=True, + ) alpha = kwargs.pop('alpha', None) if np.array(alpha).ndim == 2: - alpha, _ = warp_array(alpha, - source_proj=transform, - source_extent=original_extent, - target_proj=self.projection, - target_res=regrid_shape, - target_extent=target_extent, - mask_extrapolated=True, - ) + alpha, _ = warp_array( + alpha, + source_proj=transform, + source_extent=original_extent, + target_proj=self.projection, + target_res=regrid_shape, + target_extent=target_extent, + mask_extrapolated=True, + ) kwargs['alpha'] = alpha # As a workaround to a matplotlib limitation, turn any images # which are masked array RGB(A) into RGBA images if np.ma.is_masked(img) and len(img.shape) > 2: - # transform RGB(A) into RGBA old_img = img - img = np.ones(old_img.shape[:2] + (4, ), - dtype=old_img.dtype) + img = np.ones(old_img.shape[:2] + (4,), dtype=old_img.dtype) img[:, :, :3] = old_img[:, :, :3] # if img is RGBA, save alpha channel @@ -1339,14 +1416,31 @@ def imshow(self, img, *args, **kwargs): return result - def gridlines(self, crs=None, draw_labels=False, - xlocs=None, ylocs=None, dms=False, - x_inline=None, y_inline=None, auto_inline=True, - xformatter=None, yformatter=None, xlim=None, ylim=None, - rotate_labels=None, xlabel_style=None, ylabel_style=None, - labels_bbox_style=None, xpadding=5, ypadding=5, - offset_angle=25, auto_update=None, formatter_kwargs=None, - **kwargs): + def gridlines( + self, + crs=None, + draw_labels=False, + xlocs=None, + ylocs=None, + dms=False, + x_inline=None, + y_inline=None, + auto_inline=True, + xformatter=None, + yformatter=None, + xlim=None, + ylim=None, + rotate_labels=None, + xlabel_style=None, + ylabel_style=None, + labels_bbox_style=None, + xpadding=5, + ypadding=5, + offset_angle=25, + auto_update=None, + formatter_kwargs=None, + **kwargs, + ): """ Automatically add gridlines to the axes, in the given coordinate system, at draw time. @@ -1485,16 +1579,32 @@ def gridlines(self, crs=None, draw_labels=False, if crs is None: crs = ccrs.PlateCarree(globe=self.projection.globe) from cartopy.mpl.gridliner import Gridliner + gl = Gridliner( - self, crs=crs, draw_labels=draw_labels, xlocator=xlocs, - ylocator=ylocs, collection_kwargs=kwargs, dms=dms, - x_inline=x_inline, y_inline=y_inline, auto_inline=auto_inline, - xformatter=xformatter, yformatter=yformatter, - xlim=xlim, ylim=ylim, rotate_labels=rotate_labels, - xlabel_style=xlabel_style, ylabel_style=ylabel_style, + self, + crs=crs, + draw_labels=draw_labels, + xlocator=xlocs, + ylocator=ylocs, + collection_kwargs=kwargs, + dms=dms, + x_inline=x_inline, + y_inline=y_inline, + auto_inline=auto_inline, + xformatter=xformatter, + yformatter=yformatter, + xlim=xlim, + ylim=ylim, + rotate_labels=rotate_labels, + xlabel_style=xlabel_style, + ylabel_style=ylabel_style, labels_bbox_style=labels_bbox_style, - xpadding=xpadding, ypadding=ypadding, offset_angle=offset_angle, - auto_update=auto_update, formatter_kwargs=formatter_kwargs) + xpadding=xpadding, + ypadding=ypadding, + offset_angle=offset_angle, + auto_update=auto_update, + formatter_kwargs=formatter_kwargs, + ) self.add_artist(gl) return gl @@ -1504,9 +1614,9 @@ def _gen_axes_patch(self): def _gen_axes_spines(self, locations=None, offset=0.0, units='inches'): # generate some axes spines, as some Axes super class machinery # requires them. Just make them invisible - spines = super()._gen_axes_spines(locations=locations, - offset=offset, - units=units) + spines = super()._gen_axes_spines( + locations=locations, offset=offset, units=units + ) for spine in spines.values(): spine.set_visible(False) @@ -1586,9 +1696,11 @@ def contour(self, *args, **kwargs): if not _MPL_38: # We need to compute the dataLim correctly for contours. - bboxes = [col.get_datalim(self.transData) - for col in result.collections - if col.get_paths()] + bboxes = [ + col.get_datalim(self.transData) + for col in result.collections + if col.get_paths() + ] if bboxes: extent = mtransforms.Bbox.union(bboxes) self.update_datalim(extent.get_points()) @@ -1632,9 +1744,11 @@ def contourf(self, *args, **kwargs): if not _MPL_38: # We need to compute the dataLim correctly for contours. - bboxes = [col.get_datalim(self.transData) - for col in result.collections - if col.get_paths()] + bboxes = [ + col.get_datalim(self.transData) + for col in result.collections + if col.get_paths() + ] if bboxes: extent = mtransforms.Bbox.union(bboxes) self.update_datalim(extent.get_points()) @@ -1666,21 +1780,25 @@ def scatter(self, *args, **kwargs): """ # exclude Geodetic as a valid source CS - if (isinstance(kwargs['transform'], - InterProjectionTransform) and - kwargs['transform'].source_projection.is_geodetic()): - raise ValueError('Cartopy cannot currently do spherical ' - 'scatter. The source CRS cannot be a ' - 'geodetic, consider using the cylindrical form ' - '(PlateCarree or RotatedPole).') + if ( + isinstance(kwargs['transform'], InterProjectionTransform) + and kwargs['transform'].source_projection.is_geodetic() + ): + raise ValueError( + 'Cartopy cannot currently do spherical ' + 'scatter. The source CRS cannot be a ' + 'geodetic, consider using the cylindrical form ' + '(PlateCarree or RotatedPole).' + ) result = super().scatter(*args, **kwargs) self.autoscale_view() return result @_add_transform - def annotate(self, text, xy, xytext=None, xycoords='data', textcoords=None, - *args, **kwargs): + def annotate( + self, text, xy, xytext=None, xycoords='data', textcoords=None, *args, **kwargs + ): """ Add the "transform" keyword to :func:`~matplotlib.pyplot.annotate`. @@ -1713,8 +1831,9 @@ def annotate(self, text, xy, xytext=None, xycoords='data', textcoords=None, if isinstance(textcoords, ccrs.CRS): textcoords = textcoords._as_mpl_transform(self) - result = super().annotate(text, xy, xytext, xycoords=xycoords, - textcoords=textcoords, *args, **kwargs) + result = super().annotate( + text, xy, xytext, xycoords=xycoords, textcoords=textcoords, *args, **kwargs + ) self.autoscale_view() return result @@ -1776,8 +1895,11 @@ def _wrap_args(self, *args, **kwargs): """ default_shading = mpl.rcParams.get('pcolor.shading') shading = kwargs.get('shading') or default_shading - if not (shading in ('nearest', 'auto') and len(args) == 3 and - getattr(kwargs.get('transform'), '_wrappable', False)): + if not ( + shading in ('nearest', 'auto') + and len(args) == 3 + and getattr(kwargs.get('transform'), '_wrappable', False) + ): return args, kwargs # We have changed the shading from nearest/auto to flat @@ -1801,14 +1923,15 @@ def _interp_grid(X, wrap=0): if wrap: dX = (dX + wrap / 2) % wrap - wrap / 2 dX = dX / 2 - X = np.hstack((X[:, [0]] - dX[:, [0]], - X[:, :-1] + dX, - X[:, [-1]] + dX[:, [-1]])) + X = np.hstack( + (X[:, [0]] - dX[:, [0]], X[:, :-1] + dX, X[:, [-1]] + dX[:, [-1]]) + ) else: # This is just degenerate, but we can't reliably guess # a dX if there is just one value. X = np.hstack((X, X)) return X + t = kwargs.get('transform') xwrap = abs(t.x_limits[1] - t.x_limits[0]) if ncols == Nx: @@ -1840,27 +1963,32 @@ def _wrap_quadmesh(self, collection, **kwargs): if C.shape[-1] == 1: C = C.squeeze(axis=-1) transformed_pts = self.projection.transform_points( - t, coords[..., 0], coords[..., 1]) + t, coords[..., 0], coords[..., 1] + ) # Compute the length of diagonals in transformed coordinates # and create a mask where the wrapped cells are of shape (Ny-1, Nx-1) with np.errstate(invalid='ignore'): xs, ys = transformed_pts[..., 0], transformed_pts[..., 1] - diagonal0_lengths = np.hypot(xs[1:, 1:] - xs[:-1, :-1], - ys[1:, 1:] - ys[:-1, :-1]) - diagonal1_lengths = np.hypot(xs[1:, :-1] - xs[:-1, 1:], - ys[1:, :-1] - ys[:-1, 1:]) + diagonal0_lengths = np.hypot( + xs[1:, 1:] - xs[:-1, :-1], ys[1:, 1:] - ys[:-1, :-1] + ) + diagonal1_lengths = np.hypot( + xs[1:, :-1] - xs[:-1, 1:], ys[1:, :-1] - ys[:-1, 1:] + ) # The maximum size of the diagonal of any cell, defined to # be the projection width divided by 2*sqrt(2) # TODO: Make this dependent on the boundary of the # projection which will help with curved boundaries - size_limit = (abs(self.projection.x_limits[1] - - self.projection.x_limits[0]) / - (2 * np.sqrt(2))) - mask = (np.isnan(diagonal0_lengths) | - (diagonal0_lengths > size_limit) | - np.isnan(diagonal1_lengths) | - (diagonal1_lengths > size_limit)) + size_limit = abs( + self.projection.x_limits[1] - self.projection.x_limits[0] + ) / (2 * np.sqrt(2)) + mask = ( + np.isnan(diagonal0_lengths) + | (diagonal0_lengths > size_limit) + | np.isnan(diagonal1_lengths) + | (diagonal1_lengths > size_limit) + ) # Update the data limits based on the corners of the mesh # in transformed coordinates, ignoring nan values @@ -1868,17 +1996,17 @@ def _wrap_quadmesh(self, collection, **kwargs): warnings.filterwarnings('ignore', 'All-NaN slice encountered') # If we have all nans, that is OK and will be handled by the # Bbox calculations later, so suppress that warning from the user - corners = ((np.nanmin(xs), np.nanmin(ys)), - (np.nanmax(xs), np.nanmax(ys))) + corners = ((np.nanmin(xs), np.nanmin(ys)), (np.nanmax(xs), np.nanmax(ys))) collection._corners = mtransforms.Bbox(corners) self.update_datalim(collection._corners) # We need to keep the transform/projection check after # update_datalim to make sure we are getting the proper # datalims on the returned collection - if (not (getattr(t, '_wrappable', False) and - getattr(self.projection, '_wrappable', False)) or - not np.any(mask)): + if not ( + getattr(t, '_wrappable', False) + and getattr(self.projection, '_wrappable', False) + ) or not np.any(mask): # If both projections are unwrappable # or if there aren't any points to wrap return collection @@ -1887,10 +2015,12 @@ def _wrap_quadmesh(self, collection, **kwargs): # 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.") + 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[0], data_shape[1]), dtype=bool) # If any of the cells were wrapped, apply it to all 4 corners @@ -1906,21 +2036,24 @@ def _wrap_quadmesh(self, collection, **kwargs): # cells instead, which will properly handle the wrap. if collection.get_cmap()._rgba_bad[3] != 0.0: - warnings.warn("The colormap's 'bad' has been set, but " - "in order to wrap pcolormesh across the " - "map it must be fully transparent.", - stacklevel=3) + warnings.warn( + "The colormap's 'bad' has been set, but " + 'in order to wrap pcolormesh across the ' + 'map it must be fully transparent.', + stacklevel=3, + ) # Get hold of masked versions of the array to be passed to set_array # methods of QuadMesh and PolyQuadMesh - pcolormesh_data, pcolor_data, pcolor_mask = \ + pcolormesh_data, pcolor_data, pcolor_mask = ( cartopy.mpl.geocollection._split_wrapped_mesh_data(C, mask) + ) collection.set_array(pcolormesh_data) # plot with slightly lower zorder to avoid odd issue # where the main plot is obscured - zorder = collection.zorder - .1 + zorder = collection.zorder - 0.1 kwargs.pop('zorder', None) kwargs.pop('shading', None) kwargs.setdefault('snap', False) @@ -1943,17 +2076,17 @@ def _wrap_quadmesh(self, collection, **kwargs): # fill in the proper data later with set_array() # calls. pcolor_zeros = np.ma.array(np.zeros(C.shape), mask=pcolor_mask) - pcolor_col = self.pcolor(coords[..., 0], coords[..., 1], - pcolor_zeros, zorder=zorder, - **kwargs) + pcolor_col = self.pcolor( + coords[..., 0], coords[..., 1], pcolor_zeros, zorder=zorder, **kwargs + ) # The pcolor_col is now possibly shorter than the # actual collection, so grab the masked cells pcolor_col.set_array(pcolor_data[mask].ravel()) else: - pcolor_col = self.pcolor(coords[..., 0], coords[..., 1], - pcolor_data, zorder=zorder, - **kwargs) + pcolor_col = self.pcolor( + coords[..., 0], coords[..., 1], pcolor_data, zorder=zorder, **kwargs + ) # Currently pcolor_col.get_array() will return a compressed array # and warn unless we explicitly set the 2D array. This should be # unnecessary with future matplotlib versions. @@ -2042,26 +2175,40 @@ def quiver(self, x, y, u, v, *args, **kwargs): """ t = kwargs['transform'] regrid_shape = kwargs.pop('regrid_shape', None) - target_extent = kwargs.pop('target_extent', - self.get_extent(self.projection)) + target_extent = kwargs.pop('target_extent', self.get_extent(self.projection)) if regrid_shape is not None: # If regridding is required then we'll be handling transforms # manually and plotting in native coordinates. - regrid_shape = self._regrid_shape_aspect(regrid_shape, - target_extent) + regrid_shape = self._regrid_shape_aspect(regrid_shape, target_extent) # Lazy load vector_scalar_to_grid due to the optional # scipy dependency from cartopy.vector_transform import vector_scalar_to_grid + if args: # Interpolate color array as well as vector components. x, y, u, v, c = vector_scalar_to_grid( - t, self.projection, regrid_shape, x, y, u, v, args[0], - target_extent=target_extent) + t, + self.projection, + regrid_shape, + x, + y, + u, + v, + args[0], + target_extent=target_extent, + ) args = (c,) + args[1:] else: x, y, u, v = vector_scalar_to_grid( - t, self.projection, regrid_shape, x, y, u, v, - target_extent=target_extent) + t, + self.projection, + regrid_shape, + x, + y, + u, + v, + target_extent=target_extent, + ) kwargs.pop('transform', None) elif t != self.projection: # Transform the vectors if the projection is not the same as the @@ -2117,26 +2264,40 @@ def barbs(self, x, y, u, v, *args, **kwargs): """ t = kwargs['transform'] regrid_shape = kwargs.pop('regrid_shape', None) - target_extent = kwargs.pop('target_extent', - self.get_extent(self.projection)) + target_extent = kwargs.pop('target_extent', self.get_extent(self.projection)) if regrid_shape is not None: # If regridding is required then we'll be handling transforms # manually and plotting in native coordinates. - regrid_shape = self._regrid_shape_aspect(regrid_shape, - target_extent) + regrid_shape = self._regrid_shape_aspect(regrid_shape, target_extent) # Lazy load vector_scalar_to_grid due to the optional # scipy dependency from cartopy.vector_transform import vector_scalar_to_grid + if args: # Interpolate color array as well as vector components. x, y, u, v, c = vector_scalar_to_grid( - t, self.projection, regrid_shape, x, y, u, v, args[0], - target_extent=target_extent) + t, + self.projection, + regrid_shape, + x, + y, + u, + v, + args[0], + target_extent=target_extent, + ) args = (c,) + args[1:] else: x, y, u, v = vector_scalar_to_grid( - t, self.projection, regrid_shape, x, y, u, v, - target_extent=target_extent) + t, + self.projection, + regrid_shape, + x, + y, + u, + v, + target_extent=target_extent, + ) kwargs.pop('transform', None) elif t != self.projection: # Transform the vectors if the projection is not the same as the @@ -2203,9 +2364,18 @@ def streamplot(self, x, y, u, v, **kwargs): # Lazy load vector_scalar_to_grid due to the optional # scipy dependency from cartopy.vector_transform import vector_scalar_to_grid - gridded = vector_scalar_to_grid(t, self.projection, regrid_shape, - x, y, u, v, *scalars, - target_extent=target_extent) + + gridded = vector_scalar_to_grid( + t, + self.projection, + regrid_shape, + x, + y, + u, + v, + *scalars, + target_extent=target_extent, + ) x, y, u, v = gridded[:4] # If scalar fields were regridded then replace the appropriate keyword # arguments with the gridded arrays. @@ -2219,8 +2389,7 @@ def streamplot(self, x, y, u, v, **kwargs): # a warning which is not at all important so it is hidden from the # user to avoid confusion. message = 'Warning: converting a masked element to nan.' - warnings.filterwarnings('ignore', message=message, - category=UserWarning) + warnings.filterwarnings('ignore', message=message, category=UserWarning) sp = super().streamplot(x, y, u, v, **kwargs) return sp @@ -2248,8 +2417,10 @@ def add_wmts(self, wmts, layer_name, wmts_kwargs=None, cache=False, **kwargs): """ from cartopy.io.ogc_clients import WMTSRasterSource - wmts = WMTSRasterSource(wmts, layer_name, - gettile_extra_kwargs=wmts_kwargs, cache=cache) + + wmts = WMTSRasterSource( + wmts, layer_name, gettile_extra_kwargs=wmts_kwargs, cache=cache + ) return self.add_raster(wmts, **kwargs) def add_wms(self, wms, layers, wms_kwargs=None, **kwargs): @@ -2277,6 +2448,7 @@ def add_wms(self, wms, layers, wms_kwargs=None, **kwargs): """ from cartopy.io.ogc_clients import WMSRasterSource + wms = WMSRasterSource(wms, layers, getmap_extra_kwargs=wms_kwargs) return self.add_raster(wms, **kwargs) diff --git a/lib/cartopy/mpl/geocollection.py b/lib/cartopy/mpl/geocollection.py index 7c8ee32cb..9a7f16c32 100644 --- a/lib/cartopy/mpl/geocollection.py +++ b/lib/cartopy/mpl/geocollection.py @@ -21,8 +21,9 @@ def _split_wrapped_mesh_data(C, mask): if C.ndim == 3: # RGB(A) array. if not _MPL_38: - raise ValueError("GeoQuadMesh wrapping for RGB(A) requires " - "Matplotlib v3.8 or later") + raise ValueError( + 'GeoQuadMesh wrapping for RGB(A) requires Matplotlib v3.8 or later' + ) # mask will need an extra trailing dimension mask = np.broadcast_to(mask[..., np.newaxis], C.shape) @@ -43,6 +44,7 @@ class GeoQuadMesh(QuadMesh): A QuadMesh designed to help handle the case when the mesh is wrapped. """ + # No __init__ method here - most of the time a GeoQuadMesh will # come from GeoAxes.pcolormesh. These methods morph a QuadMesh by # fiddling with instance.__class__. @@ -81,8 +83,7 @@ def set_array(self, A): ok_shapes = [(h, w, 3), (h, w, 4), (h, w), (h * w,)] if A.shape not in ok_shapes: ok_shape_str = ' or '.join(map(str, ok_shapes)) - raise ValueError( - f"A should have shape {ok_shape_str}, not {A.shape}") + raise ValueError(f'A should have shape {ok_shape_str}, not {A.shape}') if A.ndim == 1: # Always use array with at least two dimensions. This is @@ -94,13 +95,13 @@ def set_array(self, A): # Only use the mask attribute if it is there. if hasattr(self, '_wrapped_mask'): - # Update the pcolor data with the wrapped masked data A, pcolor_data, _ = _split_wrapped_mesh_data(A, self._wrapped_mask) if not _MPL_38: self._wrapped_collection_fix.set_array( - pcolor_data[self._wrapped_mask].ravel()) + pcolor_data[self._wrapped_mask].ravel() + ) else: self._wrapped_collection_fix.set_array(pcolor_data) diff --git a/lib/cartopy/mpl/gridliner.py b/lib/cartopy/mpl/gridliner.py index 26727047d..ed327b0d1 100644 --- a/lib/cartopy/mpl/gridliner.py +++ b/lib/cartopy/mpl/gridliner.py @@ -48,7 +48,7 @@ _POLAR_PROJS = ( cartopy.crs.NorthPolarStereo, cartopy.crs.SouthPolarStereo, - cartopy.crs.Stereographic + cartopy.crs.Stereographic, ) _ROTATE_LABEL_PROJS = _POLAR_PROJS + ( cartopy.crs.AlbersEqualArea, @@ -88,31 +88,47 @@ def _lat_hemisphere(latitude): def _east_west_formatted(longitude, num_format='g'): hemisphere = _lon_hemisphere(longitude) - return f'{abs(longitude):{num_format}}\N{Degree Sign}{hemisphere}' + return f'{abs(longitude):{num_format}}\N{DEGREE SIGN}{hemisphere}' def _north_south_formatted(latitude, num_format='g'): hemisphere = _lat_hemisphere(latitude) - return f'{abs(latitude):{num_format}}\N{Degree Sign}{hemisphere}' + return f'{abs(latitude):{num_format}}\N{DEGREE SIGN}{hemisphere}' #: A formatter which turns longitude values into nice longitudes such as 110W -LONGITUDE_FORMATTER = mticker.FuncFormatter(lambda v, pos: - _east_west_formatted(v)) +LONGITUDE_FORMATTER = mticker.FuncFormatter(lambda v, pos: _east_west_formatted(v)) #: A formatter which turns longitude values into nice longitudes such as 45S -LATITUDE_FORMATTER = mticker.FuncFormatter(lambda v, pos: - _north_south_formatted(v)) +LATITUDE_FORMATTER = mticker.FuncFormatter(lambda v, pos: _north_south_formatted(v)) class Gridliner(matplotlib.artist.Artist): - def __init__(self, axes, crs, draw_labels=False, xlocator=None, - ylocator=None, collection_kwargs=None, - xformatter=None, yformatter=None, dms=False, - x_inline=None, y_inline=None, auto_inline=True, - xlim=None, ylim=None, rotate_labels=None, - xlabel_style=None, ylabel_style=None, labels_bbox_style=None, - xpadding=5, ypadding=5, offset_angle=25, - auto_update=None, formatter_kwargs=None): + def __init__( + self, + axes, + crs, + draw_labels=False, + xlocator=None, + ylocator=None, + collection_kwargs=None, + xformatter=None, + yformatter=None, + dms=False, + x_inline=None, + y_inline=None, + auto_inline=True, + xlim=None, + ylim=None, + rotate_labels=None, + xlabel_style=None, + ylabel_style=None, + labels_bbox_style=None, + xpadding=5, + ypadding=5, + offset_angle=25, + auto_update=None, + formatter_kwargs=None, + ): """ Artist used by :meth:`cartopy.mpl.geoaxes.GeoAxes.gridlines` to add gridlines and tick labels to a map. @@ -269,7 +285,7 @@ def __init__(self, axes, crs, draw_labels=False, xlocator=None, formatter_kwargs = { **(formatter_kwargs or {}), - "dms": dms, + 'dms': dms, } if xformatter is None: @@ -290,7 +306,6 @@ def __init__(self, axes, crs, draw_labels=False, xlocator=None, # Draw label argument if isinstance(draw_labels, list): - # Select to which coordinate it is applied if 'x' not in draw_labels and 'y' not in draw_labels: value = True @@ -317,7 +332,6 @@ def __init__(self, axes, crs, draw_labels=False, xlocator=None, self.geo_labels = value if 'geo' in draw_labels else False elif isinstance(draw_labels, dict): - self.top_labels = draw_labels.get('top', False) self.bottom_labels = draw_labels.get('bottom', False) self.left_labels = draw_labels.get('left', False) @@ -325,7 +339,6 @@ def __init__(self, axes, crs, draw_labels=False, xlocator=None, self.geo_labels = draw_labels.get('geo', False) else: - self.top_labels = draw_labels self.bottom_labels = draw_labels self.left_labels = draw_labels @@ -336,9 +349,8 @@ def __init__(self, axes, crs, draw_labels=False, xlocator=None, value = getattr(self, f'{loc}_labels') if isinstance(value, str): value = value.lower() - if (not isinstance(value, (list, bool)) and - value not in ('x', 'y')): - raise ValueError(f"Invalid draw_labels argument: {value}") + if not isinstance(value, (list, bool)) and value not in ('x', 'y'): + raise ValueError(f'Invalid draw_labels argument: {value}') if auto_inline: if isinstance(axes.projection, _X_INLINE_PROJS): @@ -370,9 +382,9 @@ def __init__(self, axes, crs, draw_labels=False, xlocator=None, elif self.x_inline and self.y_inline: self.inline_labels = True elif self.x_inline: - self.inline_labels = "x" + self.inline_labels = 'x' elif self.y_inline: - self.inline_labels = "y" + self.inline_labels = 'y' else: self.inline_labels = False @@ -396,8 +408,7 @@ def __init__(self, axes, crs, draw_labels=False, xlocator=None, self.ylabel_style = ylabel_style or {} #: bbox style for grid labels - self.labels_bbox_style = ( - labels_bbox_style or {'pad': 0, 'visible': False}) + self.labels_bbox_style = labels_bbox_style or {'pad': 0, 'visible': False} #: The padding from the map edge to the x labels in points. self.xpadding = xpadding @@ -407,10 +418,9 @@ def __init__(self, axes, crs, draw_labels=False, xlocator=None, #: Control the rotation of labels. if rotate_labels is None: - rotate_labels = ( - axes.projection.__class__ in _ROTATE_LABEL_PROJS) + rotate_labels = axes.projection.__class__ in _ROTATE_LABEL_PROJS if not isinstance(rotate_labels, (bool, float, int)): - raise ValueError("Invalid rotate_labels argument") + raise ValueError('Invalid rotate_labels argument') self.rotate_labels = rotate_labels self.offset_angle = offset_angle @@ -452,11 +462,13 @@ def __init__(self, axes, crs, draw_labels=False, xlocator=None, # Note #2394 should be addressed before this deprecation expires. calling_module = inspect.stack()[1].filename warnings.warn( - "The auto_update parameter was deprecated at Cartopy 0.23. In future " - "the gridlines and labels will always be updated.", + 'The auto_update parameter was deprecated at Cartopy 0.23. In future ' + 'the gridlines and labels will always be updated.', DeprecationWarning, - stacklevel=(3 if calling_module.endswith('cartopy/mpl/geoaxes.py') - else 2)) + stacklevel=( + 3 if calling_module.endswith('cartopy/mpl/geoaxes.py') else 2 + ), + ) self._auto_update = auto_update def has_labels(self): @@ -470,56 +482,47 @@ def label_artists(self): @property def top_label_artists(self): """The top labels which were created at draw time""" - return [label.artist for label in self._labels - if label.loc == "top"] + return [label.artist for label in self._labels if label.loc == 'top'] @property def bottom_label_artists(self): """The bottom labels which were created at draw time""" - return [label.artist for label in self._labels - if label.loc == "bottom"] + return [label.artist for label in self._labels if label.loc == 'bottom'] @property def left_label_artists(self): """The left labels which were created at draw time""" - return [label.artist for label in self._labels - if label.loc == "left"] + return [label.artist for label in self._labels if label.loc == 'left'] @property def right_label_artists(self): """The right labels which were created at draw time""" - return [label.artist for label in self._labels - if label.loc == "right"] + return [label.artist for label in self._labels if label.loc == 'right'] @property def geo_label_artists(self): """The geo spine labels which were created at draw time""" - return [label.artist for label in self._labels - if label.loc == "geo"] + return [label.artist for label in self._labels if label.loc == 'geo'] @property def x_inline_label_artists(self): """The x-coordinate inline labels which were created at draw time""" - return [label.artist for label in self._labels - if label.loc == "x_inline"] + return [label.artist for label in self._labels if label.loc == 'x_inline'] @property def y_inline_label_artists(self): """The y-coordinate inline labels which were created at draw time""" - return [label.artist for label in self._labels - if label.loc == "y_inline"] + return [label.artist for label in self._labels if label.loc == 'y_inline'] @property def xlabel_artists(self): """The x-coordinate labels which were created at draw time""" - return [label.artist for label in self._labels - if label.xy == "x"] + return [label.artist for label in self._labels if label.xy == 'x'] @property def ylabel_artists(self): """The y-coordinate labels which were created at draw time""" - return [label.artist for label in self._labels - if label.xy == "y"] + return [label.artist for label in self._labels if label.xy == 'y'] def _crs_transform(self): """ @@ -554,8 +557,10 @@ def _find_midpoints(self, lim, ticks): else: lq = 25 uq = 75 - midpoints = (self._round(np.percentile(lim, lq), cent), - self._round(np.percentile(lim, uq), cent)) + midpoints = ( + self._round(np.percentile(lim, lq), cent), + self._round(np.percentile(lim, uq), cent), + ) return midpoints def _draw_this_label(self, xylabel, loc): @@ -564,7 +569,7 @@ def _draw_this_label(self, xylabel, loc): # By default, only x on top/bottom and only y on left/right if draw_labels is True and loc != 'geo': - draw_labels = "x" if loc in ["top", "bottom"] else "y" + draw_labels = 'x' if loc in ['top', 'bottom'] else 'y' # Don't draw if not draw_labels: @@ -643,14 +648,15 @@ def _draw_gridliner(self, nx=None, ny=None, renderer=None): collection_kwargs = collection_kwargs.copy() collection_kwargs['transform'] = transform if not any(x in collection_kwargs for x in ['c', 'color']): - collection_kwargs.setdefault('color', - matplotlib.rcParams['grid.color']) + collection_kwargs.setdefault('color', matplotlib.rcParams['grid.color']) if not any(x in collection_kwargs for x in ['ls', 'linestyle']): - collection_kwargs.setdefault('linestyle', - matplotlib.rcParams['grid.linestyle']) + collection_kwargs.setdefault( + 'linestyle', matplotlib.rcParams['grid.linestyle'] + ) if not any(x in collection_kwargs for x in ['lw', 'linewidth']): - collection_kwargs.setdefault('linewidth', - matplotlib.rcParams['grid.linewidth']) + collection_kwargs.setdefault( + 'linewidth', matplotlib.rcParams['grid.linewidth'] + ) collection_kwargs.setdefault('clip_path', self.axes.patch) # Meridians @@ -660,27 +666,27 @@ def _draw_gridliner(self, nx=None, ny=None, renderer=None): lat_max = max(lat_max, max(lat_ticks)) lon_lines = np.empty((len(lon_ticks), n_steps, 2)) lon_lines[:, :, 0] = np.array(lon_ticks)[:, np.newaxis] - lon_lines[:, :, 1] = np.linspace( - lat_min, lat_max, n_steps)[np.newaxis, :] + lon_lines[:, :, 1] = np.linspace(lat_min, lat_max, n_steps)[np.newaxis, :] if self.xlines: nx = len(lon_lines) + 1 # XXX this bit is cartopy specific. (for circular longitudes) # Purpose: omit plotting the last x line, # as it may overlap the first. - if (isinstance(crs, Projection) and - isinstance(crs, _RectangularProjection) and - abs(np.diff(lon_lim)) == abs(np.diff(crs.x_limits))): + if ( + isinstance(crs, Projection) + and isinstance(crs, _RectangularProjection) + and abs(np.diff(lon_lim)) == abs(np.diff(crs.x_limits)) + ): nx -= 1 if self.xline_artists: # Update existing collection. - lon_lc, = self.xline_artists + (lon_lc,) = self.xline_artists lon_lc.set(segments=lon_lines, **collection_kwargs) else: # Create new collection. - lon_lc = mcollections.LineCollection(lon_lines, - **collection_kwargs) + lon_lc = mcollections.LineCollection(lon_lines, **collection_kwargs) self.xline_artists.append(lon_lc) # Parallels @@ -689,17 +695,15 @@ def _draw_gridliner(self, nx=None, ny=None, renderer=None): lon_min = min(lon_min, min(lon_ticks)) lon_max = max(lon_max, max(lon_ticks)) lat_lines = np.empty((len(lat_ticks), n_steps, 2)) - lat_lines[:, :, 0] = np.linspace(lon_min, lon_max, - n_steps)[np.newaxis, :] + lat_lines[:, :, 0] = np.linspace(lon_min, lon_max, n_steps)[np.newaxis, :] lat_lines[:, :, 1] = np.array(lat_ticks)[:, np.newaxis] if self.ylines: if self.yline_artists: # Update existing collection. - lat_lc, = self.yline_artists + (lat_lc,) = self.yline_artists lat_lc.set(segments=lat_lines, **collection_kwargs) else: - lat_lc = mcollections.LineCollection(lat_lines, - **collection_kwargs) + lat_lc = mcollections.LineCollection(lat_lines, **collection_kwargs) self.yline_artists.append(lat_lc) ################# @@ -709,8 +713,16 @@ def _draw_gridliner(self, nx=None, ny=None, renderer=None): # Clear drawn labels self._labels.clear() - if not any((self.left_labels, self.right_labels, self.bottom_labels, - self.top_labels, self.inline_labels, self.geo_labels)): + if not any( + ( + self.left_labels, + self.right_labels, + self.bottom_labels, + self.top_labels, + self.inline_labels, + self.geo_labels, + ) + ): return self._assert_can_draw_ticks() @@ -720,45 +732,46 @@ def _draw_gridliner(self, nx=None, ny=None, renderer=None): spines_specs = { 'left': { 'index': 0, - 'coord_type': "x", + 'coord_type': 'x', 'opcmp': operator.le, 'opval': max, }, 'bottom': { 'index': 1, - 'coord_type': "y", + 'coord_type': 'y', 'opcmp': operator.le, 'opval': max, }, 'right': { 'index': 0, - 'coord_type': "x", + 'coord_type': 'x', 'opcmp': operator.ge, 'opval': min, }, 'top': { 'index': 1, - 'coord_type': "y", + 'coord_type': 'y', 'opcmp': operator.ge, 'opval': min, }, } for side, specs in spines_specs.items(): bbox = self.axes.spines[side].get_window_extent(renderer) - specs['coords'] = [ - getattr(bbox, specs['coord_type'] + idx) for idx in "01"] + specs['coords'] = [getattr(bbox, specs['coord_type'] + idx) for idx in '01'] def update_artist(artist, renderer): artist.update_bbox_position_size(renderer) this_patch = artist.get_bbox_patch() - this_path = this_patch.get_path().transformed( - this_patch.get_transform()) + this_path = this_patch.get_path().transformed(this_patch.get_transform()) return this_path # Get the real map boundaries - self.axes.spines["geo"].get_window_extent(renderer) # update coords - map_boundary_path = self.axes.spines["geo"].get_path().transformed( - self.axes.spines["geo"].get_transform()) + self.axes.spines['geo'].get_window_extent(renderer) # update coords + map_boundary_path = ( + self.axes.spines['geo'] + .get_path() + .transformed(self.axes.spines['geo'].get_transform()) + ) map_boundary = sgeom.Polygon(map_boundary_path.vertices) if self.x_inline: @@ -774,18 +787,16 @@ def update_artist(artist, renderer): generate_labels = self._generate_labels() for xylabel, lines, line_ticks, formatter, label_style in ( - ('x', lon_lines, lon_ticks, - self.xformatter, self.xlabel_style.copy()), - ('y', lat_lines, lat_ticks, - self.yformatter, self.ylabel_style.copy())): - + ('x', lon_lines, lon_ticks, self.xformatter, self.xlabel_style.copy()), + ('y', lat_lines, lat_ticks, self.yformatter, self.ylabel_style.copy()), + ): x_inline = self.x_inline and xylabel == 'x' y_inline = self.y_inline and xylabel == 'y' padding = getattr(self, f'{xylabel}padding') bbox_style = self.labels_bbox_style.copy() - if "bbox" in label_style: - bbox_style.update(label_style["bbox"]) - label_style["bbox"] = bbox_style + if 'bbox' in label_style: + bbox_style.update(label_style['bbox']) + label_style['bbox'] = bbox_style formatter.set_locs(line_ticks) @@ -807,12 +818,13 @@ def update_artist(artist, renderer): if len(intersection) < 2: continue n2 = min(len(intersection), 3) - tails = [[(pt.x, pt.y) - for pt in intersection[:n2:n2 - 1]]] - heads = [[(pt.x, pt.y) - for pt in intersection[-1:-n2 - 1:-n2 + 1]]] - elif isinstance(intersection, (sgeom.LineString, - sgeom.MultiLineString)): + tails = [[(pt.x, pt.y) for pt in intersection[: n2 : n2 - 1]]] + heads = [ + [(pt.x, pt.y) for pt in intersection[-1 : -n2 - 1 : -n2 + 1]] + ] + elif isinstance( + intersection, (sgeom.LineString, sgeom.MultiLineString) + ): if isinstance(intersection, sgeom.LineString): intersection = [intersection] elif len(intersection.geoms) > 4: @@ -823,7 +835,8 @@ def update_artist(artist, renderer): # our merge still produced a multilinestring, so # manually concatenate the original coordinates xy = np.concatenate( - [inter.coords for inter in intersection.geoms], axis=0) + [inter.coords for inter in intersection.geoms], axis=0 + ) merged_line = shapely.LineString(xy) intersection = [merged_line] else: @@ -834,8 +847,8 @@ def update_artist(artist, renderer): if len(inter.coords) < 2: continue n2 = min(len(inter.coords), 8) - tails.append(inter.coords[:n2:n2 - 1]) - heads.append(inter.coords[-1:-n2 - 1:-n2 + 1]) + tails.append(inter.coords[: n2 : n2 - 1]) + heads.append(inter.coords[-1 : -n2 - 1 : -n2 + 1]) if not tails: continue elif isinstance(intersection, sgeom.GeometryCollection): @@ -866,31 +879,37 @@ def update_artist(artist, renderer): else: warnings.warn( 'Unsupported intersection geometry for gridline ' - f'labels: {intersection.__class__.__name__}') + f'labels: {intersection.__class__.__name__}' + ) continue del intersection # Loop on head and tail and plot label by extrapolation for i, (pt0, pt1) in itertools.chain.from_iterable( - enumerate(pair) for pair in zip(tails, heads)): - + enumerate(pair) for pair in zip(tails, heads) + ): # Initial text specs x0, y0 = pt0 if x_inline or y_inline: - kw = {'rotation': 0, 'transform': self.crs, - 'ha': 'center', 'va': 'center'} + kw = { + 'rotation': 0, + 'transform': self.crs, + 'ha': 'center', + 'va': 'center', + } loc = 'inline' else: x1, y1 = pt1 - segment_angle = np.arctan2(y0 - y1, - x0 - x1) * 180 / np.pi + segment_angle = np.arctan2(y0 - y1, x0 - x1) * 180 / np.pi loc = self._get_loc_from_spine_intersection( - spines_specs, xylabel, x0, y0) + spines_specs, xylabel, x0, y0 + ) if not self._draw_this_label(xylabel, loc): visible = False kw = self._get_text_specs(segment_angle, loc, xylabel) kw['transform'] = self._get_padding_transform( - segment_angle, loc, xylabel) + segment_angle, loc, xylabel + ) kw.update(label_style) # Get x and y in data coords @@ -928,15 +947,17 @@ def update_artist(artist, renderer): this_path = update_artist(artist, renderer) if not x_inline and not y_inline and loc == 'geo': new_loc = self._get_loc_from_spine_overlapping( - spines_specs, xylabel, this_path) + spines_specs, xylabel, this_path + ) if new_loc and loc != new_loc: loc = new_loc transform = self._get_padding_transform( - segment_angle, loc, xylabel) + segment_angle, loc, xylabel + ) artist.set_transform(transform) artist.update( - self._get_text_specs( - segment_angle, loc, xylabel)) + self._get_text_specs(segment_angle, loc, xylabel) + ) artist.update(label_style.copy()) this_path = update_artist(artist, renderer) @@ -949,9 +970,9 @@ def update_artist(artist, renderer): # Inline must be within the map. # TODO: When Matplotlib clip path works on text, this # clipping can be left to it. - center = (artist - .get_transform() - .transform_point(artist.get_position())) + center = artist.get_transform().transform_point( + artist.get_position() + ) visible = map_boundary_path.contains_point(center) else: # Now loop on padding factors until it does not overlap @@ -959,15 +980,14 @@ def update_artist(artist, renderer): visible = False padding_factor = 1 while padding_factor < max_padding_factor: - # Non-inline must not run through the outline. if map_boundary_path.intersects_path( - this_path, filled=padding > 0): - + this_path, filled=padding > 0 + ): # Apply new padding. transform = self._get_padding_transform( - segment_angle, loc, xylabel, - padding_factor) + segment_angle, loc, xylabel, padding_factor + ) artist.set_transform(transform) this_path = update_artist(artist, renderer) padding_factor += delta_padding_factor @@ -985,8 +1005,7 @@ def update_artist(artist, renderer): # Now check overlapping of ordered visible labels if self._labels: - self._labels.sort( - key=operator.attrgetter("priority"), reverse=True) + self._labels.sort(key=operator.attrgetter('priority'), reverse=True) visible_labels = [] for label in self._labels: if label.get_visible(): @@ -1010,8 +1029,7 @@ def _get_loc_from_angle(self, angle): loc = 'bottom' return loc - def _get_loc_from_spine_overlapping( - self, spines_specs, xylabel, label_path): + def _get_loc_from_spine_overlapping(self, spines_specs, xylabel, label_path): """Try to get the location from side spines and label path Returns None if it does not apply @@ -1044,7 +1062,7 @@ def _get_loc_from_spine_overlapping( side_max = side dist_max = dist if side_max is None: - return "geo" + return 'geo' return side_max def _get_loc_from_spine_intersection(self, spines_specs, xylabel, x, y): @@ -1052,16 +1070,16 @@ def _get_loc_from_spine_intersection(self, spines_specs, xylabel, x, y): Defaults to "geo". """ - if xylabel == "x": - sides = ["bottom", "top", "left", "right"] + if xylabel == 'x': + sides = ['bottom', 'top', 'left', 'right'] else: - sides = ["left", "right", "bottom", "top"] + sides = ['left', 'right', 'bottom', 'top'] for side in sides: - xy = x if side in ["left", "right"] else y - coords = np.round(spines_specs[side]["coords"], 2) + xy = x if side in ['left', 'right'] else y + coords = np.round(spines_specs[side]['coords'], 2) if round(xy, 2) in coords: return side - return "geo" + return 'geo' def _get_text_specs(self, angle, loc, xylabel): """Get rotation and alignments specs for a single label""" @@ -1071,14 +1089,13 @@ def _get_text_specs(self, angle, loc, xylabel): angle -= 360 # Fake for geo spine - if loc == "geo": + if loc == 'geo': loc = self._get_loc_from_angle(angle) # Check rotation if not self.rotate_labels: - # No rotation - kw = {'rotation': 0, "ha": "center", "va": "center"} + kw = {'rotation': 0, 'ha': 'center', 'va': 'center'} if loc == 'right': kw.update(ha='left') elif loc == 'left': @@ -1089,56 +1106,61 @@ def _get_text_specs(self, angle, loc, xylabel): kw.update(va='top') else: - # Rotation along gridlines - if (isinstance(self.rotate_labels, (float, int)) and - not isinstance(self.rotate_labels, bool)): + if isinstance(self.rotate_labels, (float, int)) and not isinstance( + self.rotate_labels, bool + ): angle = self.rotate_labels kw = {'rotation': angle, 'rotation_mode': 'anchor', 'va': 'center'} - if (angle < 90 + self.offset_angle and - angle > -90 + self.offset_angle): - kw.update(ha="left", rotation=angle) + if angle < 90 + self.offset_angle and angle > -90 + self.offset_angle: + kw.update(ha='left', rotation=angle) else: - kw.update(ha="right", rotation=angle + 180) + kw.update(ha='right', rotation=angle + 180) # Inside labels - if getattr(self, xylabel + "padding") < 0: - if "ha" in kw: - if kw["ha"] == "left": - kw["ha"] = "right" - elif kw["ha"] == "right": - kw["ha"] = "left" - if "va" in kw: - if kw["va"] == "top": - kw["va"] = "bottom" - elif kw["va"] == "bottom": - kw["va"] = "top" + if getattr(self, xylabel + 'padding') < 0: + if 'ha' in kw: + if kw['ha'] == 'left': + kw['ha'] = 'right' + elif kw['ha'] == 'right': + kw['ha'] = 'left' + if 'va' in kw: + if kw['va'] == 'top': + kw['va'] = 'bottom' + elif kw['va'] == 'bottom': + kw['va'] = 'top' return kw - def _get_padding_transform( - self, padding_angle, loc, xylabel, padding_factor=1): + def _get_padding_transform(self, padding_angle, loc, xylabel, padding_factor=1): """Get transform from angle and padding for non-inline labels""" # No rotation - if self.rotate_labels is False and loc != "geo": - padding_angle = { - 'top': 90., 'right': 0., 'bottom': -90., 'left': 180.}[loc] + if self.rotate_labels is False and loc != 'geo': + padding_angle = {'top': 90.0, 'right': 0.0, 'bottom': -90.0, 'left': 180.0}[ + loc + ] # Padding - if xylabel == "x": - padding = (self.xpadding if self.xpadding is not None - else matplotlib.rcParams['xtick.major.pad']) + if xylabel == 'x': + padding = ( + self.xpadding + if self.xpadding is not None + else matplotlib.rcParams['xtick.major.pad'] + ) else: - padding = (self.ypadding if self.ypadding is not None - else matplotlib.rcParams['ytick.major.pad']) + padding = ( + self.ypadding + if self.ypadding is not None + else matplotlib.rcParams['ytick.major.pad'] + ) dx = padding_factor * padding * np.cos(padding_angle * np.pi / 180) dy = padding_factor * padding * np.sin(padding_angle * np.pi / 180) # Final transform return mtrans.offset_copy( - self.axes.transData, fig=self.axes.figure, - x=dx, y=dy, units='points') + self.axes.transData, fig=self.axes.figure, x=dx, y=dy, units='points' + ) def _assert_can_draw_ticks(self): """ @@ -1148,9 +1170,11 @@ def _assert_can_draw_ticks(self): """ # Check labelling is supported, currently a limited set of options. if not isinstance(self.crs, PlateCarree): - raise TypeError(f'Cannot label {self.crs.__class__.__name__} ' - 'gridlines. Only PlateCarree gridlines are ' - 'currently supported.') + raise TypeError( + f'Cannot label {self.crs.__class__.__name__} ' + 'gridlines. Only PlateCarree gridlines are ' + 'currently supported.' + ) return True def _axes_domain(self, nx=None, ny=None): @@ -1181,10 +1205,21 @@ def _axes_domain(self, nx=None, ny=None): if DEBUG: import matplotlib.pyplot as plt - plt.plot(coords[ok, 0], coords[ok, 1], 'or', - clip_on=False, transform=ax_transform) - plt.plot(coords[~ok, 0], coords[~ok, 1], 'ob', - clip_on=False, transform=ax_transform) + + plt.plot( + coords[ok, 0], + coords[ok, 1], + 'or', + clip_on=False, + transform=ax_transform, + ) + plt.plot( + coords[~ok, 0], + coords[~ok, 1], + 'ob', + clip_on=False, + transform=ax_transform, + ) inside = in_data[ok, :] @@ -1196,8 +1231,7 @@ def _axes_domain(self, nx=None, ny=None): else: # np.isfinite must be used to prevent np.inf values that # not filtered by np.nanmax for some projections - lat_max = np.compress(np.isfinite(inside[:, 1]), - inside[:, 1]) + lat_max = np.compress(np.isfinite(inside[:, 1]), inside[:, 1]) if lat_max.size == 0: lon_range = self.crs.x_limits lat_range = self.crs.y_limits @@ -1239,14 +1273,14 @@ def _axes_domain(self, nx=None, ny=None): def get_visible_children(self): r"""Return a list of the visible child `.Artist`\s.""" - all_children = (self.xline_artists + self.yline_artists - + self.label_artists) + all_children = self.xline_artists + self.yline_artists + self.label_artists return [c for c in all_children if c.get_visible()] def get_tightbbox(self, renderer=None): self._draw_gridliner(renderer=renderer) - bboxes = [c.get_tightbbox(renderer=renderer) - for c in self.get_visible_children()] + bboxes = [ + c.get_tightbbox(renderer=renderer) for c in self.get_visible_children() + ] if bboxes: return mtrans.Bbox.union(bboxes) else: @@ -1262,7 +1296,6 @@ class Label: """Helper class to manage the attributes for a single label""" def __init__(self, artist, path, xy, loc): - self.artist = artist self.loc = loc self.path = path @@ -1270,7 +1303,7 @@ def __init__(self, artist, path, xy, loc): @property def priority(self): - return self.loc in ["left", "right", "top", "bottom"] + return self.loc in ['left', 'right', 'top', 'bottom'] def set_visible(self, value): self.artist.set_visible(value) diff --git a/lib/cartopy/mpl/patch.py b/lib/cartopy/mpl/patch.py index 3ef107067..cfdfb4212 100644 --- a/lib/cartopy/mpl/patch.py +++ b/lib/cartopy/mpl/patch.py @@ -49,9 +49,12 @@ def geos_to_path(shape): A list of :class:`matplotlib.path.Path` objects. """ - warnings.warn("geos_to_path is deprecated and will be removed in a future release." - " Use cartopy.mpl.path.shapely_to_path instead.", - DeprecationWarning, stacklevel=2) + warnings.warn( + 'geos_to_path is deprecated and will be removed in a future release.' + ' Use cartopy.mpl.path.shapely_to_path instead.', + DeprecationWarning, + stacklevel=2, + ) if isinstance(shape, (list, tuple)): paths = [] for shp in shape: @@ -63,21 +66,34 @@ def geos_to_path(shape): elif isinstance(shape, (sgeom.LineString, sgeom.Point)): return [Path(np.column_stack(shape.xy))] elif isinstance(shape, sgeom.Polygon): + def poly_codes(poly): codes = np.ones(len(poly.xy[0])) * Path.LINETO codes[0] = Path.MOVETO codes[-1] = Path.CLOSEPOLY return codes + if shape.is_empty: return [] - vertices = np.concatenate([np.array(shape.exterior.xy)] + - [np.array(ring.xy) for ring in - shape.interiors], 1).T - codes = np.concatenate([poly_codes(shape.exterior)] + - [poly_codes(ring) for ring in shape.interiors]) + vertices = np.concatenate( + [np.array(shape.exterior.xy)] + + [np.array(ring.xy) for ring in shape.interiors], + 1, + ).T + codes = np.concatenate( + [poly_codes(shape.exterior)] + + [poly_codes(ring) for ring in shape.interiors] + ) return [Path(vertices, codes)] - elif isinstance(shape, (sgeom.MultiPolygon, sgeom.GeometryCollection, - sgeom.MultiLineString, sgeom.MultiPoint)): + elif isinstance( + shape, + ( + sgeom.MultiPolygon, + sgeom.GeometryCollection, + sgeom.MultiLineString, + sgeom.MultiPoint, + ), + ): paths = [] for geom in shape.geoms: paths.extend(geos_to_path(geom)) @@ -117,8 +133,10 @@ def path_segments(path, **kwargs): """ warnings.warn( - "path_segments is deprecated and will be removed in a future release.", - DeprecationWarning, stacklevel=2) + 'path_segments is deprecated and will be removed in a future release.', + DeprecationWarning, + stacklevel=2, + ) return cpath._path_segments(path, **kwargs) @@ -149,9 +167,12 @@ def path_to_geos(path, force_ccw=False): :class:`shapely.geometry.multilinestring.MultiLineString`. """ - warnings.warn("path_to_geos is deprecated and will be removed in a future release." - " Use cartopy.mpl.path.path_to_shapely instead.", - DeprecationWarning, stacklevel=2) + warnings.warn( + 'path_to_geos is deprecated and will be removed in a future release.' + ' Use cartopy.mpl.path.path_to_shapely instead.', + DeprecationWarning, + stacklevel=2, + ) # Convert path into numpy array of vertices (and associated codes) path_verts, path_codes = cpath._path_segments(path, curves=False) @@ -173,10 +194,10 @@ def path_to_geos(path, force_ccw=False): if path_codes[-1] == Path.CLOSEPOLY: path_verts[-1, :] = path_verts[0, :] - verts_same_as_first = np.isclose(path_verts[0, :], path_verts[1:, :], - rtol=1e-10, atol=1e-13) - verts_same_as_first = np.logical_and.reduce(verts_same_as_first, - axis=1) + verts_same_as_first = np.isclose( + path_verts[0, :], path_verts[1:, :], rtol=1e-10, atol=1e-13 + ) + verts_same_as_first = np.logical_and.reduce(verts_same_as_first, axis=1) if all(verts_same_as_first): geom = sgeom.Point(path_verts[0, :]) @@ -192,10 +213,12 @@ def path_to_geos(path, force_ccw=False): # contours). This needs to be a new external geom in the collection. if geom.is_empty: pass - elif (len(collection) > 0 and - isinstance(collection[-1][0], sgeom.Polygon) and - isinstance(geom, sgeom.Polygon) and - collection[-1][0].contains(geom.exterior)): + elif ( + len(collection) > 0 + and isinstance(collection[-1][0], sgeom.Polygon) + and isinstance(geom, sgeom.Polygon) + and collection[-1][0].contains(geom.exterior) + ): if any(internal.contains(geom) for internal in collection[-1][1]): collection.append((geom, [])) else: @@ -225,15 +248,16 @@ def path_to_geos(path, force_ccw=False): # If the geom_collection only contains LineStrings combine them # into a single MultiLinestring. - if geom_collection and all(isinstance(geom, sgeom.LineString) for - geom in geom_collection): + if geom_collection and all( + isinstance(geom, sgeom.LineString) for geom in geom_collection + ): geom_collection = [sgeom.MultiLineString(geom_collection)] # Remove any zero area Polygons def not_zero_poly(geom): - return ((isinstance(geom, sgeom.Polygon) and not geom.is_empty and - geom.area != 0) or - not isinstance(geom, sgeom.Polygon)) + return ( + isinstance(geom, sgeom.Polygon) and not geom.is_empty and geom.area != 0 + ) or not isinstance(geom, sgeom.Polygon) result = list(filter(not_zero_poly, geom_collection)) diff --git a/lib/cartopy/mpl/path.py b/lib/cartopy/mpl/path.py index 97e7f8ebe..5d0091e23 100644 --- a/lib/cartopy/mpl/path.py +++ b/lib/cartopy/mpl/path.py @@ -46,7 +46,7 @@ def _ensure_path_closed(path): codes, vertices = [], [] for poly in polygons: vertices.extend([poly[0], *poly]) - codes.extend([Path.MOVETO, *[Path.LINETO]*(len(poly) - 1), Path.CLOSEPOLY]) + codes.extend([Path.MOVETO, *[Path.LINETO] * (len(poly) - 1), Path.CLOSEPOLY]) return Path(vertices, codes) @@ -109,19 +109,32 @@ def shapely_to_path(shape): elif isinstance(shape, (sgeom.LineString, sgeom.Point)): return Path(np.column_stack(shape.xy)) elif isinstance(shape, sgeom.Polygon): + def poly_codes(poly): codes = np.ones(len(poly.xy[0])) * Path.LINETO codes[0] = Path.MOVETO codes[-1] = Path.CLOSEPOLY return codes - vertices = np.concatenate([np.array(shape.exterior.xy)] + - [np.array(ring.xy) for ring in - shape.interiors], 1).T - codes = np.concatenate([poly_codes(shape.exterior)] + - [poly_codes(ring) for ring in shape.interiors]) + + vertices = np.concatenate( + [np.array(shape.exterior.xy)] + + [np.array(ring.xy) for ring in shape.interiors], + 1, + ).T + codes = np.concatenate( + [poly_codes(shape.exterior)] + + [poly_codes(ring) for ring in shape.interiors] + ) return Path(vertices, codes) - elif isinstance(shape, (sgeom.MultiPolygon, sgeom.GeometryCollection, - sgeom.MultiLineString, sgeom.MultiPoint)): + elif isinstance( + shape, + ( + sgeom.MultiPolygon, + sgeom.GeometryCollection, + sgeom.MultiLineString, + sgeom.MultiPoint, + ), + ): paths = [] for geom in shape.geoms: path = shapely_to_path(geom) @@ -179,14 +192,14 @@ def path_to_shapely(path): if path_codes[-1] == Path.CLOSEPOLY: path_verts[-1, :] = path_verts[0, :] - verts_same_as_first = np.isclose(path_verts[0, :], path_verts[1:, :], - rtol=1e-10, atol=1e-13) - verts_same_as_first = np.logical_and.reduce(verts_same_as_first, - axis=1) + verts_same_as_first = np.isclose( + path_verts[0, :], path_verts[1:, :], rtol=1e-10, atol=1e-13 + ) + verts_same_as_first = np.logical_and.reduce(verts_same_as_first, axis=1) if all(verts_same_as_first): points.append(sgeom.Point(path_verts[0, :])) - elif not(path_verts.shape[0] > 4 and path_codes[-1] == Path.CLOSEPOLY): + elif not (path_verts.shape[0] > 4 and path_codes[-1] == Path.CLOSEPOLY): linestrings.append(sgeom.LineString(path_verts)) else: geom = sgeom.Polygon(path_verts[:-1, :]) @@ -195,7 +208,7 @@ def path_to_shapely(path): # lake within a land mass). Sometimes there is a further geom within # this interior (e.g. an island in a lake, or some instances of # contours). This needs to be a new external geom in polygon_bits. - if (len(polygon_bits) > 0 and polygon_bits[-1][0].contains(geom.exterior)): + if len(polygon_bits) > 0 and polygon_bits[-1][0].contains(geom.exterior): if any(internal.contains(geom) for internal in polygon_bits[-1][1]): polygon_bits.append((geom, [])) else: @@ -217,7 +230,7 @@ def path_to_shapely(path): # Remove any zero area Polygons def not_zero_poly(geom): - return (not geom.is_empty and geom.area != 0) + return not geom.is_empty and geom.area != 0 polygons = list(filter(not_zero_poly, polygons)) diff --git a/lib/cartopy/mpl/slippy_image_artist.py b/lib/cartopy/mpl/slippy_image_artist.py index 494e13c2c..04f9d4827 100644 --- a/lib/cartopy/mpl/slippy_image_artist.py +++ b/lib/cartopy/mpl/slippy_image_artist.py @@ -14,7 +14,6 @@ class SlippyImageArtist(AxesImage): - """ A subclass of :class:`~matplotlib.image.AxesImage` which provides an interface for getting a raster from the given object with interactive @@ -56,8 +55,10 @@ def draw(self, renderer, *args, **kwargs): [x1, y1], [x2, y2] = ax.viewLim.get_points() if not self.user_is_interacting: located_images = self.raster_source.fetch_raster( - ax.projection, extent=[x1, x2, y1, y2], - target_resolution=(window_extent.width, window_extent.height)) + ax.projection, + extent=[x1, x2, y1, y2], + target_resolution=(window_extent.width, window_extent.height), + ) self.cache = located_images for img, extent in self.cache: diff --git a/lib/cartopy/mpl/style.py b/lib/cartopy/mpl/style.py index 5229a7a05..2c1d86c10 100644 --- a/lib/cartopy/mpl/style.py +++ b/lib/cartopy/mpl/style.py @@ -11,8 +11,11 @@ import warnings -warnings.warn('The style module is deprecated and will be removed in a future release.', - DeprecationWarning, stacklevel=2) +warnings.warn( + 'The style module is deprecated and will be removed in a future release.', + DeprecationWarning, + stacklevel=2, +) # Define the matplotlib style aliases that cartopy can expand. @@ -72,12 +75,12 @@ def merge(*style_dicts): if isinstance(facecolor, str) and facecolor == 'never': requested_color = this_style.pop('facecolor', None) setting_color = not ( - isinstance(requested_color, str) and - requested_color.lower() == 'none') - if (('fc' in orig_style or 'facecolor' in orig_style) and - setting_color): - warnings.warn('facecolor will have no effect as it has been ' - 'defined as "never".') + isinstance(requested_color, str) and requested_color.lower() == 'none' + ) + if ('fc' in orig_style or 'facecolor' in orig_style) and setting_color: + warnings.warn( + 'facecolor will have no effect as it has been defined as "never".' + ) else: facecolor = this_style.get('facecolor', facecolor) diff --git a/lib/cartopy/mpl/ticker.py b/lib/cartopy/mpl/ticker.py index cdc2fa4a1..35db46998 100644 --- a/lib/cartopy/mpl/ticker.py +++ b/lib/cartopy/mpl/ticker.py @@ -18,11 +18,21 @@ class _PlateCarreeFormatter(Formatter): rectangular projection (e.g. Plate Carree, Mercator). """ - def __init__(self, direction_label=True, degree_symbol='°', - number_format='g', transform_precision=1e-8, dms=False, - minute_symbol='′', second_symbol='″', - seconds_number_format='g', - auto_hide=True, decimal_point=None, cardinal_labels=None): + + def __init__( + self, + direction_label=True, + degree_symbol='°', + number_format='g', + transform_precision=1e-8, + dms=False, + minute_symbol='′', + second_symbol='″', + seconds_number_format='g', + auto_hide=True, + decimal_point=None, + cardinal_labels=None, + ): """ Base class for simpler implementation of specialised formatters for latitude and longitude axes. @@ -40,10 +50,10 @@ def __init__(self, direction_label=True, degree_symbol='°', self._auto_hide_degrees = False self._auto_hide_minutes = False self._precision = 5 # locator precision - if (decimal_point is None and - mpl.rcParams['axes.formatter.use_locale']): + if decimal_point is None and mpl.rcParams['axes.formatter.use_locale']: import locale - decimal_point = locale.localeconv()["decimal_point"] + + decimal_point = locale.localeconv()['decimal_point'] if cardinal_labels is None: cardinal_labels = {} self._cardinal_labels = cardinal_labels @@ -53,18 +63,17 @@ def __init__(self, direction_label=True, degree_symbol='°', def __call__(self, value, pos=None): if self._source_projection is not None: - projected_value = self._apply_transform(value, - self._target_projection, - self._source_projection) + projected_value = self._apply_transform( + value, self._target_projection, self._source_projection + ) # Round the transformed value using a given precision for display # purposes. Transforms can introduce minor rounding errors that # make the tick values look bad, these need to be accounted for. - f = 1. / self._transform_precision + f = 1.0 / self._transform_precision projected_value = round(f * projected_value) / f else: - # There is no projection so we assume it is already PlateCarree projected_value = value @@ -73,20 +82,17 @@ def __call__(self, value, pos=None): return self._format_value(projected_value, value) def _format_value(self, value, original_value): - hemisphere = '' sign = '' if self._direction_labels: hemisphere = self._hemisphere(value, original_value) else: - if (value != 0 and - self._hemisphere(value, original_value) in ['W', 'S']): + if value != 0 and self._hemisphere(value, original_value) in ['W', 'S']: sign = '-' if not self._dms: - return (sign + self._format_degrees(abs(value)) + - hemisphere) + return sign + self._format_degrees(abs(value)) + hemisphere value, deg, mn, sec = self._get_dms(abs(value)) @@ -137,10 +143,12 @@ def set_axis(self, axis): return self._source_projection = self.axis.axes.projection - if not isinstance(self._source_projection, (ccrs._RectangularProjection, - ccrs.Mercator)): - raise TypeError("This formatter cannot be used with " - "non-rectangular projections.") + if not isinstance( + self._source_projection, (ccrs._RectangularProjection, ccrs.Mercator) + ): + raise TypeError( + 'This formatter cannot be used with non-rectangular projections.' + ) # The transforms need to use the same globe self._target_projection = ccrs.PlateCarree(globe=self._source_projection.globe) @@ -174,9 +182,9 @@ def _format_degrees(self, deg): number_format = 'd' else: number_format = self._degrees_number_format - value = f"{abs(deg):{number_format}}{self._degree_symbol}" + value = f'{abs(deg):{number_format}}{self._degree_symbol}' if self._decimal_point is not None: - value = value.replace(".", self._decimal_point) + value = value.replace('.', self._decimal_point) return value def _format_minutes(self, mn): @@ -194,7 +202,7 @@ def _apply_transform(self, value, target_proj, source_crs): projection, returning a single value. """ - raise NotImplementedError("A subclass must implement this method.") + raise NotImplementedError('A subclass must implement this method.') def _hemisphere(self, value, value_source_crs): """ @@ -205,19 +213,26 @@ def _hemisphere(self, value, value_source_crs): Must be over-ridden by the derived class. """ - raise NotImplementedError("A subclass must implement this method.") + raise NotImplementedError('A subclass must implement this method.') class LatitudeFormatter(_PlateCarreeFormatter): """Tick formatter for latitude axes.""" - def __init__(self, direction_label=True, - degree_symbol='°', number_format='g', - transform_precision=1e-8, dms=False, - minute_symbol='′', second_symbol='″', - seconds_number_format='g', auto_hide=True, - decimal_point=None, cardinal_labels=None - ): + def __init__( + self, + direction_label=True, + degree_symbol='°', + number_format='g', + transform_precision=1e-8, + dms=False, + minute_symbol='′', + second_symbol='″', + seconds_number_format='g', + auto_hide=True, + decimal_point=None, + cardinal_labels=None, + ): """ Tick formatter for latitudes. @@ -309,14 +324,13 @@ def __init__(self, direction_label=True, seconds_number_format=seconds_number_format, auto_hide=auto_hide, decimal_point=decimal_point, - cardinal_labels=cardinal_labels + cardinal_labels=cardinal_labels, ) def _apply_transform(self, value, target_proj, source_crs): return target_proj.transform_point(0, value, source_crs)[1] def _hemisphere(self, value, value_source_crs): - if value > 0: hemisphere = self._cardinal_labels.get('north', 'N') elif value < 0: @@ -329,21 +343,22 @@ def _hemisphere(self, value, value_source_crs): class LongitudeFormatter(_PlateCarreeFormatter): """Tick formatter for a longitude axis.""" - def __init__(self, - direction_label=True, - zero_direction_label=False, - dateline_direction_label=False, - degree_symbol='°', - number_format='g', - transform_precision=1e-8, - dms=False, - minute_symbol='′', - second_symbol='″', - seconds_number_format='g', - auto_hide=True, - decimal_point=None, - cardinal_labels=None - ): + def __init__( + self, + direction_label=True, + zero_direction_label=False, + dateline_direction_label=False, + degree_symbol='°', + number_format='g', + transform_precision=1e-8, + dms=False, + minute_symbol='′', + second_symbol='″', + seconds_number_format='g', + auto_hide=True, + decimal_point=None, + cardinal_labels=None, + ): """ Create a formatter for longitudes. @@ -443,7 +458,7 @@ def __init__(self, seconds_number_format=seconds_number_format, auto_hide=auto_hide, decimal_point=decimal_point, - cardinal_labels=cardinal_labels + cardinal_labels=cardinal_labels, ) self._zero_direction_labels = zero_direction_label self._dateline_direction_labels = dateline_direction_label @@ -478,7 +493,6 @@ def _format_degrees(self, deg): return _PlateCarreeFormatter._format_degrees(self, self._fix_lons(deg)) def _hemisphere(self, value, value_source_crs): - value = self._fix_lons(value) # Perform basic hemisphere detection. if value < 0: @@ -519,21 +533,18 @@ def set_params(self, **kwargs): MaxNLocator.set_params(self, **kwargs) def _guess_steps(self, vmin, vmax): - dv = abs(vmax - vmin) if dv > 180: dv -= 180 - if dv > 50.: - + if dv > 50.0: steps = np.array([1, 2, 3, 6, 10]) - elif not self._dms or dv > 3.: - + elif not self._dms or dv > 3.0: steps = np.array([1, 1.5, 2, 2.5, 3, 5, 10]) else: - steps = np.array([1, 10 / 6., 15 / 6., 20 / 6., 30 / 6., 10]) + steps = np.array([1, 10 / 6.0, 15 / 6.0, 20 / 6.0, 30 / 6.0, 10]) self.set_params(steps=np.array(steps)) @@ -557,13 +568,13 @@ class LatitudeLocator(LongitudeLocator): """ def tick_values(self, vmin, vmax): - vmin = max(vmin, -90.) - vmax = min(vmax, 90.) + vmin = max(vmin, -90.0) + vmax = min(vmax, 90.0) return LongitudeLocator.tick_values(self, vmin, vmax) def _guess_steps(self, vmin, vmax): - vmin = max(vmin, -90.) - vmax = min(vmax, 90.) + vmin = max(vmin, -90.0) + vmax = min(vmax, 90.0) LongitudeLocator._guess_steps(self, vmin, vmax) def _raw_ticks(self, vmin, vmax): diff --git a/lib/cartopy/tests/conftest.py b/lib/cartopy/tests/conftest.py index 99e78ae3b..32d47b789 100644 --- a/lib/cartopy/tests/conftest.py +++ b/lib/cartopy/tests/conftest.py @@ -18,19 +18,16 @@ except ImportError: _HAS_SCIPY = False -requires_scipy = pytest.mark.skipif( - not _HAS_SCIPY, - reason="scipy is required") -requires_pykdtree = pytest.mark.skipif( - not _HAS_PYKDTREE, - reason="pykdtree is required") +requires_scipy = pytest.mark.skipif(not _HAS_SCIPY, reason='scipy is required') +requires_pykdtree = pytest.mark.skipif(not _HAS_PYKDTREE, reason='pykdtree is required') _HAS_PYKDTREE_OR_SCIPY = _HAS_PYKDTREE or _HAS_SCIPY def pytest_configure(config): # Register additional markers. - config.addinivalue_line('markers', - 'natural_earth: mark tests that use Natural Earth ' - 'data, and the network, if not cached.') - config.addinivalue_line('markers', - 'network: mark tests that use the network.') + config.addinivalue_line( + 'markers', + 'natural_earth: mark tests that use Natural Earth ' + 'data, and the network, if not cached.', + ) + config.addinivalue_line('markers', 'network: mark tests that use the network.') diff --git a/lib/cartopy/tests/crs/test_aitoff.py b/lib/cartopy/tests/crs/test_aitoff.py index 2048a7a15..075a3ffe0 100644 --- a/lib/cartopy/tests/crs/test_aitoff.py +++ b/lib/cartopy/tests/crs/test_aitoff.py @@ -36,8 +36,7 @@ def test_sphere_globe(): def test_ellipse_globe(): globe = ccrs.Globe(ellipse='WGS84') - with pytest.warns(UserWarning, - match='does not handle elliptical globes.') as w: + with pytest.warns(UserWarning, match='does not handle elliptical globes.') as w: aitoff = ccrs.Aitoff(globe=globe) assert len(w) == 1 @@ -50,10 +49,8 @@ def test_ellipse_globe(): def test_eccentric_globe(): - globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, - ellipse=None) - with pytest.warns(UserWarning, - match='does not handle elliptical globes.') as w: + globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, ellipse=None) + with pytest.warns(UserWarning, match='does not handle elliptical globes.') as w: aitoff = ccrs.Aitoff(globe=globe) assert len(w) == 1 @@ -80,6 +77,7 @@ def test_central_longitude(lon): other_args = {'a=6378137.0', 'lon_0={}'.format(lon)} check_proj_params('aitoff', aitoff, other_args) - assert_almost_equal(aitoff.x_limits, [-20037508.3427892, 20037508.3427892], - decimal=5) + assert_almost_equal( + aitoff.x_limits, [-20037508.3427892, 20037508.3427892], decimal=5 + ) assert_almost_equal(aitoff.y_limits, [-10018754.1713946, 10018754.1713946]) diff --git a/lib/cartopy/tests/crs/test_albers_equal_area.py b/lib/cartopy/tests/crs/test_albers_equal_area.py index 9961605fe..2a7e5e774 100644 --- a/lib/cartopy/tests/crs/test_albers_equal_area.py +++ b/lib/cartopy/tests/crs/test_albers_equal_area.py @@ -18,46 +18,73 @@ class TestAlbersEqualArea: def test_default(self): aea = ccrs.AlbersEqualArea() - other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', - 'y_0=0.0', 'lat_1=20.0', 'lat_2=50.0'} + other_args = { + 'ellps=WGS84', + 'lon_0=0.0', + 'lat_0=0.0', + 'x_0=0.0', + 'y_0=0.0', + 'lat_1=20.0', + 'lat_2=50.0', + } check_proj_params('aea', aea, other_args) - assert_almost_equal(np.array(aea.x_limits), - [-17702759.799178038, 17702759.799178038], - decimal=0) - assert_almost_equal(np.array(aea.y_limits), - [-4782937.05107294, 15922623.93176938], - decimal=4) + assert_almost_equal( + np.array(aea.x_limits), [-17702759.799178038, 17702759.799178038], decimal=0 + ) + assert_almost_equal( + np.array(aea.y_limits), [-4782937.05107294, 15922623.93176938], decimal=4 + ) def test_eccentric_globe(self): - globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, - ellipse=None) + globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, ellipse=None) aea = ccrs.AlbersEqualArea(globe=globe) - other_args = {'a=1000', 'b=500', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', - 'y_0=0.0', 'lat_1=20.0', 'lat_2=50.0'} + other_args = { + 'a=1000', + 'b=500', + 'lon_0=0.0', + 'lat_0=0.0', + 'x_0=0.0', + 'y_0=0.0', + 'lat_1=20.0', + 'lat_2=50.0', + } check_proj_params('aea', aea, other_args) - assert_almost_equal(np.array(aea.x_limits), - [-2323.47073363411, 2323.47073363411], - decimal=-2) - assert_almost_equal(np.array(aea.y_limits), - [-572.556243423972, 2402.36176984391], - decimal=10) + assert_almost_equal( + np.array(aea.x_limits), [-2323.47073363411, 2323.47073363411], decimal=-2 + ) + assert_almost_equal( + np.array(aea.y_limits), [-572.556243423972, 2402.36176984391], decimal=10 + ) def test_eastings(self): - aea_offset = ccrs.AlbersEqualArea(false_easting=1234, - false_northing=-4321) - - other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=1234', - 'y_0=-4321', 'lat_1=20.0', 'lat_2=50.0'} + aea_offset = ccrs.AlbersEqualArea(false_easting=1234, false_northing=-4321) + + other_args = { + 'ellps=WGS84', + 'lon_0=0.0', + 'lat_0=0.0', + 'x_0=1234', + 'y_0=-4321', + 'lat_1=20.0', + 'lat_2=50.0', + } check_proj_params('aea', aea_offset, other_args) @pytest.mark.parametrize('lon', [-10.0, 10.0]) def test_central_longitude(self, lon): aea = ccrs.AlbersEqualArea() aea_offset = ccrs.AlbersEqualArea(central_longitude=lon) - other_args = {'ellps=WGS84', f'lon_0={lon}', 'lat_0=0.0', - 'x_0=0.0', 'y_0=0.0', 'lat_1=20.0', 'lat_2=50.0'} + other_args = { + 'ellps=WGS84', + f'lon_0={lon}', + 'lat_0=0.0', + 'x_0=0.0', + 'y_0=0.0', + 'lat_1=20.0', + 'lat_2=50.0', + } check_proj_params('aea', aea_offset, other_args) assert_array_almost_equal( @@ -68,42 +95,70 @@ def test_central_longitude(self, lon): def test_standard_parallels(self): aea = ccrs.AlbersEqualArea(standard_parallels=(13, 37)) - other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', - 'y_0=0.0', 'lat_1=13', 'lat_2=37'} + other_args = { + 'ellps=WGS84', + 'lon_0=0.0', + 'lat_0=0.0', + 'x_0=0.0', + 'y_0=0.0', + 'lat_1=13', + 'lat_2=37', + } check_proj_params('aea', aea, other_args) - aea = ccrs.AlbersEqualArea(standard_parallels=(13, )) - other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', - 'y_0=0.0', 'lat_1=13'} + aea = ccrs.AlbersEqualArea(standard_parallels=(13,)) + other_args = { + 'ellps=WGS84', + 'lon_0=0.0', + 'lat_0=0.0', + 'x_0=0.0', + 'y_0=0.0', + 'lat_1=13', + } check_proj_params('aea', aea, other_args) aea = ccrs.AlbersEqualArea(standard_parallels=13) - other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', - 'y_0=0.0', 'lat_1=13'} + other_args = { + 'ellps=WGS84', + 'lon_0=0.0', + 'lat_0=0.0', + 'x_0=0.0', + 'y_0=0.0', + 'lat_1=13', + } check_proj_params('aea', aea, other_args) def test_sphere_transform(self): # USGS Professional Paper 1395, pg 291 - globe = ccrs.Globe(semimajor_axis=1.0, semiminor_axis=1.0, - ellipse=None) + globe = ccrs.Globe(semimajor_axis=1.0, semiminor_axis=1.0, ellipse=None) lat_1 = 29 + 30 / 60 lat_2 = 45 + 30 / 60 - aea = ccrs.AlbersEqualArea(central_latitude=23.0, - central_longitude=-96.0, - standard_parallels=(lat_1, lat_2), - globe=globe) + aea = ccrs.AlbersEqualArea( + central_latitude=23.0, + central_longitude=-96.0, + standard_parallels=(lat_1, lat_2), + globe=globe, + ) geodetic = aea.as_geodetic() - other_args = {'a=1.0', 'b=1.0', 'lon_0=-96.0', 'lat_0=23.0', 'x_0=0.0', - 'y_0=0.0', 'lat_1=29.5', 'lat_2=45.5'} + other_args = { + 'a=1.0', + 'b=1.0', + 'lon_0=-96.0', + 'lat_0=23.0', + 'x_0=0.0', + 'y_0=0.0', + 'lat_1=29.5', + 'lat_2=45.5', + } check_proj_params('aea', aea, other_args) - assert_almost_equal(np.array(aea.x_limits), - [-2.6525072042232, 2.6525072042232], - decimal=3) - assert_almost_equal(np.array(aea.y_limits), - [-1.09628087472359, 2.39834724057551], - decimal=10) + assert_almost_equal( + np.array(aea.x_limits), [-2.6525072042232, 2.6525072042232], decimal=3 + ) + assert_almost_equal( + np.array(aea.y_limits), [-1.09628087472359, 2.39834724057551], decimal=10 + ) result = aea.transform_point(-75.0, 35.0, geodetic) @@ -111,28 +166,39 @@ def test_sphere_transform(self): def test_ellipsoid_transform(self): # USGS Professional Paper 1395, pp 292 -- 293 - globe = ccrs.Globe(semimajor_axis=6378206.4, - flattening=1 - np.sqrt(1 - 0.00676866), - ellipse=None) + globe = ccrs.Globe( + semimajor_axis=6378206.4, + flattening=1 - np.sqrt(1 - 0.00676866), + ellipse=None, + ) lat_1 = 29 + 30 / 60 lat_2 = 45 + 30 / 60 - aea = ccrs.AlbersEqualArea(central_latitude=23.0, - central_longitude=-96.0, - standard_parallels=(lat_1, lat_2), - globe=globe) + aea = ccrs.AlbersEqualArea( + central_latitude=23.0, + central_longitude=-96.0, + standard_parallels=(lat_1, lat_2), + globe=globe, + ) geodetic = aea.as_geodetic() - other_args = {'a=6378206.4', 'f=0.003390076308689371', 'lon_0=-96.0', - 'lat_0=23.0', 'x_0=0.0', 'y_0=0.0', 'lat_1=29.5', - 'lat_2=45.5'} + other_args = { + 'a=6378206.4', + 'f=0.003390076308689371', + 'lon_0=-96.0', + 'lat_0=23.0', + 'x_0=0.0', + 'y_0=0.0', + 'lat_1=29.5', + 'lat_2=45.5', + } check_proj_params('aea', aea, other_args) - assert_almost_equal(np.array(aea.x_limits), - [-16900972.674607, 16900972.674607], - decimal=-3) - assert_almost_equal(np.array(aea.y_limits), - [-6971893.11311231, 15298166.8919989], - decimal=1) + assert_almost_equal( + np.array(aea.x_limits), [-16900972.674607, 16900972.674607], decimal=-3 + ) + assert_almost_equal( + np.array(aea.y_limits), [-6971893.11311231, 15298166.8919989], decimal=1 + ) result = aea.transform_point(-75.0, 35.0, geodetic) diff --git a/lib/cartopy/tests/crs/test_azimuthal_equidistant.py b/lib/cartopy/tests/crs/test_azimuthal_equidistant.py index a2c46615a..8f095f0fc 100644 --- a/lib/cartopy/tests/crs/test_azimuthal_equidistant.py +++ b/lib/cartopy/tests/crs/test_azimuthal_equidistant.py @@ -13,127 +13,345 @@ class TestAzimuthalEquidistant: def test_default(self): aeqd = ccrs.AzimuthalEquidistant() - other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', - 'y_0=0.0'} + other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', 'y_0=0.0'} check_proj_params('aeqd', aeqd, other_args) - assert_almost_equal(np.array(aeqd.x_limits), - [-20037508.34278924, 20037508.34278924], decimal=6) - assert_almost_equal(np.array(aeqd.y_limits), - [-19970326.371123, 19970326.371123], decimal=6) + assert_almost_equal( + np.array(aeqd.x_limits), [-20037508.34278924, 20037508.34278924], decimal=6 + ) + assert_almost_equal( + np.array(aeqd.y_limits), [-19970326.371123, 19970326.371123], decimal=6 + ) def test_eccentric_globe(self): - globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, - ellipse=None) + globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, ellipse=None) aeqd = ccrs.AzimuthalEquidistant(globe=globe) - other_args = {'a=1000', 'b=500', 'lon_0=0.0', 'lat_0=0.0', - 'x_0=0.0', 'y_0=0.0'} + other_args = {'a=1000', 'b=500', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', 'y_0=0.0'} check_proj_params('aeqd', aeqd, other_args) - assert_almost_equal(np.array(aeqd.x_limits), - [-3141.59265359, 3141.59265359], decimal=6) - assert_almost_equal(np.array(aeqd.y_limits), - [-1570.796326795, 1570.796326795], decimal=6) + assert_almost_equal( + np.array(aeqd.x_limits), [-3141.59265359, 3141.59265359], decimal=6 + ) + assert_almost_equal( + np.array(aeqd.y_limits), [-1570.796326795, 1570.796326795], decimal=6 + ) def test_eastings(self): - aeqd_offset = ccrs.AzimuthalEquidistant(false_easting=1234, - false_northing=-4321) + aeqd_offset = ccrs.AzimuthalEquidistant( + false_easting=1234, false_northing=-4321 + ) - other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=1234', - 'y_0=-4321'} + other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=1234', 'y_0=-4321'} check_proj_params('aeqd', aeqd_offset, other_args) - assert_almost_equal(np.array(aeqd_offset.x_limits), - [-20036274.34278924, 20038742.34278924], decimal=6) - assert_almost_equal(np.array(aeqd_offset.y_limits), - [-19974647.371123, 19966005.371123], decimal=6) + assert_almost_equal( + np.array(aeqd_offset.x_limits), + [-20036274.34278924, 20038742.34278924], + decimal=6, + ) + assert_almost_equal( + np.array(aeqd_offset.y_limits), + [-19974647.371123, 19966005.371123], + decimal=6, + ) def test_grid(self): # USGS Professional Paper 1395, pp 196--197, Table 30 - globe = ccrs.Globe(ellipse=None, - semimajor_axis=1.0, semiminor_axis=1.0) - aeqd = ccrs.AzimuthalEquidistant(central_latitude=0.0, - central_longitude=0.0, - globe=globe) + globe = ccrs.Globe(ellipse=None, semimajor_axis=1.0, semiminor_axis=1.0) + aeqd = ccrs.AzimuthalEquidistant( + central_latitude=0.0, central_longitude=0.0, globe=globe + ) geodetic = aeqd.as_geodetic() - other_args = {'a=1.0', 'b=1.0', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', - 'y_0=0.0'} + other_args = {'a=1.0', 'b=1.0', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', 'y_0=0.0'} check_proj_params('aeqd', aeqd, other_args) - assert_almost_equal(np.array(aeqd.x_limits), - [-3.14159265, 3.14159265], decimal=6) - assert_almost_equal(np.array(aeqd.y_limits), - [-3.14159265, 3.14159265], decimal=6) + assert_almost_equal( + np.array(aeqd.x_limits), [-3.14159265, 3.14159265], decimal=6 + ) + assert_almost_equal( + np.array(aeqd.y_limits), [-3.14159265, 3.14159265], decimal=6 + ) lats, lons = np.mgrid[0:100:10, 0:100:10] result = aeqd.transform_points(geodetic, lons.ravel(), lats.ravel()) - expected_x = np.array([ - [0.00000, 0.17453, 0.34907, 0.52360, 0.69813, - 0.87266, 1.04720, 1.22173, 1.39626, 1.57080], - [0.00000, 0.17275, 0.34546, 0.51807, 0.69054, - 0.86278, 1.03472, 1.20620, 1.37704, 1.54693], - [0.00000, 0.16736, 0.33454, 0.50137, 0.66762, - 0.83301, 0.99719, 1.15965, 1.31964, 1.47607], - [0.00000, 0.15822, 0.31607, 0.47314, 0.62896, - 0.78296, 0.93436, 1.08215, 1.22487, 1.36035], - [0.00000, 0.14511, 0.28959, 0.43276, 0.57386, - 0.71195, 0.84583, 0.97392, 1.09409, 1.20330], - [0.00000, 0.12765, 0.25441, 0.37931, 0.50127, - 0.61904, 0.73106, 0.83535, 0.92935, 1.00969], - [0.00000, 0.10534, 0.20955, 0.31145, 0.40976, - 0.50301, 0.58948, 0.66711, 0.73343, 0.78540], - [0.00000, 0.07741, 0.15362, 0.22740, 0.29744, - 0.36234, 0.42056, 0.47039, 0.50997, 0.53724], - [0.00000, 0.04281, 0.08469, 0.12469, 0.16188, - 0.19529, 0.22399, 0.24706, 0.26358, 0.27277], - [0.00000, 0.00000, 0.00000, 0.00000, 0.00000, - 0.00000, 0.00000, 0.00000, 0.00000, 0.00000], - ]).ravel() + expected_x = np.array( + [ + [ + 0.00000, + 0.17453, + 0.34907, + 0.52360, + 0.69813, + 0.87266, + 1.04720, + 1.22173, + 1.39626, + 1.57080, + ], + [ + 0.00000, + 0.17275, + 0.34546, + 0.51807, + 0.69054, + 0.86278, + 1.03472, + 1.20620, + 1.37704, + 1.54693, + ], + [ + 0.00000, + 0.16736, + 0.33454, + 0.50137, + 0.66762, + 0.83301, + 0.99719, + 1.15965, + 1.31964, + 1.47607, + ], + [ + 0.00000, + 0.15822, + 0.31607, + 0.47314, + 0.62896, + 0.78296, + 0.93436, + 1.08215, + 1.22487, + 1.36035, + ], + [ + 0.00000, + 0.14511, + 0.28959, + 0.43276, + 0.57386, + 0.71195, + 0.84583, + 0.97392, + 1.09409, + 1.20330, + ], + [ + 0.00000, + 0.12765, + 0.25441, + 0.37931, + 0.50127, + 0.61904, + 0.73106, + 0.83535, + 0.92935, + 1.00969, + ], + [ + 0.00000, + 0.10534, + 0.20955, + 0.31145, + 0.40976, + 0.50301, + 0.58948, + 0.66711, + 0.73343, + 0.78540, + ], + [ + 0.00000, + 0.07741, + 0.15362, + 0.22740, + 0.29744, + 0.36234, + 0.42056, + 0.47039, + 0.50997, + 0.53724, + ], + [ + 0.00000, + 0.04281, + 0.08469, + 0.12469, + 0.16188, + 0.19529, + 0.22399, + 0.24706, + 0.26358, + 0.27277, + ], + [ + 0.00000, + 0.00000, + 0.00000, + 0.00000, + 0.00000, + 0.00000, + 0.00000, + 0.00000, + 0.00000, + 0.00000, + ], + ] + ).ravel() assert_almost_equal(result[:, 0], expected_x, decimal=5) - expected_y = np.array([ - [0.00000, 0.00000, 0.00000, 0.00000, 0.00000, - 0.00000, 0.00000, 0.00000, 0.00000, 0.00000], - [0.17453, 0.17541, 0.17810, 0.18270, 0.18943, - 0.19859, 0.21067, 0.22634, 0.24656, 0.27277], - [0.34907, 0.35079, 0.35601, 0.36497, 0.37803, - 0.39579, 0.41910, 0.44916, 0.48772, 0.53724], - [0.52360, 0.52606, 0.53355, 0.54634, 0.56493, - 0.59010, 0.62291, 0.66488, 0.71809, 0.78540], - [0.69813, 0.70119, 0.71046, 0.72626, 0.74912, - 0.77984, 0.81953, 0.86967, 0.93221, 1.00969], - [0.87266, 0.87609, 0.88647, 0.90408, 0.92938, - 0.96306, 1.00602, 1.05942, 1.12464, 1.20330], - [1.04720, 1.05068, 1.06119, 1.07891, 1.10415, - 1.13733, 1.17896, 1.22963, 1.28993, 1.36035], - [1.22173, 1.22481, 1.23407, 1.24956, 1.27137, - 1.29957, 1.33423, 1.37533, 1.42273, 1.47607], - [1.39626, 1.39829, 1.40434, 1.41435, 1.42823, - 1.44581, 1.46686, 1.49104, 1.51792, 1.54693], - [1.57080, 1.57080, 1.57080, 1.57080, 1.57080, - 1.57080, 1.57080, 1.57080, 1.57080, 1.57080], - ]).ravel() + expected_y = np.array( + [ + [ + 0.00000, + 0.00000, + 0.00000, + 0.00000, + 0.00000, + 0.00000, + 0.00000, + 0.00000, + 0.00000, + 0.00000, + ], + [ + 0.17453, + 0.17541, + 0.17810, + 0.18270, + 0.18943, + 0.19859, + 0.21067, + 0.22634, + 0.24656, + 0.27277, + ], + [ + 0.34907, + 0.35079, + 0.35601, + 0.36497, + 0.37803, + 0.39579, + 0.41910, + 0.44916, + 0.48772, + 0.53724, + ], + [ + 0.52360, + 0.52606, + 0.53355, + 0.54634, + 0.56493, + 0.59010, + 0.62291, + 0.66488, + 0.71809, + 0.78540, + ], + [ + 0.69813, + 0.70119, + 0.71046, + 0.72626, + 0.74912, + 0.77984, + 0.81953, + 0.86967, + 0.93221, + 1.00969, + ], + [ + 0.87266, + 0.87609, + 0.88647, + 0.90408, + 0.92938, + 0.96306, + 1.00602, + 1.05942, + 1.12464, + 1.20330, + ], + [ + 1.04720, + 1.05068, + 1.06119, + 1.07891, + 1.10415, + 1.13733, + 1.17896, + 1.22963, + 1.28993, + 1.36035, + ], + [ + 1.22173, + 1.22481, + 1.23407, + 1.24956, + 1.27137, + 1.29957, + 1.33423, + 1.37533, + 1.42273, + 1.47607, + ], + [ + 1.39626, + 1.39829, + 1.40434, + 1.41435, + 1.42823, + 1.44581, + 1.46686, + 1.49104, + 1.51792, + 1.54693, + ], + [ + 1.57080, + 1.57080, + 1.57080, + 1.57080, + 1.57080, + 1.57080, + 1.57080, + 1.57080, + 1.57080, + 1.57080, + ], + ] + ).ravel() assert_almost_equal(result[:, 1], expected_y, decimal=5) def test_sphere_transform(self): # USGS Professional Paper 1395, pg 337 - globe = ccrs.Globe(ellipse=None, - semimajor_axis=3.0, semiminor_axis=3.0) - aeqd = ccrs.AzimuthalEquidistant(central_latitude=40.0, - central_longitude=-100.0, - globe=globe) + globe = ccrs.Globe(ellipse=None, semimajor_axis=3.0, semiminor_axis=3.0) + aeqd = ccrs.AzimuthalEquidistant( + central_latitude=40.0, central_longitude=-100.0, globe=globe + ) geodetic = aeqd.as_geodetic() - other_args = {'a=3.0', 'b=3.0', 'lon_0=-100.0', 'lat_0=40.0', - 'x_0=0.0', 'y_0=0.0'} + other_args = { + 'a=3.0', + 'b=3.0', + 'lon_0=-100.0', + 'lat_0=40.0', + 'x_0=0.0', + 'y_0=0.0', + } check_proj_params('aeqd', aeqd, other_args) - assert_almost_equal(np.array(aeqd.x_limits), - [-9.42477796, 9.42477796], decimal=6) - assert_almost_equal(np.array(aeqd.y_limits), - [-9.42477796, 9.42477796], decimal=6) + assert_almost_equal( + np.array(aeqd.x_limits), [-9.42477796, 9.42477796], decimal=6 + ) + assert_almost_equal( + np.array(aeqd.y_limits), [-9.42477796, 9.42477796], decimal=6 + ) result = aeqd.transform_point(100.0, -20.0, geodetic) @@ -141,21 +359,32 @@ def test_sphere_transform(self): def test_ellipsoid_polar_transform(self): # USGS Professional Paper 1395, pp 338--339 - globe = ccrs.Globe(ellipse=None, semimajor_axis=6378388.0, - flattening=1 - np.sqrt(1 - 0.00672267)) - aeqd = ccrs.AzimuthalEquidistant(central_latitude=90.0, - central_longitude=-100.0, - globe=globe) + globe = ccrs.Globe( + ellipse=None, + semimajor_axis=6378388.0, + flattening=1 - np.sqrt(1 - 0.00672267), + ) + aeqd = ccrs.AzimuthalEquidistant( + central_latitude=90.0, central_longitude=-100.0, globe=globe + ) geodetic = aeqd.as_geodetic() - other_args = {'a=6378388.0', 'f=0.003367003355798981', 'lon_0=-100.0', - 'lat_0=90.0', 'x_0=0.0', 'y_0=0.0'} + other_args = { + 'a=6378388.0', + 'f=0.003367003355798981', + 'lon_0=-100.0', + 'lat_0=90.0', + 'x_0=0.0', + 'y_0=0.0', + } check_proj_params('aeqd', aeqd, other_args) - assert_almost_equal(np.array(aeqd.x_limits), - [-20038296.88254529, 20038296.88254529], decimal=6) - assert_almost_equal(np.array(aeqd.y_limits), - [-19970827.86969727, 19970827.86969727], decimal=6) + assert_almost_equal( + np.array(aeqd.x_limits), [-20038296.88254529, 20038296.88254529], decimal=6 + ) + assert_almost_equal( + np.array(aeqd.y_limits), [-19970827.86969727, 19970827.86969727], decimal=6 + ) result = aeqd.transform_point(5.0, 80.0, geodetic) @@ -163,26 +392,38 @@ def test_ellipsoid_polar_transform(self): def test_ellipsoid_guam_transform(self): # USGS Professional Paper 1395, pp 339--340 - globe = ccrs.Globe(ellipse=None, semimajor_axis=6378206.4, - flattening=1 - np.sqrt(1 - 0.00676866)) + globe = ccrs.Globe( + ellipse=None, + semimajor_axis=6378206.4, + flattening=1 - np.sqrt(1 - 0.00676866), + ) lat_0 = 13 + (28 + 20.87887 / 60) / 60 lon_0 = 144 + (44 + 55.50254 / 60) / 60 - aeqd = ccrs.AzimuthalEquidistant(central_latitude=lat_0, - central_longitude=lon_0, - false_easting=50000.0, - false_northing=50000.0, - globe=globe) + aeqd = ccrs.AzimuthalEquidistant( + central_latitude=lat_0, + central_longitude=lon_0, + false_easting=50000.0, + false_northing=50000.0, + globe=globe, + ) geodetic = aeqd.as_geodetic() - other_args = {'a=6378206.4', 'f=0.003390076308689371', - 'lon_0=144.7487507055556', 'lat_0=13.47246635277778', - 'x_0=50000.0', 'y_0=50000.0'} + other_args = { + 'a=6378206.4', + 'f=0.003390076308689371', + 'lon_0=144.7487507055556', + 'lat_0=13.47246635277778', + 'x_0=50000.0', + 'y_0=50000.0', + } check_proj_params('aeqd', aeqd, other_args) - assert_almost_equal(np.array(aeqd.x_limits), - [-19987726.36931940, 20087726.36931940], decimal=6) - assert_almost_equal(np.array(aeqd.y_limits), - [-19919796.94787477, 20019796.94787477], decimal=6) + assert_almost_equal( + np.array(aeqd.x_limits), [-19987726.36931940, 20087726.36931940], decimal=6 + ) + assert_almost_equal( + np.array(aeqd.y_limits), [-19919796.94787477, 20019796.94787477], decimal=6 + ) pt_lat = 13 + (20 + 20.53846 / 60) / 60 pt_lon = 144 + (38 + 7.19265 / 60) / 60 @@ -194,26 +435,38 @@ def test_ellipsoid_guam_transform(self): def test_ellipsoid_micronesia_transform(self): # USGS Professional Paper 1395, pp 340--341 - globe = ccrs.Globe(ellipse=None, semimajor_axis=6378206.4, - flattening=1 - np.sqrt(1 - 0.00676866)) + globe = ccrs.Globe( + ellipse=None, + semimajor_axis=6378206.4, + flattening=1 - np.sqrt(1 - 0.00676866), + ) lat_0 = 15 + (11 + 5.6830 / 60) / 60 lon_0 = 145 + (44 + 29.9720 / 60) / 60 - aeqd = ccrs.AzimuthalEquidistant(central_latitude=lat_0, - central_longitude=lon_0, - false_easting=28657.52, - false_northing=67199.99, - globe=globe) + aeqd = ccrs.AzimuthalEquidistant( + central_latitude=lat_0, + central_longitude=lon_0, + false_easting=28657.52, + false_northing=67199.99, + globe=globe, + ) geodetic = aeqd.as_geodetic() - other_args = {'a=6378206.4', 'f=0.003390076308689371', - 'lon_0=145.7416588888889', 'lat_0=15.18491194444444', - 'x_0=28657.52', 'y_0=67199.99000000001'} + other_args = { + 'a=6378206.4', + 'f=0.003390076308689371', + 'lon_0=145.7416588888889', + 'lat_0=15.18491194444444', + 'x_0=28657.52', + 'y_0=67199.99000000001', + } check_proj_params('aeqd', aeqd, other_args) - assert_almost_equal(np.array(aeqd.x_limits), - [-20009068.84931940, 20066383.88931940], decimal=6) - assert_almost_equal(np.array(aeqd.y_limits), - [-19902596.95787477, 20036996.93787477], decimal=6) + assert_almost_equal( + np.array(aeqd.x_limits), [-20009068.84931940, 20066383.88931940], decimal=6 + ) + assert_almost_equal( + np.array(aeqd.y_limits), [-19902596.95787477, 20036996.93787477], decimal=6 + ) pt_lat = 15 + (14 + 47.4930 / 60) / 60 pt_lon = 145 + (47 + 34.9080 / 60) / 60 diff --git a/lib/cartopy/tests/crs/test_eckert.py b/lib/cartopy/tests/crs/test_eckert.py index ae138d484..cdd4edfd8 100644 --- a/lib/cartopy/tests/crs/test_eckert.py +++ b/lib/cartopy/tests/crs/test_eckert.py @@ -15,14 +15,17 @@ from .helpers import check_proj_params -@pytest.mark.parametrize('name, proj, lim', [ - pytest.param('eck1', ccrs.EckertI, 18460911.739778, id='EckertI'), - pytest.param('eck2', ccrs.EckertII, 18460911.739778, id='EckertII'), - pytest.param('eck3', ccrs.EckertIII, 16921202.9229432, id='EckertIII'), - pytest.param('eck4', ccrs.EckertIV, 16921202.9229432, id='EckertIV'), - pytest.param('eck5', ccrs.EckertV, 17673594.1854146, id='EckertV'), - pytest.param('eck6', ccrs.EckertVI, 17673594.1854146, id='EckertVI'), -]) +@pytest.mark.parametrize( + 'name, proj, lim', + [ + pytest.param('eck1', ccrs.EckertI, 18460911.739778, id='EckertI'), + pytest.param('eck2', ccrs.EckertII, 18460911.739778, id='EckertII'), + pytest.param('eck3', ccrs.EckertIII, 16921202.9229432, id='EckertIII'), + pytest.param('eck4', ccrs.EckertIV, 16921202.9229432, id='EckertIV'), + pytest.param('eck5', ccrs.EckertV, 17673594.1854146, id='EckertV'), + pytest.param('eck6', ccrs.EckertVI, 17673594.1854146, id='EckertVI'), + ], +) def test_default(name, proj, lim): eck = proj() other_args = {'a=6378137.0', 'lon_0=0'} @@ -32,14 +35,17 @@ def test_default(name, proj, lim): assert_almost_equal(eck.y_limits, [-lim / 2, lim / 2]) -@pytest.mark.parametrize('name, proj, lim', [ - pytest.param('eck1', ccrs.EckertI, 2894.4050182, id='EckertI'), - pytest.param('eck2', ccrs.EckertII, 2894.4050182, id='EckertII'), - pytest.param('eck3', ccrs.EckertIII, 2653.0008564, id='EckertIII'), - pytest.param('eck4', ccrs.EckertIV, 2653.0008564, id='EckertIV'), - pytest.param('eck5', ccrs.EckertV, 2770.9649676, id='EckertV'), - pytest.param('eck6', ccrs.EckertVI, 2770.9649676, id='EckertVI'), -]) +@pytest.mark.parametrize( + 'name, proj, lim', + [ + pytest.param('eck1', ccrs.EckertI, 2894.4050182, id='EckertI'), + pytest.param('eck2', ccrs.EckertII, 2894.4050182, id='EckertII'), + pytest.param('eck3', ccrs.EckertIII, 2653.0008564, id='EckertIII'), + pytest.param('eck4', ccrs.EckertIV, 2653.0008564, id='EckertIV'), + pytest.param('eck5', ccrs.EckertV, 2770.9649676, id='EckertV'), + pytest.param('eck6', ccrs.EckertVI, 2770.9649676, id='EckertVI'), + ], +) def test_sphere_globe(name, proj, lim): globe = ccrs.Globe(semimajor_axis=1000, ellipse=None) eck = proj(globe=globe) @@ -50,19 +56,21 @@ def test_sphere_globe(name, proj, lim): assert_almost_equal(eck.y_limits, [-lim / 2, lim / 2]) -@pytest.mark.parametrize('name, proj, lim', [ - # Limits are the same as default since ellipses are not supported. - pytest.param('eck1', ccrs.EckertI, 18460911.739778, id='EckertI'), - pytest.param('eck2', ccrs.EckertII, 18460911.739778, id='EckertII'), - pytest.param('eck3', ccrs.EckertIII, 16921202.9229432, id='EckertIII'), - pytest.param('eck4', ccrs.EckertIV, 16921202.9229432, id='EckertIV'), - pytest.param('eck5', ccrs.EckertV, 17673594.1854146, id='EckertV'), - pytest.param('eck6', ccrs.EckertVI, 17673594.1854146, id='EckertVI'), -]) +@pytest.mark.parametrize( + 'name, proj, lim', + [ + # Limits are the same as default since ellipses are not supported. + pytest.param('eck1', ccrs.EckertI, 18460911.739778, id='EckertI'), + pytest.param('eck2', ccrs.EckertII, 18460911.739778, id='EckertII'), + pytest.param('eck3', ccrs.EckertIII, 16921202.9229432, id='EckertIII'), + pytest.param('eck4', ccrs.EckertIV, 16921202.9229432, id='EckertIV'), + pytest.param('eck5', ccrs.EckertV, 17673594.1854146, id='EckertV'), + pytest.param('eck6', ccrs.EckertVI, 17673594.1854146, id='EckertVI'), + ], +) def test_ellipse_globe(name, proj, lim): globe = ccrs.Globe(ellipse='WGS84') - with pytest.warns(UserWarning, - match='does not handle elliptical globes.') as w: + with pytest.warns(UserWarning, match='does not handle elliptical globes.') as w: eck = proj(globe=globe) assert len(w) == 1 @@ -73,20 +81,21 @@ def test_ellipse_globe(name, proj, lim): assert_almost_equal(eck.y_limits, [-lim / 2, lim / 2]) -@pytest.mark.parametrize('name, proj, lim', [ - # Limits are the same as spheres since ellipses are not supported. - pytest.param('eck1', ccrs.EckertI, 2894.4050182, id='EckertI'), - pytest.param('eck2', ccrs.EckertII, 2894.4050182, id='EckertII'), - pytest.param('eck3', ccrs.EckertIII, 2653.0008564, id='EckertIII'), - pytest.param('eck4', ccrs.EckertIV, 2653.0008564, id='EckertIV'), - pytest.param('eck5', ccrs.EckertV, 2770.9649676, id='EckertV'), - pytest.param('eck6', ccrs.EckertVI, 2770.9649676, id='EckertVI'), -]) +@pytest.mark.parametrize( + 'name, proj, lim', + [ + # Limits are the same as spheres since ellipses are not supported. + pytest.param('eck1', ccrs.EckertI, 2894.4050182, id='EckertI'), + pytest.param('eck2', ccrs.EckertII, 2894.4050182, id='EckertII'), + pytest.param('eck3', ccrs.EckertIII, 2653.0008564, id='EckertIII'), + pytest.param('eck4', ccrs.EckertIV, 2653.0008564, id='EckertIV'), + pytest.param('eck5', ccrs.EckertV, 2770.9649676, id='EckertV'), + pytest.param('eck6', ccrs.EckertVI, 2770.9649676, id='EckertVI'), + ], +) def test_eccentric_globe(name, proj, lim): - globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, - ellipse=None) - with pytest.warns(UserWarning, - match='does not handle elliptical globes.') as w: + globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, ellipse=None) + with pytest.warns(UserWarning, match='does not handle elliptical globes.') as w: eck = proj(globe=globe) assert len(w) == 1 @@ -97,14 +106,17 @@ def test_eccentric_globe(name, proj, lim): assert_almost_equal(eck.y_limits, [-lim / 2, lim / 2]) -@pytest.mark.parametrize('name, proj', [ - pytest.param('eck1', ccrs.EckertI, id='EckertI'), - pytest.param('eck2', ccrs.EckertII, id='EckertII'), - pytest.param('eck3', ccrs.EckertIII, id='EckertIII'), - pytest.param('eck4', ccrs.EckertIV, id='EckertIV'), - pytest.param('eck5', ccrs.EckertV, id='EckertV'), - pytest.param('eck6', ccrs.EckertVI, id='EckertVI'), -]) +@pytest.mark.parametrize( + 'name, proj', + [ + pytest.param('eck1', ccrs.EckertI, id='EckertI'), + pytest.param('eck2', ccrs.EckertII, id='EckertII'), + pytest.param('eck3', ccrs.EckertIII, id='EckertIII'), + pytest.param('eck4', ccrs.EckertIV, id='EckertIV'), + pytest.param('eck5', ccrs.EckertV, id='EckertV'), + pytest.param('eck6', ccrs.EckertVI, id='EckertVI'), + ], +) def test_offset(name, proj): crs = proj() crs_offset = proj(false_easting=1234, false_northing=-4321) @@ -114,14 +126,17 @@ def test_offset(name, proj): assert tuple(np.array(crs.y_limits) - 4321) == crs_offset.y_limits -@pytest.mark.parametrize('name, proj, lim', [ - pytest.param('eck1', ccrs.EckertI, 18460911.739778, id='EckertI'), - pytest.param('eck2', ccrs.EckertII, 18460911.739778, id='EckertII'), - pytest.param('eck3', ccrs.EckertIII, 16921202.9229432, id='EckertIII'), - pytest.param('eck4', ccrs.EckertIV, 16921202.9229432, id='EckertIV'), - pytest.param('eck5', ccrs.EckertV, 17673594.1854146, id='EckertV'), - pytest.param('eck6', ccrs.EckertVI, 17673594.1854146, id='EckertVI'), -]) +@pytest.mark.parametrize( + 'name, proj, lim', + [ + pytest.param('eck1', ccrs.EckertI, 18460911.739778, id='EckertI'), + pytest.param('eck2', ccrs.EckertII, 18460911.739778, id='EckertII'), + pytest.param('eck3', ccrs.EckertIII, 16921202.9229432, id='EckertIII'), + pytest.param('eck4', ccrs.EckertIV, 16921202.9229432, id='EckertIV'), + pytest.param('eck5', ccrs.EckertV, 17673594.1854146, id='EckertV'), + pytest.param('eck6', ccrs.EckertVI, 17673594.1854146, id='EckertVI'), + ], +) @pytest.mark.parametrize('lon', [-10.0, 10.0]) def test_central_longitude(name, proj, lim, lon): eck = proj(central_longitude=lon) @@ -132,28 +147,117 @@ def test_central_longitude(name, proj, lim, lon): assert_almost_equal(eck.y_limits, [-lim / 2, lim / 2]) -@pytest.mark.parametrize('name, proj, radius, expected_x, expected_y', [ - # USGS Professional Paper 1395, pg 258, Table 43 - pytest.param('eck4', ccrs.EckertIV, 0.75386, np.array([ - 0.50000, 0.55613, 0.60820, 0.65656, 0.70141, 0.74291, 0.78117, 0.81625, - 0.84822, 0.87709, 0.90291, 0.92567, 0.94539, 0.96208, 0.97573, 0.98635, - 0.99393, 0.99848, 1.00000, - ]), np.array([ - 1.00000, 0.99368, 0.97630, 0.94971, 0.91528, 0.87406, 0.82691, 0.77455, - 0.71762, 0.65666, 0.59217, 0.52462, 0.45443, 0.38202, 0.30779, 0.23210, - 0.15533, 0.07784, 0.00000, - ]), id='EckertIV'), - # USGS Professional Paper 1395, pg 258, Table 43 - pytest.param('eck6', ccrs.EckertVI, 0.72177, np.array([ - 0.50000, 0.50487, 0.51916, 0.54198, 0.57205, 0.60782, 0.64767, 0.69004, - 0.73344, 0.77655, 0.81817, 0.85724, 0.89288, 0.92430, 0.95087, 0.97207, - 0.98749, 0.99686, 1.00000, - ]), np.array([ - 1.00000, 0.99380, 0.97560, 0.94648, 0.90794, 0.86164, 0.80913, 0.75180, - 0.69075, 0.62689, 0.56090, 0.49332, 0.42454, 0.35488, 0.28457, 0.21379, - 0.14269, 0.07140, 0.00000, - ]), id='EckertVI'), -]) +@pytest.mark.parametrize( + 'name, proj, radius, expected_x, expected_y', + [ + # USGS Professional Paper 1395, pg 258, Table 43 + pytest.param( + 'eck4', + ccrs.EckertIV, + 0.75386, + np.array( + [ + 0.50000, + 0.55613, + 0.60820, + 0.65656, + 0.70141, + 0.74291, + 0.78117, + 0.81625, + 0.84822, + 0.87709, + 0.90291, + 0.92567, + 0.94539, + 0.96208, + 0.97573, + 0.98635, + 0.99393, + 0.99848, + 1.00000, + ] + ), + np.array( + [ + 1.00000, + 0.99368, + 0.97630, + 0.94971, + 0.91528, + 0.87406, + 0.82691, + 0.77455, + 0.71762, + 0.65666, + 0.59217, + 0.52462, + 0.45443, + 0.38202, + 0.30779, + 0.23210, + 0.15533, + 0.07784, + 0.00000, + ] + ), + id='EckertIV', + ), + # USGS Professional Paper 1395, pg 258, Table 43 + pytest.param( + 'eck6', + ccrs.EckertVI, + 0.72177, + np.array( + [ + 0.50000, + 0.50487, + 0.51916, + 0.54198, + 0.57205, + 0.60782, + 0.64767, + 0.69004, + 0.73344, + 0.77655, + 0.81817, + 0.85724, + 0.89288, + 0.92430, + 0.95087, + 0.97207, + 0.98749, + 0.99686, + 1.00000, + ] + ), + np.array( + [ + 1.00000, + 0.99380, + 0.97560, + 0.94648, + 0.90794, + 0.86164, + 0.80913, + 0.75180, + 0.69075, + 0.62689, + 0.56090, + 0.49332, + 0.42454, + 0.35488, + 0.28457, + 0.21379, + 0.14269, + 0.07140, + 0.00000, + ] + ), + id='EckertVI', + ), + ], +) def test_eckert_grid(name, proj, radius, expected_x, expected_y): globe = ccrs.Globe(semimajor_axis=radius, ellipse=None) eck = proj(globe=globe) @@ -173,14 +277,19 @@ def test_eckert_grid(name, proj, radius, expected_x, expected_y): assert_almost_equal(result[:, 1], expected_y, decimal=5) -@pytest.mark.parametrize('name, proj, lim, expected', [ - # USGS Professional Paper 1395, pg 368 - pytest.param('eck4', ccrs.EckertIV, 2.65300085, [0.1875270, -0.9519210], - id='EckertIV'), - # USGS Professional Paper 1395, pg 369 - pytest.param('eck6', ccrs.EckertVI, 2.77096497, [0.1693623, -0.9570223], - id='EckertVI'), -]) +@pytest.mark.parametrize( + 'name, proj, lim, expected', + [ + # USGS Professional Paper 1395, pg 368 + pytest.param( + 'eck4', ccrs.EckertIV, 2.65300085, [0.1875270, -0.9519210], id='EckertIV' + ), + # USGS Professional Paper 1395, pg 369 + pytest.param( + 'eck6', ccrs.EckertVI, 2.77096497, [0.1693623, -0.9570223], id='EckertVI' + ), + ], +) def test_eckert_sphere_transform(name, proj, lim, expected): globe = ccrs.Globe(semimajor_axis=1.0, ellipse=None) eck = proj(central_longitude=-90.0, globe=globe) diff --git a/lib/cartopy/tests/crs/test_equal_earth.py b/lib/cartopy/tests/crs/test_equal_earth.py index c4117b4e0..c74ba0083 100644 --- a/lib/cartopy/tests/crs/test_equal_earth.py +++ b/lib/cartopy/tests/crs/test_equal_earth.py @@ -20,13 +20,12 @@ def test_default(): other_args = {'ellps=WGS84', 'lon_0=0'} check_proj_params('eqearth', eqearth, other_args) - assert_almost_equal(eqearth.x_limits, - [-17243959.0622169, 17243959.0622169]) - assert_almost_equal(eqearth.y_limits, - [-8392927.59846646, 8392927.59846646]) + assert_almost_equal(eqearth.x_limits, [-17243959.0622169, 17243959.0622169]) + assert_almost_equal(eqearth.y_limits, [-8392927.59846646, 8392927.59846646]) # Expected aspect ratio from the paper. - assert_almost_equal(np.diff(eqearth.x_limits) / np.diff(eqearth.y_limits), - 2.05458, decimal=5) + assert_almost_equal( + np.diff(eqearth.x_limits) / np.diff(eqearth.y_limits), 2.05458, decimal=5 + ) def test_offset(): @@ -39,19 +38,17 @@ def test_offset(): def test_eccentric_globe(): - globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, - ellipse=None) + globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, ellipse=None) eqearth = ccrs.EqualEarth(globe=globe) other_args = {'a=1000', 'b=500', 'lon_0=0'} check_proj_params('eqearth', eqearth, other_args) - assert_almost_equal(eqearth.x_limits, - [-2248.43664092550, 2248.43664092550]) - assert_almost_equal(eqearth.y_limits, - [-1094.35228122148, 1094.35228122148]) + assert_almost_equal(eqearth.x_limits, [-2248.43664092550, 2248.43664092550]) + assert_almost_equal(eqearth.y_limits, [-1094.35228122148, 1094.35228122148]) # Expected aspect ratio from the paper. - assert_almost_equal(np.diff(eqearth.x_limits) / np.diff(eqearth.y_limits), - 2.05458, decimal=5) + assert_almost_equal( + np.diff(eqearth.x_limits) / np.diff(eqearth.y_limits), 2.05458, decimal=5 + ) @pytest.mark.parametrize('lon', [-10.0, 10.0]) @@ -60,10 +57,11 @@ def test_central_longitude(lon): other_args = {'ellps=WGS84', f'lon_0={lon}'} check_proj_params('eqearth', eqearth, other_args) - assert_almost_equal(eqearth.x_limits, - [-17243959.0622169, 17243959.0622169], decimal=5) - assert_almost_equal(eqearth.y_limits, - [-8392927.59846646, 8392927.59846646]) + assert_almost_equal( + eqearth.x_limits, [-17243959.0622169, 17243959.0622169], decimal=5 + ) + assert_almost_equal(eqearth.y_limits, [-8392927.59846646, 8392927.59846646]) # Expected aspect ratio from the paper. - assert_almost_equal(np.diff(eqearth.x_limits) / np.diff(eqearth.y_limits), - 2.05458, decimal=5) + assert_almost_equal( + np.diff(eqearth.x_limits) / np.diff(eqearth.y_limits), 2.05458, decimal=5 + ) diff --git a/lib/cartopy/tests/crs/test_equidistant_conic.py b/lib/cartopy/tests/crs/test_equidistant_conic.py index c15ada414..4d3209f22 100644 --- a/lib/cartopy/tests/crs/test_equidistant_conic.py +++ b/lib/cartopy/tests/crs/test_equidistant_conic.py @@ -19,55 +19,74 @@ class TestEquidistantConic: def test_default(self): eqdc = ccrs.EquidistantConic() - other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', - 'y_0=0.0', 'lat_1=20.0', 'lat_2=50.0'} + other_args = { + 'ellps=WGS84', + 'lon_0=0.0', + 'lat_0=0.0', + 'x_0=0.0', + 'y_0=0.0', + 'lat_1=20.0', + 'lat_2=50.0', + } check_proj_params('eqdc', eqdc, other_args) expected_x = (-22784919.35600352, 22784919.35600352) expected_y = (-10001965.729313632, 17558791.85156368) if pyproj.__proj_version__ >= '9.2.0': - expected_x = (-22784919.3559981, 22784919.3559981) - expected_y = (-10001965.72931272, 17558791.85157471) - assert_almost_equal(np.array(eqdc.x_limits), - expected_x, - decimal=7) - assert_almost_equal(np.array(eqdc.y_limits), - expected_y, - decimal=7) + expected_x = (-22784919.3559981, 22784919.3559981) + expected_y = (-10001965.72931272, 17558791.85157471) + assert_almost_equal(np.array(eqdc.x_limits), expected_x, decimal=7) + assert_almost_equal(np.array(eqdc.y_limits), expected_y, decimal=7) def test_eccentric_globe(self): - globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, - ellipse=None) + globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, ellipse=None) eqdc = ccrs.EquidistantConic(globe=globe) - other_args = {'a=1000', 'b=500', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', - 'y_0=0.0', 'lat_1=20.0', 'lat_2=50.0'} + other_args = { + 'a=1000', + 'b=500', + 'lon_0=0.0', + 'lat_0=0.0', + 'x_0=0.0', + 'y_0=0.0', + 'lat_1=20.0', + 'lat_2=50.0', + } check_proj_params('eqdc', eqdc, other_args) expected_x = (-3016.869847713461, 3016.869847713461) expected_y = (-1216.6029342241113, 2511.0574375797723) if pyproj.__proj_version__ >= '9.2.0': - expected_x = (-2960.1009481, 2960.1009481) - expected_y = (-1211.05573766, 2606.04249537) - assert_almost_equal(np.array(eqdc.x_limits), - expected_x, - decimal=7) - assert_almost_equal(np.array(eqdc.y_limits), - expected_y, - decimal=7) + expected_x = (-2960.1009481, 2960.1009481) + expected_y = (-1211.05573766, 2606.04249537) + assert_almost_equal(np.array(eqdc.x_limits), expected_x, decimal=7) + assert_almost_equal(np.array(eqdc.y_limits), expected_y, decimal=7) def test_eastings(self): - eqdc_offset = ccrs.EquidistantConic(false_easting=1234, - false_northing=-4321) - - other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=1234', - 'y_0=-4321', 'lat_1=20.0', 'lat_2=50.0'} + eqdc_offset = ccrs.EquidistantConic(false_easting=1234, false_northing=-4321) + + other_args = { + 'ellps=WGS84', + 'lon_0=0.0', + 'lat_0=0.0', + 'x_0=1234', + 'y_0=-4321', + 'lat_1=20.0', + 'lat_2=50.0', + } check_proj_params('eqdc', eqdc_offset, other_args) @pytest.mark.parametrize('lon', [-10.0, 10.0]) def test_central_longitude(self, lon): eqdc = ccrs.EquidistantConic() eqdc_offset = ccrs.EquidistantConic(central_longitude=lon) - other_args = {'ellps=WGS84', f'lon_0={lon}', 'lat_0=0.0', - 'x_0=0.0', 'y_0=0.0', 'lat_1=20.0', 'lat_2=50.0'} + other_args = { + 'ellps=WGS84', + f'lon_0={lon}', + 'lat_0=0.0', + 'x_0=0.0', + 'y_0=0.0', + 'lat_1=20.0', + 'lat_2=50.0', + } check_proj_params('eqdc', eqdc_offset, other_args) assert_array_almost_equal( @@ -78,42 +97,72 @@ def test_central_longitude(self, lon): def test_standard_parallels(self): eqdc = ccrs.EquidistantConic(standard_parallels=(13, 37)) - other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', - 'y_0=0.0', 'lat_1=13', 'lat_2=37'} + other_args = { + 'ellps=WGS84', + 'lon_0=0.0', + 'lat_0=0.0', + 'x_0=0.0', + 'y_0=0.0', + 'lat_1=13', + 'lat_2=37', + } check_proj_params('eqdc', eqdc, other_args) - eqdc = ccrs.EquidistantConic(standard_parallels=(13, )) - other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', - 'y_0=0.0', 'lat_1=13'} + eqdc = ccrs.EquidistantConic(standard_parallels=(13,)) + other_args = { + 'ellps=WGS84', + 'lon_0=0.0', + 'lat_0=0.0', + 'x_0=0.0', + 'y_0=0.0', + 'lat_1=13', + } check_proj_params('eqdc', eqdc, other_args) eqdc = ccrs.EquidistantConic(standard_parallels=13) - other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', - 'y_0=0.0', 'lat_1=13'} + other_args = { + 'ellps=WGS84', + 'lon_0=0.0', + 'lat_0=0.0', + 'x_0=0.0', + 'y_0=0.0', + 'lat_1=13', + } check_proj_params('eqdc', eqdc, other_args) def test_sphere_transform(self): # USGS Professional Paper 1395, pg 298 - globe = ccrs.Globe(semimajor_axis=1.0, semiminor_axis=1.0, - ellipse=None) + globe = ccrs.Globe(semimajor_axis=1.0, semiminor_axis=1.0, ellipse=None) lat_1 = 29.5 lat_2 = 45.5 - eqdc = ccrs.EquidistantConic(central_longitude=-96.0, - central_latitude=23.0, - standard_parallels=(lat_1, lat_2), - globe=globe) + eqdc = ccrs.EquidistantConic( + central_longitude=-96.0, + central_latitude=23.0, + standard_parallels=(lat_1, lat_2), + globe=globe, + ) geodetic = eqdc.as_geodetic() - other_args = {'a=1.0', 'b=1.0', 'lon_0=-96.0', 'lat_0=23.0', 'x_0=0.0', - 'y_0=0.0', 'lat_1=29.5', 'lat_2=45.5'} + other_args = { + 'a=1.0', + 'b=1.0', + 'lon_0=-96.0', + 'lat_0=23.0', + 'x_0=0.0', + 'y_0=0.0', + 'lat_1=29.5', + 'lat_2=45.5', + } check_proj_params('eqdc', eqdc, other_args) - assert_almost_equal(np.array(eqdc.x_limits), - (-3.520038619089038, 3.520038619089038), - decimal=7) - assert_almost_equal(np.array(eqdc.y_limits), - (-1.9722220547535922, 2.7066811021065535), - decimal=7) + assert_almost_equal( + np.array(eqdc.x_limits), (-3.520038619089038, 3.520038619089038), decimal=7 + ) + assert_almost_equal( + np.array(eqdc.y_limits), + (-1.9722220547535922, 2.7066811021065535), + decimal=7, + ) result = eqdc.transform_point(-75.0, 35.0, geodetic) @@ -121,32 +170,39 @@ def test_sphere_transform(self): def test_ellipsoid_transform(self): # USGS Professional Paper 1395, pp 299--300 - globe = ccrs.Globe(semimajor_axis=6378206.4, - flattening=1 - np.sqrt(1 - 0.00676866), - ellipse=None) + globe = ccrs.Globe( + semimajor_axis=6378206.4, + flattening=1 - np.sqrt(1 - 0.00676866), + ellipse=None, + ) lat_1 = 29.5 lat_2 = 45.5 - eqdc = ccrs.EquidistantConic(central_latitude=23.0, - central_longitude=-96.0, - standard_parallels=(lat_1, lat_2), - globe=globe) + eqdc = ccrs.EquidistantConic( + central_latitude=23.0, + central_longitude=-96.0, + standard_parallels=(lat_1, lat_2), + globe=globe, + ) geodetic = eqdc.as_geodetic() - other_args = {'a=6378206.4', 'f=0.003390076308689371', 'lon_0=-96.0', - 'lat_0=23.0', 'x_0=0.0', 'y_0=0.0', 'lat_1=29.5', - 'lat_2=45.5'} + other_args = { + 'a=6378206.4', + 'f=0.003390076308689371', + 'lon_0=-96.0', + 'lat_0=23.0', + 'x_0=0.0', + 'y_0=0.0', + 'lat_1=29.5', + 'lat_2=45.5', + } check_proj_params('eqdc', eqdc, other_args) expected_x = (-22421870.719894886, 22421870.719894886) expected_y = (-12546277.778958388, 17260638.403203618) if pyproj.__proj_version__ >= '9.2.0': - expected_x = (-22421870.71988974, 22421870.71988976) - expected_y = (-12546277.77895742, 17260638.403216) - assert_almost_equal(np.array(eqdc.x_limits), - expected_x, - decimal=7) - assert_almost_equal(np.array(eqdc.y_limits), - expected_y, - decimal=7) + expected_x = (-22421870.71988974, 22421870.71988976) + expected_y = (-12546277.77895742, 17260638.403216) + assert_almost_equal(np.array(eqdc.x_limits), expected_x, decimal=7) + assert_almost_equal(np.array(eqdc.y_limits), expected_y, decimal=7) result = eqdc.transform_point(-75.0, 35.0, geodetic) diff --git a/lib/cartopy/tests/crs/test_geostationary.py b/lib/cartopy/tests/crs/test_geostationary.py index ddb224829..21c74768a 100644 --- a/lib/cartopy/tests/crs/test_geostationary.py +++ b/lib/cartopy/tests/crs/test_geostationary.py @@ -24,58 +24,90 @@ def adjust_expected_params(self, expected): def test_default(self): geos = self.test_class() - other_args = {'ellps=WGS84', 'h=35785831', 'lat_0=0.0', 'lon_0=0.0', - 'units=m', 'x_0=0', 'y_0=0'} + other_args = { + 'ellps=WGS84', + 'h=35785831', + 'lat_0=0.0', + 'lon_0=0.0', + 'units=m', + 'x_0=0', + 'y_0=0', + } self.adjust_expected_params(other_args) check_proj_params(self.expected_proj_name, geos, other_args) - assert_almost_equal(geos.boundary.bounds, - (-5434177.81588539, -5412932.3767, - 5434177.81588539, 5412932.3767), - decimal=4) + assert_almost_equal( + geos.boundary.bounds, + (-5434177.81588539, -5412932.3767, 5434177.81588539, 5412932.3767), + decimal=4, + ) def test_low_orbit(self): geos = self.test_class(satellite_height=700000) - other_args = {'ellps=WGS84', 'h=700000', 'lat_0=0.0', 'lon_0=0.0', - 'units=m', 'x_0=0', 'y_0=0'} + other_args = { + 'ellps=WGS84', + 'h=700000', + 'lat_0=0.0', + 'lon_0=0.0', + 'units=m', + 'x_0=0', + 'y_0=0', + } self.adjust_expected_params(other_args) check_proj_params(self.expected_proj_name, geos, other_args) - assert_almost_equal(geos.boundary.bounds, - (-785616.1189, -783815.6629, - 785616.1189, 783815.6629), - decimal=4) + assert_almost_equal( + geos.boundary.bounds, + (-785616.1189, -783815.6629, 785616.1189, 783815.6629), + decimal=4, + ) # Checking that this isn't just a simple elliptical border - assert_almost_equal(geos.boundary.coords[7], - (750051.0347, -305714.8243), - decimal=4) + assert_almost_equal( + geos.boundary.coords[7], (750051.0347, -305714.8243), decimal=4 + ) def test_eastings(self): - geos = self.test_class(false_easting=5000000, - false_northing=-125000,) - other_args = {'ellps=WGS84', 'h=35785831', 'lat_0=0.0', 'lon_0=0.0', - 'units=m', 'x_0=5000000', 'y_0=-125000'} + geos = self.test_class( + false_easting=5000000, + false_northing=-125000, + ) + other_args = { + 'ellps=WGS84', + 'h=35785831', + 'lat_0=0.0', + 'lon_0=0.0', + 'units=m', + 'x_0=5000000', + 'y_0=-125000', + } self.adjust_expected_params(other_args) check_proj_params(self.expected_proj_name, geos, other_args) - assert_almost_equal(geos.boundary.bounds, - (-434177.81588539, -5537932.3767, - 10434177.81588539, 5287932.3767), - decimal=4) + assert_almost_equal( + geos.boundary.bounds, + (-434177.81588539, -5537932.3767, 10434177.81588539, 5287932.3767), + decimal=4, + ) def test_sweep(self): geos = ccrs.Geostationary(sweep_axis='x') - other_args = {'ellps=WGS84', 'h=35785831', 'lat_0=0.0', 'lon_0=0.0', - 'sweep=x', 'units=m', 'x_0=0', 'y_0=0'} + other_args = { + 'ellps=WGS84', + 'h=35785831', + 'lat_0=0.0', + 'lon_0=0.0', + 'sweep=x', + 'units=m', + 'x_0=0', + 'y_0=0', + } check_proj_params(self.expected_proj_name, geos, other_args) pt = geos.transform_point(-60, 25, ccrs.PlateCarree()) - assert_almost_equal(pt, - (-4529521.6442, 2437479.4195), - decimal=4) + assert_almost_equal(pt, (-4529521.6442, 2437479.4195), decimal=4) diff --git a/lib/cartopy/tests/crs/test_gnomonic.py b/lib/cartopy/tests/crs/test_gnomonic.py index f6037bd61..5f18502d2 100644 --- a/lib/cartopy/tests/crs/test_gnomonic.py +++ b/lib/cartopy/tests/crs/test_gnomonic.py @@ -20,10 +20,8 @@ def test_default(): other_args = {'a=6378137.0', 'lon_0=0.0', 'lat_0=0.0'} check_proj_params('gnom', gnom, other_args) - assert_almost_equal(np.array(gnom.x_limits), - [-5e7, 5e7]) - assert_almost_equal(np.array(gnom.y_limits), - [-5e7, 5e7]) + assert_almost_equal(np.array(gnom.x_limits), [-5e7, 5e7]) + assert_almost_equal(np.array(gnom.y_limits), [-5e7, 5e7]) def test_sphere_globe(): @@ -38,8 +36,7 @@ def test_sphere_globe(): def test_ellipse_globe(): globe = ccrs.Globe(ellipse='WGS84') - with pytest.warns(UserWarning, - match='does not handle elliptical globes.') as w: + with pytest.warns(UserWarning, match='does not handle elliptical globes.') as w: gnom = ccrs.Gnomonic(globe=globe) assert len(w) == 1 @@ -52,10 +49,8 @@ def test_ellipse_globe(): def test_eccentric_globe(): - globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, - ellipse=None) - with pytest.warns(UserWarning, - match='does not handle elliptical globes.') as w: + globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, ellipse=None) + with pytest.warns(UserWarning, match='does not handle elliptical globes.') as w: gnom = ccrs.Gnomonic(globe=globe) assert len(w) == 1 @@ -71,72 +66,62 @@ def test_eccentric_globe(): @pytest.mark.parametrize('lon', [-10, 0, 10]) def test_central_params(lon, lat): gnom = ccrs.Gnomonic(central_latitude=lat, central_longitude=lon) - other_args = {f'lat_0={lat}', f'lon_0={lon}', - 'a=6378137.0'} + other_args = {f'lat_0={lat}', f'lon_0={lon}', 'a=6378137.0'} check_proj_params('gnom', gnom, other_args) - assert_almost_equal(np.array(gnom.x_limits), - [-5e7, 5e7]) - assert_almost_equal(np.array(gnom.y_limits), - [-5e7, 5e7]) + assert_almost_equal(np.array(gnom.x_limits), [-5e7, 5e7]) + assert_almost_equal(np.array(gnom.y_limits), [-5e7, 5e7]) def test_grid(): # USGS Professional Paper 1395, pg 168, Table 26 - globe = ccrs.Globe(ellipse=None, - semimajor_axis=1.0, semiminor_axis=1.0) + globe = ccrs.Globe(ellipse=None, semimajor_axis=1.0, semiminor_axis=1.0) gnom = ccrs.Gnomonic(globe=globe) geodetic = gnom.as_geodetic() other_args = {'a=1.0', 'b=1.0', 'lon_0=0.0', 'lat_0=0.0'} check_proj_params('gnom', gnom, other_args) - assert_almost_equal(np.array(gnom.x_limits), - [-5e7, 5e7]) - assert_almost_equal(np.array(gnom.y_limits), - [-5e7, 5e7]) + assert_almost_equal(np.array(gnom.x_limits), [-5e7, 5e7]) + assert_almost_equal(np.array(gnom.y_limits), [-5e7, 5e7]) lats, lons = np.mgrid[0:90:10, 0:90:10].reshape((2, -1)) expected_x = np.tile( - [0.0000, 0.1763, 0.3640, 0.5774, 0.8391, 1.1918, 1.7321, 2.7475, - 5.6713], - 9) - expected_y = np.array([ - [5.6713, 2.7475, 1.7321, 1.1918, 0.8391, 0.5774, 0.3640, 0.1763, 0], - [5.7588, 2.7899, 1.7588, 1.2101, 0.8520, 0.5863, 0.3696, 0.1790, 0], - [6.0353, 2.9238, 1.8432, 1.2682, 0.8930, 0.6144, 0.3873, 0.1876, 0], - [6.5486, 3.1725, 2.0000, 1.3761, 0.9689, 0.6667, 0.4203, 0.2036, 0], - [7.4033, 3.5866, 2.2610, 1.5557, 1.0954, 0.7537, 0.4751, 0.2302, 0], - [8.8229, 4.2743, 2.6946, 1.8540, 1.3054, 0.8982, 0.5662, 0.2743, 0], - [11.3426, 5.4950, 3.4641, 2.3835, 1.6782, 1.1547, 0.7279, 0.3527, 0], - [16.5817, 8.0331, 5.0642, 3.4845, 2.4534, 1.6881, 1.0642, 0.5155, 0], - [32.6596, 15.8221, 9.9745, 6.8630, 4.8322, 3.3248, 2.0960, 1.0154, 0], - ])[:, ::-1].T.ravel() + [0.0000, 0.1763, 0.3640, 0.5774, 0.8391, 1.1918, 1.7321, 2.7475, 5.6713], 9 + ) + expected_y = np.array( + [ + [5.6713, 2.7475, 1.7321, 1.1918, 0.8391, 0.5774, 0.3640, 0.1763, 0], + [5.7588, 2.7899, 1.7588, 1.2101, 0.8520, 0.5863, 0.3696, 0.1790, 0], + [6.0353, 2.9238, 1.8432, 1.2682, 0.8930, 0.6144, 0.3873, 0.1876, 0], + [6.5486, 3.1725, 2.0000, 1.3761, 0.9689, 0.6667, 0.4203, 0.2036, 0], + [7.4033, 3.5866, 2.2610, 1.5557, 1.0954, 0.7537, 0.4751, 0.2302, 0], + [8.8229, 4.2743, 2.6946, 1.8540, 1.3054, 0.8982, 0.5662, 0.2743, 0], + [11.3426, 5.4950, 3.4641, 2.3835, 1.6782, 1.1547, 0.7279, 0.3527, 0], + [16.5817, 8.0331, 5.0642, 3.4845, 2.4534, 1.6881, 1.0642, 0.5155, 0], + [32.6596, 15.8221, 9.9745, 6.8630, 4.8322, 3.3248, 2.0960, 1.0154, 0], + ] + )[:, ::-1].T.ravel() # Test all quadrants; they are symmetrical. for lon_sign in [1, -1]: for lat_sign in [1, -1]: - result = gnom.transform_points(geodetic, - lon_sign * lons, lat_sign * lats) + result = gnom.transform_points(geodetic, lon_sign * lons, lat_sign * lats) assert_almost_equal(result[:, 0], lon_sign * expected_x, decimal=4) assert_almost_equal(result[:, 1], lat_sign * expected_y, decimal=4) def test_sphere_transform(): # USGS Professional Paper 1395, pp 319 - 320 - globe = ccrs.Globe(semimajor_axis=1.0, semiminor_axis=1.0, - ellipse=None) - gnom = ccrs.Gnomonic(central_latitude=40.0, central_longitude=-100.0, - globe=globe) + globe = ccrs.Globe(semimajor_axis=1.0, semiminor_axis=1.0, ellipse=None) + gnom = ccrs.Gnomonic(central_latitude=40.0, central_longitude=-100.0, globe=globe) geodetic = gnom.as_geodetic() other_args = {'a=1.0', 'b=1.0', 'lon_0=-100.0', 'lat_0=40.0'} check_proj_params('gnom', gnom, other_args) - assert_almost_equal(np.array(gnom.x_limits), - [-5e7, 5e7]) - assert_almost_equal(np.array(gnom.y_limits), - [-5e7, 5e7]) + assert_almost_equal(np.array(gnom.x_limits), [-5e7, 5e7]) + assert_almost_equal(np.array(gnom.y_limits), [-5e7, 5e7]) result = gnom.transform_point(-110.0, 30.0, geodetic) assert_almost_equal(result, np.array([-0.1542826, -0.1694739])) diff --git a/lib/cartopy/tests/crs/test_hammer.py b/lib/cartopy/tests/crs/test_hammer.py index e073ce02d..a8c65ddac 100644 --- a/lib/cartopy/tests/crs/test_hammer.py +++ b/lib/cartopy/tests/crs/test_hammer.py @@ -36,8 +36,7 @@ def test_sphere_globe(): def test_ellipse_globe(): globe = ccrs.Globe(ellipse='WGS84') - with pytest.warns(UserWarning, - match='does not handle elliptical globes.') as w: + with pytest.warns(UserWarning, match='does not handle elliptical globes.') as w: hammer = ccrs.Hammer(globe=globe) assert len(w) == 1 @@ -50,10 +49,8 @@ def test_ellipse_globe(): def test_eccentric_globe(): - globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, - ellipse=None) - with pytest.warns(UserWarning, - match='does not handle elliptical globes.') as w: + globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, ellipse=None) + with pytest.warns(UserWarning, match='does not handle elliptical globes.') as w: hammer = ccrs.Hammer(globe=globe) assert len(w) == 1 @@ -80,6 +77,7 @@ def test_central_longitude(lon): other_args = {'a=6378137.0', 'lon_0={}'.format(lon)} check_proj_params('hammer', hammer, other_args) - assert_almost_equal(hammer.x_limits, [-18040095.6961473, 18040095.6961473], - decimal=5) + assert_almost_equal( + hammer.x_limits, [-18040095.6961473, 18040095.6961473], decimal=5 + ) assert_almost_equal(hammer.y_limits, [-9020047.8480736, 9020047.8480736]) diff --git a/lib/cartopy/tests/crs/test_interrupted_goode_homolosine.py b/lib/cartopy/tests/crs/test_interrupted_goode_homolosine.py index 2dfeb3079..ae3934067 100644 --- a/lib/cartopy/tests/crs/test_interrupted_goode_homolosine.py +++ b/lib/cartopy/tests/crs/test_interrupted_goode_homolosine.py @@ -15,54 +15,46 @@ from .helpers import check_proj_params -@pytest.mark.parametrize("emphasis", ["land", "ocean"]) +@pytest.mark.parametrize('emphasis', ['land', 'ocean']) def test_default(emphasis): igh = ccrs.InterruptedGoodeHomolosine(emphasis=emphasis) - other_args = {"ellps=WGS84", "lon_0=0"} - if emphasis == "land": - check_proj_params("igh", igh, other_args) - elif emphasis == "ocean": - check_proj_params("igh_o", igh, other_args) - assert_allclose( - np.array(igh.x_limits), [-20037508.3427892, 20037508.3427892] - ) - assert_allclose( - np.array(igh.y_limits), [-8683259.7164347, 8683259.7164347] - ) + other_args = {'ellps=WGS84', 'lon_0=0'} + if emphasis == 'land': + check_proj_params('igh', igh, other_args) + elif emphasis == 'ocean': + check_proj_params('igh_o', igh, other_args) + assert_allclose(np.array(igh.x_limits), [-20037508.3427892, 20037508.3427892]) + assert_allclose(np.array(igh.y_limits), [-8683259.7164347, 8683259.7164347]) -@pytest.mark.parametrize("emphasis", ["land", "ocean"]) +@pytest.mark.parametrize('emphasis', ['land', 'ocean']) def test_eccentric_globe(emphasis): globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, ellipse=None) igh = ccrs.InterruptedGoodeHomolosine(globe=globe, emphasis=emphasis) - other_args = {"a=1000", "b=500", "lon_0=0"} - if emphasis == "land": - check_proj_params("igh", igh, other_args) - elif emphasis == "ocean": - check_proj_params("igh_o", igh, other_args) + other_args = {'a=1000', 'b=500', 'lon_0=0'} + if emphasis == 'land': + check_proj_params('igh', igh, other_args) + elif emphasis == 'ocean': + check_proj_params('igh_o', igh, other_args) assert_allclose(np.array(igh.x_limits), [-3141.5926536, 3141.5926536]) assert_allclose(np.array(igh.y_limits), [-1361.410035, 1361.410035]) @pytest.mark.parametrize( - ("emphasis", "lon"), - [("land", -10.0), ("land", 10.0), ("ocean", -10.0), ("ocean", 10.0)], + ('emphasis', 'lon'), + [('land', -10.0), ('land', 10.0), ('ocean', -10.0), ('ocean', 10.0)], ) def test_central_longitude(emphasis, lon): - igh = ccrs.InterruptedGoodeHomolosine( - central_longitude=lon, emphasis=emphasis - ) - other_args = {"ellps=WGS84", f"lon_0={lon}"} - if emphasis == "land": - check_proj_params("igh", igh, other_args) - elif emphasis == "ocean": - check_proj_params("igh_o", igh, other_args) + igh = ccrs.InterruptedGoodeHomolosine(central_longitude=lon, emphasis=emphasis) + other_args = {'ellps=WGS84', f'lon_0={lon}'} + if emphasis == 'land': + check_proj_params('igh', igh, other_args) + elif emphasis == 'ocean': + check_proj_params('igh_o', igh, other_args) assert_allclose( np.array(igh.x_limits), [-20037508.3427892, 20037508.3427892], ) - assert_allclose( - np.array(igh.y_limits), [-8683259.7164347, 8683259.7164347] - ) + assert_allclose(np.array(igh.y_limits), [-8683259.7164347, 8683259.7164347]) diff --git a/lib/cartopy/tests/crs/test_lambert_azimuthal_equal_area.py b/lib/cartopy/tests/crs/test_lambert_azimuthal_equal_area.py index 99dbd6cd7..d1f094b0e 100644 --- a/lib/cartopy/tests/crs/test_lambert_azimuthal_equal_area.py +++ b/lib/cartopy/tests/crs/test_lambert_azimuthal_equal_area.py @@ -14,43 +14,45 @@ class TestLambertAzimuthalEqualArea: def test_default(self): crs = ccrs.LambertAzimuthalEqualArea() - other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', - 'y_0=0.0'} + other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', 'y_0=0.0'} check_proj_params('laea', crs, other_args) - assert_almost_equal(np.array(crs.x_limits), - [-12755636.1863, 12755636.1863], - decimal=4) - assert_almost_equal(np.array(crs.y_limits), - [-12727770.598700099, 12727770.598700099], - decimal=4) + assert_almost_equal( + np.array(crs.x_limits), [-12755636.1863, 12755636.1863], decimal=4 + ) + assert_almost_equal( + np.array(crs.y_limits), [-12727770.598700099, 12727770.598700099], decimal=4 + ) def test_eccentric_globe(self): - globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, - ellipse=None) + globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, ellipse=None) crs = ccrs.LambertAzimuthalEqualArea(globe=globe) - other_args = {'a=1000', 'b=500', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', - 'y_0=0.0'} + other_args = {'a=1000', 'b=500', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', 'y_0=0.0'} check_proj_params('laea', crs, other_args) - assert_almost_equal(np.array(crs.x_limits), - [-1999.9, 1999.9], decimal=1) - assert_almost_equal(np.array(crs.y_limits), - [-1380.17298647, 1380.17298647], decimal=4) + assert_almost_equal(np.array(crs.x_limits), [-1999.9, 1999.9], decimal=1) + assert_almost_equal( + np.array(crs.y_limits), [-1380.17298647, 1380.17298647], decimal=4 + ) def test_offset(self): crs = ccrs.LambertAzimuthalEqualArea() - crs_offset = ccrs.LambertAzimuthalEqualArea(false_easting=1234, - false_northing=-4321) - other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=1234', - 'y_0=-4321'} + crs_offset = ccrs.LambertAzimuthalEqualArea( + false_easting=1234, false_northing=-4321 + ) + other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=1234', 'y_0=-4321'} check_proj_params('laea', crs_offset, other_args) assert tuple(np.array(crs.x_limits) + 1234) == crs_offset.x_limits assert tuple(np.array(crs.y_limits) - 4321) == crs_offset.y_limits - @pytest.mark.parametrize("latitude", [-90, 90]) + @pytest.mark.parametrize('latitude', [-90, 90]) def test_extrema(self, latitude): crs = ccrs.LambertAzimuthalEqualArea(central_latitude=latitude) - other_args = {'ellps=WGS84', 'lon_0=0.0', f'lat_0={latitude}', - 'x_0=0.0', 'y_0=0.0'} + other_args = { + 'ellps=WGS84', + 'lon_0=0.0', + f'lat_0={latitude}', + 'x_0=0.0', + 'y_0=0.0', + } check_proj_params('laea', crs, other_args) diff --git a/lib/cartopy/tests/crs/test_lambert_conformal.py b/lib/cartopy/tests/crs/test_lambert_conformal.py index 1ff2ac3b2..2f0d01693 100644 --- a/lib/cartopy/tests/crs/test_lambert_conformal.py +++ b/lib/cartopy/tests/crs/test_lambert_conformal.py @@ -14,8 +14,15 @@ def test_defaults(): crs = ccrs.LambertConformal() - other_args = {'ellps=WGS84', 'lon_0=-96.0', 'lat_0=39.0', 'x_0=0.0', - 'y_0=0.0', 'lat_1=33', 'lat_2=45'} + other_args = { + 'ellps=WGS84', + 'lon_0=-96.0', + 'lat_0=39.0', + 'x_0=0.0', + 'y_0=0.0', + 'lat_1=33', + 'lat_2=45', + } check_proj_params('lcc', crs, other_args) @@ -24,8 +31,15 @@ def test_default_with_cutoff(): crs2 = ccrs.LambertConformal(cutoff=-80) default = ccrs.LambertConformal() - other_args = {'ellps=WGS84', 'lon_0=-96.0', 'lat_0=39.0', 'x_0=0.0', - 'y_0=0.0', 'lat_1=33', 'lat_2=45'} + other_args = { + 'ellps=WGS84', + 'lon_0=-96.0', + 'lat_0=39.0', + 'x_0=0.0', + 'y_0=0.0', + 'lat_1=33', + 'lat_2=45', + } check_proj_params('lcc', crs, other_args) # Check the behaviour of !=, == and (not ==) for the different cutoffs. @@ -35,8 +49,7 @@ def test_default_with_cutoff(): assert hash(crs) != hash(default) assert hash(crs) == hash(crs2) - assert_array_almost_equal(crs.y_limits, - (-49788019.81831982, 30793476.08487709)) + assert_array_almost_equal(crs.y_limits, (-49788019.81831982, 30793476.08487709)) def test_sphere(): @@ -52,30 +65,53 @@ def test_sphere(): def test_specific_lambert(): # This projection comes from EPSG Projection 3034 - ETRS89 / ETRS-LCC. - crs = ccrs.LambertConformal(central_longitude=10, - standard_parallels=(35, 65), - central_latitude=52, - false_easting=4000000, - false_northing=2800000, - globe=ccrs.Globe(ellipse='GRS80')) - other_args = {'ellps=GRS80', 'lon_0=10', 'lat_0=52', - 'x_0=4000000', 'y_0=2800000', 'lat_1=35', 'lat_2=65'} + crs = ccrs.LambertConformal( + central_longitude=10, + standard_parallels=(35, 65), + central_latitude=52, + false_easting=4000000, + false_northing=2800000, + globe=ccrs.Globe(ellipse='GRS80'), + ) + other_args = { + 'ellps=GRS80', + 'lon_0=10', + 'lat_0=52', + 'x_0=4000000', + 'y_0=2800000', + 'lat_1=35', + 'lat_2=65', + } check_proj_params('lcc', crs, other_args) def test_lambert_moon(): moon = ccrs.Globe(ellipse=None, semimajor_axis=1737400, semiminor_axis=1737400) crs = ccrs.LambertConformal(globe=moon) - other_args = {'a=1737400', 'b=1737400', 'lat_0=39.0', 'lat_1=33', 'lat_2=45', - 'lon_0=-96.0', 'x_0=0.0', 'y_0=0.0'} + other_args = { + 'a=1737400', + 'b=1737400', + 'lat_0=39.0', + 'lat_1=33', + 'lat_2=45', + 'lon_0=-96.0', + 'x_0=0.0', + 'y_0=0.0', + } check_proj_params('lcc', crs, other_args) class Test_LambertConformal_standard_parallels: def test_single_value(self): - crs = ccrs.LambertConformal(standard_parallels=[1.]) - other_args = {'ellps=WGS84', 'lon_0=-96.0', 'lat_0=39.0', - 'x_0=0.0', 'y_0=0.0', 'lat_1=1.0'} + crs = ccrs.LambertConformal(standard_parallels=[1.0]) + other_args = { + 'ellps=WGS84', + 'lon_0=-96.0', + 'lat_0=39.0', + 'x_0=0.0', + 'y_0=0.0', + 'lat_1=1.0', + } check_proj_params('lcc', crs, other_args) def test_no_parallel(self): @@ -87,33 +123,25 @@ def test_too_many_parallel(self): ccrs.LambertConformal(standard_parallels=[1, 2, 3]) def test_single_spole(self): - s_pole_crs = ccrs.LambertConformal(standard_parallels=[-1.]) + s_pole_crs = ccrs.LambertConformal(standard_parallels=[-1.0]) expected_x = (-19939660, 19939660) expected_y = (-735590302, -8183795) if pyproj.__proj_version__ >= '9.2.0': expected_x = (-19840440, 19840440) expected_y = (-370239953, -8191953) print(s_pole_crs.x_limits) - assert_array_almost_equal(s_pole_crs.x_limits, - expected_x, - decimal=0) - assert_array_almost_equal(s_pole_crs.y_limits, - expected_y, - decimal=0) + assert_array_almost_equal(s_pole_crs.x_limits, expected_x, decimal=0) + assert_array_almost_equal(s_pole_crs.y_limits, expected_y, decimal=0) def test_single_npole(self): - n_pole_crs = ccrs.LambertConformal(standard_parallels=[1.]) + n_pole_crs = ccrs.LambertConformal(standard_parallels=[1.0]) expected_x = (-20130569, 20130569) expected_y = (-8170229, 726200683) if pyproj.__proj_version__ >= '9.2.0': expected_x = (-20222156, 20222156) expected_y = (-8164817, 360848719) - assert_array_almost_equal(n_pole_crs.x_limits, - expected_x, - decimal=0) - assert_array_almost_equal(n_pole_crs.y_limits, - expected_y, - decimal=0) + assert_array_almost_equal(n_pole_crs.x_limits, expected_x, decimal=0) + assert_array_almost_equal(n_pole_crs.y_limits, expected_y, decimal=0) class TestLambertZoneII: @@ -126,13 +154,13 @@ def setup_class(self): def test_default(self): proj = ccrs.LambertZoneII() res = proj.transform_point(*self.point_a, src_crs=self.src_crs) - np.testing.assert_array_almost_equal(res, - (536690.18620, 2348515.62248), - decimal=5) + np.testing.assert_array_almost_equal( + res, (536690.18620, 2348515.62248), decimal=5 + ) res = proj.transform_point(*self.point_b, src_crs=self.src_crs) - np.testing.assert_array_almost_equal(res, - (257199.57387, 2419655.71471), - decimal=5) + np.testing.assert_array_almost_equal( + res, (257199.57387, 2419655.71471), decimal=5 + ) def test_nan(self): proj = ccrs.LambertZoneII() diff --git a/lib/cartopy/tests/crs/test_mercator.py b/lib/cartopy/tests/crs/test_mercator.py index 54dcaacd6..df39c179e 100644 --- a/lib/cartopy/tests/crs/test_mercator.py +++ b/lib/cartopy/tests/crs/test_mercator.py @@ -15,20 +15,20 @@ def test_default(): other_args = {'ellps=WGS84', 'lon_0=0.0', 'x_0=0.0', 'y_0=0.0', 'units=m'} check_proj_params('merc', crs, other_args) - assert_almost_equal(crs.boundary.bounds, - [-20037508, -15496571, 20037508, 18764656], decimal=0) + assert_almost_equal( + crs.boundary.bounds, [-20037508, -15496571, 20037508, 18764656], decimal=0 + ) def test_eccentric_globe(): - globe = ccrs.Globe(semimajor_axis=10000, semiminor_axis=5000, - ellipse=None) + globe = ccrs.Globe(semimajor_axis=10000, semiminor_axis=5000, ellipse=None) crs = ccrs.Mercator(globe=globe, min_latitude=-40, max_latitude=40) - other_args = {'a=10000', 'b=5000', 'lon_0=0.0', 'x_0=0.0', 'y_0=0.0', - 'units=m'} + other_args = {'a=10000', 'b=5000', 'lon_0=0.0', 'x_0=0.0', 'y_0=0.0', 'units=m'} check_proj_params('merc', crs, other_args) - assert_almost_equal(crs.boundary.bounds, - [-31415.93, -2190.5, 31415.93, 2190.5], decimal=2) + assert_almost_equal( + crs.boundary.bounds, [-31415.93, -2190.5, 31415.93, 2190.5], decimal=2 + ) assert_almost_equal(crs.x_limits, [-31415.93, 31415.93], decimal=2) assert_almost_equal(crs.y_limits, [-2190.5, 2190.5], decimal=2) @@ -49,46 +49,64 @@ def test_equality(): @pytest.mark.parametrize('lon', [-10.0, 10.0]) def test_central_longitude(lon): crs = ccrs.Mercator(central_longitude=lon) - other_args = {'ellps=WGS84', f'lon_0={lon}', 'x_0=0.0', 'y_0=0.0', - 'units=m'} + other_args = {'ellps=WGS84', f'lon_0={lon}', 'x_0=0.0', 'y_0=0.0', 'units=m'} check_proj_params('merc', crs, other_args) - assert_almost_equal(crs.boundary.bounds, - [-20037508, -15496570, 20037508, 18764656], decimal=0) + assert_almost_equal( + crs.boundary.bounds, [-20037508, -15496570, 20037508, 18764656], decimal=0 + ) def test_latitude_true_scale(): lat_ts = 20.0 crs = ccrs.Mercator(latitude_true_scale=lat_ts) - other_args = {'ellps=WGS84', 'lon_0=0.0', 'x_0=0.0', 'y_0=0.0', 'units=m', - f'lat_ts={lat_ts}'} + other_args = { + 'ellps=WGS84', + 'lon_0=0.0', + 'x_0=0.0', + 'y_0=0.0', + 'units=m', + f'lat_ts={lat_ts}', + } check_proj_params('merc', crs, other_args) - assert_almost_equal(crs.boundary.bounds, - [-18836475, -14567718, 18836475, 17639917], decimal=0) + assert_almost_equal( + crs.boundary.bounds, [-18836475, -14567718, 18836475, 17639917], decimal=0 + ) def test_easting_northing(): false_easting = 1000000 false_northing = -2000000 - crs = ccrs.Mercator(false_easting=false_easting, - false_northing=false_northing) - other_args = {'ellps=WGS84', 'lon_0=0.0', f'x_0={false_easting}', - f'y_0={false_northing}', 'units=m'} + crs = ccrs.Mercator(false_easting=false_easting, false_northing=false_northing) + other_args = { + 'ellps=WGS84', + 'lon_0=0.0', + f'x_0={false_easting}', + f'y_0={false_northing}', + 'units=m', + } check_proj_params('merc', crs, other_args) - assert_almost_equal(crs.boundary.bounds, - [-19037508, -17496571, 21037508, 16764656], decimal=0) + assert_almost_equal( + crs.boundary.bounds, [-19037508, -17496571, 21037508, 16764656], decimal=0 + ) def test_scale_factor(): # Should be same as lat_ts=20 for a sphere scale_factor = 0.939692620786 - crs = ccrs.Mercator(scale_factor=scale_factor, - globe=ccrs.Globe(ellipse='sphere')) - other_args = {'ellps=sphere', 'lon_0=0.0', 'x_0=0.0', 'y_0=0.0', 'units=m', - f'k_0={scale_factor:.12f}'} + crs = ccrs.Mercator(scale_factor=scale_factor, globe=ccrs.Globe(ellipse='sphere')) + other_args = { + 'ellps=sphere', + 'lon_0=0.0', + 'x_0=0.0', + 'y_0=0.0', + 'units=m', + f'k_0={scale_factor:.12f}', + } check_proj_params('merc', crs, other_args) - assert_almost_equal(crs.boundary.bounds, - [-18808021, -14585266, 18808021, 17653216], decimal=0) + assert_almost_equal( + crs.boundary.bounds, [-18808021, -14585266, 18808021, 17653216], decimal=0 + ) diff --git a/lib/cartopy/tests/crs/test_miller.py b/lib/cartopy/tests/crs/test_miller.py index 2d0719774..4764dba4a 100644 --- a/lib/cartopy/tests/crs/test_miller.py +++ b/lib/cartopy/tests/crs/test_miller.py @@ -20,10 +20,8 @@ def test_default(): other_args = {'a=6378137.0', 'lon_0=0.0'} check_proj_params('mill', mill, other_args) - assert_almost_equal(np.array(mill.x_limits), - [-20037508.3427892, 20037508.3427892]) - assert_almost_equal(np.array(mill.y_limits), - [-14691480.7691731, 14691480.7691731]) + assert_almost_equal(np.array(mill.x_limits), [-20037508.3427892, 20037508.3427892]) + assert_almost_equal(np.array(mill.y_limits), [-14691480.7691731, 14691480.7691731]) def test_sphere_globe(): @@ -38,8 +36,7 @@ def test_sphere_globe(): def test_ellipse_globe(): globe = ccrs.Globe(ellipse='WGS84') - with pytest.warns(UserWarning, - match='does not handle elliptical globes.') as w: + with pytest.warns(UserWarning, match='does not handle elliptical globes.') as w: mill = ccrs.Miller(globe=globe) assert len(w) == 1 @@ -49,17 +46,15 @@ def test_ellipse_globe(): # Limits are the same as spheres (but not the default radius) since # ellipses are not supported. mill_sph = ccrs.Miller( - globe=ccrs.Globe(semimajor_axis=ccrs.WGS84_SEMIMAJOR_AXIS, - ellipse=None)) + globe=ccrs.Globe(semimajor_axis=ccrs.WGS84_SEMIMAJOR_AXIS, ellipse=None) + ) assert_almost_equal(mill.x_limits, mill_sph.x_limits) assert_almost_equal(mill.y_limits, mill_sph.y_limits) def test_eccentric_globe(): - globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, - ellipse=None) - with pytest.warns(UserWarning, - match='does not handle elliptical globes.') as w: + globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, ellipse=None) + with pytest.warns(UserWarning, match='does not handle elliptical globes.') as w: mill = ccrs.Miller(globe=globe) assert len(w) == 1 @@ -77,10 +72,8 @@ def test_central_longitude(lon): other_args = {'a=6378137.0', f'lon_0={lon}'} check_proj_params('mill', mill, other_args) - assert_almost_equal(np.array(mill.x_limits), - [-20037508.3427892, 20037508.3427892]) - assert_almost_equal(np.array(mill.y_limits), - [-14691480.7691731, 14691480.7691731]) + assert_almost_equal(np.array(mill.x_limits), [-20037508.3427892, 20037508.3427892]) + assert_almost_equal(np.array(mill.y_limits), [-14691480.7691731, 14691480.7691731]) def test_grid(): @@ -92,18 +85,34 @@ def test_grid(): other_args = {'a=1.0', 'lon_0=0.0'} check_proj_params('mill', mill, other_args) - assert_almost_equal(np.array(mill.x_limits), - [-3.14159265, 3.14159265]) - assert_almost_equal(np.array(mill.y_limits), - [-2.3034125, 2.3034125]) + assert_almost_equal(np.array(mill.x_limits), [-3.14159265, 3.14159265]) + assert_almost_equal(np.array(mill.y_limits), [-2.3034125, 2.3034125]) lats, lons = np.mgrid[0:91:5, 0:91:10].reshape((2, -1)) expected_x = np.deg2rad(lons) - expected_y = np.array([ - 2.30341, 2.04742, 1.83239, 1.64620, 1.48131, 1.33270, 1.19683, 1.07113, - 0.95364, 0.84284, 0.73754, 0.63674, 0.53962, 0.44547, 0.35369, 0.26373, - 0.17510, 0.08734, 0.00000, - ])[::-1].repeat(10) + expected_y = np.array( + [ + 2.30341, + 2.04742, + 1.83239, + 1.64620, + 1.48131, + 1.33270, + 1.19683, + 1.07113, + 0.95364, + 0.84284, + 0.73754, + 0.63674, + 0.53962, + 0.44547, + 0.35369, + 0.26373, + 0.17510, + 0.08734, + 0.00000, + ] + )[::-1].repeat(10) result = mill.transform_points(geodetic, lons, lats) assert_almost_equal(result[:, 0], expected_x, decimal=5) @@ -119,10 +128,8 @@ def test_sphere_transform(): other_args = {'a=1.0', 'lon_0=0.0'} check_proj_params('mill', mill, other_args) - assert_almost_equal(np.array(mill.x_limits), - [-3.14159265, 3.14159265]) - assert_almost_equal(np.array(mill.y_limits), - [-2.3034125, 2.3034125]) + assert_almost_equal(np.array(mill.x_limits), [-3.14159265, 3.14159265]) + assert_almost_equal(np.array(mill.y_limits), [-2.3034125, 2.3034125]) result = mill.transform_point(-75.0, 50.0, geodetic) assert_almost_equal(result, [-1.3089969, 0.9536371]) diff --git a/lib/cartopy/tests/crs/test_mollweide.py b/lib/cartopy/tests/crs/test_mollweide.py index 9dd2573e1..c4d364f1b 100644 --- a/lib/cartopy/tests/crs/test_mollweide.py +++ b/lib/cartopy/tests/crs/test_mollweide.py @@ -20,10 +20,8 @@ def test_default(): other_args = {'a=6378137.0', 'lon_0=0'} check_proj_params('moll', moll, other_args) - assert_allclose(np.array(moll.x_limits), - [-18040095.6961473, 18040095.6961473]) - assert_allclose(np.array(moll.y_limits), - [-9020047.8480736, 9020047.8480736]) + assert_allclose(np.array(moll.x_limits), [-18040095.6961473, 18040095.6961473]) + assert_allclose(np.array(moll.y_limits), [-9020047.8480736, 9020047.8480736]) def test_sphere_globe(): @@ -38,8 +36,7 @@ def test_sphere_globe(): def test_ellipse_globe(): globe = ccrs.Globe(ellipse='WGS84') - with pytest.warns(UserWarning, - match='does not handle elliptical globes.') as w: + with pytest.warns(UserWarning, match='does not handle elliptical globes.') as w: moll = ccrs.Mollweide(globe=globe) assert len(w) == 1 @@ -52,10 +49,8 @@ def test_ellipse_globe(): def test_eccentric_globe(): - globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, - ellipse=None) - with pytest.warns(UserWarning, - match='does not handle elliptical globes.') as w: + globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, ellipse=None) + with pytest.warns(UserWarning, match='does not handle elliptical globes.') as w: moll = ccrs.Mollweide(globe=globe) assert len(w) == 1 @@ -82,16 +77,13 @@ def test_central_longitude(lon): other_args = {'a=6378137.0', f'lon_0={lon}'} check_proj_params('moll', moll, other_args) - assert_allclose(np.array(moll.x_limits), - [-18040095.6961473, 18040095.6961473]) - assert_allclose(np.array(moll.y_limits), - [-9020047.8480736, 9020047.8480736]) + assert_allclose(np.array(moll.x_limits), [-18040095.6961473, 18040095.6961473]) + assert_allclose(np.array(moll.y_limits), [-9020047.8480736, 9020047.8480736]) def test_grid(): # USGS Professional Paper 1395, pg 252, Table 42 - globe = ccrs.Globe(ellipse=None, - semimajor_axis=0.5**0.5, semiminor_axis=0.5**0.5) + globe = ccrs.Globe(ellipse=None, semimajor_axis=0.5**0.5, semiminor_axis=0.5**0.5) moll = ccrs.Mollweide(globe=globe) geodetic = moll.as_geodetic() @@ -105,36 +97,68 @@ def test_grid(): lons = np.full_like(lats, 90) result = moll.transform_points(geodetic, lons, lats) - expected_x = np.array([ - 0.00000, 0.20684, 0.32593, 0.42316, 0.50706, 0.58111, 0.64712, 0.70617, - 0.75894, 0.80591, 0.84739, 0.88362, 0.91477, 0.94096, 0.96229, 0.97882, - 0.99060, 0.99765, 1.00000, - ]) + expected_x = np.array( + [ + 0.00000, + 0.20684, + 0.32593, + 0.42316, + 0.50706, + 0.58111, + 0.64712, + 0.70617, + 0.75894, + 0.80591, + 0.84739, + 0.88362, + 0.91477, + 0.94096, + 0.96229, + 0.97882, + 0.99060, + 0.99765, + 1.00000, + ] + ) assert_allclose(result[:, 0], expected_x, atol=1e-5) - expected_y = np.array([ - 1.00000, 0.97837, 0.94539, 0.90606, 0.86191, 0.81382, 0.76239, 0.70804, - 0.65116, 0.59204, 0.53097, 0.46820, 0.40397, 0.33850, 0.27201, 0.20472, - 0.13681, 0.06851, 0.00000, - ]) + expected_y = np.array( + [ + 1.00000, + 0.97837, + 0.94539, + 0.90606, + 0.86191, + 0.81382, + 0.76239, + 0.70804, + 0.65116, + 0.59204, + 0.53097, + 0.46820, + 0.40397, + 0.33850, + 0.27201, + 0.20472, + 0.13681, + 0.06851, + 0.00000, + ] + ) assert_allclose(result[:, 1], expected_y, atol=1e-5) def test_sphere_transform(): # USGS Professional Paper 1395, pg 367 - globe = ccrs.Globe(semimajor_axis=1.0, semiminor_axis=1.0, - ellipse=None) - moll = ccrs.Mollweide(central_longitude=-90.0, - globe=globe) + globe = ccrs.Globe(semimajor_axis=1.0, semiminor_axis=1.0, ellipse=None) + moll = ccrs.Mollweide(central_longitude=-90.0, globe=globe) geodetic = moll.as_geodetic() other_args = {'a=1.0', 'b=1.0', 'lon_0=-90.0'} check_proj_params('moll', moll, other_args) - assert_allclose(np.array(moll.x_limits), - [-2.8284271247461903, 2.8284271247461903]) - assert_allclose(np.array(moll.y_limits), - [-1.4142135623730951, 1.4142135623730951]) + assert_allclose(np.array(moll.x_limits), [-2.8284271247461903, 2.8284271247461903]) + assert_allclose(np.array(moll.y_limits), [-1.4142135623730951, 1.4142135623730951]) result = moll.transform_point(-75.0, -50.0, geodetic) assert_allclose(result, [0.1788845, -0.9208758]) diff --git a/lib/cartopy/tests/crs/test_nearside_perspective.py b/lib/cartopy/tests/crs/test_nearside_perspective.py index 2a2d107b3..9196d75da 100644 --- a/lib/cartopy/tests/crs/test_nearside_perspective.py +++ b/lib/cartopy/tests/crs/test_nearside_perspective.py @@ -15,39 +15,65 @@ def test_default(): geos = ccrs.NearsidePerspective() - other_args = {'a=6378137.0', 'h=35785831', 'lat_0=0.0', 'lon_0=0.0', - 'units=m', 'x_0=0', 'y_0=0'} + other_args = { + 'a=6378137.0', + 'h=35785831', + 'lat_0=0.0', + 'lon_0=0.0', + 'units=m', + 'x_0=0', + 'y_0=0', + } check_proj_params('nsper', geos, other_args) - assert_almost_equal(geos.boundary.bounds, - (-5476336.098, -5476336.098, - 5476336.098, 5476336.098), - decimal=3) + assert_almost_equal( + geos.boundary.bounds, + (-5476336.098, -5476336.098, 5476336.098, 5476336.098), + decimal=3, + ) def test_offset(): - geos = ccrs.NearsidePerspective(false_easting=5000000, - false_northing=-123000,) - other_args = {'a=6378137.0', 'h=35785831', 'lat_0=0.0', 'lon_0=0.0', - 'units=m', 'x_0=5000000', 'y_0=-123000'} + geos = ccrs.NearsidePerspective( + false_easting=5000000, + false_northing=-123000, + ) + other_args = { + 'a=6378137.0', + 'h=35785831', + 'lat_0=0.0', + 'lon_0=0.0', + 'units=m', + 'x_0=5000000', + 'y_0=-123000', + } check_proj_params('nsper', geos, other_args) - assert_almost_equal(geos.boundary.bounds, - (-476336.098, -5599336.098, - 10476336.098, 5353336.098), - decimal=4) + assert_almost_equal( + geos.boundary.bounds, + (-476336.098, -5599336.098, 10476336.098, 5353336.098), + decimal=4, + ) def test_central_latitude(): # Check the effect of the added 'central_latitude' key. geos = ccrs.NearsidePerspective(central_latitude=53.7) - other_args = {'a=6378137.0', 'h=35785831', 'lat_0=53.7', 'lon_0=0.0', - 'units=m', 'x_0=0', 'y_0=0'} + other_args = { + 'a=6378137.0', + 'h=35785831', + 'lat_0=53.7', + 'lon_0=0.0', + 'units=m', + 'x_0=0', + 'y_0=0', + } check_proj_params('nsper', geos, other_args) - assert_almost_equal(geos.boundary.bounds, - (-5476336.098, -5476336.098, - 5476336.098, 5476336.098), - decimal=3) + assert_almost_equal( + geos.boundary.bounds, + (-5476336.098, -5476336.098, 5476336.098, 5476336.098), + decimal=3, + ) diff --git a/lib/cartopy/tests/crs/test_oblique_mercator.py b/lib/cartopy/tests/crs/test_oblique_mercator.py index adf5a1ac7..d711c2479 100644 --- a/lib/cartopy/tests/crs/test_oblique_mercator.py +++ b/lib/cartopy/tests/crs/test_oblique_mercator.py @@ -36,14 +36,14 @@ class TestCrsArgs: point_a_plate_carree = (-3.474083, 50.727301) point_b_plate_carree = (0.5, 50.5) proj_kwargs_default = dict( - ellps="WGS84", - lonc="0.0", - lat_0="0.0", - k="1.0", - x_0="0.0", - y_0="0.0", - alpha="0.0", - units="m", + ellps='WGS84', + lonc='0.0', + lat_0='0.0', + k='1.0', + x_0='0.0', + y_0='0.0', + alpha='0.0', + units='m', ) class ParamTuple(NamedTuple): @@ -55,57 +55,57 @@ class ParamTuple(NamedTuple): param_list: List[ParamTuple] = [ ParamTuple( - "default", + 'default', dict(), dict(), (-245106.75804, 5626768.52447), (35451.51708, 5595849.69689), ), ParamTuple( - "azimuth", + 'azimuth', dict(azimuth=90.0), - dict(alpha="89.999"), + dict(alpha='89.999'), (-386712.17018, 6540102.97351), (55680.57266, 6500330.56121), ), ParamTuple( - "central_longitude", + 'central_longitude', dict(central_longitude=90.0), - dict(lonc="90.0"), + dict(lonc='90.0'), (-4739202.85619, 10329047.01897), (-4786583.034, 9966930.01085), ), ParamTuple( - "central_latitude", + 'central_latitude', dict(central_latitude=45.0), - dict(lat_0="45.0"), + dict(lat_0='45.0'), (-245269.04118, 642564.31415), (35474.56405, 611638.73957), ), ParamTuple( - "false_easting_northing", + 'false_easting_northing', dict(false_easting=1000000, false_northing=-2000000), - dict(x_0="1000000", y_0="-2000000"), + dict(x_0='1000000', y_0='-2000000'), (754893.24196, 3626768.52447), (1035451.51708, 3595849.69689), ), ParamTuple( - "scale_factor", + 'scale_factor', # Number inherited from test_mercator.py . dict(scale_factor=0.939692620786), - dict(k="0.939692620786"), + dict(k='0.939692620786'), (-230325.01183, 5287432.86131), (33313.52899, 5258378.6672), ), ParamTuple( - "globe", - dict(globe=ccrs.Globe(ellipse="sphere")), - dict(ellps="sphere"), + 'globe', + dict(globe=ccrs.Globe(ellipse='sphere')), + dict(ellps='sphere'), (-244502.86059, 5646357.44304), (35364.23322, 5615460.21872), ), ParamTuple( - "combo", + 'combo', dict( azimuth=90.0, central_longitude=90.0, @@ -113,16 +113,16 @@ class ParamTuple(NamedTuple): false_easting=1000000, false_northing=-2000000, scale_factor=0.939692620786, - globe=ccrs.Globe(ellipse="sphere"), + globe=ccrs.Globe(ellipse='sphere'), ), dict( - alpha="89.999", - lonc="90.0", - lat_0="45.0", - x_0="1000000", - y_0="-2000000", - k="0.939692620786", - ellps="sphere", + alpha='89.999', + lonc='90.0', + lat_0='45.0', + x_0='1000000', + y_0='-2000000', + k='0.939692620786', + ellps='sphere', ), (-4279982.08123, 1916861.68937), (-4138080.80706, 1631302.04295), @@ -135,18 +135,14 @@ def make_variant_inputs(self, request) -> None: inputs: TestCrsArgs.ParamTuple = request.param self.oblique_mercator = ccrs.ObliqueMercator(**inputs.crs_kwargs) - proj_kwargs_expected = dict( - self.proj_kwargs_default, **inputs.proj_kwargs - ) - self.proj_params = { - f"{k}={v}" for k, v in proj_kwargs_expected.items() - } + proj_kwargs_expected = dict(self.proj_kwargs_default, **inputs.proj_kwargs) + self.proj_params = {f'{k}={v}' for k, v in proj_kwargs_expected.items()} self.expected_a = inputs.expected_a self.expected_b = inputs.expected_b def test_proj_params(self): - check_proj_params("omerc", self.oblique_mercator, self.proj_params) + check_proj_params('omerc', self.oblique_mercator, self.proj_params) def test_transform_point(self, plate_carree): # (Point equivalence has been confirmed via plotting). @@ -183,9 +179,7 @@ def test_equality(oblique_variants): assert hash(alt_1) == hash(alt_2) -@pytest.mark.parametrize( - "reverse_coord", [False, True], ids=["xy_order", "yx_order"] -) +@pytest.mark.parametrize('reverse_coord', [False, True], ids=['xy_order', 'yx_order']) def test_nan(oblique_mercator, plate_carree, reverse_coord): coord = (0.0, np.nan) if reverse_coord: diff --git a/lib/cartopy/tests/crs/test_orthographic.py b/lib/cartopy/tests/crs/test_orthographic.py index cb1797886..a728c9b63 100644 --- a/lib/cartopy/tests/crs/test_orthographic.py +++ b/lib/cartopy/tests/crs/test_orthographic.py @@ -22,10 +22,8 @@ def test_default(): check_proj_params('ortho', ortho, other_args) # WGS84 radius * 0.99999 - assert_almost_equal(np.array(ortho.x_limits), - [-6378073.21863, 6378073.21863]) - assert_almost_equal(np.array(ortho.y_limits), - [-6378073.21863, 6378073.21863]) + assert_almost_equal(np.array(ortho.x_limits), [-6378073.21863, 6378073.21863]) + assert_almost_equal(np.array(ortho.y_limits), [-6378073.21863, 6378073.21863]) def test_sphere_globe(): @@ -40,8 +38,7 @@ def test_sphere_globe(): def test_ellipse_globe(): globe = ccrs.Globe(ellipse='WGS84') - with pytest.warns(UserWarning, - match='does not handle elliptical globes.') as w: + with pytest.warns(UserWarning, match='does not handle elliptical globes.') as w: ortho = ccrs.Orthographic(globe=globe) assert len(w) == 1 @@ -54,10 +51,8 @@ def test_ellipse_globe(): def test_eccentric_globe(): - globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, - ellipse=None) - with pytest.warns(UserWarning, - match='does not handle elliptical globes.') as w: + globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, ellipse=None) + with pytest.warns(UserWarning, match='does not handle elliptical globes.') as w: ortho = ccrs.Orthographic(globe=globe) assert len(w) == 1 @@ -73,84 +68,187 @@ def test_eccentric_globe(): @pytest.mark.parametrize('lon', [-10, 0, 10]) def test_central_params(lon, lat): ortho = ccrs.Orthographic(central_latitude=lat, central_longitude=lon) - other_args = {f'lat_0={lat}', f'lon_0={lon}', - 'a=6378137.0', 'alpha=0.0'} + other_args = {f'lat_0={lat}', f'lon_0={lon}', 'a=6378137.0', 'alpha=0.0'} check_proj_params('ortho', ortho, other_args) # WGS84 radius * 0.99999 - assert_almost_equal(np.array(ortho.x_limits), - [-6378073.21863, 6378073.21863]) - assert_almost_equal(np.array(ortho.y_limits), - [-6378073.21863, 6378073.21863]) + assert_almost_equal(np.array(ortho.x_limits), [-6378073.21863, 6378073.21863]) + assert_almost_equal(np.array(ortho.y_limits), [-6378073.21863, 6378073.21863]) def test_grid(): # USGS Professional Paper 1395, pg 151, Table 22 - globe = ccrs.Globe(ellipse=None, - semimajor_axis=1.0, semiminor_axis=1.0) + globe = ccrs.Globe(ellipse=None, semimajor_axis=1.0, semiminor_axis=1.0) ortho = ccrs.Orthographic(globe=globe) geodetic = ortho.as_geodetic() other_args = {'a=1.0', 'b=1.0', 'lon_0=0.0', 'lat_0=0.0', 'alpha=0.0'} check_proj_params('ortho', ortho, other_args) - assert_almost_equal(np.array(ortho.x_limits), - [-0.99999, 0.99999]) - assert_almost_equal(np.array(ortho.y_limits), - [-0.99999, 0.99999]) + assert_almost_equal(np.array(ortho.x_limits), [-0.99999, 0.99999]) + assert_almost_equal(np.array(ortho.y_limits), [-0.99999, 0.99999]) lats, lons = np.mgrid[0:100:10, 0:100:10].reshape((2, -1)) - expected_x = np.array([ - [0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000], - [0.0000, 0.0302, 0.0594, 0.0868, 0.1116, 0.1330, 0.1504, 0.1632, - 0.1710, 0.1736], - [0.0000, 0.0594, 0.1170, 0.1710, 0.2198, 0.2620, 0.2962, 0.3214, - 0.3368, 0.3420], - [0.0000, 0.0868, 0.1710, 0.2500, 0.3214, 0.3830, 0.4330, 0.4698, - 0.4924, 0.5000], - [0.0000, 0.1116, 0.2198, 0.3214, 0.4132, 0.4924, 0.5567, 0.6040, - 0.6330, 0.6428], - [0.0000, 0.1330, 0.2620, 0.3830, 0.4924, 0.5868, 0.6634, 0.7198, - 0.7544, 0.7660], - [0.0000, 0.1504, 0.2962, 0.4330, 0.5567, 0.6634, 0.7500, 0.8138, - 0.8529, 0.8660], - [0.0000, 0.1632, 0.3214, 0.4698, 0.6040, 0.7198, 0.8138, 0.8830, - 0.9254, 0.9397], - [0.0000, 0.1710, 0.3368, 0.4924, 0.6330, 0.7544, 0.8529, 0.9254, - 0.9698, 0.9848], - [0.0000, 0.1736, 0.3420, 0.5000, 0.6428, 0.7660, 0.8660, 0.9397, - 0.9848, 1.0000], - ])[::-1, :].ravel() - expected_y = np.array([ - 1.0000, 0.9848, 0.9397, 0.8660, 0.7660, 0.6428, 0.5000, 0.3420, 0.1736, - 0.0000, - ])[::-1].repeat(10) + expected_x = np.array( + [ + [ + 0.0000, + 0.0000, + 0.0000, + 0.0000, + 0.0000, + 0.0000, + 0.0000, + 0.0000, + 0.0000, + 0.0000, + ], + [ + 0.0000, + 0.0302, + 0.0594, + 0.0868, + 0.1116, + 0.1330, + 0.1504, + 0.1632, + 0.1710, + 0.1736, + ], + [ + 0.0000, + 0.0594, + 0.1170, + 0.1710, + 0.2198, + 0.2620, + 0.2962, + 0.3214, + 0.3368, + 0.3420, + ], + [ + 0.0000, + 0.0868, + 0.1710, + 0.2500, + 0.3214, + 0.3830, + 0.4330, + 0.4698, + 0.4924, + 0.5000, + ], + [ + 0.0000, + 0.1116, + 0.2198, + 0.3214, + 0.4132, + 0.4924, + 0.5567, + 0.6040, + 0.6330, + 0.6428, + ], + [ + 0.0000, + 0.1330, + 0.2620, + 0.3830, + 0.4924, + 0.5868, + 0.6634, + 0.7198, + 0.7544, + 0.7660, + ], + [ + 0.0000, + 0.1504, + 0.2962, + 0.4330, + 0.5567, + 0.6634, + 0.7500, + 0.8138, + 0.8529, + 0.8660, + ], + [ + 0.0000, + 0.1632, + 0.3214, + 0.4698, + 0.6040, + 0.7198, + 0.8138, + 0.8830, + 0.9254, + 0.9397, + ], + [ + 0.0000, + 0.1710, + 0.3368, + 0.4924, + 0.6330, + 0.7544, + 0.8529, + 0.9254, + 0.9698, + 0.9848, + ], + [ + 0.0000, + 0.1736, + 0.3420, + 0.5000, + 0.6428, + 0.7660, + 0.8660, + 0.9397, + 0.9848, + 1.0000, + ], + ] + )[::-1, :].ravel() + expected_y = np.array( + [ + 1.0000, + 0.9848, + 0.9397, + 0.8660, + 0.7660, + 0.6428, + 0.5000, + 0.3420, + 0.1736, + 0.0000, + ] + )[::-1].repeat(10) # Test all quadrants; they are symmetrical. for lon_sign in [1, -1]: for lat_sign in [1, -1]: - result = ortho.transform_points(geodetic, - lon_sign * lons, lat_sign * lats) + result = ortho.transform_points(geodetic, lon_sign * lons, lat_sign * lats) assert_almost_equal(result[:, 0], lon_sign * expected_x, decimal=4) assert_almost_equal(result[:, 1], lat_sign * expected_y, decimal=4) def test_sphere_transform(): # USGS Professional Paper 1395, pp 311 - 312 - globe = ccrs.Globe(semimajor_axis=1.0, semiminor_axis=1.0, - ellipse=None) - ortho = ccrs.Orthographic(central_latitude=40.0, central_longitude=-100.0, - globe=globe) + globe = ccrs.Globe(semimajor_axis=1.0, semiminor_axis=1.0, ellipse=None) + ortho = ccrs.Orthographic( + central_latitude=40.0, central_longitude=-100.0, globe=globe + ) geodetic = ortho.as_geodetic() other_args = {'a=1.0', 'b=1.0', 'lon_0=-100.0', 'lat_0=40.0', 'alpha=0.0'} check_proj_params('ortho', ortho, other_args) - assert_almost_equal(np.array(ortho.x_limits), - [-0.99999, 0.99999]) - assert_almost_equal(np.array(ortho.y_limits), - [-0.99999, 0.99999]) + assert_almost_equal(np.array(ortho.x_limits), [-0.99999, 0.99999]) + assert_almost_equal(np.array(ortho.y_limits), [-0.99999, 0.99999]) result = ortho.transform_point(-110.0, 30.0, geodetic) assert_almost_equal(result, np.array([-0.1503837, -0.1651911])) @@ -158,25 +256,23 @@ def test_sphere_transform(): inverse_result = geodetic.transform_point(result[0], result[1], ortho) assert_almost_equal(inverse_result, [-110.0, 30.0]) + def test_sphere_rotate(): - globe = ccrs.Globe(semimajor_axis=1.0, semiminor_axis=1.0, - ellipse=None) - ortho = ccrs.Orthographic(central_latitude=40.0, central_longitude=-100.0, - azimuth=180.0, globe=globe) + globe = ccrs.Globe(semimajor_axis=1.0, semiminor_axis=1.0, ellipse=None) + ortho = ccrs.Orthographic( + central_latitude=40.0, central_longitude=-100.0, azimuth=180.0, globe=globe + ) geodetic = ortho.as_geodetic() - other_args = {'a=1.0', 'b=1.0', 'lon_0=-100.0', 'lat_0=40.0', - 'alpha=180.0'} + other_args = {'a=1.0', 'b=1.0', 'lon_0=-100.0', 'lat_0=40.0', 'alpha=180.0'} check_proj_params('ortho', ortho, other_args) - assert_almost_equal(np.array(ortho.x_limits), - [-0.99999, 0.99999]) - assert_almost_equal(np.array(ortho.y_limits), - [-0.99999, 0.99999]) + assert_almost_equal(np.array(ortho.x_limits), [-0.99999, 0.99999]) + assert_almost_equal(np.array(ortho.y_limits), [-0.99999, 0.99999]) result = ortho.transform_point(-110.0, 30.0, geodetic) - if pyproj.__proj_version__ >= '9.5.0': # support for alpha (azimuthal rotation) - assert_almost_equal(result, np.array([ 0.1503837, 0.1651911])) + if pyproj.__proj_version__ >= '9.5.0': # support for alpha (azimuthal rotation) + assert_almost_equal(result, np.array([0.1503837, 0.1651911])) else: assert_almost_equal(result, np.array([-0.1503837, -0.1651911])) diff --git a/lib/cartopy/tests/crs/test_robinson.py b/lib/cartopy/tests/crs/test_robinson.py index ce08ad864..e4e30b661 100644 --- a/lib/cartopy/tests/crs/test_robinson.py +++ b/lib/cartopy/tests/crs/test_robinson.py @@ -40,8 +40,7 @@ def test_sphere_globe(): def test_ellipse_globe(): globe = ccrs.Globe(ellipse='WGS84') - with pytest.warns(UserWarning, - match='does not handle elliptical globes.') as w: + with pytest.warns(UserWarning, match='does not handle elliptical globes.') as w: robin = ccrs.Robinson(globe=globe) assert len(w) == 1 @@ -54,10 +53,8 @@ def test_ellipse_globe(): def test_eccentric_globe(): - globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, - ellipse=None) - with pytest.warns(UserWarning, - match='does not handle elliptical globes.') as w: + globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, ellipse=None) + with pytest.warns(UserWarning, match='does not handle elliptical globes.') as w: robin = ccrs.Robinson(globe=globe) assert len(w) == 1 @@ -84,8 +81,9 @@ def test_central_longitude(lon): other_args = {'a=6378137.0', f'lon_0={lon}'} check_proj_params('robin', robin, other_args) - assert_almost_equal(robin.x_limits, [-17005833.3305252, 17005833.3305252], - decimal=5) + assert_almost_equal( + robin.x_limits, [-17005833.3305252, 17005833.3305252], decimal=5 + ) assert_almost_equal(robin.y_limits, [-8625154.6651000, 8625154.6651000]) @@ -117,29 +115,20 @@ def test_transform_points(): """ # these always worked - result = _CRS_ROB.transform_points(_CRS_PC, - np.array([35.0]), - np.array([70.0])) - assert_array_almost_equal(result, - [[2376187.2182271, 7275318.1162980, 0]]) - - result = _CRS_ROB.transform_points(_CRS_PC, - np.array([35.0]), - np.array([70.0]), - np.array([0.0])) - assert_array_almost_equal(result, - [[2376187.2182271, 7275318.1162980, 0]]) + result = _CRS_ROB.transform_points(_CRS_PC, np.array([35.0]), np.array([70.0])) + assert_array_almost_equal(result, [[2376187.2182271, 7275318.1162980, 0]]) + + result = _CRS_ROB.transform_points( + _CRS_PC, np.array([35.0]), np.array([70.0]), np.array([0.0]) + ) + assert_array_almost_equal(result, [[2376187.2182271, 7275318.1162980, 0]]) # this always did something, but result has altered - result = _CRS_ROB.transform_points(_CRS_PC, - np.array([np.nan]), - np.array([70.0])) + result = _CRS_ROB.transform_points(_CRS_PC, np.array([np.nan]), np.array([70.0])) assert np.all(np.isnan(result)) # this used to crash + is now fixed - result = _CRS_ROB.transform_points(_CRS_PC, - np.array([35.0]), - np.array([np.nan])) + result = _CRS_ROB.transform_points(_CRS_PC, np.array([35.0]), np.array([np.nan])) assert np.all(np.isnan(result)) # multipoint case @@ -147,12 +136,15 @@ def test_transform_points(): y = np.array([10.0, np.nan, 10.0, 77.7, 55.5, 0.0]) z = np.array([10.0, 0.0, 0.0, np.nan, 55.5, 0.0]) expect_result = np.array( - [[9.40422591e+05, 1.06952091e+06, 1.00000000e+01], - [11.1, 11.2, 11.3], - [0.0, 1069520.91213902, 0.0], - [22.1, 22.2, 22.3], - [33.1, 33.2, 33.3], - [0.0, 0.0, 0.0]]) + [ + [9.40422591e05, 1.06952091e06, 1.00000000e01], + [11.1, 11.2, 11.3], + [0.0, 1069520.91213902, 0.0], + [22.1, 22.2, 22.3], + [33.1, 33.2, 33.3], + [0.0, 0.0, 0.0], + ] + ) result = _CRS_ROB.transform_points(_CRS_PC, x, y, z) assert result.shape == (6, 3) assert np.all(np.isnan(result[[1, 3, 4], :])) diff --git a/lib/cartopy/tests/crs/test_rotated_geodetic.py b/lib/cartopy/tests/crs/test_rotated_geodetic.py index b4aca6627..a4d59784c 100644 --- a/lib/cartopy/tests/crs/test_rotated_geodetic.py +++ b/lib/cartopy/tests/crs/test_rotated_geodetic.py @@ -11,12 +11,15 @@ from .helpers import check_proj_params -common_other_args = {'o_proj=latlon', 'to_meter=111319.4907932736', - 'a=6378137.0'} +common_other_args = {'o_proj=latlon', 'to_meter=111319.4907932736', 'a=6378137.0'} def test_default(): geos = ccrs.RotatedPole(60, 50, 80) - other_args = common_other_args | {'ellps=WGS84', 'lon_0=240', 'o_lat_p=50', - 'o_lon_p=80'} + other_args = common_other_args | { + 'ellps=WGS84', + 'lon_0=240', + 'o_lat_p=50', + 'o_lon_p=80', + } check_proj_params('ob_tran', geos, other_args) diff --git a/lib/cartopy/tests/crs/test_rotated_pole.py b/lib/cartopy/tests/crs/test_rotated_pole.py index 15bca9700..246a89d99 100644 --- a/lib/cartopy/tests/crs/test_rotated_pole.py +++ b/lib/cartopy/tests/crs/test_rotated_pole.py @@ -16,6 +16,11 @@ def test_default(): geos = ccrs.RotatedGeodetic(30, 15, 27) - other_args = {'datum=WGS84', 'ellps=WGS84', 'lon_0=210', 'o_lat_p=15', - 'o_lon_p=27'} | common_other_args + other_args = { + 'datum=WGS84', + 'ellps=WGS84', + 'lon_0=210', + 'o_lat_p=15', + 'o_lon_p=27', + } | common_other_args check_proj_params('ob_tran', geos, other_args) diff --git a/lib/cartopy/tests/crs/test_sinusoidal.py b/lib/cartopy/tests/crs/test_sinusoidal.py index 3660ba921..24af02c91 100644 --- a/lib/cartopy/tests/crs/test_sinusoidal.py +++ b/lib/cartopy/tests/crs/test_sinusoidal.py @@ -18,16 +18,15 @@ def test_default(self): other_args = {'ellps=WGS84', 'lon_0=0.0', 'x_0=0.0', 'y_0=0.0'} check_proj_params('sinu', crs, other_args) - assert_almost_equal(np.array(crs.x_limits), - [-20037508.3428, 20037508.3428], - decimal=4) - assert_almost_equal(np.array(crs.y_limits), - [-10001965.7293, 10001965.7293], - decimal=4) + assert_almost_equal( + np.array(crs.x_limits), [-20037508.3428, 20037508.3428], decimal=4 + ) + assert_almost_equal( + np.array(crs.y_limits), [-10001965.7293, 10001965.7293], decimal=4 + ) def test_eccentric_globe(self): - globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, - ellipse=None) + globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, ellipse=None) crs = ccrs.Sinusoidal(globe=globe) other_args = {'a=1000', 'b=500', 'lon_0=0.0', 'x_0=0.0', 'y_0=0.0'} check_proj_params('sinu', crs, other_args) @@ -36,16 +35,13 @@ def test_eccentric_globe(self): expected_y = [-1216.60, 1216.60] if pyproj.__proj_version__ >= '9.2.0': expected_x = [-3141.60, 3141.60] - expected_y = [-1211.05, 1211.05] - assert_almost_equal(np.array(crs.x_limits), - expected_x, decimal=2) - assert_almost_equal(np.array(crs.y_limits), - expected_y, decimal=2) + expected_y = [-1211.05, 1211.05] + assert_almost_equal(np.array(crs.x_limits), expected_x, decimal=2) + assert_almost_equal(np.array(crs.y_limits), expected_y, decimal=2) def test_offset(self): crs = ccrs.Sinusoidal() - crs_offset = ccrs.Sinusoidal(false_easting=1234, - false_northing=-4321) + crs_offset = ccrs.Sinusoidal(false_easting=1234, false_northing=-4321) other_args = {'ellps=WGS84', 'lon_0=0.0', 'x_0=1234', 'y_0=-4321'} check_proj_params('sinu', crs_offset, other_args) assert tuple(np.array(crs.x_limits) + 1234) == crs_offset.x_limits @@ -54,16 +50,15 @@ def test_offset(self): @pytest.mark.parametrize('lon', [-10.0, 10.0]) def test_central_longitude(self, lon): crs = ccrs.Sinusoidal(central_longitude=lon) - other_args = {'ellps=WGS84', f'lon_0={lon}', - 'x_0=0.0', 'y_0=0.0'} + other_args = {'ellps=WGS84', f'lon_0={lon}', 'x_0=0.0', 'y_0=0.0'} check_proj_params('sinu', crs, other_args) - assert_almost_equal(np.array(crs.x_limits), - [-20037508.3428, 20037508.3428], - decimal=4) - assert_almost_equal(np.array(crs.y_limits), - [-10001965.7293, 10001965.7293], - decimal=4) + assert_almost_equal( + np.array(crs.x_limits), [-20037508.3428, 20037508.3428], decimal=4 + ) + assert_almost_equal( + np.array(crs.y_limits), [-10001965.7293, 10001965.7293], decimal=4 + ) def test_MODIS(self): # Testpoints verified with MODLAND Tile Calculator @@ -72,11 +67,10 @@ def test_MODIS(self): crs = ccrs.Sinusoidal.MODIS lons = np.array([-180, -50, 40, 180]) lats = np.array([-89.999, 30, 20, 89.999]) - expected_x = np.array([-349.33, -4814886.99, - 4179566.79, 349.33]) - expected_y = np.array([-10007443.48, 3335851.56, - 2223901.04, 10007443.48]) - assert_almost_equal(crs.transform_points(crs.as_geodetic(), - lons, lats), - np.c_[expected_x, expected_y, [0, 0, 0, 0]], - decimal=2) + expected_x = np.array([-349.33, -4814886.99, 4179566.79, 349.33]) + expected_y = np.array([-10007443.48, 3335851.56, 2223901.04, 10007443.48]) + assert_almost_equal( + crs.transform_points(crs.as_geodetic(), lons, lats), + np.c_[expected_x, expected_y, [0, 0, 0, 0]], + decimal=2, + ) diff --git a/lib/cartopy/tests/crs/test_spilhaus.py b/lib/cartopy/tests/crs/test_spilhaus.py index 3c4496436..252a4ad3f 100644 --- a/lib/cartopy/tests/crs/test_spilhaus.py +++ b/lib/cartopy/tests/crs/test_spilhaus.py @@ -5,6 +5,7 @@ """ Tests for the Spilhaus projection. """ + from packaging.version import parse as parse_version import pyproj import pytest @@ -18,21 +19,22 @@ 'ellps=WGS84', 'no_defs', } + + @pytest.mark.skipif( - (proj_version < parse_version("9.6.0")), - reason="Requires PROJ >= 9.6.0" + (proj_version < parse_version('9.6.0')), reason='Requires PROJ >= 9.6.0' ) def test_defaults(): crs = ccrs.Spilhaus() - expected = {'rot=45','x_0=0.0','y_0=0.0'} | common_arg + expected = {'rot=45', 'x_0=0.0', 'y_0=0.0'} | common_arg check_proj_params('spilhaus', crs, expected) + @pytest.mark.skipif( - (proj_version < parse_version("9.6.0")), - reason="Requires PROJ >= 9.6.0" + (proj_version < parse_version('9.6.0')), reason='Requires PROJ >= 9.6.0' ) -@pytest.mark.parametrize("rotation",[45,135,225]) +@pytest.mark.parametrize('rotation', [45, 135, 225]) def test_rotation(rotation): - crs = ccrs.Spilhaus(rotation = rotation) - expected = {f'rot={rotation}','x_0=0.0','y_0=0.0'} | common_arg + crs = ccrs.Spilhaus(rotation=rotation) + expected = {f'rot={rotation}', 'x_0=0.0', 'y_0=0.0'} | common_arg check_proj_params('spilhaus', crs, expected) diff --git a/lib/cartopy/tests/crs/test_stereographic.py b/lib/cartopy/tests/crs/test_stereographic.py index b9b47399b..a5cd8aeec 100644 --- a/lib/cartopy/tests/crs/test_stereographic.py +++ b/lib/cartopy/tests/crs/test_stereographic.py @@ -13,30 +13,27 @@ class TestStereographic: def test_default(self): stereo = ccrs.Stereographic() - other_args = {'ellps=WGS84', 'lat_0=0.0', 'lon_0=0.0', 'x_0=0.0', - 'y_0=0.0'} + other_args = {'ellps=WGS84', 'lat_0=0.0', 'lon_0=0.0', 'x_0=0.0', 'y_0=0.0'} check_proj_params('stere', stereo, other_args) - assert_almost_equal(np.array(stereo.x_limits), - [-5e7, 5e7], decimal=3) - assert_almost_equal(np.array(stereo.y_limits), - [-5e7, 5e7], decimal=3) + assert_almost_equal(np.array(stereo.x_limits), [-5e7, 5e7], decimal=3) + assert_almost_equal(np.array(stereo.y_limits), [-5e7, 5e7], decimal=3) def test_eccentric_globe(self): - globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, - ellipse=None) + globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, ellipse=None) stereo = ccrs.Stereographic(globe=globe) - other_args = {'a=1000', 'b=500', 'lat_0=0.0', 'lon_0=0.0', 'x_0=0.0', - 'y_0=0.0'} + other_args = {'a=1000', 'b=500', 'lat_0=0.0', 'lon_0=0.0', 'x_0=0.0', 'y_0=0.0'} check_proj_params('stere', stereo, other_args) # The limits in this test are sensible values, but are by no means # a "correct" answer - they mean that plotting the crs results in a # reasonable map. - assert_almost_equal(np.array(stereo.x_limits), - [-7839.27971444, 7839.27971444], decimal=4) - assert_almost_equal(np.array(stereo.y_limits), - [-3932.82587779, 3932.82587779], decimal=4) + assert_almost_equal( + np.array(stereo.x_limits), [-7839.27971444, 7839.27971444], decimal=4 + ) + assert_almost_equal( + np.array(stereo.y_limits), [-3932.82587779, 3932.82587779], decimal=4 + ) def test_true_scale(self): # The "true_scale_latitude" parameter only makes sense for @@ -45,8 +42,14 @@ def test_true_scale(self): # See test_scale_factor for test on projection. globe = ccrs.Globe(ellipse='sphere') stereo = ccrs.NorthPolarStereo(true_scale_latitude=30, globe=globe) - other_args = {'ellps=sphere', 'lat_0=90', 'lon_0=0.0', 'lat_ts=30', - 'x_0=0.0', 'y_0=0.0'} + other_args = { + 'ellps=sphere', + 'lat_0=90', + 'lon_0=0.0', + 'lat_ts=30', + 'x_0=0.0', + 'y_0=0.0', + } check_proj_params('stere', stereo, other_args) def test_scale_factor(self): @@ -56,32 +59,34 @@ def test_scale_factor(self): # In these conditions a scale factor of 0.75 corresponds exactly to # a standard parallel of 30N. globe = ccrs.Globe(ellipse='sphere') - stereo = ccrs.Stereographic(central_latitude=90., scale_factor=0.75, - globe=globe) - other_args = {'ellps=sphere', 'lat_0=90.0', 'lon_0=0.0', 'k_0=0.75', - 'x_0=0.0', 'y_0=0.0'} + stereo = ccrs.Stereographic( + central_latitude=90.0, scale_factor=0.75, globe=globe + ) + other_args = { + 'ellps=sphere', + 'lat_0=90.0', + 'lon_0=0.0', + 'k_0=0.75', + 'x_0=0.0', + 'y_0=0.0', + } check_proj_params('stere', stereo, other_args) # Now test projections lon, lat = 10, 10 - projected_scale_factor = stereo.transform_point(lon, lat, - ccrs.Geodetic()) + projected_scale_factor = stereo.transform_point(lon, lat, ccrs.Geodetic()) # should be equivalent to North Polar Stereo with # true_scale_latitude = 30 nstereo = ccrs.NorthPolarStereo(globe=globe, true_scale_latitude=30) - projected_true_scale = nstereo.transform_point(lon, lat, - ccrs.Geodetic()) + projected_true_scale = nstereo.transform_point(lon, lat, ccrs.Geodetic()) assert projected_true_scale == projected_scale_factor def test_eastings(self): stereo = ccrs.Stereographic() - stereo_offset = ccrs.Stereographic(false_easting=1234, - false_northing=-4321) + stereo_offset = ccrs.Stereographic(false_easting=1234, false_northing=-4321) - other_args = {'ellps=WGS84', 'lat_0=0.0', 'lon_0=0.0', 'x_0=1234', - 'y_0=-4321'} + other_args = {'ellps=WGS84', 'lat_0=0.0', 'lon_0=0.0', 'x_0=1234', 'y_0=-4321'} check_proj_params('stere', stereo_offset, other_args) - assert (tuple(np.array(stereo.x_limits) + 1234) == - stereo_offset.x_limits) + assert tuple(np.array(stereo.x_limits) + 1234) == stereo_offset.x_limits diff --git a/lib/cartopy/tests/crs/test_transverse_mercator.py b/lib/cartopy/tests/crs/test_transverse_mercator.py index 41060dc3b..bde071acd 100644 --- a/lib/cartopy/tests/crs/test_transverse_mercator.py +++ b/lib/cartopy/tests/crs/test_transverse_mercator.py @@ -23,28 +23,30 @@ def setup_class(self): def test_default(self, approx): proj = ccrs.TransverseMercator(approx=approx) res = proj.transform_point(*self.point_a, src_crs=self.src_crs) - np.testing.assert_array_almost_equal(res, - (-245269.53181, 5627508.74355), - decimal=5) + np.testing.assert_array_almost_equal( + res, (-245269.53181, 5627508.74355), decimal=5 + ) res = proj.transform_point(*self.point_b, src_crs=self.src_crs) - np.testing.assert_array_almost_equal(res, (35474.63566645, - 5596583.41949901)) + np.testing.assert_array_almost_equal(res, (35474.63566645, 5596583.41949901)) def test_osgb_vals(self, approx): - proj = ccrs.TransverseMercator(central_longitude=-2, - central_latitude=49, - scale_factor=0.9996012717, - false_easting=400000, - false_northing=-100000, - globe=ccrs.Globe(datum='OSGB36', - ellipse='airy'), - approx=approx) + proj = ccrs.TransverseMercator( + central_longitude=-2, + central_latitude=49, + scale_factor=0.9996012717, + false_easting=400000, + false_northing=-100000, + globe=ccrs.Globe(datum='OSGB36', ellipse='airy'), + approx=approx, + ) res = proj.transform_point(*self.point_a, src_crs=self.src_crs) - np.testing.assert_array_almost_equal(res, (295971.28668, 93064.27666), - decimal=5) + np.testing.assert_array_almost_equal( + res, (295971.28668, 93064.27666), decimal=5 + ) res = proj.transform_point(*self.point_b, src_crs=self.src_crs) - np.testing.assert_array_almost_equal(res, (577274.98380, 69740.49227), - decimal=5) + np.testing.assert_array_almost_equal( + res, (577274.98380, 69740.49227), decimal=5 + ) def test_nan(self, approx): if not approx: @@ -67,11 +69,13 @@ def setup_class(self): def test_default(self, approx): proj = ccrs.OSGB(approx=approx) res = proj.transform_point(*self.point_a, src_crs=self.src_crs) - np.testing.assert_array_almost_equal(res, (295971.28668, 93064.27666), - decimal=5) + np.testing.assert_array_almost_equal( + res, (295971.28668, 93064.27666), decimal=5 + ) res = proj.transform_point(*self.point_b, src_crs=self.src_crs) - np.testing.assert_array_almost_equal(res, (577274.98380, 69740.49227), - decimal=5) + np.testing.assert_array_almost_equal( + res, (577274.98380, 69740.49227), decimal=5 + ) def test_nan(self): proj = ccrs.OSGB(approx=True) @@ -91,8 +95,7 @@ def setup_class(self): def test_default(self, approx): proj = ccrs.OSNI(approx=approx) res = proj.transform_point(*self.point_a, src_crs=self.src_crs) - np.testing.assert_array_almost_equal(res, - (275614.267627, 386984.206430)) + np.testing.assert_array_almost_equal(res, (275614.267627, 386984.206430)) def test_nan(self): proj = ccrs.OSNI(approx=True) diff --git a/lib/cartopy/tests/crs/test_utm.py b/lib/cartopy/tests/crs/test_utm.py index c96ca17de..bc2d0075b 100644 --- a/lib/cartopy/tests/crs/test_utm.py +++ b/lib/cartopy/tests/crs/test_utm.py @@ -24,10 +24,8 @@ def test_default(south): other_args |= {'south'} check_proj_params('utm', utm, other_args) - assert_almost_equal(np.array(utm.x_limits), - [-250000, 1250000]) - assert_almost_equal(np.array(utm.y_limits), - [-10000000, 25000000]) + assert_almost_equal(np.array(utm.x_limits), [-250000, 1250000]) + assert_almost_equal(np.array(utm.y_limits), [-10000000, 25000000]) def test_ellipsoid_transform(): @@ -39,14 +37,11 @@ def test_ellipsoid_transform(): other_args = {'ellps=clrk66', 'units=m', 'zone=18'} check_proj_params('utm', utm, other_args) - assert_almost_equal(np.array(utm.x_limits), - [-250000, 1250000]) - assert_almost_equal(np.array(utm.y_limits), - [-10000000, 25000000]) + assert_almost_equal(np.array(utm.x_limits), [-250000, 1250000]) + assert_almost_equal(np.array(utm.y_limits), [-10000000, 25000000]) result = utm.transform_point(-73.5, 40.5, geodetic) - assert_almost_equal(result, np.array([127106.5 + 500000, 4484124.4]), - decimal=1) + assert_almost_equal(result, np.array([127106.5 + 500000, 4484124.4]), decimal=1) inverse_result = geodetic.transform_point(result[0], result[1], utm) assert_almost_equal(inverse_result, [-73.5, 40.5]) diff --git a/lib/cartopy/tests/feature/test_nightshade.py b/lib/cartopy/tests/feature/test_nightshade.py index 391b70ec5..4acec5b39 100644 --- a/lib/cartopy/tests/feature/test_nightshade.py +++ b/lib/cartopy/tests/feature/test_nightshade.py @@ -33,12 +33,16 @@ def test_julian_day(): # https://www.timeanddate.com/worldclock/sunearth.html # ?month=6&day=21&year=2030&hour=0&min=0&sec=0&n=&ntxt=&earth=0 -@pytest.mark.parametrize('dt, true_lat, true_lon', [ - (datetime(2018, 9, 29, 0, 0), -(2 + 18 / 60), (177 + 37 / 60)), - (datetime(2018, 9, 29, 14, 0), -(2 + 32 / 60), -(32 + 25 / 60)), - (datetime(1992, 2, 14, 0, 0), -(13 + 20 / 60), -(176 + 26 / 60)), - (datetime(2030, 6, 21, 0, 0), (23 + 26 / 60), -(179 + 34 / 60)) -]) + +@pytest.mark.parametrize( + 'dt, true_lat, true_lon', + [ + (datetime(2018, 9, 29, 0, 0), -(2 + 18 / 60), (177 + 37 / 60)), + (datetime(2018, 9, 29, 14, 0), -(2 + 32 / 60), -(32 + 25 / 60)), + (datetime(1992, 2, 14, 0, 0), -(13 + 20 / 60), -(176 + 26 / 60)), + (datetime(2030, 6, 21, 0, 0), (23 + 26 / 60), -(179 + 34 / 60)), + ], +) def test_solar_position(dt, true_lat, true_lon): lon, lat = _solar_position(dt) assert pytest.approx(true_lat, 0.1) == lat diff --git a/lib/cartopy/tests/io/test_downloaders.py b/lib/cartopy/tests/io/test_downloaders.py index 475f066d1..1e328b090 100644 --- a/lib/cartopy/tests/io/test_downloaders.py +++ b/lib/cartopy/tests/io/test_downloaders.py @@ -15,22 +15,28 @@ def test_Downloader_data(): - di = cio.Downloader('https://testing.com/{category}/{name}.zip', - str(Path('{data_dir}') / '{category}' / 'shape.shp'), - str(Path('/project') / 'foobar' / '{category}' / - 'sample.shp')) - - replacement_dict = {'category': 'example', - 'name': 'test', - 'data_dir': str(Path('/wibble') / 'foo' / 'bar')} + di = cio.Downloader( + 'https://testing.com/{category}/{name}.zip', + str(Path('{data_dir}') / '{category}' / 'shape.shp'), + str(Path('/project') / 'foobar' / '{category}' / 'sample.shp'), + ) + + replacement_dict = { + 'category': 'example', + 'name': 'test', + 'data_dir': str(Path('/wibble') / 'foo' / 'bar'), + } assert di.url(replacement_dict) == 'https://testing.com/example/test.zip' - assert (di.target_path(replacement_dict) == - Path('/wibble') / 'foo' / 'bar' / 'example' / 'shape.shp') + assert ( + di.target_path(replacement_dict) + == Path('/wibble') / 'foo' / 'bar' / 'example' / 'shape.shp' + ) - assert (di.pre_downloaded_path(replacement_dict) == - Path('/project/foobar/example/sample.shp')) + assert di.pre_downloaded_path(replacement_dict) == Path( + '/project/foobar/example/sample.shp' + ) @contextlib.contextmanager @@ -41,8 +47,9 @@ def config_replace(replacement_dict): """ with contextlib.ExitStack() as stack: - stack.callback(cartopy.config.__setitem__, 'downloaders', - cartopy.config['downloaders']) + stack.callback( + cartopy.config.__setitem__, 'downloaders', cartopy.config['downloaders'] + ) cartopy.config['downloaders'] = replacement_dict yield @@ -56,10 +63,14 @@ def download_to_temp(tmp_path_factory): """ with contextlib.ExitStack() as stack: - stack.callback(cartopy.config.__setitem__, 'downloaders', - cartopy.config['downloaders'].copy()) - stack.callback(cartopy.config.__setitem__, 'data_dir', - cartopy.config['data_dir']) + stack.callback( + cartopy.config.__setitem__, + 'downloaders', + cartopy.config['downloaders'].copy(), + ) + stack.callback( + cartopy.config.__setitem__, 'data_dir', cartopy.config['data_dir'] + ) tmp_dir = tmp_path_factory.mktemp('cartopy_data') cartopy.config['data_dir'] = str(tmp_dir) @@ -77,9 +88,10 @@ def test_from_config(): land_spec = ('shapefile', 'natural_earth', '110m', 'physical', 'land') generic_spec = ('shapefile', 'natural_earth') - target_config = {land_spec: land_downloader, - generic_spec: generic_ne_downloader, - } + target_config = { + land_spec: land_downloader, + generic_spec: generic_ne_downloader, + } with config_replace(target_config): # ocean spec is not explicitly in the config, but a subset of it is, @@ -87,8 +99,7 @@ def test_from_config(): r = cio.Downloader.from_config(ocean_spec) # check the resulting download item produces a sensible url. - assert (r.url({'name': 'ocean'}) == - 'https://example.com/generic_ne/ocean.zip') + assert r.url({'name': 'ocean'}) == 'https://example.com/generic_ne/ocean.zip' r = cio.Downloader.from_config(land_spec) assert r is land_downloader @@ -115,11 +126,12 @@ def test_downloading_simple_ascii(download_to_temp): with open(tmp_fname) as fh: fh.readline() - assert fh.readline() == " * jQuery JavaScript Library v1.8.2\n" + assert fh.readline() == ' * jQuery JavaScript Library v1.8.2\n' # check that calling path again doesn't try re-downloading - with mock.patch.object(dnld_item, 'acquire_resource', - wraps=dnld_item.acquire_resource) as counter: + with mock.patch.object( + dnld_item, 'acquire_resource', wraps=dnld_item.acquire_resource + ) as counter: assert dnld_item.path(format_dict) == tmp_fname counter.assert_not_called() @@ -137,24 +149,28 @@ def test_natural_earth_downloader(tmp_path): # picking a small-ish file to speed up download times, the file itself # isn't important - it is the download mechanism that is. - format_dict = {'category': 'physical', - 'name': 'rivers_lake_centerlines', - 'resolution': '110m'} + format_dict = { + 'category': 'physical', + 'name': 'rivers_lake_centerlines', + 'resolution': '110m', + } dnld_item = NEShpDownloader(target_path_template=shp_path_template) # check that the file gets downloaded the first time path is called - with mock.patch.object(dnld_item, 'acquire_resource', - wraps=dnld_item.acquire_resource) as counter: - with pytest.warns(cartopy.io.DownloadWarning, match="Downloading:"): + with mock.patch.object( + dnld_item, 'acquire_resource', wraps=dnld_item.acquire_resource + ) as counter: + with pytest.warns(cartopy.io.DownloadWarning, match='Downloading:'): shp_path = dnld_item.path(format_dict) counter.assert_called_once() assert shp_path_template.format(**format_dict) == str(shp_path) # check that calling path again doesn't try re-downloading - with mock.patch.object(dnld_item, 'acquire_resource', - wraps=dnld_item.acquire_resource) as counter: + with mock.patch.object( + dnld_item, 'acquire_resource', wraps=dnld_item.acquire_resource + ) as counter: assert dnld_item.path(format_dict) == shp_path counter.assert_not_called() @@ -162,16 +178,18 @@ def test_natural_earth_downloader(tmp_path): exts = ['.shp', '.shx'] for ext in exts: fname = shp_path.with_suffix(ext) - assert fname.exists(), \ - f"Shapefile's {ext} file doesn't exist in {fname}" + assert fname.exists(), f"Shapefile's {ext} file doesn't exist in {fname}" # check that providing a pre downloaded path actually works - pre_dnld = NEShpDownloader(target_path_template='/not/a/real/file.txt', - pre_downloaded_path_template=str(shp_path)) + pre_dnld = NEShpDownloader( + target_path_template='/not/a/real/file.txt', + pre_downloaded_path_template=str(shp_path), + ) # check that the pre_dnld downloader doesn't re-download, but instead # uses the path of the previously downloaded item - with mock.patch.object(pre_dnld, 'acquire_resource', - wraps=pre_dnld.acquire_resource) as counter: + with mock.patch.object( + pre_dnld, 'acquire_resource', wraps=pre_dnld.acquire_resource + ) as counter: assert pre_dnld.path(format_dict) == shp_path counter.assert_not_called() diff --git a/lib/cartopy/tests/io/test_ogc_clients.py b/lib/cartopy/tests/io/test_ogc_clients.py index ef4ff1267..ea6da2102 100644 --- a/lib/cartopy/tests/io/test_ogc_clients.py +++ b/lib/cartopy/tests/io/test_ogc_clients.py @@ -47,8 +47,8 @@ def test_string_service(self): source = ogc.WMSRasterSource(self.URI, self.layer) from owslib.map.wms111 import WebMapService_1_1_1 from owslib.map.wms130 import WebMapService_1_3_0 - assert isinstance(source.service, - (WebMapService_1_1_1, WebMapService_1_3_0)) + + assert isinstance(source.service, (WebMapService_1_1_1, WebMapService_1_3_0)) assert isinstance(source.layers, list) assert source.layers == [self.layer] @@ -67,19 +67,16 @@ def test_no_layers(self): ogc.WMSRasterSource(self.URI, []) def test_extra_kwargs_empty(self): - source = ogc.WMSRasterSource(self.URI, self.layer, - getmap_extra_kwargs={}) + source = ogc.WMSRasterSource(self.URI, self.layer, getmap_extra_kwargs={}) assert source.getmap_extra_kwargs == {} def test_extra_kwargs_None(self): - source = ogc.WMSRasterSource(self.URI, self.layer, - getmap_extra_kwargs=None) + source = ogc.WMSRasterSource(self.URI, self.layer, getmap_extra_kwargs=None) assert source.getmap_extra_kwargs == {'transparent': True} def test_extra_kwargs_non_empty(self): kwargs = {'another': 'kwarg'} - source = ogc.WMSRasterSource(self.URI, self.layer, - getmap_extra_kwargs=kwargs) + source = ogc.WMSRasterSource(self.URI, self.layer, getmap_extra_kwargs=kwargs) assert source.getmap_extra_kwargs == kwargs def test_supported_projection(self): @@ -90,9 +87,11 @@ def test_unsupported_projection(self): source = ogc.WMSRasterSource(self.URI, self.layer) # Patch dict of known Proj->SRS mappings so that it does # not include any of the available SRSs from the WMS. - with mock.patch.dict('cartopy.io.ogc_clients._CRS_TO_OGC_SRS', - {ccrs.OSNI(approx=True): 'EPSG:29901'}, - clear=True): + with mock.patch.dict( + 'cartopy.io.ogc_clients._CRS_TO_OGC_SRS', + {ccrs.OSNI(approx=True): 'EPSG:29901'}, + clear=True, + ): msg = 'not available' with pytest.raises(ValueError, match=msg): source.validate_projection(ccrs.Miller()) @@ -100,8 +99,7 @@ def test_unsupported_projection(self): def test_fetch_img(self): source = ogc.WMSRasterSource(self.URI, self.layer) extent = [-10, 10, 40, 60] - located_image, = source.fetch_raster(self.projection, extent, - RESOLUTION) + (located_image,) = source.fetch_raster(self.projection, extent, RESOLUTION) img = np.array(located_image.image) assert img.shape == RESOLUTION + (4,) # No transparency in this image. @@ -111,8 +109,7 @@ def test_fetch_img(self): def test_fetch_img_different_projection(self): source = ogc.WMSRasterSource(self.URI, self.layer) extent = [-570000, 5100000, 870000, 3500000] - located_image, = source.fetch_raster(ccrs.Orthographic(), extent, - RESOLUTION) + (located_image,) = source.fetch_raster(ccrs.Orthographic(), extent, RESOLUTION) img = np.array(located_image.image) assert img.shape == RESOLUTION + (4,) @@ -127,13 +124,12 @@ def test_float_resolution(self): # The resolution (in pixels) should be cast to ints. source = ogc.WMSRasterSource(self.URI, self.layer) extent = [-570000, 5100000, 870000, 3500000] - located_image, = source.fetch_raster(self.projection, extent, - [19.5, 39.1]) + (located_image,) = source.fetch_raster(self.projection, extent, [19.5, 39.1]) img = np.array(located_image.image) assert img.shape == (40, 20, 4) -@pytest.mark.filterwarnings("ignore:TileMatrixLimits") +@pytest.mark.filterwarnings('ignore:TileMatrixLimits') @pytest.mark.network @pytest.mark.skipif(not _OWSLIB_AVAILABLE, reason='OWSLib is unavailable.') @pytest.mark.xfail(reason='NASA servers are returning bad content metadata') @@ -171,21 +167,25 @@ def test_unsupported_projection(self): def test_fetch_img(self): source = ogc.WMTSRasterSource(self.URI, self.layer_name) extent = [-10, 10, 40, 60] - located_image, = source.fetch_raster(self.projection, extent, - RESOLUTION) + (located_image,) = source.fetch_raster(self.projection, extent, RESOLUTION) img = np.array(located_image.image) assert img.shape == (512, 512, 4) # No transparency in this image. assert img[:, :, 3].min() == 255 - assert located_image.extent == (-180.0, 107.99999999999994, - -197.99999999999994, 90.0) + assert located_image.extent == ( + -180.0, + 107.99999999999994, + -197.99999999999994, + 90.0, + ) def test_fetch_img_reprojected(self): source = ogc.WMTSRasterSource(self.URI, self.layer_name) extent = [-20, -1, 48, 50] # NB single result in this case. - located_image, = source.fetch_raster(ccrs.NorthPolarStereo(), extent, - (30, 30)) + (located_image,) = source.fetch_raster( + ccrs.NorthPolarStereo(), extent, (30, 30) + ) # Check image array is as expected (more or less). img = np.array(located_image.image) @@ -214,8 +214,9 @@ def test_fetch_img_reprojected_twoparts(self): class TestWFSGeometrySource: URI = 'https://nsidc.org/cgi-bin/atlas_south?service=WFS' typename = 'land_excluding_antarctica' - native_projection = ccrs.Stereographic(central_latitude=-90, - true_scale_latitude=-71) + native_projection = ccrs.Stereographic( + central_latitude=-90, true_scale_latitude=-71 + ) def test_string_service(self): service = WebFeatureService(self.URI) @@ -239,8 +240,7 @@ def test_unsupported_projection(self): with pytest.raises(ValueError, match=msg): source.fetch_geometries(ccrs.PlateCarree(), [-180, 180, -90, 90]) - @pytest.mark.xfail(raises=ParseError, - reason="Bad XML returned from the URL") + @pytest.mark.xfail(raises=ParseError, reason='Bad XML returned from the URL') def test_fetch_geometries(self): source = ogc.WFSGeometrySource(self.URI, self.typename) # Extent covering New Zealand. diff --git a/lib/cartopy/tests/io/test_srtm.py b/lib/cartopy/tests/io/test_srtm.py index e9d8db2bc..f74b9efd7 100644 --- a/lib/cartopy/tests/io/test_srtm.py +++ b/lib/cartopy/tests/io/test_srtm.py @@ -12,14 +12,17 @@ from .test_downloaders import download_to_temp # noqa: F401 (used as fixture) -pytestmark = [pytest.mark.network, - pytest.mark.filterwarnings('ignore:SRTM requires an account'), - pytest.mark.usefixtures('srtm_login_or_skip')] +pytestmark = [ + pytest.mark.network, + pytest.mark.filterwarnings('ignore:SRTM requires an account'), + pytest.mark.usefixtures('srtm_login_or_skip'), +] @pytest.fixture def srtm_login_or_skip(monkeypatch): import os + try: srtm_username = os.environ['SRTM_USERNAME'] except KeyError: @@ -39,35 +42,34 @@ def srtm_login_or_skip(monkeypatch): password_manager = HTTPPasswordMgrWithDefaultRealm() password_manager.add_password( - None, - "https://urs.earthdata.nasa.gov", - srtm_username, - srtm_password) + None, 'https://urs.earthdata.nasa.gov', srtm_username, srtm_password + ) cookie_jar = CookieJar() - opener = build_opener(HTTPBasicAuthHandler(password_manager), - HTTPCookieProcessor(cookie_jar)) + opener = build_opener( + HTTPBasicAuthHandler(password_manager), HTTPCookieProcessor(cookie_jar) + ) monkeypatch.setattr(cartopy.io, 'urlopen', opener.open) class TestRetrieve: - @pytest.mark.parametrize('Source, read_SRTM, max_, min_, pt', [ - (cartopy.io.srtm.SRTM3Source, cartopy.io.srtm.read_SRTM3, - 602, -34, 78), - (cartopy.io.srtm.SRTM1Source, cartopy.io.srtm.read_SRTM1, - 602, -37, 50), - ], ids=[ - 'srtm3', - 'srtm1', - ]) - def test_srtm_retrieve(self, Source, read_SRTM, max_, min_, pt, - download_to_temp): # noqa: F811 + @pytest.mark.parametrize( + 'Source, read_SRTM, max_, min_, pt', + [ + (cartopy.io.srtm.SRTM3Source, cartopy.io.srtm.read_SRTM3, 602, -34, 78), + (cartopy.io.srtm.SRTM1Source, cartopy.io.srtm.read_SRTM1, 602, -37, 50), + ], + ids=[ + 'srtm3', + 'srtm1', + ], + ) + def test_srtm_retrieve(self, Source, read_SRTM, max_, min_, pt, download_to_temp): # noqa: F811 # test that the download mechanism for SRTM works with pytest.warns(cartopy.io.DownloadWarning): r = Source().srtm_fname(-4, 50) - assert r.startswith(str(download_to_temp)), \ - 'File not downloaded to tmp dir' + assert r.startswith(str(download_to_temp)), 'File not downloaded to tmp dir' img, _, _ = read_SRTM(r) @@ -76,26 +78,34 @@ def test_srtm_retrieve(self, Source, read_SRTM, max_, min_, pt, assert img.min() == min_ assert img[-10, 12] == pt - @pytest.mark.parametrize('Source, shape', [ - (cartopy.io.srtm.SRTM3Source, (1201, 1201)), - (cartopy.io.srtm.SRTM1Source, (3601, 3601)), - ], ids=[ - 'srtm3', - 'srtm1', - ]) + @pytest.mark.parametrize( + 'Source, shape', + [ + (cartopy.io.srtm.SRTM3Source, (1201, 1201)), + (cartopy.io.srtm.SRTM1Source, (3601, 3601)), + ], + ids=[ + 'srtm3', + 'srtm1', + ], + ) def test_srtm_out_of_range(self, Source, shape): # Somewhere over the pacific the elevation should be 0. img, _, _ = Source().combined(120, 2, 2, 2) assert_array_equal(img, np.zeros(np.array(shape) * 2)) -@pytest.mark.parametrize('Source', [ - cartopy.io.srtm.SRTM3Source, - cartopy.io.srtm.SRTM1Source, -], ids=[ - 'srtm3', - 'srtm1', -]) +@pytest.mark.parametrize( + 'Source', + [ + cartopy.io.srtm.SRTM3Source, + cartopy.io.srtm.SRTM1Source, + ], + ids=[ + 'srtm3', + 'srtm1', + ], +) class TestSRTMSource__single_tile: def test_out_of_range(self, Source): source = Source() @@ -124,13 +134,17 @@ def test_zeros(self, Source): assert extent == (0, 1, 50, 51) -@pytest.mark.parametrize('Source', [ - cartopy.io.srtm.SRTM3Source, - cartopy.io.srtm.SRTM1Source, -], ids=[ - 'srtm3', - 'srtm1', -]) +@pytest.mark.parametrize( + 'Source', + [ + cartopy.io.srtm.SRTM3Source, + cartopy.io.srtm.SRTM1Source, + ], + ids=[ + 'srtm3', + 'srtm1', + ], +) class TestSRTMSource__combined: def test_trivial(self, Source): source = Source() @@ -146,25 +160,26 @@ def test_2by2(self, Source): e_img, _, e_extent = source.combined(-1, 50, 2, 1) assert e_extent == (-1, 1, 50, 51) - imgs = [source.single_tile(-1, 50)[0], - source.single_tile(0, 50)[0]] + imgs = [source.single_tile(-1, 50)[0], source.single_tile(0, 50)[0]] assert_array_equal(np.hstack(imgs), e_img) -@pytest.mark.parametrize('Source', [ - cartopy.io.srtm.SRTM3Source, - cartopy.io.srtm.SRTM1Source, -], ids=[ - 'srtm3', - 'srtm1', -]) +@pytest.mark.parametrize( + 'Source', + [ + cartopy.io.srtm.SRTM3Source, + cartopy.io.srtm.SRTM1Source, + ], + ids=[ + 'srtm3', + 'srtm1', + ], +) def test_fetch_raster_ascombined(Source): source = Source() e_img, e_crs, e_extent = source.combined(-1, 50, 2, 1) - imgs = source.fetch_raster(ccrs.PlateCarree(), - (-0.9, 0.1, 50.1, 50.999), - None) + imgs = source.fetch_raster(ccrs.PlateCarree(), (-0.9, 0.1, 50.1, 50.999), None) assert len(imgs) == 1 r_img, r_extent = imgs[0] assert e_extent == r_extent diff --git a/lib/cartopy/tests/mpl/__init__.py b/lib/cartopy/tests/mpl/__init__.py index 0151161bd..d293f2248 100644 --- a/lib/cartopy/tests/mpl/__init__.py +++ b/lib/cartopy/tests/mpl/__init__.py @@ -15,6 +15,7 @@ def show(projection, geometry): if geometry.geom_type == 'MultiPolygon' and 1: multi_polygon = geometry import cartopy.mpl.path as cpath + pth = cpath.shapely_to_path(multi_polygon) patch = mpatches.PathPatch(pth, edgecolor='none', lw=0, alpha=0.2) plt.gca().add_patch(patch) @@ -26,14 +27,12 @@ def show(projection, geometry): multi_polygon = geometry for polygon in multi_polygon.geoms: line_string = polygon.exterior - plt.plot(*zip(*line_string.coords), - marker='+', linestyle='-') + plt.plot(*zip(*line_string.coords), marker='+', linestyle='-') elif geometry.geom_type == 'MultiLineString': multi_line_string = geometry for line_string in multi_line_string.geoms: - plt.plot(*zip(*line_string.coords), - marker='+', linestyle='-') + plt.plot(*zip(*line_string.coords), marker='+', linestyle='-') elif geometry.geom_type == 'LinearRing': plt.plot(*zip(*geometry.coords), marker='+', linestyle='-') @@ -58,8 +57,13 @@ def show(projection, geometry): plt.xlim(1.55e7, 1.65e7) plt.ylim(0.3e7, 0.4e7) - plt.plot(*zip(*projection.boundary.coords), marker='o', - scalex=False, scaley=False, zorder=-1) + plt.plot( + *zip(*projection.boundary.coords), + marker='o', + scalex=False, + scaley=False, + zorder=-1, + ) plt.show() plt.switch_backend(orig_backend) diff --git a/lib/cartopy/tests/mpl/conftest.py b/lib/cartopy/tests/mpl/conftest.py index afe8b02e3..2840ba98e 100644 --- a/lib/cartopy/tests/mpl/conftest.py +++ b/lib/cartopy/tests/mpl/conftest.py @@ -34,7 +34,6 @@ def pytest_itemcollected(item): if path.basename == 'cartopy': return elif path.basename == 'tests': - subdir = item.fspath.relto(path)[:-len(item.fspath.ext)] - mpl_marker.kwargs.setdefault('baseline_dir', - f'baseline_images/{subdir}') + subdir = item.fspath.relto(path)[: -len(item.fspath.ext)] + mpl_marker.kwargs.setdefault('baseline_dir', f'baseline_images/{subdir}') break diff --git a/lib/cartopy/tests/mpl/test_axes.py b/lib/cartopy/tests/mpl/test_axes.py index 790416cab..5d9ca5abb 100644 --- a/lib/cartopy/tests/mpl/test_axes.py +++ b/lib/cartopy/tests/mpl/test_axes.py @@ -48,34 +48,37 @@ def test_transform_PlateCarree_shortcut(): trans = InterProjectionTransform(src, target) - with mock.patch.object(target, 'project_geometry', - wraps=target.project_geometry) as counter: + with mock.patch.object( + target, 'project_geometry', wraps=target.project_geometry + ) as counter: trans.transform_path(pth1) # pth1 should allow a short-cut. counter.assert_not_called() - with mock.patch.object(target, 'project_geometry', - wraps=target.project_geometry) as counter: + with mock.patch.object( + target, 'project_geometry', wraps=target.project_geometry + ) as counter: trans.transform_path(pth2) counter.assert_called_once() - with mock.patch.object(target, 'project_geometry', - wraps=target.project_geometry) as counter: + with mock.patch.object( + target, 'project_geometry', wraps=target.project_geometry + ) as counter: trans.transform_path(pth3) counter.assert_called_once() class Test_InterProjectionTransform: def pc_2_pc(self): - return InterProjectionTransform( - ccrs.PlateCarree(), ccrs.PlateCarree()) + return InterProjectionTransform(ccrs.PlateCarree(), ccrs.PlateCarree()) def pc_2_rob(self): return InterProjectionTransform(ccrs.PlateCarree(), ccrs.Robinson()) def rob_2_rob_shifted(self): return InterProjectionTransform( - ccrs.Robinson(), ccrs.Robinson(central_longitude=0)) + ccrs.Robinson(), ccrs.Robinson(central_longitude=0) + ) def test_eq(self): assert self.pc_2_pc() == self.pc_2_pc() @@ -92,27 +95,30 @@ def test_ne(self): class Test_Axes_add_geometries: - @mock.patch('cartopy.mpl.geoaxes.GeoAxes.add_feature') @mock.patch('cartopy.feature.ShapelyFeature') def test_styler_kwarg(self, ShapelyFeature, add_feature_method): - ax = GeoAxes(plt.figure(), [0, 0, 1, 1], - projection=ccrs.Robinson()) - ax.add_geometries(mock.sentinel.geometries, mock.sentinel.crs, - styler=mock.sentinel.styler, wibble='wobble') + ax = GeoAxes(plt.figure(), [0, 0, 1, 1], projection=ccrs.Robinson()) + ax.add_geometries( + mock.sentinel.geometries, + mock.sentinel.crs, + styler=mock.sentinel.styler, + wibble='wobble', + ) ShapelyFeature.assert_called_once_with( - mock.sentinel.geometries, mock.sentinel.crs, wibble='wobble') + mock.sentinel.geometries, mock.sentinel.crs, wibble='wobble' + ) add_feature_method.assert_called_once_with( - ShapelyFeature(), styler=mock.sentinel.styler) + ShapelyFeature(), styler=mock.sentinel.styler + ) @pytest.mark.natural_earth def test_single_geometry(self): # A single geometry is acceptable proj = ccrs.PlateCarree() - ax = GeoAxes(plt.figure(), [0, 0, 1, 1], - projection=proj) + ax = GeoAxes(plt.figure(), [0, 0, 1, 1], projection=proj) ax.add_geometries(next(cfeature.COASTLINE.geometries()), crs=proj) @@ -139,8 +145,9 @@ def test_geoaxes_no_subslice(): @pytest.mark.mpl_image_compare(filename='geoaxes_set_boundary_clipping.png') def test_geoaxes_set_boundary_clipping(): """Test that setting the boundary works properly for clipping #1620.""" - lon, lat = np.meshgrid(np.linspace(-180., 180., 361), - np.linspace(-90., -60., 31)) + lon, lat = np.meshgrid( + np.linspace(-180.0, 180.0, 361), np.linspace(-90.0, -60.0, 31) + ) fig = plt.figure() ax1 = fig.add_subplot(1, 1, 1, projection=ccrs.SouthPolarStereo()) @@ -150,7 +157,8 @@ def test_geoaxes_set_boundary_clipping(): ax1.contourf(lon, lat, lat, transform=ccrs.PlateCarree()) - ax1.set_boundary(mpath.Path.circle(center=(0.5, 0.5), radius=0.5), - transform=ax1.transAxes) + ax1.set_boundary( + mpath.Path.circle(center=(0.5, 0.5), radius=0.5), transform=ax1.transAxes + ) return fig diff --git a/lib/cartopy/tests/mpl/test_boundary.py b/lib/cartopy/tests/mpl/test_boundary.py index e06b99d79..6b4c2fc2b 100644 --- a/lib/cartopy/tests/mpl/test_boundary.py +++ b/lib/cartopy/tests/mpl/test_boundary.py @@ -11,45 +11,41 @@ import cartopy.crs as ccrs -circle_verts = np.array([ - (21.33625905034713, 41.90051020408163), - (20.260167503134653, 36.31721708143521), - (17.186058185078757, 31.533809612693403), - (12.554340705353228, 28.235578526280886), - (7.02857405747471, 26.895042042418392), - (1.4004024147599825, 27.704250959518802), - (-3.5238590865749853, 30.547274662874656), - (-7.038740387110195, 35.0168098237746), - (-8.701144846177232, 41.42387784534415), - (-7.802741418346137, 47.03850217758886), - (-4.224119444187849, 52.606946662776494), - (0.5099122186645388, 55.75656240544797), - (6.075429329647158, 56.92110282927732), - (11.675092899771812, 55.933731058974054), - (16.50667197715324, 52.93590205611499), - (19.87797456957319, 48.357097196578444), - (21.33625905034713, 41.90051020408163) - ]) - -circle_codes = [ - Path.MOVETO, - *[Path.LINETO]*(len(circle_verts)), - Path.CLOSEPOLY +circle_verts = np.array( + [ + (21.33625905034713, 41.90051020408163), + (20.260167503134653, 36.31721708143521), + (17.186058185078757, 31.533809612693403), + (12.554340705353228, 28.235578526280886), + (7.02857405747471, 26.895042042418392), + (1.4004024147599825, 27.704250959518802), + (-3.5238590865749853, 30.547274662874656), + (-7.038740387110195, 35.0168098237746), + (-8.701144846177232, 41.42387784534415), + (-7.802741418346137, 47.03850217758886), + (-4.224119444187849, 52.606946662776494), + (0.5099122186645388, 55.75656240544797), + (6.075429329647158, 56.92110282927732), + (11.675092899771812, 55.933731058974054), + (16.50667197715324, 52.93590205611499), + (19.87797456957319, 48.357097196578444), + (21.33625905034713, 41.90051020408163), ] +) -rectangle_verts = np.array([ - (55.676020408163225, 36.16071428571428), - (130.29336734693877, 36.16071428571428), - (130.29336734693877, -4.017857142857167), - (55.676020408163225, -4.017857142857167), - (55.676020408163225, 36.16071428571428) - ]) +circle_codes = [Path.MOVETO, *[Path.LINETO] * (len(circle_verts)), Path.CLOSEPOLY] -rectangle_codes = [ - Path.MOVETO, - *[Path.LINETO]*(len(rectangle_verts)), - Path.CLOSEPOLY +rectangle_verts = np.array( + [ + (55.676020408163225, 36.16071428571428), + (130.29336734693877, 36.16071428571428), + (130.29336734693877, -4.017857142857167), + (55.676020408163225, -4.017857142857167), + (55.676020408163225, 36.16071428571428), ] +) + +rectangle_codes = [Path.MOVETO, *[Path.LINETO] * (len(rectangle_verts)), Path.CLOSEPOLY] @pytest.mark.natural_earth @@ -65,18 +61,14 @@ def test_multi_path_boundary(): c = circle_verts + offset if close: vertices.extend([c[0], *c, c[-1]]) - codes.extend([Path.MOVETO, *[Path.LINETO]*len(c), Path.CLOSEPOLY]) + codes.extend([Path.MOVETO, *[Path.LINETO] * len(c), Path.CLOSEPOLY]) else: vertices.extend([c[0], *c]) - codes.extend([Path.MOVETO, *[Path.LINETO]*len(c)]) + codes.extend([Path.MOVETO, *[Path.LINETO] * len(c)]) # add rectangle - vertices.extend( - [rectangle_verts[0], *rectangle_verts, rectangle_verts[-1]] - ) - codes.extend( - [Path.MOVETO, *[Path.LINETO]*len(rectangle_verts), Path.CLOSEPOLY] - ) + vertices.extend([rectangle_verts[0], *rectangle_verts, rectangle_verts[-1]]) + codes.extend([Path.MOVETO, *[Path.LINETO] * len(rectangle_verts), Path.CLOSEPOLY]) bnds = [*map(min, zip(*vertices)), *map(max, zip(*vertices))] diff --git a/lib/cartopy/tests/mpl/test_caching.py b/lib/cartopy/tests/mpl/test_caching.py index 6559bdd07..db09d4cae 100644 --- a/lib/cartopy/tests/mpl/test_caching.py +++ b/lib/cartopy/tests/mpl/test_caching.py @@ -57,8 +57,9 @@ def test_coastline_loading_cache(): fig.canvas.draw() # Create another instance of the coastlines and count # the number of times shapereader.Reader is created. - with mock.patch('cartopy.io.shapereader.Reader.__init__', - return_value=None) as counter: + with mock.patch( + 'cartopy.io.shapereader.Reader.__init__', return_value=None + ) as counter: ax2 = fig.add_subplot(2, 1, 1, projection=ccrs.Robinson()) ax2.coastlines() fig.canvas.draw() @@ -70,9 +71,9 @@ def test_shapefile_transform_cache(): # a5caae040ee11e72a62a53100fe5edc355304419 added shapefile mpl # geometry caching based on geometry object id. This test ensures # it is working. - coastline_path = cartopy.io.shapereader.natural_earth(resolution="110m", - category='physical', - name='coastline') + coastline_path = cartopy.io.shapereader.natural_earth( + resolution='110m', category='physical', name='coastline' + ) geoms = cartopy.io.shapereader.Reader(coastline_path).geometries() # Use the first 10 of them. geoms = tuple(geoms)[:10] @@ -87,8 +88,9 @@ def test_shapefile_transform_cache(): assert len(FeatureArtist._geom_key_to_geometry_cache) == 0 assert len(FeatureArtist._geom_key_to_path_cache) == 0 - with mock.patch.object(ax.projection, 'project_geometry', - wraps=ax.projection.project_geometry) as counter: + with mock.patch.object( + ax.projection, 'project_geometry', wraps=ax.projection.project_geometry + ) as counter: ax.add_geometries(geoms, ccrs.PlateCarree()) ax.add_geometries(geoms, ccrs.PlateCarree()) ax.add_geometries(geoms[:], ccrs.PlateCarree()) @@ -98,7 +100,8 @@ def test_shapefile_transform_cache(): # n_calls * n_geom, but should now be just n_geom. assert counter.call_count == n_geom, ( f'The given geometry was transformed too many times (expected: ' - f'{n_geom}; got {counter.call_count}) - the caching is not working.') + f'{n_geom}; got {counter.call_count}) - the caching is not working.' + ) # Check the cache has an entry for each geometry. assert len(FeatureArtist._geom_key_to_geometry_cache) == n_geom @@ -122,8 +125,9 @@ def test_contourf_transform_path_counting(): gc.collect() initial_cache_size = len(cgeoaxes._PATH_TRANSFORM_CACHE) - with mock.patch('cartopy.mpl.path.path_to_shapely', - wraps=cartopy.mpl.path.path_to_shapely) as path_to_shapely_counter: + with mock.patch( + 'cartopy.mpl.path.path_to_shapely', wraps=cartopy.mpl.path.path_to_shapely + ) as path_to_shapely_counter: x, y, z = sample_data((30, 60)) cs = ax.contourf(x, y, z, 5, transform=ccrs.PlateCarree()) if not _MPL_38: @@ -139,7 +143,8 @@ def test_contourf_transform_path_counting(): assert path_to_shapely_counter.call_count == n_geom, ( f'The given geometry was transformed too many times (expected: ' f'{n_geom}; got {path_to_shapely_counter.call_count}) - the caching is ' - f'not working.') + f'not working.' + ) # Check the cache has an entry for each geometry. assert len(cgeoaxes._PATH_TRANSFORM_CACHE) == initial_cache_size + n_geom @@ -152,10 +157,12 @@ def test_contourf_transform_path_counting(): assert len(cgeoaxes._PATH_TRANSFORM_CACHE) == initial_cache_size -@pytest.mark.filterwarnings("ignore:TileMatrixLimits") +@pytest.mark.filterwarnings('ignore:TileMatrixLimits') @pytest.mark.network -@pytest.mark.skipif(not _HAS_PYKDTREE_OR_SCIPY or not _OWSLIB_AVAILABLE, - reason='OWSLib and at least one of pykdtree or scipy is required') +@pytest.mark.skipif( + not _HAS_PYKDTREE_OR_SCIPY or not _OWSLIB_AVAILABLE, + reason='OWSLib and at least one of pykdtree or scipy is required', +) @pytest.mark.xfail(reason='NASA servers are returning bad content metadata') def test_wmts_tile_caching(): image_cache = WMTSRasterSource._shared_image_cache @@ -172,12 +179,12 @@ def test_wmts_tile_caching(): extent = (-180, 180, -90, 90) resolution = (20, 10) n_tiles = 2 - with mock.patch.object(wmts, 'gettile', - wraps=wmts.gettile) as gettile_counter: + with mock.patch.object(wmts, 'gettile', wraps=wmts.gettile) as gettile_counter: source.fetch_raster(crs, extent, resolution) assert gettile_counter.call_count == n_tiles, ( f'Too many tile requests - expected {n_tiles}, got ' - f'{gettile_counter.call_count}.') + f'{gettile_counter.call_count}.' + ) del gettile_counter gc.collect() assert len(image_cache) == 1 @@ -186,8 +193,7 @@ def test_wmts_tile_caching(): assert len(image_cache[wmts][tiles_key]) == n_tiles # Second time around we shouldn't request any more tiles. - with mock.patch.object(wmts, 'gettile', - wraps=wmts.gettile) as gettile_counter: + with mock.patch.object(wmts, 'gettile', wraps=wmts.gettile) as gettile_counter: source.fetch_raster(crs, extent, resolution) gettile_counter.assert_not_called() del gettile_counter diff --git a/lib/cartopy/tests/mpl/test_contour.py b/lib/cartopy/tests/mpl/test_contour.py index 48fb7fbaf..75358fc6f 100644 --- a/lib/cartopy/tests/mpl/test_contour.py +++ b/lib/cartopy/tests/mpl/test_contour.py @@ -17,13 +17,12 @@ def test_contour_plot_bounds(): y = np.linspace(-263790.62, 3230840.5, 130) data = np.hypot(*np.meshgrid(x, y)) / 2e5 - proj_lcc = ccrs.LambertConformal(central_longitude=-95, - central_latitude=25, - standard_parallels=[25]) + proj_lcc = ccrs.LambertConformal( + central_longitude=-95, central_latitude=25, standard_parallels=[25] + ) ax = plt.axes(projection=proj_lcc) ax.contourf(x, y, data, levels=np.arange(0, 40, 1)) - assert_array_almost_equal(ax.get_extent(), - np.array([x[0], x[-1], y[0], y[-1]])) + assert_array_almost_equal(ax.get_extent(), np.array([x[0], x[-1], y[0], y[-1]])) # Levels that don't include data should not fail. plt.figure() @@ -76,8 +75,7 @@ def test_contour_linear_ring(): from scipy.interpolate import NearestNDInterpolator from scipy.signal import convolve2d - ax = plt.axes([0.01, 0.05, 0.898, 0.85], projection=ccrs.Mercator(), - aspect='equal') + ax = plt.axes([0.01, 0.05, 0.898, 0.85], projection=ccrs.Mercator(), aspect='equal') ax.set_extent([-99.6, -89.0, 39.8, 45.5]) xbnds = ax.get_xlim() @@ -89,14 +87,12 @@ def test_contour_linear_ring(): xi = np.linspace(min(ll[0], ul[0]), max(lr[0], ur[0]), 100) yi = np.linspace(min(ll[1], ul[1]), max(ul[1], ur[1]), 100) xi, yi = np.meshgrid(xi, yi) - nn = NearestNDInterpolator((np.arange(-94, -85), np.arange(36, 45)), - np.arange(9)) + nn = NearestNDInterpolator((np.arange(-94, -85), np.arange(36, 45)), np.arange(9)) vals = nn(xi, yi) lons = xi lats = yi window = np.ones((6, 6)) - vals = convolve2d(vals, window / window.sum(), mode='same', - boundary='symm') + vals = convolve2d(vals, window / window.sum(), mode='same', boundary='symm') ax.contourf(lons, lats, vals, np.arange(9), transform=ccrs.PlateCarree()) plt.draw() @@ -126,23 +122,17 @@ def test_contourf_transform_first(func): ax = plt.axes(projection=ccrs.PlateCarree()) test_func = getattr(ax, func) # Can't handle just Z input with the transform_first - with pytest.raises(ValueError, - match="The X and Y arguments must be provided"): - test_func(z, transform=ccrs.PlateCarree(), - transform_first=True) + with pytest.raises(ValueError, match='The X and Y arguments must be provided'): + test_func(z, transform=ccrs.PlateCarree(), transform_first=True) # X and Y must also be 2-dimensional - with pytest.raises(ValueError, - match="The X and Y arguments must be gridded"): - test_func(x, y, z, transform=ccrs.PlateCarree(), - transform_first=True) + with pytest.raises(ValueError, match='The X and Y arguments must be gridded'): + test_func(x, y, z, transform=ccrs.PlateCarree(), transform_first=True) # When calculating the contour in projection-space the extent # will now be the extent of the transformed points (-179, 180, -25, 25) - test_func(xx, yy, z, transform=ccrs.PlateCarree(), - transform_first=True) + test_func(xx, yy, z, transform=ccrs.PlateCarree(), transform_first=True) assert_array_almost_equal(ax.get_extent(), (-179, 180, -25, 25)) # The extent without the transform_first should be all the way out to -180 - test_func(xx, yy, z, transform=ccrs.PlateCarree(), - transform_first=False) + test_func(xx, yy, z, transform=ccrs.PlateCarree(), transform_first=False) assert_array_almost_equal(ax.get_extent(), (-180, 180, -25, 25)) diff --git a/lib/cartopy/tests/mpl/test_crs.py b/lib/cartopy/tests/mpl/test_crs.py index 18a9e5225..77e2f847e 100644 --- a/lib/cartopy/tests/mpl/test_crs.py +++ b/lib/cartopy/tests/mpl/test_crs.py @@ -12,9 +12,10 @@ @pytest.mark.natural_earth @pytest.mark.mpl_image_compare( - filename="igh_land.png", tolerance=0.5 if _MPL_37 else 3.6) + filename='igh_land.png', tolerance=0.5 if _MPL_37 else 3.6 +) def test_igh_land(): - crs = ccrs.InterruptedGoodeHomolosine(emphasis="land") + crs = ccrs.InterruptedGoodeHomolosine(emphasis='land') ax = plt.axes(projection=crs) ax.coastlines() ax.gridlines() @@ -22,12 +23,11 @@ def test_igh_land(): @pytest.mark.natural_earth -@pytest.mark.mpl_image_compare(filename="igh_ocean.png", - tolerance=0.5 if _MPL_37 else 4.5) +@pytest.mark.mpl_image_compare( + filename='igh_ocean.png', tolerance=0.5 if _MPL_37 else 4.5 +) def test_igh_ocean(): - crs = ccrs.InterruptedGoodeHomolosine( - central_longitude=-160, emphasis="ocean" - ) + crs = ccrs.InterruptedGoodeHomolosine(central_longitude=-160, emphasis='ocean') ax = plt.axes(projection=crs) ax.coastlines() ax.gridlines() @@ -38,8 +38,9 @@ def test_igh_ocean(): @pytest.mark.mpl_image_compare(filename='lambert_conformal_south.png') def test_lambert_south(): # Reference image: https://www.icsm.gov.au/mapping/map_projections.html - crs = ccrs.LambertConformal(central_longitude=140, cutoff=65, - standard_parallels=(-30, -60)) + crs = ccrs.LambertConformal( + central_longitude=140, cutoff=65, standard_parallels=(-30, -60) + ) ax = plt.axes(projection=crs) ax.coastlines() ax.gridlines() diff --git a/lib/cartopy/tests/mpl/test_examples.py b/lib/cartopy/tests/mpl/test_examples.py index a7f64ac93..3b0b828e3 100644 --- a/lib/cartopy/tests/mpl/test_examples.py +++ b/lib/cartopy/tests/mpl/test_examples.py @@ -37,9 +37,11 @@ def test_global_map(): @pytest.mark.natural_earth @pytest.mark.mpl_image_compare( - filename='contour_label.png', tolerance=3.9 if _MPL_38 else 0.5) + filename='contour_label.png', tolerance=3.9 if _MPL_38 else 0.5 +) def test_contour_label(): from cartopy.tests.mpl.test_caching import sample_data + fig = plt.figure() # Setup a global EckertIII map with faint coastlines. @@ -56,9 +58,9 @@ def test_contour_label(): filled_c = ax.contourf(x, y, z, transform=ccrs.PlateCarree()) # And black line contours. - line_c = ax.contour(x, y, z, levels=filled_c.levels, - colors=['black'], - transform=ccrs.PlateCarree()) + line_c = ax.contour( + x, y, z, levels=filled_c.levels, colors=['black'], transform=ccrs.PlateCarree() + ) # Uncomment to make the line contours invisible. # plt.setp(line_c.collections, visible=False) diff --git a/lib/cartopy/tests/mpl/test_feature_artist.py b/lib/cartopy/tests/mpl/test_feature_artist.py index b74842871..2c0917a03 100644 --- a/lib/cartopy/tests/mpl/test_feature_artist.py +++ b/lib/cartopy/tests/mpl/test_feature_artist.py @@ -16,16 +16,29 @@ from cartopy.mpl.feature_artist import FeatureArtist, _freeze, _GeomKey -@pytest.mark.parametrize("source, expected", [ - [{1: 0}, frozenset({(1, 0)})], - [[1, 2], (1, 2)], - [[1, {}], (1, frozenset())], - [[1, {'a': [1, 2, 3]}], (1, frozenset([('a', (1, 2, 3))]))], - [{'edgecolor': 'face', 'zorder': -1, - 'facecolor': np.array([0.9375, 0.9375, 0.859375])}, - frozenset([('edgecolor', 'face'), ('zorder', -1), - ('facecolor', (0.9375, 0.9375, 0.859375))])], -]) +@pytest.mark.parametrize( + 'source, expected', + [ + [{1: 0}, frozenset({(1, 0)})], + [[1, 2], (1, 2)], + [[1, {}], (1, frozenset())], + [[1, {'a': [1, 2, 3]}], (1, frozenset([('a', (1, 2, 3))]))], + [ + { + 'edgecolor': 'face', + 'zorder': -1, + 'facecolor': np.array([0.9375, 0.9375, 0.859375]), + }, + frozenset( + [ + ('edgecolor', 'face'), + ('zorder', -1), + ('facecolor', (0.9375, 0.9375, 0.859375)), + ] + ), + ], + ], +) def test_freeze(source, expected): assert _freeze(source) == expected @@ -47,7 +60,7 @@ def robinson_map(): `array` or a list of facecolors remains 1-to-1 with the list of geometries. """ prj_crs = ccrs.Robinson() - fig, ax = plt.subplots(subplot_kw={'projection':prj_crs}) + fig, ax = plt.subplots(subplot_kw={'projection': prj_crs}) ax.set_extent([20, 180, -90, 90]) ax.coastlines() @@ -111,7 +124,7 @@ def styler(geom): if geom == geoms[1]: return {'facecolor': 'red'} else: - return {'facecolor':'blue'} + return {'facecolor': 'blue'} fig, ax = robinson_map() ax.add_feature(feature, facecolor='red', styler=styler) diff --git a/lib/cartopy/tests/mpl/test_features.py b/lib/cartopy/tests/mpl/test_features.py index 369022958..eac2d4a3d 100644 --- a/lib/cartopy/tests/mpl/test_features.py +++ b/lib/cartopy/tests/mpl/test_features.py @@ -16,7 +16,7 @@ from cartopy.io.ogc_clients import _OWSLIB_AVAILABLE -@pytest.mark.filterwarnings("ignore:Downloading") +@pytest.mark.filterwarnings('ignore:Downloading') @pytest.mark.natural_earth @pytest.mark.mpl_image_compare(filename='natural_earth.png') def test_natural_earth(): @@ -32,14 +32,14 @@ def test_natural_earth(): return ax.figure -@pytest.mark.filterwarnings("ignore:Downloading") +@pytest.mark.filterwarnings('ignore:Downloading') @pytest.mark.natural_earth @pytest.mark.mpl_image_compare(filename='natural_earth_custom.png') def test_natural_earth_custom(): ax = plt.axes(projection=ccrs.PlateCarree()) - feature = cfeature.NaturalEarthFeature('physical', 'coastline', '50m', - edgecolor='black', - facecolor='none') + feature = cfeature.NaturalEarthFeature( + 'physical', 'coastline', '50m', edgecolor='black', facecolor='none' + ) ax.add_feature(feature) ax.set_xlim((-26, -12)) ax.set_ylim((58, 72)) @@ -57,33 +57,37 @@ def test_gshhs(): # Draw coastlines. ax.add_feature(cfeature.GSHHSFeature('coarse', edgecolor='red')) # Draw higher resolution lakes (and test overriding of kwargs) - ax.add_feature(cfeature.GSHHSFeature('low', levels=[2], - facecolor='green'), facecolor='blue') + ax.add_feature( + cfeature.GSHHSFeature('low', levels=[2], facecolor='green'), facecolor='blue' + ) return ax.figure @pytest.mark.network -@pytest.mark.skipif(not _HAS_PYKDTREE_OR_SCIPY or not _OWSLIB_AVAILABLE, - reason='OWSLib and at least one of pykdtree or scipy is required') -@pytest.mark.xfail(raises=ParseError, - reason="Bad XML returned from the URL") +@pytest.mark.skipif( + not _HAS_PYKDTREE_OR_SCIPY or not _OWSLIB_AVAILABLE, + reason='OWSLib and at least one of pykdtree or scipy is required', +) +@pytest.mark.xfail(raises=ParseError, reason='Bad XML returned from the URL') @pytest.mark.mpl_image_compare(filename='wfs.png') def test_wfs(): ax = plt.axes(projection=ccrs.OSGB(approx=True)) url = 'https://nsidc.org/cgi-bin/atlas_south?service=WFS' typename = 'land_excluding_antarctica' - feature = cfeature.WFSFeature(url, typename, - edgecolor='red') + feature = cfeature.WFSFeature(url, typename, edgecolor='red') ax.add_feature(feature) return ax.figure @pytest.mark.network -@pytest.mark.skipif(not _HAS_PYKDTREE_OR_SCIPY or not _OWSLIB_AVAILABLE, - reason='OWSLib and at least one of pykdtree or scipy is required') -@pytest.mark.xfail(raises=(ParseError, AttributeError), - reason="Bad XML returned from the URL") -@pytest.mark.xfail(reason="Unauthorized access to the WFS service") +@pytest.mark.skipif( + not _HAS_PYKDTREE_OR_SCIPY or not _OWSLIB_AVAILABLE, + reason='OWSLib and at least one of pykdtree or scipy is required', +) +@pytest.mark.xfail( + raises=(ParseError, AttributeError), reason='Bad XML returned from the URL' +) +@pytest.mark.xfail(reason='Unauthorized access to the WFS service') @pytest.mark.mpl_image_compare(filename='wfs_france.png') def test_wfs_france(): ax = plt.axes(projection=ccrs.epsg(2154)) diff --git a/lib/cartopy/tests/mpl/test_gridliner.py b/lib/cartopy/tests/mpl/test_gridliner.py index 661440707..e37609b78 100644 --- a/lib/cartopy/tests/mpl/test_gridliner.py +++ b/lib/cartopy/tests/mpl/test_gridliner.py @@ -38,13 +38,21 @@ ccrs.Robinson, ccrs.Sinusoidal, ccrs.Stereographic, - pytest.param((ccrs.InterruptedGoodeHomolosine, dict(emphasis='land')), - id='InterruptedGoodeHomolosine'), pytest.param( - (ccrs.RotatedPole, - dict(pole_longitude=180.0, pole_latitude=36.0, - central_rotated_longitude=-106.0)), - id='RotatedPole'), + (ccrs.InterruptedGoodeHomolosine, dict(emphasis='land')), + id='InterruptedGoodeHomolosine', + ), + pytest.param( + ( + ccrs.RotatedPole, + dict( + pole_longitude=180.0, + pole_latitude=36.0, + central_rotated_longitude=-106.0, + ), + ), + id='RotatedPole', + ), pytest.param((ccrs.OSGB, dict(approx=False)), id='OSGB'), ccrs.EuroPP, ccrs.Geostationary, @@ -67,54 +75,53 @@ def test_gridliner(): ax = fig.add_subplot(nx, ny, 1, projection=ccrs.PlateCarree()) ax.set_global() - ax.coastlines(resolution="110m") + ax.coastlines(resolution='110m') ax.gridlines(linestyle=':') ax = fig.add_subplot(nx, ny, 2, projection=ccrs.OSGB(approx=False)) ax.set_global() - ax.coastlines(resolution="110m") + ax.coastlines(resolution='110m') ax.gridlines(linestyle=':') ax = fig.add_subplot(nx, ny, 3, projection=ccrs.OSGB(approx=False)) ax.set_global() - ax.coastlines(resolution="110m") + ax.coastlines(resolution='110m') ax.gridlines(ccrs.PlateCarree(), color='blue', linestyle='-') ax.gridlines(ccrs.OSGB(approx=False), linestyle=':') ax = fig.add_subplot(nx, ny, 4, projection=ccrs.PlateCarree()) ax.set_global() - ax.coastlines(resolution="110m") - ax.gridlines(ccrs.NorthPolarStereo(), alpha=0.5, - linewidth=1.5, linestyle='-') + ax.coastlines(resolution='110m') + ax.gridlines(ccrs.NorthPolarStereo(), alpha=0.5, linewidth=1.5, linestyle='-') ax = fig.add_subplot(nx, ny, 5, projection=ccrs.PlateCarree()) ax.set_global() - ax.coastlines(resolution="110m") + ax.coastlines(resolution='110m') osgb = ccrs.OSGB(approx=False) ax.set_extent(tuple(osgb.x_limits) + tuple(osgb.y_limits), crs=osgb) ax.gridlines(osgb, linestyle=':') ax = fig.add_subplot(nx, ny, 6, projection=ccrs.NorthPolarStereo()) ax.set_global() - ax.coastlines(resolution="110m") + ax.coastlines(resolution='110m') ax.gridlines(alpha=0.5, linewidth=1.5, linestyle='-') ax = fig.add_subplot(nx, ny, 7, projection=ccrs.NorthPolarStereo()) ax.set_global() - ax.coastlines(resolution="110m") + ax.coastlines(resolution='110m') osgb = ccrs.OSGB(approx=False) ax.set_extent(tuple(osgb.x_limits) + tuple(osgb.y_limits), crs=osgb) ax.gridlines(osgb, linestyle=':') - ax = fig.add_subplot(nx, ny, 8, - projection=ccrs.Robinson(central_longitude=135)) + ax = fig.add_subplot(nx, ny, 8, projection=ccrs.Robinson(central_longitude=135)) ax.set_global() - ax.coastlines(resolution="110m") + ax.coastlines(resolution='110m') ax.gridlines(ccrs.PlateCarree(), alpha=0.5, linewidth=1.5, linestyle='-') delta = 1.5e-2 - fig.subplots_adjust(left=0 + delta, right=1 - delta, - top=1 - delta, bottom=0 + delta) + fig.subplots_adjust( + left=0 + delta, right=1 - delta, top=1 - delta, bottom=0 + delta + ) return fig @@ -135,10 +142,11 @@ def test_gridliner_specified_lines(): grid_label_tol = 3.9 -@pytest.mark.skipif(geos_version == (3, 9, 0), reason="GEOS intersection bug") +@pytest.mark.skipif(geos_version == (3, 9, 0), reason='GEOS intersection bug') @pytest.mark.natural_earth -@pytest.mark.mpl_image_compare(filename='gridliner_labels.png', - tolerance=grid_label_tol) +@pytest.mark.mpl_image_compare( + filename='gridliner_labels.png', tolerance=grid_label_tol +) def test_grid_labels(): fig = plt.figure(figsize=(10, 10)) @@ -146,12 +154,11 @@ def test_grid_labels(): crs_merc = ccrs.Mercator() ax = fig.add_subplot(3, 2, 1, projection=crs_pc) - ax.coastlines(resolution="110m") + ax.coastlines(resolution='110m') ax.gridlines(draw_labels=True) - ax = fig.add_subplot( - 3, 2, 2, projection=ccrs.PlateCarree(central_longitude=180)) - ax.coastlines(resolution="110m") + ax = fig.add_subplot(3, 2, 2, projection=ccrs.PlateCarree(central_longitude=180)) + ax.coastlines(resolution='110m') gl = ax.gridlines(crs=crs_pc, draw_labels=True) gl.top_labels = False @@ -159,14 +166,13 @@ def test_grid_labels(): gl.xlines = False ax = fig.add_subplot(3, 2, 3, projection=crs_merc) - ax.coastlines(resolution="110m") + ax.coastlines(resolution='110m') gl = ax.gridlines(draw_labels=True) gl.xlabel_style = gl.ylabel_style = {'size': 9} ax = fig.add_subplot(3, 2, 4, projection=crs_pc) - ax.coastlines(resolution="110m") - gl = ax.gridlines( - crs=crs_pc, linewidth=2, color='gray', alpha=0.5, linestyle=':') + ax.coastlines(resolution='110m') + gl = ax.gridlines(crs=crs_pc, linewidth=2, color='gray', alpha=0.5, linestyle=':') gl.bottom_labels = True gl.right_labels = True gl.xlines = False @@ -180,24 +186,20 @@ def test_grid_labels(): # trigger a draw at this point and check the appropriate artists are # populated on the gridliner instance fig.canvas.draw() - assert len([ - lb for lb in gl.bottom_label_artists if lb.get_visible()]) == 4 - assert len([ - lb for lb in gl.top_label_artists if lb.get_visible()]) == 0 - assert len([ - lb for lb in gl.left_label_artists if lb.get_visible()]) == 0 - assert len([ - lb for lb in gl.right_label_artists if lb.get_visible()]) != 0 + assert len([lb for lb in gl.bottom_label_artists if lb.get_visible()]) == 4 + assert len([lb for lb in gl.top_label_artists if lb.get_visible()]) == 0 + assert len([lb for lb in gl.left_label_artists if lb.get_visible()]) == 0 + assert len([lb for lb in gl.right_label_artists if lb.get_visible()]) != 0 assert len(gl.xline_artists) == 0 ax = fig.add_subplot(3, 2, 5, projection=crs_pc) ax.set_extent([-20, 10.0, 45.0, 70.0]) - ax.coastlines(resolution="110m") + ax.coastlines(resolution='110m') ax.gridlines(draw_labels=True) ax = fig.add_subplot(3, 2, 6, projection=crs_merc) ax.set_extent([-20, 10.0, 45.0, 70.0], crs=crs_pc) - ax.coastlines(resolution="110m") + ax.coastlines(resolution='110m') gl = ax.gridlines(draw_labels=True) gl.rotate_labels = False gl.xlabel_style = gl.ylabel_style = {'size': 9} @@ -208,10 +210,9 @@ def test_grid_labels(): return fig -@pytest.mark.skipif(geos_version == (3, 9, 0), reason="GEOS intersection bug") +@pytest.mark.skipif(geos_version == (3, 9, 0), reason='GEOS intersection bug') @pytest.mark.natural_earth -@pytest.mark.mpl_image_compare(filename='gridliner_labels_tight.png', - tolerance=2.9) +@pytest.mark.mpl_image_compare(filename='gridliner_labels_tight.png', tolerance=2.9) def test_grid_labels_tight(): # Ensure tight layout accounts for gridlines fig = plt.figure(figsize=(7, 5)) @@ -220,11 +221,11 @@ def test_grid_labels_tight(): crs_merc = ccrs.Mercator() ax = fig.add_subplot(2, 2, 1, projection=crs_pc) - ax.coastlines(resolution="110m") + ax.coastlines(resolution='110m') ax.gridlines(draw_labels=True) ax = fig.add_subplot(2, 2, 2, projection=crs_merc) - ax.coastlines(resolution="110m") + ax.coastlines(resolution='110m') ax.gridlines(draw_labels=True) # Matplotlib tight layout is also incorrect if cartopy fails @@ -232,12 +233,12 @@ def test_grid_labels_tight(): # changed due to set_extent. ax = fig.add_subplot(2, 2, 3, projection=crs_pc) ax.set_extent([-20, 10.0, 45.0, 70.0]) - ax.coastlines(resolution="110m") + ax.coastlines(resolution='110m') ax.gridlines(draw_labels=True) ax = fig.add_subplot(2, 2, 4, projection=crs_merc) ax.set_extent([-20, 10.0, 45.0, 70.0], crs=crs_pc) - ax.coastlines(resolution="110m") + ax.coastlines(resolution='110m') gl = ax.gridlines(draw_labels=True) gl.rotate_labels = False @@ -248,8 +249,7 @@ def test_grid_labels_tight(): num_gridliners_drawn = 0 for ax in fig.axes: for artist in ax.artists: - if isinstance(artist, Gridliner) and getattr(artist, '_drawn', - False): + if isinstance(artist, Gridliner) and getattr(artist, '_drawn', False): num_gridliners_drawn += 1 assert num_gridliners_drawn == 4 @@ -258,10 +258,10 @@ def test_grid_labels_tight(): @pytest.mark.mpl_image_compare( - filename='gridliner_constrained_adjust_datalim.png', - tolerance=grid_label_tol) + filename='gridliner_constrained_adjust_datalim.png', tolerance=grid_label_tol +) def test_gridliner_constrained_adjust_datalim(): - fig = plt.figure(figsize=(8, 4), layout="constrained") + fig = plt.figure(figsize=(8, 4), layout='constrained') # Make some axes that will fill the available space while maintaining # correct aspect ratio @@ -285,12 +285,12 @@ def test_gridliner_constrained_adjust_datalim(): ax.autoscale() # Add some gridlines - ax.gridlines(draw_labels=["bottom", "left"], linestyle="-") + ax.gridlines(draw_labels=['bottom', 'left'], linestyle='-') return fig -@pytest.mark.skipif(geos_version == (3, 9, 0), reason="GEOS intersection bug") +@pytest.mark.skipif(geos_version == (3, 9, 0), reason='GEOS intersection bug') @pytest.mark.natural_earth @pytest.mark.parametrize('proj', TEST_PROJS) @pytest.mark.mpl_image_compare(style='mpl20') @@ -302,11 +302,11 @@ def test_grid_labels_inline(proj): kwargs = {} ax = fig.add_subplot(projection=proj(**kwargs)) ax.gridlines(crs=ccrs.PlateCarree(), draw_labels=True, auto_inline=True) - ax.coastlines(resolution="110m") + ax.coastlines(resolution='110m') return fig -@pytest.mark.skipif(geos_version == (3, 9, 0), reason="GEOS intersection bug") +@pytest.mark.skipif(geos_version == (3, 9, 0), reason='GEOS intersection bug') @pytest.mark.natural_earth @pytest.mark.parametrize('proj', TEST_PROJS) @pytest.mark.mpl_image_compare(style='mpl20', tolerance=0.79) @@ -322,19 +322,19 @@ def test_grid_labels_inline_usa(proj): kwargs = {} ax = fig.add_subplot(projection=proj(**kwargs)) try: - ax.set_extent([left, right, bottom, top], - crs=ccrs.PlateCarree()) + ax.set_extent([left, right, bottom, top], crs=ccrs.PlateCarree()) except Exception: pytest.skip('Projection does not support changing extent') ax.gridlines(draw_labels=True, auto_inline=True, clip_on=True) - ax.coastlines(resolution="110m") + ax.coastlines(resolution='110m') return fig @pytest.mark.natural_earth -@pytest.mark.skipif(geos_version == (3, 9, 0), reason="GEOS intersection bug") -@pytest.mark.mpl_image_compare(filename='gridliner_labels_bbox_style.png', - tolerance=grid_label_tol) +@pytest.mark.skipif(geos_version == (3, 9, 0), reason='GEOS intersection bug') +@pytest.mark.mpl_image_compare( + filename='gridliner_labels_bbox_style.png', tolerance=grid_label_tol +) def test_gridliner_labels_bbox_style(): top = 49.3457868 # north lat left = -124.7844079 # west long @@ -343,45 +343,83 @@ def test_gridliner_labels_bbox_style(): fig = plt.figure(figsize=(6, 3)) ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree()) - ax.coastlines(resolution="110m") - ax.set_extent([left, right, bottom, top], - crs=ccrs.PlateCarree()) + ax.coastlines(resolution='110m') + ax.set_extent([left, right, bottom, top], crs=ccrs.PlateCarree()) gl = ax.gridlines(draw_labels=True) gl.labels_bbox_style = { - "pad": 0, - "visible": True, - "facecolor": "white", - "edgecolor": "black", - "boxstyle": "round, pad=0.2", + 'pad': 0, + 'visible': True, + 'facecolor': 'white', + 'edgecolor': 'black', + 'boxstyle': 'round, pad=0.2', } return fig @pytest.mark.parametrize( - "proj,gcrs,xloc,xfmt,xloc_expected,xfmt_expected", + 'proj,gcrs,xloc,xfmt,xloc_expected,xfmt_expected', [ - (ccrs.PlateCarree(), ccrs.PlateCarree(), - [10, 20], None, mticker.FixedLocator, LongitudeFormatter), - (ccrs.PlateCarree(), ccrs.Mercator(), - [10, 20], None, mticker.FixedLocator, classic_formatter), - (ccrs.PlateCarree(), ccrs.PlateCarree(), - mticker.MaxNLocator(nbins=9), None, - mticker.MaxNLocator, LongitudeFormatter), - (ccrs.PlateCarree(), ccrs.Mercator(), - mticker.MaxNLocator(nbins=9), None, - mticker.MaxNLocator, classic_formatter), - (ccrs.PlateCarree(), ccrs.PlateCarree(), - None, None, LongitudeLocator, LongitudeFormatter), - (ccrs.PlateCarree(), ccrs.Mercator(), - None, None, classic_locator.__class__, classic_formatter), - (ccrs.PlateCarree(), ccrs.PlateCarree(), - None, mticker.StrMethodFormatter('{x}'), - LongitudeLocator, mticker.StrMethodFormatter), - ]) -def test_gridliner_default_fmtloc( - proj, gcrs, xloc, xfmt, xloc_expected, xfmt_expected): + ( + ccrs.PlateCarree(), + ccrs.PlateCarree(), + [10, 20], + None, + mticker.FixedLocator, + LongitudeFormatter, + ), + ( + ccrs.PlateCarree(), + ccrs.Mercator(), + [10, 20], + None, + mticker.FixedLocator, + classic_formatter, + ), + ( + ccrs.PlateCarree(), + ccrs.PlateCarree(), + mticker.MaxNLocator(nbins=9), + None, + mticker.MaxNLocator, + LongitudeFormatter, + ), + ( + ccrs.PlateCarree(), + ccrs.Mercator(), + mticker.MaxNLocator(nbins=9), + None, + mticker.MaxNLocator, + classic_formatter, + ), + ( + ccrs.PlateCarree(), + ccrs.PlateCarree(), + None, + None, + LongitudeLocator, + LongitudeFormatter, + ), + ( + ccrs.PlateCarree(), + ccrs.Mercator(), + None, + None, + classic_locator.__class__, + classic_formatter, + ), + ( + ccrs.PlateCarree(), + ccrs.PlateCarree(), + None, + mticker.StrMethodFormatter('{x}'), + LongitudeLocator, + mticker.StrMethodFormatter, + ), + ], +) +def test_gridliner_default_fmtloc(proj, gcrs, xloc, xfmt, xloc_expected, xfmt_expected): fig = plt.figure() ax = fig.add_subplot(1, 1, 1, projection=proj) gl = ax.gridlines(crs=gcrs, draw_labels=False, xlocs=xloc, xformatter=xfmt) @@ -416,46 +454,52 @@ def test_gridliner_line_limits(): @pytest.mark.parametrize( - "draw_labels, result", + 'draw_labels, result', [ - (True, - {'left': ['40°N'], - 'right': ['40°N', '50°N'], - 'top': ['70°E', '100°E', '130°E'], - 'bottom': ['100°E']}), - (False, - {'left': [], - 'right': [], - 'top': [], - 'bottom': []}), - (['top', 'left'], - {'left': ['40°N'], - 'right': [], - 'top': ['70°E', '100°E', '130°E'], - 'bottom': []}), - ({'top': 'x', 'right': 'y'}, - {'left': [], - 'right': ['40°N', '50°N'], - 'top': ['70°E', '100°E', '130°E'], - 'bottom': []}), - ({'left': 'x'}, - {'left': ['70°E'], - 'right': [], - 'top': [], - 'bottom': []}), - ({'top': 'y'}, - {'left': [], - 'right': [], - 'top': ['50°N'], - 'bottom': []}), - ]) + ( + True, + { + 'left': ['40°N'], + 'right': ['40°N', '50°N'], + 'top': ['70°E', '100°E', '130°E'], + 'bottom': ['100°E'], + }, + ), + (False, {'left': [], 'right': [], 'top': [], 'bottom': []}), + ( + ['top', 'left'], + { + 'left': ['40°N'], + 'right': [], + 'top': ['70°E', '100°E', '130°E'], + 'bottom': [], + }, + ), + ( + {'top': 'x', 'right': 'y'}, + { + 'left': [], + 'right': ['40°N', '50°N'], + 'top': ['70°E', '100°E', '130°E'], + 'bottom': [], + }, + ), + ({'left': 'x'}, {'left': ['70°E'], 'right': [], 'top': [], 'bottom': []}), + ({'top': 'y'}, {'left': [], 'right': [], 'top': ['50°N'], 'bottom': []}), + ], +) def test_gridliner_draw_labels_param(draw_labels, result): fig = plt.figure() lambert_crs = ccrs.LambertConformal(central_longitude=105) ax = fig.add_subplot(projection=lambert_crs) ax.set_extent([75, 130, 18, 54], crs=ccrs.PlateCarree()) - gl = ax.gridlines(draw_labels=draw_labels, rotate_labels=False, dms=True, - x_inline=False, y_inline=False) + gl = ax.gridlines( + draw_labels=draw_labels, + rotate_labels=False, + dms=True, + x_inline=False, + y_inline=False, + ) gl.xlocator = mticker.FixedLocator([70, 100, 130]) gl.ylocator = mticker.FixedLocator([40, 50]) fig.canvas.draw() @@ -470,8 +514,11 @@ def test_gridliner_formatter_kwargs(): fig = plt.figure() ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree()) ax.set_extent([-80, -40.0, 10.0, -30.0]) - gl = ax.gridlines(draw_labels=True, dms=False, - formatter_kwargs=dict(cardinal_labels={'west': 'O'})) + gl = ax.gridlines( + draw_labels=True, + dms=False, + formatter_kwargs=dict(cardinal_labels={'west': 'O'}), + ) fig.canvas.draw() labels = [a.get_text() for a in gl.bottom_label_artists if a.get_visible()] assert labels == ['75°O', '70°O', '65°O', '60°O', '55°O', '50°O', '45°O'] @@ -495,7 +542,8 @@ def test_gridliner_count_draws(): @pytest.mark.natural_earth @pytest.mark.mpl_image_compare( baseline_dir='baseline_images/mpl/test_mpl_integration', - filename='simple_global.png') + filename='simple_global.png', +) def test_gridliner_remove(): fig = plt.figure() ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree()) @@ -520,19 +568,24 @@ def test_gridliner_save_tight_bbox(): @pytest.mark.natural_earth -@pytest.mark.mpl_image_compare(filename='gridliner_labels_title_adjust.png', - tolerance=grid_label_tol) +@pytest.mark.mpl_image_compare( + filename='gridliner_labels_title_adjust.png', tolerance=grid_label_tol +) def test_gridliner_title_adjust(): # Test that title do not overlap labels - projs = [ccrs.Mercator(), ccrs.AlbersEqualArea(), ccrs.LambertConformal(), - ccrs.Orthographic()] + projs = [ + ccrs.Mercator(), + ccrs.AlbersEqualArea(), + ccrs.LambertConformal(), + ccrs.Orthographic(), + ] # Turn on automatic title placement (this is default in mpl rcParams but # not in these tests). plt.rcParams['axes.titley'] = None fig = plt.figure(layout='constrained') - fig.get_layout_engine().set(h_pad=1/8) + fig.get_layout_engine().set(h_pad=1 / 8) for n, proj in enumerate(projs, 1): ax = fig.add_subplot(2, 2, n, projection=proj) ax.coastlines() @@ -561,7 +614,7 @@ def test_gridliner_title_adjust_no_layout_engine(): fig = plt.figure() ax = fig.add_subplot(projection=ccrs.PlateCarree()) gl = ax.gridlines(draw_labels=True) - title = ax.set_title("MY TITLE") + title = ax.set_title('MY TITLE') # After first draw, title should be above top labels. fig.draw_without_rendering() diff --git a/lib/cartopy/tests/mpl/test_images.py b/lib/cartopy/tests/mpl/test_images.py index fb9609517..e94ed9d0d 100644 --- a/lib/cartopy/tests/mpl/test_images.py +++ b/lib/cartopy/tests/mpl/test_images.py @@ -23,10 +23,15 @@ pytest.skip('pykdtree or scipy is required', allow_module_level=True) -NATURAL_EARTH_IMG = (config["repo_data_dir"] / 'raster' / 'natural_earth' - / '50-natural-earth-1-downsampled.png') -REGIONAL_IMG = (config['repo_data_dir'] / 'raster' / 'sample' - / 'Miriam.A2012270.2050.2km.jpg') +NATURAL_EARTH_IMG = ( + config['repo_data_dir'] + / 'raster' + / 'natural_earth' + / '50-natural-earth-1-downsampled.png' +) +REGIONAL_IMG = ( + config['repo_data_dir'] / 'raster' / 'sample' / 'Miriam.A2012270.2050.2km.jpg' +) # We have an exceptionally large tolerance for the web_tiles test. @@ -37,35 +42,53 @@ @pytest.mark.mpl_image_compare(filename='web_tiles.png', tolerance=5.91) def test_web_tiles(): extent = [-15, 0.1, 50, 60] - target_domain = sgeom.Polygon([[extent[0], extent[1]], - [extent[2], extent[1]], - [extent[2], extent[3]], - [extent[0], extent[3]], - [extent[0], extent[1]]]) + target_domain = sgeom.Polygon( + [ + [extent[0], extent[1]], + [extent[2], extent[1]], + [extent[2], extent[3]], + [extent[0], extent[3]], + [extent[0], extent[1]], + ] + ) map_prj = cimgt.GoogleTiles().crs fig = plt.figure() ax = fig.add_subplot(2, 2, 1, projection=map_prj) gt = cimgt.GoogleTiles() - gt._image_url = types.MethodType(ctest_tiles.GOOGLE_IMAGE_URL_REPLACEMENT, - gt) + gt._image_url = types.MethodType(ctest_tiles.GOOGLE_IMAGE_URL_REPLACEMENT, gt) img, extent, origin = gt.image_for_domain(target_domain, 1) - ax.imshow(np.array(img), extent=extent, transform=gt.crs, - interpolation='bilinear', origin=origin) + ax.imshow( + np.array(img), + extent=extent, + transform=gt.crs, + interpolation='bilinear', + origin=origin, + ) ax.coastlines(color='white') ax = fig.add_subplot(2, 2, 2, projection=map_prj) qt = cimgt.QuadtreeTiles() img, extent, origin = qt.image_for_domain(target_domain, 1) - ax.imshow(np.array(img), extent=extent, transform=qt.crs, - interpolation='bilinear', origin=origin) + ax.imshow( + np.array(img), + extent=extent, + transform=qt.crs, + interpolation='bilinear', + origin=origin, + ) ax.coastlines(color='white') ax = fig.add_subplot(2, 2, 3, projection=map_prj) osm = cimgt.OSM() img, extent, origin = osm.image_for_domain(target_domain, 1) - ax.imshow(np.array(img), extent=extent, transform=osm.crs, - interpolation='bilinear', origin=origin) + ax.imshow( + np.array(img), + extent=extent, + transform=osm.crs, + interpolation='bilinear', + origin=origin, + ) ax.coastlines() return fig @@ -82,8 +105,7 @@ def test_image_merge(): tiles.append((i, j, 2)) gt = cimgt.GoogleTiles() - gt._image_url = types.MethodType(ctest_tiles.GOOGLE_IMAGE_URL_REPLACEMENT, - gt) + gt._image_url = types.MethodType(ctest_tiles.GOOGLE_IMAGE_URL_REPLACEMENT, gt) images_to_merge = [] for tile in tiles: img, extent, origin = gt.get_image(tile) @@ -109,18 +131,20 @@ def test_imshow(): # form that JPG images would be loaded with imread. img = (img * 255).astype('uint8') ax = plt.axes(projection=ccrs.Orthographic()) - ax.imshow(img, transform=source_proj, - extent=[-180, 180, -90, 90]) + ax.imshow(img, transform=source_proj, extent=[-180, 180, -90, 90]) return ax.figure @pytest.mark.natural_earth -@pytest.mark.mpl_image_compare(filename='imshow_regional_projected.png', - tolerance=1.97) +@pytest.mark.mpl_image_compare(filename='imshow_regional_projected.png', tolerance=1.97) def test_imshow_projected(): source_proj = ccrs.PlateCarree() - img_extent = (-120.67660000000001, -106.32104523100001, - 13.2301484511245, 30.766899999999502) + img_extent = ( + -120.67660000000001, + -106.32104523100001, + 13.2301484511245, + 30.766899999999502, + ) img = plt.imread(REGIONAL_IMG) ax = plt.axes(projection=ccrs.LambertConformal()) ax.set_extent(img_extent, crs=source_proj) @@ -133,8 +157,11 @@ 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)) + ax.imshow( + np.random.random((10, 10)), + transform=ccrs.PlateCarree(), + extent=(0, 360, -90, 90), + ) assert ax.get_xlim() == (-180, 180) @@ -144,9 +171,13 @@ def test_imshow_arguments(): ax = plt.axes(projection=ccrs.PlateCarree()) # Set the regrid_shape parameter to ensure it isn't passed to Axes.imshow() # in the fast-path call to super() - with pytest.warns(UserWarning, match="ignoring regrid_shape"): - ax.imshow(np.random.random((10, 10)), transform=ccrs.PlateCarree(), - extent=(-180, 180, -90, 90), regrid_shape=500) + with pytest.warns(UserWarning, match='ignoring regrid_shape'): + ax.imshow( + np.random.random((10, 10)), + transform=ccrs.PlateCarree(), + extent=(-180, 180, -90, 90), + regrid_shape=500, + ) def test_imshow_rgba(): @@ -171,8 +202,7 @@ def test_imshow_rgba_alpha(): ax = plt.axes(projection=ccrs.Orthographic(-120, 45)) # Create RGBA Image with random data and linspace alpha - RGBA = np.linspace(0, 255 * 31, dx * dy * 4, - dtype=np.uint8).reshape((dy, dx, 4)) + RGBA = np.linspace(0, 255 * 31, dx * dy * 4, dtype=np.uint8).reshape((dy, dx, 4)) alpha = np.array([0, 85, 170, 255]) RGBA[:, :, 3] = alpha @@ -205,8 +235,7 @@ def test_pil_Image(): img = Image.open(NATURAL_EARTH_IMG) source_proj = ccrs.PlateCarree() ax = plt.axes(projection=ccrs.Orthographic()) - ax.imshow(img, transform=source_proj, - extent=[-180, 180, -90, 90]) + ax.imshow(img, transform=source_proj, extent=[-180, 180, -90, 90]) return ax.figure @@ -219,16 +248,15 @@ def test_background_img(): def test_alpha_2d_warp(): # tests that both image and alpha arrays (if alpha is 2D) are warped - plt_crs = ccrs.Geostationary(central_longitude=-155.) + plt_crs = ccrs.Geostationary(central_longitude=-155.0) fig = plt.figure(figsize=(5, 5)) ax = fig.add_subplot(1, 1, 1, projection=plt_crs) latlon_crs = ccrs.PlateCarree() - coords = [-162., -148., 17.5, 23.] + coords = [-162.0, -148.0, 17.5, 23.0] ax.set_extent(coords, crs=latlon_crs) fake_data = np.zeros([100, 100]) fake_alphas = np.zeros(fake_data.shape) - image = ax.imshow(fake_data, extent=coords, transform=latlon_crs, - alpha=fake_alphas) + image = ax.imshow(fake_data, extent=coords, transform=latlon_crs, alpha=fake_alphas) image_data = image.get_array() image_alpha = image.get_alpha() diff --git a/lib/cartopy/tests/mpl/test_img_transform.py b/lib/cartopy/tests/mpl/test_img_transform.py index 9b206afd3..d04e1895b 100644 --- a/lib/cartopy/tests/mpl/test_img_transform.py +++ b/lib/cartopy/tests/mpl/test_img_transform.py @@ -27,26 +27,26 @@ def test_array_dims(self): # Source data source_nx = 100 source_ny = 100 - source_x = np.linspace(-180.0, - 180.0, - source_nx).astype(np.float64) + source_x = np.linspace(-180.0, 180.0, source_nx).astype(np.float64) source_y = np.linspace(-90, 90.0, source_ny).astype(np.float64) source_x, source_y = np.meshgrid(source_x, source_y) - data = np.arange(source_nx * source_ny, - dtype=np.int32).reshape(source_ny, source_nx) + data = np.arange(source_nx * source_ny, dtype=np.int32).reshape( + source_ny, source_nx + ) source_cs = ccrs.Geodetic() # Target grid target_nx = 23 target_ny = 45 target_proj = ccrs.PlateCarree() - target_x, target_y, extent = im_trans.mesh_projection(target_proj, - target_nx, - target_ny) + target_x, target_y, extent = im_trans.mesh_projection( + target_proj, target_nx, target_ny + ) # Perform regrid - new_array = im_trans.regrid(data, source_x, source_y, source_cs, - target_proj, target_x, target_y) + new_array = im_trans.regrid( + data, source_x, source_y, source_cs, target_proj, target_x, target_y + ) # Check dimensions of return array assert new_array.shape == target_x.shape @@ -57,28 +57,30 @@ def test_different_dims(self): # Source data source_nx = 100 source_ny = 100 - source_x = np.linspace(-180.0, 180.0, - source_nx).astype(np.float64) - source_y = np.linspace(-90, 90.0, - source_ny).astype(np.float64) + source_x = np.linspace(-180.0, 180.0, source_nx).astype(np.float64) + source_y = np.linspace(-90, 90.0, source_ny).astype(np.float64) source_x, source_y = np.meshgrid(source_x, source_y) - data = np.arange(source_nx * source_ny, - dtype=np.int32).reshape(source_ny, source_nx) + data = np.arange(source_nx * source_ny, dtype=np.int32).reshape( + source_ny, source_nx + ) source_cs = ccrs.Geodetic() # Target grids (different shapes) target_x_shape = (23, 45) target_y_shape = (23, 44) - target_x = np.arange(reduce(operator.mul, target_x_shape), - dtype=np.float64).reshape(target_x_shape) - target_y = np.arange(reduce(operator.mul, target_y_shape), - dtype=np.float64).reshape(target_y_shape) + target_x = np.arange( + reduce(operator.mul, target_x_shape), dtype=np.float64 + ).reshape(target_x_shape) + target_y = np.arange( + reduce(operator.mul, target_y_shape), dtype=np.float64 + ).reshape(target_y_shape) target_proj = ccrs.PlateCarree() # Attempt regrid with pytest.raises(ValueError): - im_trans.regrid(data, source_x, source_y, source_cs, - target_proj, target_x, target_y) + im_trans.regrid( + data, source_x, source_y, source_cs, target_proj, target_x, target_y + ) # Bug in latest Matplotlib that we don't consider correct. @@ -86,8 +88,12 @@ def test_different_dims(self): @pytest.mark.mpl_image_compare(filename='regrid_image.png', tolerance=5.55) def test_regrid_image(): # Source data - fname = (config["repo_data_dir"] / 'raster' / 'natural_earth' - / '50-natural-earth-1-downsampled.png') + fname = ( + config['repo_data_dir'] + / 'raster' + / 'natural_earth' + / '50-natural-earth-1-downsampled.png' + ) nx = 720 ny = 360 source_proj = ccrs.PlateCarree() @@ -100,18 +106,18 @@ def test_regrid_image(): target_nx = 300 target_ny = 300 target_proj = ccrs.InterruptedGoodeHomolosine(emphasis='land') - target_x, target_y, target_extent = im_trans.mesh_projection(target_proj, - target_nx, - target_ny) + target_x, target_y, target_extent = im_trans.mesh_projection( + target_proj, target_nx, target_ny + ) # Perform regrid - new_array = im_trans.regrid(data, source_x, source_y, source_proj, - target_proj, target_x, target_y) + new_array = im_trans.regrid( + data, source_x, source_y, source_proj, target_proj, target_x, target_y + ) # Plot fig = plt.figure(figsize=(10, 10)) - gs = mpl.gridspec.GridSpec(nrows=4, ncols=1, - hspace=1.5, wspace=0.5) + gs = mpl.gridspec.GridSpec(nrows=4, ncols=1, hspace=1.5, wspace=0.5) # Set up axes and title ax = fig.add_subplot(gs[0], projection=target_proj) ax.imshow(new_array, origin='lower', extent=target_extent) @@ -120,8 +126,9 @@ def test_regrid_image(): cmaps = {'red': 'Reds', 'green': 'Greens', 'blue': 'Blues'} for i, color in enumerate(['red', 'green', 'blue']): ax = fig.add_subplot(gs[i + 1], projection=target_proj) - ax.imshow(new_array[:, :, i], extent=target_extent, origin='lower', - cmap=cmaps[color]) + ax.imshow( + new_array[:, :, i], extent=target_extent, origin='lower', cmap=cmaps[color] + ) ax.coastlines() # Tighten up layout diff --git a/lib/cartopy/tests/mpl/test_mpl_integration.py b/lib/cartopy/tests/mpl/test_mpl_integration.py index f1aeccdfa..4c9ab5019 100644 --- a/lib/cartopy/tests/mpl/test_mpl_integration.py +++ b/lib/cartopy/tests/mpl/test_mpl_integration.py @@ -19,9 +19,11 @@ proj_version = parse_version(pyproj.proj_version_str) + @pytest.mark.natural_earth -@pytest.mark.mpl_image_compare(filename='global_contour_wrap.png', - style='mpl20', tolerance=2.25) +@pytest.mark.mpl_image_compare( + filename='global_contour_wrap.png', style='mpl20', tolerance=2.25 +) def test_global_contour_wrap_new_transform(): ax = plt.axes(projection=ccrs.PlateCarree()) ax.coastlines() @@ -32,8 +34,9 @@ def test_global_contour_wrap_new_transform(): @pytest.mark.natural_earth -@pytest.mark.mpl_image_compare(filename='global_contour_wrap.png', - style='mpl20', tolerance=2.25) +@pytest.mark.mpl_image_compare( + filename='global_contour_wrap.png', style='mpl20', tolerance=2.25 +) def test_global_contour_wrap_no_transform(): ax = plt.axes(projection=ccrs.PlateCarree()) ax.coastlines() @@ -130,9 +133,7 @@ def test_global_hexbin_wrap(): @pytest.mark.natural_earth -@pytest.mark.mpl_image_compare( - filename='global_hexbin_wrap.png', - tolerance=0.5) +@pytest.mark.mpl_image_compare(filename='global_hexbin_wrap.png', tolerance=0.5) def test_global_hexbin_wrap_transform(): ax = plt.axes(projection=ccrs.PlateCarree()) ax.coastlines(zorder=2) @@ -150,7 +151,7 @@ def test_global_hexbin_wrap_transform(): return ax.figure -@pytest.mark.filterwarnings("ignore:Unable to determine extent") +@pytest.mark.filterwarnings('ignore:Unable to determine extent') @pytest.mark.natural_earth @pytest.mark.mpl_image_compare(filename='simple_global.png') def test_simple_global(): @@ -160,51 +161,61 @@ def test_simple_global(): return ax.figure -@pytest.mark.filterwarnings("ignore:Unable to determine extent") +@pytest.mark.filterwarnings('ignore:Unable to determine extent') @pytest.mark.natural_earth -@pytest.mark.parametrize('proj', [ - ccrs.Aitoff, - ccrs.EckertI, - ccrs.EckertII, - ccrs.EckertIII, - ccrs.EckertIV, - ccrs.EckertV, - ccrs.EckertVI, - ccrs.EqualEarth, - ccrs.Gnomonic, - ccrs.Hammer, - pytest.param((ccrs.InterruptedGoodeHomolosine, dict(emphasis='land')), - id='InterruptedGoodeHomolosine'), - ccrs.LambertCylindrical, - ccrs.LambertZoneII, - pytest.param(ccrs.Spilhaus,marks=pytest.mark.skipif( - (proj_version < parse_version("9.6.0")), - reason="Requires PROJ >= 9.6.0" - )), - pytest.param((ccrs.Mercator, dict(min_latitude=-85, max_latitude=85)), - id='Mercator'), - ccrs.Miller, - ccrs.Mollweide, - ccrs.NorthPolarStereo, - ccrs.Orthographic, - pytest.param((ccrs.OSGB, dict(approx=True)), id='OSGB'), - ccrs.PlateCarree, - ccrs.Robinson, - pytest.param((ccrs.RotatedPole, - dict(pole_latitude=45, pole_longitude=180)), - id='RotatedPole'), - ccrs.Stereographic, - ccrs.SouthPolarStereo, - pytest.param((ccrs.TransverseMercator, dict(approx=True)), - id='TransverseMercator'), - pytest.param( - (ccrs.ObliqueMercator, dict(azimuth=0.)), id='ObliqueMercator_default' - ), - pytest.param( - (ccrs.ObliqueMercator, dict(azimuth=90., central_latitude=-22)), - id='ObliqueMercator_rotated', - ), -]) +@pytest.mark.parametrize( + 'proj', + [ + ccrs.Aitoff, + ccrs.EckertI, + ccrs.EckertII, + ccrs.EckertIII, + ccrs.EckertIV, + ccrs.EckertV, + ccrs.EckertVI, + ccrs.EqualEarth, + ccrs.Gnomonic, + ccrs.Hammer, + pytest.param( + (ccrs.InterruptedGoodeHomolosine, dict(emphasis='land')), + id='InterruptedGoodeHomolosine', + ), + ccrs.LambertCylindrical, + ccrs.LambertZoneII, + pytest.param( + ccrs.Spilhaus, + marks=pytest.mark.skipif( + (proj_version < parse_version('9.6.0')), reason='Requires PROJ >= 9.6.0' + ), + ), + pytest.param( + (ccrs.Mercator, dict(min_latitude=-85, max_latitude=85)), id='Mercator' + ), + ccrs.Miller, + ccrs.Mollweide, + ccrs.NorthPolarStereo, + ccrs.Orthographic, + pytest.param((ccrs.OSGB, dict(approx=True)), id='OSGB'), + ccrs.PlateCarree, + ccrs.Robinson, + pytest.param( + (ccrs.RotatedPole, dict(pole_latitude=45, pole_longitude=180)), + id='RotatedPole', + ), + ccrs.Stereographic, + ccrs.SouthPolarStereo, + pytest.param( + (ccrs.TransverseMercator, dict(approx=True)), id='TransverseMercator' + ), + pytest.param( + (ccrs.ObliqueMercator, dict(azimuth=0.0)), id='ObliqueMercator_default' + ), + pytest.param( + (ccrs.ObliqueMercator, dict(azimuth=90.0, central_latitude=-22)), + id='ObliqueMercator_rotated', + ), + ], +) @pytest.mark.mpl_image_compare(style='mpl20') def test_global_map(proj): if isinstance(proj, tuple): @@ -216,36 +227,36 @@ def test_global_map(proj): fig = plt.figure(figsize=(2, 2)) ax = fig.add_subplot(projection=proj) ax.set_global() - ax.coastlines(resolution="110m") + ax.coastlines(resolution='110m') ax.plot(-0.08, 51.53, 'o', transform=ccrs.PlateCarree()) - ax.plot([-0.08, 132], [51.53, 43.17], color='red', - transform=ccrs.PlateCarree()) - ax.plot([-0.08, 132], [51.53, 43.17], color='blue', - transform=ccrs.Geodetic()) + ax.plot([-0.08, 132], [51.53, 43.17], color='red', transform=ccrs.PlateCarree()) + ax.plot([-0.08, 132], [51.53, 43.17], color='blue', transform=ccrs.Geodetic()) return fig def test_cursor_values(): ax = plt.axes(projection=ccrs.NorthPolarStereo()) - x, y = np.array([-969100., -4457000.]) + x, y = np.array([-969100.0, -4457000.0]) r = ax.format_coord(x, y) - assert (r.encode('ascii', 'ignore') == - b'-9.691e+05, -4.457e+06 (50.716617N, 12.267069W)') + assert ( + r.encode('ascii', 'ignore') + == b'-9.691e+05, -4.457e+06 (50.716617N, 12.267069W)' + ) ax = plt.axes(projection=ccrs.PlateCarree()) - x, y = np.array([-181.5, 50.]) + x, y = np.array([-181.5, 50.0]) r = ax.format_coord(x, y) - assert (r.encode('ascii', 'ignore') == - b'-181.5, 50 (50.000000N, 178.500000E)') + assert r.encode('ascii', 'ignore') == b'-181.5, 50 (50.000000N, 178.500000E)' ax = plt.axes(projection=ccrs.Robinson()) x, y = np.array([16060595.2, 2363093.4]) r = ax.format_coord(x, y) - assert re.search(b'1.606e\\+07, 2.363e\\+06 ' - b'\\(22.09[0-9]{4}N, 173.70[0-9]{4}E\\)', - r.encode('ascii', 'ignore')) + assert re.search( + b'1.606e\\+07, 2.363e\\+06 \\(22.09[0-9]{4}N, 173.70[0-9]{4}E\\)', + r.encode('ascii', 'ignore'), + ) SKIP_PRE_MPL38 = pytest.mark.skipif(not _MPL_38, reason='mpl < 3.8') @@ -273,8 +284,7 @@ def _to_rgb(data, mesh_data_kind): if np.ma.is_masked(data): # Use data's mask as an alpha channel mask = np.ma.getmaskarray(data) - mask = np.broadcast_to( - mask[..., np.newaxis], new_data.shape).copy() + mask = np.broadcast_to(mask[..., np.newaxis], new_data.shape).copy() new_data = np.ma.array(new_data, mask=mask) return new_data @@ -284,8 +294,7 @@ def _to_rgb(data, mesh_data_kind): @PARAMETRIZE_PCOLORMESH_WRAP @pytest.mark.natural_earth -@pytest.mark.mpl_image_compare(filename='pcolormesh_global_wrap1.png', - tolerance=1.27) +@pytest.mark.mpl_image_compare(filename='pcolormesh_global_wrap1.png', tolerance=1.27) def test_pcolormesh_global_with_wrap1(mesh_data_kind): # make up some realistic data with bounds (such as data from the UM) nx, ny = 36, 18 @@ -329,14 +338,17 @@ def test_pcolormesh_get_array_with_mask(mesh_data_kind): ax = fig.add_subplot(2, 1, 1, projection=ccrs.PlateCarree()) c = ax.pcolormesh(xbnds, ybnds, data, transform=ccrs.PlateCarree()) - assert c._wrapped_collection_fix is not None, \ + assert c._wrapped_collection_fix is not None, ( 'No pcolormesh wrapping was done when it should have been.' + ) result = c.get_array() np.testing.assert_array_equal(np.ma.getmask(result), np.isnan(data)) np.testing.assert_array_equal( - data, result, - err_msg='Data supplied does not match data retrieved in wrapped case') + data, + result, + err_msg='Data supplied does not match data retrieved in wrapped case', + ) ax.coastlines() ax.set_global() # make sure everything is visible @@ -357,8 +369,9 @@ def test_pcolormesh_get_array_with_mask(mesh_data_kind): ax.coastlines() ax.set_global() # make sure everything is visible - assert getattr(c, "_wrapped_collection_fix", None) is None, \ + assert getattr(c, '_wrapped_collection_fix', None) is None, ( 'pcolormesh wrapping was done when it should not have been.' + ) result = c.get_array() @@ -368,14 +381,15 @@ def test_pcolormesh_get_array_with_mask(mesh_data_kind): np.testing.assert_array_equal(np.ma.getmask(result), np.isnan(expected)) np.testing.assert_array_equal( - expected, result, - 'Data supplied does not match data retrieved in unwrapped case') + expected, + result, + 'Data supplied does not match data retrieved in unwrapped case', + ) @PARAMETRIZE_PCOLORMESH_WRAP @pytest.mark.natural_earth -@pytest.mark.mpl_image_compare(filename='pcolormesh_global_wrap2.png', - tolerance=1.87) +@pytest.mark.mpl_image_compare(filename='pcolormesh_global_wrap2.png', tolerance=1.87) def test_pcolormesh_global_with_wrap2(mesh_data_kind): # make up some realistic data with bounds (such as data from the UM) nx, ny = 36, 18 @@ -408,8 +422,7 @@ def test_pcolormesh_global_with_wrap2(mesh_data_kind): @PARAMETRIZE_PCOLORMESH_WRAP @pytest.mark.natural_earth -@pytest.mark.mpl_image_compare(filename='pcolormesh_global_wrap3.png', - tolerance=1.42) +@pytest.mark.mpl_image_compare(filename='pcolormesh_global_wrap3.png', tolerance=1.42) def test_pcolormesh_global_with_wrap3(mesh_data_kind): nx, ny = 33, 17 xbnds = np.linspace(-1.875, 358.125, nx, endpoint=True) @@ -431,10 +444,10 @@ def test_pcolormesh_global_with_wrap3(mesh_data_kind): data = _to_rgb(data, mesh_data_kind) ax = fig.add_subplot(3, 1, 1, projection=ccrs.PlateCarree(-45)) - c = ax.pcolormesh(xbnds, ybnds, data, transform=ccrs.PlateCarree(), - snap=False) - assert c._wrapped_collection_fix is not None, \ + c = ax.pcolormesh(xbnds, ybnds, data, transform=ccrs.PlateCarree(), snap=False) + assert c._wrapped_collection_fix is not None, ( 'No pcolormesh wrapping was done when it should have been.' + ) ax.coastlines() ax.set_global() # make sure everything is visible @@ -454,8 +467,7 @@ def test_pcolormesh_global_with_wrap3(mesh_data_kind): @PARAMETRIZE_PCOLORMESH_WRAP @pytest.mark.natural_earth -@pytest.mark.mpl_image_compare(filename='pcolormesh_global_wrap3.png', - tolerance=1.42) +@pytest.mark.mpl_image_compare(filename='pcolormesh_global_wrap3.png', tolerance=1.42) def test_pcolormesh_set_array_with_mask(mesh_data_kind): """Testing that set_array works with masked arrays properly.""" nx, ny = 33, 17 @@ -484,19 +496,22 @@ def test_pcolormesh_set_array_with_mask(mesh_data_kind): bad_data_mask = _to_rgb(bad_data_mask, mesh_data_kind) ax = fig.add_subplot(3, 1, 1, projection=ccrs.PlateCarree(-45)) - c = ax.pcolormesh(xbnds, ybnds, bad_data, - norm=norm, transform=ccrs.PlateCarree(), snap=False) + c = ax.pcolormesh( + xbnds, ybnds, bad_data, norm=norm, transform=ccrs.PlateCarree(), snap=False + ) c.set_array(data) - assert c._wrapped_collection_fix is not None, \ + assert c._wrapped_collection_fix is not None, ( 'No pcolormesh wrapping was done when it should have been.' + ) ax.coastlines() ax.set_global() # make sure everything is visible ax = fig.add_subplot(3, 1, 2, projection=ccrs.PlateCarree(-1.87499952)) - c2 = ax.pcolormesh(xbnds, ybnds, bad_data_mask, - norm=norm, transform=ccrs.PlateCarree(), snap=False) + c2 = ax.pcolormesh( + xbnds, ybnds, bad_data_mask, norm=norm, transform=ccrs.PlateCarree(), snap=False + ) if mesh_data_kind == 'standard': c2.set_array(data.ravel()) else: @@ -534,8 +549,7 @@ def test_pcolormesh_set_array_nowrap(): # For backwards compatibility, check we can set a 1D array data = rng.random((nx - 1) * (ny - 1)) mesh.set_array(data) - np.testing.assert_array_equal( - mesh.get_array(), data.reshape(ny - 1, nx - 1)) + np.testing.assert_array_equal(mesh.get_array(), data.reshape(ny - 1, nx - 1)) # Check that we can set a 2D array even if previous was flat data = rng.random((ny - 1, nx - 1)) @@ -544,8 +558,7 @@ def test_pcolormesh_set_array_nowrap(): @pytest.mark.natural_earth -@pytest.mark.mpl_image_compare(filename='pcolormesh_global_wrap3.png', - tolerance=1.42) +@pytest.mark.mpl_image_compare(filename='pcolormesh_global_wrap3.png', tolerance=1.42) def test_pcolormesh_set_clim_with_mask(): """Testing that set_clim works with masked arrays properly.""" nx, ny = 33, 17 @@ -568,10 +581,17 @@ def test_pcolormesh_set_clim_with_mask(): fig = plt.figure() ax = fig.add_subplot(3, 1, 1, projection=ccrs.PlateCarree(-45)) - c = ax.pcolormesh(xbnds, ybnds, data, transform=ccrs.PlateCarree(), - norm=bad_initial_norm, snap=False) - assert c._wrapped_collection_fix is not None, \ + c = ax.pcolormesh( + xbnds, + ybnds, + data, + transform=ccrs.PlateCarree(), + norm=bad_initial_norm, + snap=False, + ) + assert c._wrapped_collection_fix is not None, ( 'No pcolormesh wrapping was done when it should have been.' + ) ax.coastlines() ax.set_global() # make sure everything is visible @@ -593,8 +613,9 @@ def test_pcolormesh_set_clim_with_mask(): @pytest.mark.natural_earth -@pytest.mark.mpl_image_compare(filename='pcolormesh_limited_area_wrap.png', - tolerance=1.83) +@pytest.mark.mpl_image_compare( + filename='pcolormesh_limited_area_wrap.png', tolerance=1.83 +) def test_pcolormesh_limited_area_wrap(): # make up some realistic data with bounds (such as data from the UM's North # Atlantic Europe model) @@ -602,7 +623,7 @@ def test_pcolormesh_limited_area_wrap(): xbnds = np.linspace(311.91998291, 391.11999512, nx, endpoint=True) ybnds = np.linspace(-23.59000015, 24.81000137, ny, endpoint=True) x, y = np.meshgrid(xbnds, ybnds) - data = ((np.sin(np.deg2rad(x))) / 10. + np.exp(np.cos(np.deg2rad(y)))) + data = (np.sin(np.deg2rad(x))) / 10.0 + np.exp(np.cos(np.deg2rad(y))) data = data[:-1, :-1] rp = ccrs.RotatedPole(pole_longitude=177.5, pole_latitude=37.5) @@ -610,13 +631,11 @@ def test_pcolormesh_limited_area_wrap(): fig = plt.figure(figsize=(10, 6)) ax = fig.add_subplot(2, 2, 1, projection=ccrs.PlateCarree()) - ax.pcolormesh(xbnds, ybnds, data, transform=rp, cmap='Spectral', - snap=False) + ax.pcolormesh(xbnds, ybnds, data, transform=rp, cmap='Spectral', snap=False) ax.coastlines() ax = fig.add_subplot(2, 2, 2, projection=ccrs.PlateCarree(180)) - ax.pcolormesh(xbnds, ybnds, data, transform=rp, cmap='Spectral', - snap=False) + ax.pcolormesh(xbnds, ybnds, data, transform=rp, cmap='Spectral', snap=False) ax.coastlines() ax.set_global() @@ -629,8 +648,7 @@ def test_pcolormesh_limited_area_wrap(): ax.set_extent([-70, 0, 0, 80]) ax = fig.add_subplot(2, 2, 4, projection=rp) - ax.pcolormesh(xbnds, ybnds, data, transform=rp, cmap='Spectral', - snap=False) + ax.pcolormesh(xbnds, ybnds, data, transform=rp, cmap='Spectral', snap=False) ax.coastlines() return fig @@ -646,7 +664,7 @@ def test_pcolormesh_single_column_wrap(): xbnds = np.array([360.9485619, 364.71999105]) ybnds = np.linspace(-23.59000015, 24.81000137, ny, endpoint=True) x, y = np.meshgrid(xbnds, ybnds) - data = ((np.sin(np.deg2rad(x))) / 10. + np.exp(np.cos(np.deg2rad(y)))) + data = (np.sin(np.deg2rad(x))) / 10.0 + np.exp(np.cos(np.deg2rad(y))) data = data[:-1, :-1] rp = ccrs.RotatedPole(pole_longitude=177.5, pole_latitude=37.5) @@ -655,8 +673,7 @@ def test_pcolormesh_single_column_wrap(): ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree(180)) # TODO: Remove snap when updating this image - ax.pcolormesh(xbnds, ybnds, data, transform=rp, cmap='Spectral', - snap=False) + ax.pcolormesh(xbnds, ybnds, data, transform=rp, cmap='Spectral', snap=False) ax.coastlines() ax.set_global() @@ -667,7 +684,7 @@ def test_pcolormesh_wrap_gouraud_shading_failing_mask_creation(): x_range = np.linspace(-180, 180, 50) y_range = np.linspace(90, -90, 50) x, y = np.meshgrid(x_range, y_range) - data = ((np.sin(np.deg2rad(x))) / 10. + np.exp(np.cos(np.deg2rad(y)))) + data = (np.sin(np.deg2rad(x))) / 10.0 + np.exp(np.cos(np.deg2rad(y))) fig = plt.figure(figsize=(10, 6)) ax = fig.add_subplot(1, 1, 1, projection=ccrs.Mercator()) @@ -685,7 +702,7 @@ def test_pcolormesh_diagonal_wrap(): mesh = ax.pcolormesh(xs, ys, zs) # And that the wrapped_collection is added - assert hasattr(mesh, "_wrapped_collection_fix") + assert hasattr(mesh, '_wrapped_collection_fix') def test_pcolormesh_nan_wrap(): @@ -697,7 +714,7 @@ def test_pcolormesh_nan_wrap(): ax = plt.axes(projection=ccrs.PlateCarree()) mesh = ax.pcolormesh(xs, ys, data) - pcolor = getattr(mesh, "_wrapped_collection_fix") + pcolor = getattr(mesh, '_wrapped_collection_fix') if not _MPL_38: assert len(pcolor.get_paths()) == 2 else: @@ -716,7 +733,7 @@ def test_pcolormesh_goode_wrap(): x = np.linspace(0, 360, 73) y = np.linspace(-87.5, 87.5, 36) X, Y = np.meshgrid(*[np.deg2rad(c) for c in (x, y)]) - Z = np.cos(Y) + 0.375 * np.sin(2. * X) + Z = np.cos(Y) + 0.375 * np.sin(2.0 * X) Z = Z[:-1, :-1] ax = plt.axes(projection=ccrs.InterruptedGoodeHomolosine(emphasis='land')) ax.coastlines() @@ -731,7 +748,7 @@ def test_pcolormesh_mercator_wrap(): x = np.linspace(0, 360, 73) y = np.linspace(-87.5, 87.5, 36) X, Y = np.meshgrid(*[np.deg2rad(c) for c in (x, y)]) - Z = np.cos(Y) + 0.375 * np.sin(2. * X) + Z = np.cos(Y) + 0.375 * np.sin(2.0 * X) Z = Z[:-1, :-1] ax = plt.axes(projection=ccrs.Mercator()) ax.coastlines() @@ -746,7 +763,7 @@ def test_pcolormesh_wrap_set_array(mesh_data_kind): x = np.linspace(0, 360, 73) y = np.linspace(-87.5, 87.5, 36) X, Y = np.meshgrid(*[np.deg2rad(c) for c in (x, y)]) - Z = np.cos(Y) + 0.375 * np.sin(2. * X) + Z = np.cos(Y) + 0.375 * np.sin(2.0 * X) Z = Z[:-1, :-1] Z = _to_rgb(Z, mesh_data_kind) @@ -755,21 +772,25 @@ def test_pcolormesh_wrap_set_array(mesh_data_kind): norm = plt.Normalize(np.min(Z), np.max(Z)) ax.coastlines() # Start off with bad data - coll = ax.pcolormesh(x, y, np.ones(Z.shape), norm=norm, - transform=ccrs.PlateCarree(), snap=False) + coll = ax.pcolormesh( + x, y, np.ones(Z.shape), norm=norm, transform=ccrs.PlateCarree(), snap=False + ) # Now update the plot with the set_array method coll.set_array(Z) return ax.figure -@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') -]) +@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 @@ -794,7 +815,8 @@ def test__wrap_args_default_shading(): ax = plt.subplot(projection=ccrs.Orthographic()) args_ref, kwargs_ref = ax._wrap_args(x, y, z, transform=ccrs.PlateCarree()) args_test, kwargs_test = ax._wrap_args( - x, y, z, transform=ccrs.PlateCarree(), shading=None) + x, y, z, transform=ccrs.PlateCarree(), shading=None + ) for array_ref, array_test in zip(args_ref, args_test): np.testing.assert_allclose(array_ref, array_test) @@ -809,14 +831,14 @@ def test_quiver_plate_carree(): y = np.arange(30, 72.5, 2.5) x2d, y2d = np.meshgrid(x, y) u = np.cos(np.deg2rad(y2d)) - v = np.cos(2. * np.deg2rad(x2d)) + v = np.cos(2.0 * np.deg2rad(x2d)) mag = np.hypot(u, v) plot_extent = [-60, 40, 30, 70] fig = plt.figure(figsize=(6, 6)) # plot on native projection ax = fig.add_subplot(2, 1, 1, projection=ccrs.PlateCarree()) ax.set_extent(plot_extent, crs=ccrs.PlateCarree()) - ax.coastlines(resolution="110m") + ax.coastlines(resolution='110m') ax.quiver(x, y, u, v, mag) # plot on a different projection ax = fig.add_subplot(2, 1, 2, projection=ccrs.NorthPolarStereo()) @@ -834,7 +856,7 @@ def test_quiver_rotated_pole(): y = np.linspace(-23.59000015, 24.81000137, ny, endpoint=True) x2d, y2d = np.meshgrid(x, y) u = np.cos(np.deg2rad(y2d)) - v = -2. * np.cos(2. * np.deg2rad(y2d)) * np.sin(np.deg2rad(x2d)) + v = -2.0 * np.cos(2.0 * np.deg2rad(y2d)) * np.sin(np.deg2rad(x2d)) mag = np.hypot(u, v) rp = ccrs.RotatedPole(pole_longitude=177.5, pole_latitude=37.5) plot_extent = [x[0], x[-1], y[0], y[-1]] @@ -860,28 +882,26 @@ def test_quiver_regrid(): y = np.arange(30, 72.5, 2.5) x2d, y2d = np.meshgrid(x, y) u = np.cos(np.deg2rad(y2d)) - v = np.cos(2. * np.deg2rad(x2d)) + v = np.cos(2.0 * np.deg2rad(x2d)) mag = np.hypot(u, v) plot_extent = [-60, 40, 30, 70] fig = plt.figure(figsize=(6, 3)) ax = fig.add_subplot(projection=ccrs.NorthPolarStereo()) ax.set_extent(plot_extent, crs=ccrs.PlateCarree()) ax.coastlines() - ax.quiver(x, y, u, v, mag, transform=ccrs.PlateCarree(), - regrid_shape=30) + ax.quiver(x, y, u, v, mag, transform=ccrs.PlateCarree(), regrid_shape=30) return fig @requires_scipy @pytest.mark.natural_earth -@pytest.mark.mpl_image_compare(filename='quiver_regrid_with_extent.png', - tolerance=0.54) +@pytest.mark.mpl_image_compare(filename='quiver_regrid_with_extent.png', tolerance=0.54) def test_quiver_regrid_with_extent(): x = np.arange(-60, 42.5, 2.5) y = np.arange(30, 72.5, 2.5) x2d, y2d = np.meshgrid(x, y) u = np.cos(np.deg2rad(y2d)) - v = np.cos(2. * np.deg2rad(x2d)) + v = np.cos(2.0 * np.deg2rad(x2d)) mag = np.hypot(u, v) plot_extent = [-60, 40, 30, 70] target_extent = [-3e6, 2e6, -6e6, -2.5e6] @@ -889,8 +909,16 @@ def test_quiver_regrid_with_extent(): ax = fig.add_subplot(projection=ccrs.NorthPolarStereo()) ax.set_extent(plot_extent, crs=ccrs.PlateCarree()) ax.coastlines() - ax.quiver(x, y, u, v, mag, transform=ccrs.PlateCarree(), - regrid_shape=10, target_extent=target_extent) + ax.quiver( + x, + y, + u, + v, + mag, + transform=ccrs.PlateCarree(), + regrid_shape=10, + target_extent=target_extent, + ) return fig @@ -902,19 +930,19 @@ def test_barbs(): y = np.arange(30, 75, 5) x2d, y2d = np.meshgrid(x, y) u = 40 * np.cos(np.deg2rad(y2d)) - v = 40 * np.cos(2. * np.deg2rad(x2d)) + v = 40 * np.cos(2.0 * np.deg2rad(x2d)) plot_extent = [-60, 40, 30, 70] fig = plt.figure(figsize=(6, 6)) # plot on native projection ax = fig.add_subplot(2, 1, 1, projection=ccrs.PlateCarree()) ax.set_extent(plot_extent, crs=ccrs.PlateCarree()) - ax.coastlines(resolution="110m") - ax.barbs(x, y, u, v, length=4, linewidth=.25) + ax.coastlines(resolution='110m') + ax.barbs(x, y, u, v, length=4, linewidth=0.25) # plot on a different projection ax = fig.add_subplot(2, 1, 2, projection=ccrs.NorthPolarStereo()) ax.set_extent(plot_extent, crs=ccrs.PlateCarree()) - ax.coastlines(resolution="110m") - ax.barbs(x, y, u, v, transform=ccrs.PlateCarree(), length=4, linewidth=.25) + ax.coastlines(resolution='110m') + ax.barbs(x, y, u, v, transform=ccrs.PlateCarree(), length=4, linewidth=0.25) return fig @@ -926,28 +954,36 @@ def test_barbs_regrid(): y = np.arange(30, 72.5, 2.5) x2d, y2d = np.meshgrid(x, y) u = 40 * np.cos(np.deg2rad(y2d)) - v = 40 * np.cos(2. * np.deg2rad(x2d)) + v = 40 * np.cos(2.0 * np.deg2rad(x2d)) mag = np.hypot(u, v) plot_extent = [-60, 40, 30, 70] fig = plt.figure(figsize=(6, 3)) ax = fig.add_subplot(projection=ccrs.NorthPolarStereo()) ax.set_extent(plot_extent, crs=ccrs.PlateCarree()) ax.coastlines() - ax.barbs(x, y, u, v, mag, transform=ccrs.PlateCarree(), - length=4, linewidth=.4, regrid_shape=20) + ax.barbs( + x, + y, + u, + v, + mag, + transform=ccrs.PlateCarree(), + length=4, + linewidth=0.4, + regrid_shape=20, + ) return fig @requires_scipy @pytest.mark.natural_earth -@pytest.mark.mpl_image_compare(filename='barbs_regrid_with_extent.png', - tolerance=0.54) +@pytest.mark.mpl_image_compare(filename='barbs_regrid_with_extent.png', tolerance=0.54) def test_barbs_regrid_with_extent(): x = np.arange(-60, 42.5, 2.5) y = np.arange(30, 72.5, 2.5) x2d, y2d = np.meshgrid(x, y) u = 40 * np.cos(np.deg2rad(y2d)) - v = 40 * np.cos(2. * np.deg2rad(x2d)) + v = 40 * np.cos(2.0 * np.deg2rad(x2d)) mag = np.hypot(u, v) plot_extent = [-60, 40, 30, 70] target_extent = [-3e6, 2e6, -6e6, -2.5e6] @@ -955,43 +991,54 @@ def test_barbs_regrid_with_extent(): ax = fig.add_subplot(projection=ccrs.NorthPolarStereo()) ax.set_extent(plot_extent, crs=ccrs.PlateCarree()) ax.coastlines() - ax.barbs(x, y, u, v, mag, transform=ccrs.PlateCarree(), - length=4, linewidth=.25, regrid_shape=10, - target_extent=target_extent) + ax.barbs( + x, + y, + u, + v, + mag, + transform=ccrs.PlateCarree(), + length=4, + linewidth=0.25, + regrid_shape=10, + target_extent=target_extent, + ) return fig @pytest.mark.natural_earth @pytest.mark.mpl_image_compare(filename='barbs_1d.png') def test_barbs_1d(): - x = np.array([20., 30., -17., 15.]) - y = np.array([-1., 35., 11., 40.]) - u = np.array([23., -18., 2., -11.]) - v = np.array([5., -4., 19., 11.]) + x = np.array([20.0, 30.0, -17.0, 15.0]) + y = np.array([-1.0, 35.0, 11.0, 40.0]) + u = np.array([23.0, -18.0, 2.0, -11.0]) + v = np.array([5.0, -4.0, 19.0, 11.0]) plot_extent = [-21, 40, -5, 45] fig = plt.figure(figsize=(6, 5)) ax = fig.add_subplot(projection=ccrs.PlateCarree()) ax.set_extent(plot_extent, crs=ccrs.PlateCarree()) - ax.coastlines(resolution="110m") - ax.barbs(x, y, u, v, transform=ccrs.PlateCarree(), - length=8, linewidth=1, color='#7f7f7f') + ax.coastlines(resolution='110m') + ax.barbs( + x, y, u, v, transform=ccrs.PlateCarree(), length=8, linewidth=1, color='#7f7f7f' + ) return fig @pytest.mark.natural_earth @pytest.mark.mpl_image_compare(filename='barbs_1d_transformed.png') def test_barbs_1d_transformed(): - x = np.array([20., 30., -17., 15.]) - y = np.array([-1., 35., 11., 40.]) - u = np.array([23., -18., 2., -11.]) - v = np.array([5., -4., 19., 11.]) + x = np.array([20.0, 30.0, -17.0, 15.0]) + y = np.array([-1.0, 35.0, 11.0, 40.0]) + u = np.array([23.0, -18.0, 2.0, -11.0]) + v = np.array([5.0, -4.0, 19.0, 11.0]) plot_extent = [-20, 31, -5, 45] fig = plt.figure(figsize=(6, 5)) ax = fig.add_subplot(projection=ccrs.NorthPolarStereo()) ax.set_extent(plot_extent, crs=ccrs.PlateCarree()) ax.coastlines() - ax.barbs(x, y, u, v, transform=ccrs.PlateCarree(), - length=8, linewidth=1, color='#7f7f7f') + ax.barbs( + x, y, u, v, transform=ccrs.PlateCarree(), length=8, linewidth=1, color='#7f7f7f' + ) return fig @@ -1003,22 +1050,30 @@ def test_streamplot(): y = np.arange(30, 72.5, 2.5) x2d, y2d = np.meshgrid(x, y) u = np.cos(np.deg2rad(y2d)) - v = np.cos(2. * np.deg2rad(x2d)) + v = np.cos(2.0 * np.deg2rad(x2d)) mag = np.hypot(u, v) plot_extent = [-60, 40, 30, 70] fig = plt.figure(figsize=(6, 3)) ax = fig.add_subplot(projection=ccrs.NorthPolarStereo()) ax.set_extent(plot_extent, crs=ccrs.PlateCarree()) ax.coastlines() - ax.streamplot(x, y, u, v, transform=ccrs.PlateCarree(), - density=(1.5, 2), color=mag, linewidth=2 * mag) + ax.streamplot( + x, + y, + u, + v, + transform=ccrs.PlateCarree(), + density=(1.5, 2), + color=mag, + linewidth=2 * mag, + ) return fig @pytest.mark.natural_earth @pytest.mark.mpl_image_compare() def test_annotate(): - """ test a variety of annotate options on multiple projections + """test a variety of annotate options on multiple projections Annotate defaults to coords passed as if they're in map projection space. `transform` or `xycoords` & `textcoords` control the marker and text offset @@ -1035,21 +1090,20 @@ def test_annotate(): ax = fig.add_subplot(1, 1, 1, projection=map_projection) ax.set_global() ax.coastlines() - arrowprops = {'facecolor': 'red', - 'arrowstyle': "-|>", - 'connectionstyle': "arc3,rad=-0.2", - } + arrowprops = { + 'facecolor': 'red', + 'arrowstyle': '-|>', + 'connectionstyle': 'arc3,rad=-0.2', + } platecarree = ccrs.PlateCarree() mpltransform = platecarree._as_mpl_transform(ax) # Add annotation with xycoords as mpltransform as suggested here # https://stackoverflow.com/questions/25416600/why-the-annotate-worked-unexpected-here-in-cartopy/25421922#25421922 - ax.annotate('mpl xycoords', (-45, 43), xycoords=mpltransform, - size=5) + ax.annotate('mpl xycoords', (-45, 43), xycoords=mpltransform, size=5) # Add annotation with xycoords as projection - ax.annotate('crs xycoords', (-75, 13), xycoords=platecarree, - size=5) + ax.annotate('crs xycoords', (-75, 13), xycoords=platecarree, size=5) # set up coordinates in map projection space map_coords = map_projection.transform_point(-175, -35, platecarree) @@ -1058,26 +1112,36 @@ def test_annotate(): # data in map projection using default transform, with # text positioned in platecarree transform - ax.annotate('mixed crs transforms', map_coords, xycoords='data', - xytext=(-175, -55), - textcoords=platecarree, - size=5, - arrowprops=arrowprops, - ) + ax.annotate( + 'mixed crs transforms', + map_coords, + xycoords='data', + xytext=(-175, -55), + textcoords=platecarree, + size=5, + arrowprops=arrowprops, + ) # Add annotation with point and text via transform - ax.annotate('crs transform', (-75, -20), xytext=(0, -55), - transform=platecarree, - arrowprops=arrowprops, - ) + ax.annotate( + 'crs transform', + (-75, -20), + xytext=(0, -55), + transform=platecarree, + arrowprops=arrowprops, + ) # Add annotation with point via transform and text non transformed - ax.annotate('offset textcoords', (-149.8, 61.22), transform=platecarree, - xytext=(-35, 10), textcoords='offset points', - size=5, - ha='right', - arrowprops=arrowprops, - ) + ax.annotate( + 'offset textcoords', + (-149.8, 61.22), + transform=platecarree, + xytext=(-35, 10), + textcoords='offset points', + size=5, + ha='right', + arrowprops=arrowprops, + ) return fig diff --git a/lib/cartopy/tests/mpl/test_path.py b/lib/cartopy/tests/mpl/test_path.py index 9837ee546..1f20fa964 100644 --- a/lib/cartopy/tests/mpl/test_path.py +++ b/lib/cartopy/tests/mpl/test_path.py @@ -16,17 +16,29 @@ class Test_path_to_shapely: def test_empty_polygon(self, use_legacy_path_to_geos): p = Path( [ - [0, 0], [0, 0], [0, 0], [0, 0], - [1, 2], [1, 2], [1, 2], [1, 2], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [1, 2], + [1, 2], + [1, 2], + [1, 2], # The vertex for CLOSEPOLY should be ignored. - [2, 3], [2, 3], [2, 3], [42, 42], + [2, 3], + [2, 3], + [2, 3], + [42, 42], # Very close points should be treated the same. - [193.75, -14.166664123535156], [193.75, -14.166664123535158], - [193.75, -14.166664123535156], [193.75, -14.166664123535156], + [193.75, -14.166664123535156], + [193.75, -14.166664123535158], + [193.75, -14.166664123535156], + [193.75, -14.166664123535156], ], - codes=[1, 2, 2, 79] * 4) + codes=[1, 2, 2, 79] * 4, + ) if use_legacy_path_to_geos: - with pytest.warns(DeprecationWarning, match="path_to_geos is deprecated"): + with pytest.warns(DeprecationWarning, match='path_to_geos is deprecated'): geoms = cpatch.path_to_geos(p) assert [type(geom) for geom in geoms] == [sgeom.Point] * 4 assert len(geoms) == 4 @@ -36,10 +48,9 @@ def test_empty_polygon(self, use_legacy_path_to_geos): assert len(geoms.geoms) == 4 def test_non_polygon_loop(self, use_legacy_path_to_geos): - p = Path([[0, 10], [170, 20], [-170, 30], [0, 10]], - codes=[1, 2, 2, 2]) + p = Path([[0, 10], [170, 20], [-170, 30], [0, 10]], codes=[1, 2, 2, 2]) if use_legacy_path_to_geos: - with pytest.warns(DeprecationWarning, match="path_to_geos is deprecated"): + with pytest.warns(DeprecationWarning, match='path_to_geos is deprecated'): geoms = cpatch.path_to_geos(p) assert [type(geom) for geom in geoms] == [sgeom.MultiLineString] @@ -50,12 +61,28 @@ def test_non_polygon_loop(self, use_legacy_path_to_geos): def test_polygon_with_interior_and_singularity(self, use_legacy_path_to_geos): # A geometry with two interiors, one a single point. - p = Path([[0, -90], [200, -40], [200, 40], [0, 40], [0, -90], - [126, 26], [126, 26], [126, 26], [126, 26], [126, 26], - [114, 5], [103, 8], [126, 12], [126, 0], [114, 5]], - codes=[1, 2, 2, 2, 79, 1, 2, 2, 2, 79, 1, 2, 2, 2, 79]) + p = Path( + [ + [0, -90], + [200, -40], + [200, 40], + [0, 40], + [0, -90], + [126, 26], + [126, 26], + [126, 26], + [126, 26], + [126, 26], + [114, 5], + [103, 8], + [126, 12], + [126, 0], + [114, 5], + ], + codes=[1, 2, 2, 2, 79, 1, 2, 2, 2, 79, 1, 2, 2, 2, 79], + ) if use_legacy_path_to_geos: - with pytest.warns(DeprecationWarning, match="path_to_geos is deprecated"): + with pytest.warns(DeprecationWarning, match='path_to_geos is deprecated'): geoms = cpatch.path_to_geos(p) assert [type(geom) for geom in geoms] == [sgeom.Polygon, sgeom.Point] @@ -68,16 +95,30 @@ def test_polygon_with_interior_and_singularity(self, use_legacy_path_to_geos): def test_nested_polygons(self, use_legacy_path_to_geos): # A geometry with three nested squares. - vertices = [[0, 0], [0, 10], [10, 10], [10, 0], [0, 0], - [2, 2], [2, 8], [8, 8], [8, 2], [2, 2], - [4, 4], [4, 6], [6, 6], [6, 4], [4, 4]] + vertices = [ + [0, 0], + [0, 10], + [10, 10], + [10, 0], + [0, 0], + [2, 2], + [2, 8], + [8, 8], + [8, 2], + [2, 2], + [4, 4], + [4, 6], + [6, 6], + [6, 4], + [4, 4], + ] codes = [1, 2, 2, 2, 79, 1, 2, 2, 2, 79, 1, 2, 2, 2, 79] p = Path(vertices, codes=codes) # The first square makes the first geometry with the second square as # its interior. The third square is its own geometry with no interior. if use_legacy_path_to_geos: - with pytest.warns(DeprecationWarning, match="path_to_geos is deprecated"): + with pytest.warns(DeprecationWarning, match='path_to_geos is deprecated'): geoms = cpatch.path_to_geos(p) assert len(geoms) == 2 diff --git a/lib/cartopy/tests/mpl/test_pseudo_color.py b/lib/cartopy/tests/mpl/test_pseudo_color.py index 3b747af50..af3624c86 100644 --- a/lib/cartopy/tests/mpl/test_pseudo_color.py +++ b/lib/cartopy/tests/mpl/test_pseudo_color.py @@ -20,8 +20,7 @@ def test_pcolormesh_partially_masked(): with mock.patch('cartopy.mpl.geoaxes.GeoAxes.pcolor') as pcolor: ax = plt.axes(projection=ccrs.PlateCarree()) ax.pcolormesh(np.linspace(0, 360, 30), np.linspace(-90, 90, 40), data) - assert pcolor.call_count == 1, ("pcolor should have been called " - "exactly once.") + assert pcolor.call_count == 1, 'pcolor should have been called exactly once.' def test_pcolormesh_invisible(): @@ -30,10 +29,13 @@ def test_pcolormesh_invisible(): # Check that a fully invisible mesh doesn't fail. with mock.patch('cartopy.mpl.geoaxes.GeoAxes.pcolor') as pcolor: ax = plt.axes(projection=ccrs.Orthographic()) - ax.pcolormesh(np.linspace(-75, 75, 3), np.linspace(105, 255, 3), data, - transform=ccrs.PlateCarree()) - assert pcolor.call_count == 0, ("pcolor shouldn't have been called, " - "but was.") + ax.pcolormesh( + np.linspace(-75, 75, 3), + np.linspace(105, 255, 3), + data, + transform=ccrs.PlateCarree(), + ) + assert pcolor.call_count == 0, "pcolor shouldn't have been called, but was." def test_savefig_tight(): @@ -63,22 +65,16 @@ def test_pcolormesh_arg_interpolation(): z = np.zeros(xs.shape) ax = plt.subplot(2, 1, 1, projection=ccrs.PlateCarree()) - coll = ax.pcolormesh(xs, ys, z, shading='auto', - transform=ccrs.PlateCarree()) + coll = ax.pcolormesh(xs, ys, z, shading='auto', transform=ccrs.PlateCarree()) # Compare the output coordinates of the generated mesh - expected = np.array([[[358, -20], - [360, -20], - [2, -20], - [4, -20]], - [[358, 0], - [360, 0], - [2, 0], - [4, 0]], - [[358, 20], - [360, 20], - [2, 20], - [4, 20]]]) + expected = np.array( + [ + [[358, -20], [360, -20], [2, -20], [4, -20]], + [[358, 0], [360, 0], [2, 0], [4, 0]], + [[358, 20], [360, 20], [2, 20], [4, 20]], + ] + ) np.testing.assert_array_almost_equal(expected, coll._coordinates) @@ -92,8 +88,7 @@ def test_pcolormesh_datalim(): z = np.zeros(xs.shape) ax = plt.subplot(3, 1, 1, projection=ccrs.PlateCarree()) - coll = ax.pcolormesh(xs, ys, z, shading='auto', - transform=ccrs.PlateCarree()) + coll = ax.pcolormesh(xs, ys, z, shading='auto', transform=ccrs.PlateCarree()) coll_bbox = coll.get_datalim(ax.transData) np.testing.assert_array_equal(coll_bbox, [[-2, -20], [4, 20]]) @@ -104,8 +99,7 @@ def test_pcolormesh_datalim(): xs, ys = np.meshgrid(x, y) ax = plt.subplot(3, 1, 2, projection=ccrs.PlateCarree()) - coll = ax.pcolormesh(xs, ys, z, shading='auto', - transform=ccrs.PlateCarree()) + coll = ax.pcolormesh(xs, ys, z, shading='auto', transform=ccrs.PlateCarree()) coll_bbox = coll.get_datalim(ax.transData) np.testing.assert_array_equal(coll_bbox, [[-120, -20], [120, 20]]) @@ -116,10 +110,8 @@ def test_pcolormesh_datalim(): xs, ys = np.meshgrid(x, y) ax = plt.subplot(3, 1, 3, projection=ccrs.Orthographic()) - coll = ax.pcolormesh(xs, ys, z, shading='auto', - transform=ccrs.PlateCarree()) + coll = ax.pcolormesh(xs, ys, z, shading='auto', transform=ccrs.PlateCarree()) coll_bbox = coll.get_datalim(ax.transData) - expected = [[-1650783.327873, -2181451.330891], - [1650783.327873, 2181451.330891]] + expected = [[-1650783.327873, -2181451.330891], [1650783.327873, 2181451.330891]] np.testing.assert_array_almost_equal(coll_bbox, expected) diff --git a/lib/cartopy/tests/mpl/test_quiver.py b/lib/cartopy/tests/mpl/test_quiver.py index 4766fc3a4..a36f2d3b4 100644 --- a/lib/cartopy/tests/mpl/test_quiver.py +++ b/lib/cartopy/tests/mpl/test_quiver.py @@ -21,7 +21,7 @@ def setup_method(self): self.y = np.linspace(30, 72.5, 7) self.x2d, self.y2d = np.meshgrid(self.x, self.y) self.u = np.cos(np.deg2rad(self.y2d)) - self.v = np.cos(2. * np.deg2rad(self.x2d)) + self.v = np.cos(2.0 * np.deg2rad(self.x2d)) self.rp = ccrs.RotatedPole(pole_longitude=177.5, pole_latitude=37.5) self.pc = ccrs.PlateCarree() self.fig = plt.figure() @@ -29,14 +29,19 @@ def setup_method(self): def test_quiver_transform_xyuv_1d(self): with mock.patch('matplotlib.axes.Axes.quiver') as patch: - self.ax.quiver(self.x2d.ravel(), self.y2d.ravel(), - self.u.ravel(), self.v.ravel(), transform=self.rp) + self.ax.quiver( + self.x2d.ravel(), + self.y2d.ravel(), + self.u.ravel(), + self.v.ravel(), + transform=self.rp, + ) args, kwargs = patch.call_args assert len(args) == 4 assert sorted(kwargs.keys()) == ['transform'] shapes = [arg.shape for arg in args] # Assert that all the shapes have been broadcast. - assert shapes == [(70, )] * 4 + assert shapes == [(70,)] * 4 def test_quiver_transform_xy_1d_uv_2d(self): with mock.patch('matplotlib.axes.Axes.quiver') as patch: @@ -50,10 +55,12 @@ def test_quiver_transform_xy_1d_uv_2d(self): def test_quiver_transform_xy_2d_uv_1d(self): with pytest.raises(ValueError): - self.ax.quiver(self.x2d, self.y2d, - self.u.ravel(), self.v.ravel(), transform=self.rp) + self.ax.quiver( + self.x2d, self.y2d, self.u.ravel(), self.v.ravel(), transform=self.rp + ) def test_quiver_transform_inconsistent_shape(self): with pytest.raises(ValueError): - self.ax.quiver(self.x, self.y, - self.u.ravel(), self.v.ravel(), transform=self.rp) + self.ax.quiver( + self.x, self.y, self.u.ravel(), self.v.ravel(), transform=self.rp + ) diff --git a/lib/cartopy/tests/mpl/test_set_extent.py b/lib/cartopy/tests/mpl/test_set_extent.py index 89f636cf7..b1722b9d5 100644 --- a/lib/cartopy/tests/mpl/test_set_extent.py +++ b/lib/cartopy/tests/mpl/test_set_extent.py @@ -20,29 +20,34 @@ def test_extents(): ax.set_extent(uk, crs=uk_crs) # enable to see what is going on (and to make sure it is a plot of the uk) # ax.coastlines() - assert_array_almost_equal(ax.viewLim.get_points(), - np.array([[-12.5, 49.], [4., 60.]])) + assert_array_almost_equal( + ax.viewLim.get_points(), np.array([[-12.5, 49.0], [4.0, 60.0]]) + ) ax = plt.axes(projection=ccrs.NorthPolarStereo(), label='npstere') ax.set_extent(uk, crs=uk_crs) # enable to see what is going on (and to make sure it is a plot of the uk) # ax.coastlines() - assert_array_almost_equal(ax.viewLim.get_points(), - np.array([[-1034046.22566261, -4765889.76601514], - [333263.47741164, -3345219.0594531]]) - ) + assert_array_almost_equal( + ax.viewLim.get_points(), + np.array( + [ + [-1034046.22566261, -4765889.76601514], + [333263.47741164, -3345219.0594531], + ] + ), + ) # given that we know the PolarStereo coordinates of the UK, try using # those in a PlateCarree plot ax = plt.axes(projection=ccrs.PlateCarree(), label='pc') - ax.set_extent([-1034046, 333263, -4765889, -3345219], - crs=ccrs.NorthPolarStereo()) + ax.set_extent([-1034046, 333263, -4765889, -3345219], crs=ccrs.NorthPolarStereo()) # enable to see what is going on (and to make sure it is a plot of the uk) # ax.coastlines() - assert_array_almost_equal(ax.viewLim.get_points(), - np.array([[-17.17698577, 48.21879707], - [5.68924381, 60.54218893]]) - ) + assert_array_almost_equal( + ax.viewLim.get_points(), + np.array([[-17.17698577, 48.21879707], [5.68924381, 60.54218893]]), + ) def test_get_extent(): @@ -58,8 +63,7 @@ def test_get_extent(): ax.set_extent(uk, crs=uk_crs) assert_array_almost_equal(ax.get_extent(uk_crs), uk) - ax = plt.axes(projection=ccrs.Mercator(min_latitude=uk[2], - max_latitude=uk[3])) + ax = plt.axes(projection=ccrs.Mercator(min_latitude=uk[2], max_latitude=uk[3])) ax.set_extent(uk, crs=uk_crs) assert_array_almost_equal(ax.get_extent(uk_crs), uk, decimal=1) @@ -87,15 +91,16 @@ def test_update_lim(): # check that the standard data lim setting works ax = plt.axes(projection=ccrs.PlateCarree()) ax.update_datalim([(-10, -10), (-5, -5)]) - assert_array_almost_equal(ax.dataLim.get_points(), - np.array([[-10., -10.], [-5., -5.]])) + assert_array_almost_equal( + ax.dataLim.get_points(), np.array([[-10.0, -10.0], [-5.0, -5.0]]) + ) def test_limits_contour(): xs, ys = np.meshgrid(np.linspace(250, 350, 15), np.linspace(-45, 45, 20)) - data = np.sin((xs * ys) * 1.e7) + data = np.sin((xs * ys) * 1.0e7) - resulting_extent = np.array([[250 - 180, -45.], [-10. + 180, 45.]]) + resulting_extent = np.array([[250 - 180, -45.0], [-10.0 + 180, 45.0]]) ax = plt.axes(projection=ccrs.PlateCarree()) ax.coastlines() @@ -111,9 +116,9 @@ def test_limits_contour(): def test_limits_pcolor(): xs, ys = np.meshgrid(np.linspace(250, 350, 15), np.linspace(-45, 45, 20)) - data = (np.sin((xs * ys) * 1.e7))[:-1, :-1] + data = (np.sin((xs * ys) * 1.0e7))[:-1, :-1] - resulting_extent = np.array([[250 - 180, -45.], [-10. + 180, 45.]]) + resulting_extent = np.array([[250 - 180, -45.0], [-10.0 + 180, 45.0]]) ax = plt.axes(projection=ccrs.PlateCarree()) ax.coastlines() @@ -134,26 +139,22 @@ def test_view_lim_autoscaling(): ax = plt.axes(projection=ccrs.RotatedPole(37.5, 357.5)) ax.scatter(x, y, x * y, transform=ccrs.PlateCarree()) - expected = np.array([[86.12433701, 52.51570463], - [86.69696603, 52.86372057]]) + expected = np.array([[86.12433701, 52.51570463], [86.69696603, 52.86372057]]) - assert_array_almost_equal(ax.viewLim.frozen().get_points(), expected, - decimal=2) + assert_array_almost_equal(ax.viewLim.frozen().get_points(), expected, decimal=2) plt.draw() - assert_array_almost_equal(ax.viewLim.frozen().get_points(), expected, - decimal=2) + assert_array_almost_equal(ax.viewLim.frozen().get_points(), expected, decimal=2) ax.autoscale_view(tight=False) expected_non_tight = np.array([[86, 52.45], [86.8, 52.9]]) - assert_array_almost_equal(ax.viewLim.frozen().get_points(), - expected_non_tight, decimal=1) + assert_array_almost_equal( + ax.viewLim.frozen().get_points(), expected_non_tight, decimal=1 + ) def test_view_lim_default_global(tmp_path): ax = plt.axes(projection=ccrs.PlateCarree()) # The view lim should be the default unit bbox until it is drawn. - assert_array_almost_equal(ax.viewLim.frozen().get_points(), - [[0, 0], [1, 1]]) + assert_array_almost_equal(ax.viewLim.frozen().get_points(), [[0, 0], [1, 1]]) plt.savefig(tmp_path / 'view_lim_default_global.png') expected = np.array([[-180, -90], [180, 90]]) - assert_array_almost_equal(ax.viewLim.frozen().get_points(), - expected) + assert_array_almost_equal(ax.viewLim.frozen().get_points(), expected) diff --git a/lib/cartopy/tests/mpl/test_shapely_to_mpl.py b/lib/cartopy/tests/mpl/test_shapely_to_mpl.py index 9d28471eb..132da4679 100644 --- a/lib/cartopy/tests/mpl/test_shapely_to_mpl.py +++ b/lib/cartopy/tests/mpl/test_shapely_to_mpl.py @@ -28,22 +28,34 @@ def test_polygon_interiors(use_legacy_geos_funcs): ax.coastlines() ax.set_global() - pth = Path([[0, -45], [60, -45], [60, 45], [0, 45], [0, 45], - [10, -20], [10, 20], [40, 20], [40, -20], [10, 20]], - [1, 2, 2, 2, 79, 1, 2, 2, 2, 79]) + pth = Path( + [ + [0, -45], + [60, -45], + [60, 45], + [0, 45], + [0, 45], + [10, -20], + [10, 20], + [40, 20], + [40, -20], + [10, 20], + ], + [1, 2, 2, 2, 79, 1, 2, 2, 2, 79], + ) if use_legacy_geos_funcs: patches_native = [] patches = [] - with pytest.warns(DeprecationWarning, match="path_to_geos is deprecated"): + with pytest.warns(DeprecationWarning, match='path_to_geos is deprecated'): for geos in cpatch.path_to_geos(pth): - with pytest.warns(DeprecationWarning, match="geos_to_path is deprecat"): + with pytest.warns(DeprecationWarning, match='geos_to_path is deprecat'): for pth in cpatch.geos_to_path(geos): patches.append(mpatches.PathPatch(pth)) # buffer by 10 degrees (leaves a small hole in the middle) geos_buffered = geos.buffer(10) - with pytest.warns(DeprecationWarning, match="geos_to_path is deprecat"): + with pytest.warns(DeprecationWarning, match='geos_to_path is deprecat'): for pth in cpatch.geos_to_path(geos_buffered): patches_native.append(mpatches.PathPatch(pth)) else: @@ -57,36 +69,42 @@ def test_polygon_interiors(use_legacy_geos_funcs): patches_native = [mpatches.PathPatch(path_buffered)] # Set high zorder to ensure the polygons are drawn on top of coastlines. - collection = PatchCollection(patches_native, facecolor='red', alpha=0.4, - transform=ax.projection, zorder=10) + collection = PatchCollection( + patches_native, facecolor='red', alpha=0.4, transform=ax.projection, zorder=10 + ) ax.add_collection(collection) - collection = PatchCollection(patches, facecolor='yellow', alpha=0.4, - transform=ccrs.Geodetic(), zorder=10) + collection = PatchCollection( + patches, facecolor='yellow', alpha=0.4, transform=ccrs.Geodetic(), zorder=10 + ) ax.add_collection(collection) # test multiple interior polygons - ax = fig.add_subplot(2, 1, 2, projection=ccrs.PlateCarree(), - xlim=[-5, 15], ylim=[-5, 15]) - ax.coastlines(resolution="110m") + ax = fig.add_subplot( + 2, 1, 2, projection=ccrs.PlateCarree(), xlim=[-5, 15], ylim=[-5, 15] + ) + ax.coastlines(resolution='110m') exterior = np.array(sgeom.box(0, 0, 12, 12).exterior.coords) - interiors = [np.array(sgeom.box(1, 1, 2, 2, ccw=False).exterior.coords), - np.array(sgeom.box(1, 8, 2, 9, ccw=False).exterior.coords)] + interiors = [ + np.array(sgeom.box(1, 1, 2, 2, ccw=False).exterior.coords), + np.array(sgeom.box(1, 8, 2, 9, ccw=False).exterior.coords), + ] poly = sgeom.Polygon(exterior, interiors) if use_legacy_geos_funcs: patches = [] - with pytest.warns(DeprecationWarning, match="geos_to_path is deprecated"): + with pytest.warns(DeprecationWarning, match='geos_to_path is deprecated'): for pth in cpatch.geos_to_path(poly): patches.append(mpatches.PathPatch(pth)) else: path = cpath.shapely_to_path(poly) patches = [mpatches.PathPatch(path)] - collection = PatchCollection(patches, facecolor='yellow', alpha=0.4, - transform=ccrs.Geodetic(), zorder=10) + collection = PatchCollection( + patches, facecolor='yellow', alpha=0.4, transform=ccrs.Geodetic(), zorder=10 + ) ax.add_collection(collection) return fig @@ -98,8 +116,7 @@ def test_contour_interiors(): # produces a polygon with multiple holes: nx, ny = 10, 10 numlev = 2 - lons, lats = np.meshgrid(np.linspace(-50, 50, nx), - np.linspace(-45, 45, ny)) + lons, lats = np.meshgrid(np.linspace(-50, 50, nx), np.linspace(-45, 45, ny)) data = np.sin(np.hypot(lons, lats)) fig = plt.figure() diff --git a/lib/cartopy/tests/mpl/test_style.py b/lib/cartopy/tests/mpl/test_style.py index be4df17f6..8f09ac4f9 100644 --- a/lib/cartopy/tests/mpl/test_style.py +++ b/lib/cartopy/tests/mpl/test_style.py @@ -15,41 +15,52 @@ @pytest.mark.parametrize( ('styles', 'expected'), - [([], {}), - ([{}, {}, {}], {}), - ([{}, dict(a=2), dict(a=1)], dict(a=1)), - ([dict(fc='red')], dict(facecolor='red')), - ([dict(fc='red', color='blue')], - dict(facecolor='blue', edgecolor='blue')), - ([dict(fc='red', facecolor='blue')], dict(facecolor='blue')), - ([dict(color='red')], - dict(edgecolor='red', facecolor='red')), - ([dict(edgecolor='blue'), dict(color='red')], - dict(edgecolor='red', facecolor='red')), - ([dict(edgecolor='blue'), dict(color='red')], - dict(edgecolor='red', facecolor='red')), - ([dict(color='blue'), dict(edgecolor='red')], - dict(edgecolor='red', facecolor='blue')), - # Even if you set an edgecolor, color should trump it. - ([dict(color='blue'), dict(edgecolor='red', color='yellow')], - dict(edgecolor='yellow', facecolor='yellow')), - # Support for 'never' being honoured. - ([dict(facecolor='never'), dict(color='yellow')], - dict(edgecolor='yellow', facecolor='never')), - ([dict(lw=1, linewidth=2)], dict(linewidth=2)), - ([dict(lw=1, linewidth=2), dict(lw=3)], dict(linewidth=3)), - ([dict(color=None), dict(facecolor='red')], - dict(facecolor='red', edgecolor=None)), - ([dict(linewidth=1), dict(lw=None)], dict(linewidth=None)), - ([dict(facecolor='never'), dict(fc='NoNe')], dict(facecolor='never')), - ] + [ + ([], {}), + ([{}, {}, {}], {}), + ([{}, dict(a=2), dict(a=1)], dict(a=1)), + ([dict(fc='red')], dict(facecolor='red')), + ([dict(fc='red', color='blue')], dict(facecolor='blue', edgecolor='blue')), + ([dict(fc='red', facecolor='blue')], dict(facecolor='blue')), + ([dict(color='red')], dict(edgecolor='red', facecolor='red')), + ( + [dict(edgecolor='blue'), dict(color='red')], + dict(edgecolor='red', facecolor='red'), + ), + ( + [dict(edgecolor='blue'), dict(color='red')], + dict(edgecolor='red', facecolor='red'), + ), + ( + [dict(color='blue'), dict(edgecolor='red')], + dict(edgecolor='red', facecolor='blue'), + ), + # Even if you set an edgecolor, color should trump it. + ( + [dict(color='blue'), dict(edgecolor='red', color='yellow')], + dict(edgecolor='yellow', facecolor='yellow'), + ), + # Support for 'never' being honoured. + ( + [dict(facecolor='never'), dict(color='yellow')], + dict(edgecolor='yellow', facecolor='never'), + ), + ([dict(lw=1, linewidth=2)], dict(linewidth=2)), + ([dict(lw=1, linewidth=2), dict(lw=3)], dict(linewidth=3)), + ( + [dict(color=None), dict(facecolor='red')], + dict(facecolor='red', edgecolor=None), + ), + ([dict(linewidth=1), dict(lw=None)], dict(linewidth=None)), + ([dict(facecolor='never'), dict(fc='NoNe')], dict(facecolor='never')), + ], ) def test_merge(styles, expected): merged_style = style.merge(*styles) assert merged_style == expected -@pytest.mark.parametrize("case", [{'fc': 'red'}, {'fc': 1}]) +@pytest.mark.parametrize('case', [{'fc': 'red'}, {'fc': 1}]) def test_merge_warning(case): with pytest.warns(UserWarning, match=r'defined as \"never\"'): style.merge({'facecolor': 'never'}, case) @@ -59,9 +70,12 @@ def test_merge_warning(case): ('style_d', 'expected'), [ # Support for 'never' being honoured. - (dict(facecolor='never', edgecolor='yellow'), - dict(edgecolor='yellow', facecolor='none')), - ]) + ( + dict(facecolor='never', edgecolor='yellow'), + dict(edgecolor='yellow', facecolor='none'), + ), + ], +) def test_finalize(style_d, expected): assert style.finalize(style_d) == expected # Double check we are updating in-place diff --git a/lib/cartopy/tests/mpl/test_ticker.py b/lib/cartopy/tests/mpl/test_ticker.py index 9918b492b..3f7acfba8 100644 --- a/lib/cartopy/tests/mpl/test_ticker.py +++ b/lib/cartopy/tests/mpl/test_ticker.py @@ -19,8 +19,8 @@ ) -ONE_MIN = 1 / 60. -ONE_SEC = 1 / 3600. +ONE_MIN = 1 / 60.0 +ONE_SEC = 1 / 3600.0 @pytest.mark.parametrize('cls', [LatitudeFormatter, LongitudeFormatter]) @@ -67,8 +67,15 @@ def test_LatitudeFormatter_number_format(): formatter.set_axis(Mock(axes=Mock(GeoAxes, projection=p))) test_ticks = [-90, -60, -30, 0, 30, 60, 90] result = [formatter(tick) for tick in test_ticks] - expected = ['90.00°S', '60.00°S', '30.00°S', '0.00°', - '30.00°N', '60.00°N', '90.00°N'] + expected = [ + '90.00°S', + '60.00°S', + '30.00°S', + '0.00°', + '30.00°N', + '60.00°N', + '90.00°N', + ] assert result == expected @@ -76,9 +83,15 @@ def test_LatitudeFormatter_mercator(): formatter = LatitudeFormatter() p = ccrs.Mercator() formatter.set_axis(Mock(axes=Mock(GeoAxes, projection=p))) - test_ticks = [-15496570.739707904, -8362698.548496634, - -3482189.085407435, 0.0, 3482189.085407435, - 8362698.548496634, 15496570.739707898] + test_ticks = [ + -15496570.739707904, + -8362698.548496634, + -3482189.085407435, + 0.0, + 3482189.085407435, + 8362698.548496634, + 15496570.739707898, + ] result = [formatter(tick) for tick in test_ticks] expected = ['80°S', '60°S', '30°S', '0°', '30°N', '60°N', '80°N'] assert result == expected @@ -95,9 +108,9 @@ def test_LatitudeFormatter_small_numbers(): def test_LongitudeFormatter_direction_label(): - formatter = LongitudeFormatter(direction_label=False, - dateline_direction_label=True, - zero_direction_label=True) + formatter = LongitudeFormatter( + direction_label=False, dateline_direction_label=True, zero_direction_label=True + ) p = ccrs.PlateCarree() formatter.set_axis(Mock(axes=Mock(GeoAxes, projection=p))) test_ticks = [-180, -120, -60, 0, 60, 120, 180] @@ -106,16 +119,23 @@ def test_LongitudeFormatter_direction_label(): assert result == expected -@pytest.mark.parametrize('central_longitude, kwargs, expected', [ - (0, {'dateline_direction_label': True}, - ['180°W', '120°W', '60°W', '0°', '60°E', '120°E', '180°E']), - (180, {'zero_direction_label': True}, - ['0°E', '60°E', '120°E', '180°', '120°W', '60°W', '0°W']), - (120, {}, - ['60°W', '0°', '60°E', '120°E', '180°', '120°W', '60°W']), -]) -def test_LongitudeFormatter_central_longitude(central_longitude, kwargs, - expected): +@pytest.mark.parametrize( + 'central_longitude, kwargs, expected', + [ + ( + 0, + {'dateline_direction_label': True}, + ['180°W', '120°W', '60°W', '0°', '60°E', '120°E', '180°E'], + ), + ( + 180, + {'zero_direction_label': True}, + ['0°E', '60°E', '120°E', '180°', '120°W', '60°W', '0°W'], + ), + (120, {}, ['60°W', '0°', '60°E', '120°E', '180°', '120°W', '60°W']), + ], +) +def test_LongitudeFormatter_central_longitude(central_longitude, kwargs, expected): formatter = LongitudeFormatter(**kwargs) p = ccrs.PlateCarree(central_longitude=central_longitude) formatter.set_axis(Mock(axes=Mock(GeoAxes, projection=p))) @@ -125,8 +145,7 @@ def test_LongitudeFormatter_central_longitude(central_longitude, kwargs, def test_LongitudeFormatter_degree_symbol(): - formatter = LongitudeFormatter(degree_symbol='', - dateline_direction_label=True) + formatter = LongitudeFormatter(degree_symbol='', dateline_direction_label=True) p = ccrs.PlateCarree() formatter.set_axis(Mock(axes=Mock(GeoAxes, projection=p))) test_ticks = [-180, -120, -60, 0, 60, 120, 180] @@ -136,14 +155,22 @@ def test_LongitudeFormatter_degree_symbol(): def test_LongitudeFormatter_number_format(): - formatter = LongitudeFormatter(number_format='.2f', dms=False, - dateline_direction_label=True) + formatter = LongitudeFormatter( + number_format='.2f', dms=False, dateline_direction_label=True + ) p = ccrs.PlateCarree() formatter.set_axis(Mock(axes=Mock(GeoAxes, projection=p))) test_ticks = [-180, -120, -60, 0, 60, 120, 180] result = [formatter(tick) for tick in test_ticks] - expected = ['180.00°W', '120.00°W', '60.00°W', '0.00°', - '60.00°E', '120.00°E', '180.00°E'] + expected = [ + '180.00°W', + '120.00°W', + '60.00°W', + '0.00°', + '60.00°E', + '120.00°E', + '180.00°E', + ] assert result == expected @@ -151,22 +178,33 @@ def test_LongitudeFormatter_mercator(): formatter = LongitudeFormatter(dateline_direction_label=True) p = ccrs.Mercator() formatter.set_axis(Mock(axes=Mock(GeoAxes, projection=p))) - test_ticks = [-20037508.342783064, -13358338.895188706, - -6679169.447594353, 0.0, 6679169.447594353, - 13358338.895188706, 20037508.342783064] + test_ticks = [ + -20037508.342783064, + -13358338.895188706, + -6679169.447594353, + 0.0, + 6679169.447594353, + 13358338.895188706, + 20037508.342783064, + ] result = [formatter(tick) for tick in test_ticks] expected = ['180°W', '120°W', '60°W', '0°', '60°E', '120°E', '180°E'] assert result == expected -@pytest.mark.parametrize('central_longitude, zero_direction_label, expected', [ - (0, False, ['17.1142343°W', '17.1142340°W', '17.1142337°W']), - (180, True, ['162.8857657°E', '162.8857660°E', '162.8857663°E']), -]) -def test_LongitudeFormatter_small_numbers(central_longitude, - zero_direction_label, expected): - formatter = LongitudeFormatter(number_format='.7f', dms=False, - zero_direction_label=zero_direction_label) +@pytest.mark.parametrize( + 'central_longitude, zero_direction_label, expected', + [ + (0, False, ['17.1142343°W', '17.1142340°W', '17.1142337°W']), + (180, True, ['162.8857657°E', '162.8857660°E', '162.8857663°E']), + ], +) +def test_LongitudeFormatter_small_numbers( + central_longitude, zero_direction_label, expected +): + formatter = LongitudeFormatter( + number_format='.7f', dms=False, zero_direction_label=zero_direction_label + ) p = ccrs.PlateCarree(central_longitude=central_longitude) formatter.set_axis(Mock(axes=Mock(GeoAxes, projection=p))) test_ticks = [-17.1142343, -17.1142340, -17.1142337] @@ -175,30 +213,34 @@ def test_LongitudeFormatter_small_numbers(central_longitude, @pytest.mark.parametrize('direction_label', [False, True]) -@pytest.mark.parametrize('test_ticks,expected', [ - pytest.param([-3.75, -3.5], ['3°45′W', '3°30′W'], id='minutes_no_hide'), - pytest.param([-3.5, -3], ['30′', '3°W'], id='minutes_hide'), - pytest.param([-3 - 2 * ONE_MIN - 30 * ONE_SEC], ['3°2′30″W'], - id='seconds'), -]) -def test_LongitudeFormatter_minutes_seconds(test_ticks, direction_label, - expected): - formatter = LongitudeFormatter(dms=True, auto_hide=True, - direction_label=direction_label) +@pytest.mark.parametrize( + 'test_ticks,expected', + [ + pytest.param([-3.75, -3.5], ['3°45′W', '3°30′W'], id='minutes_no_hide'), + pytest.param([-3.5, -3], ['30′', '3°W'], id='minutes_hide'), + pytest.param([-3 - 2 * ONE_MIN - 30 * ONE_SEC], ['3°2′30″W'], id='seconds'), + ], +) +def test_LongitudeFormatter_minutes_seconds(test_ticks, direction_label, expected): + formatter = LongitudeFormatter( + dms=True, auto_hide=True, direction_label=direction_label + ) formatter.set_locs(test_ticks) result = [formatter(tick) for tick in test_ticks] prefix = '' if direction_label else '-' suffix = 'W' if direction_label else '' expected = [ - f'{prefix}{text[:-1]}{suffix}' if text[-1] == 'W' else text - for text in expected + f'{prefix}{text[:-1]}{suffix}' if text[-1] == 'W' else text for text in expected ] assert result == expected -@pytest.mark.parametrize("test_ticks,expected", [ - pytest.param([-3.75, -3.5], ['3°45′S', '3°30′S'], id='minutes_no_hide'), -]) +@pytest.mark.parametrize( + 'test_ticks,expected', + [ + pytest.param([-3.75, -3.5], ['3°45′S', '3°30′S'], id='minutes_no_hide'), + ], +) def test_LatitudeFormatter_minutes_seconds(test_ticks, expected): formatter = LatitudeFormatter(dms=True, auto_hide=True) formatter.set_locs(test_ticks) @@ -206,8 +248,9 @@ def test_LatitudeFormatter_minutes_seconds(test_ticks, expected): assert result == expected -@pytest.mark.parametrize("cls,letter", - [(LongitudeFormatter, 'E'), (LatitudeFormatter, 'N')]) +@pytest.mark.parametrize( + 'cls,letter', [(LongitudeFormatter, 'E'), (LatitudeFormatter, 'N')] +) def test_lonlatformatter_non_geoaxes(cls, letter): ticks = [2, 2.5] fig = plt.figure() @@ -220,21 +263,42 @@ def test_lonlatformatter_non_geoaxes(cls, letter): assert ticklabels == [f'{v:g}{letter}' for v in ticks] -@pytest.mark.parametrize("cls,vmin,vmax,expected", [ - pytest.param(LongitudeLocator, -180, 180, - [-180, -120, -60, 0, 60, 120, 180], id='lon_large'), - pytest.param(LatitudeLocator, -180, 180, [-90, -60, -30, 0, 30, 60, 90], - id='lat_large'), - pytest.param(LongitudeLocator, -10, 0, - [-10.5, -9, -7.5, -6, -4.5, -3, -1.5, 0], - id='lon_medium'), - pytest.param(LongitudeLocator, -1, 0, - np.array([-60, -50, -40, -30, -20, -10, 0]) / 60, - id='lon_small'), - pytest.param(LongitudeLocator, 0, 2 * ONE_MIN, - np.array([0, 18, 36, 54, 72, 90, 108, 126]) / 3600, - id='lon_tiny'), -]) +@pytest.mark.parametrize( + 'cls,vmin,vmax,expected', + [ + pytest.param( + LongitudeLocator, + -180, + 180, + [-180, -120, -60, 0, 60, 120, 180], + id='lon_large', + ), + pytest.param( + LatitudeLocator, -180, 180, [-90, -60, -30, 0, 30, 60, 90], id='lat_large' + ), + pytest.param( + LongitudeLocator, + -10, + 0, + [-10.5, -9, -7.5, -6, -4.5, -3, -1.5, 0], + id='lon_medium', + ), + pytest.param( + LongitudeLocator, + -1, + 0, + np.array([-60, -50, -40, -30, -20, -10, 0]) / 60, + id='lon_small', + ), + pytest.param( + LongitudeLocator, + 0, + 2 * ONE_MIN, + np.array([0, 18, 36, 54, 72, 90, 108, 126]) / 3600, + id='lon_tiny', + ), + ], +) def test_LongitudeLocator(cls, vmin, vmax, expected): locator = cls(dms=True) result = locator.tick_values(vmin, vmax) @@ -244,12 +308,12 @@ def test_LongitudeLocator(cls, vmin, vmax, expected): def test_lonlatformatter_decimal_point(): xticker = LongitudeFormatter(decimal_point=',', number_format='0.2f') yticker = LatitudeFormatter(decimal_point=',', number_format='0.2f') - assert xticker(-10) == "10,00°W" - assert yticker(-10) == "10,00°S" + assert xticker(-10) == '10,00°W' + assert yticker(-10) == '10,00°S' def test_lonlatformatter_cardinal_labels(): xticker = LongitudeFormatter(cardinal_labels={'west': 'O'}) yticker = LatitudeFormatter(cardinal_labels={'south': 'South'}) - assert xticker(-10) == "10°O" - assert yticker(-10) == "10°South" + assert xticker(-10) == '10°O' + assert yticker(-10) == '10°South' diff --git a/lib/cartopy/tests/mpl/test_ticks.py b/lib/cartopy/tests/mpl/test_ticks.py index f75ad2d27..8401ca716 100644 --- a/lib/cartopy/tests/mpl/test_ticks.py +++ b/lib/cartopy/tests/mpl/test_ticks.py @@ -57,8 +57,7 @@ def test_set_yticks_cylindrical(): 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, 15, 45, 75], minor=True, - crs=ccrs.PlateCarree()) + ax.set_yticks([-75, -45, -15, 15, 45, 75], minor=True, crs=ccrs.PlateCarree()) return ax.figure @@ -67,17 +66,18 @@ 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, 15, 45, 75], minor=True, - crs=ccrs.Geodetic()) + ax.set_yticks([-75, -45, -15, 15, 45, 75], minor=True, crs=ccrs.Geodetic()) @pytest.mark.natural_earth @pytest.mark.mpl_image_compare(filename='xyticks.png') def test_set_xyticks(): fig = plt.figure(figsize=(10, 10)) - projections = (ccrs.PlateCarree(), - ccrs.Mercator(), - ccrs.TransverseMercator(approx=False)) + projections = ( + ccrs.PlateCarree(), + ccrs.Mercator(), + ccrs.TransverseMercator(approx=False), + ) x = -3.275024 y = 50.753998 for i, prj in enumerate(projections, 1): diff --git a/lib/cartopy/tests/mpl/test_web_services.py b/lib/cartopy/tests/mpl/test_web_services.py index 6207e7bbb..bdec4a046 100644 --- a/lib/cartopy/tests/mpl/test_web_services.py +++ b/lib/cartopy/tests/mpl/test_web_services.py @@ -17,7 +17,7 @@ from cartopy.io.ogc_clients import _OWSLIB_AVAILABLE -@pytest.mark.filterwarnings("ignore:TileMatrixLimits") +@pytest.mark.filterwarnings('ignore:TileMatrixLimits') @pytest.mark.network @pytest.mark.skipif(not _OWSLIB_AVAILABLE, reason='OWSLib is unavailable.') @pytest.mark.mpl_image_compare(filename='wmts.png', tolerance=0.03) diff --git a/lib/cartopy/tests/test_coastline.py b/lib/cartopy/tests/test_coastline.py index 2123ed009..b4f7890fc 100644 --- a/lib/cartopy/tests/test_coastline.py +++ b/lib/cartopy/tests/test_coastline.py @@ -9,7 +9,7 @@ import cartopy.io.shapereader as shp -@pytest.mark.filterwarnings("ignore:Downloading") +@pytest.mark.filterwarnings('ignore:Downloading') @pytest.mark.natural_earth class TestCoastline: def test_robust(self): @@ -17,8 +17,7 @@ def test_robust(self): # Make sure all the coastlines can be projected without raising any # exceptions. - projection = cartopy.crs.TransverseMercator(central_longitude=-90, - approx=False) + projection = cartopy.crs.TransverseMercator(central_longitude=-90, approx=False) reader = shp.Reader(COASTLINE_PATH) all_geometries = list(reader.geometries()) geometries = [] diff --git a/lib/cartopy/tests/test_coding_standards.py b/lib/cartopy/tests/test_coding_standards.py index aaca102ea..d0aa71164 100644 --- a/lib/cartopy/tests/test_coding_standards.py +++ b/lib/cartopy/tests/test_coding_standards.py @@ -31,8 +31,7 @@ # Guess cartopy repo directory of cartopy - realpath is used to mitigate # against Python finding the cartopy package via a symlink. CARTOPY_DIR = Path(cartopy.__file__).parent.resolve() -REPO_DIR = Path(os.getenv('CARTOPY_GIT_DIR', - CARTOPY_DIR.parent.parent)) +REPO_DIR = Path(os.getenv('CARTOPY_GIT_DIR', CARTOPY_DIR.parent.parent)) class TestLicenseHeaders: @@ -52,43 +51,46 @@ def list_tracked_files(): if not (REPO_DIR / '.git').is_dir(): raise ValueError(f'{REPO_DIR} is not a git repository.') - output = subprocess.check_output(['git', 'ls-tree', '-z', '-r', - '--name-only', 'HEAD'], - cwd=REPO_DIR) + output = subprocess.check_output( + ['git', 'ls-tree', '-z', '-r', '--name-only', 'HEAD'], cwd=REPO_DIR + ) output = output.rstrip(b'\0').split(b'\0') res = [fname.decode() for fname in output] return res def test_license_headers(self): - exclude_patterns = ('build/*', - 'dist/*', - 'docs/build/*', - 'docs/source/gallery/*', - 'examples/*', - 'lib/cartopy/_version.py', - ) + exclude_patterns = ( + 'build/*', + 'dist/*', + 'docs/build/*', + 'docs/source/gallery/*', + 'examples/*', + 'lib/cartopy/_version.py', + ) try: tracked_files = self.list_tracked_files() except ValueError as e: # Caught the case where this is not a git repo. - return pytest.skip('cartopy installation did not look like a git ' - f'repo: {e}') + return pytest.skip( + f'cartopy installation did not look like a git repo: {e}' + ) failed = [] for fname in sorted(tracked_files): full_fname = REPO_DIR / fname ext = full_fname.suffix - if ext in ('.py', '.pyx', '.c', '.cpp', '.h') and \ - full_fname.is_file() and \ - not any(fnmatch(fname, pat) for pat in exclude_patterns): - + if ( + ext in ('.py', '.pyx', '.c', '.cpp', '.h') + and full_fname.is_file() + and not any(fnmatch(fname, pat) for pat in exclude_patterns) + ): if full_fname.stat().st_size == 0: # Allow completely empty files (e.g. ``__init__.py``) continue - content = full_fname.read_text(encoding="utf-8") + content = full_fname.read_text(encoding='utf-8') if not bool(LICENSE_RE.match(content)): failed.append(full_fname) diff --git a/lib/cartopy/tests/test_crs.py b/lib/cartopy/tests/test_crs.py index cffe2087f..1655a0794 100644 --- a/lib/cartopy/tests/test_crs.py +++ b/lib/cartopy/tests/test_crs.py @@ -36,16 +36,15 @@ def test_osni(self, approx): ll = ccrs.Geodetic() # results obtained by nearby.org.uk. - lon, lat = np.array([-5.54159863617957, 54.5622169298669], - dtype=np.double) + lon, lat = np.array([-5.54159863617957, 54.5622169298669], dtype=np.double) east, north = np.array([359000, 371000], dtype=np.double) - assert_arr_almost_eq(osni.transform_point(lon, lat, ll), - np.array([east, north]), - -1) - assert_arr_almost_eq(ll.transform_point(east, north, osni), - np.array([lon, lat]), - 3) + assert_arr_almost_eq( + osni.transform_point(lon, lat, ll), np.array([east, north]), -1 + ) + assert_arr_almost_eq( + ll.transform_point(east, north, osni), np.array([lon, lat]), 3 + ) def _check_osgb(self, osgb): precision = 1 @@ -53,13 +52,15 @@ def _check_osgb(self, osgb): 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() + 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') + + warnings.warn( + f'{grid_name} is unavailable; testing OSGB at reduced precision' + ) precision = -1 ll = ccrs.Geodetic() @@ -69,10 +70,12 @@ def _check_osgb(self, osgb): east, north = np.array([295132.1, 63512.6], dtype=np.double) # note the handling of precision here... - 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) + 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)) @@ -96,8 +99,7 @@ def test_epsg(self): expected_x = (-104728.764, 688806.007) expected_y = (-8908.36, 1256616.32) expected_threshold = 7935.34 - assert_almost_equal(uk.x_limits, - expected_x, decimal=3) + assert_almost_equal(uk.x_limits, expected_x, decimal=3) assert_almost_equal(uk.y_limits, expected_y, decimal=2) assert_almost_equal(uk.threshold, expected_threshold, decimal=2) self._check_osgb(uk) @@ -116,42 +118,44 @@ def test_europp(self): assert '+ellps=intl' in proj4_init def test_transform_points_nD(self): - rlons = np.array([[350., 352., 354.], [350., 352., 354.]]) - rlats = np.array([[-5., -0., 1.], [-4., -1., 0.]]) + rlons = np.array([[350.0, 352.0, 354.0], [350.0, 352.0, 354.0]]) + rlats = np.array([[-5.0, -0.0, 1.0], [-4.0, -1.0, 0.0]]) - src_proj = ccrs.RotatedGeodetic(pole_longitude=178.0, - pole_latitude=38.0) + src_proj = ccrs.RotatedGeodetic(pole_longitude=178.0, pole_latitude=38.0) target_proj = ccrs.Geodetic() - res = target_proj.transform_points(x=rlons, y=rlats, - src_crs=src_proj) + res = target_proj.transform_points(x=rlons, y=rlats, src_crs=src_proj) unrotated_lon = res[..., 0] unrotated_lat = res[..., 1] # Solutions derived by proj direct. - solx = np.array([[-16.42176094, -14.85892262, -11.90627520], - [-16.71055023, -14.58434624, -11.68799988]]) - soly = np.array([[46.00724251, 51.29188893, 52.59101488], - [46.98728486, 50.30706042, 51.60004528]]) + solx = np.array( + [ + [-16.42176094, -14.85892262, -11.90627520], + [-16.71055023, -14.58434624, -11.68799988], + ] + ) + soly = np.array( + [ + [46.00724251, 51.29188893, 52.59101488], + [46.98728486, 50.30706042, 51.60004528], + ] + ) assert_arr_almost_eq(unrotated_lon, solx) assert_arr_almost_eq(unrotated_lat, soly) def test_transform_points_1D(self): - rlons = np.array([350., 352., 354., 356.]) - rlats = np.array([-5., -0., 5., 10.]) + rlons = np.array([350.0, 352.0, 354.0, 356.0]) + rlats = np.array([-5.0, -0.0, 5.0, 10.0]) - src_proj = ccrs.RotatedGeodetic(pole_longitude=178.0, - pole_latitude=38.0) + src_proj = ccrs.RotatedGeodetic(pole_longitude=178.0, pole_latitude=38.0) target_proj = ccrs.Geodetic() - res = target_proj.transform_points(x=rlons, y=rlats, - src_crs=src_proj) + res = target_proj.transform_points(x=rlons, y=rlats, src_crs=src_proj) unrotated_lon = res[..., 0] unrotated_lat = res[..., 1] # Solutions derived by proj direct. - solx = np.array([-16.42176094, -14.85892262, - -12.88946157, -10.35078336]) - soly = np.array([46.00724251, 51.29188893, - 56.55031485, 61.77015703]) + solx = np.array([-16.42176094, -14.85892262, -12.88946157, -10.35078336]) + soly = np.array([46.00724251, 51.29188893, 56.55031485, 61.77015703]) assert_arr_almost_eq(unrotated_lon, solx) assert_arr_almost_eq(unrotated_lat, soly) @@ -165,8 +169,7 @@ def test_transform_points_xyz(self): src_proj = ccrs.Geocentric() target_proj = ccrs.Geodetic() - res = target_proj.transform_points(x=rx, y=ry, z=rz, - src_crs=src_proj) + res = target_proj.transform_points(x=rx, y=ry, z=rz, src_crs=src_proj) glat = res[..., 0] glon = res[..., 1] @@ -194,21 +197,25 @@ def test_transform_points_180(self): def test_globe(self): # Ensure the globe affects output. - rugby_globe = ccrs.Globe(semimajor_axis=9000000, - semiminor_axis=9000000, - ellipse=None) - footy_globe = ccrs.Globe(semimajor_axis=1000000, - semiminor_axis=1000000, - ellipse=None) + rugby_globe = ccrs.Globe( + semimajor_axis=9000000, semiminor_axis=9000000, ellipse=None + ) + footy_globe = ccrs.Globe( + semimajor_axis=1000000, semiminor_axis=1000000, ellipse=None + ) rugby_moll = ccrs.Mollweide(globe=rugby_globe) footy_moll = ccrs.Mollweide(globe=footy_globe) rugby_pt = rugby_moll.transform_point( - 10, 10, rugby_moll.as_geodetic(), + 10, + 10, + rugby_moll.as_geodetic(), ) footy_pt = footy_moll.transform_point( - 10, 10, footy_moll.as_geodetic(), + 10, + 10, + footy_moll.as_geodetic(), ) assert_arr_almost_eq(rugby_pt, (1400915, 1741319), decimal=0) @@ -222,44 +229,48 @@ def test_project_point(self): pc_rotated = ccrs.PlateCarree(central_longitude=180) result = pc_rotated.project_geometry(point, pc) - assert_arr_almost_eq(result.xy, [[-180.], [45.]]) + assert_arr_almost_eq(result.xy, [[-180.0], [45.0]]) result = pc_rotated.project_geometry(multi_point, pc) assert isinstance(result, sgeom.MultiPoint) assert len(result.geoms) == 2 - assert_arr_almost_eq(result.geoms[0].xy, [[-180.], [45.]]) - assert_arr_almost_eq(result.geoms[1].xy, [[0], [45.]]) + assert_arr_almost_eq(result.geoms[0].xy, [[-180.0], [45.0]]) + assert_arr_almost_eq(result.geoms[1].xy, [[0], [45.0]]) def test_utm(self): utm30n = ccrs.UTM(30) ll = ccrs.Geodetic() lon, lat = np.array([-3.0, 51.5], dtype=np.double) east, north = np.array([500000, 5705429.2], dtype=np.double) - assert_arr_almost_eq(utm30n.transform_point(lon, lat, ll), - [east, north], - decimal=1) - assert_arr_almost_eq(ll.transform_point(east, north, utm30n), - [lon, lat], - decimal=1) + assert_arr_almost_eq( + utm30n.transform_point(lon, lat, ll), [east, north], decimal=1 + ) + assert_arr_almost_eq( + ll.transform_point(east, north, utm30n), [lon, lat], decimal=1 + ) utm38s = ccrs.UTM(38, southern_hemisphere=True) lon, lat = np.array([47.5, -18.92], dtype=np.double) east, north = np.array([763316.7, 7906160.8], dtype=np.double) - assert_arr_almost_eq(utm38s.transform_point(lon, lat, ll), - [east, north], - decimal=1) - assert_arr_almost_eq(ll.transform_point(east, north, utm38s), - [lon, lat], - decimal=1) - - -@pytest.fixture(params=[ - [ccrs.PlateCarree, {}], - [ccrs.PlateCarree, dict(central_longitude=1.23)], - [ccrs.NorthPolarStereo, dict(central_longitude=42.5, - globe=ccrs.Globe(ellipse="helmert"))], - [ccrs.CRS, dict(proj4_params="3088")], - [ccrs.epsg, dict(code="3088")] -]) + assert_arr_almost_eq( + utm38s.transform_point(lon, lat, ll), [east, north], decimal=1 + ) + assert_arr_almost_eq( + ll.transform_point(east, north, utm38s), [lon, lat], decimal=1 + ) + + +@pytest.fixture( + params=[ + [ccrs.PlateCarree, {}], + [ccrs.PlateCarree, dict(central_longitude=1.23)], + [ + ccrs.NorthPolarStereo, + dict(central_longitude=42.5, globe=ccrs.Globe(ellipse='helmert')), + ], + [ccrs.CRS, dict(proj4_params='3088')], + [ccrs.epsg, dict(code='3088')], + ] +) def proj_to_copy(request): cls, kwargs = request.param return cls(**kwargs) @@ -281,16 +292,16 @@ def test_deepcopy(proj_to_copy): def test_PlateCarree_shortcut(): - central_lons = [[0, 0], [0, 180], [0, 10], [10, 0], [-180, 180], [ - 180, -180]] + central_lons = [[0, 0], [0, 180], [0, 10], [10, 0], [-180, 180], [180, -180]] - target = [([[-180, -180], [-180, 180]], 0), - ([[-180, 0], [0, 180]], 180), - ([[-180, -170], [-170, 180]], 10), - ([[-180, 170], [170, 180]], -10), - ([[-180, 180], [180, 180]], 360), - ([[-180, -180], [-180, 180]], -360), - ] + target = [ + ([[-180, -180], [-180, 180]], 0), + ([[-180, 0], [0, 180]], 180), + ([[-180, -170], [-170, 180]], 10), + ([[-180, 170], [170, 180]], -10), + ([[-180, 180], [180, 180]], 360), + ([[-180, -180], [-180, 180]], -360), + ] assert len(target) == len(central_lons) @@ -309,8 +320,7 @@ def test_PlateCarree_shortcut(): def test_transform_points_empty(): """Test CRS.transform_points with empty array.""" crs = ccrs.Stereographic() - result = crs.transform_points(ccrs.PlateCarree(), - np.array([]), np.array([])) + result = crs.transform_points(ccrs.PlateCarree(), np.array([]), np.array([])) assert_array_equal(result, np.array([], dtype=np.float64).reshape(0, 3)) @@ -320,17 +330,17 @@ def test_transform_points_outside_domain(): # greater than 1 arrays put infinity into the return array # where the bad values occur crs = ccrs.Orthographic() - result = crs.transform_points(ccrs.PlateCarree(), - np.array([-120]), np.array([80])) + result = crs.transform_points(ccrs.PlateCarree(), np.array([-120]), np.array([80])) assert np.all(np.isnan(result)) - result = crs.transform_points(ccrs.PlateCarree(), - np.array([-120]), np.array([80]), - trap=True) + result = crs.transform_points( + ccrs.PlateCarree(), np.array([-120]), np.array([80]), trap=True + ) assert np.all(np.isnan(result)) # A length-2 array of the same transform produces "inf" rather # than nan due to PROJ never returning nan itself. - result = crs.transform_points(ccrs.PlateCarree(), - np.array([-120, -120]), np.array([80, 80])) + result = crs.transform_points( + ccrs.PlateCarree(), np.array([-120, -120]), np.array([80, 80]) + ) assert np.all(~np.isfinite(result[..., :2])) # Test singular transform to make sure it is producing all nan's @@ -340,8 +350,8 @@ def test_transform_points_outside_domain(): def test_projection__from_string(): - crs = ccrs.Projection("NAD83 / Pennsylvania South") - assert crs.as_geocentric().datum.name == "North American Datum 1983" + crs = ccrs.Projection('NAD83 / Pennsylvania South') + assert crs.as_geocentric().datum.name == 'North American Datum 1983' assert_almost_equal( crs.bounds, [361633.1351868, 859794.6690229, 45575.5693199, 209415.9845754], @@ -349,7 +359,7 @@ def test_projection__from_string(): def test_crs__from_pyproj_crs(): - assert ccrs.CRS(pyproj.CRS("EPSG:4326")) == "EPSG:4326" + assert ccrs.CRS(pyproj.CRS('EPSG:4326')) == 'EPSG:4326' def test_transform_point_no_warning(): @@ -358,15 +368,17 @@ def test_transform_point_no_warning(): p = ccrs.PlateCarree() p2 = ccrs.Mercator() with warnings.catch_warnings(): - warnings.simplefilter("error") + warnings.simplefilter('error') p2.transform_point(1, 2, p) def test_geographic_bounds_no_area_of_use(): # Should default to global bounds if no area of use - wkt = ('GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",' - 'SPHEROID["WGS_1984",6378137.0,298.257223563]],' - 'PRIMEM["Greenwich",0.0],UNIT["Degree",0.017453292519943295]]') + wkt = ( + 'GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",' + 'SPHEROID["WGS_1984",6378137.0,298.257223563]],' + 'PRIMEM["Greenwich",0.0],UNIT["Degree",0.017453292519943295]]' + ) p = pyproj.CRS.from_wkt(wkt) assert p.is_geographic assert p.area_of_use is None diff --git a/lib/cartopy/tests/test_crs_transform_vectors.py b/lib/cartopy/tests/test_crs_transform_vectors.py index bc6ef560e..ce14f5c82 100644 --- a/lib/cartopy/tests/test_crs_transform_vectors.py +++ b/lib/cartopy/tests/test_crs_transform_vectors.py @@ -13,37 +13,29 @@ class TestTransformVectors: - def test_transform(self): # Test some simple vectors to make sure they are transformed # correctly. - rlons = np.array([-90., 0, 90., 180.]) - rlats = np.array([0., 0., 0., 0.]) + rlons = np.array([-90.0, 0, 90.0, 180.0]) + rlats = np.array([0.0, 0.0, 0.0, 0.0]) src_proj = ccrs.PlateCarree() - target_proj = ccrs.Stereographic(central_latitude=90, - central_longitude=0) + target_proj = ccrs.Stereographic(central_latitude=90, central_longitude=0) # transform grid eastward vectors - ut, vt = target_proj.transform_vectors(src_proj, - rlons, - rlats, - np.ones([4]), - np.zeros([4])) + ut, vt = target_proj.transform_vectors( + src_proj, rlons, rlats, np.ones([4]), np.zeros([4]) + ) assert_array_almost_equal(ut, np.array([0, 1, 0, -1]), decimal=2) assert_array_almost_equal(vt, np.array([-1, 0, 1, 0]), decimal=2) # transform grid northward vectors - ut, vt = target_proj.transform_vectors(src_proj, - rlons, - rlats, - np.zeros([4]), - np.ones([4])) + ut, vt = target_proj.transform_vectors( + src_proj, rlons, rlats, np.zeros([4]), np.ones([4]) + ) assert_array_almost_equal(ut, np.array([1, 0, -1, 0]), decimal=2) assert_array_almost_equal(vt, np.array([0, 1, 0, -1]), decimal=2) # transform grid north-eastward vectors - ut, vt = target_proj.transform_vectors(src_proj, - rlons, - rlats, - np.ones([4]), - np.ones([4])) + ut, vt = target_proj.transform_vectors( + src_proj, rlons, rlats, np.ones([4]), np.ones([4]) + ) assert_array_almost_equal(ut, np.array([1, 1, -1, -1]), decimal=2) assert_array_almost_equal(vt, np.array([-1, 1, 1, -1]), decimal=2) @@ -53,10 +45,9 @@ def test_transform_and_inverse(self): y = np.arange(30, 72.5, 2.5) x2d, y2d = np.meshgrid(x, y) u = np.cos(np.deg2rad(y2d)) - v = np.cos(2. * np.deg2rad(x2d)) + v = np.cos(2.0 * np.deg2rad(x2d)) src_proj = ccrs.PlateCarree() - target_proj = ccrs.Stereographic(central_latitude=90, - central_longitude=0) + target_proj = ccrs.Stereographic(central_latitude=90, central_longitude=0) proj_xyz = target_proj.transform_points(src_proj, x2d, y2d) xt, yt = proj_xyz[..., 0], proj_xyz[..., 1] ut, vt = target_proj.transform_vectors(src_proj, x2d, y2d, u, v) @@ -67,13 +58,12 @@ def test_transform_and_inverse(self): def test_invalid_input_domain(self): # If an input coordinate is outside the input projection domain # we should be able to handle it correctly. - rlon = np.array([270.]) - rlat = np.array([0.]) - u = np.array([1.]) - v = np.array([0.]) + rlon = np.array([270.0]) + rlat = np.array([0.0]) + u = np.array([1.0]) + v = np.array([0.0]) src_proj = ccrs.PlateCarree() - target_proj = ccrs.Stereographic(central_latitude=90, - central_longitude=0) + target_proj = ccrs.Stereographic(central_latitude=90, central_longitude=0) ut, vt = target_proj.transform_vectors(src_proj, rlon, rlat, u, v) assert_array_almost_equal(ut, np.array([0]), decimal=2) assert_array_almost_equal(vt, np.array([-1]), decimal=2) @@ -82,57 +72,53 @@ def test_invalid_x_domain(self): # If the point we need to calculate the vector angle falls outside the # source projection x-domain it should be handled correctly as long as # it is not a corner point. - rlon = np.array([180.]) - rlat = np.array([0.]) - u = np.array([1.]) - v = np.array([0.]) + rlon = np.array([180.0]) + rlat = np.array([0.0]) + u = np.array([1.0]) + v = np.array([0.0]) src_proj = ccrs.PlateCarree() - target_proj = ccrs.Stereographic(central_latitude=90, - central_longitude=0) + target_proj = ccrs.Stereographic(central_latitude=90, central_longitude=0) ut, vt = target_proj.transform_vectors(src_proj, rlon, rlat, u, v) assert_array_almost_equal(ut, np.array([-1]), decimal=2) - assert_array_almost_equal(vt, np.array([0.]), decimal=2) + assert_array_almost_equal(vt, np.array([0.0]), decimal=2) def test_invalid_y_domain(self): # If the point we need to calculate the vector angle falls outside the # source projection y-domain it should be handled correctly as long as # it is not a corner point. - rlon = np.array([0.]) - rlat = np.array([90.]) - u = np.array([0.]) - v = np.array([1.]) + rlon = np.array([0.0]) + rlat = np.array([90.0]) + u = np.array([0.0]) + v = np.array([1.0]) src_proj = ccrs.PlateCarree() - target_proj = ccrs.Stereographic(central_latitude=90, - central_longitude=0) + target_proj = ccrs.Stereographic(central_latitude=90, central_longitude=0) ut, vt = target_proj.transform_vectors(src_proj, rlon, rlat, u, v) - assert_array_almost_equal(ut, np.array([0.]), decimal=2) - assert_array_almost_equal(vt, np.array([1.]), decimal=2) + assert_array_almost_equal(ut, np.array([0.0]), decimal=2) + assert_array_almost_equal(vt, np.array([1.0]), decimal=2) def test_invalid_xy_domain_corner(self): # If the point we need to calculate the vector angle falls outside the # source projection x and y-domain it should be handled correctly. - rlon = np.array([180.]) - rlat = np.array([90.]) - u = np.array([1.]) - v = np.array([1.]) + rlon = np.array([180.0]) + rlat = np.array([90.0]) + u = np.array([1.0]) + v = np.array([1.0]) src_proj = ccrs.PlateCarree() - target_proj = ccrs.Stereographic(central_latitude=90, - central_longitude=0) + target_proj = ccrs.Stereographic(central_latitude=90, central_longitude=0) ut, vt = target_proj.transform_vectors(src_proj, rlon, rlat, u, v) - assert_array_almost_equal(ut, np.array([0.]), decimal=2) - assert_array_almost_equal(vt, np.array([-2**.5]), decimal=2) + assert_array_almost_equal(ut, np.array([0.0]), decimal=2) + assert_array_almost_equal(vt, np.array([-(2**0.5)]), decimal=2) def test_invalid_x_domain_corner(self): # If the point we need to calculate the vector angle falls outside the # source projection x-domain and is a corner point, it may be handled # incorrectly and a warning should be raised. - rlon = np.array([180.]) - rlat = np.array([90.]) - u = np.array([1.]) - v = np.array([-1.]) + rlon = np.array([180.0]) + rlat = np.array([90.0]) + u = np.array([1.0]) + v = np.array([-1.0]) src_proj = ccrs.PlateCarree() - target_proj = ccrs.Stereographic(central_latitude=90, - central_longitude=0) + target_proj = ccrs.Stereographic(central_latitude=90, central_longitude=0) with pytest.warns(UserWarning): warnings.simplefilter('always') ut, vt = target_proj.transform_vectors(src_proj, rlon, rlat, u, v) @@ -141,13 +127,12 @@ def test_invalid_y_domain_corner(self): # If the point we need to calculate the vector angle falls outside the # source projection y-domain and is a corner point, it may be handled # incorrectly and a warning should be raised. - rlon = np.array([180.]) - rlat = np.array([90.]) - u = np.array([-1.]) - v = np.array([1.]) + rlon = np.array([180.0]) + rlat = np.array([90.0]) + u = np.array([-1.0]) + v = np.array([1.0]) src_proj = ccrs.PlateCarree() - target_proj = ccrs.Stereographic(central_latitude=90, - central_longitude=0) + target_proj = ccrs.Stereographic(central_latitude=90, central_longitude=0) with pytest.warns(UserWarning): warnings.simplefilter('always') ut, vt = target_proj.transform_vectors(src_proj, rlon, rlat, u, v) diff --git a/lib/cartopy/tests/test_geodesic.py b/lib/cartopy/tests/test_geodesic.py index a46ea4f6e..4d92c53e6 100644 --- a/lib/cartopy/tests/test_geodesic.py +++ b/lib/cartopy/tests/test_geodesic.py @@ -25,49 +25,121 @@ def setup_class(self): data = np.array( [ - (0.0000000000, 36.5300423550, 176.1258751622, 5.7623446947, - -48.1642707791, 175.3343083163, 9398502.0434687007), - (0.0000000000, 20.8766024619, 6.9012827094, 163.9792202999, - 64.2764863397, 165.0440144913, 10462971.2273696996), - (0.0000000000, 59.7405712203, 80.9569174535, 80.1969954660, - 30.9857449391, 144.4488137288, 6549489.1863671001), - (0.0000000000, 38.6508883588, 18.3455177945, 23.5931524958, - 66.3457305181, 37.7145989984, 3425212.4767990001), - (0.0000000000, 23.2214345509, 165.5720618611, 148.3625110902, - -68.8453788967, 39.2692310682, 14506511.2971898001), - (0.0000000000, 31.2989275984, 155.7723493796, 93.8764112107, - -69.2776346668, 98.5250397385, 13370814.5013951007), - (0.0000000000, 49.6823298563, 1.0175398481, 5.3554086646, - 83.8681965431, 6.1667605618, 3815028.2543704999), - (0.0000000000, 32.7651878215, 98.6494285944, 70.3527194957, - 2.4777491770, 123.5999412794, 8030520.7178932996), - (0.0000000000, 46.3648067071, 94.9148631993, 56.5676529172, - 25.2581951337, 130.4405565458, 5485075.9286326999), - (0.0000000000, 33.7321188396, 147.9041907517, 33.1346935645, - -26.3211288531, 150.4502346224, 7512675.5414637001), + ( + 0.0000000000, + 36.5300423550, + 176.1258751622, + 5.7623446947, + -48.1642707791, + 175.3343083163, + 9398502.0434687007, + ), + ( + 0.0000000000, + 20.8766024619, + 6.9012827094, + 163.9792202999, + 64.2764863397, + 165.0440144913, + 10462971.2273696996, + ), + ( + 0.0000000000, + 59.7405712203, + 80.9569174535, + 80.1969954660, + 30.9857449391, + 144.4488137288, + 6549489.1863671001, + ), + ( + 0.0000000000, + 38.6508883588, + 18.3455177945, + 23.5931524958, + 66.3457305181, + 37.7145989984, + 3425212.4767990001, + ), + ( + 0.0000000000, + 23.2214345509, + 165.5720618611, + 148.3625110902, + -68.8453788967, + 39.2692310682, + 14506511.2971898001, + ), + ( + 0.0000000000, + 31.2989275984, + 155.7723493796, + 93.8764112107, + -69.2776346668, + 98.5250397385, + 13370814.5013951007, + ), + ( + 0.0000000000, + 49.6823298563, + 1.0175398481, + 5.3554086646, + 83.8681965431, + 6.1667605618, + 3815028.2543704999, + ), + ( + 0.0000000000, + 32.7651878215, + 98.6494285944, + 70.3527194957, + 2.4777491770, + 123.5999412794, + 8030520.7178932996, + ), + ( + 0.0000000000, + 46.3648067071, + 94.9148631993, + 56.5676529172, + 25.2581951337, + 130.4405565458, + 5485075.9286326999, + ), + ( + 0.0000000000, + 33.7321188396, + 147.9041907517, + 33.1346935645, + -26.3211288531, + 150.4502346224, + 7512675.5414637001, + ), ], - dtype=[('start_lon', np.float64), - ('start_lat', np.float64), - ('start_azi', np.float64), - ('end_lon', np.float64), - ('end_lat', np.float64), - ('end_azi', np.float64), - ('dist', np.float64)]) + dtype=[ + ('start_lon', np.float64), + ('start_lat', np.float64), + ('start_azi', np.float64), + ('end_lon', np.float64), + ('end_lat', np.float64), + ('end_azi', np.float64), + ('dist', np.float64), + ], + ) self.data = data.view(np.recarray) - self.start_pts = np.column_stack( - [self.data.start_lon, self.data.start_lat]) - self.end_pts = np.column_stack( - [self.data.end_lon, self.data.end_lat]) + self.start_pts = np.column_stack([self.data.start_lon, self.data.start_lat]) + self.end_pts = np.column_stack([self.data.end_lon, self.data.end_lat]) self.direct_solution = np.column_stack( - [self.data.end_lon, self.data.end_lat, self.data.end_azi]) + [self.data.end_lon, self.data.end_lat, self.data.end_azi] + ) self.inverse_solution = np.column_stack( - [self.data.dist, self.data.start_azi, self.data.end_azi]) + [self.data.dist, self.data.start_azi, self.data.end_azi] + ) def test_direct(self): - geod_dir = self.geod.direct(self.start_pts, self.data.start_azi, - self.data.dist) + geod_dir = self.geod.direct(self.start_pts, self.data.start_azi, self.data.dist) assert_array_almost_equal(geod_dir, self.direct_solution, decimal=5) def test_direct_broadcast(self): @@ -75,10 +147,12 @@ def test_direct_broadcast(self): repeat_start_pts = np.repeat(self.start_pts[0:1, :], 10, axis=0) repeat_results = np.repeat(self.direct_solution[0:1, :], 10, axis=0) - geod_dir1 = self.geod.direct(self.start_pts[0], self.data.start_azi[0], - repeat_dists) - geod_dir2 = self.geod.direct(repeat_start_pts, self.data.start_azi[0], - self.data.dist[0]) + geod_dir1 = self.geod.direct( + self.start_pts[0], self.data.start_azi[0], repeat_dists + ) + geod_dir2 = self.geod.direct( + repeat_start_pts, self.data.start_azi[0], self.data.dist[0] + ) assert_array_almost_equal(geod_dir1, repeat_results, decimal=5) assert_array_almost_equal(geod_dir2, repeat_results, decimal=5) @@ -100,10 +174,17 @@ def test_inverse_broadcast(self): def test_circle(self): geod_circle = self.geod.circle(40, 50, 500000, n_samples=3) - assert_almost_equal(geod_circle, - np.array([[40., 54.49349757], - [34.23766162, 47.60355349], - [45.76233838, 47.60355349]]), decimal=5) + assert_almost_equal( + geod_circle, + np.array( + [ + [40.0, 54.49349757], + [34.23766162, 47.60355349], + [45.76233838, 47.60355349], + ] + ), + decimal=5, + ) def test_str(self): expected = '' @@ -140,8 +221,8 @@ def test_geometry_length_linestring(): def test_geometry_length_multilinestring(): geod = geodesic.Geodesic() geom = sgeom.MultiLineString( - [sgeom.LineString(np.array([lhr, jfk])), - sgeom.LineString(np.array([tul, jfk]))]) + [sgeom.LineString(np.array([lhr, jfk])), sgeom.LineString(np.array([tul, jfk]))] + ) expected = pytest.approx(lhr_to_jfk + jfk_to_tul, abs=1) assert geod.geometry_length(geom) == expected diff --git a/lib/cartopy/tests/test_img_nest.py b/lib/cartopy/tests/test_img_nest.py index d450716f2..118cd871e 100644 --- a/lib/cartopy/tests/test_img_nest.py +++ b/lib/cartopy/tests/test_img_nest.py @@ -24,25 +24,67 @@ #: An integer version which should be increased if the test data needs #: to change in some way. _TEST_DATA_VERSION = 1 -_TEST_DATA_DIR = config["data_dir"] / 'wmts' / 'aerial' - - -@pytest.mark.parametrize('fname, expected', [ - ('one', ['one.w', 'one.W', 'ONE.w', 'ONE.W']), - ('one.png', - ['one.pngw', 'one.pgw', 'one.PNGW', 'one.PGW', 'ONE.pngw', 'ONE.pgw', - 'ONE.PNGW', 'ONE.PGW']), - ('/one.png', - ['/one.pngw', '/one.pgw', '/one.PNGW', '/one.PGW', '/ONE.pngw', - '/ONE.pgw', '/ONE.PNGW', '/ONE.PGW']), - ('/one/two.png', - ['/one/two.pngw', '/one/two.pgw', '/one/two.PNGW', '/one/two.PGW', - '/one/TWO.pngw', '/one/TWO.pgw', '/one/TWO.PNGW', '/one/TWO.PGW']), - ('/one/two/THREE.png', - ['/one/two/THREE.pngw', '/one/two/THREE.pgw', '/one/two/THREE.PNGW', - '/one/two/THREE.PGW', '/one/two/three.pngw', '/one/two/three.pgw', - '/one/two/three.PNGW', '/one/two/three.PGW']), -]) +_TEST_DATA_DIR = config['data_dir'] / 'wmts' / 'aerial' + + +@pytest.mark.parametrize( + 'fname, expected', + [ + ('one', ['one.w', 'one.W', 'ONE.w', 'ONE.W']), + ( + 'one.png', + [ + 'one.pngw', + 'one.pgw', + 'one.PNGW', + 'one.PGW', + 'ONE.pngw', + 'ONE.pgw', + 'ONE.PNGW', + 'ONE.PGW', + ], + ), + ( + '/one.png', + [ + '/one.pngw', + '/one.pgw', + '/one.PNGW', + '/one.PGW', + '/ONE.pngw', + '/ONE.pgw', + '/ONE.PNGW', + '/ONE.PGW', + ], + ), + ( + '/one/two.png', + [ + '/one/two.pngw', + '/one/two.pgw', + '/one/two.PNGW', + '/one/two.PGW', + '/one/TWO.pngw', + '/one/TWO.pgw', + '/one/TWO.PNGW', + '/one/TWO.PGW', + ], + ), + ( + '/one/two/THREE.png', + [ + '/one/two/THREE.pngw', + '/one/two/THREE.pgw', + '/one/two/THREE.PNGW', + '/one/two/THREE.PGW', + '/one/two/three.pngw', + '/one/two/three.pgw', + '/one/two/three.PNGW', + '/one/two/three.PGW', + ], + ), + ], +) def test_world_files(fname, expected): if sys.platform == 'win32': fname = fname.replace('/', '\\') @@ -55,12 +97,14 @@ def test_world_files(fname, expected): def _save_world(fname, args): - _world = ('{x_pix_size}\n' - '{y_rotation}\n' - '{x_rotation}\n' - '{y_pix_size}\n' - '{x_center}\n' - '{y_center}\n') + _world = ( + '{x_pix_size}\n' + '{y_rotation}\n' + '{x_rotation}\n' + '{y_pix_size}\n' + '{x_center}\n' + '{y_center}\n' + ) fname.write_text(_world.format(**args)) @@ -69,8 +113,9 @@ def test_intersect(tmp_path): # File 1: Parent space of all images. z_0_dir = tmp_path / 'z_0' z_0_dir.mkdir() - world = dict(x_pix_size=2, y_rotation=0, x_rotation=0, - y_pix_size=2, x_center=1, y_center=1) + world = dict( + x_pix_size=2, y_rotation=0, x_rotation=0, y_pix_size=2, x_center=1, y_center=1 + ) im = Image.new('RGB', (50, 50)) _save_world(z_0_dir / 'p0.tfw', world) im.save(z_0_dir / 'p0.tif') @@ -79,8 +124,9 @@ def test_intersect(tmp_path): # File 1: complete containment within p0. z_1_dir = tmp_path / 'z_1' z_1_dir.mkdir() - world = dict(x_pix_size=2, y_rotation=0, x_rotation=0, - y_pix_size=2, x_center=21, y_center=21) + world = dict( + x_pix_size=2, y_rotation=0, x_rotation=0, y_pix_size=2, x_center=21, y_center=21 + ) im = Image.new('RGB', (30, 30)) _save_world(z_1_dir / 'p1.tfw', world) im.save(z_1_dir / 'p1.tif') @@ -89,44 +135,45 @@ def test_intersect(tmp_path): # File 1: intersect right edge with p1 left edge. z_2_dir = tmp_path / 'z_2' z_2_dir.mkdir() - world = dict(x_pix_size=2, y_rotation=0, x_rotation=0, - y_pix_size=2, x_center=6, y_center=21) + world = dict( + x_pix_size=2, y_rotation=0, x_rotation=0, y_pix_size=2, x_center=6, y_center=21 + ) im = Image.new('RGB', (5, 5)) _save_world(z_2_dir / 'p2-1.tfw', world) im.save(z_2_dir / 'p2-1.tif') # File 2: intersect upper right corner with p1 # lower left corner. - world = dict(x_pix_size=2, y_rotation=0, x_rotation=0, - y_pix_size=2, x_center=6, y_center=6) + world = dict( + x_pix_size=2, y_rotation=0, x_rotation=0, y_pix_size=2, x_center=6, y_center=6 + ) im = Image.new('RGB', (5, 5)) _save_world(z_2_dir / 'p2-2.tfw', world) im.save(z_2_dir / 'p2-2.tif') # File 3: complete containment within p1. - world = dict(x_pix_size=2, y_rotation=0, x_rotation=0, - y_pix_size=2, x_center=41, y_center=41) + world = dict( + x_pix_size=2, y_rotation=0, x_rotation=0, y_pix_size=2, x_center=41, y_center=41 + ) im = Image.new('RGB', (5, 5)) _save_world(z_2_dir / 'p2-3.tfw', world) im.save(z_2_dir / 'p2-3.tif') # File 4: overlap with p1 right edge. - world = dict(x_pix_size=2, y_rotation=0, x_rotation=0, - y_pix_size=2, x_center=76, y_center=61) + world = dict( + x_pix_size=2, y_rotation=0, x_rotation=0, y_pix_size=2, x_center=76, y_center=61 + ) im = Image.new('RGB', (5, 5)) _save_world(z_2_dir / 'p2-4.tfw', world) im.save(z_2_dir / 'p2-4.tif') # File 5: overlap with p1 bottom right corner. - world = dict(x_pix_size=2, y_rotation=0, x_rotation=0, - y_pix_size=2, x_center=76, y_center=76) + world = dict( + x_pix_size=2, y_rotation=0, x_rotation=0, y_pix_size=2, x_center=76, y_center=76 + ) im = Image.new('RGB', (5, 5)) _save_world(z_2_dir / 'p2-5.tfw', world) im.save(z_2_dir / 'p2-5.tif') # Provided in reverse order in order to test the area sorting. - items = [('dummy-z-2', z_2_dir), - ('dummy-z-1', z_1_dir), - ('dummy-z-0', z_0_dir)] - nic = cimg_nest.NestedImageCollection.from_configuration('dummy', - None, - items) + items = [('dummy-z-2', z_2_dir), ('dummy-z-1', z_1_dir), ('dummy-z-0', z_0_dir)] + nic = cimg_nest.NestedImageCollection.from_configuration('dummy', None, items) names = [collection.name for collection in nic._collections] zoom_levels = ['dummy-z-0', 'dummy-z-1', 'dummy-z-2'] @@ -141,8 +188,10 @@ def test_intersect(tmp_path): zoom_levels = ['dummy-z-0', 'dummy-z-1'] assert sorted(k[0] for k in nic._ancestry.keys()) == zoom_levels - expected = [('dummy-z-0', ['p1.tif']), - ('dummy-z-1', ['p2-3.tif', 'p2-4.tif', 'p2-5.tif'])] + expected = [ + ('dummy-z-0', ['p1.tif']), + ('dummy-z-1', ['p2-3.tif', 'p2-4.tif', 'p2-5.tif']), + ] for zoom, image_names in expected: key = [k for k in nic._ancestry.keys() if k[0] == zoom][0] ancestry = nic._ancestry[key] @@ -150,9 +199,11 @@ def test_intersect(tmp_path): assert image_names == fnames # Check image retrieval for specific domain. - items = [(sgeom.box(20, 20, 80, 80), 3), - (sgeom.box(20, 20, 75, 75), 1), - (sgeom.box(40, 40, 85, 85), 3)] + items = [ + (sgeom.box(20, 20, 80, 80), 3), + (sgeom.box(20, 20, 75, 75), 1), + (sgeom.box(40, 40, 85, 85), 3), + ] for domain, expected in items: result = [image for image in nic.find_images(domain, 'dummy-z-2')] assert len(result) == expected @@ -190,16 +241,19 @@ def world_file_extent(*args, **kwargs): def test_nest(nest_from_config): crs = cimgt.GoogleTiles().crs z0 = cimg_nest.ImageCollection('aerial z0 test', crs) - z0.scan_dir_for_imgs(_TEST_DATA_DIR / 'z_0', - glob_pattern='*.png', img_class=RoundedImg) + z0.scan_dir_for_imgs( + _TEST_DATA_DIR / 'z_0', glob_pattern='*.png', img_class=RoundedImg + ) z1 = cimg_nest.ImageCollection('aerial z1 test', crs) - z1.scan_dir_for_imgs(_TEST_DATA_DIR / 'z_1', - glob_pattern='*.png', img_class=RoundedImg) + z1.scan_dir_for_imgs( + _TEST_DATA_DIR / 'z_1', glob_pattern='*.png', img_class=RoundedImg + ) z2 = cimg_nest.ImageCollection('aerial z2 test', crs) - z2.scan_dir_for_imgs(_TEST_DATA_DIR / 'z_2', - glob_pattern='*.png', img_class=RoundedImg) + z2.scan_dir_for_imgs( + _TEST_DATA_DIR / 'z_2', glob_pattern='*.png', img_class=RoundedImg + ) # make sure all the images from z1 are contained by the z0 image. The # only reason this might occur is if the tfw files are handling @@ -210,10 +264,9 @@ def test_nest(nest_from_config): 'The test images aren\'t all "contained" by the z0 images, ' 'the nest cannot possibly work.\n' f'img {img!s} not contained by {z0.images[0]!s}\n' - f'Extents: {img.extent!s}; {z0.images[0].extent!s}') - nest_z0_z1 = cimg_nest.NestedImageCollection('aerial test', - crs, - [z0, z1]) + f'Extents: {img.extent!s}; {z0.images[0].extent!s}' + ) + nest_z0_z1 = cimg_nest.NestedImageCollection('aerial test', crs, [z0, z1]) nest = cimg_nest.NestedImageCollection('aerial test', crs, [z0, z1, z2]) @@ -228,14 +281,15 @@ def test_nest(nest_from_config): key = ('aerial z0 test', z0.images[0]) assert ('aerial z1 test', img) in nest_z0_z1._ancestry[key] - x1_y0_z1, = (img for img in z1.images - if img.filename.name.endswith('x_1_y_0.png')) + (x1_y0_z1,) = ( + img for img in z1.images if img.filename.name.endswith('x_1_y_0.png') + ) assert (1, 0, 1) == _tile_from_img(x1_y0_z1) - assert ([(2, 0, 2), (2, 1, 2), (3, 0, 2), (3, 1, 2)] == - sorted(_tile_from_img(img) for z, img in - nest.subtiles(('aerial z1 test', x1_y0_z1)))) + assert [(2, 0, 2), (2, 1, 2), (3, 0, 2), (3, 1, 2)] == sorted( + _tile_from_img(img) for z, img in nest.subtiles(('aerial z1 test', x1_y0_z1)) + ) # check that the images in the nest from configuration are the # same as those created by hand. @@ -293,8 +347,9 @@ def wmts_data(): pass finally: if test_data_version != _TEST_DATA_VERSION: - warnings.warn('WMTS test data is out of date, regenerating at ' - f'{_TEST_DATA_DIR}.') + warnings.warn( + f'WMTS test data is out of date, regenerating at {_TEST_DATA_DIR}.' + ) shutil.rmtree(_TEST_DATA_DIR) _TEST_DATA_DIR.mkdir(parents=True) data_version_fname.write_text(str(_TEST_DATA_VERSION)) @@ -314,17 +369,17 @@ def wmts_data(): pix_size_x = x_rng / nx pix_size_y = y_rng / ny - upper_left_center = (extent[0] + pix_size_x / 2, - extent[2] + pix_size_y / 2) + upper_left_center = (extent[0] + pix_size_x / 2, extent[2] + pix_size_y / 2) pgw_fname = fname.with_suffix('.pgw') - pgw_keys = {'x_pix_size': np.float64(pix_size_x), - 'y_rotation': 0, - 'x_rotation': 0, - 'y_pix_size': np.float64(pix_size_y), - 'x_center': np.float64(upper_left_center[0]), - 'y_center': np.float64(upper_left_center[1]), - } + pgw_keys = { + 'x_pix_size': np.float64(pix_size_x), + 'y_rotation': 0, + 'x_rotation': 0, + 'y_pix_size': np.float64(pix_size_y), + 'x_center': np.float64(upper_left_center[0]), + 'y_center': np.float64(upper_left_center[1]), + } _save_world(pgw_fname, pgw_keys) img.save(fname) @@ -337,10 +392,11 @@ def test_find_images(wmts_data): img = RoundedImg.from_world_file(img_fname, world_file_fname) assert img.filename == img_fname - assert_array_almost_equal(img.extent, - (0., 10018754.17139462, - 10018754.17139462, 20037508.342789244), - decimal=4) + assert_array_almost_equal( + img.extent, + (0.0, 10018754.17139462, 10018754.17139462, 20037508.342789244), + decimal=4, + ) assert img.origin == 'lower' assert_array_equal(img, np.array(Image.open(img.filename))) assert img.pixel_size == (39135.7585, 39135.7585) @@ -350,13 +406,14 @@ def test_find_images(wmts_data): def nest_from_config(wmts_data): from_config = cimg_nest.NestedImageCollection.from_configuration - files = [['aerial z0 test', _TEST_DATA_DIR / 'z_0'], - ['aerial z1 test', _TEST_DATA_DIR / 'z_1'], - ] + files = [ + ['aerial z0 test', _TEST_DATA_DIR / 'z_0'], + ['aerial z1 test', _TEST_DATA_DIR / 'z_1'], + ] crs = cimgt.GoogleTiles().crs - nest_z0_z1 = from_config('aerial test', - crs, files, glob_pattern='*.png', - img_class=RoundedImg) + nest_z0_z1 = from_config( + 'aerial test', crs, files, glob_pattern='*.png', img_class=RoundedImg + ) return nest_z0_z1 diff --git a/lib/cartopy/tests/test_img_tiles.py b/lib/cartopy/tests/test_img_tiles.py index d92224ee7..86232082b 100644 --- a/lib/cartopy/tests/test_img_tiles.py +++ b/lib/cartopy/tests/test_img_tiles.py @@ -23,30 +23,33 @@ #: Maps Google tile coordinates to native mercator coordinates as defined #: by https://goo.gl/pgJi. -KNOWN_EXTENTS = {(0, 0, 0): (-20037508.342789244, 20037508.342789244, - -20037508.342789244, 20037508.342789244), - (2, 0, 2): (0., 10018754.17139462, - 10018754.17139462, 20037508.342789244), - (0, 2, 2): (-20037508.342789244, -10018754.171394622, - -10018754.171394622, 0), - (2, 2, 2): (0, 10018754.17139462, - -10018754.171394622, 0), - (8, 9, 4): (0, 2504688.542848654, - -5009377.085697312, -2504688.542848654), - } +KNOWN_EXTENTS = { + (0, 0, 0): ( + -20037508.342789244, + 20037508.342789244, + -20037508.342789244, + 20037508.342789244, + ), + (2, 0, 2): (0.0, 10018754.17139462, 10018754.17139462, 20037508.342789244), + (0, 2, 2): (-20037508.342789244, -10018754.171394622, -10018754.171394622, 0), + (2, 2, 2): (0, 10018754.17139462, -10018754.171394622, 0), + (8, 9, 4): (0, 2504688.542848654, -5009377.085697312, -2504688.542848654), +} def GOOGLE_IMAGE_URL_REPLACEMENT(self, tile): # TODO: This is a hack to replace the Google image URL with a static image. # This service has been deprecated by Google, so we need to replace it # See https://developers.google.com/chart/image for the notice - pytest.xfail(reason="Google has deprecated the tile API used in this test") + pytest.xfail(reason='Google has deprecated the tile API used in this test') x, y, z = tile - return (f'https://chart.googleapis.com/chart?chst=d_text_outline&' - f'chs=256x256&chf=bg,s,00000055&chld=FFFFFF|16|h|000000|b||||' - f'Google:%20%20({x},{y})|Zoom%20{z}||||||' - f'____________________________') + return ( + f'https://chart.googleapis.com/chart?chst=d_text_outline&' + f'chs=256x256&chf=bg,s,00000055&chld=FFFFFF|16|h|000000|b||||' + f'Google:%20%20({x},{y})|Zoom%20{z}||||||' + f'____________________________' + ) def test_google_tile_styles(): @@ -56,38 +59,40 @@ def test_google_tile_styles(): This is essentially just assures information is properly propagated through the class structure. """ - reference_url = ("https://mts0.google.com/vt/lyrs={style}@177000000&hl=en" - "&src=api&x=1&y=2&z=3&s=G") - tile = ["1", "2", "3"] + reference_url = ( + 'https://mts0.google.com/vt/lyrs={style}@177000000&hl=en' + '&src=api&x=1&y=2&z=3&s=G' + ) + tile = ['1', '2', '3'] # Default is street. gt = cimgt.GoogleTiles() url = gt._image_url(tile) - assert reference_url.format(style="m") == url + assert reference_url.format(style='m') == url # Street - gt = cimgt.GoogleTiles(style="street") + gt = cimgt.GoogleTiles(style='street') url = gt._image_url(tile) - assert reference_url.format(style="m") == url + assert reference_url.format(style='m') == url # Satellite - gt = cimgt.GoogleTiles(style="satellite") + gt = cimgt.GoogleTiles(style='satellite') url = gt._image_url(tile) - assert reference_url.format(style="s") == url + assert reference_url.format(style='s') == url # Terrain - gt = cimgt.GoogleTiles(style="terrain") + gt = cimgt.GoogleTiles(style='terrain') url = gt._image_url(tile) - assert reference_url.format(style="t") == url + assert reference_url.format(style='t') == url # Streets only - gt = cimgt.GoogleTiles(style="only_streets") + gt = cimgt.GoogleTiles(style='only_streets') url = gt._image_url(tile) - assert reference_url.format(style="h") == url + assert reference_url.format(style='h') == url # Exception is raised if unknown style is passed. with pytest.raises(ValueError): - cimgt.GoogleTiles(style="random_style") + cimgt.GoogleTiles(style='random_style') def test_google_wts(): @@ -99,15 +104,11 @@ def test_google_wts(): with pytest.raises(AssertionError): list(gt.find_images(target_domain, -1)) - assert (tuple(gt.find_images(target_domain, 0)) == - ((0, 0, 0),)) - assert (tuple(gt.find_images(target_domain, 2)) == - ((1, 1, 2), (2, 1, 2))) + assert tuple(gt.find_images(target_domain, 0)) == ((0, 0, 0),) + assert tuple(gt.find_images(target_domain, 2)) == ((1, 1, 2), (2, 1, 2)) - assert (list(gt.subtiles((0, 0, 0))) == - [(0, 0, 1), (0, 1, 1), (1, 0, 1), (1, 1, 1)]) - assert (list(gt.subtiles((1, 0, 1))) == - [(2, 0, 2), (2, 1, 2), (3, 0, 2), (3, 1, 2)]) + assert list(gt.subtiles((0, 0, 0))) == [(0, 0, 1), (0, 1, 1), (1, 0, 1), (1, 1, 1)] + assert list(gt.subtiles((1, 0, 1))) == [(2, 0, 2), (2, 1, 2), (3, 0, 2), (3, 1, 2)] with pytest.raises(AssertionError): gt.tileextent((0, 1, 0)) @@ -123,8 +124,10 @@ def test_tile_bbox_y0_at_south_pole(): tms = cimgt.MapQuestOpenAerial() # Check the y0_at_north_pole keywords returns the appropriate bounds. - assert_arr_almost(tms.tile_bbox(8, 6, 4, y0_at_north_pole=False), - np.array(KNOWN_EXTENTS[(8, 9, 4)]).reshape([2, 2])) + assert_arr_almost( + tms.tile_bbox(8, 6, 4, y0_at_north_pole=False), + np.array(KNOWN_EXTENTS[(8, 9, 4)]).reshape([2, 2]), + ) def test_tile_find_images(): @@ -134,8 +137,12 @@ def test_tile_find_images(): multi_poly = gt.crs.project_geometry(ll_target_domain, ccrs.PlateCarree()) target_domain = multi_poly.geoms[0] - assert (list(gt.find_images(target_domain, 4)) == - [(7, 4, 4), (7, 5, 4), (8, 4, 4), (8, 5, 4)]) + assert list(gt.find_images(target_domain, 4)) == [ + (7, 4, 4), + (7, 5, 4), + (8, 4, 4), + (8, 5, 4), + ] @pytest.mark.network @@ -149,12 +156,10 @@ def test_image_for_domain(): _, extent, _ = gt.image_for_domain(target_domain, 6) - ll_extent = ccrs.Geodetic().transform_points(gt.crs, - np.array(extent[:2]), - np.array(extent[2:])) - assert_arr_almost(ll_extent[:, :2], - [[-11.25, 48.92249926], - [11.25, 61.60639637]]) + ll_extent = ccrs.Geodetic().transform_points( + gt.crs, np.array(extent[:2]), np.array(extent[2:]) + ) + assert_arr_almost(ll_extent[:, :2], [[-11.25, 48.92249926], [11.25, 61.60639637]]) def test_quadtree_wts(): @@ -183,24 +188,33 @@ def test_quadtree_wts(): qt.tileextent('4') assert_arr_almost(qt.tileextent(''), KNOWN_EXTENTS[(0, 0, 0)]) - assert_arr_almost(qt.tileextent(qt.tms_to_quadkey((2, 0, 2), google=True)), - KNOWN_EXTENTS[(2, 0, 2)]) - assert_arr_almost(qt.tileextent(qt.tms_to_quadkey((0, 2, 2), google=True)), - KNOWN_EXTENTS[(0, 2, 2)]) - assert_arr_almost(qt.tileextent(qt.tms_to_quadkey((2, 0, 2), google=True)), - KNOWN_EXTENTS[(2, 0, 2)]) - assert_arr_almost(qt.tileextent(qt.tms_to_quadkey((2, 2, 2), google=True)), - KNOWN_EXTENTS[(2, 2, 2)]) - assert_arr_almost(qt.tileextent(qt.tms_to_quadkey((8, 9, 4), google=True)), - KNOWN_EXTENTS[(8, 9, 4)]) + assert_arr_almost( + qt.tileextent(qt.tms_to_quadkey((2, 0, 2), google=True)), + KNOWN_EXTENTS[(2, 0, 2)], + ) + assert_arr_almost( + qt.tileextent(qt.tms_to_quadkey((0, 2, 2), google=True)), + KNOWN_EXTENTS[(0, 2, 2)], + ) + assert_arr_almost( + qt.tileextent(qt.tms_to_quadkey((2, 0, 2), google=True)), + KNOWN_EXTENTS[(2, 0, 2)], + ) + assert_arr_almost( + qt.tileextent(qt.tms_to_quadkey((2, 2, 2), google=True)), + KNOWN_EXTENTS[(2, 2, 2)], + ) + assert_arr_almost( + qt.tileextent(qt.tms_to_quadkey((8, 9, 4), google=True)), + KNOWN_EXTENTS[(8, 9, 4)], + ) def test_mapbox_tiles_api_url(): token = 'foo' map_name = 'bar' tile = [0, 1, 2] - exp_url = ('https://api.mapbox.com/styles/v1/mapbox/bar/tiles' - '/2/0/1?access_token=foo') + exp_url = 'https://api.mapbox.com/styles/v1/mapbox/bar/tiles/2/0/1?access_token=foo' mapbox_sample = cimgt.MapboxTiles(token, map_name) url_str = mapbox_sample._image_url(tile) @@ -212,25 +226,31 @@ def test_mapbox_style_tiles_api_url(): username = 'baz' map_id = 'bar' tile = [0, 1, 2] - exp_url = ('https://api.mapbox.com/styles/v1/' - 'baz/bar/tiles/256/2/0/1' - '?access_token=foo') + exp_url = ( + 'https://api.mapbox.com/styles/v1/baz/bar/tiles/256/2/0/1?access_token=foo' + ) mapbox_sample = cimgt.MapboxStyleTiles(token, username, map_id) url_str = mapbox_sample._image_url(tile) assert url_str == exp_url -@pytest.mark.parametrize("style,extension,resolution", [ - ("alidade_smooth", "png", ""), - ("alidade_smooth", "png", "@2x"), - ("stamen_watercolor", "jpg", "")]) +@pytest.mark.parametrize( + 'style,extension,resolution', + [ + ('alidade_smooth', 'png', ''), + ('alidade_smooth', 'png', '@2x'), + ('stamen_watercolor', 'jpg', ''), + ], +) def test_stadia_maps_tiles_api_url(style, extension, resolution): apikey = 'foo' tile = [0, 1, 2] - exp_url = ('http://tiles.stadiamaps.com/tiles/' - f'{style}/2/0/1{resolution}.{extension}' - '?api_key=foo') + exp_url = ( + 'http://tiles.stadiamaps.com/tiles/' + f'{style}/2/0/1{resolution}.{extension}' + '?api_key=foo' + ) sample = cimgt.StadiaMapsTiles(apikey, style=style, resolution=resolution) url_str = sample._image_url(tile) @@ -244,29 +264,32 @@ def test_ordnance_survey_tile_styles(): This is essentially just assures information is properly propagated through the class structure. """ - dummy_apikey = "None" + dummy_apikey = 'None' - ref_url = "https://api.os.uk/maps/raster/v1/zxy/" \ - "{layer}/{z}/{x}/{y}.png?key=None" - tile = ["1", "2", "3"] + ref_url = 'https://api.os.uk/maps/raster/v1/zxy/{layer}/{z}/{x}/{y}.png?key=None' + tile = ['1', '2', '3'] # Default is Road_3857. ordsurvey = cimgt.OrdnanceSurvey(dummy_apikey) url = ordsurvey._image_url(tile) - assert url == ref_url.format(layer="Road_3857", - z=tile[2], y=tile[1], x=tile[0]) - - for layer in ("Road_3857", "Light_3857", "Outdoor_3857", - "Road", "Light", "Outdoor"): + assert url == ref_url.format(layer='Road_3857', z=tile[2], y=tile[1], x=tile[0]) + + for layer in ( + 'Road_3857', + 'Light_3857', + 'Outdoor_3857', + 'Road', + 'Light', + 'Outdoor', + ): ordsurvey = cimgt.OrdnanceSurvey(dummy_apikey, layer=layer) url = ordsurvey._image_url(tile) - layer = layer if layer.endswith("_3857") else layer + "_3857" - assert url == ref_url.format(layer=layer, - z=tile[2], y=tile[1], x=tile[0]) + layer = layer if layer.endswith('_3857') else layer + '_3857' + assert url == ref_url.format(layer=layer, z=tile[2], y=tile[1], x=tile[0]) # Exception is raised if unknown style is passed. with pytest.raises(ValueError): - cimgt.OrdnanceSurvey(dummy_apikey, layer="random_style") + cimgt.OrdnanceSurvey(dummy_apikey, layer='random_style') @pytest.mark.network @@ -278,8 +301,8 @@ def test_ordnance_survey_get_image(): except KeyError: pytest.skip('ORDNANCE_SURVEY_API_KEY environment variable is unset.') - os1 = cimgt.OrdnanceSurvey(api_key, layer="Outdoor_3857") - os2 = cimgt.OrdnanceSurvey(api_key, layer="Light_3857") + os1 = cimgt.OrdnanceSurvey(api_key, layer='Outdoor_3857') + os2 = cimgt.OrdnanceSurvey(api_key, layer='Light_3857') tile = (500, 300, 10) @@ -298,8 +321,10 @@ def test_azuremaps_tiles_api_url(): tileset_id = 'bar' tile = [0, 1, 2] - exp_url = ('https://atlas.microsoft.com/map/tile?api-version=2.0' - '&tilesetId=bar&x=0&y=1&zoom=2&subscription-key=foo') + exp_url = ( + 'https://atlas.microsoft.com/map/tile?api-version=2.0' + '&tilesetId=bar&x=0&y=1&zoom=2&subscription-key=foo' + ) az_maps_sample = cimgt.AzureMapsTiles(subscription_key, tileset_id) url_str = az_maps_sample._image_url(tile) @@ -313,11 +338,10 @@ def test_azuremaps_get_image(): try: api_key = os.environ['AZURE_MAPS_SUBSCRIPTION_KEY'] except KeyError: - pytest.skip('AZURE_MAPS_SUBSCRIPTION_KEY environment variable ' - 'is unset.') + pytest.skip('AZURE_MAPS_SUBSCRIPTION_KEY environment variable is unset.') - am1 = cimgt.AzureMapsTiles(api_key, tileset_id="microsoft.imagery") - am2 = cimgt.AzureMapsTiles(api_key, tileset_id="microsoft.base.road") + am1 = cimgt.AzureMapsTiles(api_key, tileset_id='microsoft.imagery') + am2 = cimgt.AzureMapsTiles(api_key, tileset_id='microsoft.base.road') tile = (500, 300, 10) @@ -332,21 +356,21 @@ def test_azuremaps_get_image(): @pytest.mark.network -@pytest.mark.parametrize('cache_dir', ["tmpdir", True, False]) +@pytest.mark.parametrize('cache_dir', ['tmpdir', True, False]) @pytest.mark.skipif(not ogc._OWSLIB_AVAILABLE, reason='OWSLib is unavailable.') def test_wmts_cache(cache_dir, tmp_path): - if cache_dir == "tmpdir": + if cache_dir == 'tmpdir': tmpdir_str = str(tmp_path) else: tmpdir_str = cache_dir if cache_dir is True: - config["cache_dir"] = str(tmp_path) + config['cache_dir'] = str(tmp_path) # URI = 'https://map1c.vis.earthdata.nasa.gov/wmts-geo/wmts.cgi' # layer_name = 'VIIRS_CityLights_2012' URI = 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryOnly/MapServer/WMTS/1.0.0/WMTSCapabilities.xml' - layer_name='USGSImageryOnly' + layer_name = 'USGSImageryOnly' projection = ccrs.PlateCarree() # Fetch tiles and save them in the cache @@ -354,8 +378,7 @@ def test_wmts_cache(cache_dir, tmp_path): warnings.simplefilter('always') source = ogc.WMTSRasterSource(URI, layer_name, cache=tmpdir_str) extent = [-10, 10, 40, 60] - located_image, = source.fetch_raster(projection, extent, - RESOLUTION) + (located_image,) = source.fetch_raster(projection, extent, RESOLUTION) # Do not check the result if the cache is disabled if cache_dir is False: @@ -375,21 +398,16 @@ def test_wmts_cache(cache_dir, tmp_path): ] # Check the results - cache_dir_res = source.cache_path / "WMTSRasterSource" + cache_dir_res = source.cache_path / 'WMTSRasterSource' files = list(cache_dir_res.iterdir()) hashes = { - f: - hashlib.md5( - np.load(cache_dir_res / f, allow_pickle=True).data - ).hexdigest() + f: hashlib.md5(np.load(cache_dir_res / f, allow_pickle=True).data).hexdigest() for f in files } assert sorted(files) == [cache_dir_res / f for x, y, f, h in x_y_f_h] assert set(files) == set([cache_dir_res / c for c in source.cache]) - assert sorted(hashes.values()) == sorted( - h for x, y, f, h in x_y_f_h - ) + assert sorted(hashes.values()) == sorted(h for x, y, f, h in x_y_f_h) # Update images in cache (all white) for f in files: @@ -399,8 +417,7 @@ def test_wmts_cache(cache_dir, tmp_path): np.save(filename, img, allow_pickle=True) wmts_cache = ogc.WMTSRasterSource(URI, layer_name, cache=tmpdir_str) - located_image_cache, = wmts_cache.fetch_raster(projection, extent, - RESOLUTION) + (located_image_cache,) = wmts_cache.fetch_raster(projection, extent, RESOLUTION) # Check that the new fetch_raster() call used cached images assert wmts_cache.cache == set([cache_dir_res / c for c in source.cache]) @@ -408,15 +425,15 @@ def test_wmts_cache(cache_dir, tmp_path): @pytest.mark.network -@pytest.mark.parametrize('cache_dir', ["tmpdir", True, False]) +@pytest.mark.parametrize('cache_dir', ['tmpdir', True, False]) def test_cache(cache_dir, tmp_path): - if cache_dir == "tmpdir": + if cache_dir == 'tmpdir': tmpdir_str = str(tmp_path) else: tmpdir_str = cache_dir if cache_dir is True: - config["cache_dir"] = str(tmp_path) + config['cache_dir'] = str(tmp_path) # Fetch tiles and save them in the cache with warnings.catch_warnings(record=True) as w: @@ -458,25 +475,20 @@ def test_cache(cache_dir, tmp_path): (33, 18, 6, '33_18_6.npy', '4e51e32da73fb99229817dcd7b7e1f4f'), (33, 19, 6, '33_19_6.npy', 'b9b5057fa012c5788cbbe1e18c9bb512'), (33, 20, 6, '33_20_6.npy', 'b55a7c0a8d86167df496732f85bddcf9'), - (33, 21, 6, '33_21_6.npy', '4208ba897c460e9bb0d2469552e127ff') + (33, 21, 6, '33_21_6.npy', '4208ba897c460e9bb0d2469552e127ff'), ] # Check the results - cache_dir_res = gt.cache_path / "GoogleTiles" + cache_dir_res = gt.cache_path / 'GoogleTiles' files = list(cache_dir_res.iterdir()) hashes = { - f: - hashlib.md5( - np.load(cache_dir_res / f, allow_pickle=True).data - ).hexdigest() + f: hashlib.md5(np.load(cache_dir_res / f, allow_pickle=True).data).hexdigest() for f in files } assert sorted(files) == [cache_dir_res / f for x, y, z, f, h in x_y_z_f_h] assert set(files) == gt.cache - assert sorted(hashes.values()) == sorted( - h for x, y, z, f, h in x_y_z_f_h - ) + assert sorted(hashes.values()) == sorted(h for x, y, z, f, h in x_y_z_f_h) # Update images in cache (all white) for f in files: @@ -486,8 +498,7 @@ def test_cache(cache_dir, tmp_path): np.save(filename, img, allow_pickle=True) gt_cache = cimgt.GoogleTiles(cache=tmpdir_str) - gt_cache._image_url = types.MethodType( - GOOGLE_IMAGE_URL_REPLACEMENT, gt_cache) + gt_cache._image_url = types.MethodType(GOOGLE_IMAGE_URL_REPLACEMENT, gt_cache) img_cache, _, _ = gt_cache.image_for_domain(target_domain, 6) # Check that the new image_for_domain() call used cached images diff --git a/lib/cartopy/tests/test_img_transform.py b/lib/cartopy/tests/test_img_transform.py index 0a008386a..84a43f6a7 100644 --- a/lib/cartopy/tests/test_img_transform.py +++ b/lib/cartopy/tests/test_img_transform.py @@ -15,28 +15,45 @@ if not _HAS_PYKDTREE_OR_SCIPY: - pytest.skip("pykdtree or scipy are required", allow_module_level=True) + pytest.skip('pykdtree or scipy are required', allow_module_level=True) import cartopy.crs as ccrs import cartopy.img_transform as img_trans -@pytest.mark.parametrize('xmin, xmax', [ - (-90, 0), (-90, 90), (-90, None), - (0, 90), (0, None), - (None, 0), (None, 90), (None, None)]) -@pytest.mark.parametrize('ymin, ymax', [ - (-45, 0), (-45, 45), (-45, None), - (0, 45), (0, None), - (None, 0), (None, 45), (None, None)]) +@pytest.mark.parametrize( + 'xmin, xmax', + [ + (-90, 0), + (-90, 90), + (-90, None), + (0, 90), + (0, None), + (None, 0), + (None, 90), + (None, None), + ], +) +@pytest.mark.parametrize( + 'ymin, ymax', + [ + (-45, 0), + (-45, 45), + (-45, None), + (0, 45), + (0, None), + (None, 0), + (None, 45), + (None, None), + ], +) def test_mesh_projection_extent(xmin, xmax, ymin, ymax): proj = ccrs.PlateCarree() nx = 4 ny = 2 target_x, target_y, extent = img_trans.mesh_projection( - proj, nx, ny, - x_extents=(xmin, xmax), - y_extents=(ymin, ymax)) + proj, nx, ny, x_extents=(xmin, xmax), y_extents=(ymin, ymax) + ) if xmin is None: xmin = proj.x_limits[0] @@ -63,21 +80,36 @@ def test_gridding_data_std_range(): target_x, target_y, extent = img_trans.mesh_projection(target_prj, 8, 4) - image = img_trans.regrid(data, lons, lats, data_trans, target_prj, - target_x, target_y, - mask_extrapolated=True) + image = img_trans.regrid( + data, + lons, + lats, + data_trans, + target_prj, + target_x, + target_y, + mask_extrapolated=True, + ) # The expected image. n.b. on a map the data is reversed in the y axis. - expected = np.array([[1, 1, 2, 2, 3, 3, 3, 3], - [1, 1, 2, 2, 2, 3, 3, 3], - [1, 1, 1, 2, 2, 2, 3, 3], - [1, 1, 1, 2, 2, 2, 3, 3]], dtype=np.float64) + expected = np.array( + [ + [1, 1, 2, 2, 3, 3, 3, 3], + [1, 1, 2, 2, 2, 3, 3, 3], + [1, 1, 1, 2, 2, 2, 3, 3], + [1, 1, 1, 2, 2, 2, 3, 3], + ], + dtype=np.float64, + ) expected_mask = np.array( - [[True, False, False, False, False, False, False, True], - [True, False, False, False, False, False, False, True], - [True, False, False, False, False, False, False, True], - [True, False, False, False, False, False, False, True]]) + [ + [True, False, False, False, False, False, False, True], + [True, False, False, False, False, False, False, True], + [True, False, False, False, False, False, False, True], + [True, False, False, False, False, False, False, True], + ] + ) assert_array_equal([-180, 180, -90, 90], extent) assert_array_equal(expected, image) @@ -96,32 +128,50 @@ def test_gridding_data_outside_projection(): target_x, target_y, extent = img_trans.mesh_projection(target_prj, 8, 4) - image = img_trans.regrid(data, lons, lats, data_trans, target_prj, - target_x, target_y, - mask_extrapolated=True) + image = img_trans.regrid( + data, + lons, + lats, + data_trans, + target_prj, + target_x, + target_y, + mask_extrapolated=True, + ) # The expected image. n.b. on a map the data is reversed in the y axis. expected = np.array( - [[3, 3, 3, 3, 3, 2, 2, 2], - [3, 3, 3, 3, 1, 1, 2, 2], - [3, 3, 3, 3, 1, 1, 1, 2], - [3, 3, 3, 1, 1, 1, 1, 1]], dtype=np.float64) + [ + [3, 3, 3, 3, 3, 2, 2, 2], + [3, 3, 3, 3, 1, 1, 2, 2], + [3, 3, 3, 3, 1, 1, 1, 2], + [3, 3, 3, 1, 1, 1, 1, 1], + ], + dtype=np.float64, + ) expected_mask = np.array( - [[False, False, True, True, True, True, False, False], - [False, False, True, True, True, True, False, False], - [False, False, True, True, True, True, False, False], - [False, False, True, True, True, True, False, False]]) + [ + [False, False, True, True, True, True, False, False], + [False, False, True, True, True, True, False, False], + [False, False, True, True, True, True, False, False], + [False, False, True, True, True, True, False, False], + ] + ) assert_array_equal([-180, 180, -90, 90], extent) assert_array_equal(expected, image) assert_array_equal(expected_mask, image.mask) -@pytest.mark.parametrize("target_prj", - (ccrs.Mollweide(), ccrs.Orthographic())) -@pytest.mark.parametrize("use_scipy", (pytest.param(True, marks=requires_scipy), - pytest.param(False, marks=requires_pykdtree))) +@pytest.mark.parametrize('target_prj', (ccrs.Mollweide(), ccrs.Orthographic())) +@pytest.mark.parametrize( + 'use_scipy', + ( + pytest.param(True, marks=requires_scipy), + pytest.param(False, marks=requires_pykdtree), + ), +) def test_regridding_with_invalid_extent(target_prj, use_scipy, monkeypatch): # tests that when a valid extent results in invalid points in the # transformed coordinates, the regridding does not error. @@ -135,8 +185,8 @@ def test_regridding_with_invalid_extent(target_prj, use_scipy, monkeypatch): target_x, target_y, extent = img_trans.mesh_projection(target_prj, 8, 4) if use_scipy: - monkeypatch.setattr(img_trans, "_is_pykdtree", False) + monkeypatch.setattr(img_trans, '_is_pykdtree', False) import scipy.spatial - monkeypatch.setattr(img_trans, "_kdtreeClass", scipy.spatial.cKDTree) - _ = img_trans.regrid(data, lons, lats, data_trans, target_prj, - target_x, target_y) + + monkeypatch.setattr(img_trans, '_kdtreeClass', scipy.spatial.cKDTree) + _ = img_trans.regrid(data, lons, lats, data_trans, target_prj, target_x, target_y) diff --git a/lib/cartopy/tests/test_line_string.py b/lib/cartopy/tests/test_line_string.py index 926dd5b5b..12bd0ace5 100644 --- a/lib/cartopy/tests/test_line_string.py +++ b/lib/cartopy/tests/test_line_string.py @@ -33,8 +33,9 @@ def test_out_of_bounds(self): expected = 0 else: expected = 1 - assert len(multi_line_string.geoms) == expected, \ + assert len(multi_line_string.geoms) == expected, ( f'Unexpected line when working from {start} to {end}' + ) def test_simple_fragment_count(self): projection = ccrs.PlateCarree() @@ -74,13 +75,15 @@ def test_out_of_domain_efficiency(self): tgt_proj.project_geometry(line_string, src_proj) assert time.time() < cutoff_time, 'Projection took too long' - @pytest.mark.skipif(shapely.__version__ < "2", - reason="Shapely <2 has an incorrect geom_type ") + @pytest.mark.skipif( + shapely.__version__ < '2', reason='Shapely <2 has an incorrect geom_type ' + ) def test_multi_linestring_return_type(self): # Check that the return type of project_geometry is a MultiLineString # and not an empty list multi_line_string = ccrs.Mercator().project_geometry( - sgeom.MultiLineString(), ccrs.PlateCarree()) + sgeom.MultiLineString(), ccrs.PlateCarree() + ) assert isinstance(multi_line_string, sgeom.MultiLineString) @@ -97,11 +100,15 @@ def __init__(self, left_offset=0, right_offset=0): def boundary(self): # XXX Should this be a LinearRing? w, h = self._half_width, self._half_height - return sgeom.LineString([(-w + self.left_offset, -h), - (-w + self.left_offset, h), - (w - self.right_offset, h), - (w - self.right_offset, -h), - (-w + self.left_offset, -h)]) + return sgeom.LineString( + [ + (-w + self.left_offset, -h), + (-w + self.left_offset, h), + (w - self.right_offset, h), + (w - self.right_offset, -h), + (-w + self.left_offset, -h), + ] + ) class TestBisect: @@ -180,19 +187,16 @@ def test_point_on_boundary(self): assert len(multi_line_string.geoms[0].coords) == 2 def test_nan_start(self): - projection = ccrs.TransverseMercator(central_longitude=-90, - approx=False) + projection = ccrs.TransverseMercator(central_longitude=-90, approx=False) line_string = sgeom.LineString([(10, 50), (-10, 30)]) multi_line_string = projection.project_geometry(line_string) assert len(multi_line_string.geoms) == 1 for line_string in multi_line_string.geoms: for coord in line_string.coords: - assert not any(np.isnan(coord)), \ - 'Unexpected NaN in projected coords.' + assert not any(np.isnan(coord)), 'Unexpected NaN in projected coords.' def test_nan_end(self): - projection = ccrs.TransverseMercator(central_longitude=-90, - approx=False) + projection = ccrs.TransverseMercator(central_longitude=-90, approx=False) line_string = sgeom.LineString([(-10, 30), (10, 50)]) multi_line_string = projection.project_geometry(line_string) # from cartopy.tests.mpl import show @@ -200,43 +204,40 @@ def test_nan_end(self): assert len(multi_line_string.geoms) == 1 for line_string in multi_line_string.geoms: for coord in line_string.coords: - assert not any(np.isnan(coord)), \ - 'Unexpected NaN in projected coords.' + assert not any(np.isnan(coord)), 'Unexpected NaN in projected coords.' def test_nan_rectangular(self): # Make sure rectangular projections can handle invalid geometries projection = ccrs.Robinson() - line_string = sgeom.LineString([(0, 0), (1, 1), (np.nan, np.nan), - (2, 2), (3, 3)]) - multi_line_string = projection.project_geometry(line_string, - ccrs.PlateCarree()) + line_string = sgeom.LineString( + [(0, 0), (1, 1), (np.nan, np.nan), (2, 2), (3, 3)] + ) + multi_line_string = projection.project_geometry(line_string, ccrs.PlateCarree()) assert len(multi_line_string.geoms) == 2 class TestMisc: def test_misc(self): - projection = ccrs.TransverseMercator(central_longitude=-90, - approx=False) + projection = ccrs.TransverseMercator(central_longitude=-90, approx=False) line_string = sgeom.LineString([(10, 50), (-10, 30)]) multi_line_string = projection.project_geometry(line_string) # from cartopy.tests.mpl import show # show(projection, multi_line_string) for line_string in multi_line_string.geoms: for coord in line_string.coords: - assert not any(np.isnan(coord)), \ - 'Unexpected NaN in projected coords.' + assert not any(np.isnan(coord)), 'Unexpected NaN in projected coords.' def test_something(self): - projection = ccrs.RotatedPole(pole_longitude=177.5, - pole_latitude=37.5) + projection = ccrs.RotatedPole(pole_longitude=177.5, pole_latitude=37.5) line_string = sgeom.LineString([(0, 0), (1e-14, 0)]) multi_line_string = projection.project_geometry(line_string) assert len(multi_line_string.geoms) == 1 assert len(multi_line_string.geoms[0].coords) == 2 def test_global_boundary(self): - linear_ring = sgeom.LineString([(-180, -180), (-180, 180), - (180, 180), (180, -180)]) + linear_ring = sgeom.LineString( + [(-180, -180), (-180, 180), (180, 180), (180, -180)] + ) pc = ccrs.PlateCarree() merc = ccrs.Mercator() multi_line_string = pc.project_geometry(linear_ring, merc) @@ -266,5 +267,6 @@ def test_curve(self): assert len(multi_line_string2.geoms) == 1 coords = multi_line_string.geoms[0].coords coords2 = multi_line_string2.geoms[0].coords - np.testing.assert_allclose(coords, coords2[::-1], - err_msg='Asymmetric curve generation') + np.testing.assert_allclose( + coords, coords2[::-1], err_msg='Asymmetric curve generation' + ) diff --git a/lib/cartopy/tests/test_linear_ring.py b/lib/cartopy/tests/test_linear_ring.py index 1bfa59e1d..b5219379c 100644 --- a/lib/cartopy/tests/test_linear_ring.py +++ b/lib/cartopy/tests/test_linear_ring.py @@ -26,8 +26,9 @@ def assert_close_to_boundary(xy): # Are we close to the boundary, which we are considering within # a fraction of the x domain limits limit = (projection.x_limits[1] - projection.x_limits[0]) / 1e4 - assert sgeom.Point(*xy).distance(projection.boundary) < limit, \ + assert sgeom.Point(*xy).distance(projection.boundary) < limit, ( 'Bad topology near boundary' + ) # Each line resulting from the split should be close to the boundary. # (This is important when considering polygon rings which need to be @@ -73,11 +74,13 @@ def test_small(self): # What happens when a small (i.e. < threshold) feature crosses the # boundary? projection = ccrs.Mercator() - linear_ring = sgeom.LinearRing([ - (-179.9173693847652942, -16.5017831356493616), - (-180.0000000000000000, -16.0671326636424396), - (-179.7933201090486079, -16.0208822567412312), - ]) + linear_ring = sgeom.LinearRing( + [ + (-179.9173693847652942, -16.5017831356493616), + (-180.0000000000000000, -16.0671326636424396), + (-179.7933201090486079, -16.0208822567412312), + ] + ) *rings, multi_line_string = projection.project_geometry(linear_ring).geoms # There should be one, and only one, returned ring. assert isinstance(multi_line_string, sgeom.MultiLineString) @@ -93,18 +96,20 @@ def test_three_points(self): # If an attempt is made to form a LinearRing from the three points # by combining the first and last an exception will be raised. # Check that this object can be projected without error. - coords = [(0.0, -45.0), - (0.0, -44.99974961593933), - (0.000727869825138, -45.0), - (0.0, -45.000105851567454), - (0.0, -45.0)] + coords = [ + (0.0, -45.0), + (0.0, -44.99974961593933), + (0.000727869825138, -45.0), + (0.0, -45.000105851567454), + (0.0, -45.0), + ] linear_ring = sgeom.LinearRing(coords) src_proj = ccrs.PlateCarree() target_proj = ccrs.PlateCarree(180.0) try: target_proj.project_geometry(linear_ring, src_proj) except ValueError: - pytest.fail("Failed to project LinearRing.") + pytest.fail('Failed to project LinearRing.') def test_stitch(self): # The following LinearRing wanders in/out of the map domain @@ -115,16 +120,18 @@ def test_stitch(self): # Check that these ends are stitched together to avoid the # boundary ordering ambiguity. # NB. This kind of polygon often occurs with MPL's contouring. - coords = [(0.0, -70.70499926182919), - (0.0, -71.25), - (0.0, -72.5), - (0.0, -73.49076371657017), - (360.0, -73.49076371657017), - (360.0, -72.5), - (360.0, -71.25), - (360.0, -70.70499926182919), - (350, -73), - (10, -73)] + coords = [ + (0.0, -70.70499926182919), + (0.0, -71.25), + (0.0, -72.5), + (0.0, -73.49076371657017), + (360.0, -73.49076371657017), + (360.0, -72.5), + (360.0, -71.25), + (360.0, -70.70499926182919), + (350, -73), + (10, -73), + ] src_proj = ccrs.PlateCarree() target_proj = ccrs.Stereographic(80) @@ -144,19 +151,22 @@ def test_at_boundary(self): # as a result of being on the boundary, determined by tolerance. exterior = np.array( - [[177.5, -79.912], - [178.333, -79.946], - [181.666, -83.494], - [180.833, -83.570], - [180., -83.620], - [178.438, -83.333], - [178.333, -83.312], - [177.956, -83.888], - [180., -84.086], - [180.833, -84.318], - [183., -86.], - [183., -78.], - [177.5, -79.912]]) + [ + [177.5, -79.912], + [178.333, -79.946], + [181.666, -83.494], + [180.833, -83.570], + [180.0, -83.620], + [178.438, -83.333], + [178.333, -83.312], + [177.956, -83.888], + [180.0, -84.086], + [180.833, -84.318], + [183.0, -86.0], + [183.0, -78.0], + [177.5, -79.912], + ] + ) tring = sgeom.LinearRing(exterior) tcrs = ccrs.PlateCarree() diff --git a/lib/cartopy/tests/test_polygon.py b/lib/cartopy/tests/test_polygon.py index c9fb435a0..4296fc866 100644 --- a/lib/cartopy/tests/test_polygon.py +++ b/lib/cartopy/tests/test_polygon.py @@ -58,18 +58,19 @@ def test_out_of_bounds(self): class TestMisc: def test_misc(self): - projection = ccrs.TransverseMercator(central_longitude=-90, - approx=False) + projection = ccrs.TransverseMercator(central_longitude=-90, approx=False) polygon = sgeom.Polygon([(-10, 30), (10, 60), (10, 50)]) projection.project_geometry(polygon) def test_small(self): projection = ccrs.Mercator() - polygon = sgeom.Polygon([ - (-179.7933201090486079, -16.0208822567412312), - (-180.0000000000000000, -16.0671326636424396), - (-179.9173693847652942, -16.5017831356493616), - ]) + polygon = sgeom.Polygon( + [ + (-179.7933201090486079, -16.0208822567412312), + (-180.0000000000000000, -16.0671326636424396), + (-179.9173693847652942, -16.5017831356493616), + ] + ) multi_polygon = projection.project_geometry(polygon) assert len(multi_polygon.geoms) == 1 assert len(multi_polygon.geoms[0].exterior.coords) == 4 @@ -77,10 +78,15 @@ def test_small(self): def test_former_infloop_case(self): # test a polygon which used to get stuck in an infinite loop # see https://github.com/SciTools/cartopy/issues/60 - coords = [(260.625, 68.90383337092122), (360.0, 79.8556091996901), - (360.0, 77.76848175458498), (0.0, 88.79068047337279), - (210.0, 90.0), (135.0, 88.79068047337279), - (260.625, 68.90383337092122)] + coords = [ + (260.625, 68.90383337092122), + (360.0, 79.8556091996901), + (360.0, 77.76848175458498), + (0.0, 88.79068047337279), + (210.0, 90.0), + (135.0, 88.79068047337279), + (260.625, 68.90383337092122), + ] geom = sgeom.Polygon(coords) target_projection = ccrs.PlateCarree() @@ -106,7 +112,8 @@ def test_project_previous_infinite_loop(self): '179.9999995231628702 -80.0499999523162842, ' '179.5000000000000000 -80.0999999999999943, ' '179.5000000000000000 -80.2000000000000171, ' - '179.9999990463256836 -80.2000000000000171))') + '179.9999990463256836 -80.2000000000000171))' + ) mstring2 = shapely.wkt.loads( 'MULTILINESTRING (' '(179.9999996185302678 -79.9999999904632659, ' @@ -116,31 +123,33 @@ def test_project_previous_infinite_loop(self): '(-179.9999999047436177 -79.9600000000000080, ' '-179.9000000001110777 -79.9600000000000080, ' '-179.9000000001110777 -80.0000000000000000, ' - '-179.9999999047436177 -80.0000000000000000))') + '-179.9999999047436177 -80.0000000000000000))' + ) multi_line_strings = [mstring1, mstring2] src = ccrs.PlateCarree() src._attach_lines_to_boundary(multi_line_strings, True) - @pytest.mark.parametrize('proj', - [ccrs.InterruptedGoodeHomolosine, ccrs.Mollweide]) + @pytest.mark.parametrize('proj', [ccrs.InterruptedGoodeHomolosine, ccrs.Mollweide]) def test_infinite_loop_bounds(self, proj): # test a polygon which used to get stuck in an infinite loop but is now # erroneously clipped. # see https://github.com/SciTools/cartopy/issues/1131 # These names are for IGH; effectively the same for Mollweide. - bottom = [0., 70.] - right = [0., 90.] - top = [-180., 90.] - left = [-180., 70.] - verts = np.array([ - bottom, - right, - top, - left, - bottom, - ]) + bottom = [0.0, 70.0] + right = [0.0, 90.0] + top = [-180.0, 90.0] + left = [-180.0, 70.0] + verts = np.array( + [ + bottom, + right, + top, + left, + bottom, + ] + ) bad_path = sgeom.Polygon(verts) target = proj() @@ -153,55 +162,58 @@ def test_infinite_loop_bounds(self, proj): # than it should. Check that the bounds match the individual points at # the expected edges. projected_left = target.transform_point(left[0], left[1], source) - assert projected.bounds[0] == pytest.approx(projected_left[0], - rel=target.threshold) + assert projected.bounds[0] == pytest.approx( + projected_left[0], rel=target.threshold + ) projected_bottom = target.transform_point(bottom[0], bottom[1], source) - assert projected.bounds[1] == pytest.approx(projected_bottom[1], - rel=target.threshold) + assert projected.bounds[1] == pytest.approx( + projected_bottom[1], rel=target.threshold + ) projected_right = target.transform_point(right[0], right[1], source) - assert projected.bounds[2] == pytest.approx(projected_right[0], - rel=target.threshold, - abs=1e-8) + assert projected.bounds[2] == pytest.approx( + projected_right[0], rel=target.threshold, abs=1e-8 + ) projected_top = target.transform_point(top[0], top[1], source) - assert projected.bounds[3] == pytest.approx(projected_top[1], - rel=target.threshold) + assert projected.bounds[3] == pytest.approx( + projected_top[1], rel=target.threshold + ) def test_3pt_poly(self): projection = ccrs.OSGB(approx=True) - polygon = sgeom.Polygon([(-1000, -1000), - (-1000, 200000), - (200000, -1000)]) - multi_polygon = projection.project_geometry(polygon, - ccrs.OSGB(approx=True)) + polygon = sgeom.Polygon([(-1000, -1000), (-1000, 200000), (200000, -1000)]) + multi_polygon = projection.project_geometry(polygon, ccrs.OSGB(approx=True)) assert len(multi_polygon.geoms) == 1 assert len(multi_polygon.geoms[0].exterior.coords) == 4 def test_self_intersecting_1(self): # Geometry comes from a matplotlib contourf (see #537) - wkt = ('POLYGON ((366.22000122 -9.71489298, ' - '366.73212393 -9.679999349999999, ' - '366.77412634 -8.767753000000001, ' - '366.17762962 -9.679999349999999, ' - '366.22000122 -9.71489298), ' - '(366.22000122 -9.692636309999999, ' - '366.32998657 -9.603356099999999, ' - '366.74765799 -9.019999500000001, ' - '366.5094086 -9.63175386, ' - '366.22000122 -9.692636309999999))') + wkt = ( + 'POLYGON ((366.22000122 -9.71489298, ' + '366.73212393 -9.679999349999999, ' + '366.77412634 -8.767753000000001, ' + '366.17762962 -9.679999349999999, ' + '366.22000122 -9.71489298), ' + '(366.22000122 -9.692636309999999, ' + '366.32998657 -9.603356099999999, ' + '366.74765799 -9.019999500000001, ' + '366.5094086 -9.63175386, ' + '366.22000122 -9.692636309999999))' + ) geom = shapely.wkt.loads(wkt) source, target = ccrs.RotatedPole(198.0, 39.25), ccrs.EuroPP() projected = target.project_geometry(geom, source) # Before handling self intersecting interiors, the area would be # approximately 13262233761329. area = projected.area - assert 2.2e9 < area < 2.3e9, \ - f'Got area {area}, expecting ~2.2e9' + assert 2.2e9 < area < 2.3e9, f'Got area {area}, expecting ~2.2e9' def test_self_intersecting_2(self): # Geometry comes from a matplotlib contourf (see #509) - wkt = ('POLYGON ((343 20, 345 23, 342 25, 343 22, ' - '340 25, 341 25, 340 25, 343 20), (343 21, ' - '343 22, 344 23, 343 21))') + wkt = ( + 'POLYGON ((343 20, 345 23, 342 25, 343 22, ' + '340 25, 341 25, 340 25, 343 20), (343 21, ' + '343 22, 344 23, 343 21))' + ) geom = shapely.wkt.loads(wkt) source = target = ccrs.RotatedPole(193.0, 41.0) projected = target.project_geometry(geom, source) @@ -216,21 +228,22 @@ def test_tiny_point_between_boundary_points(self): wkt = 'POLYGON ((132 -40, 133 -6, 125.3 1, 115 -6, 132 -40))' geom = shapely.wkt.loads(wkt) - target = ccrs.Orthographic(central_latitude=90., central_longitude=0) + target = ccrs.Orthographic(central_latitude=90.0, central_longitude=0) source = ccrs.PlateCarree() projected = target.project_geometry(geom, source) area = projected.area # Before fixing, this geometry used to fill the whole disk. Approx # 1.2e14. - assert 81330 < area < 81340, \ - f'Got area {area}, expecting ~81336' + assert 81330 < area < 81340, f'Got area {area}, expecting ~81336' def test_same_points_on_boundary_1(self): source = ccrs.PlateCarree() target = ccrs.PlateCarree(central_longitude=180) - geom = sgeom.Polygon([(-20, -20), (20, -20), (20, 20), (-20, 20)], - [[(-10, 0), (0, 20), (10, 0), (0, -20)]]) + geom = sgeom.Polygon( + [(-20, -20), (20, -20), (20, 20), (-20, 20)], + [[(-10, 0), (0, 20), (10, 0), (0, -20)]], + ) projected = target.project_geometry(geom, source) assert abs(1200 - projected.area) < 1e-5 @@ -239,9 +252,13 @@ def test_same_points_on_boundary_2(self): source = ccrs.PlateCarree() target = ccrs.PlateCarree(central_longitude=180) - geom = sgeom.Polygon([(-20, -20), (20, -20), (20, 20), (-20, 20)], - [[(0, 0), (-10, 10), (0, 20), (10, 10)], - [(0, 0), (10, -10), (0, -20), (-10, -10)]]) + geom = sgeom.Polygon( + [(-20, -20), (20, -20), (20, 20), (-20, 20)], + [ + [(0, 0), (-10, 10), (0, 20), (10, 10)], + [(0, 0), (10, -10), (0, -20), (-10, -10)], + ], + ) projected = target.project_geometry(geom, source) assert abs(1200 - projected.area) < 1e-5 @@ -252,7 +269,8 @@ def test_attach_short_loop(self): 'MULTILINESTRING (' '(-179.9999982118607 71.87500000000001,' '-179.0625 71.87500000000001,' - '-179.9999982118607 71.87500000000001))') + '-179.9999982118607 71.87500000000001))' + ) multi_line_strings = [mstring] src = ccrs.PlateCarree() @@ -273,7 +291,8 @@ def test_project_degenerate_poly(self): '180.9375 71.875, ' '179.0625 71.875, ' '177.1875 71.875, ' - '178.9687499944748 70.625))') + '178.9687499944748 70.625))' + ) source = ccrs.PlateCarree() target = ccrs.PlateCarree() @@ -284,13 +303,14 @@ def test_project_degenerate_poly(self): class TestQuality: def setup_class(self): - projection = ccrs.RotatedPole(pole_longitude=177.5, - pole_latitude=37.5) - polygon = sgeom.Polygon([ - (177.5, -57.38460319), - (180.1, -57.445077), - (175.0, -57.19913331), - ]) + projection = ccrs.RotatedPole(pole_longitude=177.5, pole_latitude=37.5) + polygon = sgeom.Polygon( + [ + (177.5, -57.38460319), + (180.1, -57.445077), + (175.0, -57.19913331), + ] + ) self.multi_polygon = projection.project_geometry(polygon) # from cartopy.tests.mpl import show # show(projection, self.multi_polygon) @@ -385,8 +405,9 @@ def ring(minx, miny, maxx, maxy, ccw): class TestHoles(PolygonTests): def test_simple(self): proj = ccrs.PlateCarree() - poly = sgeom.Polygon(ring(-40, -40, 40, 40, True), - [ring(-20, -20, 20, 20, False)]) + poly = sgeom.Polygon( + ring(-40, -40, 40, 40, True), [ring(-20, -20, 20, 20, False)] + ) multi_polygon = proj.project_geometry(poly) # Check the structure assert len(multi_polygon.geoms) == 1 @@ -398,8 +419,9 @@ def test_simple(self): def test_wrapped_poly_simple_hole(self): proj = ccrs.PlateCarree(-150) - poly = sgeom.Polygon(ring(-40, -40, 40, 40, True), - [ring(-20, -20, 20, 20, False)]) + poly = sgeom.Polygon( + ring(-40, -40, 40, 40, True), [ring(-20, -20, 20, 20, False)] + ) multi_polygon = proj.project_geometry(poly) # Check the structure assert len(multi_polygon.geoms) == 2 @@ -419,8 +441,9 @@ def test_wrapped_poly_simple_hole(self): def test_wrapped_poly_wrapped_hole(self): proj = ccrs.PlateCarree(-180) - poly = sgeom.Polygon(ring(-40, -40, 40, 40, True), - [ring(-20, -20, 20, 20, False)]) + poly = sgeom.Polygon( + ring(-40, -40, 40, 40, True), [ring(-20, -20, 20, 20, False)] + ) multi_polygon = proj.project_geometry(poly) # Check the structure assert len(multi_polygon.geoms) == 2 @@ -434,8 +457,10 @@ def test_wrapped_poly_wrapped_hole(self): def test_inverted_poly_simple_hole(self): proj = ccrs.NorthPolarStereo() - poly = sgeom.Polygon([(0, 0), (-90, 0), (-180, 0), (-270, 0)], - [[(0, -30), (90, -30), (180, -30), (270, -30)]]) + poly = sgeom.Polygon( + [(0, 0), (-90, 0), (-180, 0), (-270, 0)], + [[(0, -30), (90, -30), (180, -30), (270, -30)]], + ) multi_polygon = proj.project_geometry(poly) # Check the structure assert len(multi_polygon.geoms) == 1 @@ -443,15 +468,19 @@ def test_inverted_poly_simple_hole(self): # Check the rough shape polygon = multi_polygon.geoms[0] self._assert_bounds(polygon.bounds, -2.4e7, -2.4e7, 2.4e7, 2.4e7, 1e6) - self._assert_bounds(polygon.interiors[0].bounds, - - 1.2e7, -1.2e7, 1.2e7, 1.2e7, 1e6) + self._assert_bounds( + polygon.interiors[0].bounds, -1.2e7, -1.2e7, 1.2e7, 1.2e7, 1e6 + ) def test_inverted_poly_multi_hole(self): # Adapted from 1149 proj = ccrs.LambertAzimuthalEqualArea( - central_latitude=45, central_longitude=-100) - poly = sgeom.Polygon([(-180, -80), (180, -80), (180, 90), (-180, 90)], - [[(-50, -50), (-50, 0), (0, 0), (0, -50)]]) + central_latitude=45, central_longitude=-100 + ) + poly = sgeom.Polygon( + [(-180, -80), (180, -80), (180, 90), (-180, 90)], + [[(-50, -50), (-50, 0), (0, 0), (0, -50)]], + ) multi_polygon = proj.project_geometry(poly) # Should project to single polygon with multiple holes assert len(multi_polygon.geoms) == 1 @@ -460,17 +489,22 @@ def test_inverted_poly_multi_hole(self): def test_inverted_poly_merged_holes(self): proj = ccrs.LambertAzimuthalEqualArea(central_latitude=-90) pc = ccrs.PlateCarree() - poly = sgeom.Polygon([(-180, -80), (180, -80), (180, 90), (-180, 90)], - [[(-50, 60), (-50, 80), (0, 80), (0, 60)], - [(-50, 81), (-50, 85), (0, 85), (0, 81)]]) + poly = sgeom.Polygon( + [(-180, -80), (180, -80), (180, 90), (-180, 90)], + [ + [(-50, 60), (-50, 80), (0, 80), (0, 60)], + [(-50, 81), (-50, 85), (0, 85), (0, 81)], + ], + ) # Smoke test that nearby holes do not cause side location conflict proj.project_geometry(poly, pc) def test_inverted_poly_clipped_hole(self): proj = ccrs.NorthPolarStereo() - poly = sgeom.Polygon([(0, 0), (-90, 0), (-180, 0), (-270, 0)], - [[(-135, -60), (-45, -60), - (45, -60), (135, -60)]]) + poly = sgeom.Polygon( + [(0, 0), (-90, 0), (-180, 0), (-270, 0)], + [[(-135, -60), (-45, -60), (45, -60), (135, -60)]], + ) multi_polygon = proj.project_geometry(poly) # Check the structure assert len(multi_polygon.geoms) == 1 @@ -478,15 +512,17 @@ def test_inverted_poly_clipped_hole(self): # Check the rough shape polygon = multi_polygon.geoms[0] self._assert_bounds(polygon.bounds, -5.0e7, -5.0e7, 5.0e7, 5.0e7, 1e6) - self._assert_bounds(polygon.interiors[0].bounds, - - 1.2e7, -1.2e7, 1.2e7, 1.2e7, 1e6) + self._assert_bounds( + polygon.interiors[0].bounds, -1.2e7, -1.2e7, 1.2e7, 1.2e7, 1e6 + ) assert abs(polygon.area - 7.30e15) < 1e13 def test_inverted_poly_removed_hole(self): proj = ccrs.NorthPolarStereo(globe=ccrs.Globe(ellipse='WGS84')) - poly = sgeom.Polygon([(0, 0), (-90, 0), (-180, 0), (-270, 0)], - [[(-135, -75), (-45, -75), - (45, -75), (135, -75)]]) + poly = sgeom.Polygon( + [(0, 0), (-90, 0), (-180, 0), (-270, 0)], + [[(-135, -75), (-45, -75), (45, -75), (135, -75)]], + ) multi_polygon = proj.project_geometry(poly) # Check the structure assert len(multi_polygon.geoms) == 1 @@ -494,8 +530,9 @@ def test_inverted_poly_removed_hole(self): # Check the rough shape polygon = multi_polygon.geoms[0] self._assert_bounds(polygon.bounds, -5.0e7, -5.0e7, 5.0e7, 5.0e7, 1e6) - self._assert_bounds(polygon.interiors[0].bounds, - - 1.2e7, -1.2e7, 1.2e7, 1.2e7, 1e6) + self._assert_bounds( + polygon.interiors[0].bounds, -1.2e7, -1.2e7, 1.2e7, 1.2e7, 1e6 + ) assert abs(polygon.area - 7.34e15) < 1e13 def test_multiple_interiors(self): diff --git a/lib/cartopy/tests/test_shapereader.py b/lib/cartopy/tests/test_shapereader.py index c13a3c759..3e0989b7a 100644 --- a/lib/cartopy/tests/test_shapereader.py +++ b/lib/cartopy/tests/test_shapereader.py @@ -15,21 +15,19 @@ class TestLakes: @pytest.fixture(autouse=True, params=[0, 1]) def setup_class(self, request): - LAKES_PATH = (Path(__file__).parent / 'lakes_shapefile' - / 'ne_110m_lakes.shp') + LAKES_PATH = Path(__file__).parent / 'lakes_shapefile' / 'ne_110m_lakes.shp' # run tests with both available Readers if request.param == 0: self.reader = shp.BasicReader(LAKES_PATH) elif not shp._HAS_FIONA: - pytest.skip("Fiona library not available") + pytest.skip('Fiona library not available') else: self.reader = shp.FionaReader(LAKES_PATH) names = [record.attributes['name'] for record in self.reader.records()] # Choose a nice small lake self.lake_name = 'Lago de\rNicaragua' self.lake_index = names.index(self.lake_name) - self.test_lake_geometry = \ - list(self.reader.geometries())[self.lake_index] + self.test_lake_geometry = list(self.reader.geometries())[self.lake_index] self.test_lake_record = list(self.reader.records())[self.lake_index] def test_geometry(self): @@ -40,14 +38,18 @@ def test_geometry(self): # with pyshp 2.2.0 forcing a specific orientation. polygon = sgeom.polygon.orient(lake_geometry, -1) - expected = np.array([(-84.85548682324658, 11.147898667846633), - (-85.29013729525353, 11.176165676310276), - (-85.79132117383625, 11.509737046754324), - (-85.8851655748783, 11.900100816287136), - (-85.5653401354239, 11.940330918826362), - (-85.03684526237491, 11.5216484643976), - (-84.85548682324658, 11.147898667846633), - (-84.85548682324658, 11.147898667846633)]) + expected = np.array( + [ + (-84.85548682324658, 11.147898667846633), + (-85.29013729525353, 11.176165676310276), + (-85.79132117383625, 11.509737046754324), + (-85.8851655748783, 11.900100816287136), + (-85.5653401354239, 11.940330918826362), + (-85.03684526237491, 11.5216484643976), + (-84.85548682324658, 11.147898667846633), + (-84.85548682324658, 11.147898667846633), + ] + ) assert_array_almost_equal(expected, polygon.exterior.coords) @@ -56,8 +58,17 @@ def test_geometry(self): def test_record(self): lake_record = self.test_lake_record assert lake_record.attributes.get('name') == self.lake_name - expected = sorted(['admin', 'featurecla', 'min_label', 'min_zoom', - 'name', 'name_alt', 'scalerank']) + expected = sorted( + [ + 'admin', + 'featurecla', + 'min_label', + 'min_zoom', + 'name', + 'name_alt', + 'scalerank', + ] + ) actual = sorted(lake_record.attributes.keys()) assert actual == expected assert lake_record.geometry == self.test_lake_geometry @@ -71,30 +82,29 @@ def test_bounds(self): # tests that a file which has a record with a bbox can # use the bbox without first creating the geometry record = next(self.reader.records()) - assert not record._geometry, \ - 'The geometry was loaded before it was needed.' + assert not record._geometry, 'The geometry was loaded before it was needed.' assert len(record._bounds) == 4 assert record._bounds == record.bounds - assert not record._geometry, \ + assert not record._geometry, ( 'The geometry was loaded in order to create the bounds.' + ) else: pytest.skip("Fiona reader doesn't support lazy loading") -@pytest.mark.filterwarnings("ignore:Downloading") +@pytest.mark.filterwarnings('ignore:Downloading') @pytest.mark.natural_earth class TestRivers: def setup_class(self): - RIVERS_PATH = shp.natural_earth(resolution='110m', - category='physical', - name='rivers_lake_centerlines') + RIVERS_PATH = shp.natural_earth( + resolution='110m', category='physical', name='rivers_lake_centerlines' + ) self.reader = shp.Reader(RIVERS_PATH) names = [record.attributes['name'] for record in self.reader.records()] # Choose a nice small river self.river_name = 'Peace' self.river_index = names.index(self.river_name) - self.test_river_geometry = \ - list(self.reader.geometries())[self.river_index] + self.test_river_geometry = list(self.reader.geometries())[self.river_index] self.test_river_record = list(self.reader.records())[self.river_index] def test_geometry(self): @@ -114,12 +124,14 @@ def test_record(self): # Choose a nice small lake river_record = records[self.river_index] - expected_attributes = {'featurecla': 'River', - 'min_label': 3.1, - 'min_zoom': 2.1, - 'name': self.river_name, - 'name_en': self.river_name, - 'scalerank': 2} + expected_attributes = { + 'featurecla': 'River', + 'min_label': 3.1, + 'min_zoom': 2.1, + 'name': self.river_name, + 'name_en': self.river_name, + 'scalerank': 2, + } for key, value in river_record.attributes.items(): if key in expected_attributes: assert value == expected_attributes[key] @@ -127,12 +139,14 @@ def test_record(self): def test_included_projection_file(self): # This shapefile includes a .prj definition - wkt = ('GEOGCRS["WGS 84",DATUM["World Geodetic System 1984",' + wkt = ( + 'GEOGCRS["WGS 84",DATUM["World Geodetic System 1984",' 'ELLIPSOID["WGS 84",6378137,298.257223563,LENGTHUNIT["metre",1]],' 'ID["EPSG",6326]],PRIMEM["Greenwich",0,' 'ANGLEUNIT["Degree",0.0174532925199433]],CS[ellipsoidal,2],' 'AXIS["longitude",east,ORDER[1],' 'ANGLEUNIT["Degree",0.0174532925199433]],' 'AXIS["latitude",north,ORDER[2],' - 'ANGLEUNIT["Degree",0.0174532925199433]]]') + 'ANGLEUNIT["Degree",0.0174532925199433]]]' + ) assert self.reader.crs.to_wkt() == wkt diff --git a/lib/cartopy/tests/test_util.py b/lib/cartopy/tests/test_util.py index 19fc89b14..9c35269b6 100644 --- a/lib/cartopy/tests/test_util.py +++ b/lib/cartopy/tests/test_util.py @@ -12,13 +12,11 @@ class Test_add_cyclic_point: - @classmethod def setup_class(cls): cls.lons = np.arange(0, 360, 60) cls.data2d = np.ones([3, 6]) * np.arange(6) - cls.data4d = np.ones([4, 6, 2, 3]) * \ - np.arange(6)[..., np.newaxis, np.newaxis] + cls.data4d = np.ones([4, 6, 2, 3]) * np.arange(6)[..., np.newaxis, np.newaxis] def test_data_only(self): c_data = add_cyclic_point(self.data2d) @@ -57,8 +55,7 @@ def test_invalid_coord_dimensionality(self): def test_invalid_coord_size(self): with pytest.raises(ValueError): - c_data, c_lons = add_cyclic_point(self.data2d, - coord=self.lons[:-1]) + c_data, c_lons = add_cyclic_point(self.data2d, coord=self.lons[:-1]) def test_invalid_axis(self): with pytest.raises(ValueError): @@ -79,8 +76,7 @@ class TestAddCyclic: def setup_class(cls): # 2d and 4d data cls.data2d = np.ones([3, 6]) * np.arange(6) - cls.data4d = np.ones([4, 6, 2, 3]) * \ - np.arange(6)[..., np.newaxis, np.newaxis] + cls.data4d = np.ones([4, 6, 2, 3]) * np.arange(6)[..., np.newaxis, np.newaxis] # 1d lat (5) and lon (6) # len(lat) != data.shape[0] # len(lon) == data.shape[1] @@ -96,140 +92,132 @@ def setup_class(cls): cls.c_data4d = np.concatenate((cls.data4d, cls.data4d[:, :1]), axis=1) cls.c_lons = np.concatenate((cls.lons, np.array([360]))) cls.c_lon2d = np.concatenate( - (cls.lon2d, np.full((cls.lon2d.shape[0], 1), 360)), - axis=1) + (cls.lon2d, np.full((cls.lon2d.shape[0], 1), 360)), axis=1 + ) cls.c_lon3d = np.concatenate( - (cls.lon3d, - np.full((cls.lon3d.shape[0], 1, cls.lon3d.shape[2]), 360)), - axis=1) + (cls.lon3d, np.full((cls.lon3d.shape[0], 1, cls.lon3d.shape[2]), 360)), + axis=1, + ) cls.c_lats = cls.lats cls.c_lat2d = np.concatenate((cls.lat2d, cls.lat2d[:, -1:]), axis=1) cls.c_lat3d = np.concatenate((cls.lat3d, cls.lat3d[:, -1:, :]), axis=1) def test_data_only(self): - '''Test only data no x given''' + """Test only data no x given""" c_data = add_cyclic(self.data2d) assert_array_equal(c_data, self.c_data2d) def test_data_only_ignore_y(self): - '''Test y given but no x''' + """Test y given but no x""" c_data = add_cyclic(self.data2d, y=self.lat2d) assert_array_equal(c_data, self.c_data2d) def test_data_and_x_1d(self): - '''Test data 2d and x 1d''' + """Test data 2d and x 1d""" c_data, c_lons = add_cyclic(self.data2d, x=self.lons) assert_array_equal(c_data, self.c_data2d) assert_array_equal(c_lons, self.c_lons) def test_data_and_x_2d(self): - '''Test data and x 2d; no keyword name for x''' + """Test data and x 2d; no keyword name for x""" c_data, c_lons = add_cyclic(self.data2d, self.lon2d) assert_array_equal(c_data, self.c_data2d) assert_array_equal(c_lons, self.c_lon2d) def test_data_and_x_y_1d(self): - '''Test data and x and y 1d''' - c_data, c_lons, c_lats = add_cyclic(self.data2d, x=self.lons, - y=self.lats) + """Test data and x and y 1d""" + c_data, c_lons, c_lats = add_cyclic(self.data2d, x=self.lons, y=self.lats) assert_array_equal(c_data, self.c_data2d) assert_array_equal(c_lons, self.c_lons) assert_array_equal(c_lats, self.c_lats) def test_data_and_x_1d_y_2d(self): - '''Test data and x 1d and y 2d''' - c_data, c_lons, c_lats = add_cyclic(self.data2d, x=self.lons, - y=self.lat2d) + """Test data and x 1d and y 2d""" + c_data, c_lons, c_lats = add_cyclic(self.data2d, x=self.lons, y=self.lat2d) assert_array_equal(c_data, self.c_data2d) assert_array_equal(c_lons, self.c_lons) assert_array_equal(c_lats, self.c_lat2d) def test_data_and_x_y_2d(self): - '''Test data, x, and y 2d; no keyword name for x and y''' - c_data, c_lons, c_lats = add_cyclic(self.data2d, - self.lon2d, - self.lat2d) + """Test data, x, and y 2d; no keyword name for x and y""" + c_data, c_lons, c_lats = add_cyclic(self.data2d, self.lon2d, self.lat2d) assert_array_equal(c_data, self.c_data2d) assert_array_equal(c_lons, self.c_lon2d) assert_array_equal(c_lats, self.c_lat2d) def test_has_cyclic_1d(self): - '''Test detection of cyclic point 1d''' + """Test detection of cyclic point 1d""" c_data, c_lons = add_cyclic(self.c_data2d, x=self.c_lons) assert_array_equal(c_data, self.c_data2d) assert_array_equal(c_lons, self.c_lons) def test_has_cyclic_2d(self): - '''Test detection of cyclic point 2d''' + """Test detection of cyclic point 2d""" c_data, c_lons = add_cyclic(self.c_data2d, x=self.c_lon2d) assert_array_equal(c_data, self.c_data2d) assert_array_equal(c_lons, self.c_lon2d) def test_has_cyclic_2d_full(self): - '''Test detection of cyclic point 2d including y''' - c_data, c_lons, c_lats = add_cyclic(self.c_data2d, x=self.c_lon2d, - y=self.c_lat2d) + """Test detection of cyclic point 2d including y""" + c_data, c_lons, c_lats = add_cyclic( + self.c_data2d, x=self.c_lon2d, y=self.c_lat2d + ) assert_array_equal(c_data, self.c_data2d) assert_array_equal(c_lons, self.c_lon2d) assert_array_equal(c_lats, self.c_lat2d) def test_data_only_with_axis(self): - '''Test axis keyword data only''' + """Test axis keyword data only""" c_data = add_cyclic(self.data4d, axis=1) assert_array_equal(c_data, self.c_data4d) def test_data_and_x_with_axis_1d(self): - '''Test axis keyword data 4d, x 1d''' + """Test axis keyword data 4d, x 1d""" c_data, c_lons = add_cyclic(self.data4d, x=self.lons, axis=1) assert_array_equal(c_data, self.c_data4d) assert_array_equal(c_lons, self.c_lons) def test_data_and_x_with_axis_2d(self): - '''Test axis keyword data 4d, x 2d''' - c_data, c_lons = add_cyclic(self.data4d, x=self.lon2d, - axis=1) + """Test axis keyword data 4d, x 2d""" + c_data, c_lons = add_cyclic(self.data4d, x=self.lon2d, axis=1) assert_array_equal(c_data, self.c_data4d) assert_array_equal(c_lons, self.c_lon2d) def test_data_and_x_with_axis_3d(self): - '''Test axis keyword data 4d, x 3d''' - c_data, c_lons = add_cyclic(self.data4d, x=self.lon3d, - axis=1) + """Test axis keyword data 4d, x 3d""" + c_data, c_lons = add_cyclic(self.data4d, x=self.lon3d, axis=1) assert_array_equal(c_data, self.c_data4d) assert_array_equal(c_lons, self.c_lon3d) def test_data_and_x_y_with_axis_2d(self): - '''Test axis keyword data 4d, x and y 2d''' - c_data, c_lons, c_lats = add_cyclic(self.data4d, - x=self.lon2d, - y=self.lat2d, - axis=1) + """Test axis keyword data 4d, x and y 2d""" + c_data, c_lons, c_lats = add_cyclic( + self.data4d, x=self.lon2d, y=self.lat2d, axis=1 + ) assert_array_equal(c_data, self.c_data4d) assert_array_equal(c_lons, self.c_lon2d) assert_array_equal(c_lats, self.c_lat2d) def test_data_and_x_y_with_axis_3d(self): - '''Test axis keyword data 4d, x and y 3d''' - c_data, c_lons, c_lats = add_cyclic(self.data4d, - x=self.lon3d, - y=self.lat3d, - axis=1) + """Test axis keyword data 4d, x and y 3d""" + c_data, c_lons, c_lats = add_cyclic( + self.data4d, x=self.lon3d, y=self.lat3d, axis=1 + ) assert_array_equal(c_data, self.c_data4d) assert_array_equal(c_lons, self.c_lon3d) assert_array_equal(c_lats, self.c_lat3d) def test_data_and_x_y_with_axis_nd(self): - '''Test axis keyword data 4d, x 3d and y 2d''' - c_data, c_lons, c_lats = add_cyclic(self.data4d, - x=self.lon3d, - y=self.lat2d, - axis=1) + """Test axis keyword data 4d, x 3d and y 2d""" + c_data, c_lons, c_lats = add_cyclic( + self.data4d, x=self.lon3d, y=self.lat2d, axis=1 + ) assert_array_equal(c_data, self.c_data4d) assert_array_equal(c_lons, self.c_lon3d) assert_array_equal(c_lats, self.c_lat2d) def test_masked_data(self): - '''Test masked data''' + """Test masked data""" new_data = ma.masked_less(self.data2d, 3) c_data = add_cyclic(new_data) r_data = ma.concatenate((self.data2d, self.data2d[:, :1]), axis=1) @@ -237,16 +225,14 @@ def test_masked_data(self): assert ma.is_masked(c_data) def test_masked_data_and_x_y_2d(self): - '''Test masked data and x''' + """Test masked data and x""" new_data = ma.masked_less(self.data2d, 3) new_lon = ma.masked_less(self.lon2d, 2) - c_data, c_lons, c_lats = add_cyclic(new_data, - x=new_lon, - y=self.lat2d) + c_data, c_lons, c_lats = add_cyclic(new_data, x=new_lon, y=self.lat2d) r_data = ma.concatenate((self.data2d, self.data2d[:, :1]), axis=1) - r_lons = np.concatenate((self.lon2d, - np.full((self.lon2d.shape[0], 1), 360)), - axis=1) + r_lons = np.concatenate( + (self.lon2d, np.full((self.lon2d.shape[0], 1), 360)), axis=1 + ) assert_array_equal(c_data, r_data) assert_array_equal(c_lons, r_lons) assert_array_equal(c_lats, self.c_lat2d) @@ -255,42 +241,42 @@ def test_masked_data_and_x_y_2d(self): assert not ma.is_masked(c_lats) def test_cyclic(self): - '''Test cyclic keyword with axis data 4d, x 3d and y 2d''' + """Test cyclic keyword with axis data 4d, x 3d and y 2d""" new_lons = np.deg2rad(self.lon3d) new_lats = np.deg2rad(self.lat2d) - c_data, c_lons, c_lats = add_cyclic(self.data4d, x=new_lons, - y=new_lats, axis=1, - cyclic=np.deg2rad(360)) + c_data, c_lons, c_lats = add_cyclic( + self.data4d, x=new_lons, y=new_lats, axis=1, cyclic=np.deg2rad(360) + ) r_lons = np.concatenate( - (new_lons, - np.full((new_lons.shape[0], 1, new_lons.shape[2]), - np.deg2rad(360))), - axis=1) + ( + new_lons, + np.full((new_lons.shape[0], 1, new_lons.shape[2]), np.deg2rad(360)), + ), + axis=1, + ) r_lats = np.concatenate((new_lats, new_lats[:, -1:]), axis=1) assert_array_equal(c_data, self.c_data4d) assert_array_equal(c_lons, r_lons) assert_array_equal(c_lats, r_lats) def test_cyclic_has_cyclic(self): - '''Test detection of cyclic point with cyclic keyword''' + """Test detection of cyclic point with cyclic keyword""" new_lons = np.deg2rad(self.lon2d) new_lats = np.deg2rad(self.lat2d) r_data = np.concatenate((self.data2d, self.data2d[:, :1]), axis=1) r_lons = np.concatenate( - (new_lons, - np.full((new_lons.shape[0], 1), np.deg2rad(360))), - axis=1) + (new_lons, np.full((new_lons.shape[0], 1), np.deg2rad(360))), axis=1 + ) r_lats = np.concatenate((new_lats, new_lats[:, -1:]), axis=1) - c_data, c_lons, c_lats = add_cyclic(r_data, x=r_lons, - y=r_lats, - - cyclic=np.deg2rad(360)) + c_data, c_lons, c_lats = add_cyclic( + r_data, x=r_lons, y=r_lats, cyclic=np.deg2rad(360) + ) assert_array_equal(c_data, self.c_data2d) assert_array_equal(c_lons, r_lons) assert_array_equal(c_lats, r_lats) def test_precision_has_cyclic(self): - '''Test precision keyword detecting cyclic point''' + """Test precision keyword detecting cyclic point""" r_data = np.concatenate((self.data2d, self.data2d[:, :1]), axis=1) r_lons = np.concatenate((self.lons, np.array([360 + 1e-3]))) c_data, c_lons = add_cyclic(r_data, x=r_lons, precision=1e-2) @@ -298,9 +284,9 @@ def test_precision_has_cyclic(self): assert_array_equal(c_lons, r_lons) def test_precision_has_cyclic_no(self): - '''Test precision keyword detecting no cyclic point''' + """Test precision keyword detecting no cyclic point""" new_data = np.concatenate((self.data2d, self.data2d[:, :1]), axis=1) - new_lons = np.concatenate((self.lons, np.array([360. + 1e-3]))) + new_lons = np.concatenate((self.lons, np.array([360.0 + 1e-3]))) c_data, c_lons = add_cyclic(new_data, x=new_lons, precision=2e-4) r_data = np.concatenate((new_data, new_data[:, :1]), axis=1) r_lons = np.concatenate((new_lons, np.array([360]))) @@ -308,43 +294,39 @@ def test_precision_has_cyclic_no(self): assert_array_equal(c_lons, r_lons) def test_invalid_x_dimensionality(self): - '''Catch wrong x dimensions''' + """Catch wrong x dimensions""" with pytest.raises(ValueError): c_data, c_lons = add_cyclic(self.data2d, x=self.lon3d) def test_invalid_y_dimensionality(self): - '''Catch wrong y dimensions''' + """Catch wrong y dimensions""" with pytest.raises(ValueError): - c_data, c_lons, c_lats = add_cyclic(self.data2d, - x=self.lon2d, - y=self.lat3d) + c_data, c_lons, c_lats = add_cyclic(self.data2d, x=self.lon2d, y=self.lat3d) def test_invalid_x_size_1d(self): - '''Catch wrong x size 1d''' + """Catch wrong x size 1d""" with pytest.raises(ValueError): - c_data, c_lons = add_cyclic(self.data2d, - x=self.lons[:-1]) + c_data, c_lons = add_cyclic(self.data2d, x=self.lons[:-1]) def test_invalid_x_size_2d(self): - '''Catch wrong x size 2d''' + """Catch wrong x size 2d""" with pytest.raises(ValueError): - c_data, c_lons = add_cyclic(self.data2d, - x=self.lon2d[:, :-1]) + c_data, c_lons = add_cyclic(self.data2d, x=self.lon2d[:, :-1]) def test_invalid_x_size_3d(self): - '''Catch wrong x size 3d''' + """Catch wrong x size 3d""" with pytest.raises(ValueError): - c_data, c_lons = add_cyclic(self.data4d, - x=self.lon3d[:, :-1, :], axis=1) + c_data, c_lons = add_cyclic(self.data4d, x=self.lon3d[:, :-1, :], axis=1) def test_invalid_y_size(self): - '''Catch wrong y size 2d''' + """Catch wrong y size 2d""" with pytest.raises(ValueError): c_data, c_lons, c_lats = add_cyclic( - self.data2d, x=self.lon2d, y=self.lat2d[:, 1:]) + self.data2d, x=self.lon2d, y=self.lat2d[:, 1:] + ) def test_invalid_axis(self): - '''Catch wrong axis keyword''' + """Catch wrong axis keyword""" with pytest.raises(ValueError): add_cyclic(self.data2d, axis=-3) @@ -366,50 +348,44 @@ class TestHasCyclic: lon3d = np.repeat(lon2d, 4).reshape((*lon2d.shape, 4)) # cyclic lon 1d, 2d, 3d c_lons = np.concatenate((lons, np.array([360]))) - c_lon2d = np.concatenate( - (lon2d, - np.full((lon2d.shape[0], 1), 360)), - axis=1) + c_lon2d = np.concatenate((lon2d, np.full((lon2d.shape[0], 1), 360)), axis=1) c_lon3d = np.concatenate( - (lon3d, - np.full((lon3d.shape[0], 1, lon3d.shape[2]), 360)), - axis=1) + (lon3d, np.full((lon3d.shape[0], 1, lon3d.shape[2]), 360)), axis=1 + ) - @pytest.mark.parametrize( - "lon, clon", - [(lons, c_lons), - (lon2d, c_lon2d)]) + @pytest.mark.parametrize('lon, clon', [(lons, c_lons), (lon2d, c_lon2d)]) def test_data(self, lon, clon): - '''Test lon is not cyclic, clon is cyclic''' + """Test lon is not cyclic, clon is cyclic""" assert not has_cyclic(lon) assert has_cyclic(clon) @pytest.mark.parametrize( - "lon, clon, axis", - [(lons, c_lons, 0), - (lon2d, c_lon2d, 1), - (ma.masked_inside(lon2d, 100, 200), - ma.masked_inside(c_lon2d, 100, 200), - 1)]) + 'lon, clon, axis', + [ + (lons, c_lons, 0), + (lon2d, c_lon2d, 1), + (ma.masked_inside(lon2d, 100, 200), ma.masked_inside(c_lon2d, 100, 200), 1), + ], + ) def test_data_axis(self, lon, clon, axis): - '''Test lon is not cyclic, clon is cyclic, with axis keyword''' + """Test lon is not cyclic, clon is cyclic, with axis keyword""" assert not has_cyclic(lon, axis=axis) assert has_cyclic(clon, axis=axis) def test_3d_axis(self): - '''Test 3d with axis keyword, no keyword name for axis''' + """Test 3d with axis keyword, no keyword name for axis""" assert has_cyclic(self.c_lon3d, 1) assert not has_cyclic(self.lon3d, 1) def test_3d_axis_cyclic(self): - '''Test 3d with axis and cyclic keywords''' + """Test 3d with axis and cyclic keywords""" new_clons = np.deg2rad(self.c_lon3d) new_lons = np.deg2rad(self.lon3d) assert has_cyclic(new_clons, axis=1, cyclic=np.deg2rad(360)) assert not has_cyclic(new_lons, axis=1, cyclic=np.deg2rad(360)) def test_1d_precision(self): - '''Test 1d with precision keyword''' + """Test 1d with precision keyword""" new_clons = np.concatenate((self.lons, np.array([360 + 1e-3]))) assert has_cyclic(new_clons, precision=1e-2) assert not has_cyclic(new_clons, precision=2e-4) diff --git a/lib/cartopy/tests/test_vector_transform.py b/lib/cartopy/tests/test_vector_transform.py index f479e97f5..09b73a094 100644 --- a/lib/cartopy/tests/test_vector_transform.py +++ b/lib/cartopy/tests/test_vector_transform.py @@ -11,7 +11,7 @@ try: import scipy # noqa: F401 except ImportError: - pytest.skip("scipy is required for vector transforms", allow_module_level=True) + pytest.skip('scipy is required for vector transforms', allow_module_level=True) import cartopy.crs as ccrs @@ -30,12 +30,11 @@ def _sample_plate_carree_scalar_field(): def _sample_plate_carree_vector_field(): u = np.array([2, 4, 2, 1.2, 3, 1.2]) - v = np.array([5.5, 4, 5.5, 1.2, .3, 1.2]) + v = np.array([5.5, 4, 5.5, 1.2, 0.3, 1.2]) return u, v class Test_interpolate_to_grid: - @classmethod def setup_class(cls): cls.x, cls.y = _sample_plate_carree_coordinates() @@ -43,18 +42,31 @@ def setup_class(cls): def test_data_extent(self): # Interpolation to a grid with extents of the input data. - expected_x_grid = np.array([[-10., -5., 0., 5., 10.], - [-10., -5., 0., 5., 10.], - [-10., -5., 0., 5., 10.]]) - expected_y_grid = np.array([[5., 5., 5., 5., 5.], - [7.5, 7.5, 7.5, 7.5, 7.5], - [10., 10., 10., 10., 10]]) - expected_s_grid = np.array([[np.nan, 2., 3., 2., np.nan], - [np.nan, 2.5, 3.5, 2.5, np.nan], - [2., 3., 4., 3., 2.]]) + expected_x_grid = np.array( + [ + [-10.0, -5.0, 0.0, 5.0, 10.0], + [-10.0, -5.0, 0.0, 5.0, 10.0], + [-10.0, -5.0, 0.0, 5.0, 10.0], + ] + ) + expected_y_grid = np.array( + [ + [5.0, 5.0, 5.0, 5.0, 5.0], + [7.5, 7.5, 7.5, 7.5, 7.5], + [10.0, 10.0, 10.0, 10.0, 10], + ] + ) + expected_s_grid = np.array( + [ + [np.nan, 2.0, 3.0, 2.0, np.nan], + [np.nan, 2.5, 3.5, 2.5, np.nan], + [2.0, 3.0, 4.0, 3.0, 2.0], + ] + ) x_grid, y_grid, s_grid = vec_trans._interpolate_to_grid( - 5, 3, self.x, self.y, self.s) + 5, 3, self.x, self.y, self.s + ) assert_array_equal(x_grid, expected_x_grid) assert_array_equal(y_grid, expected_y_grid) @@ -62,16 +74,14 @@ def test_data_extent(self): def test_explicit_extent(self): # Interpolation to a grid with explicit extents. - expected_x_grid = np.array([[-5., 0., 5., 10.], - [-5., 0., 5., 10.]]) - expected_y_grid = np.array([[7.5, 7.5, 7.5, 7.5], - [10., 10., 10., 10]]) - expected_s_grid = np.array([[2.5, 3.5, 2.5, np.nan], - [3., 4., 3., 2.]]) + expected_x_grid = np.array([[-5.0, 0.0, 5.0, 10.0], [-5.0, 0.0, 5.0, 10.0]]) + expected_y_grid = np.array([[7.5, 7.5, 7.5, 7.5], [10.0, 10.0, 10.0, 10]]) + expected_s_grid = np.array([[2.5, 3.5, 2.5, np.nan], [3.0, 4.0, 3.0, 2.0]]) extent = (-5, 10, 7.5, 10) x_grid, y_grid, s_grid = vec_trans._interpolate_to_grid( - 4, 2, self.x, self.y, self.s, target_extent=extent) + 4, 2, self.x, self.y, self.s, target_extent=extent + ) assert_array_equal(x_grid, expected_x_grid) assert_array_equal(y_grid, expected_y_grid) @@ -79,19 +89,31 @@ def test_explicit_extent(self): def test_multiple_fields(self): # Interpolation of multiple fields in one go. - expected_x_grid = np.array([[-10., -5., 0., 5., 10.], - [-10., -5., 0., 5., 10.], - [-10., -5., 0., 5., 10.]]) - expected_y_grid = np.array([[5., 5., 5., 5., 5.], - [7.5, 7.5, 7.5, 7.5, 7.5], - [10., 10., 10., 10., 10]]) - expected_s_grid = np.array([[np.nan, 2., 3., 2., np.nan], - [np.nan, 2.5, 3.5, 2.5, np.nan], - [2., 3., 4., 3., 2.]]) - - x_grid, y_grid, s_grid1, s_grid2, s_grid3 = \ - vec_trans._interpolate_to_grid(5, 3, self.x, self.y, - self.s, self.s, self.s) + expected_x_grid = np.array( + [ + [-10.0, -5.0, 0.0, 5.0, 10.0], + [-10.0, -5.0, 0.0, 5.0, 10.0], + [-10.0, -5.0, 0.0, 5.0, 10.0], + ] + ) + expected_y_grid = np.array( + [ + [5.0, 5.0, 5.0, 5.0, 5.0], + [7.5, 7.5, 7.5, 7.5, 7.5], + [10.0, 10.0, 10.0, 10.0, 10], + ] + ) + expected_s_grid = np.array( + [ + [np.nan, 2.0, 3.0, 2.0, np.nan], + [np.nan, 2.5, 3.5, 2.5, np.nan], + [2.0, 3.0, 4.0, 3.0, 2.0], + ] + ) + + x_grid, y_grid, s_grid1, s_grid2, s_grid3 = vec_trans._interpolate_to_grid( + 5, 3, self.x, self.y, self.s, self.s, self.s + ) assert_array_equal(x_grid, expected_x_grid) assert_array_equal(y_grid, expected_y_grid) @@ -101,7 +123,6 @@ def test_multiple_fields(self): class Test_vector_scalar_to_grid: - @classmethod def setup_class(cls): cls.x, cls.y = _sample_plate_carree_coordinates() @@ -110,22 +131,39 @@ def setup_class(cls): def test_no_transform(self): # Transform and regrid vector (with no projection transform). - expected_x_grid = np.array([[-10., -5., 0., 5., 10.], - [-10., -5., 0., 5., 10.], - [-10., -5., 0., 5., 10.]]) - expected_y_grid = np.array([[5., 5., 5., 5., 5.], - [7.5, 7.5, 7.5, 7.5, 7.5], - [10., 10., 10., 10., 10]]) - expected_u_grid = np.array([[np.nan, 2., 3., 2., np.nan], - [np.nan, 2.5, 3.5, 2.5, np.nan], - [2., 3., 4., 3., 2.]]) - expected_v_grid = np.array([[np.nan, .8, .3, .8, np.nan], - [np.nan, 2.675, 2.15, 2.675, np.nan], - [5.5, 4.75, 4., 4.75, 5.5]]) + expected_x_grid = np.array( + [ + [-10.0, -5.0, 0.0, 5.0, 10.0], + [-10.0, -5.0, 0.0, 5.0, 10.0], + [-10.0, -5.0, 0.0, 5.0, 10.0], + ] + ) + expected_y_grid = np.array( + [ + [5.0, 5.0, 5.0, 5.0, 5.0], + [7.5, 7.5, 7.5, 7.5, 7.5], + [10.0, 10.0, 10.0, 10.0, 10], + ] + ) + expected_u_grid = np.array( + [ + [np.nan, 2.0, 3.0, 2.0, np.nan], + [np.nan, 2.5, 3.5, 2.5, np.nan], + [2.0, 3.0, 4.0, 3.0, 2.0], + ] + ) + expected_v_grid = np.array( + [ + [np.nan, 0.8, 0.3, 0.8, np.nan], + [np.nan, 2.675, 2.15, 2.675, np.nan], + [5.5, 4.75, 4.0, 4.75, 5.5], + ] + ) src_crs = target_crs = ccrs.PlateCarree() x_grid, y_grid, u_grid, v_grid = vec_trans.vector_scalar_to_grid( - src_crs, target_crs, (5, 3), self.x, self.y, self.u, self.v) + src_crs, target_crs, (5, 3), self.x, self.y, self.u, self.v + ) assert_array_equal(x_grid, expected_x_grid) assert_array_equal(y_grid, expected_y_grid) @@ -137,28 +175,48 @@ def test_with_transform(self): target_crs = ccrs.PlateCarree() src_crs = ccrs.NorthPolarStereo() - input_coords = [src_crs.transform_point(xp, yp, target_crs) - for xp, yp in zip(self.x, self.y)] + input_coords = [ + src_crs.transform_point(xp, yp, target_crs) + for xp, yp in zip(self.x, self.y) + ] x_nps = np.array([ic[0] for ic in input_coords]) y_nps = np.array([ic[1] for ic in input_coords]) - u_nps, v_nps = src_crs.transform_vectors(target_crs, self.x, self.y, - self.u, self.v) - - expected_x_grid = np.array([[-10., -5., 0., 5., 10.], - [-10., -5., 0., 5., 10.], - [-10., -5., 0., 5., 10.]]) - expected_y_grid = np.array([[5., 5., 5., 5., 5.], - [7.5, 7.5, 7.5, 7.5, 7.5], - [10., 10., 10., 10., 10]]) - expected_u_grid = np.array([[np.nan, np.nan, np.nan, np.nan, np.nan], - [np.nan, 2.3838, 3.5025, 2.6152, np.nan], - [2, 3.0043, 4, 2.9022, 2]]) - expected_v_grid = np.array([[np.nan, np.nan, np.nan, np.nan, np.nan], - [np.nan, 2.6527, 2.1904, 2.4192, np.nan], - [5.5, 4.6483, 4, 4.47, 5.5]]) + u_nps, v_nps = src_crs.transform_vectors( + target_crs, self.x, self.y, self.u, self.v + ) + + expected_x_grid = np.array( + [ + [-10.0, -5.0, 0.0, 5.0, 10.0], + [-10.0, -5.0, 0.0, 5.0, 10.0], + [-10.0, -5.0, 0.0, 5.0, 10.0], + ] + ) + expected_y_grid = np.array( + [ + [5.0, 5.0, 5.0, 5.0, 5.0], + [7.5, 7.5, 7.5, 7.5, 7.5], + [10.0, 10.0, 10.0, 10.0, 10], + ] + ) + expected_u_grid = np.array( + [ + [np.nan, np.nan, np.nan, np.nan, np.nan], + [np.nan, 2.3838, 3.5025, 2.6152, np.nan], + [2, 3.0043, 4, 2.9022, 2], + ] + ) + expected_v_grid = np.array( + [ + [np.nan, np.nan, np.nan, np.nan, np.nan], + [np.nan, 2.6527, 2.1904, 2.4192, np.nan], + [5.5, 4.6483, 4, 4.47, 5.5], + ] + ) x_grid, y_grid, u_grid, v_grid = vec_trans.vector_scalar_to_grid( - src_crs, target_crs, (5, 3), x_nps, y_nps, u_nps, v_nps) + src_crs, target_crs, (5, 3), x_nps, y_nps, u_nps, v_nps + ) assert_array_almost_equal(x_grid, expected_x_grid) assert_array_almost_equal(y_grid, expected_y_grid) @@ -170,27 +228,46 @@ def test_with_transform(self): def test_with_scalar_field(self): # Transform and regrid vector (with no projection transform) with an # additional scalar field. - expected_x_grid = np.array([[-10., -5., 0., 5., 10.], - [-10., -5., 0., 5., 10.], - [-10., -5., 0., 5., 10.]]) - expected_y_grid = np.array([[5., 5., 5., 5., 5.], - [7.5, 7.5, 7.5, 7.5, 7.5], - [10., 10., 10., 10., 10]]) - expected_u_grid = np.array([[np.nan, 2., 3., 2., np.nan], - [np.nan, 2.5, 3.5, 2.5, np.nan], - [2., 3., 4., 3., 2.]]) - expected_v_grid = np.array([[np.nan, .8, .3, .8, np.nan], - [np.nan, 2.675, 2.15, 2.675, np.nan], - [5.5, 4.75, 4., 4.75, 5.5]]) - expected_s_grid = np.array([[np.nan, 2., 3., 2., np.nan], - [np.nan, 2.5, 3.5, 2.5, np.nan], - [2., 3., 4., 3., 2.]]) + expected_x_grid = np.array( + [ + [-10.0, -5.0, 0.0, 5.0, 10.0], + [-10.0, -5.0, 0.0, 5.0, 10.0], + [-10.0, -5.0, 0.0, 5.0, 10.0], + ] + ) + expected_y_grid = np.array( + [ + [5.0, 5.0, 5.0, 5.0, 5.0], + [7.5, 7.5, 7.5, 7.5, 7.5], + [10.0, 10.0, 10.0, 10.0, 10], + ] + ) + expected_u_grid = np.array( + [ + [np.nan, 2.0, 3.0, 2.0, np.nan], + [np.nan, 2.5, 3.5, 2.5, np.nan], + [2.0, 3.0, 4.0, 3.0, 2.0], + ] + ) + expected_v_grid = np.array( + [ + [np.nan, 0.8, 0.3, 0.8, np.nan], + [np.nan, 2.675, 2.15, 2.675, np.nan], + [5.5, 4.75, 4.0, 4.75, 5.5], + ] + ) + expected_s_grid = np.array( + [ + [np.nan, 2.0, 3.0, 2.0, np.nan], + [np.nan, 2.5, 3.5, 2.5, np.nan], + [2.0, 3.0, 4.0, 3.0, 2.0], + ] + ) src_crs = target_crs = ccrs.PlateCarree() - x_grid, y_grid, u_grid, v_grid, s_grid = \ - vec_trans.vector_scalar_to_grid(src_crs, target_crs, (5, 3), - self.x, self.y, - self.u, self.v, self.s) + x_grid, y_grid, u_grid, v_grid, s_grid = vec_trans.vector_scalar_to_grid( + src_crs, target_crs, (5, 3), self.x, self.y, self.u, self.v, self.s + ) assert_array_equal(x_grid, expected_x_grid) assert_array_equal(y_grid, expected_y_grid) @@ -201,28 +278,53 @@ def test_with_scalar_field(self): def test_with_scalar_field_non_ndarray_data(self): # Transform and regrid vector (with no projection transform) with an # additional scalar field which is not a ndarray. - expected_x_grid = np.array([[-10., -5., 0., 5., 10.], - [-10., -5., 0., 5., 10.], - [-10., -5., 0., 5., 10.]]) - expected_y_grid = np.array([[5., 5., 5., 5., 5.], - [7.5, 7.5, 7.5, 7.5, 7.5], - [10., 10., 10., 10., 10]]) - expected_u_grid = np.array([[np.nan, 2., 3., 2., np.nan], - [np.nan, 2.5, 3.5, 2.5, np.nan], - [2., 3., 4., 3., 2.]]) - expected_v_grid = np.array([[np.nan, .8, .3, .8, np.nan], - [np.nan, 2.675, 2.15, 2.675, np.nan], - [5.5, 4.75, 4., 4.75, 5.5]]) - expected_s_grid = np.array([[np.nan, 2., 3., 2., np.nan], - [np.nan, 2.5, 3.5, 2.5, np.nan], - [2., 3., 4., 3., 2.]]) + expected_x_grid = np.array( + [ + [-10.0, -5.0, 0.0, 5.0, 10.0], + [-10.0, -5.0, 0.0, 5.0, 10.0], + [-10.0, -5.0, 0.0, 5.0, 10.0], + ] + ) + expected_y_grid = np.array( + [ + [5.0, 5.0, 5.0, 5.0, 5.0], + [7.5, 7.5, 7.5, 7.5, 7.5], + [10.0, 10.0, 10.0, 10.0, 10], + ] + ) + expected_u_grid = np.array( + [ + [np.nan, 2.0, 3.0, 2.0, np.nan], + [np.nan, 2.5, 3.5, 2.5, np.nan], + [2.0, 3.0, 4.0, 3.0, 2.0], + ] + ) + expected_v_grid = np.array( + [ + [np.nan, 0.8, 0.3, 0.8, np.nan], + [np.nan, 2.675, 2.15, 2.675, np.nan], + [5.5, 4.75, 4.0, 4.75, 5.5], + ] + ) + expected_s_grid = np.array( + [ + [np.nan, 2.0, 3.0, 2.0, np.nan], + [np.nan, 2.5, 3.5, 2.5, np.nan], + [2.0, 3.0, 4.0, 3.0, 2.0], + ] + ) src_crs = target_crs = ccrs.PlateCarree() - x_grid, y_grid, u_grid, v_grid, s_grid = \ - vec_trans.vector_scalar_to_grid(src_crs, target_crs, (5, 3), - list(self.x), list(self.y), - list(self.u), list(self.v), - list(self.s)) + x_grid, y_grid, u_grid, v_grid, s_grid = vec_trans.vector_scalar_to_grid( + src_crs, + target_crs, + (5, 3), + list(self.x), + list(self.y), + list(self.u), + list(self.v), + list(self.s), + ) assert_array_equal(x_grid, expected_x_grid) assert_array_equal(y_grid, expected_y_grid) diff --git a/lib/cartopy/util.py b/lib/cartopy/util.py index 28142de35..31e5efb56 100644 --- a/lib/cartopy/util.py +++ b/lib/cartopy/util.py @@ -6,6 +6,7 @@ Utilities that are useful in conjunction with cartopy. """ + import numpy as np import numpy.ma as ma @@ -68,10 +69,12 @@ def add_cyclic_point(data, coord=None, axis=-1): if coord.ndim != 1: raise ValueError('The coordinate must be 1-dimensional.') if len(coord) != data.shape[axis]: - raise ValueError(f'The length of the coordinate does not match ' - f'the size of the corresponding dimension of ' - f'the data array: len(coord) = {len(coord)}, ' - f'data.shape[{axis}] = {data.shape[axis]}.') + raise ValueError( + f'The length of the coordinate does not match ' + f'the size of the corresponding dimension of ' + f'the data array: len(coord) = {len(coord)}, ' + f'data.shape[{axis}] = {data.shape[axis]}.' + ) delta_coord = np.diff(coord) if not np.allclose(delta_coord, delta_coord[0]): raise ValueError('The coordinate must be equally spaced.') @@ -80,8 +83,9 @@ def add_cyclic_point(data, coord=None, axis=-1): try: slicer[axis] = slice(0, 1) except IndexError: - raise ValueError('The specified axis does not correspond to an ' - 'array dimension.') + raise ValueError( + 'The specified axis does not correspond to an array dimension.' + ) new_data = ma.concatenate((data, data[tuple(slicer)]), axis=axis) if coord is None: return_value = new_data @@ -112,7 +116,8 @@ def _add_cyclic_data(data, axis=-1): slicer[axis] = slice(0, 1) except IndexError: raise ValueError( - 'The specified axis does not correspond to an array dimension.') + 'The specified axis does not correspond to an array dimension.' + ) npc = np.ma if np.ma.is_masked(data) else np return npc.concatenate((data, data[tuple(slicer)]), axis=axis) @@ -141,9 +146,9 @@ def _add_cyclic_x(x, axis=-1, cyclic=360): # get cyclic x-coordinates # cx is the code from basemap (addcyclic) # https://github.com/matplotlib/basemap/blob/master/lib/mpl_toolkits/basemap/__init__.py - cx = (np.take(x, [0], axis=axis) + - cyclic * np.sign(np.diff(np.take(x, [0, -1], axis=axis), - axis=axis))) + cx = np.take(x, [0], axis=axis) + cyclic * np.sign( + np.diff(np.take(x, [0, -1], axis=axis), axis=axis) + ) # basemap ensures that the values do not exceed cyclic # (next code line). We do not do this to deal with rotated grids that # might have values not exactly 0. @@ -207,8 +212,7 @@ def has_cyclic(x, axis=-1, cyclic=360, precision=1e-4): return False -def add_cyclic(data, x=None, y=None, axis=-1, - cyclic=360, precision=1e-4): +def add_cyclic(data, x=None, y=None, axis=-1, cyclic=360, precision=1e-4): """ Add a cyclic point to an array and optionally corresponding x/longitude and y/latitude coordinates. @@ -373,10 +377,12 @@ def add_cyclic(data, x=None, y=None, axis=-1, else: xaxis = -1 if x.shape[xaxis] != data.shape[axis]: - estr = (f'x.shape[{xaxis}] does not match the size of the' - f' corresponding dimension of the data array:' - f' x.shape[{xaxis}] = {x.shape[xaxis]},' - f' data.shape[{axis}] = {data.shape[axis]}.') + estr = ( + f'x.shape[{xaxis}] does not match the size of the' + f' corresponding dimension of the data array:' + f' x.shape[{xaxis}] = {x.shape[xaxis]},' + f' data.shape[{axis}] = {data.shape[axis]}.' + ) raise ValueError(estr) if has_cyclic(x, axis=xaxis, cyclic=cyclic, precision=precision): if y is None: @@ -396,10 +402,12 @@ def add_cyclic(data, x=None, y=None, axis=-1, else: yaxis = -1 if y.shape[yaxis] != data.shape[axis]: - estr = (f'y.shape[{yaxis}] does not match the size of the' - f' corresponding dimension of the data array:' - f' y.shape[{yaxis}] = {y.shape[yaxis]},' - f' data.shape[{axis}] = {data.shape[axis]}.') + estr = ( + f'y.shape[{yaxis}] does not match the size of the' + f' corresponding dimension of the data array:' + f' y.shape[{yaxis}] = {y.shape[yaxis]},' + f' data.shape[{axis}] = {data.shape[axis]}.' + ) raise ValueError(estr) out_y = _add_cyclic_data(y, axis=yaxis) return out_data, out_x, out_y diff --git a/lib/cartopy/vector_transform.py b/lib/cartopy/vector_transform.py index b52212a58..e248bd09c 100644 --- a/lib/cartopy/vector_transform.py +++ b/lib/cartopy/vector_transform.py @@ -13,7 +13,7 @@ try: from scipy.interpolate import griddata except ImportError as e: - raise ImportError("Regridding vectors requires scipy.") from e + raise ImportError('Regridding vectors requires scipy.') from e def _interpolate_to_grid(nx, ny, x, y, *scalars, **kwargs): @@ -50,17 +50,18 @@ def _interpolate_to_grid(nx, ny, x, y, *scalars, **kwargs): xr = x1 - x0 yr = y1 - y0 points = np.column_stack([(x.ravel() - x0) / xr, (y.ravel() - y0) / yr]) - x_grid, y_grid = np.meshgrid(np.linspace(0, 1, nx), - np.linspace(0, 1, ny)) + x_grid, y_grid = np.meshgrid(np.linspace(0, 1, nx), np.linspace(0, 1, ny)) s_grid_tuple = tuple() for s in scalars: - s_grid_tuple += (griddata(points, s.ravel(), (x_grid, y_grid), - method='linear'),) + s_grid_tuple += ( + griddata(points, s.ravel(), (x_grid, y_grid), method='linear'), + ) return (x_grid * xr + x0, y_grid * yr + y0) + s_grid_tuple -def vector_scalar_to_grid(src_crs, target_proj, regrid_shape, x, y, u, v, - *scalars, **kwargs): +def vector_scalar_to_grid( + src_crs, target_proj, regrid_shape, x, y, u, v, *scalars, **kwargs +): """ Transform and interpolate a vector field to a regular grid in the target projection. @@ -119,16 +120,19 @@ def vector_scalar_to_grid(src_crs, target_proj, regrid_shape, x, y, u, v, if x.shape != u.shape: x, y = np.meshgrid(x, y) if not (x.shape == y.shape == u.shape): - raise ValueError('x and y coordinates are not compatible ' - 'with the shape of the vector components') + raise ValueError( + 'x and y coordinates are not compatible ' + 'with the shape of the vector components' + ) if scalars: np_like_scalars = () for s in scalars: s = np.asanyarray(s) np_like_scalars = np_like_scalars + (s,) if s.shape != u.shape: - raise ValueError('scalar fields must have the same ' - 'shape as the vector components') + raise ValueError( + 'scalar fields must have the same shape as the vector components' + ) scalars = np_like_scalars try: nx, ny = regrid_shape @@ -161,14 +165,12 @@ def vector_scalar_to_grid(src_crs, target_proj, regrid_shape, x, y, u, v, # projection to account for original points outside the wrapped domain xyz = src_crs.transform_points(src_crs, x, y) x, y = xyz[..., 0], xyz[..., 1] - points = np.column_stack([(x.ravel() - x0) / xr, - (y.ravel() - y0) / yr]) + points = np.column_stack([(x.ravel() - x0) / xr, (y.ravel() - y0) / yr]) newx = (sourcex - x0) / xr newy = (sourcey - y0) / yr s_grid_tuple = tuple() for s in (u, v) + scalars: - s_grid_tuple += (griddata(points, s.ravel(), (newx, newy), - method='linear'),) + s_grid_tuple += (griddata(points, s.ravel(), (newx, newy), method='linear'),) u, v = s_grid_tuple[0], s_grid_tuple[1] # Finally, transform the vectors (in the source frame) to the target CRS. diff --git a/pyproject.toml b/pyproject.toml index f0172aac6..058bcfde9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,7 +98,10 @@ testpaths = ["lib"] python_files = ["test_*.py"] [tool.ruff] -lint.select = ["E", "F", "I", "W"] +lint.select = ["E", "F", "I", "RUF022", "W"] + +[tool.ruff.format] +quote-style = "single" [tool.ruff.lint.isort] force-sort-within-sections = true diff --git a/setup.py b/setup.py index 0967754db..1430dc67e 100644 --- a/setup.py +++ b/setup.py @@ -19,9 +19,9 @@ USE_CYTHON = not IS_SDIST or FORCE_CYTHON if USE_CYTHON: import Cython + if Cython.__version__ < '0.29': - raise ImportError( - "Cython 0.29+ is required to install cartopy from source.") + raise ImportError('Cython 0.29+ is required to install cartopy from source.') ext = '.pyx' else: ext = '.cpp' @@ -37,14 +37,16 @@ [f'lib/cartopy/trace{ext}'], include_dirs=[np.get_include()], language='c++', - define_macros=define_macros), + define_macros=define_macros, + ), ] if USE_CYTHON: # We need to explicitly cythonize the extension in order # to control the Cython compiler_directives. from Cython.Build import cythonize - compiler_directives = {"profile": True, "linetrace": True} + + compiler_directives = {'profile': True, 'linetrace': True} extensions = cythonize(extensions, compiler_directives=compiler_directives)