Skip to content

Commit

Permalink
Added Tiles element from geoviews (#3515)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr authored and jlstevens committed Feb 22, 2019
1 parent 1ce1649 commit e3b3d4d
Show file tree
Hide file tree
Showing 7 changed files with 327 additions and 8 deletions.
101 changes: 101 additions & 0 deletions examples/reference/elements/bokeh/Tiles.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<div class=\"contentcontainer med left\" style=\"margin-left: -50px;\">\n",
"<dl class=\"dl-horizontal\">\n",
" <dt>Title</dt> <dd> Tiles Element</dd>\n",
" <dt>Dependencies</dt> <dd>Bokeh</dd>\n",
" <dt>Backends</dt> <dd><a href='./Tiles.ipynb'>Bokeh</a></dd>\n",
"</dl>\n",
"</div>"
]
},
{
"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",
" opts.Tiles(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
}
1 change: 1 addition & 0 deletions holoviews/element/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
134 changes: 134 additions & 0 deletions holoviews/element/tiles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
from __future__ import absolute_import, division, unicode_literals

from types import FunctionType

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 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:
* 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')],
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', constant=True)

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',) : (
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
),
('cartodb',) : (
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, '
'&copy; <a href="https://cartodb.com/attributions">CartoDB</a>'
),
('cartocdn',) : (
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, '
'&copy; <a href="https://cartodb.com/attributions">CartoDB</a>'
),
('stamen', 'com/t') : ( # to match both 'toner' and 'terrain'
'Map tiles by <a href="https://stamen.com">Stamen Design</a>, '
'under <a href="https://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. '
'Data by <a href="https://openstreetmap.org">OpenStreetMap</a>, '
'under <a href="https://www.openstreetmap.org/copyright">ODbL</a>.'
),
('stamen', 'watercolor') : (
'Map tiles by <a href="https://stamen.com">Stamen Design</a>, '
'under <a href="https://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. '
'Data by <a href="https://openstreetmap.org">OpenStreetMap</a>, '
'under <a href="https://creativecommons.org/licenses/by-sa/3.0">CC BY SA</a>.'
),
('wikimedia',) : (
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
),
('arcgis','Terrain') : (
'&copy; <a href="http://downloads.esri.com/ArcGISOnline/docs/tou_summary.pdf">Esri</a>, '
'USGS, NOAA'
),
('arcgis','Reference') : (
'&copy; <a href="http://downloads.esri.com/ArcGISOnline/docs/tou_summary.pdf">Esri</a>, '
'Garmin, USGS, NPS'
),
('arcgis','Imagery') : (
'&copy; <a href="http://downloads.esri.com/ArcGISOnline/docs/tou_summary.pdf">Esri</a>, '
'Earthstar Geographics'
),
('arcgis','NatGeo') : (
'&copy; <a href="http://downloads.esri.com/ArcGISOnline/docs/tou_summary.pdf">Esri</a>, '
'NatGeo, Garmin, HERE, UNEP-WCMC, USGS, NASA, ESA, METI, NRCAN, GEBCO, NOAA, Increment P'
),
('arcgis','USA_Topo') : (
'&copy; <a href="http://downloads.esri.com/ArcGISOnline/docs/tou_summary.pdf">Esri</a>, '
'NatGeo, i-cubed'
)
}

# CartoDB basemaps
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 = 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 = 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 = 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, FunctionType) and k != 'ESRI'}
5 changes: 4 additions & 1 deletion holoviews/plotting/bokeh/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)


Expand Down Expand Up @@ -100,6 +102,7 @@
Spline: SplinePlot,
Arrow: ArrowPlot,
Div: DivPlot,
Tiles: TilePlot,

# Graph Elements
Graph: GraphPlot,
Expand Down
20 changes: 16 additions & 4 deletions holoviews/plotting/bokeh/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -557,7 +562,14 @@ 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)
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'):
# set the group labels equal to major (actually minor)
Expand Down
68 changes: 68 additions & 0 deletions holoviews/plotting/bokeh/tiles.py
Original file line number Diff line number Diff line change
@@ -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', 'glyph')
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
Loading

0 comments on commit e3b3d4d

Please sign in to comment.