From 30acac42b4bbdb910b9cd1e0b7fce937cee0d36b Mon Sep 17 00:00:00 2001 From: Qiusheng Wu Date: Sat, 20 Apr 2024 21:22:49 -0400 Subject: [PATCH 1/8] Add layer loading spinner and remove layer button --- geemap/map_widgets.py | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/geemap/map_widgets.py b/geemap/map_widgets.py index 30f283b11f..2d0dc55df6 100644 --- a/geemap/map_widgets.py +++ b/geemap/map_widgets.py @@ -765,7 +765,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, @@ -809,7 +809,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" @@ -822,9 +822,43 @@ def _render_layer_row(self, layer): ) settings_button.on_click(self._on_layer_settings_click) + spinner = ipywidgets.Button( + 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._host_map.remove_layer(layer) + self.refresh_layers() + + 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 _compute_layer_opacity(self, layer): From a2846d92ce1ac325bf48d9cff91042e0f83d812e Mon Sep 17 00:00:00 2001 From: Qiusheng Wu Date: Sat, 20 Apr 2024 21:51:31 -0400 Subject: [PATCH 2/8] Add observe method to FakeEeTileLayer --- tests/fake_map.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/fake_map.py b/tests/fake_map.py index 5a30da3941..3e1f149934 100644 --- a/tests/fake_map.py +++ b/tests/fake_map.py @@ -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): From adedbf9777e91b79908667e22c7b4a00127e5f16 Mon Sep 17 00:00:00 2001 From: Qiusheng Wu Date: Sat, 20 Apr 2024 21:55:26 -0400 Subject: [PATCH 3/8] Add observe method to FakeGeoJSONLayer --- tests/fake_map.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/fake_map.py b/tests/fake_map.py index 3e1f149934..78a9d49552 100644 --- a/tests/fake_map.py +++ b/tests/fake_map.py @@ -131,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 From 76054459e07913442698de6415ef3aa7d2bd8aa9 Mon Sep 17 00:00:00 2001 From: Qiusheng Wu Date: Fri, 10 May 2024 00:24:34 -0400 Subject: [PATCH 4/8] Add remove layer confirmation --- geemap/map_widgets.py | 55 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/geemap/map_widgets.py b/geemap/map_widgets.py index 2d0dc55df6..50fe6e2877 100644 --- a/geemap/map_widgets.py +++ b/geemap/map_widgets.py @@ -6,6 +6,7 @@ from IPython.core.display import HTML, display import ee +import ipyleaflet import ipytree import ipywidgets @@ -845,8 +846,7 @@ def loading_change(change): ) def remove_layer_click(_): - self._host_map.remove_layer(layer) - self.refresh_layers() + self._on_layer_remove_click(layer) remove_layer_btn.on_click(remove_layer_click) @@ -861,6 +861,57 @@ def remove_layer_click(_): 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) From 869d96c3d911675df7a18a079440f7282dab69b4 Mon Sep 17 00:00:00 2001 From: Qiusheng Wu Date: Tue, 14 May 2024 22:04:37 -0500 Subject: [PATCH 5/8] Add spinner event for removing layer --- geemap/map_widgets.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/geemap/map_widgets.py b/geemap/map_widgets.py index 50fe6e2877..cac366fcdb 100644 --- a/geemap/map_widgets.py +++ b/geemap/map_widgets.py @@ -9,6 +9,7 @@ import ipyleaflet import ipytree import ipywidgets +import ipyevents from . import common @@ -835,20 +836,29 @@ def loading_change(change): spinner.icon = "spinner spin lg" else: spinner.tooltip = "Loaded" - spinner.icon = "check" + spinner.icon = "times" layer.observe(loading_change, "loading") - remove_layer_btn = ipywidgets.Button( - icon="times", - layout=ipywidgets.Layout(width="25px", height="25px", padding="0px"), - tooltip="Remove layer", + spinner_event = ipyevents.Event( + source=spinner, watched_events=["mouseenter", "mouseleave"] ) + def handle_spinner_event(event): + if event["type"] == "mouseenter": + spinner.icon = "times" + elif event["type"] == "mouseleave": + if layer.loading: + spinner.icon = "spinner spin lg" + else: + spinner.icon = "times" + + spinner_event.on_dom_event(handle_spinner_event) + def remove_layer_click(_): self._on_layer_remove_click(layer) - remove_layer_btn.on_click(remove_layer_click) + spinner.on_click(remove_layer_click) return ipywidgets.HBox( [ @@ -856,7 +866,6 @@ def remove_layer_click(_): opacity_slider, settings_button, spinner, - remove_layer_btn, ], layout=ipywidgets.Layout(padding="0px 4px 0px 4px"), ) @@ -878,7 +887,8 @@ def _on_layer_remove_click(self, layer): button_style="primary", ) confirm_widget = ipywidgets.VBox( - [label, ipywidgets.HBox([yes_button, no_button])] + [label, ipywidgets.HBox([yes_button, no_button])], + layout=ipywidgets.Layout(width="284px"), ) confirm_control = ipyleaflet.WidgetControl( From f66d26507bcf3abf3cd42245568aab2b04500146 Mon Sep 17 00:00:00 2001 From: Qiusheng Wu Date: Tue, 14 May 2024 22:56:23 -0500 Subject: [PATCH 6/8] Add support for removing non-ee layers --- geemap/map_widgets.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/geemap/map_widgets.py b/geemap/map_widgets.py index cac366fcdb..2cf88fa8b8 100644 --- a/geemap/map_widgets.py +++ b/geemap/map_widgets.py @@ -825,7 +825,7 @@ def _render_layer_row(self, layer): settings_button.on_click(self._on_layer_settings_click) spinner = ipywidgets.Button( - icon="check", + icon="times", layout=ipywidgets.Layout(width="25px", height="25px", padding="0px"), tooltip="Loaded", ) @@ -872,7 +872,10 @@ def remove_layer_click(_): def _on_layer_remove_click(self, layer): - layer_dict = self._host_map.ee_layers[layer.name] + if layer.name in self._host_map.ee_layers: + layer_dict = self._host_map.ee_layers[layer.name] + else: + layer_dict = {layer.name: {"layer": layer}} if "remove_control" not in layer_dict: label = ipywidgets.Label( f"Remove {layer.name} layer?", @@ -902,10 +905,9 @@ def on_yes_button_click(_): self._host_map.remove_control(confirm_control) if "confirm_widget" in layer_dict: layer_dict["confirm_widget"].close() + del layer_dict["confirm_widget"] 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"] + del layer_dict["remove_control"] yes_button.on_click(on_yes_button_click) From a59b512902acc1d271585cc69250da3dcca81609 Mon Sep 17 00:00:00 2001 From: Qiusheng Wu Date: Sat, 25 May 2024 00:29:16 -0400 Subject: [PATCH 7/8] Update layer remove widget --- geemap/map_widgets.py | 104 +++++++++++++++++++++--------------------- 1 file changed, 53 insertions(+), 51 deletions(-) diff --git a/geemap/map_widgets.py b/geemap/map_widgets.py index 2cf88fa8b8..43d34a4ae7 100644 --- a/geemap/map_widgets.py +++ b/geemap/map_widgets.py @@ -6,7 +6,6 @@ from IPython.core.display import HTML, display import ee -import ipyleaflet import ipytree import ipywidgets import ipyevents @@ -870,59 +869,62 @@ def remove_layer_click(_): layout=ipywidgets.Layout(padding="0px 4px 0px 4px"), ) + def _find_layer_row_index(self, layer): + for index, child in enumerate(self._toolbar_footer.children[1:]): + if child.children[0].description == layer.name: + return index + 1 + return -1 + + def _remove_confirm_widget(self): + for index, child in enumerate(self._toolbar_footer.children[1:]): + if child.children[0].value == "Remove layer?": + self._toolbar_footer.children = ( + self._toolbar_footer.children[: index + 1] + + self._toolbar_footer.children[index + 2 :] + ) + break + def _on_layer_remove_click(self, layer): - if layer.name in self._host_map.ee_layers: - layer_dict = self._host_map.ee_layers[layer.name] - else: - layer_dict = {layer.name: {"layer": layer}} - 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])], - layout=ipywidgets.Layout(width="284px"), - ) + self._remove_confirm_widget() - 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() - del layer_dict["confirm_widget"] - if "remove_control" in layer_dict: - del layer_dict["remove_control"] - - 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 + label = ipywidgets.Label( + f"Remove layer?", + layout=ipywidgets.Layout(padding="0px 4px 0px 4px"), + ) + yes_button = ipywidgets.Button( + description="Yes", + button_style="primary", + ) + yes_button.layout.width = "86px" + no_button = ipywidgets.Button( + description="No", + button_style="primary", + ) + no_button.layout.width = "86px" + + confirm_widget = ipywidgets.HBox( + [label, yes_button, no_button], layout=ipywidgets.Layout(width="284px") + ) + + layer_row_index = self._find_layer_row_index(layer) + + self._toolbar_footer.children = ( + list(self._toolbar_footer.children[: layer_row_index + 1]) + + [confirm_widget] + + list(self._toolbar_footer.children[layer_row_index + 1 :]) + ) + + def on_yes_button_click(_): + self._host_map.remove_layer(layer) + self._remove_confirm_widget() + + yes_button.on_click(on_yes_button_click) + + def on_no_button_click(_): + self._remove_confirm_widget() + + no_button.on_click(on_no_button_click) def _compute_layer_opacity(self, layer): if layer in self._host_map.geojson_layers: From c71d6463a155341a4663fc017d98bba50bbe684c Mon Sep 17 00:00:00 2001 From: Nathaniel Schmitz Date: Wed, 12 Jun 2024 16:18:20 +0000 Subject: [PATCH 8/8] Minor style tweaks. --- geemap/map_widgets.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/geemap/map_widgets.py b/geemap/map_widgets.py index 43d34a4ae7..8d6d6b01bd 100644 --- a/geemap/map_widgets.py +++ b/geemap/map_widgets.py @@ -6,9 +6,9 @@ from IPython.core.display import HTML, display import ee +import ipyevents import ipytree import ipywidgets -import ipyevents from . import common @@ -885,11 +885,10 @@ def _remove_confirm_widget(self): break def _on_layer_remove_click(self, layer): - self._remove_confirm_widget() label = ipywidgets.Label( - f"Remove layer?", + "Remove layer?", layout=ipywidgets.Layout(padding="0px 4px 0px 4px"), ) yes_button = ipywidgets.Button(