-
Notifications
You must be signed in to change notification settings - Fork 228
Add Figure.tilemap to plot XYZ tile maps #2394
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
adbd9ab
1d24291
fa8641b
c51d9c6
6398829
56f5df8
ff2fba0
c315609
fd93c67
a1c29e4
2b20535
7efdbfd
d503268
0a03a47
fde94ad
9158c09
17d0734
de74e3d
125574e
4d98102
10908d5
972c2c4
e22d012
c3fca14
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -103,16 +103,16 @@ def load_tile_map(region, zoom="auto", source=None, lonlat=True, wait=0, max_ret | |
Frozen({'band': 3, 'y': 256, 'x': 512}) | ||
>>> raster.coords | ||
Coordinates: | ||
* band (band) uint8 0 1 2 | ||
* y (y) float64 -7.081e-10 -7.858e+04 ... -1.996e+07 -2.004e+07 | ||
* x (x) float64 -2.004e+07 -1.996e+07 ... 1.996e+07 2.004e+07 | ||
* band (band) uint8 0 1 2 | ||
* y (y) float64 -7.081e-10 -7.858e+04 ... -1.996e+07 ... | ||
* x (x) float64 -2.004e+07 -1.996e+07 ... 1.996e+07 2.004e+07 | ||
""" | ||
# pylint: disable=too-many-locals | ||
if contextily is None: | ||
raise ImportError( | ||
"Package `contextily` is required to be installed to use this function. " | ||
"Please use `pip install contextily` or " | ||
"`conda install -c conda-forge contextily` " | ||
"`mamba install -c conda-forge contextily` " | ||
"to install the package." | ||
) | ||
|
||
|
@@ -147,6 +147,6 @@ def load_tile_map(region, zoom="auto", source=None, lonlat=True, wait=0, max_ret | |
|
||
# If rioxarray is installed, set the coordinate reference system | ||
if hasattr(dataarray, "rio"): | ||
dataarray = dataarray.rio.write_crs(input_crs="EPSG:3857") | ||
dataarray = dataarray.rio.set_crs(input_crs="EPSG:3857") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed from using If this looks ok (and the tests pass), I'll proceed to merge in this PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks good. |
||
|
||
return dataarray |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -523,6 +523,7 @@ def _repr_html_(self): | |
subplot, | ||
ternary, | ||
text, | ||
tilemap, | ||
timestamp, | ||
velo, | ||
wiggle, | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,155 @@ | ||||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||||
tilemap - Plot XYZ tile maps. | ||||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||||
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, | ||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
try: | ||||||||||||||||||||||||||||||
import rioxarray | ||||||||||||||||||||||||||||||
except ImportError: | ||||||||||||||||||||||||||||||
rioxarray = None | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
@fmt_docstring | ||||||||||||||||||||||||||||||
@use_alias( | ||||||||||||||||||||||||||||||
B="frame", | ||||||||||||||||||||||||||||||
E="dpi", | ||||||||||||||||||||||||||||||
I="shading", | ||||||||||||||||||||||||||||||
J="projection", | ||||||||||||||||||||||||||||||
M="monochrome", | ||||||||||||||||||||||||||||||
N="no_clip", | ||||||||||||||||||||||||||||||
Q="nan_transparent", | ||||||||||||||||||||||||||||||
# R="region", | ||||||||||||||||||||||||||||||
Comment on lines
+27
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was playing with the Alternatively, we could also do the clipping in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added commit 17d0734 to ensure that:
|
||||||||||||||||||||||||||||||
V="verbose", | ||||||||||||||||||||||||||||||
c="panel", | ||||||||||||||||||||||||||||||
p="perspective", | ||||||||||||||||||||||||||||||
t="transparency", | ||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||
@kwargs_to_strings(c="sequence_comma", p="sequence") # R="sequence", | ||||||||||||||||||||||||||||||
def tilemap( | ||||||||||||||||||||||||||||||
self, region, zoom="auto", source=None, lonlat=True, wait=0, max_retries=2, **kwargs | ||||||||||||||||||||||||||||||
): | ||||||||||||||||||||||||||||||
r""" | ||||||||||||||||||||||||||||||
Plots an XYZ tile map. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
This method loads XYZ tile maps from a tile server or local file using | ||||||||||||||||||||||||||||||
:func:`pygmt.datasets.load_tile_map` into a georeferenced form, and plots | ||||||||||||||||||||||||||||||
the tiles as a basemap or overlay using :meth:`pygmt.Figure.grdimage`. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
**Note**: By default, standard web map tiles served in a Spherical Mercator | ||||||||||||||||||||||||||||||
(EPSG:3857) Cartesian format will be reprojected to a geographic coordinate | ||||||||||||||||||||||||||||||
reference system (OGC:WGS84) and plotted with longitude/latitude bounds | ||||||||||||||||||||||||||||||
when ``lonlat=True``. If reprojection is not desired, please set | ||||||||||||||||||||||||||||||
``lonlat=False`` and provide Spherical Mercator (EPSG:3857) coordinates to | ||||||||||||||||||||||||||||||
the ``region`` parameter. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
{aliases} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
Parameters | ||||||||||||||||||||||||||||||
---------- | ||||||||||||||||||||||||||||||
region : list | ||||||||||||||||||||||||||||||
The bounding box of the map in the form of a list [*xmin*, *xmax*, | ||||||||||||||||||||||||||||||
*ymin*, *ymax*]. These coordinates should be in longitude/latitude if | ||||||||||||||||||||||||||||||
``lonlat=True`` or Spherical Mercator (EPSG:3857) if ``lonlat=False``. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
zoom : int or str | ||||||||||||||||||||||||||||||
Optional. Level of detail. Higher levels (e.g. ``22``) mean a zoom | ||||||||||||||||||||||||||||||
level closer to the Earth's surface, with more tiles covering a smaller | ||||||||||||||||||||||||||||||
geographical area and thus more detail. Lower levels (e.g. ``0``) mean | ||||||||||||||||||||||||||||||
a zoom level further from the Earth's surface, with less tiles covering | ||||||||||||||||||||||||||||||
a larger geographical area and thus less detail [Default is | ||||||||||||||||||||||||||||||
``"auto"`` to automatically determine the zoom level based on the | ||||||||||||||||||||||||||||||
bounding box region extent]. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
**Note**: The maximum possible zoom level may be smaller than ``22``, | ||||||||||||||||||||||||||||||
and depends on what is supported by the chosen web tile provider | ||||||||||||||||||||||||||||||
source. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
source : xyzservices.TileProvider or str | ||||||||||||||||||||||||||||||
Optional. The tile source: web tile provider or path to a local file. | ||||||||||||||||||||||||||||||
Provide either: | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
- A web tile provider in the form of a | ||||||||||||||||||||||||||||||
:class:`xyzservices.TileProvider` object. See | ||||||||||||||||||||||||||||||
:doc:`Contextily providers <contextily:providers_deepdive>` for a | ||||||||||||||||||||||||||||||
list of tile providers [Default is | ||||||||||||||||||||||||||||||
``xyzservices.providers.Stamen.Terrain``, i.e. Stamen Terrain web | ||||||||||||||||||||||||||||||
tiles]. | ||||||||||||||||||||||||||||||
- A web tile provider in the form of a URL. The placeholders for the | ||||||||||||||||||||||||||||||
XYZ in the URL need to be {{x}}, {{y}}, {{z}}, respectively. E.g. | ||||||||||||||||||||||||||||||
``https://{{s}}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png``. | ||||||||||||||||||||||||||||||
- A local file path. The file is read with | ||||||||||||||||||||||||||||||
:doc:`rasterio <rasterio:index>` and all bands are loaded into the | ||||||||||||||||||||||||||||||
basemap. See | ||||||||||||||||||||||||||||||
:doc:`contextily:working_with_local_files`. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
IMPORTANT: Tiles are assumed to be in the Spherical Mercator projection | ||||||||||||||||||||||||||||||
(EPSG:3857). | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
lonlat : bool | ||||||||||||||||||||||||||||||
Optional. If ``False``, coordinates in ``region`` are assumed to be | ||||||||||||||||||||||||||||||
Spherical Mercator as opposed to longitude/latitude [Default is | ||||||||||||||||||||||||||||||
``True``]. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
wait : int | ||||||||||||||||||||||||||||||
Optional. If the tile API is rate-limited, the number of seconds to | ||||||||||||||||||||||||||||||
wait between a failed request and the next try [Default is ``0``]. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
max_retries : int | ||||||||||||||||||||||||||||||
Optional. Total number of rejected requests allowed before contextily | ||||||||||||||||||||||||||||||
will stop trying to fetch more tiles from a rate-limited API [Default | ||||||||||||||||||||||||||||||
is ``2``]. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
kwargs : dict | ||||||||||||||||||||||||||||||
Extra keyword arguments to pass to :meth:`pygmt.Figure.grdimage`. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
Raises | ||||||||||||||||||||||||||||||
------ | ||||||||||||||||||||||||||||||
ImportError | ||||||||||||||||||||||||||||||
If ``rioxarray`` is not installed. Follow | ||||||||||||||||||||||||||||||
:doc:`install instructions for rioxarray <rioxarray:installation>`, | ||||||||||||||||||||||||||||||
(e.g. via ``pip install rioxarray``) before using this function. | ||||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||||
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 `pip install rioxarray` or " | ||||||||||||||||||||||||||||||
"`mamba install -c conda-forge rioxarray` " | ||||||||||||||||||||||||||||||
"to install the package." | ||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
raster = load_tile_map( | ||||||||||||||||||||||||||||||
region=region, | ||||||||||||||||||||||||||||||
zoom=zoom, | ||||||||||||||||||||||||||||||
source=source, | ||||||||||||||||||||||||||||||
lonlat=lonlat, | ||||||||||||||||||||||||||||||
wait=wait, | ||||||||||||||||||||||||||||||
max_retries=max_retries, | ||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
# Reproject raster from Spherical Mercator (EPSG:3857) to | ||||||||||||||||||||||||||||||
# lonlat (OGC:CRS84) if bounding box region was provided in lonlat | ||||||||||||||||||||||||||||||
if lonlat and raster.rio.crs == "EPSG:3857": | ||||||||||||||||||||||||||||||
raster = raster.rio.reproject(dst_crs="OGC:CRS84") | ||||||||||||||||||||||||||||||
raster.gmt.gtype = 1 # set to geographic type | ||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Setting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, rioxarray's |
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
# Only set region if no_clip is None or False, so that plot is clipped to | ||||||||||||||||||||||||||||||
# exact bounding box region | ||||||||||||||||||||||||||||||
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: | ||||||||||||||||||||||||||||||
lib.call_module( | ||||||||||||||||||||||||||||||
module="grdimage", args=build_arg_string(kwargs, infile=tmpfile.name) | ||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||
Comment on lines
+152
to
+155
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that this is not the full implementation of PyGMT's Lines 179 to 192 in 8f31706
Question is: should we support shading of the XYZ tiles with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Currently I would say no to shading. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
outs: | ||
- md5: 9317080021b0ce6f3b9ea6d17feece00 | ||
size: 23275 | ||
path: test_tilemap_no_clip_False.png |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
outs: | ||
- md5: 83e6119b2351f9d472ca7e3cc45388c3 | ||
size: 60984 | ||
path: test_tilemap_no_clip_True.png |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
outs: | ||
- md5: 3de0555d86aca49b92425c8d5272a934 | ||
size: 59286 | ||
path: test_tilemap_ogc_wgs84.png |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
outs: | ||
- md5: a76d9a9a1890d6b1345305eaea598bc3 | ||
size: 122195 | ||
path: test_tilemap_web_mercator.png |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
""" | ||
Tests Figure.tilemap. | ||
""" | ||
import pytest | ||
from pygmt import Figure | ||
|
||
contextily = pytest.importorskip("contextily") | ||
rioxarray = pytest.importorskip("rioxarray") | ||
|
||
|
||
@pytest.mark.mpl_image_compare | ||
def test_tilemap_web_mercator(): | ||
""" | ||
Create a tilemap plot in Spherical Mercator projection (EPSG:3857). | ||
""" | ||
fig = Figure() | ||
fig.tilemap( | ||
region=[-20000000.0, 20000000.0, -20000000.0, 20000000.0], | ||
zoom=0, | ||
lonlat=False, | ||
frame="afg", | ||
) | ||
return fig | ||
|
||
|
||
@pytest.mark.mpl_image_compare | ||
def test_tilemap_ogc_wgs84(): | ||
""" | ||
Create a tilemap plot using longitude/latitude coordinates (OGC:WGS84), | ||
centred on the international date line. | ||
""" | ||
fig = Figure() | ||
fig.tilemap( | ||
region=[-180.0, 180.0, -90, 90], zoom=0, frame="afg", projection="R180/5c" | ||
) | ||
return fig | ||
|
||
|
||
@pytest.mark.mpl_image_compare | ||
@pytest.mark.parametrize("no_clip", [False, True]) | ||
def test_tilemap_no_clip(no_clip): | ||
""" | ||
Create a tilemap plot clipped to the Southern Hemisphere when no_clip is | ||
False, but for the whole globe when no_clip is True. | ||
""" | ||
fig = Figure() | ||
fig.tilemap( | ||
region=[-180.0, 180.0, -90, 0.6886], | ||
zoom=0, | ||
frame="afg", | ||
projection="H180/5c", | ||
no_clip=no_clip, | ||
) | ||
return fig |
Uh oh!
There was an error while loading. Please reload this page.