Skip to content

Commit fad08a6

Browse files
weiji14seismanmichaelgrundyvonnefroehlich
authored
Add Figure.tilemap to plot XYZ tile maps (#2394)
Adding the tilemap method for plotting XYZ tile maps. This is a wrapper around `pygmt.datasets.load_tile_map` and `pygmt.Figure.grdimage`. Aliases from `grdimage` have been copied over, and docstring for parameters from `load_tile_map` have been copied over too. * Add rioxarray to CI build matrix and include it as optional dependency Let the Continuous Integration tests run with `rioxarray`, include it in pyproject.toml and environment.yml, and document it in `doc/install.rst` as an optional dependency. * Reproject and plot map in lonlat coordinates by default Given a region in lonlat coordinates, ensure that the output figure is also plotted in lonlat instead of Web Mercator. * Ensure that plot is clipped to bounding box region when no_clip is False Pass the region to the grdimage call so that the plot extent is the same as the bounding box region. Set `no_clip=True` to prevent the clipping from happening (i.e., the plot will extend out from the region of interest). Added a unit test checking results for both no_clip True/False, and updated some previous baseline images that have changed slightly. * Web Mercator to Spherical Mercator and remove verbose statement * Use rio.set_crs instead of rio.write_crs So that the `spatial_ref` CF coordinate won't be set, which would result in an int32 variable on Windows but int64 on Linux/macOS. --------- Co-authored-by: Dongdong Tian <[email protected]> Co-authored-by: Michael Grund <[email protected]> Co-authored-by: Yvonne Fröhlich <[email protected]>
1 parent 9af83e5 commit fad08a6

19 files changed

+247
-13
lines changed

.github/workflows/ci_docs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ jobs:
7171
- name: Install dependencies
7272
run: |
7373
mamba install gmt=6.4.0 numpy pandas xarray netCDF4 packaging \
74-
build ipython make myst-parser contextily geopandas \
74+
build ipython make myst-parser contextily geopandas rioxarray \
7575
sphinx sphinx-copybutton sphinx-design sphinx-gallery sphinx_rtd_theme
7676
7777
# Show installed pkg information for postmortem diagnostic

.github/workflows/ci_tests.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ jobs:
4747
optional-packages: ''
4848
- python-version: '3.11'
4949
numpy-version: '1.24'
50-
optional-packages: 'contextily geopandas ipython'
50+
optional-packages: 'contextily geopandas ipython rioxarray'
5151
timeout-minutes: 30
5252
defaults:
5353
run:

.github/workflows/ci_tests_dev.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,9 @@ jobs:
101101
geopandas ghostscript libnetcdf hdf5 zlib curl pcre make
102102
pip install --pre --prefer-binary \
103103
numpy pandas xarray netCDF4 packaging \
104-
build contextily dvc ipython 'pytest>=6.0' pytest-cov \
105-
pytest-doctestplus pytest-mpl sphinx-gallery
104+
build contextily dvc ipython rioxarray \
105+
'pytest>=6.0' pytest-cov pytest-doctestplus pytest-mpl \
106+
sphinx-gallery
106107
107108
# Pull baseline image data from dvc remote (DAGsHub)
108109
- name: Pull baseline image data from dvc remote

.github/workflows/ci_tests_legacy.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ jobs:
6666
run: |
6767
mamba install gmt=${{ matrix.gmt_version }} numpy \
6868
pandas xarray netCDF4 packaging \
69-
contextily geopandas ipython \
69+
contextily geopandas ipython rioxarray \
7070
build dvc make 'pytest>=6.0' \
7171
pytest-cov pytest-doctestplus pytest-mpl sphinx-gallery
7272

ci/requirements/docs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ dependencies:
1414
# Optional dependencies
1515
- contextily
1616
- geopandas
17+
- rioxarray
1718
# Development dependencies (general)
1819
- build
1920
- ipython

doc/api/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ Plotting raster data
6262
Figure.grdimage
6363
Figure.grdview
6464
Figure.image
65+
Figure.tilemap
6566

6667
Configuring layout
6768
~~~~~~~~~~~~~~~~~~

doc/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"python": ("https://docs.python.org/3/", None),
6161
"pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None),
6262
"rasterio": ("https://rasterio.readthedocs.io/en/stable/", None),
63+
"rioxarray": ("https://corteva.github.io/rioxarray/stable/", None),
6364
"xarray": ("https://docs.xarray.dev/en/stable/", None),
6465
"xyzservices": ("https://xyzservices.readthedocs.io/en/stable", None),
6566
}

doc/install.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ The following are optional dependencies:
108108
* `IPython <https://ipython.org>`__: For embedding the figures in Jupyter notebooks (recommended).
109109
* `Contextily <https://contextily.readthedocs.io>`__: For retrieving tile maps from the internet.
110110
* `GeoPandas <https://geopandas.org>`__: For using and plotting GeoDataFrame objects.
111+
* `RioXarray <https://corteva.github.io/rioxarray>`__: For saving multi-band rasters to GeoTIFFs.
111112

112113
Installing GMT and other dependencies
113114
-------------------------------------

environment.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ dependencies:
1515
- contextily
1616
- geopandas
1717
- ipython
18+
- rioxarray
1819
# Development dependencies (general)
1920
- build
2021
- dvc

pygmt/datasets/tile_map.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,16 +103,16 @@ def load_tile_map(region, zoom="auto", source=None, lonlat=True, wait=0, max_ret
103103
Frozen({'band': 3, 'y': 256, 'x': 512})
104104
>>> raster.coords
105105
Coordinates:
106-
* band (band) uint8 0 1 2
107-
* y (y) float64 -7.081e-10 -7.858e+04 ... -1.996e+07 -2.004e+07
108-
* x (x) float64 -2.004e+07 -1.996e+07 ... 1.996e+07 2.004e+07
106+
* band (band) uint8 0 1 2
107+
* y (y) float64 -7.081e-10 -7.858e+04 ... -1.996e+07 ...
108+
* x (x) float64 -2.004e+07 -1.996e+07 ... 1.996e+07 2.004e+07
109109
"""
110110
# pylint: disable=too-many-locals
111111
if contextily is None:
112112
raise ImportError(
113113
"Package `contextily` is required to be installed to use this function. "
114114
"Please use `pip install contextily` or "
115-
"`conda install -c conda-forge contextily` "
115+
"`mamba install -c conda-forge contextily` "
116116
"to install the package."
117117
)
118118

@@ -147,6 +147,6 @@ def load_tile_map(region, zoom="auto", source=None, lonlat=True, wait=0, max_ret
147147

148148
# If rioxarray is installed, set the coordinate reference system
149149
if hasattr(dataarray, "rio"):
150-
dataarray = dataarray.rio.write_crs(input_crs="EPSG:3857")
150+
dataarray = dataarray.rio.set_crs(input_crs="EPSG:3857")
151151

152152
return dataarray

pygmt/figure.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,7 @@ def _repr_html_(self):
523523
subplot,
524524
ternary,
525525
text,
526+
tilemap,
526527
timestamp,
527528
velo,
528529
wiggle,

pygmt/src/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
from pygmt.src.surface import surface
5252
from pygmt.src.ternary import ternary
5353
from pygmt.src.text import text_ as text # "text" is an argument within "text_"
54+
from pygmt.src.tilemap import tilemap
5455
from pygmt.src.timestamp import timestamp
5556
from pygmt.src.triangulate import triangulate
5657
from pygmt.src.velo import velo

pygmt/src/tilemap.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
"""
2+
tilemap - Plot XYZ tile maps.
3+
"""
4+
from pygmt.clib import Session
5+
from pygmt.datasets.tile_map import load_tile_map
6+
from pygmt.helpers import (
7+
GMTTempFile,
8+
build_arg_string,
9+
fmt_docstring,
10+
kwargs_to_strings,
11+
use_alias,
12+
)
13+
14+
try:
15+
import rioxarray
16+
except ImportError:
17+
rioxarray = None
18+
19+
20+
@fmt_docstring
21+
@use_alias(
22+
B="frame",
23+
E="dpi",
24+
I="shading",
25+
J="projection",
26+
M="monochrome",
27+
N="no_clip",
28+
Q="nan_transparent",
29+
# R="region",
30+
V="verbose",
31+
c="panel",
32+
p="perspective",
33+
t="transparency",
34+
)
35+
@kwargs_to_strings(c="sequence_comma", p="sequence") # R="sequence",
36+
def tilemap(
37+
self, region, zoom="auto", source=None, lonlat=True, wait=0, max_retries=2, **kwargs
38+
):
39+
r"""
40+
Plots an XYZ tile map.
41+
42+
This method loads XYZ tile maps from a tile server or local file using
43+
:func:`pygmt.datasets.load_tile_map` into a georeferenced form, and plots
44+
the tiles as a basemap or overlay using :meth:`pygmt.Figure.grdimage`.
45+
46+
**Note**: By default, standard web map tiles served in a Spherical Mercator
47+
(EPSG:3857) Cartesian format will be reprojected to a geographic coordinate
48+
reference system (OGC:WGS84) and plotted with longitude/latitude bounds
49+
when ``lonlat=True``. If reprojection is not desired, please set
50+
``lonlat=False`` and provide Spherical Mercator (EPSG:3857) coordinates to
51+
the ``region`` parameter.
52+
53+
{aliases}
54+
55+
Parameters
56+
----------
57+
region : list
58+
The bounding box of the map in the form of a list [*xmin*, *xmax*,
59+
*ymin*, *ymax*]. These coordinates should be in longitude/latitude if
60+
``lonlat=True`` or Spherical Mercator (EPSG:3857) if ``lonlat=False``.
61+
62+
zoom : int or str
63+
Optional. Level of detail. Higher levels (e.g. ``22``) mean a zoom
64+
level closer to the Earth's surface, with more tiles covering a smaller
65+
geographical area and thus more detail. Lower levels (e.g. ``0``) mean
66+
a zoom level further from the Earth's surface, with less tiles covering
67+
a larger geographical area and thus less detail [Default is
68+
``"auto"`` to automatically determine the zoom level based on the
69+
bounding box region extent].
70+
71+
**Note**: The maximum possible zoom level may be smaller than ``22``,
72+
and depends on what is supported by the chosen web tile provider
73+
source.
74+
75+
source : xyzservices.TileProvider or str
76+
Optional. The tile source: web tile provider or path to a local file.
77+
Provide either:
78+
79+
- A web tile provider in the form of a
80+
:class:`xyzservices.TileProvider` object. See
81+
:doc:`Contextily providers <contextily:providers_deepdive>` for a
82+
list of tile providers [Default is
83+
``xyzservices.providers.Stamen.Terrain``, i.e. Stamen Terrain web
84+
tiles].
85+
- A web tile provider in the form of a URL. The placeholders for the
86+
XYZ in the URL need to be {{x}}, {{y}}, {{z}}, respectively. E.g.
87+
``https://{{s}}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png``.
88+
- A local file path. The file is read with
89+
:doc:`rasterio <rasterio:index>` and all bands are loaded into the
90+
basemap. See
91+
:doc:`contextily:working_with_local_files`.
92+
93+
IMPORTANT: Tiles are assumed to be in the Spherical Mercator projection
94+
(EPSG:3857).
95+
96+
lonlat : bool
97+
Optional. If ``False``, coordinates in ``region`` are assumed to be
98+
Spherical Mercator as opposed to longitude/latitude [Default is
99+
``True``].
100+
101+
wait : int
102+
Optional. If the tile API is rate-limited, the number of seconds to
103+
wait between a failed request and the next try [Default is ``0``].
104+
105+
max_retries : int
106+
Optional. Total number of rejected requests allowed before contextily
107+
will stop trying to fetch more tiles from a rate-limited API [Default
108+
is ``2``].
109+
110+
kwargs : dict
111+
Extra keyword arguments to pass to :meth:`pygmt.Figure.grdimage`.
112+
113+
Raises
114+
------
115+
ImportError
116+
If ``rioxarray`` is not installed. Follow
117+
:doc:`install instructions for rioxarray <rioxarray:installation>`,
118+
(e.g. via ``pip install rioxarray``) before using this function.
119+
"""
120+
kwargs = self._preprocess(**kwargs) # pylint: disable=protected-access
121+
122+
if rioxarray is None:
123+
raise ImportError(
124+
"Package `rioxarray` is required to be installed to use this function. "
125+
"Please use `pip install rioxarray` or "
126+
"`mamba install -c conda-forge rioxarray` "
127+
"to install the package."
128+
)
129+
130+
raster = load_tile_map(
131+
region=region,
132+
zoom=zoom,
133+
source=source,
134+
lonlat=lonlat,
135+
wait=wait,
136+
max_retries=max_retries,
137+
)
138+
139+
# Reproject raster from Spherical Mercator (EPSG:3857) to
140+
# lonlat (OGC:CRS84) if bounding box region was provided in lonlat
141+
if lonlat and raster.rio.crs == "EPSG:3857":
142+
raster = raster.rio.reproject(dst_crs="OGC:CRS84")
143+
raster.gmt.gtype = 1 # set to geographic type
144+
145+
# Only set region if no_clip is None or False, so that plot is clipped to
146+
# exact bounding box region
147+
if kwargs.get("N") in [None, False]:
148+
kwargs["R"] = "/".join(str(coordinate) for coordinate in region)
149+
150+
with GMTTempFile(suffix=".tif") as tmpfile:
151+
raster.rio.to_raster(raster_path=tmpfile.name)
152+
with Session() as lib:
153+
lib.call_module(
154+
module="grdimage", args=build_arg_string(kwargs, infile=tmpfile.name)
155+
)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
outs:
2+
- md5: 9317080021b0ce6f3b9ea6d17feece00
3+
size: 23275
4+
path: test_tilemap_no_clip_False.png
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
outs:
2+
- md5: 83e6119b2351f9d472ca7e3cc45388c3
3+
size: 60984
4+
path: test_tilemap_no_clip_True.png
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
outs:
2+
- md5: 3de0555d86aca49b92425c8d5272a934
3+
size: 59286
4+
path: test_tilemap_ogc_wgs84.png
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
outs:
2+
- md5: a76d9a9a1890d6b1345305eaea598bc3
3+
size: 122195
4+
path: test_tilemap_web_mercator.png

pygmt/tests/test_tilemap.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""
2+
Tests Figure.tilemap.
3+
"""
4+
import pytest
5+
from pygmt import Figure
6+
7+
contextily = pytest.importorskip("contextily")
8+
rioxarray = pytest.importorskip("rioxarray")
9+
10+
11+
@pytest.mark.mpl_image_compare
12+
def test_tilemap_web_mercator():
13+
"""
14+
Create a tilemap plot in Spherical Mercator projection (EPSG:3857).
15+
"""
16+
fig = Figure()
17+
fig.tilemap(
18+
region=[-20000000.0, 20000000.0, -20000000.0, 20000000.0],
19+
zoom=0,
20+
lonlat=False,
21+
frame="afg",
22+
)
23+
return fig
24+
25+
26+
@pytest.mark.mpl_image_compare
27+
def test_tilemap_ogc_wgs84():
28+
"""
29+
Create a tilemap plot using longitude/latitude coordinates (OGC:WGS84),
30+
centred on the international date line.
31+
"""
32+
fig = Figure()
33+
fig.tilemap(
34+
region=[-180.0, 180.0, -90, 90], zoom=0, frame="afg", projection="R180/5c"
35+
)
36+
return fig
37+
38+
39+
@pytest.mark.mpl_image_compare
40+
@pytest.mark.parametrize("no_clip", [False, True])
41+
def test_tilemap_no_clip(no_clip):
42+
"""
43+
Create a tilemap plot clipped to the Southern Hemisphere when no_clip is
44+
False, but for the whole globe when no_clip is True.
45+
"""
46+
fig = Figure()
47+
fig.tilemap(
48+
region=[-180.0, 180.0, -90, 0.6886],
49+
zoom=0,
50+
frame="afg",
51+
projection="H180/5c",
52+
no_clip=no_clip,
53+
)
54+
return fig

pyproject.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ keywords = [
1616
"geophysics",
1717
"geospatial",
1818
"oceanography",
19-
"seismology"
19+
"seismology",
2020
]
2121
classifiers = [
2222
"Development Status :: 4 - Beta",
@@ -36,15 +36,16 @@ dependencies = [
3636
"pandas",
3737
"xarray",
3838
"netCDF4",
39-
"packaging"
39+
"packaging",
4040
]
4141
dynamic = ["version"]
4242

4343
[project.optional-dependencies]
4444
all = [
4545
"contextily",
4646
"geopandas",
47-
"ipython"
47+
"ipython",
48+
"rioxarray",
4849
]
4950

5051
[project.urls]

0 commit comments

Comments
 (0)