From 1654f0afb241c242b3991cccaa76cf0dcc413cb9 Mon Sep 17 00:00:00 2001 From: lgolston <30876419+lgolston@users.noreply.github.com> Date: Sat, 16 Mar 2024 21:05:23 -0500 Subject: [PATCH] Allow version specific NE --- lib/cartopy/feature/__init__.py | 16 +++++++--- lib/cartopy/io/shapereader.py | 43 ++++++++++++++++++++++---- lib/cartopy/tests/mpl/test_features.py | 2 +- lib/cartopy/tests/test_shapereader.py | 3 +- 4 files changed, 51 insertions(+), 13 deletions(-) diff --git a/lib/cartopy/feature/__init__.py b/lib/cartopy/feature/__init__.py index 295921746..3910352bc 100644 --- a/lib/cartopy/feature/__init__.py +++ b/lib/cartopy/feature/__init__.py @@ -230,11 +230,13 @@ class NaturalEarthFeature(Feature): """ A simple interface to Natural Earth shapefiles. - See https://www.naturalearthdata.com/ + See https://www.naturalearthdata.com/ for an overview of the data + and https://github.com/nvkelso/natural-earth-vector/releases for recent + version information. """ - def __init__(self, category, name, scale, **kwargs): + def __init__(self, category, name, scale, version=None, **kwargs): """ Parameters ---------- @@ -246,6 +248,8 @@ def __init__(self, category, name, scale, **kwargs): The dataset scale, i.e. one of '10m', '50m', or '110m', or Scaler object. Dataset scales correspond to 1:10,000,000, 1:50,000,000, and 1:110,000,000 respectively. + version: optional + The specific dataset version to use, e.g. '5.1.0'. Other Parameters ---------------- @@ -256,6 +260,7 @@ def __init__(self, category, name, scale, **kwargs): super().__init__(cartopy.crs.PlateCarree(), **kwargs) self.category = category self.name = name + self.version = version # Cast the given scale to a (constant) Scaler if a string is passed. if isinstance(scale, str): @@ -281,11 +286,12 @@ def geometries(self): Returns an iterator of (shapely) geometries for this feature. """ - key = (self.name, self.category, self.scale) + key = (self.name, self.category, self.scale, self.version) if key not in _NATURAL_EARTH_GEOM_CACHE: path = shapereader.natural_earth(resolution=self.scale, category=self.category, - name=self.name) + name=self.name, + version=self.version) geometries = tuple(shapereader.Reader(path).geometries()) _NATURAL_EARTH_GEOM_CACHE[key] = geometries else: @@ -316,7 +322,7 @@ def with_scale(self, new_scale): """ return NaturalEarthFeature(self.category, self.name, new_scale, - **self.kwargs) + self.version, **self.kwargs) class GSHHSFeature(Feature): diff --git a/lib/cartopy/io/shapereader.py b/lib/cartopy/io/shapereader.py index a9fe8820b..86d71fc05 100644 --- a/lib/cartopy/io/shapereader.py +++ b/lib/cartopy/io/shapereader.py @@ -274,10 +274,12 @@ def records(self): """ -def natural_earth(resolution='110m', category='physical', name='coastline'): +def natural_earth(resolution='110m', category='physical', + name='coastline', version=None): """ Return the path to the requested natural earth shapefile, - downloading and unzipping if necessary. + downloading and unzipping if necessary. If version is not specified, + the latest available version will be downloaded. To identify valid components for this function, either browse NaturalEarthData.com, or if you know what you are looking for, go to @@ -298,10 +300,14 @@ 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} + if version is None: + ne_downloader = Downloader.from_config(('shapefiles', 'natural_earth', + resolution, category, name)) + else: + ne_downloader = Downloader.from_config(('shapefiles', 'natural_earth_versioned', + resolution, category, name)) + format_dict = {'config': config, 'category': category, 'name': name, + 'resolution': resolution, 'version': version} return ne_downloader.path(format_dict) @@ -394,6 +400,27 @@ def default_downloader(): return NEShpDownloader(target_path_template=ne_path_template, pre_downloaded_path_template=pre_path_template) + @staticmethod + def versioned_downloader(): + """ + Return a NEShpDownloader instance, which extends the default downloader + with support for specific file versions. + + """ + _NE_URL_TEMPLATE = ('https://naturalearth.s3.amazonaws.com/' + '{version}/{resolution}_{category}/' + 'ne_{resolution}_{name}.zip') + + default_spec = ('shapefiles', 'natural_earth', '{category}', '{version}', + '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(url_template=_NE_URL_TEMPLATE, + 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. @@ -401,6 +428,10 @@ def default_downloader(): config['downloaders'].setdefault(_ne_key, NEShpDownloader.default_downloader()) +_ne_key = ('shapefiles', 'natural_earth_versioned') +config['downloaders'].setdefault(_ne_key, + NEShpDownloader.versioned_downloader()) + def gshhs(scale='c', level=1): """ diff --git a/lib/cartopy/tests/mpl/test_features.py b/lib/cartopy/tests/mpl/test_features.py index 1b1b7fea1..684a05926 100644 --- a/lib/cartopy/tests/mpl/test_features.py +++ b/lib/cartopy/tests/mpl/test_features.py @@ -37,7 +37,7 @@ def test_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', + feature = cfeature.NaturalEarthFeature('physical', 'coastline', '50m', '5.1.0', edgecolor='black', facecolor='none') ax.add_feature(feature) diff --git a/lib/cartopy/tests/test_shapereader.py b/lib/cartopy/tests/test_shapereader.py index 66c4e4d6e..0c6590ffb 100644 --- a/lib/cartopy/tests/test_shapereader.py +++ b/lib/cartopy/tests/test_shapereader.py @@ -83,7 +83,8 @@ class TestRivers: def setup_class(self): RIVERS_PATH = shp.natural_earth(resolution='110m', category='physical', - name='rivers_lake_centerlines') + name='rivers_lake_centerlines', + version='5.0.0') self.reader = shp.Reader(RIVERS_PATH) names = [record.attributes['name'] for record in self.reader.records()] # Choose a nice small river