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'
from bokeh.sampledata.airport_routes import airports, routes
gv.tile_sources.Wikipedia *\
gv.Points(airports, ['Longitude', 'Latitude']).iloc[:100] *\
gv.Points(airports, ['Longitude', 'Latitude']).iloc[200:300]
%%output backend='bokeh'
gv.tile_sources.Wikipedia *\
gv.Points(airports, ['Longitude', 'Latitude']).iloc[:100] *\
gv.Points(airports, ['Longitude', 'Latitude']).iloc[200:300].options(width=800, height=400)
Terrain() * gv.Path([[(0, 0), (-70, 30)]])
paths = []
honolulu = (-157.9219970703125, 21.318700790405273)
routes = routes[routes.SourceID==3728]
airports = airports[airports.AirportID.isin(list(routes.DestinationID)+[3728])]
points = gv.Nodes(airports, ['Longitude', 'Latitude', 'AirportID'], ['Name', 'City'])
graph = gv.Graph((routes, points), ['SourceID', 'DestinationID'])
gv.tile_sources.OSM * graph
%%output backend='bokeh'
gv.tile_sources.OSM * graph.options(width=800, height=400)
Plot two polygons from a geopandas dataframe:
import geopandas as gpd
gv.tile_sources.OSM * gv.Polygons(gpd.read_file(gpd.datasets.get_path('naturalearth_lowres')).iloc[[0, 5]], vdims=['pop_est', ('name', 'Country')])
duck = 'https://assets.holoviews.org/temp/Duck/glTF/Duck.gltf'
lantern = 'https://assets.holoviews.org/temp/Lantern/glTF/Lantern.gltf'
def random_position(lon, lat):
    return np.random.rand()*40+lon, np.random.rand()*20+lat, 0
Randomly positions lantern and duck 3d models on a terrain map:
Terrain() *\
hv.Overlay([Model3d(duck, random_position(-60, 30), 100000) for i in range(50)]) *\
hv.Overlay([Model3d(lantern, random_position(-110, 30), 10000) for i in range(50)])
Risks:
Time estimates: