From 7e4c411c42580063955112a2729e3113ca9244cf Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Mon, 3 Jul 2023 17:35:28 +1200 Subject: [PATCH 01/10] Allow passing RGB xarray.DataArray images into grdimage Saving 3-band xarray.DataArray images to a temporary GeoTIFF so that they can be plotted with grdimage. --- pygmt/src/grdimage.py | 35 +++++++---- .../baseline/test_grdimage_image.png.dvc | 4 ++ pygmt/tests/test_grdimage_image.py | 63 +++++++++++++++++++ 3 files changed, 89 insertions(+), 13 deletions(-) create mode 100644 pygmt/tests/baseline/test_grdimage_image.png.dvc create mode 100644 pygmt/tests/test_grdimage_image.py diff --git a/pygmt/src/grdimage.py b/pygmt/src/grdimage.py index 93e1fffb2c4..91322ef2b62 100644 --- a/pygmt/src/grdimage.py +++ b/pygmt/src/grdimage.py @@ -5,6 +5,7 @@ from pygmt.clib import Session from pygmt.helpers import ( + GMTTempFile, build_arg_string, data_kind, fmt_docstring, @@ -179,17 +180,25 @@ def grdimage(self, grid, **kwargs): >>> fig.show() """ kwargs = self._preprocess(**kwargs) # pylint: disable=protected-access - with Session() as lib: - file_context = lib.virtualfile_from_data(check_kind="raster", data=grid) - with contextlib.ExitStack() as stack: - # shading using an xr.DataArray - if kwargs.get("I") is not None and data_kind(kwargs["I"]) == "grid": - shading_context = lib.virtualfile_from_data( - check_kind="raster", data=kwargs["I"] - ) - kwargs["I"] = stack.enter_context(shading_context) - fname = stack.enter_context(file_context) - lib.call_module( - module="grdimage", args=build_arg_string(kwargs, infile=fname) - ) + with GMTTempFile(suffix=".tif") as tmpfile: + if hasattr(grid, "dims") and len(grid.dims) == 3: + grid.rio.to_raster(raster_path=tmpfile.name) + _grid = tmpfile.name + else: + _grid = grid + + with Session() as lib: + file_context = lib.virtualfile_from_data(check_kind="raster", data=_grid) + with contextlib.ExitStack() as stack: + # shading using an xr.DataArray + if kwargs.get("I") is not None and data_kind(kwargs["I"]) == "grid": + shading_context = lib.virtualfile_from_data( + check_kind="raster", data=kwargs["I"] + ) + kwargs["I"] = stack.enter_context(shading_context) + + fname = stack.enter_context(file_context) + lib.call_module( + module="grdimage", args=build_arg_string(kwargs, infile=fname) + ) diff --git a/pygmt/tests/baseline/test_grdimage_image.png.dvc b/pygmt/tests/baseline/test_grdimage_image.png.dvc new file mode 100644 index 00000000000..4af74249741 --- /dev/null +++ b/pygmt/tests/baseline/test_grdimage_image.png.dvc @@ -0,0 +1,4 @@ +outs: +- md5: 2e919645d5af956ec4f8aa054a86a70a + size: 110214 + path: test_grdimage_image.png diff --git a/pygmt/tests/test_grdimage_image.py b/pygmt/tests/test_grdimage_image.py new file mode 100644 index 00000000000..1a2158ba84d --- /dev/null +++ b/pygmt/tests/test_grdimage_image.py @@ -0,0 +1,63 @@ +""" +Test Figure.grdimage on 3-band RGB images. +""" +import numpy as np +import pandas as pd +import pytest +import xarray as xr +from pygmt import Figure, which + +rasterio = pytest.importorskip("rasterio") +rioxarray = pytest.importorskip("rioxarray") + + +@pytest.fixture(scope="module", name="xr_image") +def fixture_xr_image(): + """ + Load the image data from Blue Marble as an xarray.DataArray with shape + {"band": 3, "y": 180, "x": 360}. + """ + geotiff = which(fname="@earth_day_01d_p", download="c") + with rioxarray.open_rasterio(filename=geotiff) as rda: + if len(rda.band) == 1: + with rasterio.open(fp=geotiff) as src: + df_colormap = pd.DataFrame.from_dict( + data=src.colormap(1), orient="index" + ) + array = src.read() + + red = np.vectorize(df_colormap[0].get)(array) + green = np.vectorize(df_colormap[1].get)(array) + blue = np.vectorize(df_colormap[2].get)(array) + # alpha = np.vectorize(df_colormap[3].get)(array) + + rda.data = red + da_red = rda.astype(dtype=np.uint8).copy() + rda.data = green + da_green = rda.astype(dtype=np.uint8).copy() + rda.data = blue + da_blue = rda.astype(dtype=np.uint8).copy() + + xr_image = xr.concat(objs=[da_red, da_green, da_blue], dim="band") + assert xr_image.sizes == {"band": 3, "y": 180, "x": 360} + return xr_image + + +@pytest.mark.mpl_image_compare +def test_grdimage_image(): + """ + Plot a 3-band RGB image using file input. + """ + fig = Figure() + fig.grdimage(grid="@earth_day_01d") + return fig + + +@pytest.mark.mpl_image_compare(filename="test_grdimage_image.png") +def test_grdimage_image_dataarray(xr_image): + """ + Plot a 3-band RGB image using xarray.DataArray input. + """ + fig = Figure() + fig.grdimage(grid=xr_image) + return fig From 89da91603705b817d70bf60f08464ba817f40314 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Tue, 4 Jul 2023 17:51:04 +1200 Subject: [PATCH 02/10] Refactor to use tempfile_from_image Putting the temporary GeoTIFF creation logic in a dedicated tempfile_from_image helper function, so that it can be reused by other GMT modules besides grdimage. Also ensure that an ImportError is raised when the `.rio` attribute cannot be found when rioxarray is not installed. --- pygmt/clib/session.py | 12 +++++++++--- pygmt/helpers/__init__.py | 7 ++++++- pygmt/helpers/tempfile.py | 31 +++++++++++++++++++++++++++++++ pygmt/helpers/utils.py | 9 +++++++-- pygmt/src/grdimage.py | 34 +++++++++++++--------------------- 5 files changed, 66 insertions(+), 27 deletions(-) diff --git a/pygmt/clib/session.py b/pygmt/clib/session.py index d7f1b585f8d..796723ef8a8 100644 --- a/pygmt/clib/session.py +++ b/pygmt/clib/session.py @@ -25,7 +25,12 @@ GMTInvalidInput, GMTVersionError, ) -from pygmt.helpers import data_kind, fmt_docstring, tempfile_from_geojson +from pygmt.helpers import ( + data_kind, + fmt_docstring, + tempfile_from_geojson, + tempfile_from_image, +) FAMILIES = [ "GMT_IS_DATASET", # Entity is a data table @@ -1530,7 +1535,7 @@ def virtualfile_from_data( """ kind = data_kind(data, x, y, z, required_z=required_z) - if check_kind == "raster" and kind not in ("file", "grid"): + if check_kind == "raster" and kind not in ("file", "grid", "image"): raise GMTInvalidInput(f"Unrecognized data type for grid: {type(data)}") if check_kind == "vector" and kind not in ( "file", @@ -1545,6 +1550,7 @@ def virtualfile_from_data( "file": nullcontext, "geojson": tempfile_from_geojson, "grid": self.virtualfile_from_grid, + "image": tempfile_from_image, # Note: virtualfile_from_matrix is not used because a matrix can be # converted to vectors instead, and using vectors allows for better # handling of string type inputs (e.g. for datetime data types) @@ -1553,7 +1559,7 @@ def virtualfile_from_data( }[kind] # Ensure the data is an iterable (Python list or tuple) - if kind in ("geojson", "grid"): + if kind in ("geojson", "grid", "image"): _data = (data,) elif kind == "file": # Useful to handle `pathlib.Path` and string file path alike diff --git a/pygmt/helpers/__init__.py b/pygmt/helpers/__init__.py index efea2845cc7..eabcb87500f 100644 --- a/pygmt/helpers/__init__.py +++ b/pygmt/helpers/__init__.py @@ -7,7 +7,12 @@ kwargs_to_strings, use_alias, ) -from pygmt.helpers.tempfile import GMTTempFile, tempfile_from_geojson, unique_name +from pygmt.helpers.tempfile import ( + GMTTempFile, + tempfile_from_geojson, + tempfile_from_image, + unique_name, +) from pygmt.helpers.utils import ( args_in_kwargs, build_arg_string, diff --git a/pygmt/helpers/tempfile.py b/pygmt/helpers/tempfile.py index c184c5e73d3..26835a3b98e 100644 --- a/pygmt/helpers/tempfile.py +++ b/pygmt/helpers/tempfile.py @@ -147,3 +147,34 @@ def tempfile_from_geojson(geojson): geoseries.to_file(**ogrgmt_kwargs) yield tmpfile.name + + +@contextmanager +def tempfile_from_image(image): + """ + Saves a 3-band `xarray.DataArray` to a temporary GeoTIFF file via + rioxarray. + + Parameters + ---------- + image : xarray.DataArray + An xarray.DataArray with three dimensions, having a shape like + (3, Y, X). + + Yields + ------ + tmpfilename : str + A temporary GeoTIFF file holding the image data. E.g. '1a2b3c4d5.tif'. + """ + with GMTTempFile(suffix=".tif") as tmpfile: + os.remove(tmpfile.name) # ensure file is deleted first + try: + image.rio.to_raster(raster_path=tmpfile.name) + except AttributeError as e: # object has no attribute 'rio' + raise ImportError( + "Package `rioxarray` is required to be installed to use this function. " + "Please use `python -m pip install rioxarray` or " + "`mamba install -c conda-forge rioxarray` " + "to install the package." + ) from e + yield tmpfile.name diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py index a7092a7ea9b..472db295f7b 100644 --- a/pygmt/helpers/utils.py +++ b/pygmt/helpers/utils.py @@ -45,7 +45,7 @@ def data_kind(data, x=None, y=None, z=None, required_z=False): Returns ------- kind : str - One of: ``'file'``, ``'grid'``, ``'matrix'``, ``'vectors'``. + One of: ``'file'``, ``'grid'``, ``image``, ``'matrix'``, ``'vectors'``. Examples -------- @@ -63,6 +63,8 @@ def data_kind(data, x=None, y=None, z=None, required_z=False): 'file' >>> data_kind(data=xr.DataArray(np.random.rand(4, 3))) 'grid' + >>> data_kind(data=xr.DataArray(np.random.rand(3, 4, 5))) + 'image' """ if data is None and x is None and y is None: raise GMTInvalidInput("No input data provided.") @@ -76,7 +78,10 @@ def data_kind(data, x=None, y=None, z=None, required_z=False): if isinstance(data, (str, pathlib.PurePath)): kind = "file" elif isinstance(data, xr.DataArray): - kind = "grid" + if len(data.dims) == 3: + kind = "image" + else: + kind = "grid" elif hasattr(data, "__geo_interface__"): kind = "geojson" elif data is not None: diff --git a/pygmt/src/grdimage.py b/pygmt/src/grdimage.py index 91322ef2b62..62bab0e75fc 100644 --- a/pygmt/src/grdimage.py +++ b/pygmt/src/grdimage.py @@ -5,7 +5,6 @@ from pygmt.clib import Session from pygmt.helpers import ( - GMTTempFile, build_arg_string, data_kind, fmt_docstring, @@ -181,24 +180,17 @@ def grdimage(self, grid, **kwargs): """ kwargs = self._preprocess(**kwargs) # pylint: disable=protected-access - with GMTTempFile(suffix=".tif") as tmpfile: - if hasattr(grid, "dims") and len(grid.dims) == 3: - grid.rio.to_raster(raster_path=tmpfile.name) - _grid = tmpfile.name - else: - _grid = grid - - with Session() as lib: - file_context = lib.virtualfile_from_data(check_kind="raster", data=_grid) - with contextlib.ExitStack() as stack: - # shading using an xr.DataArray - if kwargs.get("I") is not None and data_kind(kwargs["I"]) == "grid": - shading_context = lib.virtualfile_from_data( - check_kind="raster", data=kwargs["I"] - ) - kwargs["I"] = stack.enter_context(shading_context) - - fname = stack.enter_context(file_context) - lib.call_module( - module="grdimage", args=build_arg_string(kwargs, infile=fname) + with Session() as lib: + file_context = lib.virtualfile_from_data(check_kind="raster", data=grid) + with contextlib.ExitStack() as stack: + # shading using an xr.DataArray + if kwargs.get("I") is not None and data_kind(kwargs["I"]) == "grid": + shading_context = lib.virtualfile_from_data( + check_kind="raster", data=kwargs["I"] ) + kwargs["I"] = stack.enter_context(shading_context) + + fname = stack.enter_context(file_context) + lib.call_module( + module="grdimage", args=build_arg_string(kwargs, infile=fname) + ) From a77392909c9fd8ee44da78ace44267a2f39288de Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Tue, 4 Jul 2023 18:01:02 +1200 Subject: [PATCH 03/10] Let tilemap use tempfile_from_image func in virtualfile_from_data Refactor Figure.tilemap to use the same tempfile_from_image function that generates a temporary GeoTIFF file from the 3-band xarray.DataArray images. --- pygmt/src/tilemap.py | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/pygmt/src/tilemap.py b/pygmt/src/tilemap.py index ae614ae11fd..d6d18a46ee4 100644 --- a/pygmt/src/tilemap.py +++ b/pygmt/src/tilemap.py @@ -11,11 +11,6 @@ use_alias, ) -try: - import rioxarray -except ImportError: - rioxarray = None - @fmt_docstring @use_alias( @@ -120,14 +115,6 @@ def tilemap( """ kwargs = self._preprocess(**kwargs) # pylint: disable=protected-access - if rioxarray is None: - raise ImportError( - "Package `rioxarray` is required to be installed to use this function. " - "Please use `python -m pip install rioxarray` or " - "`mamba install -c conda-forge rioxarray` " - "to install the package." - ) - raster = load_tile_map( region=region, zoom=zoom, @@ -148,9 +135,9 @@ def tilemap( if kwargs.get("N") in [None, False]: kwargs["R"] = "/".join(str(coordinate) for coordinate in region) - with GMTTempFile(suffix=".tif") as tmpfile: - raster.rio.to_raster(raster_path=tmpfile.name) - with Session() as lib: + with Session() as lib: + file_context = lib.virtualfile_from_data(check_kind="raster", data=raster) + with file_context as infile: lib.call_module( - module="grdimage", args=build_arg_string(kwargs, infile=tmpfile.name) + module="grdimage", args=build_arg_string(kwargs, infile=infile) ) From 8c89da2db56042e23bd89be75ab21401e610dbb4 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Tue, 4 Jul 2023 18:03:44 +1200 Subject: [PATCH 04/10] Lint to remove unneeded imports --- pygmt/src/tilemap.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pygmt/src/tilemap.py b/pygmt/src/tilemap.py index d6d18a46ee4..7d1a36a8a49 100644 --- a/pygmt/src/tilemap.py +++ b/pygmt/src/tilemap.py @@ -3,13 +3,7 @@ """ from pygmt.clib import Session from pygmt.datasets.tile_map import load_tile_map -from pygmt.helpers import ( - GMTTempFile, - build_arg_string, - fmt_docstring, - kwargs_to_strings, - use_alias, -) +from pygmt.helpers import build_arg_string, fmt_docstring, kwargs_to_strings, use_alias @fmt_docstring From 819cc8b3288d1d64c94cbb15f58355474ea92e91 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Sat, 8 Jul 2023 17:53:51 +1200 Subject: [PATCH 05/10] Update docstring of grdimage with upstream GMT 6.4.0 Various updates from upstream GMT at https://github.com/GenericMappingTools/gmt/pull/6258, https://github.com/GenericMappingTools/gmt/commit/906996722687d728887e0e84b2822b9835963a03, https://github.com/GenericMappingTools/gmt/pull/7260. --- pygmt/src/grdimage.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/pygmt/src/grdimage.py b/pygmt/src/grdimage.py index 62bab0e75fc..9bff79af84f 100644 --- a/pygmt/src/grdimage.py +++ b/pygmt/src/grdimage.py @@ -49,11 +49,11 @@ def grdimage(self, grid, **kwargs): instructions to derive intensities from the input data grid. Values outside this range will be clipped. Such intensity files can be created from the grid using :func:`pygmt.grdgradient` and, optionally, modified by - :gmt-docs:`grdmath.html` or :class:`pygmt.grdhisteq`. If GMT is built - with GDAL support, ``grid`` can be an image file (geo-referenced or not). - In this case the image can optionally be illuminated with the file - provided via the ``shading`` parameter. Here, if image has no coordinates - then those of the intensity file will be used. + :gmt-docs:`grdmath.html` or :class:`pygmt.grdhisteq`. Alternatively, pass + *image* which can be an image file (geo-referenced or not). In this case + the image can optionally be illuminated with the file provided via the + ``shading`` parameter. Here, if image has no coordinates then those of the + intensity file will be used. When using map projections, the grid is first resampled on a new rectangular grid with the same dimensions. Higher resolution images can @@ -82,10 +82,7 @@ def grdimage(self, grid, **kwargs): :gmt-docs:`grdimage.html#grid-file-formats`). img_out : str *out_img*\[=\ *driver*]. - Save an image in a raster format instead of PostScript. Use - extension .ppm for a Portable Pixel Map format which is the only - raster format GMT can natively write. For GMT installations - configured with GDAL support there are more choices: Append + Save an image in a raster format instead of PostScript. Append *out_img* to select the image file name and extension. If the extension is one of .bmp, .gif, .jpg, .png, or .tif then no driver information is required. For other output formats you must append @@ -139,8 +136,8 @@ def grdimage(self, grid, **kwargs): :func:`pygmt.grdgradient` separately first. If we should derive intensities from another file than grid, specify the file with suitable modifiers [Default is no illumination]. **Note**: If the - input data is an *image* then an *intensfile* or constant *intensity* - must be provided. + input data represent an *image* then an *intensfile* or constant + *intensity* must be provided. {projection} monochrome : bool Force conversion to monochrome image using the (television) YIQ @@ -152,10 +149,9 @@ def grdimage(self, grid, **kwargs): [**+z**\ *value*][*color*] Make grid nodes with z = NaN transparent, using the color-masking feature in PostScript Level 3 (the PS device must support PS Level - 3). If the input is a grid, use **+z** with a *value* to select - another grid value than NaN. If the input is instead an image, - append an alternate *color* to select another pixel value to be - transparent [Default is ``"black"``]. + 3). If the input is a grid, use **+z** to select another grid value + than NaN. If input is instead an image, append an alternate *color* to + select another pixel value to be transparent [Default is ``"black"``]. {region} {verbose} {panel} From 6a0960618afe7c2e18f5c8d9d3e3ddaac711adbc Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Sat, 8 Jul 2023 19:04:01 +1200 Subject: [PATCH 06/10] Raise RuntimeWarning when input image dtype is not uint8 Plotting a non-uint8 dtype xarray.DataArray works in grdimage, but the results will likely be incorrect. Warning the user about the incorrect dtype, and suggest recasting to uint8 with 0-255 range, e.g. using a histogram equalization function like skimage.exposure.equalize_hist. --- pygmt/clib/session.py | 10 ++++++++++ pygmt/tests/test_grdimage_image.py | 16 ++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/pygmt/clib/session.py b/pygmt/clib/session.py index 796723ef8a8..3627c11ff40 100644 --- a/pygmt/clib/session.py +++ b/pygmt/clib/session.py @@ -6,6 +6,7 @@ """ import ctypes as ctp import sys +import warnings from contextlib import contextmanager, nullcontext import numpy as np @@ -1560,6 +1561,15 @@ def virtualfile_from_data( # Ensure the data is an iterable (Python list or tuple) if kind in ("geojson", "grid", "image"): + if kind == "image" and data.dtype != "uint8": + msg = ( + f"Input image has dtype: {data.dtype} which is unsupported, " + "and may result in an incorrect output. Please recast image " + "to a uint8 dtype and/or scale to 0-255 range, e.g. " + "using a histogram equalization function like " + "skimage.exposure.equalize_hist." + ) + warnings.warn(message=msg, category=RuntimeWarning, stacklevel=2) _data = (data,) elif kind == "file": # Useful to handle `pathlib.Path` and string file path alike diff --git a/pygmt/tests/test_grdimage_image.py b/pygmt/tests/test_grdimage_image.py index 1a2158ba84d..ade642605df 100644 --- a/pygmt/tests/test_grdimage_image.py +++ b/pygmt/tests/test_grdimage_image.py @@ -61,3 +61,19 @@ def test_grdimage_image_dataarray(xr_image): fig = Figure() fig.grdimage(grid=xr_image) return fig + + +@pytest.mark.parametrize( + "dtype", + ["int8", "uint16", "int16", "uint32", "int32", "float32", "float64"], +) +def test_grdimage_image_dataarray_unsupported_dtype(dtype, xr_image): + """ + Plot a 3-band RGB image using xarray.DataArray input, with an unsupported + data type. + """ + fig = Figure() + image = xr_image.astype(dtype=dtype) + with pytest.warns(expected_warning=RuntimeWarning) as record: + fig.grdimage(grid=image) + assert len(record) == 1 From fabcb09a799c5626bf099d44f3361b6239d64b34 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Sun, 9 Jul 2023 21:09:49 +1200 Subject: [PATCH 07/10] Update pygmt/helpers/utils.py Co-authored-by: Dongdong Tian --- pygmt/helpers/utils.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py index 3f7d2d0a053..bf0ca028983 100644 --- a/pygmt/helpers/utils.py +++ b/pygmt/helpers/utils.py @@ -136,10 +136,7 @@ def data_kind(data=None, x=None, y=None, z=None, required_z=False): if isinstance(data, (str, pathlib.PurePath)): kind = "file" elif isinstance(data, xr.DataArray): - if len(data.dims) == 3: - kind = "image" - else: - kind = "grid" + kind = "image" if len(data.dims) == 3 else "grid" elif hasattr(data, "__geo_interface__"): kind = "geojson" elif data is not None: From ba9a2a34dce71dab1276d1ab0f6f4f5473286997 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Fri, 21 Jul 2023 09:17:06 +1200 Subject: [PATCH 08/10] Set intersphinx link for xr.DataArray in docstring Co-authored-by: Dongdong Tian --- pygmt/helpers/tempfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/helpers/tempfile.py b/pygmt/helpers/tempfile.py index 26835a3b98e..8ac63006565 100644 --- a/pygmt/helpers/tempfile.py +++ b/pygmt/helpers/tempfile.py @@ -152,7 +152,7 @@ def tempfile_from_geojson(geojson): @contextmanager def tempfile_from_image(image): """ - Saves a 3-band `xarray.DataArray` to a temporary GeoTIFF file via + Saves a 3-band :class:`xarray.DataArray` to a temporary GeoTIFF file via rioxarray. Parameters From d1d692beb9d079646963beed4585c63f212b9b6d Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Tue, 8 Aug 2023 09:30:23 +1200 Subject: [PATCH 09/10] Add back rioxarray import and ImportError check to tilemap.py Partially revert a77392909c9fd8ee44da78ace44267a2f39288de to fix `AttributeError: 'DataArray' object has no attribute 'rio'` in `pygmt/src/tilemap.py`, because the reproject from EPSG:3857 to OGC:CRS84 step requires it. --- pygmt/src/tilemap.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pygmt/src/tilemap.py b/pygmt/src/tilemap.py index 7d1a36a8a49..483d38a01f3 100644 --- a/pygmt/src/tilemap.py +++ b/pygmt/src/tilemap.py @@ -5,6 +5,10 @@ from pygmt.datasets.tile_map import load_tile_map from pygmt.helpers import build_arg_string, fmt_docstring, kwargs_to_strings, use_alias +try: + import rioxarray +except ImportError: + rioxarray = None @fmt_docstring @use_alias( @@ -109,6 +113,14 @@ def tilemap( """ kwargs = self._preprocess(**kwargs) # pylint: disable=protected-access + if rioxarray is None: + raise ImportError( + "Package `rioxarray` is required to be installed to use this function. " + "Please use `python -m pip install rioxarray` or " + "`mamba install -c conda-forge rioxarray` " + "to install the package." + ) + raster = load_tile_map( region=region, zoom=zoom, From 15a7207fc774a146647d4fb1f8c53af1c18d1777 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Tue, 8 Aug 2023 09:32:10 +1200 Subject: [PATCH 10/10] Lint to add missing line --- pygmt/src/tilemap.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pygmt/src/tilemap.py b/pygmt/src/tilemap.py index 483d38a01f3..b0fc0164076 100644 --- a/pygmt/src/tilemap.py +++ b/pygmt/src/tilemap.py @@ -10,6 +10,7 @@ except ImportError: rioxarray = None + @fmt_docstring @use_alias( B="frame",