from enum import Enum
from typing import Dict, Union, List, Optional, Tuple, Set
import numpy as np
import bokeh.models
from shyft.dashboard.base.hashable import Hashable
from shyft.dashboard.time_series.bindable import Bindable, BindableError
[docs]
class MapLayerError(RuntimeError):
pass
[docs]
class MapLayerType(Enum):
"""
Enum defining expected keys to create a bokeh glyph
Note:
- First variable should be x coordinates
- Second variable should be y coordinates
"""
POINT = ('x', 'y')
POLYGON = ('xs', 'ys')
LABEL = ('x', 'y', 'text')
layer_type_glyph_reference = {MapLayerType.POINT: bokeh.models.Circle,
MapLayerType.POLYGON: bokeh.models.Patches,
MapLayerType.LABEL: bokeh.models.Text}
[docs]
class MapLayer(Bindable, Hashable):
[docs]
def __init__(self, *,
map_viewer: 'statkraft.bokeh.maps.map_viewer.MapViewer',
name: str,
layer_type: MapLayerType,
glyph_variable_kwargs: Dict[str, Union[str, float, int]],
glyph_fixed_kwargs: Dict[str, Union[str, float, int]],
visible: bool = True,
selectable: bool = False,
update_axes: bool = True,
hover_tool_tips: Optional[List[Tuple[str, str]]] = None) -> None:
"""
Layer of Map Viewer
Each layer adds a bokeh glyph to the map. The draw order is controlled by the order the layers are created.
The type of bokeh glyphs are controlled by the LayerType:
- LayerType.POINT => Circles
- nLayerType.POLYGON => Patches
Labels are only supported for LayerType.POINT
Parameters
----------
map_viewer:
MapViewer instance to add this layer to
name:
name of the layer
layer_type:
type of the layer
glyph_variable_kwargs:
glyph variable kwargs which are controlled in the data source e.g. {'xs': 'x_values'}
glyph_fixed_kwargs:
glyph fixed kwargs which are set only by initialisation e.g {'color': 'red'}
visible:
visibility of the layer at initialisation
selectable:
if glyphs can be selected with the selection tool
update_axes:
if the bbox of this layer should be used in MapViewer.figure_axis
hover_tool_tips:
hover over information
"""
Hashable.__init__(self)
Bindable.__init__(self)
self.name = name
self.hover_tool_tips = hover_tool_tips
self.hover_tool = None
self.selectable = selectable
self._visible = visible
self.update_axes = update_axes
if glyph_variable_kwargs is None:
raise MapLayerError(f'Map layer {name}: No glyph_variable_kwargs for source defined!')
tag_diff = set(glyph_variable_kwargs.keys()).symmetric_difference(glyph_fixed_kwargs.keys())
if len(tag_diff) != len(glyph_variable_kwargs.keys()) + len(glyph_fixed_kwargs.keys()):
msg = f"""Map layer {self.name}: kwargs are double defined in glyph_variable_kwargs
and glyph_fixed_kwargs: {tag_diff} in both!"""
raise MapLayerError(msg)
self.source_keys = {v for v in glyph_variable_kwargs.values()}
if hover_tool_tips:
required_source_keys = find_tags_from_tooltips(hover_tool_tips)
self.source_keys.update(required_source_keys)
# check layer type
for coordinate_ref in layer_type.value:
if coordinate_ref not in glyph_variable_kwargs.keys():
raise MapLayerError(f'Map layer {name}: {coordinate_ref} not defined in bokeh_source_kwargs!')
self.layer_type = layer_type
# create bokeh data source
self.source = bokeh.models.ColumnDataSource({k: [] for k in self.source_keys})
# create bokeh glyph
try:
self.glyph = layer_type_glyph_reference[self.layer_type](**glyph_variable_kwargs, **glyph_fixed_kwargs)
except (RuntimeError, AttributeError) as e:
raise MapLayerError(f'Map layer {name}: cannot create bokeh glyph! {e}')
# bokeh renderer
self.renderer = None
self.hover_tool = None
# bind to the map provided
self.bind(parent=map_viewer)
[docs]
def on_bind(self, *, parent: 'statkraft.bokeh.maps.map_viewer.MapViewer') -> None:
"""
Function which is call when bound to a figure
"""
try:
self.renderer, self.hover_tool = parent.add_layer(self)
except (RuntimeError, AttributeError) as e:
raise BindableError(f"Cannot bind to {parent}: {e}")
# set visibility of the renderer
self.renderer.visible = self._visible
@property
def bbox(self) -> Optional[Tuple[float, float, float, float]]:
"""
returns bounding box of defined geometry i.e x_min, y_min, x_max, y_max if data is defined
"""
layer_ref = self.layer_type.value
if len(layer_ref) < 2:
raise MapLayerError(f"Layer type not proper defined {self.layer_type}")
x_ref, y_ref = layer_ref[0], layer_ref[1]
x_coords = self.source.data[x_ref]
y_coords = self.source.data[y_ref]
if len(x_coords) == 0 or len(y_coords) == 0:
return None
return np.min(x_coords), np.min(y_coords), np.max(x_coords), np.max(y_coords)
[docs]
def check_data_compatibility(self, new_data: Dict[str, Union[List[str], List[int], List[float]]]) -> None:
"""
This function checks if new data is compatible with layer
Raises
------
LayerError when incompatible
"""
expected_keys = self.source_keys
if len(set(new_data.keys()).symmetric_difference(expected_keys)):
diff = set(new_data.keys()).symmetric_difference(expected_keys)
raise MapLayerError(f'Map layer {self.name}: cannot update data, different keys defined {diff}!')
if not all(isinstance(x, list) for x in new_data.values()):
t = ' '.join([f'{k}: {type(v)}' for k, v in new_data.items()])
raise MapLayerError(f'Map layer {self.name}: cannot update data, not all values of data are type list: {t}!')
n = len(list(new_data.values())[0])
if not all(len(x) == n for x in new_data.values()):
l = ' '.join([f'{k}: {len(v)}' for k, v in new_data.items()])
raise MapLayerError(f'Map layer {self.name}: cannot update data, different variable lengths defined: {l}!')
[docs]
def updated_data(self, new_data: Dict[str, Union[List[str], List[int], List[float]]]) -> None:
"""
This function is updates the layer data if proper defined
"""
self.check_data_compatibility(new_data)
self.source.data = new_data
@property
def visible(self) -> bool:
"""
Visibility of the glyph
"""
return self._visible
@visible.setter
def visible(self, vis: bool) -> None:
"""
Setter of visibility
"""
if vis == self._visible:
return
self._visible = vis
if self.renderer is not None:
self.renderer.visible = self._visible