From adbd9ab3b7907f6f33a22d618706478bae60424d Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Sat, 4 Mar 2023 13:51:53 +1300 Subject: [PATCH 01/18] Add Figure.tilemap to plot XYZ tile maps Initial commit for adding the tilemap function 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. --- doc/api/index.rst | 1 + doc/conf.py | 1 + pygmt/figure.py | 1 + pygmt/src/__init__.py | 1 + pygmt/src/tilemap.py | 145 ++++++++++++++++++++++ pygmt/tests/baseline/test_tilemap.png.dvc | 4 + pygmt/tests/test_tilemap.py | 18 +++ 7 files changed, 171 insertions(+) create mode 100644 pygmt/src/tilemap.py create mode 100644 pygmt/tests/baseline/test_tilemap.png.dvc create mode 100644 pygmt/tests/test_tilemap.py diff --git a/doc/api/index.rst b/doc/api/index.rst index 8bd68a01d42..29c1ea3bdf1 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -62,6 +62,7 @@ Plotting raster data Figure.grdimage Figure.grdview Figure.image + Figure.tilemap Configuring layout ~~~~~~~~~~~~~~~~~~ diff --git a/doc/conf.py b/doc/conf.py index 9fa464c5c67..6c356c0fe2e 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -60,6 +60,7 @@ "python": ("https://docs.python.org/3/", None), "pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None), "rasterio": ("https://rasterio.readthedocs.io/en/stable/", None), + "rioxarray": ("https://corteva.github.io/rioxarray/stable/", None), "xarray": ("https://docs.xarray.dev/en/stable/", None), "xyzservices": ("https://xyzservices.readthedocs.io/en/stable", None), } diff --git a/pygmt/figure.py b/pygmt/figure.py index e0ad3c2d47f..98205ec6749 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -518,6 +518,7 @@ def _repr_html_(self): subplot, ternary, text, + tilemap, timestamp, velo, wiggle, diff --git a/pygmt/src/__init__.py b/pygmt/src/__init__.py index 087e8a063b4..e2fd1752750 100644 --- a/pygmt/src/__init__.py +++ b/pygmt/src/__init__.py @@ -51,6 +51,7 @@ from pygmt.src.surface import surface from pygmt.src.ternary import ternary from pygmt.src.text import text_ as text # "text" is an argument within "text_" +from pygmt.src.tilemap import tilemap from pygmt.src.timestamp import timestamp from pygmt.src.triangulate import triangulate from pygmt.src.velo import velo diff --git a/pygmt/src/tilemap.py b/pygmt/src/tilemap.py new file mode 100644 index 00000000000..ae689144173 --- /dev/null +++ b/pygmt/src/tilemap.py @@ -0,0 +1,145 @@ +""" +tilemap - Plot XYZ tile maps. +""" + +from pygmt.clib import Session +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( + A="img_out", + B="frame", + C="cmap", + D="img_in", + E="dpi", + G="bit_color", + I="shading", + J="projection", + M="monochrome", + N="no_clip", + Q="nan_transparent", + # R="region", + V="verbose", + n="interpolation", + c="panel", + f="coltypes", + p="perspective", + t="transparency", + x="cores", +) +@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 is a wrapper around :func:`pygmt.datasets.load_tile_map` and + :meth:`pygmt.Figure.grdimage`. + + {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 ` 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 ` 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 + ------ + ModuleNotFoundError + If ``rioxarray`` is not installed. Follow + :doc:`install instructions for rioxarray `, + (e.g. via ``pip install rioxarray``) before using this function. + """ + from pygmt.datasets import load_tile_map # pylint: disable=import-outside-toplevel + + kwargs = self._preprocess(**kwargs) # pylint: disable=protected-access + + if rioxarray is None: + raise ModuleNotFoundError( + "Package `rioxarray` is required to be installed to use this function. " + "Please use `pip install rioxarray` or " + "`conda install -c conda-forge rioxarray` " + "to install the package." + ) + + with GMTTempFile(suffix=".tif") as tmpfile: + raster = load_tile_map( + region=region, + zoom=zoom, + source=source, + lonlat=lonlat, + wait=wait, + max_retries=max_retries, + ) + 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) + ) diff --git a/pygmt/tests/baseline/test_tilemap.png.dvc b/pygmt/tests/baseline/test_tilemap.png.dvc new file mode 100644 index 00000000000..ced94aa30b4 --- /dev/null +++ b/pygmt/tests/baseline/test_tilemap.png.dvc @@ -0,0 +1,4 @@ +outs: +- md5: c07e5f703941bd60a84bc109074dfb50 + size: 181869 + path: test_tilemap.png diff --git a/pygmt/tests/test_tilemap.py b/pygmt/tests/test_tilemap.py new file mode 100644 index 00000000000..bad59ce2af4 --- /dev/null +++ b/pygmt/tests/test_tilemap.py @@ -0,0 +1,18 @@ +""" +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(): + """ + Create a simple tilemap plot. + """ + fig = Figure() + fig.tilemap(region=[-157.6, -157.1, 1.68, 2.08], frame="afg") + return fig From 1d2429103a12ed426a8b69111aa32f54163cfae9 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Sat, 4 Mar 2023 14:20:28 +1300 Subject: [PATCH 02/18] 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. --- .github/workflows/ci_docs.yml | 2 +- .github/workflows/ci_tests.yaml | 2 +- .github/workflows/ci_tests_dev.yaml | 5 +++-- .github/workflows/ci_tests_legacy.yaml | 2 +- ci/requirements/docs.yml | 1 + doc/install.rst | 1 + environment.yml | 1 + pyproject.toml | 3 ++- 8 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci_docs.yml b/.github/workflows/ci_docs.yml index c7a87a005bc..2bc5904dfd3 100644 --- a/.github/workflows/ci_docs.yml +++ b/.github/workflows/ci_docs.yml @@ -71,7 +71,7 @@ jobs: - name: Install dependencies run: | mamba install gmt=6.4.0 numpy pandas xarray netCDF4 packaging \ - build ipython make myst-parser contextily geopandas \ + build ipython make myst-parser contextily geopandas rioxarray \ sphinx sphinx-copybutton sphinx-design sphinx-gallery sphinx_rtd_theme # Show installed pkg information for postmortem diagnostic diff --git a/.github/workflows/ci_tests.yaml b/.github/workflows/ci_tests.yaml index 6cd6b0a1c5b..03a2b6acbc2 100644 --- a/.github/workflows/ci_tests.yaml +++ b/.github/workflows/ci_tests.yaml @@ -47,7 +47,7 @@ jobs: optional-packages: '' - python-version: '3.11' numpy-version: '1.24' - optional-packages: 'contextily geopandas ipython' + optional-packages: 'contextily geopandas ipython rioxarray' timeout-minutes: 30 defaults: run: diff --git a/.github/workflows/ci_tests_dev.yaml b/.github/workflows/ci_tests_dev.yaml index bbeaba52c44..9b37a52303c 100644 --- a/.github/workflows/ci_tests_dev.yaml +++ b/.github/workflows/ci_tests_dev.yaml @@ -101,8 +101,9 @@ jobs: geopandas ghostscript libnetcdf hdf5 zlib curl pcre make pip install --pre --prefer-binary \ numpy pandas xarray netCDF4 packaging \ - build contextily dvc ipython 'pytest>=6.0' pytest-cov \ - pytest-doctestplus pytest-mpl sphinx-gallery + build contextily dvc ipython rioxarray \ + 'pytest>=6.0' pytest-cov pytest-doctestplus pytest-mpl \ + sphinx-gallery # Pull baseline image data from dvc remote (DAGsHub) - name: Pull baseline image data from dvc remote diff --git a/.github/workflows/ci_tests_legacy.yaml b/.github/workflows/ci_tests_legacy.yaml index fd47dae75d4..6ecb628fc95 100644 --- a/.github/workflows/ci_tests_legacy.yaml +++ b/.github/workflows/ci_tests_legacy.yaml @@ -66,7 +66,7 @@ jobs: run: | mamba install gmt=${{ matrix.gmt_version }} numpy \ pandas xarray netCDF4 packaging \ - contextily geopandas ipython \ + contextily geopandas ipython rioxarray \ build dvc make 'pytest>=6.0' \ pytest-cov pytest-doctestplus pytest-mpl sphinx-gallery diff --git a/ci/requirements/docs.yml b/ci/requirements/docs.yml index 31decb7123f..f623b310bc1 100644 --- a/ci/requirements/docs.yml +++ b/ci/requirements/docs.yml @@ -14,6 +14,7 @@ dependencies: # Optional dependencies - contextily - geopandas + - rioxarray # Development dependencies (general) - build - ipython diff --git a/doc/install.rst b/doc/install.rst index dce36f59a5e..aeab680c27f 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -108,6 +108,7 @@ The following are optional dependencies: * `IPython `__: For embedding the figures in Jupyter notebooks (recommended). * `Contextily `__: For retrieving tile maps from the internet. * `GeoPandas `__: For using and plotting GeoDataFrame objects. +* `RioXarray `__: For saving multi-band rasters to GeoTIFFs. Installing GMT and other dependencies ------------------------------------- diff --git a/environment.yml b/environment.yml index 3ed7b19fd2d..3ddd497e019 100644 --- a/environment.yml +++ b/environment.yml @@ -15,6 +15,7 @@ dependencies: - contextily - geopandas - ipython + - rioxarray # Development dependencies (general) - build - dvc diff --git a/pyproject.toml b/pyproject.toml index ac3c37ad57d..bfab29d94fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,8 @@ dynamic = ["version"] all = [ "contextily", "geopandas", - "ipython" + "ipython", + "rioxarray" ] [project.urls] From fa8641b12ff77fed86653426e78b75157dcdf223 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Sat, 4 Mar 2023 21:21:15 +1300 Subject: [PATCH 03/18] Fix typo on rioxarray URL Co-Authored-By: Dongdong Tian --- doc/install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/install.rst b/doc/install.rst index aeab680c27f..baf6952e9c1 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -108,7 +108,7 @@ The following are optional dependencies: * `IPython `__: For embedding the figures in Jupyter notebooks (recommended). * `Contextily `__: For retrieving tile maps from the internet. * `GeoPandas `__: For using and plotting GeoDataFrame objects. -* `RioXarray `__: For saving multi-band rasters to GeoTIFFs. +* `RioXarray `__: For saving multi-band rasters to GeoTIFFs. Installing GMT and other dependencies ------------------------------------- From c51d9c692e522b3eae3e1f3eaa0bb13069871d96 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Sat, 4 Mar 2023 21:25:30 +1300 Subject: [PATCH 04/18] Fix import-outside-toplevel for load_tile_map function Need to go one more level down. Co-Authored-By: Dongdong Tian --- pygmt/src/tilemap.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pygmt/src/tilemap.py b/pygmt/src/tilemap.py index ae689144173..b44332b192d 100644 --- a/pygmt/src/tilemap.py +++ b/pygmt/src/tilemap.py @@ -1,8 +1,8 @@ """ 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, @@ -116,8 +116,6 @@ def tilemap( :doc:`install instructions for rioxarray `, (e.g. via ``pip install rioxarray``) before using this function. """ - from pygmt.datasets import load_tile_map # pylint: disable=import-outside-toplevel - kwargs = self._preprocess(**kwargs) # pylint: disable=protected-access if rioxarray is None: From 56f5df8fc2b6e53a5113c97c23edb667dd878ff8 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Sat, 4 Mar 2023 21:27:55 +1300 Subject: [PATCH 05/18] Fix doctest for load_tile_map to include spatial_ref Running the doctest with rioxarray installed with georeference the resulting `xarray.DataArray` and add a `spatial_ref` coordinate. --- pygmt/datasets/tile_map.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pygmt/datasets/tile_map.py b/pygmt/datasets/tile_map.py index 44344d5903b..e0702b2f1af 100644 --- a/pygmt/datasets/tile_map.py +++ b/pygmt/datasets/tile_map.py @@ -103,9 +103,10 @@ 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 + spatial_ref int64 0 """ # pylint: disable=too-many-locals if contextily is None: From ff2fba05f205250a8ab71802f0ac3bcb4e340581 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Sat, 4 Mar 2023 21:31:34 +1300 Subject: [PATCH 06/18] Speed up test_tilemap by using zoom level 0 Just use a global map at zoom level 0 to make the test run in about 2s instead of >10s. --- pygmt/tests/baseline/test_tilemap.png.dvc | 4 ++-- pygmt/tests/test_tilemap.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pygmt/tests/baseline/test_tilemap.png.dvc b/pygmt/tests/baseline/test_tilemap.png.dvc index ced94aa30b4..03efddb23bb 100644 --- a/pygmt/tests/baseline/test_tilemap.png.dvc +++ b/pygmt/tests/baseline/test_tilemap.png.dvc @@ -1,4 +1,4 @@ outs: -- md5: c07e5f703941bd60a84bc109074dfb50 - size: 181869 +- md5: ea1fc9d829e8534aed7dac825ea0ddff + size: 123201 path: test_tilemap.png diff --git a/pygmt/tests/test_tilemap.py b/pygmt/tests/test_tilemap.py index bad59ce2af4..d2a5b23123d 100644 --- a/pygmt/tests/test_tilemap.py +++ b/pygmt/tests/test_tilemap.py @@ -14,5 +14,5 @@ def test_tilemap(): Create a simple tilemap plot. """ fig = Figure() - fig.tilemap(region=[-157.6, -157.1, 1.68, 2.08], frame="afg") + fig.tilemap(region=[-180.0, 180.0, -90, 90], zoom=0, frame="afg") return fig From c3156097bb512386b7f861b9e7f2b45a185831db Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Mon, 6 Mar 2023 22:04:49 +1300 Subject: [PATCH 07/18] Remove aliases img_out, cmap, img_in, bit_color, interpolation, coltypes, cores Probably not needed with tilemap function. --- pygmt/src/tilemap.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pygmt/src/tilemap.py b/pygmt/src/tilemap.py index b44332b192d..ec26b5dae0f 100644 --- a/pygmt/src/tilemap.py +++ b/pygmt/src/tilemap.py @@ -19,12 +19,8 @@ @fmt_docstring @use_alias( - A="img_out", B="frame", - C="cmap", - D="img_in", E="dpi", - G="bit_color", I="shading", J="projection", M="monochrome", @@ -32,12 +28,9 @@ Q="nan_transparent", # R="region", V="verbose", - n="interpolation", c="panel", - f="coltypes", p="perspective", t="transparency", - x="cores", ) @kwargs_to_strings(c="sequence_comma", p="sequence") # R="sequence", def tilemap( From a1c29e4d4097b605997d599b7ad27d20f851ba7e Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Sun, 12 Mar 2023 09:48:14 +1300 Subject: [PATCH 08/18] Switch from conda install to mamba install Since `mamba` is recommended over `conda` as per #2385. --- pygmt/datasets/tile_map.py | 2 +- pygmt/src/tilemap.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pygmt/datasets/tile_map.py b/pygmt/datasets/tile_map.py index e0702b2f1af..cebceb2e79f 100644 --- a/pygmt/datasets/tile_map.py +++ b/pygmt/datasets/tile_map.py @@ -113,7 +113,7 @@ def load_tile_map(region, zoom="auto", source=None, lonlat=True, wait=0, max_ret raise ModuleNotFoundError( "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." ) diff --git a/pygmt/src/tilemap.py b/pygmt/src/tilemap.py index ec26b5dae0f..0fc2b19474a 100644 --- a/pygmt/src/tilemap.py +++ b/pygmt/src/tilemap.py @@ -115,7 +115,7 @@ def tilemap( raise ModuleNotFoundError( "Package `rioxarray` is required to be installed to use this function. " "Please use `pip install rioxarray` or " - "`conda install -c conda-forge rioxarray` " + "`mamba install -c conda-forge rioxarray` " "to install the package." ) From 2b205350e5c274b2297d90f257e2a702c12e764f Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Mon, 20 Mar 2023 22:07:38 +1300 Subject: [PATCH 09/18] Add trailing comma to lists in pyproject.toml Co-Authored-By: Dongdong Tian --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bfab29d94fd..bdd62c8e28b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ keywords = [ "geophysics", "geospatial", "oceanography", - "seismology" + "seismology", ] classifiers = [ "Development Status :: 4 - Beta", @@ -36,7 +36,7 @@ dependencies = [ "pandas", "xarray", "netCDF4", - "packaging" + "packaging", ] dynamic = ["version"] @@ -45,7 +45,7 @@ all = [ "contextily", "geopandas", "ipython", - "rioxarray" + "rioxarray", ] [project.urls] From 7efdbfda7db75c4705b95585698a5c571753317b Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Mon, 20 Mar 2023 22:12:57 +1300 Subject: [PATCH 10/18] Reword description of fig.tilemap --- pygmt/src/tilemap.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pygmt/src/tilemap.py b/pygmt/src/tilemap.py index 0fc2b19474a..3ebb5ff4f4c 100644 --- a/pygmt/src/tilemap.py +++ b/pygmt/src/tilemap.py @@ -39,8 +39,9 @@ def tilemap( r""" Plots an XYZ tile map. - This is a wrapper around :func:`pygmt.datasets.load_tile_map` and - :meth:`pygmt.Figure.grdimage`. + This function 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`. {aliases} From d503268969ee44ccf98876273c38e52d0e95163d Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Mon, 20 Mar 2023 22:14:31 +1300 Subject: [PATCH 11/18] Move load_tile_map call out of GMTTempFile context Co-Authored-By: Dongdong Tian --- pygmt/src/tilemap.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/pygmt/src/tilemap.py b/pygmt/src/tilemap.py index 3ebb5ff4f4c..bb84f47952d 100644 --- a/pygmt/src/tilemap.py +++ b/pygmt/src/tilemap.py @@ -120,17 +120,16 @@ def tilemap( "to install the package." ) + raster = load_tile_map( + region=region, + zoom=zoom, + source=source, + lonlat=lonlat, + wait=wait, + max_retries=max_retries, + ) with GMTTempFile(suffix=".tif") as tmpfile: - raster = load_tile_map( - region=region, - zoom=zoom, - source=source, - lonlat=lonlat, - wait=wait, - max_retries=max_retries, - ) 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) From fde94ad448bf61bb4e0d7f0f1a03f71fd8967fd1 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Mon, 20 Mar 2023 22:57:29 +1300 Subject: [PATCH 12/18] 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. --- pygmt/src/tilemap.py | 14 ++++++++++ .../baseline/test_tilemap_ogc_wgs84.png.dvc | 4 +++ ....dvc => test_tilemap_web_mercator.png.dvc} | 2 +- pygmt/tests/test_tilemap.py | 28 +++++++++++++++++-- 4 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 pygmt/tests/baseline/test_tilemap_ogc_wgs84.png.dvc rename pygmt/tests/baseline/{test_tilemap.png.dvc => test_tilemap_web_mercator.png.dvc} (61%) diff --git a/pygmt/src/tilemap.py b/pygmt/src/tilemap.py index bb84f47952d..e5244cdd5fc 100644 --- a/pygmt/src/tilemap.py +++ b/pygmt/src/tilemap.py @@ -43,6 +43,13 @@ def tilemap( :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 Web 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 Web Mercator (EPSG:3857) coordinates to the + ``region`` parameter. + {aliases} Parameters @@ -128,6 +135,13 @@ def tilemap( wait=wait, max_retries=max_retries, ) + + # Reproject raster from Web 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 + with GMTTempFile(suffix=".tif") as tmpfile: raster.rio.to_raster(raster_path=tmpfile.name) with Session() as lib: diff --git a/pygmt/tests/baseline/test_tilemap_ogc_wgs84.png.dvc b/pygmt/tests/baseline/test_tilemap_ogc_wgs84.png.dvc new file mode 100644 index 00000000000..f38d48b4f2a --- /dev/null +++ b/pygmt/tests/baseline/test_tilemap_ogc_wgs84.png.dvc @@ -0,0 +1,4 @@ +outs: +- md5: cc40d807da1218cd440dd8655f5f57fb + size: 56875 + path: test_tilemap_ogc_wgs84.png diff --git a/pygmt/tests/baseline/test_tilemap.png.dvc b/pygmt/tests/baseline/test_tilemap_web_mercator.png.dvc similarity index 61% rename from pygmt/tests/baseline/test_tilemap.png.dvc rename to pygmt/tests/baseline/test_tilemap_web_mercator.png.dvc index 03efddb23bb..a12be402dff 100644 --- a/pygmt/tests/baseline/test_tilemap.png.dvc +++ b/pygmt/tests/baseline/test_tilemap_web_mercator.png.dvc @@ -1,4 +1,4 @@ outs: - md5: ea1fc9d829e8534aed7dac825ea0ddff size: 123201 - path: test_tilemap.png + path: test_tilemap_web_mercator.png diff --git a/pygmt/tests/test_tilemap.py b/pygmt/tests/test_tilemap.py index d2a5b23123d..30a4c19cca8 100644 --- a/pygmt/tests/test_tilemap.py +++ b/pygmt/tests/test_tilemap.py @@ -9,10 +9,32 @@ @pytest.mark.mpl_image_compare -def test_tilemap(): +def test_tilemap_web_mercator(): """ - Create a simple tilemap plot. + Create a tilemap plot in Web Mercator projection (EPSG:3857). """ fig = Figure() - fig.tilemap(region=[-180.0, 180.0, -90, 90], zoom=0, frame="afg") + 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", + verbose=True, + ) return fig From 9158c09aaf86d87f30046330efc6fc3b3c111307 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Mon, 20 Mar 2023 23:12:17 +1300 Subject: [PATCH 13/18] Switch from ModuleNotFoundError to ImportError Xref https://github.com/GenericMappingTools/pygmt/issues/2436 --- pygmt/src/tilemap.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygmt/src/tilemap.py b/pygmt/src/tilemap.py index e5244cdd5fc..cd486397b9c 100644 --- a/pygmt/src/tilemap.py +++ b/pygmt/src/tilemap.py @@ -112,7 +112,7 @@ def tilemap( Raises ------ - ModuleNotFoundError + ImportError If ``rioxarray`` is not installed. Follow :doc:`install instructions for rioxarray `, (e.g. via ``pip install rioxarray``) before using this function. @@ -120,7 +120,7 @@ def tilemap( kwargs = self._preprocess(**kwargs) # pylint: disable=protected-access if rioxarray is None: - raise ModuleNotFoundError( + 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` " From 17d07341eea751bc392cad3225c8a72b8ee87559 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Mon, 20 Mar 2023 23:48:01 +1300 Subject: [PATCH 14/18] 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. --- pygmt/src/tilemap.py | 5 +++++ .../test_tilemap_no_clip_False.png.dvc | 4 ++++ .../baseline/test_tilemap_no_clip_True.png.dvc | 4 ++++ .../baseline/test_tilemap_ogc_wgs84.png.dvc | 4 ++-- .../baseline/test_tilemap_web_mercator.png.dvc | 4 ++-- pygmt/tests/test_tilemap.py | 18 ++++++++++++++++++ 6 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 pygmt/tests/baseline/test_tilemap_no_clip_False.png.dvc create mode 100644 pygmt/tests/baseline/test_tilemap_no_clip_True.png.dvc diff --git a/pygmt/src/tilemap.py b/pygmt/src/tilemap.py index cd486397b9c..2f2ed8c95a9 100644 --- a/pygmt/src/tilemap.py +++ b/pygmt/src/tilemap.py @@ -142,6 +142,11 @@ def tilemap( raster = raster.rio.reproject(dst_crs="OGC:CRS84") raster.gmt.gtype = 1 # set to geographic type + # Only set region if no_clip is False, so that plot is clipped to exact + # bounding box region + if not kwargs.get("N"): + 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: diff --git a/pygmt/tests/baseline/test_tilemap_no_clip_False.png.dvc b/pygmt/tests/baseline/test_tilemap_no_clip_False.png.dvc new file mode 100644 index 00000000000..8de7c7e7197 --- /dev/null +++ b/pygmt/tests/baseline/test_tilemap_no_clip_False.png.dvc @@ -0,0 +1,4 @@ +outs: +- md5: 9317080021b0ce6f3b9ea6d17feece00 + size: 23275 + path: test_tilemap_no_clip_False.png diff --git a/pygmt/tests/baseline/test_tilemap_no_clip_True.png.dvc b/pygmt/tests/baseline/test_tilemap_no_clip_True.png.dvc new file mode 100644 index 00000000000..e5c1f4935ea --- /dev/null +++ b/pygmt/tests/baseline/test_tilemap_no_clip_True.png.dvc @@ -0,0 +1,4 @@ +outs: +- md5: 83e6119b2351f9d472ca7e3cc45388c3 + size: 60984 + path: test_tilemap_no_clip_True.png diff --git a/pygmt/tests/baseline/test_tilemap_ogc_wgs84.png.dvc b/pygmt/tests/baseline/test_tilemap_ogc_wgs84.png.dvc index f38d48b4f2a..94a2d317aa3 100644 --- a/pygmt/tests/baseline/test_tilemap_ogc_wgs84.png.dvc +++ b/pygmt/tests/baseline/test_tilemap_ogc_wgs84.png.dvc @@ -1,4 +1,4 @@ outs: -- md5: cc40d807da1218cd440dd8655f5f57fb - size: 56875 +- md5: 3de0555d86aca49b92425c8d5272a934 + size: 59286 path: test_tilemap_ogc_wgs84.png diff --git a/pygmt/tests/baseline/test_tilemap_web_mercator.png.dvc b/pygmt/tests/baseline/test_tilemap_web_mercator.png.dvc index a12be402dff..ad6cc210818 100644 --- a/pygmt/tests/baseline/test_tilemap_web_mercator.png.dvc +++ b/pygmt/tests/baseline/test_tilemap_web_mercator.png.dvc @@ -1,4 +1,4 @@ outs: -- md5: ea1fc9d829e8534aed7dac825ea0ddff - size: 123201 +- md5: a76d9a9a1890d6b1345305eaea598bc3 + size: 122195 path: test_tilemap_web_mercator.png diff --git a/pygmt/tests/test_tilemap.py b/pygmt/tests/test_tilemap.py index 30a4c19cca8..bc3ac1e6526 100644 --- a/pygmt/tests/test_tilemap.py +++ b/pygmt/tests/test_tilemap.py @@ -38,3 +38,21 @@ def test_tilemap_ogc_wgs84(): verbose=True, ) 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 From de74e3dbacaf7b2c1dd69d3b0da04bdb4dda6c92 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Thu, 23 Mar 2023 21:10:17 +1300 Subject: [PATCH 15/18] Web Mercator to Spherical Mercator and remove verbose statement Co-Authored-By: Dongdong Tian Co-Authored-By: Michael Grund <23025878+michaelgrund@users.noreply.github.com> --- pygmt/src/tilemap.py | 10 +++++----- pygmt/tests/test_tilemap.py | 8 ++------ 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/pygmt/src/tilemap.py b/pygmt/src/tilemap.py index 2f2ed8c95a9..35003a7aa3d 100644 --- a/pygmt/src/tilemap.py +++ b/pygmt/src/tilemap.py @@ -43,12 +43,12 @@ def tilemap( :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 Web Mercator + **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 Web Mercator (EPSG:3857) coordinates to the - ``region`` parameter. + ``lonlat=False`` and provide Spherical Mercator (EPSG:3857) coordinates to + the ``region`` parameter. {aliases} @@ -136,8 +136,8 @@ def tilemap( max_retries=max_retries, ) - # Reproject raster from Web Mercator (EPSG:3857) to lonlat (OGC:CRS84) if - # bounding box region was provided in lonlat + # 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 diff --git a/pygmt/tests/test_tilemap.py b/pygmt/tests/test_tilemap.py index bc3ac1e6526..06d377f4462 100644 --- a/pygmt/tests/test_tilemap.py +++ b/pygmt/tests/test_tilemap.py @@ -11,7 +11,7 @@ @pytest.mark.mpl_image_compare def test_tilemap_web_mercator(): """ - Create a tilemap plot in Web Mercator projection (EPSG:3857). + Create a tilemap plot in Spherical Mercator projection (EPSG:3857). """ fig = Figure() fig.tilemap( @@ -31,11 +31,7 @@ def test_tilemap_ogc_wgs84(): """ fig = Figure() fig.tilemap( - region=[-180.0, 180.0, -90, 90], - zoom=0, - frame="afg", - projection="R180/5c", - verbose=True, + region=[-180.0, 180.0, -90, 90], zoom=0, frame="afg", projection="R180/5c" ) return fig From 125574ee52f91624cb406769ee8b45297ebdb5f5 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Thu, 23 Mar 2023 21:22:44 +1300 Subject: [PATCH 16/18] Refactor no_clip if-condition to be a little more clear Double negatives are confusing Co-Authored-By: Dongdong Tian --- pygmt/src/tilemap.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pygmt/src/tilemap.py b/pygmt/src/tilemap.py index 35003a7aa3d..0fe922bdcd5 100644 --- a/pygmt/src/tilemap.py +++ b/pygmt/src/tilemap.py @@ -142,9 +142,9 @@ def tilemap( raster = raster.rio.reproject(dst_crs="OGC:CRS84") raster.gmt.gtype = 1 # set to geographic type - # Only set region if no_clip is False, so that plot is clipped to exact - # bounding box region - if not kwargs.get("N"): + # 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: From 972c2c4dffe31edb32f518d5a41c7e28d8ce909a Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Mon, 27 Mar 2023 14:11:24 +1300 Subject: [PATCH 17/18] Edit docstring to say method instead of function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yvonne Fröhlich <94163266+yvonnefroehlich@users.noreply.github.com> --- pygmt/src/tilemap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/src/tilemap.py b/pygmt/src/tilemap.py index 0fe922bdcd5..be2338fa05e 100644 --- a/pygmt/src/tilemap.py +++ b/pygmt/src/tilemap.py @@ -39,7 +39,7 @@ def tilemap( r""" Plots an XYZ tile map. - This function loads XYZ tile maps from a tile server or local file using + 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`. From c3fca1419454d34221fea6dbc9471ac398d03199 Mon Sep 17 00:00:00 2001 From: Wei Ji <23487320+weiji14@users.noreply.github.com> Date: Mon, 27 Mar 2023 15:08:49 +1300 Subject: [PATCH 18/18] 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. --- pygmt/datasets/tile_map.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pygmt/datasets/tile_map.py b/pygmt/datasets/tile_map.py index 92bfe07bda3..fd5268a408a 100644 --- a/pygmt/datasets/tile_map.py +++ b/pygmt/datasets/tile_map.py @@ -106,7 +106,6 @@ def load_tile_map(region, zoom="auto", source=None, lonlat=True, wait=0, max_ret * 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 - spatial_ref int64 0 """ # pylint: disable=too-many-locals if contextily is None: @@ -148,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") return dataarray