Skip to content

Commit 1478fa2

Browse files
seismanweiji14
andauthored
load_tile_map: Add the new parameter 'crs' to set the CRS of the returned dataarray (#3554)
Co-authored-by: Wei Ji <[email protected]>
1 parent 848f5fd commit 1478fa2

File tree

3 files changed

+97
-46
lines changed

3 files changed

+97
-46
lines changed

pygmt/datasets/tile_map.py

+43-19
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,29 @@
33
:class:`xarray.DataArray`.
44
"""
55

6-
import contextlib
76
from collections.abc import Sequence
87
from typing import Literal
98

109
from packaging.version import Version
1110

1211
try:
1312
import contextily
13+
from rasterio.crs import CRS
1414
from xyzservices import TileProvider
1515

1616
_HAS_CONTEXTILY = True
1717
except ImportError:
18+
CRS = None
1819
TileProvider = None
1920
_HAS_CONTEXTILY = False
2021

21-
with contextlib.suppress(ImportError):
22-
# rioxarray is needed to register the rio accessor
22+
try:
2323
import rioxarray # noqa: F401
2424

25+
_HAS_RIOXARRAY = True
26+
except ImportError:
27+
_HAS_RIOXARRAY = False
28+
2529
import numpy as np
2630
import xarray as xr
2731

@@ -33,6 +37,7 @@ def load_tile_map(
3337
zoom: int | Literal["auto"] = "auto",
3438
source: TileProvider | str | None = None,
3539
lonlat: bool = True,
40+
crs: str | CRS = "EPSG:3857",
3641
wait: int = 0,
3742
max_retries: int = 2,
3843
zoom_adjust: int | None = None,
@@ -42,7 +47,8 @@ def load_tile_map(
4247
4348
The tiles that compose the map are merged and georeferenced into an
4449
:class:`xarray.DataArray` image with 3 bands (RGB). Note that the returned image is
45-
in a Spherical Mercator (EPSG:3857) coordinate reference system.
50+
in a Spherical Mercator (EPSG:3857) coordinate reference system (CRS) by default,
51+
but can be customized using the ``crs`` parameter.
4652
4753
Parameters
4854
----------
@@ -80,6 +86,10 @@ def load_tile_map(
8086
lonlat
8187
If ``False``, coordinates in ``region`` are assumed to be Spherical Mercator as
8288
opposed to longitude/latitude.
89+
crs
90+
Coordinate reference system (CRS) of the returned :class:`xarray.DataArray`
91+
image. Default is ``"EPSG:3857"`` (i.e., Spherical Mercator). The CRS can be in
92+
either string or :class:`rasterio.crs.CRS` format.
8393
wait
8494
If the tile API is rate-limited, the number of seconds to wait between a failed
8595
request and the next try.
@@ -128,6 +138,10 @@ def load_tile_map(
128138
... raster.rio.crs.to_string()
129139
'EPSG:3857'
130140
"""
141+
# The CRS of the source tile provider. If the source is a TileProvider object, use
142+
# its crs attribute if available. Otherwise, default to EPSG:3857.
143+
_source_crs = getattr(source, "crs", "EPSG:3857")
144+
131145
if not _HAS_CONTEXTILY:
132146
msg = (
133147
"Package `contextily` is required to be installed to use this function. "
@@ -136,28 +150,34 @@ def load_tile_map(
136150
)
137151
raise ImportError(msg)
138152

139-
contextily_kwargs = {}
153+
if crs != _source_crs and not _HAS_RIOXARRAY:
154+
msg = (
155+
f"Package `rioxarray` is required if CRS is not '{_source_crs}'. "
156+
"Please use `python -m pip install rioxarray` or "
157+
"`mamba install -c conda-forge rioxarray` to install the package."
158+
)
159+
raise ImportError(msg)
160+
161+
# Keyword arguments for contextily.bounds2img
162+
contextily_kwargs = {
163+
"zoom": zoom,
164+
"source": source,
165+
"ll": lonlat,
166+
"wait": wait,
167+
"max_retries": max_retries,
168+
}
140169
if zoom_adjust is not None:
141-
contextily_kwargs["zoom_adjust"] = zoom_adjust
142170
if Version(contextily.__version__) < Version("1.5.0"):
143171
msg = (
144172
"The `zoom_adjust` parameter requires `contextily>=1.5.0` to work. "
145173
"Please upgrade contextily, or manually set the `zoom` level instead."
146174
)
147-
raise TypeError(msg)
175+
raise ValueError(msg)
176+
contextily_kwargs["zoom_adjust"] = zoom_adjust
148177

149178
west, east, south, north = region
150179
image, extent = contextily.bounds2img(
151-
w=west,
152-
s=south,
153-
e=east,
154-
n=north,
155-
zoom=zoom,
156-
source=source,
157-
ll=lonlat,
158-
wait=wait,
159-
max_retries=max_retries,
160-
**contextily_kwargs,
180+
w=west, s=south, e=east, n=north, **contextily_kwargs
161181
)
162182

163183
# Turn RGBA img from channel-last to channel-first and get 3-band RGB only
@@ -176,8 +196,12 @@ def load_tile_map(
176196
dims=("band", "y", "x"),
177197
)
178198

179-
# If rioxarray is installed, set the coordinate reference system
199+
# If rioxarray is installed, set the coordinate reference system.
180200
if hasattr(dataarray, "rio"):
181-
dataarray = dataarray.rio.write_crs(input_crs="EPSG:3857")
201+
dataarray = dataarray.rio.write_crs(input_crs=_source_crs)
202+
203+
# Reproject raster image from the source CRS to the specified CRS.
204+
if crs != _source_crs:
205+
dataarray = dataarray.rio.reproject(dst_crs=crs)
182206

183207
return dataarray

pygmt/src/tilemap.py

+3-25
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,9 @@
99
from pygmt.helpers import build_arg_list, fmt_docstring, kwargs_to_strings, use_alias
1010

1111
try:
12-
import rioxarray # noqa: F401
1312
from xyzservices import TileProvider
14-
15-
_HAS_RIOXARRAY = True
1613
except ImportError:
1714
TileProvider = None
18-
_HAS_RIOXARRAY = False
1915

2016

2117
@fmt_docstring
@@ -111,39 +107,21 @@ def tilemap(
111107
112108
kwargs : dict
113109
Extra keyword arguments to pass to :meth:`pygmt.Figure.grdimage`.
114-
115-
Raises
116-
------
117-
ImportError
118-
If ``rioxarray`` is not installed. Follow
119-
:doc:`install instructions for rioxarray <rioxarray:installation>`, (e.g. via
120-
``python -m pip install rioxarray``) before using this function.
121110
"""
122111
kwargs = self._preprocess(**kwargs)
123112

124-
if not _HAS_RIOXARRAY:
125-
msg = (
126-
"Package `rioxarray` is required to be installed to use this function. "
127-
"Please use `python -m pip install rioxarray` or "
128-
"`mamba install -c conda-forge rioxarray` to install the package."
129-
)
130-
raise ImportError(msg)
131-
132113
raster = load_tile_map(
133114
region=region,
134115
zoom=zoom,
135116
source=source,
136117
lonlat=lonlat,
118+
crs="OGC:CRS84" if lonlat is True else "EPSG:3857",
137119
wait=wait,
138120
max_retries=max_retries,
139121
zoom_adjust=zoom_adjust,
140122
)
141-
142-
# Reproject raster from Spherical Mercator (EPSG:3857) to lonlat (OGC:CRS84) if
143-
# bounding box region was provided in lonlat
144-
if lonlat and raster.rio.crs == "EPSG:3857":
145-
raster = raster.rio.reproject(dst_crs="OGC:CRS84")
146-
raster.gmt.gtype = 1 # set to geographic type
123+
if lonlat:
124+
raster.gmt.gtype = 1 # Set to geographic type
147125

148126
# Only set region if no_clip is None or False, so that plot is clipped to exact
149127
# bounding box region

pygmt/tests/test_tilemap.py

+51-2
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,24 @@
22
Test Figure.tilemap.
33
"""
44

5+
import importlib
6+
from unittest.mock import patch
7+
58
import pytest
69
from pygmt import Figure
710

8-
contextily = pytest.importorskip("contextily")
9-
rioxarray = pytest.importorskip("rioxarray")
11+
try:
12+
import contextily
13+
14+
_HAS_CONTEXTILY = True
15+
except ImportError:
16+
_HAS_CONTEXTILY = False
17+
18+
_HAS_RIOXARRAY = bool(importlib.util.find_spec("rioxarray"))
1019

1120

1221
@pytest.mark.mpl_image_compare
22+
@pytest.mark.skipif(not _HAS_CONTEXTILY, reason="contextily is not installed")
1323
def test_tilemap_web_mercator():
1424
"""
1525
Create a tilemap plot in Spherical Mercator projection (EPSG:3857).
@@ -27,6 +37,10 @@ def test_tilemap_web_mercator():
2737

2838
@pytest.mark.benchmark
2939
@pytest.mark.mpl_image_compare
40+
@pytest.mark.skipif(
41+
not (_HAS_CONTEXTILY and _HAS_RIOXARRAY),
42+
reason="contextily and rioxarray are not installed",
43+
)
3044
def test_tilemap_ogc_wgs84():
3145
"""
3246
Create a tilemap plot using longitude/latitude coordinates (OGC:WGS84), centred on
@@ -45,6 +59,10 @@ def test_tilemap_ogc_wgs84():
4559

4660
@pytest.mark.mpl_image_compare
4761
@pytest.mark.parametrize("no_clip", [False, True])
62+
@pytest.mark.skipif(
63+
not (_HAS_CONTEXTILY and _HAS_RIOXARRAY),
64+
reason="contextily and rioxarray are not installed",
65+
)
4866
def test_tilemap_no_clip(no_clip):
4967
"""
5068
Create a tilemap plot clipped to the Southern Hemisphere when no_clip is False, but
@@ -60,3 +78,34 @@ def test_tilemap_no_clip(no_clip):
6078
no_clip=no_clip,
6179
)
6280
return fig
81+
82+
83+
@pytest.mark.skipif(_HAS_CONTEXTILY, reason="contextily is installed.")
84+
def test_tilemap_no_contextily():
85+
"""
86+
Raise an ImportError when contextily is not installed.
87+
"""
88+
fig = Figure()
89+
with pytest.raises(ImportError, match="Package `contextily` is required"):
90+
fig.tilemap(
91+
region=[-20000000.0, 20000000.0, -20000000.0, 20000000.0],
92+
zoom=0,
93+
lonlat=False,
94+
frame="afg",
95+
)
96+
97+
98+
@pytest.mark.skipif(_HAS_RIOXARRAY, reason="rioxarray is installed.")
99+
def test_tilemap_no_rioxarray():
100+
"""
101+
Raise an ImportError when rioxarray is not installed and contextily is installed.
102+
"""
103+
fig = Figure()
104+
# In our CI, contextily and rioxarray are installed together, so we will see the
105+
# error about contextily, not rioxarray. Here we mock contextily as installed, to
106+
# make sure that we see the rioxarray error message when rioxarray is not installed.
107+
with patch("pygmt.datasets.tile_map._HAS_CONTEXTILY", True):
108+
with pytest.raises(ImportError, match="Package `rioxarray` is required"):
109+
fig.tilemap(
110+
region=[-180.0, 180.0, -90, 90], zoom=0, lonlat=True, frame="afg"
111+
)

0 commit comments

Comments
 (0)