|
| 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 |
0 commit comments