Skip to content

Commit f379164

Browse files
weiji14seismanmichaelgrundyvonnefroehlich
authored
Add function to load raster tile maps using contextily (#2125)
New dataset function to load XYZ tiles! Uses contextily to retrieve the tiles based on a bounding box. Included an example doctest that shows how the map tiles can be loaded into an xarray.DataArray. Added a new section in the API docs and intersphinx mappings for contextily and xyzservices. * Use correct Spherical Mercator coordinates Can't assume that the input bounding box (which can be in longitude/latitude) is the same as the returned extent (which is always in EPSG:3857). * Change ll parameter to lonlat To fix pylint `C0103: Argument name "ll" doesn't conform to snake_case naming style (invalid-name)`. * Add contextily to CI build matrix and include it as optional dependency Let the Continuous Integration tests run with `contextily`, include it in pyproject.toml and environment.yml, and document it in `doc/install.rst` as an optional dependency. * Set default bounding box coordinates to be lonlat Bounding box coordinates are assumed to be longitude/latitude by default, rather than in Spherical Mercator. * Skip doctest when contextily is not installed Using the `__doctest_requires__` variable, see https://github.com/astropy/pytest-doctestplus/tree/v0.12.1#doctest-dependencies * Add intersphinx link for rasterio Also updated intersphinx link for xarray to new URL at https://docs.xarray.dev/en/stable * Document wait and max_retries parameters used in contextily.bounds2img * Use PyGMT's convention for default values in docstrings Modified from original contextily.bounds2img docstring to fit PyGMT's standards. Xref https://github.com/Generi cMappingTools/pygmt/pull/1182. * Rename load_map_tiles to load_tile_map Also create new dedicated section for load_tile_map in the API docs index. * Add zoom parameter and remove kwargs Wrap all of the parameters in contextily.bounds2img, and so can remove kwargs. Need to disable the pylint recommendation `R0914: Too many local variables`. * Add contextily to docs build CI requirements * Add contextily to pygmt.show_versions() dependency list * Document the three possible source options thoroughly Split out the three possible source options into bullet points. Link to https://contextily.readthedocs.io/en/latest/providers_deepdive.html, give an example OpenStreetMap URL, and link to https://contextily.readthedocs.io/en/stable/working_with_local_files.html. * Add more detail about the zoom level of detail --------- Co-authored-by: Dongdong Tian <[email protected]> Co-authored-by: Michael Grund <[email protected]> Co-authored-by: Yvonne Fröhlich <[email protected]>
1 parent 3305bf2 commit f379164

File tree

13 files changed

+180
-6
lines changed

13 files changed

+180
-6
lines changed

.github/workflows/ci_docs.yml

+1-1
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 geopandas \
74+
build ipython make myst-parser contextily geopandas \
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

+1-1
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: 'geopandas ipython'
50+
optional-packages: 'contextily geopandas ipython'
5151
timeout-minutes: 30
5252
defaults:
5353
run:

.github/workflows/ci_tests_dev.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ jobs:
101101
geopandas ghostscript libnetcdf hdf5 zlib curl pcre make
102102
pip install --pre --prefer-binary \
103103
numpy pandas xarray netCDF4 packaging \
104-
build dvc ipython 'pytest>=6.0' pytest-cov \
104+
build contextily dvc ipython 'pytest>=6.0' pytest-cov \
105105
pytest-doctestplus pytest-mpl sphinx-gallery
106106
107107
# Pull baseline image data from dvc remote (DAGsHub)

.github/workflows/ci_tests_legacy.yaml

+1-1
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-
geopandas ipython \
69+
contextily geopandas ipython \
7070
build dvc make 'pytest>=6.0' \
7171
pytest-cov pytest-doctestplus pytest-mpl sphinx-gallery
7272

ci/requirements/docs.yml

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ dependencies:
1212
- netCDF4
1313
- packaging
1414
# Optional dependencies
15+
- contextily
1516
- geopandas
1617
# Development dependencies (general)
1718
- build

doc/api/index.rst

+7
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,13 @@ and store them in GMT's user data directory.
227227
datasets.load_earth_vertical_gravity_gradient
228228
datasets.load_sample_data
229229

230+
In addition, there is also a special function to load XYZ tile maps via
231+
:doc:`contextily <contextily:index>` to be used as base maps.
232+
233+
.. autosummary::
234+
:toctree: generated
235+
236+
datasets.load_tile_map
230237

231238
.. currentmodule:: pygmt
232239

doc/conf.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,14 @@
5454

5555
# intersphinx configuration
5656
intersphinx_mapping = {
57-
"python": ("https://docs.python.org/3/", None),
57+
"contextily": ("https://contextily.readthedocs.io/en/stable/", None),
5858
"geopandas": ("https://geopandas.org/en/stable/", None),
5959
"numpy": ("https://numpy.org/doc/stable/", None),
60+
"python": ("https://docs.python.org/3/", None),
6061
"pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None),
62+
"rasterio": ("https://rasterio.readthedocs.io/en/stable/", None),
6163
"xarray": ("https://docs.xarray.dev/en/stable/", None),
64+
"xyzservices": ("https://xyzservices.readthedocs.io/en/stable", None),
6265
}
6366

6467
# options for sphinx-copybutton

doc/install.rst

+1
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ PyGMT requires the following libraries to be installed:
106106
The following are optional dependencies:
107107

108108
* `IPython <https://ipython.org>`__: For embedding the figures in Jupyter notebooks (recommended).
109+
* `Contextily <https://contextily.readthedocs.io>`__: For retrieving tile maps from the internet.
109110
* `GeoPandas <https://geopandas.org>`__: For using and plotting GeoDataFrame objects.
110111

111112
Installing GMT and other dependencies

environment.yml

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ dependencies:
1212
- netCDF4
1313
- packaging
1414
# Optional dependencies
15+
- contextily
1516
- geopandas
1617
- ipython
1718
# Development dependencies (general)

pygmt/__init__.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,15 @@ def _get_ghostscript_version():
158158
"machine": platform.platform(),
159159
}
160160

161-
deps = ["numpy", "pandas", "xarray", "netCDF4", "packaging", "geopandas"]
161+
deps = [
162+
"numpy",
163+
"pandas",
164+
"xarray",
165+
"netCDF4",
166+
"packaging",
167+
"contextily",
168+
"geopandas",
169+
]
162170

163171
print("PyGMT information:")
164172
print(f" version: {__version__}")

pygmt/datasets/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@
1414
load_earth_vertical_gravity_gradient,
1515
)
1616
from pygmt.datasets.samples import list_sample_data, load_sample_data
17+
from pygmt.datasets.tile_map import load_tile_map

pygmt/datasets/tile_map.py

+151
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
"""
2+
Function to load raster tile maps from XYZ tile providers, and load as
3+
:class:`xarray.DataArray`.
4+
"""
5+
6+
try:
7+
import contextily
8+
except ImportError:
9+
contextily = None
10+
11+
import numpy as np
12+
import xarray as xr
13+
14+
__doctest_requires__ = {("load_tile_map"): ["contextily"]}
15+
16+
17+
def load_tile_map(region, zoom="auto", source=None, lonlat=True, wait=0, max_retries=2):
18+
"""
19+
Load a georeferenced raster tile map from XYZ tile providers.
20+
21+
The tiles that compose the map are merged and georeferenced into an
22+
:class:`xarray.DataArray` image with 3 bands (RGB). Note that the returned
23+
image is in a Spherical Mercator (EPSG:3857) coordinate reference system.
24+
25+
Parameters
26+
----------
27+
region : list
28+
The bounding box of the map in the form of a list [*xmin*, *xmax*,
29+
*ymin*, *ymax*]. These coordinates should be in longitude/latitude if
30+
``lonlat=True`` or Spherical Mercator (EPSG:3857) if ``lonlat=False``.
31+
32+
zoom : int or str
33+
Optional. Level of detail. Higher levels (e.g. ``22``) mean a zoom
34+
level closer to the Earth's surface, with more tiles covering a smaller
35+
geographical area and thus more detail. Lower levels (e.g. ``0``) mean
36+
a zoom level further from the Earth's surface, with less tiles covering
37+
a larger geographical area and thus less detail [Default is
38+
``"auto"`` to automatically determine the zoom level based on the
39+
bounding box region extent].
40+
41+
**Note**: The maximum possible zoom level may be smaller than ``22``,
42+
and depends on what is supported by the chosen web tile provider
43+
source.
44+
45+
source : xyzservices.TileProvider or str
46+
Optional. The tile source: web tile provider or path to a local file.
47+
Provide either:
48+
49+
- A web tile provider in the form of a
50+
:class:`xyzservices.TileProvider` object. See
51+
:doc:`Contextily providers <contextily:providers_deepdive>` for a
52+
list of tile providers [Default is
53+
``xyzservices.providers.Stamen.Terrain``, i.e. Stamen Terrain web
54+
tiles].
55+
- A web tile provider in the form of a URL. The placeholders for the
56+
XYZ in the URL need to be {x}, {y}, {z}, respectively. E.g.
57+
``https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png``.
58+
- A local file path. The file is read with
59+
:doc:`rasterio <rasterio:index>` and all bands are loaded into the
60+
basemap. See
61+
:doc:`contextily:working_with_local_files`.
62+
63+
IMPORTANT: Tiles are assumed to be in the Spherical Mercator projection
64+
(EPSG:3857).
65+
66+
lonlat : bool
67+
Optional. If ``False``, coordinates in ``region`` are assumed to be
68+
Spherical Mercator as opposed to longitude/latitude [Default is
69+
``True``].
70+
71+
wait : int
72+
Optional. If the tile API is rate-limited, the number of seconds to
73+
wait between a failed request and the next try [Default is ``0``].
74+
75+
max_retries : int
76+
Optional. Total number of rejected requests allowed before contextily
77+
will stop trying to fetch more tiles from a rate-limited API [Default
78+
is ``2``].
79+
80+
Returns
81+
-------
82+
raster : xarray.DataArray
83+
Georeferenced 3-D data array of RGB values.
84+
85+
Raises
86+
------
87+
ModuleNotFoundError
88+
If ``contextily`` is not installed. Follow
89+
:doc:`install instructions for contextily <contextily:index>`, (e.g.
90+
via ``pip install contextily``) before using this function.
91+
92+
Examples
93+
--------
94+
>>> import contextily
95+
>>> from pygmt.datasets import load_tile_map
96+
>>> raster = load_tile_map(
97+
... region=[103.60, 104.06, 1.22, 1.49], # West, East, South, North
98+
... source=contextily.providers.Stamen.TerrainBackground,
99+
... lonlat=True, # bounding box coordinates are longitude/latitude
100+
... )
101+
>>> raster.sizes
102+
Frozen({'band': 3, 'y': 1024, 'x': 1536})
103+
>>> raster.coords
104+
Coordinates:
105+
* band (band) int64 0 1 2
106+
* y (y) float64 1.663e+05 1.663e+05 1.663e+05 ... 1.272e+05 ...
107+
* x (x) float64 1.153e+07 1.153e+07 1.153e+07 ... 1.158e+07 ...
108+
"""
109+
# pylint: disable=too-many-locals
110+
if contextily is None:
111+
raise ModuleNotFoundError(
112+
"Package `contextily` is required to be installed to use this function. "
113+
"Please use `pip install contextily` or "
114+
"`conda install -c conda-forge contextily` "
115+
"to install the package."
116+
)
117+
118+
west, east, south, north = region
119+
image, extent = contextily.bounds2img(
120+
w=west,
121+
s=south,
122+
e=east,
123+
n=north,
124+
zoom=zoom,
125+
source=source,
126+
ll=lonlat,
127+
wait=wait,
128+
max_retries=max_retries,
129+
)
130+
131+
# Turn RGBA img from channel-last to channel-first and get 3-band RGB only
132+
_image = image.transpose(2, 0, 1) # Change image from (H, W, C) to (C, H, W)
133+
rgb_image = _image[0:3, :, :] # Get just RGB by dropping RGBA's alpha channel
134+
135+
# Georeference RGB image into an xarray.DataArray
136+
left, right, bottom, top = extent
137+
dataarray = xr.DataArray(
138+
data=rgb_image,
139+
coords={
140+
"band": [0, 1, 2], # Red, Green, Blue
141+
"y": np.linspace(start=top, stop=bottom, num=rgb_image.shape[1]),
142+
"x": np.linspace(start=left, stop=right, num=rgb_image.shape[2]),
143+
},
144+
dims=("band", "y", "x"),
145+
)
146+
147+
# If rioxarray is installed, set the coordinate reference system
148+
if hasattr(dataarray, "rio"):
149+
dataarray = dataarray.rio.write_crs(input_crs="EPSG:3857")
150+
151+
return dataarray

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ dynamic = ["version"]
4242

4343
[project.optional-dependencies]
4444
all = [
45+
"contextily",
4546
"geopandas",
4647
"ipython"
4748
]

0 commit comments

Comments
 (0)