Skip to content

Commit

Permalink
Refactor layer manager into an ipywidgets subclass
Browse files Browse the repository at this point in the history
  • Loading branch information
naschmitz committed Aug 22, 2023
1 parent ff81509 commit 1408c02
Show file tree
Hide file tree
Showing 5 changed files with 488 additions and 262 deletions.
55 changes: 49 additions & 6 deletions geemap/geemap.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ def __init__(self, **kwargs):
zoom = 2

self.inspector_control = None
self.layer_manager_widget = None
self.layer_manager_control = None

# Set map width and height
if "height" not in kwargs:
Expand Down Expand Up @@ -1202,7 +1204,29 @@ def create_vis_widget(self, layer_dict):
Returns:
object: An ipywidget.
"""
has_vis_widget = hasattr(self, "_vis_widget") and self._vis_widget is not None
if layer_dict:
if has_vis_widget:
self._vis_widget = None
self._vis_widget = self._render_vis_widget(layer_dict)
if hasattr(self, "_vis_control") and self._vis_control in self.controls:
self.remove_control(self._vis_control)
self._vis_control = None
vis_control = ipyleaflet.WidgetControl(
widget=self._vis_widget, position="topright"
)
self.add((vis_control))
self._vis_control = vis_control
else:
if has_vis_widget:
self._vis_widget = None
if hasattr(self, "_vis_control") and self._vis_control is not None:
if self._vis_control in self.controls:
self.remove_control(self._vis_control)
self._vis_control = None

def _render_vis_widget(self, layer_dict):
"""Returns the vis widget."""
import matplotlib as mpl
import matplotlib.pyplot as plt

Expand Down Expand Up @@ -2582,15 +2606,34 @@ def add_layer_manager(
opened (bool, optional): Whether the control is opened. Defaults to True.
show_close_button (bool, optional): Whether to show the close button. Defaults to True.
"""
from .toolbar import layer_manager_gui
if self.layer_manager_control:
return

layer_manager_gui(self, position, opened, show_close_button=show_close_button)
def _on_close():
self.toolbar_reset()
if self.layer_manager_control:
if self.layer_manager_control in self.controls:
self.remove_control(self.layer_manager_control)
self.layer_manager_control.close()
self.layer_manager_control = None

def _on_open_vis(layer_name):
self.create_vis_widget(self.ee_layer_dict.get(layer_name, None))

self.layer_manager_widget = map_widgets.LayerManager(self)
self.layer_manager_widget.collapsed = not opened
self.layer_manager_widget.close_button_hidden = not show_close_button
self.layer_manager_widget.on_close = _on_close
self.layer_manager_widget.on_open_vis = _on_open_vis
self.layer_manager_control = ipyleaflet.WidgetControl(
widget=self.layer_manager_widget, position=position
)
self.add(self.layer_manager_control)

def update_layer_manager(self):
"""Update the Layer Manager."""
from .toolbar import layer_manager_gui

self.layer_manager_widget.children = layer_manager_gui(self, return_widget=True)
if self.layer_manager_widget:
self.layer_manager_widget.refresh_layers()

def add_draw_control(self, position="topleft"):
"""Add a draw control to the map
Expand Down Expand Up @@ -3358,7 +3401,7 @@ def close_btn_click(change):

if right_label is not None:
self.remove_control(right_control)

self.dragging = True

close_button.observe(close_btn_click, "value")
Expand Down
173 changes: 173 additions & 0 deletions geemap/map_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -606,3 +606,176 @@ def _handle_geometry_deleted(self, geo_json):
del self.properties[i]
self._redraw_layer()
self._geometry_delete_dispatcher(self, geometry=geometry)


class LayerManager(ipywidgets.VBox):
def __init__(self, host_map):
"""Initializes a layer manager widget.
Args:
host_map (geemap.Map): The geemap.Map object.
"""
self._host_map = host_map
if not host_map:
raise ValueError("Must pass a valid map when creating a layer manager.")

self._collapse_button = ipywidgets.ToggleButton(
value=False,
tooltip="Layer Manager",
icon="server",
layout=ipywidgets.Layout(
width="28px", height="28px", padding="0px 0px 0px 4px"
),
)
self._close_button = ipywidgets.Button(
tooltip="Close the tool",
icon="times",
button_style="primary",
layout=ipywidgets.Layout(width="28px", height="28px", padding="0px"),
)

self._toolbar_header = ipywidgets.HBox(
children=[self._close_button, self._collapse_button]
)
self._toolbar_footer = ipywidgets.VBox(children=[])

self._collapse_button.observe(self._on_collapse_click, "value")
self._close_button.on_click(self._on_close_click)

self.on_close = None
self.on_open_vis = None

self.collapsed = False
self.header_hidden = False
self.close_button_hidden = False

super().__init__([self._toolbar_header, self._toolbar_footer])

@property
def collapsed(self):
return not self._collapse_button.value

@collapsed.setter
def collapsed(self, value):
self._collapse_button.value = not value

@property
def header_hidden(self):
return self._toolbar_header.layout.display == "none"

@header_hidden.setter
def header_hidden(self, value):
self._toolbar_header.layout.display = "none" if value else "block"

@property
def close_button_hidden(self):
return self._close_button.style.display == "none"

@close_button_hidden.setter
def close_button_hidden(self, value):
self._close_button.style.display = "none" if value else "inline-block"

def refresh_layers(self):
"""Recreates all the layer widgets."""
toggle_all_layout = ipywidgets.Layout(
height="18px", width="30ex", padding="0px 8px 25px 8px"
)
toggle_all_checkbox = ipywidgets.Checkbox(
value=False,
description="All layers on/off",
indent=False,
layout=toggle_all_layout,
)
toggle_all_checkbox.observe(self._on_all_layers_visibility_toggled, "value")

layer_rows = [toggle_all_checkbox]
for layer in self._host_map.layers[1:]:
layer_rows.append(self._render_layer_row(layer))
self._toolbar_footer.children = layer_rows

def _on_close_click(self, _):
if self.on_close:
self.on_close()

def _on_collapse_click(self, change):
if change["new"]:
self.refresh_layers()
self.children = [self._toolbar_header, self._toolbar_footer]
else:
self.children = [self._collapse_button]

def _render_layer_row(self, layer):
visibility_checkbox = ipywidgets.Checkbox(
value=self._compute_layer_visibility(layer),
description=layer.name,
indent=False,
layout=ipywidgets.Layout(height="18px", width="140px"),
)
visibility_checkbox.observe(
lambda change: self._on_layer_visibility_changed(change, layer), "value"
)

opacity_slider = ipywidgets.FloatSlider(
value=self._compute_layer_opacity(layer),
min=0,
max=1,
step=0.01,
readout=False,
layout=ipywidgets.Layout(width="80px"),
)
opacity_slider.observe(
lambda change: self._on_layer_opacity_changed(change, layer), "value"
)

settings_button = ipywidgets.Button(
icon="gear",
layout=ipywidgets.Layout(width="25px", height="25px", padding="0px"),
)
settings_button.on_click(self._on_layer_settings_click)

return ipywidgets.HBox(
[visibility_checkbox, settings_button, opacity_slider],
layout=ipywidgets.Layout(padding="0px 8px 0px 8px"),
)

def _compute_layer_opacity(self, layer):
if layer in self._host_map.geojson_layers:
opacity = layer.style.get("opacity", 1.0)
fill_opacity = layer.style.get("fillOpacity", 1.0)
return max(opacity, fill_opacity)
return layer.opacity if hasattr(layer, "opacity") else 1.0

def _compute_layer_visibility(self, layer):
return layer.visible if hasattr(layer, "visible") else True

def _on_layer_settings_click(self, button):
if self.on_open_vis:
self.on_open_vis(button.tooltip)

def _on_all_layers_visibility_toggled(self, change):
for layer in self._host_map.layers:
if hasattr(layer, "visible"):
layer.visible = change["new"]

def _on_layer_opacity_changed(self, change, layer):
if layer in self._host_map.geojson_layers:
# For non-TileLayer, use layer.style.opacity and layer.style.fillOpacity.
layer.style.update({"opacity": change["new"], "fillOpacity": change["new"]})
elif hasattr(layer, "opacity"):
layer.opacity = change["new"]

def _on_layer_visibility_changed(self, change, layer):
if hasattr(layer, "visible"):
layer.visible = change["new"]

layer_name = change["owner"].description
if layer_name not in self._host_map.ee_layer_names:
return

layer_dict = self._host_map.ee_layer_dict[layer_name]
for attachment_name in ["legend", "colorbar"]:
attachment = layer_dict.get(attachment_name, None)
attachment_on_map = attachment in self._host_map.controls
if change["new"] and not attachment_on_map:
self._host_map.add(attachment)
elif not change["new"] and attachment_on_map:
self._host_map.remove_control(attachment)
Loading

0 comments on commit 1408c02

Please sign in to comment.