Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add layer loading spinner and remove layer button #1983

Merged
merged 12 commits into from
Jun 12, 2024
93 changes: 89 additions & 4 deletions geemap/map_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from IPython.core.display import HTML, display

import ee
import ipyleaflet
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't import ipyleaflet in map_widgets. This file is supposed to be agnostic to map implementation. Two possible solutions are listed below.

  1. The preferred option is to avoid this problem entirely by showing the confirmation dialog above (or inside) the existing widget. It could look like another row that's added below the row scheduled for deletion or a dialog that appears over everything.

  2. Build this new "confirmation" widget as any of the other core widgets and have the widget control code live in core. The new widget needs some sort of "on_confirm_deletion" event that the code code must set. Example:

    def _add_basemap_selector(self, position: str, **kwargs) -> None:

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have implemented the preferred option 1:

another row that's added below the row scheduled for deletion

remove.layer.mp4

import ipytree
import ipywidgets

Expand Down Expand Up @@ -765,7 +766,7 @@ def close_button_hidden(self, value):
def refresh_layers(self):
"""Recreates all the layer widgets."""
toggle_all_layout = ipywidgets.Layout(
height="18px", width="30ex", padding="0px 8px 25px 8px"
height="18px", width="30ex", padding="0px 4px 25px 4px"
)
toggle_all_checkbox = ipywidgets.Checkbox(
value=False,
Expand Down Expand Up @@ -809,7 +810,7 @@ def _render_layer_row(self, layer):
max=1,
step=0.01,
readout=False,
layout=ipywidgets.Layout(width="80px"),
layout=ipywidgets.Layout(width="70px", padding="0px 3px 0px 0px"),
)
opacity_slider.observe(
lambda change: self._on_layer_opacity_changed(change, layer), "value"
Expand All @@ -822,11 +823,95 @@ def _render_layer_row(self, layer):
)
settings_button.on_click(self._on_layer_settings_click)

spinner = ipywidgets.Button(
giswqs marked this conversation as resolved.
Show resolved Hide resolved
icon="check",
layout=ipywidgets.Layout(width="25px", height="25px", padding="0px"),
tooltip="Loaded",
)

def loading_change(change):
if change["new"]:
spinner.tooltip = "Loading ..."
spinner.icon = "spinner spin lg"
else:
spinner.tooltip = "Loaded"
spinner.icon = "check"

layer.observe(loading_change, "loading")

remove_layer_btn = ipywidgets.Button(
icon="times",
layout=ipywidgets.Layout(width="25px", height="25px", padding="0px"),
tooltip="Remove layer",
)

def remove_layer_click(_):
self._on_layer_remove_click(layer)

remove_layer_btn.on_click(remove_layer_click)

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

def _on_layer_remove_click(self, layer):

layer_dict = self._host_map.ee_layers[layer.name]
if "remove_control" not in layer_dict:
label = ipywidgets.Label(
f"Remove {layer.name} layer?",
layout=ipywidgets.Layout(padding="0px 4px 0px 4px"),
)
yes_button = ipywidgets.Button(
description="Yes",
button_style="primary",
)
no_button = ipywidgets.Button(
description="No",
button_style="primary",
)
confirm_widget = ipywidgets.VBox(
[label, ipywidgets.HBox([yes_button, no_button])]
)

confirm_control = ipyleaflet.WidgetControl(
widget=confirm_widget, position="topright"
)
self._host_map.add(confirm_control)

def on_yes_button_click(_):
self._host_map.remove_layer(layer)
self.refresh_layers()
self._host_map.remove_control(confirm_control)
if "confirm_widget" in layer_dict:
layer_dict["confirm_widget"].close()
if "remove_control" in layer_dict:
self._host_map.remove_control(layer_dict["remove_control"])
del layer_dict["remove_control"]
del layer_dict["confirm_widget"]

yes_button.on_click(on_yes_button_click)

def on_no_button_click(_):
if "confirm_widget" in layer_dict:
layer_dict["confirm_widget"].close()
if "remove_control" in layer_dict:
self._host_map.remove_control(layer_dict["remove_control"])
del layer_dict["remove_control"]
del layer_dict["confirm_widget"]

no_button.on_click(on_no_button_click)

layer_dict["remove_control"] = confirm_control
layer_dict["confirm_widget"] = confirm_widget

def _compute_layer_opacity(self, layer):
if layer in self._host_map.geojson_layers:
opacity = layer.style.get("opacity", 1.0)
Expand Down
6 changes: 6 additions & 0 deletions tests/fake_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ def __init__(self, name="test-layer", visible=True, opacity=1.0):
self.visible = visible
self.opacity = opacity

def observe(self, func, names):
pass


class FakeTileLayer:
def __init__(self, name="test-layer", visible=True, opacity=1.0):
Expand All @@ -128,3 +131,6 @@ def __init__(self, name="test-layer", visible=True, style=None):
self.name = name
self.visible = visible
self.style = style or {}

def observe(self, func, names):
pass
Loading