"""Extend functionalities of the ipyleaflet layer control."""
import json
from types import SimpleNamespace
from typing import Optional
import ipyvuetify as v
from ipyleaflet import GeoJSON, Map, TileLayer
from ipywidgets import link
from sepal_ui import color
from sepal_ui import sepalwidgets as sw
from sepal_ui.frontend import styles as ss
from sepal_ui.mapping.menu_control import MenuControl
from sepal_ui.message import ms
[docs]
class BaseRow(sw.Html):
w_radio: Optional[sw.SimpleCheckbox] = None
"the radio to hide/show the layer"
[docs]
def __init__(self, layer: TileLayer) -> None:
"""Html row element to describe a base layer.
This Html element include all the controls to manipulate the basemap displayed in the map
- a checkbox to show/hide (it will behave like a radio)
Args:
layer: the layer associated to the row
"""
# create the checkbox, by default layer are visible
self.w_radio = sw.Radio(small=True, value=layer.name)
kwargs = {"style": "width: 10%;", "tag": "td"}
radio_cell = sw.Html(children=[self.w_radio], **kwargs)
# create the label
kwargs = {"style_": "width: 40%;", "tag": "td"}
label_cell = sw.Html(children=[layer.name], **kwargs)
# create an empty row to align on
kwargs = {"style_": "width: 50%;", "tag": "td"}
empty_cell = sw.Html(children=[""], **kwargs)
# build a html tr from it
super().__init__(tag="tr", children=[label_cell, empty_cell, radio_cell])
# add js behavior
link((layer, "visible"), (self.w_radio, "active"))
[docs]
class LayerRow(sw.Html):
w_checkbox: Optional[sw.SimpleCheckbox] = None
"the ckeckbox to hide/show the layer"
w_slider: Optional[sw.SimpleSlider] = None
"the slider linked to the opacity of the layer"
[docs]
def __init__(self, layer: TileLayer) -> None:
"""Html row element to describe a normal layer.
This Html element include all the controls to manipulate the layer displayed in the map
- a checkbox to show/hide
- a slider to change alpha channel
Args:
layer: the layer associated to the row
"""
# create the checkbox, by default layer are visible
self.w_checkbox = sw.SimpleCheckbox(
v_model=True, small=True, label=layer.name, color=color.primary
)
kwargs = {"style": "width: 10%;", "tag": "td"}
checkbox_cell = sw.Html(children=[self.w_checkbox], **kwargs)
# create the label
kwargs = {"style_": "width: 40%;", "tag": "td"}
label_cell = sw.Html(children=[layer.name], **kwargs)
# create the slider
self.w_slider = sw.SimpleSlider(v_model=1, max=1, step=0.01, small=True)
kwargs = {"style_": "width: 50%;", "tag": "td"}
slider_cell = sw.Html(children=[self.w_slider], **kwargs)
# build a html tr from it
super().__init__(tag="tr", children=[label_cell, slider_cell, checkbox_cell])
# add js behavior
self.w_checkbox.observe(self._toggle_slider, "v_model")
link((layer, "opacity"), (self.w_slider, "v_model"))
link((layer, "visible"), (self.w_checkbox, "v_model"))
def _toggle_slider(self, *args) -> None:
"""Toggle the modification of the slider."""
self.w_slider.disabled = not self.w_checkbox.v_model
return
[docs]
class VectorRow(sw.Html):
w_checkbox: Optional[sw.SimpleCheckbox] = None
"the ckeckbox to hide/show the layer"
[docs]
def __init__(self, layer: TileLayer) -> None:
"""Html row element to describe a vector layer.
This Html element include all the controls to manipulate the layer displayed in the map
- a checkbox to show/hide
Vector are always placed on top of the map
Args:
layer: the vector layer associated to the row
"""
# create the checkbox, by default layer are visible
self.w_checkbox = sw.SimpleCheckbox(
v_model=True, small=True, label=layer.name, color=color.primary
)
kwargs = {"style": "width: 10%;", "tag": "td"}
checkbox_cell = sw.Html(children=[self.w_checkbox], **kwargs)
# create the label
kwargs = {"style_": "width: 40%;", "tag": "td"}
label_cell = sw.Html(children=[layer.name], **kwargs)
# create the slider
kwargs = {"style_": "width: 50%;", "tag": "td"}
empty_cell = sw.Html(children=[""], **kwargs)
# build a html tr from it
super().__init__(tag="tr", children=[label_cell, empty_cell, checkbox_cell])
# add js behavior
link((layer, "visible"), (self.w_checkbox, "v_model"))
[docs]
class LayersControl(MenuControl):
m: Optional[Map] = None
"the map controlled by the layercontrol"
group: Optional[sw.RadioGroup] = None
"As radio button cannot behave individually we add an extra GroupRadio to wrap the table"
[docs]
def __init__(self, m: Map, **kwargs) -> None:
"""Richer layerControl to add some controls over the lyers displayed on the map.
Each layer is associated to a line where the user can adapt the alpha channel or even hide it completely
Args:
m: the map to display the layers
kwargs: optional extra parameters for the ipyleaflet.WidgetControl
"""
# save the map
self.m = m
# create a loading to place it on top of the card. It will always be visible
# even when the card is scrolled
p_style = json.loads((ss.JSON_DIR / "progress_bar.json").read_text())
self.w_loading = sw.ProgressLinear(
indeterminate=False,
background_color=color.menu,
color=p_style["color"][v.theme.dark],
)
self.tile = sw.Tile("nested", "")
# set the kwargs parameters
kwargs.setdefault("position", "topright")
super().__init__(
icon_content="fa-solid fa-layer-group",
card_content=self.tile,
m=m,
**kwargs
)
# customize the menu to make it look more like a layercontrol
self.menu.open_on_hover = True
self.menu.close_delay = 200
# set the height according to the content
self.set_size(min_height=None, max_height=None)
# update the table at instance creation
self.update_table({})
# add js behavior
self.m.observe(self.update_table, "layers")
[docs]
def update_table(self, change: dict) -> None:
"""Update the table content."""
# create the vector line
vectors = [lyr for lyr in reversed(self.m.layers) if isinstance(lyr, GeoJSON)]
vector_rows = []
if len(vectors) > 0:
head = [HeaderRow(ms.layer_control.vector.header)]
rows = [VectorRow(lyr) for lyr in vectors]
vector_rows = head + rows
# create a table of layerLine
layers = [
lyr
for lyr in reversed(self.m.layers)
if lyr.base is False and isinstance(lyr, TileLayer)
]
layer_rows = []
if len(layers) > 0:
head = [HeaderRow(ms.layer_control.layer.header)]
rows = [LayerRow(lyr) for lyr in layers]
layer_rows = head + rows
# create another table of basemapLine it should always be a basemap
# the error raised if you delete the last one is a feature
bases = [lyr for lyr in self.m.layers if lyr.base is True]
base_rows = []
current = next(
(lyr for lyr in bases if lyr.visible is True), SimpleNamespace(name=None)
)
if len(bases) > 0:
head = [HeaderRow(ms.layer_control.basemap.header)]
empy_cell = sw.Html(tag="td", children=[" "], attributes={"colspan": 3})
empty_row = sw.Html(tag="tr", class_="v-no-hever", children=[empy_cell])
rows = [BaseRow(lyr) for lyr in bases] + [empty_row]
base_rows = head + rows
# create a table from these rows and wrap it in the radioGroup
tbody = sw.Html(tag="tbody", children=vector_rows + layer_rows + base_rows)
table = sw.SimpleTable(children=[tbody], dense=True, class_="v-no-border")
self.group = sw.RadioGroup(v_model=current.name, children=[table])
# set the table as children of the widget
self.tile.children = [self.group]
return