Skip to content

Commit

Permalink
Add support to xyzservices tiles (#1307)
Browse files Browse the repository at this point in the history
  • Loading branch information
maximlt committed Apr 16, 2024
1 parent 1750bd5 commit a68c291
Show file tree
Hide file tree
Showing 11 changed files with 302 additions and 102 deletions.
287 changes: 214 additions & 73 deletions doc/user_guide/Geographic_Data.ipynb

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions envs/py3.10-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ dependencies:
- fiona
- flake8
- fugue
- geodatasets>=2023.12.0
- geopandas
- geoviews-core>=1.9.0
- holoviews>=1.11.0
Expand Down Expand Up @@ -65,6 +66,7 @@ dependencies:
- streamz>=0.3.0
- xarray
- xarray>=0.18.2
- xyzservices>=2022.9.0
- pip
- pip:
- -e ..
2 changes: 2 additions & 0 deletions envs/py3.11-docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ dependencies:
- datashader>=0.6.5
- fiona
- fugue
- geodatasets>=2023.12.0
- geopandas
- geoviews-core>=1.9.0
- holoviews>=1.11.0
Expand Down Expand Up @@ -55,6 +56,7 @@ dependencies:
- sphinxext-rediraffe
- streamz>=0.3.0
- xarray>=0.18.2
- xyzservices>=2022.9.0
- pip
- pip:
- -e ..
2 changes: 2 additions & 0 deletions envs/py3.11-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ dependencies:
- fiona
- flake8
- fugue
- geodatasets>=2023.12.0
- geopandas
- geoviews-core>=1.9.0
- holoviews>=1.11.0
Expand Down Expand Up @@ -65,6 +66,7 @@ dependencies:
- streamz>=0.3.0
- xarray
- xarray>=0.18.2
- xyzservices>=2022.9.0
- pip
- pip:
- -e ..
2 changes: 2 additions & 0 deletions envs/py3.12-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ dependencies:
- fiona
- flake8
- fugue
- geodatasets>=2023.12.0
- geopandas
- geoviews-core>=1.9.0
- holoviews>=1.11.0
Expand Down Expand Up @@ -65,6 +66,7 @@ dependencies:
- streamz>=0.3.0
- xarray
- xarray>=0.18.2
- xyzservices>=2022.9.0
- pip
- pip:
- -e ..
2 changes: 2 additions & 0 deletions envs/py3.8-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ dependencies:
- fiona
- flake8
- fugue
- geodatasets>=2023.12.0
- geopandas
- geoviews-core>=1.9.0
- holoviews>=1.11.0
Expand Down Expand Up @@ -65,6 +66,7 @@ dependencies:
- streamz>=0.3.0
- xarray
- xarray>=0.18.2
- xyzservices>=2022.9.0
- pip
- pip:
- -e ..
2 changes: 2 additions & 0 deletions envs/py3.9-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ dependencies:
- fiona
- flake8
- fugue
- geodatasets>=2023.12.0
- geopandas
- geoviews-core>=1.9.0
- holoviews>=1.11.0
Expand Down Expand Up @@ -65,6 +66,7 @@ dependencies:
- streamz>=0.3.0
- xarray
- xarray>=0.18.2
- xyzservices>=2022.9.0
- pip
- pip:
- -e ..
76 changes: 48 additions & 28 deletions hvplot/converter.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import difflib
import sys
from functools import partial

import param
Expand Down Expand Up @@ -249,8 +250,10 @@ class HoloViewsConverter:
Whether to display a coastline on top of the plot, setting
coastline='10m'/'50m'/'110m' specifies a specific scale.
crs (default=None):
Coordinate reference system of the data specified as Cartopy
CRS object, proj.4 string or EPSG code.
Coordinate reference system of the data specified as a string
or integer EPSG code, a CRS or Proj pyproj object, a Cartopy
CRS object, a WKT string, or a proj.4 string. Defaults to
PlateCarree.
features (default=None): dict or list
A list of features or a dictionary of features and the scale
at which to render it. Available features include 'borders',
Expand All @@ -269,9 +272,13 @@ class HoloViewsConverter:
Coordinate reference system of the plot specified as Cartopy
CRS object or class name.
tiles (default=False):
Whether to overlay the plot on a tile source. Tiles sources
can be selected by name or a tiles object or class can be passed,
the default is 'Wikipedia'.
Whether to overlay the plot on a tile source:
- `True`: OpenStreetMap layer
- `xyzservices.TileProvider` instance (requires xyzservices to
be installed)
- a map string name based on one of the default layers made
available by HoloViews or GeoViews.
- a `holoviews.Tiles` or `geoviews.WMTS` instance or class
tiles_opts (default=None): dict
Options to customize the tiles layer created when `tiles` is set,
e.g. `dict(alpha=0.5)`.
Expand Down Expand Up @@ -709,9 +716,11 @@ def _process_crs(self, data, crs):
if hasattr(data, 'rio') and data.rio.crs is not None:
# if data is a rioxarray
_crs = data.rio.crs.to_wkt()
else:
# get the proj string: either the value of data.attrs[crs] or crs itself
# get the proj string: either the value of data.attrs[crs] or crs itself
elif isinstance(crs, str):
_crs = getattr(data, 'attrs', {}).get(crs or 'crs', crs)
else:
_crs = crs

try:
return process_crs(_crs)
Expand Down Expand Up @@ -1491,37 +1500,48 @@ def _apply_layers(self, obj):
# overlay everything else
obj = obj * feature_obj.opts(projection=self.output_projection)

tiles = None
if self.tiles and not self.geo:
tiles = self._get_tiles(
self.tiles,
hv.element.tile_sources,
hv.element.tiles.Tiles
)
elif self.tiles and self.geo:
import geoviews as gv
tiles = self._get_tiles(
self.tiles,
gv.tile_sources.tile_sources,
(gv.element.WMTS, hv.element.tiles.Tiles),
)
if tiles is not None:
obj = tiles.opts(clone=True, **self.tiles_opts) * obj
if self.tiles:
if not self.geo:
tiles = self._get_tiles(self.tiles)
else:
tiles = self._get_tiles(self.tiles, lib="geoviews")
tiles = tiles.opts(clone=True, **self.tiles_opts)
obj = tiles * obj
return obj

def _get_tiles(self, source, sources, types):
tile_source = 'EsriImagery' if self.tiles == 'ESRI' else self.tiles
def _get_tiles(self, source, lib="holoviews"):
if lib == "geoviews":
import geoviews as gv
sources = gv.tile_sources.tile_sources
kls = gv.element.WMTS
types = (kls, hv.element.tiles.Tiles)
else:
sources = hv.element.tile_sources
kls = hv.element.tiles.Tiles
types = kls

tile_source = 'EsriImagery' if source == 'ESRI' else source
tiles = None
if tile_source is True:
tiles = sources["OSM"]()
elif tile_source in sources:
elif isinstance(tile_source, str) and tile_source in sources:
tiles = sources[tile_source]()
elif tile_source in sources.values():
tiles = tile_source()
elif isinstance(tile_source, types):
tiles = tile_source
else:
elif "xyzservices" in sys.modules:
import xyzservices
if isinstance(tile_source, xyzservices.TileProvider):
tiles = kls(tile_source)
if tiles is None:
msg = (
f"{tile_source} tiles not recognized, must be one of: {sorted(sources)} or a tile object"
f"{tile_source} tiles not recognized. tiles must be either True, a "
"xyzservices.TileProvider instance, a HoloViews"
+ (" or Geoviews" if lib == "geoviews" else "") + " basemap string "
f"(one of {', '.join(sorted(sources))}), a HoloViews Tiles instance"
+ (", a Geoviews WMTS instance" if lib == "geoviews" else "")
+ "."
)
raise ValueError(msg)
return tiles
Expand Down
18 changes: 17 additions & 1 deletion hvplot/tests/testgeo.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ def setUp(self):
import rasterio # noqa
import geoviews # noqa
import cartopy.crs as ccrs # noqa
import pyproj # noqa
import rioxarray as rxr
except:
raise SkipTest('xarray, rasterio, geoviews, cartopy, or rioxarray not available')
raise SkipTest('xarray, rasterio, geoviews, cartopy, pyproj or rioxarray not available')
import hvplot.xarray # noqa
import hvplot.pandas # noqa
self.da = rxr.open_rasterio(
Expand Down Expand Up @@ -87,6 +88,13 @@ def test_plot_with_crs_as_attr_str(self):
plot = da.hvplot.image('x', 'y', crs='bar')
self.assertCRS(plot)

def test_plot_with_crs_as_pyproj_Proj(self):
import pyproj
da = self.da.copy()
da.rio._crs = False # To not treat it as a rioxarray
plot = da.hvplot.image('x', 'y', crs=pyproj.Proj(self.crs))
self.assertCRS(plot)

def test_plot_with_crs_as_nonexistent_attr_str(self):
da = self.da.copy()
da.rio._crs = False # To not treat it as a rioxarray
Expand Down Expand Up @@ -270,6 +278,14 @@ def test_plot_with_specific_gv_tile_obj(self):
self.assertEqual(len(plot), 2)
self.assertIsInstance(plot.get(0), gv.element.WMTS)

def test_plot_with_xyzservices_tiles(self):
xyzservices = pytest.importorskip("xyzservices")
import geoviews as gv
plot = self.df.hvplot.points('x', 'y', geo=True, tiles=xyzservices.providers.Esri.WorldImagery)
assert len(plot) == 2
assert isinstance(plot.get(0), gv.element.WMTS)
assert isinstance(plot.get(0).data, xyzservices.TileProvider)

def test_plot_with_features_properly_overlaid_underlaid(self):
# land should be under, borders should be over
plot = self.df.hvplot.points('x', 'y', features=["land", "borders"])
Expand Down
9 changes: 9 additions & 0 deletions hvplot/tests/testgeowithoutgv.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,12 @@ def test_plot_with_specific_tile_obj(self, simple_df):
assert 'ArcGIS' in plot.get(0).data
bk_plot = bk_renderer.get_plot(plot)
assert bk_plot.projection == 'mercator'

def test_plot_with_xyzservices_tileprovider(self, simple_df):
xyzservices = pytest.importorskip("xyzservices")
plot = simple_df.hvplot.points('x', 'y', tiles=xyzservices.providers.Esri.WorldImagery)
assert len(plot) == 2
assert isinstance(plot.get(0), hv.Tiles)
assert isinstance(plot.get(0).data, xyzservices.TileProvider)
bk_plot = bk_renderer.get_plot(plot)
assert bk_plot.projection == 'mercator'
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ examples = [
"selenium >=3.141.0",
"streamz >=0.3.0",
"xarray >=0.18.2",
"xyzservices >=2022.9.0",
"geodatasets >=2023.12.0",
]
tests-nb = [
"pytest-xdist",
Expand Down

0 comments on commit a68c291

Please sign in to comment.