From 1ac3fad56d5c9c358da40c4b0993194d471eb675 Mon Sep 17 00:00:00 2001 From: Sufyan Abbasi Date: Wed, 4 Oct 2023 22:15:52 +0000 Subject: [PATCH 1/7] Enable toggle on all relevant toolbar tools. - Note that closing the tool directly does not update the toolbar button yet. --- geemap/map_widgets.py | 19 +- geemap/toolbar.py | 423 +++++++++++++++++++++++------------------- 2 files changed, 247 insertions(+), 195 deletions(-) diff --git a/geemap/map_widgets.py b/geemap/map_widgets.py index 7d87265abf..7a9b76032b 100644 --- a/geemap/map_widgets.py +++ b/geemap/map_widgets.py @@ -460,6 +460,14 @@ def __init__( children=[self.toolbar_header, self.inspector_checks, self.tree_output] ) + def cleanup(self): + """Removes the widget from the map and performs cleanup.""" + if self._host_map: + self._host_map.default_style = {"cursor": "default"} + self._host_map.on_interaction(self._on_map_interaction, remove=True) + if self.on_close is not None: + self.on_close() + def _create_checkbox(self, title, checked): layout = ipywidgets.Layout(width="auto", padding="0px 6px 0px 0px") return ipywidgets.Checkbox( @@ -516,11 +524,7 @@ def _on_toolbar_btn_click(self, change): def _on_close_btn_click(self, change): if change["new"]: - if self._host_map: - self._host_map.default_style = {"cursor": "default"} - self._host_map.on_interaction(self._on_map_interaction, remove=True) - if self.on_close is not None: - self.on_close() + self.close() def _get_visible_map_layers(self): layers = {} @@ -835,10 +839,13 @@ def _on_dropdown_click(self, change): if self.on_basemap_changed and change["new"]: self.on_basemap_changed(self._dropdown.value) - def _on_close_click(self, _): + def cleanup(self): if self.on_close: self.on_close() + def _on_close_click(self, _): + self.cleanup() + class LayerEditor(ipywidgets.VBox): """Widget for displaying and editing layer visualization properties.""" diff --git a/geemap/toolbar.py b/geemap/toolbar.py index c599873daf..64b2c18cc0 100644 --- a/geemap/toolbar.py +++ b/geemap/toolbar.py @@ -15,9 +15,11 @@ import ipyevents import ipyleaflet import ipywidgets as widgets + +from ipywidgets.widgets import Widget from ipyfilechooser import FileChooser from IPython.core.display import display -from typing import Callable +from typing import Any, Callable, Optional from .common import * from .timelapse import * @@ -37,16 +39,19 @@ class Item: icon: The icon to use for the item, from https://fontawesome.com/icons. tooltip: The tooltip text to show a user on hover. callback: A callback function to execute when the item icon is clicked. - Its signature should be `callback(map, selected)`, where `map` is the - host map and `selected` is a boolean indicating if the user selected - or unselected the tool. + Its signature should be `callback(map, selected, item)`, where + `map` is the host map, `selected` is a boolean indicating if the + user selected or unselected the tool, and `item` is this object. reset: Whether to reset the selection after the callback has finished. + control: The control widget associated with this item. Used to + cleanup state when toggled off. """ icon: str tooltip: str - callback: Callable[[any, bool], None] + callback: Callable[[Any, bool, Any], None] reset: bool = True + control: Optional[Widget] = None ICON_WIDTH = "32px" ICON_HEIGHT = "32px" @@ -108,20 +113,21 @@ def __init__(self, host_map, main_tools, extra_tools=None): ), ) - def curry_callback(callback, should_reset_after, widget): + def curry_callback(callback, should_reset_after, widget, item): def returned_callback(change): if change["type"] != "change": return - # Unselect all other tool widgets. - self._reset_others(widget) - callback(self.host_map, change["new"]) + callback(self.host_map, change["new"], item) if should_reset_after: widget.value = False return returned_callback for id, widget in enumerate(self.all_widgets): - widget.observe(curry_callback(callbacks[id], resets[id], widget), "value") + widget.observe( + curry_callback(callbacks[id], resets[id], widget, all_tools[id]), + "value", + ) self.toolbar_button = widgets.ToggleButton( value=False, @@ -163,7 +169,7 @@ def _reset_others(self, current): if other is not current: other.value = False - def _toggle_callback(self, m, selected): + def _toggle_callback(self, m, selected, _): del m # unused if not selected: return @@ -379,31 +385,33 @@ def toolbar_btn_click(change): toolbar_button.observe(toolbar_btn_click, "value") - def close_btn_click(change): - if change["new"]: - toolbar_button.value = False - if m is not None: - if hasattr(m, "inspector_mode"): - delattr(m, "inspector_mode") - m.toolbar_reset() - if m.tool_control is not None and m.tool_control in m.controls: - m.remove_control(m.tool_control) - m.tool_control = None - m.default_style = {"cursor": "default"} + def cleanup(): + toolbar_button.value = False + if m is not None: + if hasattr(m, "inspector_mode"): + delattr(m, "inspector_mode") + if m.tool_control is not None and m.tool_control in m.controls: + m.remove_control(m.tool_control) + m.tool_control = None + m.default_style = {"cursor": "default"} - m.marker_cluster.markers = [] - m.pixel_values = [] - marker_cluster_layer = m.find_layer("Inspector Markers") - if marker_cluster_layer is not None: - m.remove_layer(marker_cluster_layer) + m.marker_cluster.markers = [] + m.pixel_values = [] + marker_cluster_layer = m.find_layer("Inspector Markers") + if marker_cluster_layer is not None: + m.remove_layer(marker_cluster_layer) - if hasattr(m, "pixel_values"): - delattr(m, "pixel_values") + if hasattr(m, "pixel_values"): + delattr(m, "pixel_values") - if hasattr(m, "marker_cluster"): - delattr(m, "marker_cluster") + if hasattr(m, "marker_cluster"): + delattr(m, "marker_cluster") - toolbar_widget.close() + toolbar_widget.close() + + def close_btn_click(change): + if change["new"]: + cleanup() close_button.observe(close_btn_click, "value") @@ -432,27 +440,7 @@ def button_clicked(change): if hasattr(m, "marker_cluster"): m.marker_cluster.markers = [] elif change["new"] == "Close": - if m is not None: - if hasattr(m, "inspector_mode"): - delattr(m, "inspector_mode") - m.toolbar_reset() - if m.tool_control is not None and m.tool_control in m.controls: - m.remove_control(m.tool_control) - m.tool_control = None - m.default_style = {"cursor": "default"} - m.marker_cluster.markers = [] - marker_cluster_layer = m.find_layer("Inspector Markers") - if marker_cluster_layer is not None: - m.remove_layer(marker_cluster_layer) - m.pixel_values = [] - - if hasattr(m, "pixel_values"): - delattr(m, "pixel_values") - - if hasattr(m, "marker_cluster"): - delattr(m, "marker_cluster") - - toolbar_widget.close() + cleanup() buttons.value = None @@ -587,6 +575,7 @@ def handle_interaction(**kwargs): toolbar_control = ipyleaflet.WidgetControl( widget=toolbar_widget, position="topright" ) + setattr(toolbar_control, "cleanup", cleanup) if toolbar_control not in m.controls: m.add_control(toolbar_control) @@ -602,9 +591,10 @@ def handle_interaction(**kwargs): return toolbar_widget -def _plotting_tool_callback(map, selected): +def _plotting_tool_callback(map, selected, item): if selected: ee_plot_gui(map) + item.control = map._plot_dropdown_control return # User has unselected tool. if not hasattr(map, "_plot_dropdown_widget"): @@ -790,8 +780,7 @@ def handle_draw(_, geometry): draw_control.on_geometry_create(handle_draw) - def close_click(change): - m.toolbar_reset() + def cleanup(): m._plot_checked = False if ( @@ -819,6 +808,11 @@ def close_click(change): else: m.remove_draw_control() + setattr(m._plot_dropdown_control, "cleanup", cleanup) + + def close_click(_): + cleanup() + close_btn.on_click(close_click) @@ -1674,6 +1668,17 @@ def file_type_changed(change): convert_hbox.children = [] http_widget.children = [filepath] + def cleanup(): + if ( + hasattr(m, "_tool_output_ctrl") + and m._tool_output_ctrl is not None + and m._tool_output_ctrl in m.controls + ): + m.remove_control(m._tool_output_ctrl) + m._tool_output_ctrl = None + + setattr(tool_output_ctrl, "cleanup", cleanup) + def ok_cancel_clicked(change): if change["new"] == "Apply": m.default_style = {"cursor": "wait"} @@ -1746,7 +1751,6 @@ def ok_cancel_clicked(change): else: print("Please select a file to open.") - m.toolbar_reset() m.default_style = {"cursor": "default"} elif change["new"] == "Reset": @@ -1754,17 +1758,8 @@ def ok_cancel_clicked(change): tool_output.outputs = () with tool_output: display(main_widget) - m.toolbar_reset() elif change["new"] == "Close": - if ( - hasattr(m, "_tool_output_ctrl") - and m._tool_output_ctrl is not None - and m._tool_output_ctrl in m.controls - ): - m.remove_control(m._tool_output_ctrl) - m._tool_output_ctrl = None - m.toolbar_reset() - + cleanup() ok_cancel.value = None file_type.observe(file_type_changed, names="value") @@ -1775,9 +1770,10 @@ def ok_cancel_clicked(change): m._tool_output_ctrl = tool_output_ctrl -def _convert_js_tool_callback(map, selected): +def _convert_js_tool_callback(map, selected, item): if selected: convert_js2py(map) + item.control = map._convert_ctrl return # User has unselected tool. if map._convert_ctrl is not None and map._convert_ctrl in map.controls: @@ -1828,7 +1824,6 @@ def button_clicked(change): elif change["new"] == "Clear": text_widget.value = "" elif change["new"] == "Close": - m.toolbar_reset() if m._convert_ctrl is not None and m._convert_ctrl in m.controls: m.remove_control(m._convert_ctrl) full_widget.close() @@ -1842,12 +1837,6 @@ def button_clicked(change): m._convert_ctrl = widget_control -def _collect_samples_tool_callback(map, selected): - if selected: - map.training_ctrl = None - collect_samples(map) - - def collect_samples(m): full_widget = widgets.VBox() layout = widgets.Layout(width="100px") @@ -1886,6 +1875,17 @@ def collect_samples(m): old_draw_control = m.get_draw_control() + def cleanup(): + if m.training_ctrl is not None and m.training_ctrl in m.controls: + m.remove_control(m.training_ctrl) + full_widget.close() + # Restore default draw control. + if old_draw_control: + old_draw_control.open() + m.substitute(m.get_draw_control(), old_draw_control) + else: + m.remove_draw_control() + def button_clicked(change): if change["new"] == "Apply": if len(color.value) != 7: @@ -1933,16 +1933,7 @@ def set_properties(_, geometry): value_text2.value = "" color.value = "#3388ff" elif change["new"] == "Close": - m.toolbar_reset() - if m.training_ctrl is not None and m.training_ctrl in m.controls: - m.remove_control(m.training_ctrl) - full_widget.close() - # Restore default draw control. - if old_draw_control: - old_draw_control.open() - m.substitute(m.get_draw_control(), old_draw_control) - else: - m.remove_draw_control() + cleanup() buttons.value = None buttons.observe(button_clicked, "value") @@ -1955,6 +1946,7 @@ def set_properties(_, geometry): ] widget_control = ipyleaflet.WidgetControl(widget=full_widget, position="topright") + setattr(widget_control, "cleanup", cleanup) m.add_control(widget_control) m.training_ctrl = widget_control @@ -2563,16 +2555,6 @@ def reset_btn_click(change): layout=widgets.Layout(padding="0px", width=button_width), ) - def close_click(change): - if m is not None: - m.toolbar_reset() - if m.tool_control is not None and m.tool_control in m.controls: - m.remove_control(m.tool_control) - m.tool_control = None - toolbar_widget.close() - - close_btn.on_click(close_click) - output = widgets.Output(layout=widgets.Layout(width=widget_width, padding=padding)) toolbar_widget = widgets.VBox() @@ -2620,16 +2602,18 @@ def toolbar_btn_click(change): toolbar_button.observe(toolbar_btn_click, "value") + def cleanup(): + if m is not None: + if m.tool_control is not None and m.tool_control in m.controls: + m.remove_control(m.tool_control) + m.tool_control = None + toolbar_widget.close() + def close_btn_click(change): if change["new"]: - toolbar_button.value = False - if m is not None: - if m.tool_control is not None and m.tool_control in m.controls: - m.remove_control(m.tool_control) - m.tool_control = None - m.toolbar_reset() - toolbar_widget.close() + cleanup() + close_btn.on_click(lambda _: cleanup()) close_button.observe(close_btn_click, "value") toolbar_button.value = True @@ -2637,7 +2621,7 @@ def close_btn_click(change): toolbar_control = ipyleaflet.WidgetControl( widget=toolbar_widget, position="topright" ) - + setattr(toolbar_control, "cleanup", cleanup) if toolbar_control not in m.controls: m.add_control(toolbar_control) m.tool_control = toolbar_control @@ -2754,7 +2738,7 @@ def time_slider(m=None): ) region = widgets.Dropdown( - options=["User-drawn ROI"] + m.ee_vector_layers.keys(), + options=["User-drawn ROI"] + list(m.ee_vector_layers.keys()), value="User-drawn ROI", description="Region:", layout=widgets.Layout(width=widget_width, padding=padding), @@ -3297,19 +3281,23 @@ def reset_btn_click(change): layout=widgets.Layout(padding="0px", width=button_width), ) - def close_click(change): + def cleanup(): + toolbar_button.value = False if m is not None: - m.toolbar_reset() if m.tool_control is not None and m.tool_control in m.controls: m.remove_control(m.tool_control) m.tool_control = None - - if hasattr(m, "_colorbar_ctrl") and (m._colorbar_ctrl is not None): - m.remove_control(m._colorbar_ctrl) - m._colorbar_ctrl = None toolbar_widget.close() - close_btn.on_click(close_click) + if hasattr(m, "_colorbar_ctrl") and (m._colorbar_ctrl is not None): + m.remove_control(m._colorbar_ctrl) + m._colorbar_ctrl = None + + def close_btn_click(change): + if change["new"]: + cleanup() + + close_button.observe(close_btn_click, "value") def collection_changed(change): if change["new"]: @@ -3509,27 +3497,12 @@ def toolbar_btn_click(change): toolbar_button.observe(toolbar_btn_click, "value") - def close_btn_click(change): - if change["new"]: - toolbar_button.value = False - if m is not None: - if m.tool_control is not None and m.tool_control in m.controls: - m.remove_control(m.tool_control) - m.tool_control = None - m.toolbar_reset() - toolbar_widget.close() - - if hasattr(m, "_colorbar_ctrl") and (m._colorbar_ctrl is not None): - m.remove_control(m._colorbar_ctrl) - m._colorbar_ctrl = None - - close_button.observe(close_btn_click, "value") - toolbar_button.value = True if m is not None: toolbar_control = ipyleaflet.WidgetControl( widget=toolbar_widget, position="topright" ) + setattr(toolbar_control, "cleanup", cleanup) if toolbar_control not in m.controls: m.add_control(toolbar_control) @@ -3701,18 +3674,22 @@ def toolbar_btn_click(change): toolbar_button.observe(toolbar_btn_click, "value") + def cleanup(): + toolbar_button.value = False + if m is not None: + if m.tool_control is not None and m.tool_control in m.controls: + m.remove_control(m.tool_control) + m.tool_control = None + if m.transect_control is not None and m.transect_control in m.controls: + m.remove_control(m.transect_control) + m.transect_control = None + toolbar_widget.close() + + setattr(m.transect_control, "cleanup", cleanup) + def close_btn_click(change): if change["new"]: - toolbar_button.value = False - if m is not None: - m.toolbar_reset() - if m.tool_control is not None and m.tool_control in m.controls: - m.remove_control(m.tool_control) - m.tool_control = None - if m.transect_control is not None and m.transect_control in m.controls: - m.remove_control(m.transect_control) - m.transect_control = None - toolbar_widget.close() + cleanup() close_button.observe(close_btn_click, "value") @@ -3759,7 +3736,6 @@ def button_clicked(change): output.outputs = () elif change["new"] == "Close": if m is not None: - m.toolbar_reset() if m.tool_control is not None and m.tool_control in m.controls: m.remove_control(m.tool_control) m.tool_control = None @@ -3777,7 +3753,7 @@ def button_clicked(change): toolbar_control = ipyleaflet.WidgetControl( widget=toolbar_widget, position="topright" ) - + setattr(toolbar_control, "cleanup", cleanup) if toolbar_control not in m.controls: m.add_control(toolbar_control) m.tool_control = toolbar_control @@ -3933,9 +3909,11 @@ def dataset_changed(change): if m is not None: if "Las Vegas" not in m.ee_vector_layers.keys(): - region.options = ["User-drawn ROI", "Las Vegas"] + m.ee_vector_layers.keys() + region.options = ["User-drawn ROI", "Las Vegas"] + list( + m.ee_vector_layers.keys() + ) else: - region.options = ["User-drawn ROI"] + m.ee_vector_layers.keys() + region.options = ["User-drawn ROI"] + list(m.ee_vector_layers.keys()) plot_close_btn = widgets.Button( tooltip="Close the plot", @@ -4115,18 +4093,22 @@ def toolbar_btn_click(change): toolbar_button.observe(toolbar_btn_click, "value") + def cleanup(): + toolbar_button.value = False + if m is not None: + if m.tool_control is not None and m.tool_control in m.controls: + m.remove_control(m.tool_control) + m.tool_control = None + if m.sankee_control is not None and m.sankee_control in m.controls: + m.remove_control(m.sankee_control) + m.sankee_control = None + toolbar_widget.close() + + setattr(m.sankee_control, "cleanup", cleanup) + def close_btn_click(change): if change["new"]: - toolbar_button.value = False - if m is not None: - m.toolbar_reset() - if m.tool_control is not None and m.tool_control in m.controls: - m.remove_control(m.tool_control) - m.tool_control = None - if m.sankee_control is not None and m.sankee_control in m.controls: - m.remove_control(m.sankee_control) - m.sankee_control = None - toolbar_widget.close() + cleanup() close_button.observe(close_btn_click, "value") @@ -4222,7 +4204,6 @@ def button_clicked(change): elif change["new"] == "Close": if m is not None: - m.toolbar_reset() if m.tool_control is not None and m.tool_control in m.controls: m.remove_control(m.tool_control) m.tool_control = None @@ -4248,7 +4229,7 @@ def button_clicked(change): return toolbar_widget -def _split_basemaps_tool_callback(map, selected): +def _split_basemaps_tool_callback(map, selected, _): if selected: try: split_basemaps(map, layers_dict=planet_tiles()) @@ -4348,53 +4329,115 @@ def right_change(change): right_dropdown.observe(right_change, "value") -def _whitebox_tool_callback(map, selected): +def _open_help_page_callback(map, selected, _): + del map if selected: - import whiteboxgui.whiteboxgui as wbt - - tools_dict = wbt.get_wbt_dict() - wbt_toolbox = wbt.build_toolbox( - tools_dict, - max_width="800px", - max_height="500px", - sandbox_path=map.sandbox_path, - ) - wbt_control = ipyleaflet.WidgetControl( - widget=wbt_toolbox, position="bottomright" - ) - map.whitebox = wbt_control - map.add(wbt_control) - return - # User has unselected tool. - if map.whitebox is not None and map.whitebox in map.controls: - map.remove_control(map.whitebox) + import webbrowser + + webbrowser.open_new_tab("https://geemap.org") -def _gee_toolbox_tool_callback(map, selected): - if not selected: - return +def _cleanup_toolbar_item(func): + def wrapper(map, selected, item): + if selected: + func(map, selected, item) + elif item.control and hasattr(item.control, "cleanup"): + item.control.cleanup() + + return wrapper + + +@_cleanup_toolbar_item +def _inspector_tool_callback(map, _, item): + map.add_inspector() + item.control = map._inspector + + +@_cleanup_toolbar_item +def _timelapse_tool_callback(map, _, item): + timelapse_gui(map) + item.control = map.tool_control + + +@_cleanup_toolbar_item +def _basemap_tool_callback(map, _, item): + map.add_basemap_widget() + item.control = map._basemap_selector + + +@_cleanup_toolbar_item +def _open_data_tool_callback(map, _, item): + open_data_widget(map) + item.control = map._tool_output_ctrl + + +@_cleanup_toolbar_item +def _gee_toolbox_tool_callback(map, _, item): tools_dict = get_tools_dict() gee_toolbox = build_toolbox(tools_dict, max_width="800px", max_height="500px") geetoolbox_control = ipyleaflet.WidgetControl( widget=gee_toolbox, position="bottomright" ) + setattr( + geetoolbox_control, "cleanup", lambda: map.remove_control(geetoolbox_control) + ) map.geetoolbox = geetoolbox_control + item.control = geetoolbox_control map.add(geetoolbox_control) +@_cleanup_toolbar_item +def _time_slider_tool_callback(map, _, item): + time_slider(map) + item.control = map.tool_control -def _open_help_page_callback(map, selected): - del map - if selected: - import webbrowser +@_cleanup_toolbar_item +def _whitebox_tool_callback(map, _, item): + import whiteboxgui.whiteboxgui as wbt + + tools_dict = wbt.get_wbt_dict() + wbt_toolbox = wbt.build_toolbox( + tools_dict, + max_width="800px", + max_height="500px", + sandbox_path=map.sandbox_path, + ) + wbt_control = ipyleaflet.WidgetControl(widget=wbt_toolbox, position="bottomright") + setattr(wbt_control, "cleanup", lambda: map.remove_control(wbt_control)) + map.whitebox = wbt_control + item.control = wbt_control + map.add(wbt_control) + + +@_cleanup_toolbar_item +def _collect_samples_tool_callback(map, _, item): + collect_samples(map) + item.control = map.training_ctrl + + +@_cleanup_toolbar_item +def _plot_transect_tool_callback(map, _, item): + plot_transect(map) + item.control = map.transect_control - webbrowser.open_new_tab("https://geemap.org") + +@_cleanup_toolbar_item +def _sankee_tool_callback(map, _, item): + sankee_gui(map) + item.control = map.sankee_control + + +@_cleanup_toolbar_item +def _cog_stac_inspector_callback(map, _, item): + inspector_gui(map) + item.control = map.tool_control main_tools = [ Toolbar.Item( icon="info", tooltip="Inspector", - callback=lambda m, selected: m.add_inspector() if selected else None, + callback=_inspector_tool_callback, + reset=False, ), Toolbar.Item( icon="bar-chart", @@ -4405,12 +4448,13 @@ def _open_help_page_callback(map, selected): Toolbar.Item( icon="globe", tooltip="Create timelapse", - callback=lambda m, selected: timelapse_gui(m) if selected else None, + callback=_timelapse_tool_callback, + reset=False, ), Toolbar.Item( icon="map", tooltip="Change basemap", - callback=lambda m, selected: m.add_basemap_widget() if selected else None, + callback=_basemap_tool_callback, reset=False, ), Toolbar.Item( @@ -4425,12 +4469,12 @@ def _open_help_page_callback(map, selected): Toolbar.Item( icon="eraser", tooltip="Remove all drawn features", - callback=lambda m, selected: m.remove_drawn_features() if selected else None, + callback=lambda m, selected, _: m.remove_drawn_features() if selected else None, ), Toolbar.Item( icon="folder-open", tooltip="Open local vector/raster data", - callback=lambda m, selected: open_data_widget(m) if selected else None, + callback=_open_data_tool_callback, reset=False, ), Toolbar.Item( @@ -4448,7 +4492,8 @@ def _open_help_page_callback(map, selected): Toolbar.Item( icon="fast-forward", tooltip="Activate timeslider", - callback=lambda m, selected: time_slider(m) if selected else None, + callback=_time_slider_tool_callback, + reset=False, ), Toolbar.Item( icon="hand-o-up", @@ -4459,13 +4504,13 @@ def _open_help_page_callback(map, selected): Toolbar.Item( icon="line-chart", tooltip="Creating and plotting transects", - callback=lambda m, selected: plot_transect(m) if selected else None, + callback=_plot_transect_tool_callback, reset=False, ), Toolbar.Item( icon="random", tooltip="Sankey plots", - callback=lambda m, selected: sankee_gui(m) if selected else None, + callback=_sankee_tool_callback, reset=False, ), Toolbar.Item( @@ -4476,7 +4521,7 @@ def _open_help_page_callback(map, selected): Toolbar.Item( icon="info-circle", tooltip="Get COG/STAC pixel value", - callback=lambda m, selected: inspector_gui(m) if selected else None, + callback=_cog_stac_inspector_callback, reset=False, ), Toolbar.Item( From a085486c00c1f335fa336a98ed5a3a0040361b59 Mon Sep 17 00:00:00 2001 From: Sufyan Abbasi Date: Thu, 5 Oct 2023 01:13:51 +0000 Subject: [PATCH 2/7] Ensure that only one tool is active at a time --- geemap/core.py | 22 +++++++++++++++++----- geemap/toolbar.py | 5 +++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/geemap/core.py b/geemap/core.py index ac563ff5ef..8495f1ca9e 100644 --- a/geemap/core.py +++ b/geemap/core.py @@ -768,25 +768,37 @@ def add_layer( addLayer = add_layer - def _open_help_page(self, host_map: MapInterface, selected: bool) -> None: + def _open_help_page( + self, host_map: MapInterface, selected: bool, item: toolbar.Toolbar.Item + ) -> None: del host_map # Unused. + del item # Unused. if selected: common.open_url("https://geemap.org") def _toolbar_main_tools(self) -> List[toolbar.Toolbar.Item]: + @toolbar._cleanup_toolbar_item + def inspector_tool_callback(map, _, item): + map.add("inspector") + item.control = map._inspector + + @toolbar._cleanup_toolbar_item + def basemap_tool_callback(map, _, item): + map.add("basemap_selector") + item.control = map._basemap_selector + return [ toolbar.Toolbar.Item( icon="map", tooltip="Basemap selector", - callback=lambda m, selected: m.add("basemap_selector") - if selected - else None, + callback=basemap_tool_callback, reset=False, ), toolbar.Toolbar.Item( icon="info", tooltip="Inspector", - callback=lambda m, selected: m.add("inspector") if selected else None, + callback=inspector_tool_callback, + reset=False ), toolbar.Toolbar.Item( icon="question", tooltip="Get help", callback=self._open_help_page diff --git a/geemap/toolbar.py b/geemap/toolbar.py index 70f5b647f8..dd17945a7b 100644 --- a/geemap/toolbar.py +++ b/geemap/toolbar.py @@ -117,6 +117,9 @@ def curry_callback(callback, should_reset_after, widget, item): def returned_callback(change): if change["type"] != "change": return + if change["new"]: + # Unselect all other tool widgets if one is enabled. + self._reset_others(widget) callback(self.host_map, change["new"], item) if should_reset_after: widget.value = False @@ -4381,11 +4384,13 @@ def _gee_toolbox_tool_callback(map, _, item): item.control = geetoolbox_control map.add(geetoolbox_control) + @_cleanup_toolbar_item def _time_slider_tool_callback(map, _, item): time_slider(map) item.control = map.tool_control + @_cleanup_toolbar_item def _whitebox_tool_callback(map, _, item): import whiteboxgui.whiteboxgui as wbt From 8f927be8b8914bdeded0e831f9657bb677f6198e Mon Sep 17 00:00:00 2001 From: Sufyan Abbasi Date: Fri, 6 Oct 2023 02:57:17 +0000 Subject: [PATCH 3/7] Make sure that buttons untoggle when widget is closed --- geemap/core.py | 4 +- geemap/map_widgets.py | 4 +- geemap/toolbar.py | 255 +++++++++++++++++++++--------------------- 3 files changed, 134 insertions(+), 129 deletions(-) diff --git a/geemap/core.py b/geemap/core.py index 8495f1ca9e..9d52f9320d 100644 --- a/geemap/core.py +++ b/geemap/core.py @@ -780,12 +780,12 @@ def _toolbar_main_tools(self) -> List[toolbar.Toolbar.Item]: @toolbar._cleanup_toolbar_item def inspector_tool_callback(map, _, item): map.add("inspector") - item.control = map._inspector + return map._inspector @toolbar._cleanup_toolbar_item def basemap_tool_callback(map, _, item): map.add("basemap_selector") - item.control = map._basemap_selector + return map._basemap_selector return [ toolbar.Toolbar.Item( diff --git a/geemap/map_widgets.py b/geemap/map_widgets.py index 09789987d5..3ce9cbde8c 100644 --- a/geemap/map_widgets.py +++ b/geemap/map_widgets.py @@ -585,7 +585,7 @@ def _on_toolbar_btn_click(self, change): def _on_close_btn_click(self, change): if change["new"]: - self.close() + self.cleanup() def _get_visible_map_layers(self): layers = {} @@ -902,6 +902,8 @@ def _on_dropdown_click(self, change): def cleanup(self): if self.on_close: self.on_close() + if hasattr(self, 'toggle_off'): + self.toggle_off() def _on_close_click(self, _): self.cleanup() diff --git a/geemap/toolbar.py b/geemap/toolbar.py index dd17945a7b..c6bd421a28 100644 --- a/geemap/toolbar.py +++ b/geemap/toolbar.py @@ -45,6 +45,7 @@ class Item: reset: Whether to reset the selection after the callback has finished. control: The control widget associated with this item. Used to cleanup state when toggled off. + toggle_button: The toggle button controlling the item. """ icon: str @@ -52,6 +53,11 @@ class Item: callback: Callable[[Any, bool, Any], None] reset: bool = True control: Optional[Widget] = None + toggle_button: Any = None + + def toggle_off(self): + if self.toggle_button: + self.toggle_button.value = False ICON_WIDTH = "32px" ICON_HEIGHT = "32px" @@ -127,6 +133,7 @@ def returned_callback(change): return returned_callback for id, widget in enumerate(self.all_widgets): + all_tools[id].toggle_button = widget widget.observe( curry_callback(callbacks[id], resets[id], widget, all_tools[id]), "value", @@ -414,7 +421,7 @@ def cleanup(): def close_btn_click(change): if change["new"]: - cleanup() + m.tool_control.cleanup() close_button.observe(close_btn_click, "value") @@ -443,7 +450,7 @@ def button_clicked(change): if hasattr(m, "marker_cluster"): m.marker_cluster.markers = [] elif change["new"] == "Close": - cleanup() + m.tool_control.cleanup() buttons.value = None @@ -594,50 +601,6 @@ def handle_interaction(**kwargs): return toolbar_widget -def _plotting_tool_callback(map, selected, item): - if selected: - ee_plot_gui(map) - item.control = map._plot_dropdown_control - return - # User has unselected tool. - if not hasattr(map, "_plot_dropdown_widget"): - map._plot_dropdown_widget = None - if not hasattr(map, "_plot_dropdown_control"): - map._plot_dropdown_control = None - plot_dropdown_widget = map._plot_dropdown_widget - plot_dropdown_control = map._plot_dropdown_control - if plot_dropdown_control in map.controls: - map.remove_control(plot_dropdown_control) - del plot_dropdown_widget - del plot_dropdown_control - - if not hasattr(map, "_plot_widget"): - map._plot_widget = None - if not hasattr(map, "_plot_control"): - map._plot_control = None - - if map._plot_control in map.controls: - plot_control = map._plot_control - plot_widget = map._plot_widget - map.remove_control(plot_control) - map._plot_control = None - map._plot_widget = None - del plot_control - del plot_widget - if ( - hasattr(map, "_plot_marker_cluster") - and map._plot_marker_cluster is not None - and map._plot_marker_cluster in map.layers - ): - map.remove_layer(map._plot_marker_cluster) - if hasattr(map, "_chart_points"): - map._chart_points = [] - if hasattr(map, "_chart_values"): - map._chart_values = [] - if hasattr(map, "_chart_labels"): - map._chart_labels = None - - def ee_plot_gui(m, position="topright", **kwargs): """Widget for plotting Earth Engine data. @@ -813,18 +776,56 @@ def cleanup(): setattr(m._plot_dropdown_control, "cleanup", cleanup) + def cleanup(): + if not hasattr(m, "_plot_dropdown_widget"): + m._plot_dropdown_widget = None + if not hasattr(m, "_plot_dropdown_control"): + m._plot_dropdown_control = None + plot_dropdown_widget = m._plot_dropdown_widget + plot_dropdown_control = m._plot_dropdown_control + if plot_dropdown_control in m.controls: + m.remove_control(plot_dropdown_control) + del plot_dropdown_widget + del plot_dropdown_control + + if not hasattr(m, "_plot_widget"): + m._plot_widget = None + if not hasattr(m, "_plot_control"): + m._plot_control = None + + if m._plot_control in m.controls: + plot_control = m._plot_control + plot_widget = m._plot_widget + m.remove_control(plot_control) + m._plot_control = None + m._plot_widget = None + del plot_control + del plot_widget + if ( + hasattr(m, "_plot_marker_cluster") + and m._plot_marker_cluster is not None + and m._plot_marker_cluster in m.layers + ): + m.remove_layer(m._plot_marker_cluster) + if hasattr(m, "_chart_points"): + m._chart_points = [] + if hasattr(m, "_chart_values"): + m._chart_values = [] + if hasattr(m, "_chart_labels"): + m._chart_labels = None + + setattr(m._plot_dropdown_control, "cleanup", cleanup) + def close_click(_): - cleanup() + m._plot_dropdown_control.cleanup() close_btn.on_click(close_click) @map_widgets.Theme.apply class SearchDataGUI(widgets.HBox): - def __init__(self, m, **kwargs): - - # Adds search button and search box + # Adds search button and search box from .conversion import js_snippet_to_py @@ -837,7 +838,9 @@ def __init__(self, m, **kwargs): value=False, tooltip="Search location/data", icon="globe", - layout=widgets.Layout(width="28px", height="28px", padding="0px 0px 0px 4px"), + layout=widgets.Layout( + width="28px", height="28px", padding="0px 0px 0px 4px" + ), ) search_type = widgets.ToggleButtons( @@ -885,7 +888,9 @@ def get_ee_example(asset_id): pkg_dir = os.path.dirname( pkg_resources.resource_filename("geemap", "geemap.py") ) - with open(os.path.join(pkg_dir, "data/gee_f.json"), encoding="utf-8") as f: + with open( + os.path.join(pkg_dir, "data/gee_f.json"), encoding="utf-8" + ) as f: functions = json.load(f) details = [ dataset["code"] @@ -1758,7 +1763,7 @@ def ok_cancel_clicked(change): with tool_output: display(main_widget) elif change["new"] == "Close": - cleanup() + tool_output_ctrl.cleanup() ok_cancel.value = None file_type.observe(file_type_changed, names="value") @@ -1769,16 +1774,6 @@ def ok_cancel_clicked(change): m._tool_output_ctrl = tool_output_ctrl -def _convert_js_tool_callback(map, selected, item): - if selected: - convert_js2py(map) - item.control = map._convert_ctrl - return - # User has unselected tool. - if map._convert_ctrl is not None and map._convert_ctrl in map.controls: - map.remove_control(map._convert_ctrl) - - def convert_js2py(m): """A widget for converting Earth Engine JavaScript to Python. @@ -1801,6 +1796,11 @@ def convert_js2py(m): ) buttons.style.button_width = "128px" + def cleanup(): + if m._convert_ctrl is not None and m._convert_ctrl in m.controls: + m.remove_control(m._convert_ctrl) + full_widget.close() + def button_clicked(change): if change["new"] == "Convert": from .conversion import create_new_cell, js_snippet_to_py @@ -1823,15 +1823,14 @@ def button_clicked(change): elif change["new"] == "Clear": text_widget.value = "" elif change["new"] == "Close": - if m._convert_ctrl is not None and m._convert_ctrl in m.controls: - m.remove_control(m._convert_ctrl) - full_widget.close() + m._convert_ctrl.cleanup() buttons.value = None buttons.observe(button_clicked, "value") full_widget.children = [text_widget, buttons] widget_control = ipyleaflet.WidgetControl(widget=full_widget, position="topright") + setattr(widget_control, "cleanup", cleanup) m.add_control(widget_control) m._convert_ctrl = widget_control @@ -1932,7 +1931,7 @@ def set_properties(_, geometry): value_text2.value = "" color.value = "#3388ff" elif change["new"] == "Close": - cleanup() + m.training_ctrl.cleanup() buttons.value = None buttons.observe(button_clicked, "value") @@ -2181,9 +2180,14 @@ def search_changed(change): search_widget.observe(search_changed, "value") - def close_btn_clicked(b): + def cleanup(): full_widget.close() + setattr(full_widget, "cleanup", cleanup) + + def close_btn_clicked(b): + full_widget.cleanup() + close_btn.on_click(close_btn_clicked) category_widget.value = list(categories.keys())[0] @@ -2610,9 +2614,9 @@ def cleanup(): def close_btn_click(change): if change["new"]: - cleanup() + m.tool_control.cleanup() - close_btn.on_click(lambda _: cleanup()) + close_btn.on_click(lambda _: m.tool_control.cleanup()) close_button.observe(close_btn_click, "value") toolbar_button.value = True @@ -3294,7 +3298,7 @@ def cleanup(): def close_btn_click(change): if change["new"]: - cleanup() + m.tool_control.cleanup() close_button.observe(close_btn_click, "value") @@ -3684,11 +3688,9 @@ def cleanup(): m.transect_control = None toolbar_widget.close() - setattr(m.transect_control, "cleanup", cleanup) - def close_btn_click(change): if change["new"]: - cleanup() + m.tool_control.cleanup() close_button.observe(close_btn_click, "value") @@ -3734,14 +3736,7 @@ def button_clicked(change): elif change["new"] == "Reset": output.outputs = () elif change["new"] == "Close": - if m is not None: - if m.tool_control is not None and m.tool_control in m.controls: - m.remove_control(m.tool_control) - m.tool_control = None - if m.transect_control is not None and m.transect_control in m.controls: - m.remove_control(m.transect_control) - m.transect_control = None - toolbar_widget.close() + m.tool_control.cleanup() buttons.value = None @@ -4103,11 +4098,9 @@ def cleanup(): m.sankee_control = None toolbar_widget.close() - setattr(m.sankee_control, "cleanup", cleanup) - def close_btn_click(change): if change["new"]: - cleanup() + m.tool_control.cleanup() close_button.observe(close_btn_click, "value") @@ -4202,14 +4195,7 @@ def button_clicked(change): plot_widget.children = [] elif change["new"] == "Close": - if m is not None: - if m.tool_control is not None and m.tool_control in m.controls: - m.remove_control(m.tool_control) - m.tool_control = None - if m.sankee_control is not None and m.sankee_control in m.controls: - m.remove_control(m.sankee_control) - m.sankee_control = None - toolbar_widget.close() + m.tool_control.cleanup() buttons.value = None @@ -4220,7 +4206,7 @@ def button_clicked(change): toolbar_control = ipyleaflet.WidgetControl( widget=toolbar_widget, position="topright" ) - + setattr(toolbar_control, "cleanup", cleanup) if toolbar_control not in m.controls: m.add_control(toolbar_control) m.tool_control = toolbar_control @@ -4339,7 +4325,17 @@ def _open_help_page_callback(map, selected, _): def _cleanup_toolbar_item(func): def wrapper(map, selected, item): if selected: - func(map, selected, item) + item.control = func(map, selected, item) + if not hasattr(item.control, "toggle_off"): + setattr(item.control, "toggle_off", item.toggle_off) + if hasattr(item.control, "cleanup"): + cleanup = item.control.cleanup + + def cleanup_and_toggle_off(): + cleanup() + item.toggle_off() + + item.control.cleanup = cleanup_and_toggle_off elif item.control and hasattr(item.control, "cleanup"): item.control.cleanup() @@ -4347,48 +4343,37 @@ def wrapper(map, selected, item): @_cleanup_toolbar_item -def _inspector_tool_callback(map, _, item): +def _inspector_tool_callback(map, _, __): map.add_inspector() - item.control = map._inspector + return map._inspector +@_cleanup_toolbar_item +def _plotting_tool_callback(map, _, __): + ee_plot_gui(map) + return map._plot_dropdown_control @_cleanup_toolbar_item def _timelapse_tool_callback(map, _, item): timelapse_gui(map) - item.control = map.tool_control + return map.tool_control + + +@_cleanup_toolbar_item +def _convert_js_tool_callback(map, selected, item): + convert_js2py(map) + return map._convert_ctrl @_cleanup_toolbar_item def _basemap_tool_callback(map, _, item): map.add_basemap_widget() - item.control = map._basemap_selector + return map._basemap_selector @_cleanup_toolbar_item def _open_data_tool_callback(map, _, item): open_data_widget(map) - item.control = map._tool_output_ctrl - - -@_cleanup_toolbar_item -def _gee_toolbox_tool_callback(map, _, item): - tools_dict = get_tools_dict() - gee_toolbox = build_toolbox(tools_dict, max_width="800px", max_height="500px") - geetoolbox_control = ipyleaflet.WidgetControl( - widget=gee_toolbox, position="bottomright" - ) - setattr( - geetoolbox_control, "cleanup", lambda: map.remove_control(geetoolbox_control) - ) - map.geetoolbox = geetoolbox_control - item.control = geetoolbox_control - map.add(geetoolbox_control) - - -@_cleanup_toolbar_item -def _time_slider_tool_callback(map, _, item): - time_slider(map) - item.control = map.tool_control + return map._tool_output_ctrl @_cleanup_toolbar_item @@ -4405,32 +4390,50 @@ def _whitebox_tool_callback(map, _, item): wbt_control = ipyleaflet.WidgetControl(widget=wbt_toolbox, position="bottomright") setattr(wbt_control, "cleanup", lambda: map.remove_control(wbt_control)) map.whitebox = wbt_control - item.control = wbt_control map.add(wbt_control) + return wbt_control + + +@_cleanup_toolbar_item +def _gee_toolbox_tool_callback(map, _, item): + tools_dict = get_tools_dict() + gee_toolbox = build_toolbox(tools_dict, max_width="800px", max_height="500px") + geetoolbox_control = ipyleaflet.WidgetControl( + widget=gee_toolbox, position="bottomright" + ) + map.geetoolbox = geetoolbox_control + map.add(geetoolbox_control) + return gee_toolbox + + +@_cleanup_toolbar_item +def _time_slider_tool_callback(map, _, item): + time_slider(map) + return map.tool_control @_cleanup_toolbar_item def _collect_samples_tool_callback(map, _, item): collect_samples(map) - item.control = map.training_ctrl + return map.training_ctrl @_cleanup_toolbar_item def _plot_transect_tool_callback(map, _, item): plot_transect(map) - item.control = map.transect_control + return map.tool_control @_cleanup_toolbar_item def _sankee_tool_callback(map, _, item): sankee_gui(map) - item.control = map.sankee_control + return map.tool_control @_cleanup_toolbar_item def _cog_stac_inspector_callback(map, _, item): inspector_gui(map) - item.control = map.tool_control + return map.tool_control main_tools = [ From 6d0d9e2f25298cadd4e4f23a1885762f84e58b0a Mon Sep 17 00:00:00 2001 From: Sufyan Abbasi Date: Fri, 6 Oct 2023 06:07:01 +0000 Subject: [PATCH 4/7] Document the cleanup_toolbar_item decorator --- geemap/toolbar.py | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/geemap/toolbar.py b/geemap/toolbar.py index c6bd421a28..6035c225ad 100644 --- a/geemap/toolbar.py +++ b/geemap/toolbar.py @@ -4323,6 +4323,12 @@ def _open_help_page_callback(map, selected, _): def _cleanup_toolbar_item(func): + """Wraps a toolbar item callback to clean up the widget when unselected.""" + + # The callback should construct the widget and return an object that + # contains a "cleanup" property, a function that removes the widget from the + # map. The decorator will handle construction and cleanup, and will also + # un-toggle the associated toolbar item. def wrapper(map, selected, item): if selected: item.control = func(map, selected, item) @@ -4335,6 +4341,9 @@ def cleanup_and_toggle_off(): cleanup() item.toggle_off() + # Ensures that when cleanup() is invoked on the widget, for + # example by a close button on the widget, the toggle is + # also turned off. item.control.cleanup = cleanup_and_toggle_off elif item.control and hasattr(item.control, "cleanup"): item.control.cleanup() @@ -4343,17 +4352,19 @@ def cleanup_and_toggle_off(): @_cleanup_toolbar_item -def _inspector_tool_callback(map, _, __): +def _inspector_tool_callback(map, selected, item): map.add_inspector() return map._inspector + @_cleanup_toolbar_item -def _plotting_tool_callback(map, _, __): +def _plotting_tool_callback(map, selected, item): ee_plot_gui(map) return map._plot_dropdown_control + @_cleanup_toolbar_item -def _timelapse_tool_callback(map, _, item): +def _timelapse_tool_callback(map, selected, item): timelapse_gui(map) return map.tool_control @@ -4365,19 +4376,19 @@ def _convert_js_tool_callback(map, selected, item): @_cleanup_toolbar_item -def _basemap_tool_callback(map, _, item): +def _basemap_tool_callback(map, selected, item): map.add_basemap_widget() return map._basemap_selector @_cleanup_toolbar_item -def _open_data_tool_callback(map, _, item): +def _open_data_tool_callback(map, selected, item): open_data_widget(map) return map._tool_output_ctrl @_cleanup_toolbar_item -def _whitebox_tool_callback(map, _, item): +def _whitebox_tool_callback(map, selected, item): import whiteboxgui.whiteboxgui as wbt tools_dict = wbt.get_wbt_dict() @@ -4395,7 +4406,7 @@ def _whitebox_tool_callback(map, _, item): @_cleanup_toolbar_item -def _gee_toolbox_tool_callback(map, _, item): +def _gee_toolbox_tool_callback(map, selected, item): tools_dict = get_tools_dict() gee_toolbox = build_toolbox(tools_dict, max_width="800px", max_height="500px") geetoolbox_control = ipyleaflet.WidgetControl( @@ -4407,31 +4418,31 @@ def _gee_toolbox_tool_callback(map, _, item): @_cleanup_toolbar_item -def _time_slider_tool_callback(map, _, item): +def _time_slider_tool_callback(map, selected, item): time_slider(map) return map.tool_control @_cleanup_toolbar_item -def _collect_samples_tool_callback(map, _, item): +def _collect_samples_tool_callback(map, selected, item): collect_samples(map) return map.training_ctrl @_cleanup_toolbar_item -def _plot_transect_tool_callback(map, _, item): +def _plot_transect_tool_callback(map, selected, item): plot_transect(map) return map.tool_control @_cleanup_toolbar_item -def _sankee_tool_callback(map, _, item): +def _sankee_tool_callback(map, selected, item): sankee_gui(map) return map.tool_control @_cleanup_toolbar_item -def _cog_stac_inspector_callback(map, _, item): +def _cog_stac_inspector_callback(map, selected, item): inspector_gui(map) return map.tool_control From 75ffea1dbb24c250193e78c8551e78d89da3b870 Mon Sep 17 00:00:00 2001 From: Sufyan Abbasi Date: Fri, 6 Oct 2023 06:59:11 +0000 Subject: [PATCH 5/7] Add unit tests for toolbar cleanup functionality --- tests/test_toolbar.py | 50 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/tests/test_toolbar.py b/tests/test_toolbar.py index 16a5822b4a..aed699e1d7 100644 --- a/tests/test_toolbar.py +++ b/tests/test_toolbar.py @@ -2,14 +2,15 @@ """Tests for `map_widgets` module.""" - import unittest + +from dataclasses import dataclass from unittest.mock import patch, Mock import ipywidgets import geemap -from geemap.toolbar import Toolbar +from geemap.toolbar import Toolbar, _cleanup_toolbar_item from tests import fake_map, utils @@ -32,6 +33,7 @@ def _query_tool_grid(self, toolbar): def setUp(self) -> None: self.callback_calls = 0 self.last_called_with_selected = None + self.last_called_item = None self.item = Toolbar.Item( icon="info", tooltip="dummy item", callback=self.dummy_callback ) @@ -47,9 +49,10 @@ def tearDown(self) -> None: patch.stopall() return super().tearDown() - def dummy_callback(self, m, selected): + def dummy_callback(self, m, selected, item): del m self.last_called_with_selected = selected + self.last_called_item = item self.callback_calls += 1 def test_no_tools_throws(self): @@ -113,18 +116,21 @@ def test_triggers_callbacks(self): map = geemap.Map(ee_initialize=False) toolbar = Toolbar(map, [self.item, self.no_reset_item]) self.assertIsNone(self.last_called_with_selected) + self.assertIsNone(self.last_called_item) # Select first tool, which resets. toolbar.all_widgets[0].value = True self.assertFalse(self.last_called_with_selected) # was reset by callback self.assertEqual(self.callback_calls, 2) self.assertFalse(toolbar.all_widgets[0].value) + self.assertEqual(self.item, self.last_called_item) # Select second tool, which does not reset. toolbar.all_widgets[1].value = True self.assertTrue(self.last_called_with_selected) self.assertEqual(self.callback_calls, 3) self.assertTrue(toolbar.all_widgets[1].value) + self.assertEqual(self.no_reset_item, self.last_called_item) def test_layers_toggle_callback(self): """Verifies the on_layers_toggled callback is triggered.""" @@ -154,3 +160,41 @@ def test_accessory_widget(self): toolbar, ipywidgets.ToggleButton, lambda c: c.tooltip == "test-button" ) ) + + @dataclass + class TestWidget: + selected_count = 0 + cleanup_count = 0 + + def cleanup(self): + self.cleanup_count += 1 + + def test_cleanup_toolbar_item_decorator(self): + widget = TestToolbar.TestWidget() + + @_cleanup_toolbar_item + def callback(m, selected, item): + widget.selected_count += 1 + return widget + + item = Toolbar.Item( + icon="info", tooltip="dummy item", callback=callback, reset=False + ) + map_fake = fake_map.FakeMap() + toolbar = Toolbar(map_fake, [item]) + toolbar.all_widgets[0].value = True + self.assertEqual(1, widget.selected_count) + self.assertEqual(0, widget.cleanup_count) + + toolbar.all_widgets[0].value = False + self.assertEqual(1, widget.selected_count) + self.assertEqual(1, widget.cleanup_count) + + toolbar.all_widgets[0].value = True + self.assertEqual(2, widget.selected_count) + self.assertEqual(1, widget.cleanup_count) + + widget.cleanup() + self.assertEqual(2, widget.selected_count) + self.assertEqual(3, widget.cleanup_count) + self.assertFalse(toolbar.all_widgets[0].value) From 4ba90ca81f5f5469783ae9a7b27cb5d52d85610f Mon Sep 17 00:00:00 2001 From: Sufyan Abbasi Date: Fri, 6 Oct 2023 23:08:51 +0000 Subject: [PATCH 6/7] Allow multiple widgets, code cleanup --- geemap/core.py | 13 +++++++----- geemap/map_widgets.py | 2 -- geemap/toolbar.py | 47 ++++++++++++++++++++++++++----------------- 3 files changed, 36 insertions(+), 26 deletions(-) diff --git a/geemap/core.py b/geemap/core.py index a2f9d8e4f0..3b309a4545 100644 --- a/geemap/core.py +++ b/geemap/core.py @@ -768,19 +768,22 @@ def add_layer( def _open_help_page( self, host_map: MapInterface, selected: bool, item: toolbar.Toolbar.Item ) -> None: - del host_map # Unused. - del item # Unused. + del host_map, item # Unused. if selected: common.open_url("https://geemap.org") def _toolbar_main_tools(self) -> List[toolbar.Toolbar.Item]: @toolbar._cleanup_toolbar_item - def inspector_tool_callback(map, _, item): + def inspector_tool_callback( + map: Map, selected: bool, item: toolbar.Toolbar.Item + ): + del selected, item # Unused. map.add("inspector") return map._inspector @toolbar._cleanup_toolbar_item - def basemap_tool_callback(map, _, item): + def basemap_tool_callback(map: Map, selected: bool, item: toolbar.Toolbar.Item): + del selected, item # Unused. map.add("basemap_selector") return map._basemap_selector @@ -795,7 +798,7 @@ def basemap_tool_callback(map, _, item): icon="info", tooltip="Inspector", callback=inspector_tool_callback, - reset=False + reset=False, ), toolbar.Toolbar.Item( icon="question", tooltip="Get help", callback=self._open_help_page diff --git a/geemap/map_widgets.py b/geemap/map_widgets.py index 523a71eec3..f04dc51a3d 100644 --- a/geemap/map_widgets.py +++ b/geemap/map_widgets.py @@ -903,8 +903,6 @@ def _on_dropdown_click(self, change): def cleanup(self): if self.on_close: self.on_close() - if hasattr(self, 'toggle_off'): - self.toggle_off() def _on_close_click(self, _): self.cleanup() diff --git a/geemap/toolbar.py b/geemap/toolbar.py index 6035c225ad..b6a72f33ea 100644 --- a/geemap/toolbar.py +++ b/geemap/toolbar.py @@ -16,7 +16,6 @@ import ipyleaflet import ipywidgets as widgets -from ipywidgets.widgets import Widget from ipyfilechooser import FileChooser from IPython.core.display import display from typing import Any, Callable, Optional @@ -52,8 +51,8 @@ class Item: tooltip: str callback: Callable[[Any, bool, Any], None] reset: bool = True - control: Optional[Widget] = None - toggle_button: Any = None + control: Optional[widgets.Widget] = None + toggle_button: Optional[widgets.ToggleButton] = None def toggle_off(self): if self.toggle_button: @@ -123,9 +122,6 @@ def curry_callback(callback, should_reset_after, widget, item): def returned_callback(change): if change["type"] != "change": return - if change["new"]: - # Unselect all other tool widgets if one is enabled. - self._reset_others(widget) callback(self.host_map, change["new"], item) if should_reset_after: widget.value = False @@ -179,8 +175,8 @@ def _reset_others(self, current): if other is not current: other.value = False - def _toggle_callback(self, m, selected, _): - del m # unused + def _toggle_callback(self, m, selected, item): + del m, item # unused if not selected: return if self.toggle_widget.icon == self._TOGGLE_TOOL_EXPAND_ICON: @@ -585,7 +581,7 @@ def handle_interaction(**kwargs): toolbar_control = ipyleaflet.WidgetControl( widget=toolbar_widget, position="topright" ) - setattr(toolbar_control, "cleanup", cleanup) + toolbar_control.cleanup = cleanup if toolbar_control not in m.controls: m.add_control(toolbar_control) @@ -774,7 +770,7 @@ def cleanup(): else: m.remove_draw_control() - setattr(m._plot_dropdown_control, "cleanup", cleanup) + m._plot_dropdown_control.cleanup = cleanup def cleanup(): if not hasattr(m, "_plot_dropdown_widget"): @@ -814,7 +810,7 @@ def cleanup(): if hasattr(m, "_chart_labels"): m._chart_labels = None - setattr(m._plot_dropdown_control, "cleanup", cleanup) + m._plot_dropdown_control.cleanup = cleanup def close_click(_): m._plot_dropdown_control.cleanup() @@ -1681,7 +1677,7 @@ def cleanup(): m.remove_control(m._tool_output_ctrl) m._tool_output_ctrl = None - setattr(tool_output_ctrl, "cleanup", cleanup) + tool_output_ctrl.cleanup = cleanup def ok_cancel_clicked(change): if change["new"] == "Apply": @@ -1830,7 +1826,7 @@ def button_clicked(change): full_widget.children = [text_widget, buttons] widget_control = ipyleaflet.WidgetControl(widget=full_widget, position="topright") - setattr(widget_control, "cleanup", cleanup) + widget_control.cleanup = cleanup m.add_control(widget_control) m._convert_ctrl = widget_control @@ -1944,7 +1940,7 @@ def set_properties(_, geometry): ] widget_control = ipyleaflet.WidgetControl(widget=full_widget, position="topright") - setattr(widget_control, "cleanup", cleanup) + widget_control.cleanup = cleanup m.add_control(widget_control) m.training_ctrl = widget_control @@ -2183,7 +2179,7 @@ def search_changed(change): def cleanup(): full_widget.close() - setattr(full_widget, "cleanup", cleanup) + full_widget.cleanup = cleanup def close_btn_clicked(b): full_widget.cleanup() @@ -2624,7 +2620,7 @@ def close_btn_click(change): toolbar_control = ipyleaflet.WidgetControl( widget=toolbar_widget, position="topright" ) - setattr(toolbar_control, "cleanup", cleanup) + toolbar_control.cleanup = cleanup if toolbar_control not in m.controls: m.add_control(toolbar_control) m.tool_control = toolbar_control @@ -3505,7 +3501,7 @@ def toolbar_btn_click(change): toolbar_control = ipyleaflet.WidgetControl( widget=toolbar_widget, position="topright" ) - setattr(toolbar_control, "cleanup", cleanup) + toolbar_control.cleanup = cleanup if toolbar_control not in m.controls: m.add_control(toolbar_control) @@ -3747,7 +3743,7 @@ def button_clicked(change): toolbar_control = ipyleaflet.WidgetControl( widget=toolbar_widget, position="topright" ) - setattr(toolbar_control, "cleanup", cleanup) + toolbar_control.cleanup = cleanup if toolbar_control not in m.controls: m.add_control(toolbar_control) m.tool_control = toolbar_control @@ -4206,7 +4202,7 @@ def button_clicked(change): toolbar_control = ipyleaflet.WidgetControl( widget=toolbar_widget, position="topright" ) - setattr(toolbar_control, "cleanup", cleanup) + toolbar_control.cleanup = cleanup if toolbar_control not in m.controls: m.add_control(toolbar_control) m.tool_control = toolbar_control @@ -4353,42 +4349,49 @@ def cleanup_and_toggle_off(): @_cleanup_toolbar_item def _inspector_tool_callback(map, selected, item): + del selected, item # Unused. map.add_inspector() return map._inspector @_cleanup_toolbar_item def _plotting_tool_callback(map, selected, item): + del selected, item # Unused. ee_plot_gui(map) return map._plot_dropdown_control @_cleanup_toolbar_item def _timelapse_tool_callback(map, selected, item): + del selected, item # Unused. timelapse_gui(map) return map.tool_control @_cleanup_toolbar_item def _convert_js_tool_callback(map, selected, item): + del selected, item # Unused. convert_js2py(map) return map._convert_ctrl @_cleanup_toolbar_item def _basemap_tool_callback(map, selected, item): + del selected, item # Unused. map.add_basemap_widget() return map._basemap_selector @_cleanup_toolbar_item def _open_data_tool_callback(map, selected, item): + del selected, item # Unused. open_data_widget(map) return map._tool_output_ctrl @_cleanup_toolbar_item def _whitebox_tool_callback(map, selected, item): + del selected, item # Unused. import whiteboxgui.whiteboxgui as wbt tools_dict = wbt.get_wbt_dict() @@ -4407,6 +4410,7 @@ def _whitebox_tool_callback(map, selected, item): @_cleanup_toolbar_item def _gee_toolbox_tool_callback(map, selected, item): + del selected, item # Unused. tools_dict = get_tools_dict() gee_toolbox = build_toolbox(tools_dict, max_width="800px", max_height="500px") geetoolbox_control = ipyleaflet.WidgetControl( @@ -4419,30 +4423,35 @@ def _gee_toolbox_tool_callback(map, selected, item): @_cleanup_toolbar_item def _time_slider_tool_callback(map, selected, item): + del selected, item # Unused. time_slider(map) return map.tool_control @_cleanup_toolbar_item def _collect_samples_tool_callback(map, selected, item): + del selected, item # Unused. collect_samples(map) return map.training_ctrl @_cleanup_toolbar_item def _plot_transect_tool_callback(map, selected, item): + del selected, item # Unused. plot_transect(map) return map.tool_control @_cleanup_toolbar_item def _sankee_tool_callback(map, selected, item): + del selected, item # Unused. sankee_gui(map) return map.tool_control @_cleanup_toolbar_item def _cog_stac_inspector_callback(map, selected, item): + del selected, item # Unused. inspector_gui(map) return map.tool_control From 6701c97e21116db11a089fe6d5888815ec9f44b3 Mon Sep 17 00:00:00 2001 From: Qiusheng Wu Date: Sat, 7 Oct 2023 18:39:01 -0400 Subject: [PATCH 7/7] Removed unused webbrowser --- geemap/core.py | 1 - 1 file changed, 1 deletion(-) diff --git a/geemap/core.py b/geemap/core.py index 3b309a4545..6829800d5f 100644 --- a/geemap/core.py +++ b/geemap/core.py @@ -4,7 +4,6 @@ import logging import math from typing import Any, Dict, List, Optional, Sequence, Tuple, Type -import webbrowser import ee import ipyleaflet