From c53c7926b63ba2a1604ffb0cfec97bebd1df374b Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 21 Feb 2019 17:03:46 +0000 Subject: [PATCH 01/11] Added Tiles element from geoviews --- holoviews/element/__init__.py | 1 + holoviews/element/tiles.py | 124 +++++++++++++++++++++++++++ holoviews/plotting/bokeh/__init__.py | 5 +- holoviews/plotting/bokeh/tiles.py | 68 +++++++++++++++ 4 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 holoviews/element/tiles.py create mode 100644 holoviews/plotting/bokeh/tiles.py diff --git a/holoviews/element/__init__.py b/holoviews/element/__init__.py index 1d798164ce..a4f51d6a00 100644 --- a/holoviews/element/__init__.py +++ b/holoviews/element/__init__.py @@ -9,6 +9,7 @@ from .sankey import * # noqa (API import) from .stats import * # noqa (API import) from .tabular import * # noqa (API import) +from .tiles import * # noqa (API import) class ElementConversion(DataConversion): diff --git a/holoviews/element/tiles.py b/holoviews/element/tiles.py new file mode 100644 index 0000000000..8cc7cce0db --- /dev/null +++ b/holoviews/element/tiles.py @@ -0,0 +1,124 @@ +from __future__ import absolute_import, division, unicode_literals + +import param +import numpy as np + +from ..core import util +from ..core.dimension import Dimension +from ..core.element import Element2D + + +class Tiles(Element2D): + """ + The Tiles Element represents a Web Map Tile Service specified as a + URL containing {x}, {y}, and {z} templating variables, e.g.: + + https://maps.wikimedia.org/osm-intl/{Z}/{X}/{Y}@2x.png + """ + + kdims = param.List(default=[Dimension('x'), Dimension('y')], + bounds=(2, 2), constant=True, doc=""" + The key dimensions of a geometry represent the x- and y- + coordinates in a 2D space.""") + + group = param.String(default='Tiles') + + def __init__(self, data, kdims=None, vdims=None, **params): + try: + from bokeh.models import MercatorTileSource + except: + MercatorTileSource = None + if MercatorTileSource and isinstance(data, MercatorTileSource): + data = data.url + elif not isinstance(data, util.basestring): + raise TypeError('%s data should be a tile service URL not a %s type.' + % (type(self).__name__, type(data).__name__) ) + super(Tiles, self).__init__(data, kdims=kdims, vdims=vdims, **params) + + + def range(self, dim, data_range=True, dimension_range=True): + return np.nan, np.nan + + def dimension_values(self, dimension, expanded=True, flat=True): + return np.array([]) + + +# Mapping between patterns to match specified as tuples and tuples containing attributions +_ATTRIBUTIONS = { + ('openstreetmap',) : ( + '© OpenStreetMap contributors' + ), + ('cartodb',) : ( + '© OpenStreetMap contributors, ' + '© CartoDB' + ), + ('cartocdn',) : ( + '© OpenStreetMap contributors, ' + '© CartoDB' + ), + ('stamen', 'com/t') : ( # to match both 'toner' and 'terrain' + 'Map tiles by Stamen Design, ' + 'under CC BY 3.0. ' + 'Data by OpenStreetMap, ' + 'under ODbL.' + ), + ('stamen', 'watercolor') : ( + 'Map tiles by Stamen Design, ' + 'under CC BY 3.0. ' + 'Data by OpenStreetMap, ' + 'under CC BY SA.' + ), + ('wikimedia',) : ( + '© OpenStreetMap contributors' + ), + ('arcgis','Terrain') : ( + '© Esri, ' + 'USGS, NOAA' + ), + ('arcgis','Reference') : ( + '© Esri, ' + 'Garmin, USGS, NPS' + ), + ('arcgis','Imagery') : ( + '© Esri, ' + 'Earthstar Geographics' + ), + ('arcgis','NatGeo') : ( + '© Esri, ' + 'NatGeo, Garmin, HERE, UNEP-WCMC, USGS, NASA, ESA, METI, NRCAN, GEBCO, NOAA, Increment P' + ), + ('arcgis','USA_Topo') : ( + '© Esri, ' + 'NatGeo, i-cubed' + ) +} + +# CartoDB basemaps +CartoDark = Tiles('https://cartodb-basemaps-4.global.ssl.fastly.net/dark_all/{Z}/{X}/{Y}.png', name="CartoDark") +CartoEco = Tiles('http://3.api.cartocdn.com/base-eco/{Z}/{X}/{Y}.png', name="CartoEco") +CartoLight = Tiles('https://cartodb-basemaps-4.global.ssl.fastly.net/light_all/{Z}/{X}/{Y}.png', name="CartoLight") +CartoMidnight = Tiles('http://3.api.cartocdn.com/base-midnight/{Z}/{X}/{Y}.png', name="CartoMidnight") + +# Stamen basemaps +StamenTerrain = Tiles('http://tile.stamen.com/terrain/{Z}/{X}/{Y}.png', name="StamenTerrain") +StamenTerrainRetina = Tiles('http://tile.stamen.com/terrain/{Z}/{X}/{Y}@2x.png', name="StamenTerrainRetina") +StamenWatercolor = Tiles('http://tile.stamen.com/watercolor/{Z}/{X}/{Y}.jpg', name="StamenWatercolor") +StamenToner = Tiles('http://tile.stamen.com/toner/{Z}/{X}/{Y}.png', name="StamenToner") +StamenTonerBackground = Tiles('http://tile.stamen.com/toner-background/{Z}/{X}/{Y}.png', name="StamenTonerBackground") +StamenLabels = Tiles('http://tile.stamen.com/toner-labels/{Z}/{X}/{Y}.png', name="StamenLabels") + +# Esri maps (see https://server.arcgisonline.com/arcgis/rest/services for the full list) +EsriImagery = Tiles('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{Z}/{Y}/{X}.jpg', name="EsriImagery") +EsriNatGeo = Tiles('https://server.arcgisonline.com/ArcGIS/rest/services/NatGeo_World_Map/MapServer/tile/{Z}/{Y}/{X}', name="EsriNatGeo") +EsriUSATopo = Tiles('https://server.arcgisonline.com/ArcGIS/rest/services/USA_Topo_Maps/MapServer/tile/{Z}/{Y}/{X}', name="EsriUSATopo") +EsriTerrain = Tiles('https://server.arcgisonline.com/ArcGIS/rest/services/World_Terrain_Base/MapServer/tile/{Z}/{Y}/{X}', name="EsriTerrain") +EsriReference = Tiles('http://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Reference_Overlay/MapServer/tile/{Z}/{Y}/{X}', name="EsriReference") +ESRI = EsriImagery # For backwards compatibility with gv 1.5 + + +# Miscellaneous +OSM = Tiles('http://c.tile.openstreetmap.org/{Z}/{X}/{Y}.png', name="OSM") +Wikipedia = Tiles('https://maps.wikimedia.org/osm-intl/{Z}/{X}/{Y}@2x.png', name="Wikipedia") + + +tile_sources = {k: v for k, v in locals().items() if isinstance(v, Tiles) and k != 'ESRI'} diff --git a/holoviews/plotting/bokeh/__init__.py b/holoviews/plotting/bokeh/__init__.py index 6d7d0741e9..899c6ad406 100644 --- a/holoviews/plotting/bokeh/__init__.py +++ b/holoviews/plotting/bokeh/__init__.py @@ -12,7 +12,8 @@ ErrorBars, Text, HLine, VLine, Spline, Spikes, Table, ItemTable, Area, HSV, QuadMesh, VectorField, Graph, Nodes, EdgePaths, Distribution, Bivariate, - TriMesh, Violin, Chord, Div, HexTiles, Labels, Sankey) + TriMesh, Violin, Chord, Div, HexTiles, Labels, Sankey, + Tiles) from ...core.options import Options, Cycle, Palette from ...core.util import LooseVersion, VersionError @@ -44,6 +45,7 @@ from .sankey import SankeyPlot from .stats import DistributionPlot, BivariatePlot, BoxWhiskerPlot, ViolinPlot from .tabular import TablePlot +from .tiles import TilePlot from .util import bokeh_version # noqa (API import) @@ -100,6 +102,7 @@ Spline: SplinePlot, Arrow: ArrowPlot, Div: DivPlot, + Tiles: TilePlot, # Graph Elements Graph: GraphPlot, diff --git a/holoviews/plotting/bokeh/tiles.py b/holoviews/plotting/bokeh/tiles.py new file mode 100644 index 0000000000..f9f749fed2 --- /dev/null +++ b/holoviews/plotting/bokeh/tiles.py @@ -0,0 +1,68 @@ +from __future__ import absolute_import, division, unicode_literals + +import numpy as np + +from bokeh.models import WMTSTileSource, BBoxTileSource, QUADKEYTileSource, SaveTool + +from ...core import util +from ...core.options import SkipRendering +from ...element.tiles import _ATTRIBUTIONS +from .element import ElementPlot + + +class TilePlot(ElementPlot): + + style_opts = ['alpha', 'render_parents', 'level', 'smoothing', 'min_zoom', 'max_zoom'] + + def get_extents(self, element, ranges, range_type='combined'): + extents = super(TilePlot, self).get_extents(element, ranges, range_type) + if (not self.overlaid and all(e is None or not np.isfinite(e) for e in extents) + and range_type in ('combined', 'data')): + x0, x1 = (-20037508.342789244, 20037508.342789244) + y0, y1 = (-20037508.342789255, 20037508.342789244) + global_extent = (x0, y0, x1, y1) + return global_extent + return extents + + def get_data(self, element, ranges, style): + if not isinstance(element.data, util.basestring): + SkipRendering("WMTS element data must be a URL string, " + "bokeh cannot render %r" % element.data) + if '{Q}' in element.data: + tile_source = QUADKEYTileSource + elif all(kw in element.data for kw in ('{XMIN}', '{XMAX}', '{YMIN}', '{YMAX}')): + tile_source = BBoxTileSource + elif all(kw in element.data for kw in ('{X}', '{Y}', '{Z}')): + tile_source = WMTSTileSource + else: + raise ValueError('Tile source URL format not recognized. ' + 'Must contain {X}/{Y}/{Z}, {XMIN}/{XMAX}/{YMIN}/{YMAX} ' + 'or {Q} template strings.') + params = {'url': element.data} + for zoom in ('min_zoom', 'max_zoom'): + if zoom in style: + params[zoom] = style[zoom] + for key, attribution in _ATTRIBUTIONS.items(): + if all(k in element.data for k in key): + params['attribution'] = attribution + return {}, {'tile_source': tile_source(**params)}, style + + def _update_glyph(self, renderer, properties, mapping, glyph, source=None, data=None): + glyph.url = mapping['tile_source'].url + glyph.update(**{k: v for k, v in properties.items() + if k in glyph.properties()}) + renderer.update(**{k: v for k, v in properties.items() + if k in renderer.properties()}) + + def _init_glyph(self, plot, mapping, properties): + """ + Returns a Bokeh glyph object. + """ + tile_source = mapping['tile_source'] + level = properties.pop('level', 'underlay') + renderer = plot.add_tile(tile_source, level=level) + renderer.alpha = properties.get('alpha', 1) + + # Remove save tool + plot.tools = [t for t in plot.tools if not isinstance(t, SaveTool)] + return renderer, tile_source From aa98ae181215cb27c19c4f0840acba9292604c56 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Thu, 21 Feb 2019 15:46:10 -0600 Subject: [PATCH 02/11] Deleted trailing whitespace --- holoviews/element/tiles.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/holoviews/element/tiles.py b/holoviews/element/tiles.py index 8cc7cce0db..e0997e8e38 100644 --- a/holoviews/element/tiles.py +++ b/holoviews/element/tiles.py @@ -35,13 +35,13 @@ def __init__(self, data, kdims=None, vdims=None, **params): % (type(self).__name__, type(data).__name__) ) super(Tiles, self).__init__(data, kdims=kdims, vdims=vdims, **params) - + def range(self, dim, data_range=True, dimension_range=True): return np.nan, np.nan def dimension_values(self, dimension, expanded=True, flat=True): return np.array([]) - + # Mapping between patterns to match specified as tuples and tuples containing attributions _ATTRIBUTIONS = { From 1de4c98708cd3dbe3911a91c5c20aaca12670ab8 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Thu, 21 Feb 2019 15:46:35 -0600 Subject: [PATCH 03/11] Declared the Tiles group parameter as constant --- holoviews/element/tiles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/element/tiles.py b/holoviews/element/tiles.py index e0997e8e38..99e7ef8fe2 100644 --- a/holoviews/element/tiles.py +++ b/holoviews/element/tiles.py @@ -21,7 +21,7 @@ class Tiles(Element2D): The key dimensions of a geometry represent the x- and y- coordinates in a 2D space.""") - group = param.String(default='Tiles') + group = param.String(default='Tiles', constant=True) def __init__(self, data, kdims=None, vdims=None, **params): try: From 4b445fabe2a9dc967a0f179679c47e1c83a87f19 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 22 Feb 2019 16:31:39 +0000 Subject: [PATCH 04/11] Predefine Tiles as lambdas --- holoviews/element/tiles.py | 41 +++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/holoviews/element/tiles.py b/holoviews/element/tiles.py index 99e7ef8fe2..e4d1f86f52 100644 --- a/holoviews/element/tiles.py +++ b/holoviews/element/tiles.py @@ -1,5 +1,7 @@ from __future__ import absolute_import, division, unicode_literals +from types import FunctionType + import param import numpy as np @@ -35,7 +37,6 @@ def __init__(self, data, kdims=None, vdims=None, **params): % (type(self).__name__, type(data).__name__) ) super(Tiles, self).__init__(data, kdims=kdims, vdims=vdims, **params) - def range(self, dim, data_range=True, dimension_range=True): return np.nan, np.nan @@ -94,31 +95,29 @@ def dimension_values(self, dimension, expanded=True, flat=True): } # CartoDB basemaps -CartoDark = Tiles('https://cartodb-basemaps-4.global.ssl.fastly.net/dark_all/{Z}/{X}/{Y}.png', name="CartoDark") -CartoEco = Tiles('http://3.api.cartocdn.com/base-eco/{Z}/{X}/{Y}.png', name="CartoEco") -CartoLight = Tiles('https://cartodb-basemaps-4.global.ssl.fastly.net/light_all/{Z}/{X}/{Y}.png', name="CartoLight") -CartoMidnight = Tiles('http://3.api.cartocdn.com/base-midnight/{Z}/{X}/{Y}.png', name="CartoMidnight") +CartoDark = lambda: Tiles('https://cartodb-basemaps-4.global.ssl.fastly.net/dark_all/{Z}/{X}/{Y}.png', name="CartoDark") +CartoEco = lambda: Tiles('http://3.api.cartocdn.com/base-eco/{Z}/{X}/{Y}.png', name="CartoEco") +CartoLight = lambda: Tiles('https://cartodb-basemaps-4.global.ssl.fastly.net/light_all/{Z}/{X}/{Y}.png', name="CartoLight") +CartoMidnight = lambda: Tiles('http://3.api.cartocdn.com/base-midnight/{Z}/{X}/{Y}.png', name="CartoMidnight") # Stamen basemaps -StamenTerrain = Tiles('http://tile.stamen.com/terrain/{Z}/{X}/{Y}.png', name="StamenTerrain") -StamenTerrainRetina = Tiles('http://tile.stamen.com/terrain/{Z}/{X}/{Y}@2x.png', name="StamenTerrainRetina") -StamenWatercolor = Tiles('http://tile.stamen.com/watercolor/{Z}/{X}/{Y}.jpg', name="StamenWatercolor") -StamenToner = Tiles('http://tile.stamen.com/toner/{Z}/{X}/{Y}.png', name="StamenToner") -StamenTonerBackground = Tiles('http://tile.stamen.com/toner-background/{Z}/{X}/{Y}.png', name="StamenTonerBackground") -StamenLabels = Tiles('http://tile.stamen.com/toner-labels/{Z}/{X}/{Y}.png', name="StamenLabels") +StamenTerrain = lambda: Tiles('http://tile.stamen.com/terrain/{Z}/{X}/{Y}.png', name="StamenTerrain") +StamenTerrainRetina = lambda: Tiles('http://tile.stamen.com/terrain/{Z}/{X}/{Y}@2x.png', name="StamenTerrainRetina") +StamenWatercolor = lambda: Tiles('http://tile.stamen.com/watercolor/{Z}/{X}/{Y}.jpg', name="StamenWatercolor") +StamenToner = lambda: Tiles('http://tile.stamen.com/toner/{Z}/{X}/{Y}.png', name="StamenToner") +StamenTonerBackground = lambda: Tiles('http://tile.stamen.com/toner-background/{Z}/{X}/{Y}.png', name="StamenTonerBackground") +StamenLabels = lambda: Tiles('http://tile.stamen.com/toner-labels/{Z}/{X}/{Y}.png', name="StamenLabels") # Esri maps (see https://server.arcgisonline.com/arcgis/rest/services for the full list) -EsriImagery = Tiles('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{Z}/{Y}/{X}.jpg', name="EsriImagery") -EsriNatGeo = Tiles('https://server.arcgisonline.com/ArcGIS/rest/services/NatGeo_World_Map/MapServer/tile/{Z}/{Y}/{X}', name="EsriNatGeo") -EsriUSATopo = Tiles('https://server.arcgisonline.com/ArcGIS/rest/services/USA_Topo_Maps/MapServer/tile/{Z}/{Y}/{X}', name="EsriUSATopo") -EsriTerrain = Tiles('https://server.arcgisonline.com/ArcGIS/rest/services/World_Terrain_Base/MapServer/tile/{Z}/{Y}/{X}', name="EsriTerrain") -EsriReference = Tiles('http://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Reference_Overlay/MapServer/tile/{Z}/{Y}/{X}', name="EsriReference") +EsriImagery = lambda: Tiles('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{Z}/{Y}/{X}.jpg', name="EsriImagery") +EsriNatGeo = lambda: Tiles('https://server.arcgisonline.com/ArcGIS/rest/services/NatGeo_World_Map/MapServer/tile/{Z}/{Y}/{X}', name="EsriNatGeo") +EsriUSATopo = lambda: Tiles('https://server.arcgisonline.com/ArcGIS/rest/services/USA_Topo_Maps/MapServer/tile/{Z}/{Y}/{X}', name="EsriUSATopo") +EsriTerrain = lambda: Tiles('https://server.arcgisonline.com/ArcGIS/rest/services/World_Terrain_Base/MapServer/tile/{Z}/{Y}/{X}', name="EsriTerrain") +EsriReference = lambda: Tiles('http://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Reference_Overlay/MapServer/tile/{Z}/{Y}/{X}', name="EsriReference") ESRI = EsriImagery # For backwards compatibility with gv 1.5 - # Miscellaneous -OSM = Tiles('http://c.tile.openstreetmap.org/{Z}/{X}/{Y}.png', name="OSM") -Wikipedia = Tiles('https://maps.wikimedia.org/osm-intl/{Z}/{X}/{Y}@2x.png', name="Wikipedia") - +OSM = lambda: Tiles('http://c.tile.openstreetmap.org/{Z}/{X}/{Y}.png', name="OSM") +Wikipedia = lambda: Tiles('https://maps.wikimedia.org/osm-intl/{Z}/{X}/{Y}@2x.png', name="Wikipedia") -tile_sources = {k: v for k, v in locals().items() if isinstance(v, Tiles) and k != 'ESRI'} +tile_sources = {k: v for k, v in locals().items() if isinstance(v, FunctionType) and k != 'ESRI'} From b0d2f3fa10acb05b169d709e71b4c5bcfea0a046 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 22 Feb 2019 16:51:05 +0000 Subject: [PATCH 05/11] Implement Mercator axis formatting on tiles --- holoviews/plotting/bokeh/element.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 70cd95c813..54a0bd8ac2 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -13,8 +13,10 @@ from bokeh.models import tools from bokeh.models import ( Renderer, Range1d, DataRange1d, Title, FactorRange, Legend, - FuncTickFormatter, TickFormatter, PrintfTickFormatter) -from bokeh.models.tickers import Ticker, BasicTicker, FixedTicker, LogTicker + FuncTickFormatter, TickFormatter, PrintfTickFormatter, + MercatorTickFormatter, BoxZoomTool) +from bokeh.models.tickers import ( + Ticker, BasicTicker, FixedTicker, LogTicker, MercatorTicker) from bokeh.models.widgets import Panel, Tabs from bokeh.models.mappers import LinearColorMapper from bokeh.models import CategoricalAxis @@ -28,7 +30,7 @@ from ...core import DynamicMap, CompositeOverlay, Element, Dimension from ...core.options import abbreviated_exception, SkipRendering from ...core import util -from ...element import Graph, VectorField, Path, Contours +from ...element import Graph, VectorField, Path, Contours, Tiles from ...streams import Buffer from ...util.transform import dim from ..plot import GenericElementPlot, GenericOverlayPlot @@ -145,6 +147,9 @@ def __init__(self, element, plot=None, **params): self.callbacks = self._construct_callbacks() self.static_source = False self.streaming = [s for s in self.streams if isinstance(s, Buffer)] + self.geographic = bool(self.hmap.last.traverse(lambda x: x, Tiles)) + if self.geographic and self.projection is None: + self.projection = 'mercator' # Whether axes are shared between plots self._shared = {'x': False, 'y': False} @@ -557,7 +562,11 @@ def _axis_properties(self, axis, key, plot, dimension=None, elif axis == 'y': axis_obj = plot.yaxis[0] - if isinstance(axis_obj, CategoricalAxis): + if self.geographic and self.projection == 'mercator': + dimension = 'lon' if axis == 'x' else 'lat' + axis_props['ticker'] = MercatorTicker(dimension=dimension) + axis_props['formatter'] = MercatorTickFormatter(dimension=dimension) + elif isinstance(axis_obj, CategoricalAxis): for key in list(axis_props): if key.startswith('major_label'): # set the group labels equal to major (actually minor) From 549458c6b9f7c964dbb41be51f4efbc70286c499 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 22 Feb 2019 18:27:17 +0000 Subject: [PATCH 06/11] Fix aspect for tile plots --- holoviews/plotting/bokeh/element.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 54a0bd8ac2..28c6893c73 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -566,6 +566,9 @@ def _axis_properties(self, axis, key, plot, dimension=None, dimension = 'lon' if axis == 'x' else 'lat' axis_props['ticker'] = MercatorTicker(dimension=dimension) axis_props['formatter'] = MercatorTickFormatter(dimension=dimension) + box_zoom = self.state.select(type=BoxZoomTool) + if box_zoom: + box_zoom[0].match_aspect = True elif isinstance(axis_obj, CategoricalAxis): for key in list(axis_props): if key.startswith('major_label'): From 74a887274cf8cf145e87687ab1d97df8ca6dc23f Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 22 Feb 2019 18:42:40 +0000 Subject: [PATCH 07/11] Added Tiles element reference notebook --- examples/reference/elements/bokeh/Tiles.ipynb | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 examples/reference/elements/bokeh/Tiles.ipynb diff --git a/examples/reference/elements/bokeh/Tiles.ipynb b/examples/reference/elements/bokeh/Tiles.ipynb new file mode 100644 index 0000000000..92832baf21 --- /dev/null +++ b/examples/reference/elements/bokeh/Tiles.ipynb @@ -0,0 +1,101 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "
\n", + "
Title
Tiles Element
\n", + "
Dependencies
Bokeh
\n", + "
Backends
Bokeh
\n", + "
\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import holoviews as hv\n", + "from holoviews import opts, dim\n", + "hv.extension('bokeh')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The ``Tiles`` element represents a so called web mapping tile source usually used for geographic plots, which fetches tiles appropriate to the current zoom level. To declare a ``Tiles`` element simply provide a URL to the tile server, a standard tile server URL has a number of templated variables which describe the location and zoom level. In the most common case that is a WMTS tile source which looks like this:\n", + "\n", + " 'https://maps.wikimedia.org/osm-intl/{Z}/{X}/{Y}@2x.png'\n", + " \n", + "Here ``{X}``, ``{Y}`` and ``{Z}`` describe the location and zoom level of each tile. Alternative formats include bbox tile sources with ``{XMIN}``, ``{XMAX}``, ``{YMIN}``, ``{YMAX}`` variables, and quad-key tile sources with a single ``{Q}`` variable.\n", + "\n", + "A simple example of a WMTS tile source is the Wikipedia maps:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "hv.Tiles('https://maps.wikimedia.org/osm-intl/{Z}/{X}/{Y}@2x.png', name=\"Wikipedia\").opts(width=600, height=550)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "One thing to note about tile sources is that they are always defined in the [pseudo-Mercator projection](https://epsg.io/3857), which means that if you want to overlay any data on top of a tile source the values have to be expressed as eastings and northings. If you have data in another projection, e.g. latitudes and longitudes, it may make sense to use [GeoViews](http://geoviews.org/) for it to handle the projections for you.\n", + "\n", + "Both HoloViews and GeoViews provides a number of tile sources by default, provided by CartoDB, Stamen, OpenStreetMap, Esri and Wikipedia. These can be imported from the ``holoviews.element.tiles`` module and are provided as callable functions which return a ``Tiles`` element:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "hv.element.tiles.EsriImagery().opts(width=600, height=550)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The full set of predefined tile sources can be accessed on the ``holoviews.element.tiles.tile_sources`` dictionary:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "hv.Layout([ts().relabel(name) for name, ts in hv.element.tiles.tile_sources.items()]).opts(\n", + " 'WMTS', xaxis=None, yaxis=None, width=225, height=225).cols(4)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For full documentation and the available style and plot options, use ``hv.help(hv.Tiles).``" + ] + } + ], + "metadata": { + "language_info": { + "name": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 1738a9c53d671f9a9c6ae1c429d2bf74e9b6ee47 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 22 Feb 2019 18:49:52 +0000 Subject: [PATCH 08/11] Small fix to tiles notebook --- examples/reference/elements/bokeh/Tiles.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/reference/elements/bokeh/Tiles.ipynb b/examples/reference/elements/bokeh/Tiles.ipynb index 92832baf21..38d750a198 100644 --- a/examples/reference/elements/bokeh/Tiles.ipynb +++ b/examples/reference/elements/bokeh/Tiles.ipynb @@ -79,7 +79,7 @@ "outputs": [], "source": [ "hv.Layout([ts().relabel(name) for name, ts in hv.element.tiles.tile_sources.items()]).opts(\n", - " 'WMTS', xaxis=None, yaxis=None, width=225, height=225).cols(4)" + " opts.Tiles(xaxis=None, yaxis=None, width=225, height=225)).cols(4)" ] }, { From 03c8232d14508eb5192ac0dd5b124377041b1031 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 22 Feb 2019 18:59:21 +0000 Subject: [PATCH 09/11] Set TilePlot level to glyph --- holoviews/plotting/bokeh/tiles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/plotting/bokeh/tiles.py b/holoviews/plotting/bokeh/tiles.py index f9f749fed2..c9c6d9a93e 100644 --- a/holoviews/plotting/bokeh/tiles.py +++ b/holoviews/plotting/bokeh/tiles.py @@ -59,7 +59,7 @@ def _init_glyph(self, plot, mapping, properties): Returns a Bokeh glyph object. """ tile_source = mapping['tile_source'] - level = properties.pop('level', 'underlay') + level = properties.pop('level', 'glyph') renderer = plot.add_tile(tile_source, level=level) renderer.alpha = properties.get('alpha', 1) From b289a54ab3282bba5de4598b8f40a960edf089d9 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 22 Feb 2019 19:34:36 +0000 Subject: [PATCH 10/11] Handle empty data test for Tiles --- holoviews/tests/element/testelementconstructors.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/holoviews/tests/element/testelementconstructors.py b/holoviews/tests/element/testelementconstructors.py index 8f4f426798..593051985d 100644 --- a/holoviews/tests/element/testelementconstructors.py +++ b/holoviews/tests/element/testelementconstructors.py @@ -5,7 +5,7 @@ Path, Histogram, HeatMap, Contours, Scatter, Points, Polygons, VectorField, Spikes, Area, Bars, ErrorBars, BoxWhisker, Raster, Image, - QuadMesh, RGB, Graph, TriMesh, Div) + QuadMesh, RGB, Graph, TriMesh, Div, Tiles) from holoviews.element.path import BaseShape from holoviews.element.comparison import ComparisonTestCase @@ -30,7 +30,7 @@ def setUp(self): def test_empty_element_constructor(self): failed_elements = [] for name, el in param.concrete_descendents(Element).items(): - if issubclass(el, (Annotation, BaseShape, Div)): + if issubclass(el, (Annotation, BaseShape, Div, Tiles)): continue try: el([]) @@ -41,7 +41,7 @@ def test_empty_element_constructor(self): def test_none_element_constructor(self): failed_elements = [] for name, el in param.concrete_descendents(Element).items(): - if issubclass(el, (Annotation, BaseShape)): + if issubclass(el, (Annotation, BaseShape, Tiles)): continue try: el(None) From 4d137cad2e75ea7266bcb7871faaffab3a26a168 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 22 Feb 2019 19:58:53 +0000 Subject: [PATCH 11/11] Improved Tiles element docstring --- holoviews/element/tiles.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/holoviews/element/tiles.py b/holoviews/element/tiles.py index e4d1f86f52..b8bc98d925 100644 --- a/holoviews/element/tiles.py +++ b/holoviews/element/tiles.py @@ -12,10 +12,21 @@ class Tiles(Element2D): """ - The Tiles Element represents a Web Map Tile Service specified as a - URL containing {x}, {y}, and {z} templating variables, e.g.: + The Tiles element represents tile sources, specified as URL + containing different template variables. These variables + correspond to three different formats for specifying the spatial + location and zoom level of the requested tiles: - https://maps.wikimedia.org/osm-intl/{Z}/{X}/{Y}@2x.png + * Web mapping tiles sources containing {x}, {y}, and {z} variables + + * Bounding box tile sources containing {XMIN}, {XMAX}, {YMIN}, {YMAX} variables + + * Quadkey tile sources containin a {Q} variable + + Tiles are defined in a pseudo-Mercator projection (EPSG:3857) + defined as eastings and northings. Any data overlaid on a tile + source therefore has to be defined in those coordinates or be + projected (e.g. using GeoViews). """ kdims = param.List(default=[Dimension('x'), Dimension('y')],