import numpy as np
import holoviews as hv
import geoviews as gv
import cartopy.crs as ccrs
import cesiumpy
Requires: https://github.com/philippjfr/cesiumpy
import param
from holoviews.plotting.renderer import Renderer, MIME_TYPES
import cesiumpy.util.html as cesium_html
NOTEBOOK_DIV = """
{plot_div}
<script type="text/javascript">
{plot_script}
</script>
"""
class CesiumRenderer(Renderer):
backend = param.String(default='cesium', doc="The backend name.")
fig = param.ObjectSelector(default='auto', objects=['html', 'auto'], doc="""
Output render format for static figures. If None, no figure
rendering will occur. """)
holomap = param.ObjectSelector(default='auto',
objects=['widgets', 'scrubber',
None, 'auto'], doc="""
Output render multi-frame (typically animated) format. If
None, no multi-frame rendering will occur.""")
mode = param.ObjectSelector(default='default',
objects=['default', 'server'], doc="""
Whether to render the object in regular or server mode. In server
mode a bokeh Document will be returned which can be served as a
bokeh server app. By default renders all output is rendered to HTML.""")
token = param.String(default=None)
# Defines the valid output formats for each mode.
mode_formats = {'fig': {'default': ['html', 'auto'],
'server': ['html', 'auto']},
'holomap': {'default': ['widgets', 'scrubber', 'auto', None],
'server': ['server', 'auto', None]}}
backend_dependencies = {'js': ['https://cesiumjs.org/Cesium/Build/Cesium/Cesium.js'],
'css': ['https://cesiumjs.org/Cesium/Build/Cesium/Widgets/widgets.css']}
_loaded = False
def __call__(self, obj, fmt=None, doc=None):
"""
Render the supplied HoloViews component using the appropriate
backend. The output is not a file format but a suitable,
in-memory byte stream together with any suitable metadata.
"""
plot, fmt = self._validate(obj, fmt)
info = {'file-ext': fmt, 'mime_type': MIME_TYPES[fmt]}
if fmt == 'html':
html = self._figure_data(plot)
html = "<div style='display: table; margin: 0 auto;'>%s</div>" % html
return self._apply_post_render_hooks(html, obj, fmt), info
def _figure_data(self, plot, fmt='html', as_script=False, **kwargs):
obj = plot.state
div = obj.container
if False: #self.token:
token = "Cesium.Ion.DefaultAccessToken = %r;" % self.token
else:
token = ''
raw_js = '\n'.join([token]+obj.script)
js = ('function load_{id}() {{ if ((window.Cesium == undefined) || (document.readyState != "complete"))'
'{{ setTimeout(load_{id}, 50); return;}}; %s }}; setTimeout(load_{id}, 50)'.format(id=plot.id) % raw_js)
if as_script:
return div, js
return NOTEBOOK_DIV.format(plot_div=div, plot_script=js)
@classmethod
def load_nb(cls, inline=True):
"""
Loads the bokeh notebook resources.
"""
from bokeh.core.templates import AUTOLOAD_NB_JS
from IPython.display import publish_display_data
load_type = MIME_TYPES['jlab-hv-load']
JS = AUTOLOAD_NB_JS.render(
elementid = '',
js_urls = cls.backend_dependencies['js'],
css_urls = cls.backend_dependencies['css'],
js_raw = '',
css_raw = '',
force = True,
timeout = 5000,
register_mime = False
)
mime_bundle = {load_type: JS, 'application/javascript': JS}
publish_display_data(data=mime_bundle)
def plot_options(cls, obj, percent_size):
return {}
hv.Store.renderers['cesium'] = CesiumRenderer.instance()
from geoviews.element.geo import _GeoFeature
class Terrain(_GeoFeature):
def __init__(self, data=None, **params):
super(Terrain, self).__init__(data, **params)
class Model3d(_GeoFeature):
def __init__(self, data, position, scale, **params):
m = cesiumpy.Model(url=data, modelMatrix=position, scale=scale)
super(Model3d, self).__init__(m, **params)
import uuid
from holoviews.core import util
from holoviews.plotting.plot import DimensionedPlot, GenericElementPlot, GenericOverlayPlot
from holoviews.core import CompositeOverlay
from holoviews.plotting.util import hex2rgb
def preprocess_style(style, rename={}):
new_style = {}
for k, v in style.items():
if ('color' in k and v.startswith('#')):
v = cesiumpy.color.Color(*(c/255. for c in hex2rgb(v)))
k = rename.get(k, k)
new_style[k] = v
return new_style
class CesiumPlot(DimensionedPlot):
width = param.Integer(default=900, doc="""
Width of the plot in pixels""")
height = param.Integer(default=500, doc="""
Height of the plot in pixels""")
backend = 'cesium'
def __init__(self, obj, **kwargs):
self._id = uuid.uuid4().hex
super(CesiumPlot, self).__init__(obj, **kwargs)
@property
def id(self):
return self._id
@property
def state(self):
"""
The plotting state that gets updated via the update method and
used by the renderer to generate output.
"""
return self.handles['plot']
class ElementPlot(CesiumPlot, GenericElementPlot):
finalize_hooks = param.HookList(default=[], doc="""
Optional list of hooks called when finalizing an axis.
The hook is passed the plot object and the displayed
object, other plotting handles can be accessed via plot.handles.""")
def initialize_plot(self, ranges=None, plot=None):
"""
Initializes a new plot object with the last available frame.
"""
# Get element key and ranges for frame
if self.batched:
element = [el for el in self.hmap.data.values() if el][-1]
else:
element = self.hmap.last
key = util.wrap_tuple(self.hmap.last_key)
ranges = self.compute_ranges(self.hmap, key, ranges)
self.current_ranges = ranges
self.current_frame = element
self.current_key = key
style_element = element.last if self.batched else element
ranges = util.match_spec(style_element, ranges)
# Initialize plot, source and glyph
if plot is None:
plot = self._init_plot(key, style_element, ranges=ranges)
self._init_axes(plot)
self.handles['plot'] = plot
self._init_glyphs(plot, element, ranges)
if not self.overlaid:
self._update_plot(key, plot, style_element)
self._update_ranges(style_element, ranges)
self._execute_hooks(element)
self.drawn = True
return plot
def _init_plot(self, key, element, ranges):
width = '%dpx' % self.width
height = '%dpx' % self.height
divid = uuid.uuid4().hex
return cesiumpy.Viewer(width=width, height=height, divid=divid)
def _init_axes(self, plot):
pass
def _init_glyphs(self, plot, element, ranges):
pass
def _update_plot(self, key, plot, element):
pass
def _update_ranges(self, element, ranges):
pass
class OverlayPlot(GenericOverlayPlot, ElementPlot):
_propagate_options = ['width', 'height']
def initialize_plot(self, ranges=None, plot=None, plots=None):
key = util.wrap_tuple(self.hmap.last_key)
nonempty = [el for el in self.hmap.data.values() if el]
if not nonempty:
raise SkipRendering('All Overlays empty, cannot initialize plot.')
element = nonempty[-1]
ranges = self.compute_ranges(self.hmap, key, ranges)
if plot is None and not self.batched:
plot = self._init_plot(key, element, ranges=ranges)
self._init_axes(plot)
self.handles['plot'] = plot
if plot and not self.overlaid:
self._update_plot(key, plot, element)
self._update_ranges(element, ranges)
panels = []
for key, subplot in self.subplots.items():
frame = None
child = subplot.initialize_plot(ranges, plot)
if isinstance(element, CompositeOverlay):
frame = element.get(key, None)
subplot.current_frame = frame
if self.batched:
self.handles['plot'] = child
self.drawn = True
self.handles['plots'] = plots
self._execute_hooks(element)
return self.handles['plot']
class WMTSPlot(ElementPlot):
def _init_glyphs(self, plot, element, ranges):
index = element.data.index('{Z}')-1
url = element.data[:index]
extension = element.data.split('.')[-1]
imagery = cesiumpy.createOpenStreetMapImageryProvider(url=url, fileExtension=extension)
plot.imageryProvider = imagery
plot.baseLayerPicker = False
class TerrainPlot(ElementPlot):
def _init_glyphs(self, plot, element, ranges):
url = element.data
if url is None:
imagery = cesiumpy.provider.createWorldTerrain()
else:
imagery = cesiumpy.provider.TerrainProvider(url=url)
plot.terrainProvider = imagery
plot.baseLayerPicker = False
class PointPlot(ElementPlot):
style_opts = ['color']
def _init_glyphs(self, plot, element, ranges):
style = preprocess_style(self.style[self.cyclic_index])
points = gv.project(element, projection=ccrs.PlateCarree())
for x, y in points.array([0, 1]):
point = cesiumpy.Point(position=[x, y, 0], **style)
plot.entities.add(point)
class Model3dPlot(ElementPlot):
def _init_glyphs(self, plot, element, ranges):
plot.scene.primitives.add(element.data)
class PathPlot(ElementPlot):
style_opts = ['color', 'line_width']
def _init_glyphs(self, plot, element, ranges):
rename = {'line_width': 'width', 'color': 'material'}
style = preprocess_style(self.style[self.cyclic_index], rename)
path = gv.project(element, projection=ccrs.PlateCarree())
for p in path.split(datatype='array'):
polyline = cesiumpy.Polyline(positions=p.flatten(), **style)
plot.entities.add(polyline)
class PolygonPlot(ElementPlot):
style_opts = ['color', 'line_width']
def _init_glyphs(self, plot, element, ranges):
rename = {'line_width': 'width', 'color': 'material'}
style = preprocess_style(self.style[self.cyclic_index], rename)
path = gv.project(element, projection=ccrs.PlateCarree())
for p in path.split():
polygon = cesiumpy.Polygon(hierarchy=list(p.array([0, 1]).flatten()), **style)
plot.entities.add(polygon)
class GraphPlot(ElementPlot):
style_opts = ['color', 'line_width']
def _init_glyphs(self, plot, element, ranges):
rename = {'line_width': 'width', 'color': 'material'}
style = preprocess_style(self.style[self.cyclic_index], rename)
graph = gv.project(element, projection=ccrs.PlateCarree())
for p in graph.edgepaths.split(datatype='array'):
polyline = cesiumpy.Polyline(positions=p.flatten(), **style)
plot.entities.add(polyline)
for x, y in graph.nodes.array([0, 1]):
point = cesiumpy.Point(position=[x, y, 0])
plot.entities.add(point)
#class ImagePlot()
hv.Store.register({hv.Element: ElementPlot, hv.Overlay: OverlayPlot,
gv.WMTS: WMTSPlot,
gv.Points: PointPlot,
gv.Path: PathPlot,
gv.Graph: GraphPlot,
gv.Polygons: PolygonPlot,
Terrain: TerrainPlot, Model3d: Model3dPlot}, 'cesium')
options = hv.Store.options('cesium')
options.Points = hv.Options('style', color=hv.Cycle())
options.Path = hv.Options('style', color=hv.Cycle())
hv.extension('cesium', 'bokeh')
hv.Store.current_backend = 'cesium'