From 013a5470b536e5f0aeba7beec3f60ffd5ea7f431 Mon Sep 17 00:00:00 2001 From: 29riyasaxena <29riyasaxena@gmail.com> Date: Sun, 16 Jun 2024 00:32:00 +0530 Subject: [PATCH 01/17] grass.jupyter: Allow Users to draw computational region --- python/grass/jupyter/interactivemap.py | 109 ++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 1 deletion(-) diff --git a/python/grass/jupyter/interactivemap.py b/python/grass/jupyter/interactivemap.py index da06582bfba..3cc8ce85d16 100644 --- a/python/grass/jupyter/interactivemap.py +++ b/python/grass/jupyter/interactivemap.py @@ -16,6 +16,7 @@ import base64 import json from .reprojection_renderer import ReprojectionRenderer +import grass.script as gs def get_backend(interactive_map): @@ -347,13 +348,119 @@ def add_layer_control(self, **kwargs): else: self.layer_control_object = self._ipyleaflet.LayersControl(**kwargs) + def draw_computational_region(self): + import ipywidgets as widgets + from IPython.display import display + + draw_control_region = self._ipyleaflet.DrawControl( + rectangle={"shapeOptions": {"color": "#0000FF"}}, + polyline={}, + polygon={}, + circle={}, + circlemarker={}, + marker={}, + edit=False, + remove=True, + ) + self.map.add_control(draw_control_region) + + region_coordinates = {} + + temp_layer = None + saved_layer = None + + temp_geo_json = None + self.output_widget = widgets.Output() + display(self.output_widget) + + def handle_draw(action, geo_json): + nonlocal temp_layer, temp_geo_json, saved_layer + if action == "created" and geo_json["geometry"]["type"] == "Polygon": + if temp_layer: + self.map.remove_layer(temp_layer) + + temp_geo_json = geo_json + + temp_layer = self._ipyleaflet.GeoJSON(data=geo_json) + self.map.add_layer(temp_layer) + + coords = geo_json["geometry"]["coordinates"][0] + min_x = min([point[0] for point in coords]) + max_x = max([point[0] for point in coords]) + min_y = min([point[1] for point in coords]) + max_y = max([point[1] for point in coords]) + + with self.output_widget: + print( + f""" + Drawn Region coordinates: + North={max_y}, South={min_y}, East={max_x}, West={min_x} + """ + ) + + elif action == "deleted": + if temp_layer: + self.map.remove_layer(temp_layer) + temp_layer = None + temp_geo_json = None + + if saved_layer: + self.map.remove_layer(saved_layer) + saved_layer = None + region_coordinates.clear() + + with self.output_widget: + print("Region deleted") + + def save_region(): + nonlocal temp_layer, temp_geo_json, saved_layer + if temp_geo_json: + geo_json_copy = self.temp_geo_json.copy() + geo_json_copy["properties"] = {"name": "computational_region"} + + if temp_layer: + self.map.remove_layer(temp_layer) + temp_layer = None + + if saved_layer: + self.map.remove_layer(saved_layer) + + saved_layer = self._ipyleaflet.GeoJSON( + data=temp_geo_json, name="computational_region" + ) + self.map.add_layer(saved_layer) + + coords = geo_json_copy["geometry"]["coordinates"][0] + min_x = min([point[0] for point in coords]) + max_x = max([point[0] for point in coords]) + min_y = min([point[1] for point in coords]) + max_y = max([point[1] for point in coords]) + + region_coordinates["north"] = max_y + region_coordinates["south"] = min_y + region_coordinates["east"] = max_x + region_coordinates["west"] = min_x + + gs.run_command("g.region", n=max_y, s=min_y, e=max_x, w=min_x) + with self.output_widget: + print(f"Saved Region coordinates: {region_coordinates}") + + temp_geo_json = None + + save_button = widgets.Button(description="Save Region") + save_button.on_click(save_region) + display(save_button) + + draw_control_region.on_draw(handle_draw) + def show(self): """This function returns a folium figure or ipyleaflet map object with a GRASS raster and/or vector overlaid on a basemap. If map has layer control enabled, additional layers cannot be added after calling show().""" - + if self._ipyleaflet: + self.draw_computational_region() self.map.fit_bounds(self._renderer.get_bbox()) if self._folium: if self.layer_control: From a09413ce8c6024c43ce0675a9a5d43526c6b31cc Mon Sep 17 00:00:00 2001 From: 29riyasaxena <29riyasaxena@gmail.com> Date: Mon, 24 Jun 2024 20:06:03 +0530 Subject: [PATCH 02/17] Add Draw Computational Region Button --- python/grass/jupyter/interactivemap.py | 197 +++++++++++++++---------- 1 file changed, 120 insertions(+), 77 deletions(-) diff --git a/python/grass/jupyter/interactivemap.py b/python/grass/jupyter/interactivemap.py index 3cc8ce85d16..0351e36d568 100644 --- a/python/grass/jupyter/interactivemap.py +++ b/python/grass/jupyter/interactivemap.py @@ -16,7 +16,6 @@ import base64 import json from .reprojection_renderer import ReprojectionRenderer -import grass.script as gs def get_backend(interactive_map): @@ -350,108 +349,152 @@ def add_layer_control(self, **kwargs): def draw_computational_region(self): import ipywidgets as widgets - from IPython.display import display + from shapely.geometry import Polygon + + region_mode_button = widgets.ToggleButton( + icon="square", + value=False, + tooltip="Click to add computational region", + button_style="info", + layout=widgets.Layout(width="33px", margin="0px 0px 0px 0px"), + ) + + bottom_output_widget = widgets.Output( + layout={ + "width": "100%", + "max_height": "300px", + "max_width": "300px", + "overflow": "auto", + } + ) - draw_control_region = self._ipyleaflet.DrawControl( - rectangle={"shapeOptions": {"color": "#0000FF"}}, + region_coordinates = {} + drawn_feature = None + temp_drawn_feature = None + bottom_output_control = None + + draw_control = self._ipyleaflet.DrawControl( + rectangle={"shapeOptions": {"color": "#ff0000"}}, polyline={}, - polygon={}, - circle={}, circlemarker={}, - marker={}, + circle={}, + polygon={}, edit=False, remove=True, ) - self.map.add_control(draw_control_region) - - region_coordinates = {} - temp_layer = None - saved_layer = None + def handle_draw(_, action, geo_json): + nonlocal temp_drawn_feature, drawn_feature - temp_geo_json = None - self.output_widget = widgets.Output() - display(self.output_widget) + temp_drawn_feature = temp_drawn_feature - def handle_draw(action, geo_json): - nonlocal temp_layer, temp_geo_json, saved_layer if action == "created" and geo_json["geometry"]["type"] == "Polygon": - if temp_layer: - self.map.remove_layer(temp_layer) + bounds = geo_json["geometry"]["coordinates"][0] + min_x, min_y = bounds[0] + max_x, max_y = bounds[2] + + region_coordinates["North"] = max_y + region_coordinates["South"] = min_y + region_coordinates["East"] = max_x + region_coordinates["West"] = min_x + + if temp_drawn_feature: + self.map.remove_layer(temp_drawn_feature) + + polygon = Polygon([tuple(coord) for coord in bounds]) + geo_json_data = self._ipyleaflet.GeoJSON( + data=polygon.__geo_interface__, + style={"color": "red"}, + name="Computational Region", + ) + temp_drawn_feature = geo_json_data + + save_button.disabled = False + + elif action == "deleted": + if drawn_feature: + self.map.remove_layer(drawn_feature) + drawn_feature = None + + if temp_drawn_feature: + self.map.remove_layer(temp_drawn_feature) + temp_drawn_feature = None + + save_button.disabled = True + if bottom_output_control and bottom_output_control in self.map.controls: + self.map.remove_control(bottom_output_control) + + draw_control.on_draw(handle_draw) + + save_button = widgets.Button( + icon="download", + tooltip="Click to save computational region", + button_style="success", + disabled=True, + layout=widgets.Layout(width="33px", margin="0px 0px 0px 0px"), + ) - temp_geo_json = geo_json + def save_region(_): + nonlocal drawn_feature, temp_drawn_feature, bottom_output_control + temp_drawn_feature = temp_drawn_feature - temp_layer = self._ipyleaflet.GeoJSON(data=geo_json) - self.map.add_layer(temp_layer) + if temp_drawn_feature: + if drawn_feature: + self.map.remove_layer(drawn_feature) - coords = geo_json["geometry"]["coordinates"][0] - min_x = min([point[0] for point in coords]) - max_x = max([point[0] for point in coords]) - min_y = min([point[1] for point in coords]) - max_y = max([point[1] for point in coords]) + self.map.add_layer(temp_drawn_feature) + drawn_feature = temp_drawn_feature + temp_drawn_feature = None - with self.output_widget: + with bottom_output_widget: + bottom_output_widget.clear_output() print( f""" - Drawn Region coordinates: - North={max_y}, South={min_y}, East={max_x}, West={min_x} + Saved Region coordinates: + North={region_coordinates['North']}, + South={region_coordinates['South']}, + East={region_coordinates['East']}, + West={region_coordinates['West']} """ ) - elif action == "deleted": - if temp_layer: - self.map.remove_layer(temp_layer) - temp_layer = None - temp_geo_json = None - - if saved_layer: - self.map.remove_layer(saved_layer) - saved_layer = None - region_coordinates.clear() - - with self.output_widget: - print("Region deleted") - - def save_region(): - nonlocal temp_layer, temp_geo_json, saved_layer - if temp_geo_json: - geo_json_copy = self.temp_geo_json.copy() - geo_json_copy["properties"] = {"name": "computational_region"} - - if temp_layer: - self.map.remove_layer(temp_layer) - temp_layer = None - - if saved_layer: - self.map.remove_layer(saved_layer) - - saved_layer = self._ipyleaflet.GeoJSON( - data=temp_geo_json, name="computational_region" + if bottom_output_control and bottom_output_control in self.map.controls: + self.map.remove_control(bottom_output_control) + bottom_output_control = self._ipyleaflet.WidgetControl( + widget=bottom_output_widget, position="bottomright" ) - self.map.add_layer(saved_layer) + self.map.add_control(bottom_output_control) - coords = geo_json_copy["geometry"]["coordinates"][0] - min_x = min([point[0] for point in coords]) - max_x = max([point[0] for point in coords]) - min_y = min([point[1] for point in coords]) - max_y = max([point[1] for point in coords]) + save_button.on_click(save_region) - region_coordinates["north"] = max_y - region_coordinates["south"] = min_y - region_coordinates["east"] = max_x - region_coordinates["west"] = min_x + save_button_control = self._ipyleaflet.WidgetControl( + widget=save_button, position="topright" + ) - gs.run_command("g.region", n=max_y, s=min_y, e=max_x, w=min_x) - with self.output_widget: - print(f"Saved Region coordinates: {region_coordinates}") + def toggle_region_mode(change): + nonlocal save_button_control, bottom_output_control - temp_geo_json = None + if change["new"]: + self.map.add_control(draw_control) + self.map.add_control(save_button_control) + else: + self.map.remove_control(draw_control) + if save_button_control in self.map.controls: + self.map.remove_control(save_button_control) + if bottom_output_control and bottom_output_control in self.map.controls: + self.map.remove_control(bottom_output_control) + bottom_output_widget.clear_output() - save_button = widgets.Button(description="Save Region") - save_button.on_click(save_region) - display(save_button) + if temp_drawn_feature: + self.map.remove_layer(temp_drawn_feature) + save_button.disabled = True - draw_control_region.on_draw(handle_draw) + region_mode_button.observe(toggle_region_mode, names="value") + + region_mode_control = self._ipyleaflet.WidgetControl( + widget=region_mode_button, position="topright" + ) + self.map.add_control(region_mode_control) def show(self): """This function returns a folium figure or ipyleaflet map object From 70811b1dac43c589fc8e25fff0b2e1c76496a1b1 Mon Sep 17 00:00:00 2001 From: 29riyasaxena <29riyasaxena@gmail.com> Date: Thu, 27 Jun 2024 20:04:50 +0530 Subject: [PATCH 03/17] Add Rectangle to InteractiveMap --- python/grass/jupyter/interactivemap.py | 246 ++++++++++--------------- 1 file changed, 100 insertions(+), 146 deletions(-) diff --git a/python/grass/jupyter/interactivemap.py b/python/grass/jupyter/interactivemap.py index 0351e36d568..59d9596af18 100644 --- a/python/grass/jupyter/interactivemap.py +++ b/python/grass/jupyter/interactivemap.py @@ -16,6 +16,7 @@ import base64 import json from .reprojection_renderer import ReprojectionRenderer +from .utils import get_computational_region_bb def get_backend(interactive_map): @@ -348,153 +349,106 @@ def add_layer_control(self, **kwargs): self.layer_control_object = self._ipyleaflet.LayersControl(**kwargs) def draw_computational_region(self): - import ipywidgets as widgets - from shapely.geometry import Polygon - - region_mode_button = widgets.ToggleButton( - icon="square", - value=False, - tooltip="Click to add computational region", - button_style="info", - layout=widgets.Layout(width="33px", margin="0px 0px 0px 0px"), + # import ipywidgets as widgets + # import pyproj + # from shapely.geometry import box + # from shapely.ops import transform + + # region_mode_button = widgets.ToggleButton( + # icon="square", + # value=False, + # tooltip="Click to add computational region", + # button_style="info", + # layout=widgets.Layout(width="33px", margin="0px 0px 0px 0px"), + # ) + + # bottom_output_widget = widgets.Output( + # layout={ + # "width": "100%", + # "max_height": "300px", + # "max_width": "300px", + # "overflow": "auto", + # } + # ) + + # region_coordinates = {} + rectangle = None + latlon_bounds = get_computational_region_bb() + print(latlon_bounds) + latlon_bounds = get_computational_region_bb() + rectangle = self._ipyleaflet.Rectangle( + bounds=latlon_bounds, + color="red", + fill_color="red", + fill_opacity=0.5, + draggable=True, ) - - bottom_output_widget = widgets.Output( - layout={ - "width": "100%", - "max_height": "300px", - "max_width": "300px", - "overflow": "auto", - } - ) - - region_coordinates = {} - drawn_feature = None - temp_drawn_feature = None - bottom_output_control = None - - draw_control = self._ipyleaflet.DrawControl( - rectangle={"shapeOptions": {"color": "#ff0000"}}, - polyline={}, - circlemarker={}, - circle={}, - polygon={}, - edit=False, - remove=True, - ) - - def handle_draw(_, action, geo_json): - nonlocal temp_drawn_feature, drawn_feature - - temp_drawn_feature = temp_drawn_feature - - if action == "created" and geo_json["geometry"]["type"] == "Polygon": - bounds = geo_json["geometry"]["coordinates"][0] - min_x, min_y = bounds[0] - max_x, max_y = bounds[2] - - region_coordinates["North"] = max_y - region_coordinates["South"] = min_y - region_coordinates["East"] = max_x - region_coordinates["West"] = min_x - - if temp_drawn_feature: - self.map.remove_layer(temp_drawn_feature) - - polygon = Polygon([tuple(coord) for coord in bounds]) - geo_json_data = self._ipyleaflet.GeoJSON( - data=polygon.__geo_interface__, - style={"color": "red"}, - name="Computational Region", - ) - temp_drawn_feature = geo_json_data - - save_button.disabled = False - - elif action == "deleted": - if drawn_feature: - self.map.remove_layer(drawn_feature) - drawn_feature = None - - if temp_drawn_feature: - self.map.remove_layer(temp_drawn_feature) - temp_drawn_feature = None - - save_button.disabled = True - if bottom_output_control and bottom_output_control in self.map.controls: - self.map.remove_control(bottom_output_control) - - draw_control.on_draw(handle_draw) - - save_button = widgets.Button( - icon="download", - tooltip="Click to save computational region", - button_style="success", - disabled=True, - layout=widgets.Layout(width="33px", margin="0px 0px 0px 0px"), - ) - - def save_region(_): - nonlocal drawn_feature, temp_drawn_feature, bottom_output_control - temp_drawn_feature = temp_drawn_feature - - if temp_drawn_feature: - if drawn_feature: - self.map.remove_layer(drawn_feature) - - self.map.add_layer(temp_drawn_feature) - drawn_feature = temp_drawn_feature - temp_drawn_feature = None - - with bottom_output_widget: - bottom_output_widget.clear_output() - print( - f""" - Saved Region coordinates: - North={region_coordinates['North']}, - South={region_coordinates['South']}, - East={region_coordinates['East']}, - West={region_coordinates['West']} - """ - ) - - if bottom_output_control and bottom_output_control in self.map.controls: - self.map.remove_control(bottom_output_control) - bottom_output_control = self._ipyleaflet.WidgetControl( - widget=bottom_output_widget, position="bottomright" - ) - self.map.add_control(bottom_output_control) - - save_button.on_click(save_region) - - save_button_control = self._ipyleaflet.WidgetControl( - widget=save_button, position="topright" - ) - - def toggle_region_mode(change): - nonlocal save_button_control, bottom_output_control - - if change["new"]: - self.map.add_control(draw_control) - self.map.add_control(save_button_control) - else: - self.map.remove_control(draw_control) - if save_button_control in self.map.controls: - self.map.remove_control(save_button_control) - if bottom_output_control and bottom_output_control in self.map.controls: - self.map.remove_control(bottom_output_control) - bottom_output_widget.clear_output() - - if temp_drawn_feature: - self.map.remove_layer(temp_drawn_feature) - save_button.disabled = True - - region_mode_button.observe(toggle_region_mode, names="value") - - region_mode_control = self._ipyleaflet.WidgetControl( - widget=region_mode_button, position="topright" - ) - self.map.add_control(region_mode_control) + self.map.add_layer(rectangle) + + # def transform_bounds(bounds, from_crs, to_crs): + # project = + # pyproj.Transformer.from_crs(from_crs, to_crs, always_xy=True).transform + # transformed_bounds = transform(project, box(*bounds)).bounds + # return [[transformed_bounds[1], transformed_bounds[0]], + # [transformed_bounds[3], transformed_bounds[2]]] + + # def toggle_region_mode(change): + # nonlocal rectangle + + # if change["new"]: + # if rectangle is None: + # latlon_bounds = get_computational_region_bb() + # rectangle = ipyleaflet.Rectangle( + # bounds=latlon_bounds, + # color="red", + # fill_color="red", + # fill_opacity=0.5, + # draggable=True + # ) + # self.map.add_layer(rectangle) + # else: + # rectangle.editable = True + # rectangle.draggable = True + # else: + # if rectangle: + # rectangle.editable = False + # rectangle.draggable = False + # latlon_bounds = + # rectangle.bounds + # bounds = + # transform_bounds([latlon_bounds[0][1], latlon_bounds[0][0], + # latlon_bounds[1][1], latlon_bounds[1][0]], + # "epsg:4326", "epsg:3857") + # region_coordinates["North"] = bounds[3] + # region_coordinates["South"] = bounds[1] + # region_coordinates["East"] = bounds[2] + # region_coordinates["West"] = bounds[0] + + # with bottom_output_widget: + # bottom_output_widget.clear_output() + # print( + # f""" + # Saved Region coordinates: + # North={region_coordinates['North']}, + # South={region_coordinates['South']}, + # East={region_coordinates['East']}, + # West={region_coordinates['West']} + # """ + # ) + # self.map.remove_layer(rectangle) + # rectangle = None + + # region_mode_button.observe(toggle_region_mode, names="value") + + # region_mode_control = ipyleaflet.WidgetControl( + # widget=region_mode_button, position="topright" + # ) + # self.map.add_control(region_mode_control) + + # output_control = ipyleaflet.WidgetControl( + # widget=bottom_output_widget, position="bottomright" + # ) + # self.map.add_control(output_control) def show(self): """This function returns a folium figure or ipyleaflet map object From 6c6fd2414bb693b6f7d56030fd4e3043440d5dc8 Mon Sep 17 00:00:00 2001 From: 29riyasaxena <29riyasaxena@gmail.com> Date: Thu, 27 Jun 2024 23:22:40 +0530 Subject: [PATCH 04/17] Modify utils.py --- python/grass/jupyter/utils.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/python/grass/jupyter/utils.py b/python/grass/jupyter/utils.py index f06adfa36d1..fdfe008fca7 100644 --- a/python/grass/jupyter/utils.py +++ b/python/grass/jupyter/utils.py @@ -202,6 +202,39 @@ def get_rendering_size(region, width, height, default_width=600, default_height= return (default_width, round(default_width * region_height / region_width)) +def dms_to_dd(dms_str): + dms_str = dms_str.strip() + direction = dms_str[-1] + dms_str = dms_str[:-1] + degrees, minutes, seconds = map(float, dms_str.split(":")) + dd = degrees + minutes / 60 + seconds / 3600 + if direction in ["W", "S"]: + dd *= -1 + return dd + + +def get_computational_region_bb(): + region = gs.read_command("g.region", flags="b") + output = gs.decode(region).splitlines() + output_dict = {} + + for line in output: + if line.strip(): + key, value = line.split(":", 1) + output_dict[key.strip()] = value.strip() + north_latitude = dms_to_dd(output_dict["north latitude"]) + south_latitude = dms_to_dd(output_dict["south latitude"]) + west_longitude = dms_to_dd(output_dict["west longitude"]) + east_longitude = dms_to_dd(output_dict["east longitude"]) + + # Create tuples for SW and NE locations + sw_location = (west_longitude, south_latitude) + ne_location = (east_longitude, north_latitude) + + # Return the list of tuples + return [sw_location, ne_location] + + def save_gif( input_files, output_filename, From a63c1f223fea3d31b9122e1d22d3f347b517efcf Mon Sep 17 00:00:00 2001 From: 29riyasaxena <29riyasaxena@gmail.com> Date: Sun, 30 Jun 2024 00:02:45 +0530 Subject: [PATCH 05/17] Modify utils and allow users to modify computational region --- python/grass/jupyter/interactivemap.py | 219 ++++++++++++++----------- python/grass/jupyter/utils.py | 4 +- 2 files changed, 123 insertions(+), 100 deletions(-) diff --git a/python/grass/jupyter/interactivemap.py b/python/grass/jupyter/interactivemap.py index 59d9596af18..885856f63d9 100644 --- a/python/grass/jupyter/interactivemap.py +++ b/python/grass/jupyter/interactivemap.py @@ -349,106 +349,129 @@ def add_layer_control(self, **kwargs): self.layer_control_object = self._ipyleaflet.LayersControl(**kwargs) def draw_computational_region(self): - # import ipywidgets as widgets - # import pyproj - # from shapely.geometry import box - # from shapely.ops import transform - - # region_mode_button = widgets.ToggleButton( - # icon="square", - # value=False, - # tooltip="Click to add computational region", - # button_style="info", - # layout=widgets.Layout(width="33px", margin="0px 0px 0px 0px"), - # ) - - # bottom_output_widget = widgets.Output( - # layout={ - # "width": "100%", - # "max_height": "300px", - # "max_width": "300px", - # "overflow": "auto", - # } - # ) - - # region_coordinates = {} + import ipywidgets as widgets + from ipyleaflet import Rectangle, WidgetControl + + region_mode_button = widgets.ToggleButton( + icon="square", + value=False, + tooltip="Click to add computational region", + button_style="info", + layout=widgets.Layout(width="33px", margin="0px 0px 0px 0px"), + ) + + save_button = widgets.Button( + description="Save Region", + button_style="success", + layout=widgets.Layout(width="100px", margin="0px 0px 0px 5px"), + disabled=True, + ) + + bottom_output_widget = widgets.Output( + layout={ + "width": "100%", + "max_height": "300px", + "max_width": "300px", + "overflow": "auto", + "display": "none", + } + ) + + region_coordinates = {} rectangle = None - latlon_bounds = get_computational_region_bb() - print(latlon_bounds) - latlon_bounds = get_computational_region_bb() - rectangle = self._ipyleaflet.Rectangle( - bounds=latlon_bounds, - color="red", - fill_color="red", - fill_opacity=0.5, - draggable=True, + save_button_control = None + + def update_output(): + with bottom_output_widget: + bottom_output_widget.clear_output() + print("Current Bounds:") + print(f"North: {region_coordinates['North']}") + print(f"South: {region_coordinates['South']}") + print(f"East: {region_coordinates['East']}") + print(f"West: {region_coordinates['West']}") + + def on_rectangle_change(event, property_name, value): + nonlocal region_coordinates + if property_name == "bounds": + latlon_bounds = value + region_coordinates = { + "North": latlon_bounds[1][0], + "South": latlon_bounds[0][0], + "East": latlon_bounds[1][1], + "West": latlon_bounds[0][1], + } + update_output() + + def toggle_region_mode(change): + nonlocal rectangle, save_button_control, region_coordinates + + if change["new"]: + if rectangle is None: + latlon_bounds = get_computational_region_bb() + region_coordinates = { + "North": latlon_bounds[1][0], + "South": latlon_bounds[0][0], + "East": latlon_bounds[1][1], + "West": latlon_bounds[0][1], + } + rectangle = Rectangle( + bounds=latlon_bounds, + color="red", + fill_color="red", + fill_opacity=0.5, + draggable=True, + editable=True, + transform=True, + name="computational_region", + ) + rectangle.observe(on_rectangle_change, names="bounds") + self.map.add_layer(rectangle) + + else: + latlon_bounds = rectangle.bounds + rectangle.editable = True + rectangle.draggable = True + rectangle.transform = True + + save_button.disabled = False + bottom_output_widget.layout.display = "block" + update_output() + + if save_button_control is None: + save_button_control = WidgetControl( + widget=save_button, position="topright" + ) + self.map.add_control(save_button_control) + else: + self.map.add_control(save_button_control) + else: + if rectangle: + rectangle.editable = False + rectangle.draggable = False + + save_button.disabled = True + bottom_output_widget.layout.display = "none" + + if save_button_control: + self.map.remove_control(save_button_control) + + def save_region(change): + if rectangle: + update_output() + rectangle.name = "computational_region" + + region_mode_button.observe(toggle_region_mode, names="value") + save_button.on_click(save_region) + + region_mode_control = WidgetControl( + widget=region_mode_button, position="topright" + ) + self.map.add_control(region_mode_control) + + output_control = WidgetControl( + widget=bottom_output_widget, position="bottomright" ) - self.map.add_layer(rectangle) - - # def transform_bounds(bounds, from_crs, to_crs): - # project = - # pyproj.Transformer.from_crs(from_crs, to_crs, always_xy=True).transform - # transformed_bounds = transform(project, box(*bounds)).bounds - # return [[transformed_bounds[1], transformed_bounds[0]], - # [transformed_bounds[3], transformed_bounds[2]]] - - # def toggle_region_mode(change): - # nonlocal rectangle - - # if change["new"]: - # if rectangle is None: - # latlon_bounds = get_computational_region_bb() - # rectangle = ipyleaflet.Rectangle( - # bounds=latlon_bounds, - # color="red", - # fill_color="red", - # fill_opacity=0.5, - # draggable=True - # ) - # self.map.add_layer(rectangle) - # else: - # rectangle.editable = True - # rectangle.draggable = True - # else: - # if rectangle: - # rectangle.editable = False - # rectangle.draggable = False - # latlon_bounds = - # rectangle.bounds - # bounds = - # transform_bounds([latlon_bounds[0][1], latlon_bounds[0][0], - # latlon_bounds[1][1], latlon_bounds[1][0]], - # "epsg:4326", "epsg:3857") - # region_coordinates["North"] = bounds[3] - # region_coordinates["South"] = bounds[1] - # region_coordinates["East"] = bounds[2] - # region_coordinates["West"] = bounds[0] - - # with bottom_output_widget: - # bottom_output_widget.clear_output() - # print( - # f""" - # Saved Region coordinates: - # North={region_coordinates['North']}, - # South={region_coordinates['South']}, - # East={region_coordinates['East']}, - # West={region_coordinates['West']} - # """ - # ) - # self.map.remove_layer(rectangle) - # rectangle = None - - # region_mode_button.observe(toggle_region_mode, names="value") - - # region_mode_control = ipyleaflet.WidgetControl( - # widget=region_mode_button, position="topright" - # ) - # self.map.add_control(region_mode_control) - - # output_control = ipyleaflet.WidgetControl( - # widget=bottom_output_widget, position="bottomright" - # ) - # self.map.add_control(output_control) + self.map.add_control(output_control) def show(self): """This function returns a folium figure or ipyleaflet map object diff --git a/python/grass/jupyter/utils.py b/python/grass/jupyter/utils.py index fdfe008fca7..81c5a11a209 100644 --- a/python/grass/jupyter/utils.py +++ b/python/grass/jupyter/utils.py @@ -228,8 +228,8 @@ def get_computational_region_bb(): east_longitude = dms_to_dd(output_dict["east longitude"]) # Create tuples for SW and NE locations - sw_location = (west_longitude, south_latitude) - ne_location = (east_longitude, north_latitude) + sw_location = (south_latitude, west_longitude) + ne_location = (north_latitude, east_longitude) # Return the list of tuples return [sw_location, ne_location] From 1bcb17ab5476f1d4f7d94896a317bc7c89cf708f Mon Sep 17 00:00:00 2001 From: 29riyasaxena <29riyasaxena@gmail.com> Date: Tue, 2 Jul 2024 12:51:04 +0530 Subject: [PATCH 06/17] Display coordinates in CRS, update output widget and computational region --- python/grass/jupyter/interactivemap.py | 69 ++++++++++++++++++-------- python/grass/jupyter/utils.py | 40 ++++----------- 2 files changed, 59 insertions(+), 50 deletions(-) diff --git a/python/grass/jupyter/interactivemap.py b/python/grass/jupyter/interactivemap.py index 885856f63d9..b6aaee828d5 100644 --- a/python/grass/jupyter/interactivemap.py +++ b/python/grass/jupyter/interactivemap.py @@ -16,7 +16,12 @@ import base64 import json from .reprojection_renderer import ReprojectionRenderer -from .utils import get_computational_region_bb +from .utils import ( + get_computational_region_bb, + reproject_region, + update_region, + get_location_proj_string, +) def get_backend(interactive_map): @@ -349,6 +354,9 @@ def add_layer_control(self, **kwargs): self.layer_control_object = self._ipyleaflet.LayersControl(**kwargs) def draw_computational_region(self): + """ + Allow Users to draw the computational region and modify it. + """ import ipywidgets as widgets from ipyleaflet import Rectangle, WidgetControl @@ -361,9 +369,10 @@ def draw_computational_region(self): ) save_button = widgets.Button( - description="Save Region", + icon="save", button_style="success", - layout=widgets.Layout(width="100px", margin="0px 0px 0px 5px"), + tooltip="Click to save region", + layout=widgets.Layout(width="33px", margin="0px 0px 0px 0px"), disabled=True, ) @@ -378,6 +387,12 @@ def draw_computational_region(self): ) region_coordinates = {} + reprojected_region = { + "north": None, + "south": None, + "east": None, + "west": None, + } rectangle = None save_button_control = None @@ -385,22 +400,31 @@ def update_output(): with bottom_output_widget: bottom_output_widget.clear_output() print("Current Bounds:") - print(f"North: {region_coordinates['North']}") - print(f"South: {region_coordinates['South']}") - print(f"East: {region_coordinates['East']}") - print(f"West: {region_coordinates['West']}") - - def on_rectangle_change(event, property_name, value): - nonlocal region_coordinates - if property_name == "bounds": - latlon_bounds = value - region_coordinates = { - "North": latlon_bounds[1][0], - "South": latlon_bounds[0][0], - "East": latlon_bounds[1][1], - "West": latlon_bounds[0][1], - } - update_output() + print(f"North: {reprojected_region['north']}") + print(f"South: {reprojected_region['south']}") + print(f"East: {reprojected_region['east']}") + print(f"West: {reprojected_region['west']}") + + def on_rectangle_change(value): + nonlocal region_coordinates, reprojected_region + latlon_bounds = value["new"][0] + region_coordinates = { + "north": latlon_bounds[2]["lat"], + "south": latlon_bounds[0]["lat"], + "east": latlon_bounds[2]["lng"], + "west": latlon_bounds[0]["lng"], + } + from_proj = "+proj=longlat +datum=WGS84 +no_defs" + to_proj = get_location_proj_string() + reprojected_region = reproject_region( + region_coordinates, from_proj, to_proj + ) + update_region( + reprojected_region["north"], + reprojected_region["south"], + reprojected_region["east"], + reprojected_region["west"], + ) def toggle_region_mode(change): nonlocal rectangle, save_button_control, region_coordinates @@ -422,9 +446,10 @@ def toggle_region_mode(change): draggable=True, editable=True, transform=True, + rotation=False, name="computational_region", ) - rectangle.observe(on_rectangle_change, names="bounds") + rectangle.observe(on_rectangle_change, names="locations") self.map.add_layer(rectangle) else: @@ -432,10 +457,10 @@ def toggle_region_mode(change): rectangle.editable = True rectangle.draggable = True rectangle.transform = True + rectangle.rotation = False save_button.disabled = False bottom_output_widget.layout.display = "block" - update_output() if save_button_control is None: save_button_control = WidgetControl( @@ -448,6 +473,8 @@ def toggle_region_mode(change): if rectangle: rectangle.editable = False rectangle.draggable = False + rectangle.transform = False + rectangle.rotation = False save_button.disabled = True bottom_output_widget.layout.display = "none" diff --git a/python/grass/jupyter/utils.py b/python/grass/jupyter/utils.py index 81c5a11a209..3484ac98cdc 100644 --- a/python/grass/jupyter/utils.py +++ b/python/grass/jupyter/utils.py @@ -202,37 +202,19 @@ def get_rendering_size(region, width, height, default_width=600, default_height= return (default_width, round(default_width * region_height / region_width)) -def dms_to_dd(dms_str): - dms_str = dms_str.strip() - direction = dms_str[-1] - dms_str = dms_str[:-1] - degrees, minutes, seconds = map(float, dms_str.split(":")) - dd = degrees + minutes / 60 + seconds / 3600 - if direction in ["W", "S"]: - dd *= -1 - return dd +def get_computational_region_bb(): + """ + Gets the current computational region. + """ + region = gs.parse_command("g.region", flags="bg") + return [(region["ll_s"], region["ll_w"]), (region["ll_n"], region["ll_e"])] -def get_computational_region_bb(): - region = gs.read_command("g.region", flags="b") - output = gs.decode(region).splitlines() - output_dict = {} - - for line in output: - if line.strip(): - key, value = line.split(":", 1) - output_dict[key.strip()] = value.strip() - north_latitude = dms_to_dd(output_dict["north latitude"]) - south_latitude = dms_to_dd(output_dict["south latitude"]) - west_longitude = dms_to_dd(output_dict["west longitude"]) - east_longitude = dms_to_dd(output_dict["east longitude"]) - - # Create tuples for SW and NE locations - sw_location = (south_latitude, west_longitude) - ne_location = (north_latitude, east_longitude) - - # Return the list of tuples - return [sw_location, ne_location] +def update_region(north, south, east, west): + """ + Updates the GRASS GIS region using the given coordinates. + """ + gs.run_command("g.region", n=north, s=south, e=east, w=west) def save_gif( From 2ea876955bc4853b5a4b8e0d08e4232acce9f95c Mon Sep 17 00:00:00 2001 From: 29riyasaxena <29riyasaxena@gmail.com> Date: Wed, 3 Jul 2024 23:14:26 +0530 Subject: [PATCH 07/17] Add test for draw computational region --- .../jupyter/testsuite/interactivemap_test.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/python/grass/jupyter/testsuite/interactivemap_test.py b/python/grass/jupyter/testsuite/interactivemap_test.py index 665ccc72136..80836cdf31e 100644 --- a/python/grass/jupyter/testsuite/interactivemap_test.py +++ b/python/grass/jupyter/testsuite/interactivemap_test.py @@ -37,6 +37,16 @@ def can_import_folium(): return False +def can_import_ipyleaflet(): + """Test ipyleaflet import to see if test can be run.""" + try: + import ipyleaflet # noqa + + return True + except ImportError: + return False + + class TestDisplay(TestCase): # Setup variables files = [] @@ -88,6 +98,14 @@ def test_save_as_html(self): interactive_map.save(filename) self.assertFileExists(filename) + @unittest.skipIf(not can_import_ipyleaflet(), "Cannot import ipyleaflet") + def test_draw_computational_region(self): + """Test the draw_computational_region method.""" + # Create InteractiveMap + interactive_map = gj.InteractiveMap() + interactive_map.draw_computational_region() + self.assertTrue(callable(interactive_map.draw_computational_region)) + if __name__ == "__main__": test() From 74dec706eaf2fdc9b113b2da700a55e0f111e199 Mon Sep 17 00:00:00 2001 From: 29riyasaxena <29riyasaxena@gmail.com> Date: Thu, 4 Jul 2024 18:33:02 +0530 Subject: [PATCH 08/17] Add Changes --- python/grass/jupyter/interactivemap.py | 23 +++++++++++------------ python/grass/jupyter/utils.py | 10 ++++++++-- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/python/grass/jupyter/interactivemap.py b/python/grass/jupyter/interactivemap.py index b6aaee828d5..8957e1530b0 100644 --- a/python/grass/jupyter/interactivemap.py +++ b/python/grass/jupyter/interactivemap.py @@ -369,10 +369,9 @@ def draw_computational_region(self): ) save_button = widgets.Button( - icon="save", + description="Update Region", button_style="success", tooltip="Click to save region", - layout=widgets.Layout(width="33px", margin="0px 0px 0px 0px"), disabled=True, ) @@ -419,12 +418,7 @@ def on_rectangle_change(value): reprojected_region = reproject_region( region_coordinates, from_proj, to_proj ) - update_region( - reprojected_region["north"], - reprojected_region["south"], - reprojected_region["east"], - reprojected_region["west"], - ) + update_region(reprojected_region) def toggle_region_mode(change): nonlocal rectangle, save_button_control, region_coordinates @@ -433,10 +427,10 @@ def toggle_region_mode(change): if rectangle is None: latlon_bounds = get_computational_region_bb() region_coordinates = { - "North": latlon_bounds[1][0], - "South": latlon_bounds[0][0], - "East": latlon_bounds[1][1], - "West": latlon_bounds[0][1], + "north": latlon_bounds[1][0], + "south": latlon_bounds[0][0], + "east": latlon_bounds[1][1], + "west": latlon_bounds[0][1], } rectangle = Rectangle( bounds=latlon_bounds, @@ -458,6 +452,9 @@ def toggle_region_mode(change): rectangle.draggable = True rectangle.transform = True rectangle.rotation = False + rectangle.fill_opacity = 0.5 + rectangle.fill_color = "red" + rectangle.color = "red" save_button.disabled = False bottom_output_widget.layout.display = "block" @@ -475,6 +472,8 @@ def toggle_region_mode(change): rectangle.draggable = False rectangle.transform = False rectangle.rotation = False + rectangle.opacity = 0 + rectangle.fill_opacity = 0 save_button.disabled = True bottom_output_widget.layout.display = "none" diff --git a/python/grass/jupyter/utils.py b/python/grass/jupyter/utils.py index 3484ac98cdc..38a536e54c8 100644 --- a/python/grass/jupyter/utils.py +++ b/python/grass/jupyter/utils.py @@ -210,11 +210,17 @@ def get_computational_region_bb(): return [(region["ll_s"], region["ll_w"]), (region["ll_n"], region["ll_e"])] -def update_region(north, south, east, west): +def update_region(region_cordinates): """ Updates the GRASS GIS region using the given coordinates. """ - gs.run_command("g.region", n=north, s=south, e=east, w=west) + gs.run_command( + "g.region", + n=region_cordinates["north"], + s=region_cordinates["south"], + e=region_cordinates["east"], + w=region_cordinates["west"], + ) def save_gif( From c62486bbf67fb2ff65e8887c097e5a51affb098a Mon Sep 17 00:00:00 2001 From: 29riyasaxena <29riyasaxena@gmail.com> Date: Sun, 7 Jul 2024 12:19:21 +0530 Subject: [PATCH 09/17] Change solid-square button --- python/grass/jupyter/interactivemap.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/grass/jupyter/interactivemap.py b/python/grass/jupyter/interactivemap.py index 8957e1530b0..660fd255e64 100644 --- a/python/grass/jupyter/interactivemap.py +++ b/python/grass/jupyter/interactivemap.py @@ -361,7 +361,7 @@ def draw_computational_region(self): from ipyleaflet import Rectangle, WidgetControl region_mode_button = widgets.ToggleButton( - icon="square", + icon="square-o", value=False, tooltip="Click to add computational region", button_style="info", @@ -369,7 +369,7 @@ def draw_computational_region(self): ) save_button = widgets.Button( - description="Update Region", + description="Update region", button_style="success", tooltip="Click to save region", disabled=True, From 4c36b2fd5909c5cd4c6d31a21e6ef02b4f2797d6 Mon Sep 17 00:00:00 2001 From: 29riyasaxena <29riyasaxena@gmail.com> Date: Mon, 8 Jul 2024 14:06:56 +0530 Subject: [PATCH 10/17] Add resolution and improve display --- python/grass/jupyter/interactivemap.py | 17 +++++++++++++---- python/grass/jupyter/utils.py | 11 ++++++++--- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/python/grass/jupyter/interactivemap.py b/python/grass/jupyter/interactivemap.py index 660fd255e64..f876990a287 100644 --- a/python/grass/jupyter/interactivemap.py +++ b/python/grass/jupyter/interactivemap.py @@ -362,8 +362,9 @@ def draw_computational_region(self): region_mode_button = widgets.ToggleButton( icon="square-o", + description="", value=False, - tooltip="Click to add computational region", + tooltip="Click to show computational region", button_style="info", layout=widgets.Layout(width="33px", margin="0px 0px 0px 0px"), ) @@ -392,6 +393,7 @@ def draw_computational_region(self): "east": None, "west": None, } + res = {} rectangle = None save_button_control = None @@ -418,20 +420,27 @@ def on_rectangle_change(value): reprojected_region = reproject_region( region_coordinates, from_proj, to_proj ) - update_region(reprojected_region) + update_region(reprojected_region, res) def toggle_region_mode(change): - nonlocal rectangle, save_button_control, region_coordinates + nonlocal rectangle, save_button_control + nonlocal region_coordinates, reprojected_region, res if change["new"]: if rectangle is None: - latlon_bounds = get_computational_region_bb() + latlon_bounds, rregion, res = get_computational_region_bb() region_coordinates = { "north": latlon_bounds[1][0], "south": latlon_bounds[0][0], "east": latlon_bounds[1][1], "west": latlon_bounds[0][1], } + reprojected_region = { + "north": rregion[0], + "south": rregion[1], + "east": rregion[2], + "west": rregion[3], + } rectangle = Rectangle( bounds=latlon_bounds, color="red", diff --git a/python/grass/jupyter/utils.py b/python/grass/jupyter/utils.py index 38a536e54c8..302f5b73e1b 100644 --- a/python/grass/jupyter/utils.py +++ b/python/grass/jupyter/utils.py @@ -206,11 +206,14 @@ def get_computational_region_bb(): """ Gets the current computational region. """ - region = gs.parse_command("g.region", flags="bg") - return [(region["ll_s"], region["ll_w"]), (region["ll_n"], region["ll_e"])] + region = gs.parse_command("g.region", flags="gbp") + latlon = [(region["ll_s"], region["ll_w"]), (region["ll_n"], region["ll_e"])] + reprojected = [region["n"], region["s"], region["e"], region["w"]] + res = [region["nsres"], region["ewres"]] + return latlon, reprojected, res -def update_region(region_cordinates): +def update_region(region_cordinates, res): """ Updates the GRASS GIS region using the given coordinates. """ @@ -220,6 +223,8 @@ def update_region(region_cordinates): s=region_cordinates["south"], e=region_cordinates["east"], w=region_cordinates["west"], + nsres=res[0], + ewers=res[1], ) From a9d54909785b674187429602412c14d18be5bbf2 Mon Sep 17 00:00:00 2001 From: 29riyasaxena <29riyasaxena@gmail.com> Date: Thu, 11 Jul 2024 21:59:20 +0530 Subject: [PATCH 11/17] Add the zoom-in feature --- python/grass/jupyter/interactivemap.py | 34 +++++++++++++++++++++++++- python/grass/jupyter/utils.py | 13 +++++----- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/python/grass/jupyter/interactivemap.py b/python/grass/jupyter/interactivemap.py index f876990a287..af4314ec03e 100644 --- a/python/grass/jupyter/interactivemap.py +++ b/python/grass/jupyter/interactivemap.py @@ -415,12 +415,25 @@ def on_rectangle_change(value): "east": latlon_bounds[2]["lng"], "west": latlon_bounds[0]["lng"], } + rectangle.bounds = latlon_bounds from_proj = "+proj=longlat +datum=WGS84 +no_defs" to_proj = get_location_proj_string() reprojected_region = reproject_region( region_coordinates, from_proj, to_proj ) update_region(reprojected_region, res) + self.map.fit_bounds( + [ + [ + float(region_coordinates["south"]), + float(region_coordinates["west"]), + ], + [ + float(region_coordinates["north"]), + float(region_coordinates["east"]), + ], + ] + ) def toggle_region_mode(change): nonlocal rectangle, save_button_control @@ -454,9 +467,20 @@ def toggle_region_mode(change): ) rectangle.observe(on_rectangle_change, names="locations") self.map.add_layer(rectangle) + self.map.fit_bounds( + [ + [ + float(region_coordinates["south"]), + float(region_coordinates["west"]), + ], + [ + float(region_coordinates["north"]), + float(region_coordinates["east"]), + ], + ] + ) else: - latlon_bounds = rectangle.bounds rectangle.editable = True rectangle.draggable = True rectangle.transform = True @@ -464,6 +488,13 @@ def toggle_region_mode(change): rectangle.fill_opacity = 0.5 rectangle.fill_color = "red" rectangle.color = "red" + rectangle.opacity = 1 + self.map.fit_bounds( + [ + [rectangle.bounds[0]["lat"], rectangle.bounds[0]["lng"]], + [rectangle.bounds[2]["lat"], rectangle.bounds[2]["lng"]], + ] + ) save_button.disabled = False bottom_output_widget.layout.display = "block" @@ -475,6 +506,7 @@ def toggle_region_mode(change): self.map.add_control(save_button_control) else: self.map.add_control(save_button_control) + else: if rectangle: rectangle.editable = False diff --git a/python/grass/jupyter/utils.py b/python/grass/jupyter/utils.py index 302f5b73e1b..53990dfcfd7 100644 --- a/python/grass/jupyter/utils.py +++ b/python/grass/jupyter/utils.py @@ -213,18 +213,19 @@ def get_computational_region_bb(): return latlon, reprojected, res -def update_region(region_cordinates, res): +def update_region(region_coordinates, res): """ Updates the GRASS GIS region using the given coordinates. """ gs.run_command( "g.region", - n=region_cordinates["north"], - s=region_cordinates["south"], - e=region_cordinates["east"], - w=region_cordinates["west"], + flags="a", + n=region_coordinates["north"], + s=region_coordinates["south"], + e=region_coordinates["east"], + w=region_coordinates["west"], nsres=res[0], - ewers=res[1], + ewres=res[1], ) From 8c2d3b9d95b276b3e8784117e42f90f2d40aee7d Mon Sep 17 00:00:00 2001 From: 29riyasaxena <29riyasaxena@gmail.com> Date: Fri, 12 Jul 2024 19:03:02 +0530 Subject: [PATCH 12/17] Update zoom in --- python/grass/jupyter/interactivemap.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/python/grass/jupyter/interactivemap.py b/python/grass/jupyter/interactivemap.py index af4314ec03e..91c6a952836 100644 --- a/python/grass/jupyter/interactivemap.py +++ b/python/grass/jupyter/interactivemap.py @@ -422,18 +422,6 @@ def on_rectangle_change(value): region_coordinates, from_proj, to_proj ) update_region(reprojected_region, res) - self.map.fit_bounds( - [ - [ - float(region_coordinates["south"]), - float(region_coordinates["west"]), - ], - [ - float(region_coordinates["north"]), - float(region_coordinates["east"]), - ], - ] - ) def toggle_region_mode(change): nonlocal rectangle, save_button_control @@ -526,6 +514,12 @@ def save_region(change): if rectangle: update_output() rectangle.name = "computational_region" + self.map.fit_bounds( + [ + [rectangle.bounds[0]["lat"], rectangle.bounds[0]["lng"]], + [rectangle.bounds[2]["lat"], rectangle.bounds[2]["lng"]], + ] + ) region_mode_button.observe(toggle_region_mode, names="value") save_button.on_click(save_region) From d9fbf176d7367f424b418810173506ff06d370cb Mon Sep 17 00:00:00 2001 From: 29riyasaxena <29riyasaxena@gmail.com> Date: Sat, 13 Jul 2024 21:12:16 +0530 Subject: [PATCH 13/17] Update code --- python/grass/jupyter/interactivemap.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/python/grass/jupyter/interactivemap.py b/python/grass/jupyter/interactivemap.py index 91c6a952836..bdbdf91277a 100644 --- a/python/grass/jupyter/interactivemap.py +++ b/python/grass/jupyter/interactivemap.py @@ -358,7 +358,6 @@ def draw_computational_region(self): Allow Users to draw the computational region and modify it. """ import ipywidgets as widgets - from ipyleaflet import Rectangle, WidgetControl region_mode_button = widgets.ToggleButton( icon="square-o", @@ -442,7 +441,7 @@ def toggle_region_mode(change): "east": rregion[2], "west": rregion[3], } - rectangle = Rectangle( + rectangle = self._ipyleaflet.Rectangle( bounds=latlon_bounds, color="red", fill_color="red", @@ -488,7 +487,7 @@ def toggle_region_mode(change): bottom_output_widget.layout.display = "block" if save_button_control is None: - save_button_control = WidgetControl( + save_button_control = self._ipyleaflet.WidgetControl( widget=save_button, position="topright" ) self.map.add_control(save_button_control) @@ -524,12 +523,12 @@ def save_region(change): region_mode_button.observe(toggle_region_mode, names="value") save_button.on_click(save_region) - region_mode_control = WidgetControl( + region_mode_control = self._ipyleaflet.WidgetControl( widget=region_mode_button, position="topright" ) self.map.add_control(region_mode_control) - output_control = WidgetControl( + output_control = self._ipyleaflet.WidgetControl( widget=bottom_output_widget, position="bottomright" ) self.map.add_control(output_control) From 5d5050ac64ed9b3622a526deeed92eb22fbfdc8f Mon Sep 17 00:00:00 2001 From: Anna Petrasova Date: Thu, 18 Jul 2024 16:50:53 -0400 Subject: [PATCH 14/17] fix behavior, simplify code --- python/grass/jupyter/interactivemap.py | 177 ++++++++----------------- python/grass/jupyter/utils.py | 39 +++--- 2 files changed, 72 insertions(+), 144 deletions(-) diff --git a/python/grass/jupyter/interactivemap.py b/python/grass/jupyter/interactivemap.py index bdbdf91277a..c8a29ab0d3e 100644 --- a/python/grass/jupyter/interactivemap.py +++ b/python/grass/jupyter/interactivemap.py @@ -17,7 +17,7 @@ import json from .reprojection_renderer import ReprojectionRenderer from .utils import ( - get_computational_region_bb, + get_region_bounds_latlon, reproject_region, update_region, get_location_proj_string, @@ -308,6 +308,7 @@ def _import_ipyleaflet(error): # Set LayerControl default self.layer_control = False self.layer_control_object = None + self.region_rectangle = None self._renderer = ReprojectionRenderer( use_region=use_region, saved_region=saved_region @@ -363,162 +364,88 @@ def draw_computational_region(self): icon="square-o", description="", value=False, - tooltip="Click to show computational region", - button_style="info", - layout=widgets.Layout(width="33px", margin="0px 0px 0px 0px"), + tooltip="Click to show and edit computational region", + layout=widgets.Layout(width="40px", margin="0px 0px 0px 0px"), ) save_button = widgets.Button( description="Update region", - button_style="success", - tooltip="Click to save region", + tooltip="Click to update region", disabled=True, ) - bottom_output_widget = widgets.Output( layout={ "width": "100%", "max_height": "300px", - "max_width": "300px", "overflow": "auto", "display": "none", } ) - region_coordinates = {} - reprojected_region = { - "north": None, - "south": None, - "east": None, - "west": None, - } - res = {} - rectangle = None + changed_region = {} save_button_control = None - def update_output(): + def update_output(region): with bottom_output_widget: bottom_output_widget.clear_output() - print("Current Bounds:") - print(f"North: {reprojected_region['north']}") - print(f"South: {reprojected_region['south']}") - print(f"East: {reprojected_region['east']}") - print(f"West: {reprojected_region['west']}") + print( + _( + "Region changed to: n={n}, s={s}, e={e}, w={w} " + "nsres={nsres} ewres={ewres}" + ).format(**region) + ) def on_rectangle_change(value): - nonlocal region_coordinates, reprojected_region + save_button.disabled = False + bottom_output_widget.layout.display = "none" latlon_bounds = value["new"][0] - region_coordinates = { - "north": latlon_bounds[2]["lat"], - "south": latlon_bounds[0]["lat"], - "east": latlon_bounds[2]["lng"], - "west": latlon_bounds[0]["lng"], - } - rectangle.bounds = latlon_bounds - from_proj = "+proj=longlat +datum=WGS84 +no_defs" - to_proj = get_location_proj_string() - reprojected_region = reproject_region( - region_coordinates, from_proj, to_proj - ) - update_region(reprojected_region, res) + changed_region["north"] = latlon_bounds[2]["lat"] + changed_region["south"] = latlon_bounds[0]["lat"] + changed_region["east"] = latlon_bounds[2]["lng"] + changed_region["west"] = latlon_bounds[0]["lng"] def toggle_region_mode(change): - nonlocal rectangle, save_button_control - nonlocal region_coordinates, reprojected_region, res + nonlocal save_button_control if change["new"]: - if rectangle is None: - latlon_bounds, rregion, res = get_computational_region_bb() - region_coordinates = { - "north": latlon_bounds[1][0], - "south": latlon_bounds[0][0], - "east": latlon_bounds[1][1], - "west": latlon_bounds[0][1], - } - reprojected_region = { - "north": rregion[0], - "south": rregion[1], - "east": rregion[2], - "west": rregion[3], - } - rectangle = self._ipyleaflet.Rectangle( - bounds=latlon_bounds, - color="red", - fill_color="red", - fill_opacity=0.5, - draggable=True, - editable=True, - transform=True, - rotation=False, - name="computational_region", - ) - rectangle.observe(on_rectangle_change, names="locations") - self.map.add_layer(rectangle) - self.map.fit_bounds( - [ - [ - float(region_coordinates["south"]), - float(region_coordinates["west"]), - ], - [ - float(region_coordinates["north"]), - float(region_coordinates["east"]), - ], - ] - ) - - else: - rectangle.editable = True - rectangle.draggable = True - rectangle.transform = True - rectangle.rotation = False - rectangle.fill_opacity = 0.5 - rectangle.fill_color = "red" - rectangle.color = "red" - rectangle.opacity = 1 - self.map.fit_bounds( - [ - [rectangle.bounds[0]["lat"], rectangle.bounds[0]["lng"]], - [rectangle.bounds[2]["lat"], rectangle.bounds[2]["lng"]], - ] - ) - - save_button.disabled = False - bottom_output_widget.layout.display = "block" - - if save_button_control is None: - save_button_control = self._ipyleaflet.WidgetControl( - widget=save_button, position="topright" - ) - self.map.add_control(save_button_control) - else: - self.map.add_control(save_button_control) - + region_bounds = get_region_bounds_latlon() + self.region_rectangle = self._ipyleaflet.Rectangle( + bounds=region_bounds, + color="red", + fill_color="red", + fill_opacity=0.5, + draggable=True, + transform=True, + rotation=False, + name="Computational region", + ) + self.region_rectangle.observe(on_rectangle_change, names="locations") + self.map.fit_bounds(region_bounds) + self.map.add(self.region_rectangle) + + save_button_control = self._ipyleaflet.WidgetControl( + widget=save_button, position="topright" + ) + self.map.add_control(save_button_control) else: - if rectangle: - rectangle.editable = False - rectangle.draggable = False - rectangle.transform = False - rectangle.rotation = False - rectangle.opacity = 0 - rectangle.fill_opacity = 0 + if self.region_rectangle: + self.region_rectangle.transform = False + self.map.remove(self.region_rectangle) + self.region_rectangle = None save_button.disabled = True - bottom_output_widget.layout.display = "none" if save_button_control: - self.map.remove_control(save_button_control) + self.map.remove(save_button_control) + bottom_output_widget.layout.display = "none" def save_region(change): - if rectangle: - update_output() - rectangle.name = "computational_region" - self.map.fit_bounds( - [ - [rectangle.bounds[0]["lat"], rectangle.bounds[0]["lng"]], - [rectangle.bounds[2]["lat"], rectangle.bounds[2]["lng"]], - ] - ) + from_proj = "+proj=longlat +datum=WGS84 +no_defs" + to_proj = get_location_proj_string() + reprojected_region = reproject_region(changed_region, from_proj, to_proj) + new = update_region(reprojected_region) + bottom_output_widget.layout.display = "block" + update_output(new) region_mode_button.observe(toggle_region_mode, names="value") save_button.on_click(save_region) @@ -529,7 +456,7 @@ def save_region(change): self.map.add_control(region_mode_control) output_control = self._ipyleaflet.WidgetControl( - widget=bottom_output_widget, position="bottomright" + widget=bottom_output_widget, position="bottomleft" ) self.map.add_control(output_control) diff --git a/python/grass/jupyter/utils.py b/python/grass/jupyter/utils.py index 53990dfcfd7..20a77867176 100644 --- a/python/grass/jupyter/utils.py +++ b/python/grass/jupyter/utils.py @@ -202,31 +202,32 @@ def get_rendering_size(region, width, height, default_width=600, default_height= return (default_width, round(default_width * region_height / region_width)) -def get_computational_region_bb(): - """ - Gets the current computational region. - """ +def get_region_bounds_latlon(): + """Gets the current computational region bounds in latlon.""" region = gs.parse_command("g.region", flags="gbp") - latlon = [(region["ll_s"], region["ll_w"]), (region["ll_n"], region["ll_e"])] - reprojected = [region["n"], region["s"], region["e"], region["w"]] - res = [region["nsres"], region["ewres"]] - return latlon, reprojected, res + return [ + (float(region["ll_s"]), float(region["ll_w"])), + (float(region["ll_n"]), float(region["ll_e"])), + ] -def update_region(region_coordinates, res): - """ - Updates the GRASS GIS region using the given coordinates. +def update_region(region): + """Updates the computational region bounds. + + :return: the new region """ - gs.run_command( + current = gs.region() + new = gs.parse_command( "g.region", - flags="a", - n=region_coordinates["north"], - s=region_coordinates["south"], - e=region_coordinates["east"], - w=region_coordinates["west"], - nsres=res[0], - ewres=res[1], + flags="ga", + n=region["north"], + s=region["south"], + e=region["east"], + w=region["west"], + nsres=current["nsres"], + ewres=current["ewres"], ) + return new def save_gif( From 07a01159d8e450a43154efcb610464f2bf60de42 Mon Sep 17 00:00:00 2001 From: Anna Petrasova Date: Fri, 19 Jul 2024 17:30:50 -0400 Subject: [PATCH 15/17] address ruff; fix using obsolete ipyleaflet methods --- python/grass/jupyter/interactivemap.py | 6 +++--- python/grass/jupyter/utils.py | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/python/grass/jupyter/interactivemap.py b/python/grass/jupyter/interactivemap.py index c8a29ab0d3e..a72688a4649 100644 --- a/python/grass/jupyter/interactivemap.py +++ b/python/grass/jupyter/interactivemap.py @@ -426,7 +426,7 @@ def toggle_region_mode(change): save_button_control = self._ipyleaflet.WidgetControl( widget=save_button, position="topright" ) - self.map.add_control(save_button_control) + self.map.add(save_button_control) else: if self.region_rectangle: self.region_rectangle.transform = False @@ -453,12 +453,12 @@ def save_region(change): region_mode_control = self._ipyleaflet.WidgetControl( widget=region_mode_button, position="topright" ) - self.map.add_control(region_mode_control) + self.map.add(region_mode_control) output_control = self._ipyleaflet.WidgetControl( widget=bottom_output_widget, position="bottomleft" ) - self.map.add_control(output_control) + self.map.add(output_control) def show(self): """This function returns a folium figure or ipyleaflet map object diff --git a/python/grass/jupyter/utils.py b/python/grass/jupyter/utils.py index 20a77867176..caa0a339d22 100644 --- a/python/grass/jupyter/utils.py +++ b/python/grass/jupyter/utils.py @@ -217,7 +217,7 @@ def update_region(region): :return: the new region """ current = gs.region() - new = gs.parse_command( + return gs.parse_command( "g.region", flags="ga", n=region["north"], @@ -227,7 +227,6 @@ def update_region(region): nsres=current["nsres"], ewres=current["ewres"], ) - return new def save_gif( From ffbe986d0ed73392d5285d6a37948a0162c041ca Mon Sep 17 00:00:00 2001 From: Anna Petrasova Date: Sat, 20 Jul 2024 15:58:53 -0400 Subject: [PATCH 16/17] fix pylint errors --- python/grass/jupyter/interactivemap.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/grass/jupyter/interactivemap.py b/python/grass/jupyter/interactivemap.py index a72688a4649..f20d805b527 100644 --- a/python/grass/jupyter/interactivemap.py +++ b/python/grass/jupyter/interactivemap.py @@ -356,9 +356,9 @@ def add_layer_control(self, **kwargs): def draw_computational_region(self): """ - Allow Users to draw the computational region and modify it. + Allow users to draw the computational region and modify it. """ - import ipywidgets as widgets + import ipywidgets as widgets # pylint: disable=import-outside-toplevel region_mode_button = widgets.ToggleButton( icon="square-o", @@ -439,7 +439,7 @@ def toggle_region_mode(change): self.map.remove(save_button_control) bottom_output_widget.layout.display = "none" - def save_region(change): + def save_region(): from_proj = "+proj=longlat +datum=WGS84 +no_defs" to_proj = get_location_proj_string() reprojected_region = reproject_region(changed_region, from_proj, to_proj) From ac18de5fdade0c8ee6c4f6d90ba00087d599ec1e Mon Sep 17 00:00:00 2001 From: Anna Petrasova Date: Sun, 21 Jul 2024 15:42:44 -0400 Subject: [PATCH 17/17] fix unused argument --- python/grass/jupyter/interactivemap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/grass/jupyter/interactivemap.py b/python/grass/jupyter/interactivemap.py index fb5643e840a..561b2cdfbb2 100644 --- a/python/grass/jupyter/interactivemap.py +++ b/python/grass/jupyter/interactivemap.py @@ -438,7 +438,7 @@ def toggle_region_mode(change): self.map.remove(save_button_control) bottom_output_widget.layout.display = "none" - def save_region(): + def save_region(_change): from_proj = "+proj=longlat +datum=WGS84 +no_defs" to_proj = get_location_proj_string() reprojected_region = reproject_region(changed_region, from_proj, to_proj)