Skip to content

Commit

Permalink
Added edit vector GUI opengeos#179
Browse files Browse the repository at this point in the history
Former-commit-id: 77daa2e
  • Loading branch information
giswqs committed Jan 25, 2022
1 parent 8b800b1 commit fb45fd9
Show file tree
Hide file tree
Showing 2 changed files with 315 additions and 9 deletions.
125 changes: 119 additions & 6 deletions leafmap/leafmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def __init__(self, **kwargs):
self.draw_features = []
self.api_keys = {}
self.geojson_layers = []
self.edit_mode = False

# sandbox path for Voila app to restrict access to system directories.
if "sandbox_path" not in kwargs:
Expand Down Expand Up @@ -101,12 +102,33 @@ def __init__(self, **kwargs):
self.add_control(draw_control)
self.draw_control = draw_control

draw_output = widgets.Output()
control = ipyleaflet.WidgetControl(
widget=draw_output, position="bottomright"
)
self.add_control(control)

def handle_draw(target, action, geo_json):
if "style" in geo_json["properties"]:
del geo_json["properties"]["style"]
self.user_roi = geo_json
if action == "deleted" and len(self.draw_features) > 0:
self.draw_features.remove(geo_json)
else:

if action in ["created", "edited"]:
feature = {
"type": "Feature",
"geometry": geo_json["geometry"],
}
self.draw_features.append(geo_json)
elif action == "deleted":
geometries = [
feature["geometry"] for feature in self.draw_control.data
]
for geom in geometries:
if geom == geo_json["geometry"]:
geometries.remove(geom)
for feature in self.draw_features:
if feature["geometry"] not in geometries:
self.draw_features.remove(feature)
self.user_rois = {
"type": "FeatureCollection",
"features": self.draw_features,
Expand Down Expand Up @@ -2731,7 +2753,7 @@ def get_pc_collections(self):
if not hasattr(self, "pc_collections"):
setattr(self, "pc_collections", get_pc_collections())

def save_draw_features(self, out_file, indent=4):
def save_draw_features(self, out_file, indent=4, **kwargs):
"""Save the draw features to a file.
Args:
Expand All @@ -2740,10 +2762,101 @@ def save_draw_features(self, out_file, indent=4):
"""
import json

self.update_draw_features()
out_file = check_file_path(out_file)

geojson = {
"type": "FeatureCollection",
"features": self.draw_features,
}
with open(out_file, "w") as f:
json.dump(self.user_rois, f, indent=indent)
if indent is None:
json.dump(geojson, f, **kwargs)
else:
json.dump(geojson, f, indent=indent, **kwargs)

def last_edit_data(self):

import pandas as pd

df = pd.DataFrame(
{
"Key": [""],
"Value": [""],
}
)
if self.draw_control.last_action == "edited":
properties = self.draw_control.last_draw["properties"]
if "style" in properties:
properties.pop("style")
print(properties)
df = pd.DataFrame(
{"Key": list(properties.keys()), "Value": list(properties.values())}
)
elif (
self.draw_control.last_action == "created"
and len(self.draw_control.data) > 1
):
print(len(self.draw_control.data))
print(self.draw_control.data)
properties = self.draw_control.data[-2]["properties"]
if "style" in properties:
properties.pop("style")
print(properties)
df = pd.DataFrame(
{"Key": list(properties.keys()), "Value": [""] * len(properties)}
)
return df

def update_draw_features(self):
"""Update the draw features by removing features that have been edited and no longer exist."""

geometries = [feature["geometry"] for feature in self.draw_control.data]

for feature in self.draw_features:
if feature["geometry"] not in geometries:
self.draw_features.remove(feature)

def get_draw_props(self, n=None, return_df=False):
"""Get the properties of the draw features.
Args:
n (int, optional): The maximum number of attributes to return. Defaults to None.
return_df (bool, optional): If True, return a pandas dataframe. Defaults to False.
Returns:
pd.DataFrame: A pandas dataframe containing the properties of the draw features.
"""

import pandas as pd

props = None
if self.draw_control.last_action == "edited":
self.update_draw_features()
if len(self.draw_features) > 0:
keys = self.draw_features[-1]["properties"].keys()
if len(keys) > 0:
props = list(keys)

if props is not None:
if n is not None and n <= len(props):
n = len(props)
elif n is not None and n > len(props):
props = props + [""] * (n - len(props))

if not return_df:
return props
else:

df = pd.DataFrame({"Key": props, "Value": [""] * len(props)})
df.index += 1
return df
else:
if not return_df:
return []
else:
df = pd.DataFrame({"Key": [""] * n, "Value": [""] * n})
df.index += 1
return df


# The functions below are outside the Map class.
Expand Down
199 changes: 196 additions & 3 deletions leafmap/toolbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,10 +264,14 @@ def main_toolbar(m):
"name": "attribute_table",
"tooltip": "Open attribute table",
},
"smile-o": {
"name": "placeholder2",
"tooltip": "This is a placeholder",
"pencil-square-o": {
"name": "edit_vector",
"tooltip": "Edit vector data attribute table",
},
# "smile-o": {
# "name": "placeholder2",
# "tooltip": "This is a placeholder",
# },
"spinner": {
"name": "placeholder2",
"tooltip": "This is a placeholder",
Expand Down Expand Up @@ -372,6 +376,8 @@ def tool_callback(change):
search_geojson_gui(m)
elif tool_name == "attribute_table":
select_table_gui(m)
elif tool_name == "edit_vector":
edit_draw_gui(m)
elif tool_name == "help":
import webbrowser

Expand Down Expand Up @@ -4053,3 +4059,190 @@ def close_btn_click(change):
m.table_control = table_control
else:
return toolbar_widget


def edit_draw_gui(m):
"""Generates a tool GUI for editing vector data attribute table.
Args:
m (leafmap.Map, optional): The leaflet Map object. Defaults to None.
Returns:
ipywidgets: The tool GUI widget.
"""
import ipysheet
import pandas as pd

widget_width = "250px"
padding = "0px 0px 0px 5px" # upper, right, bottom, left
style = {"description_width": "initial"}
m.edit_mode = True

n_props = len(m.get_draw_props())
if n_props == 0:
n_props = 1

toolbar_button = widgets.ToggleButton(
value=False,
tooltip="Edit attribute table",
icon="pencil-square-o",
layout=widgets.Layout(width="28px", height="28px", padding="0px 0px 0px 4px"),
)

close_button = widgets.ToggleButton(
value=False,
tooltip="Close the tool",
icon="times",
button_style="primary",
layout=widgets.Layout(height="28px", width="28px", padding="0px 0px 0px 4px"),
)

save_button = widgets.ToggleButton(
value=False,
tooltip="Save to file",
icon="floppy-o",
# button_style="primary",
layout=widgets.Layout(height="28px", width="28px", padding="0px 0px 0px 4px"),
)

int_slider = widgets.IntSlider(
min=n_props,
max=n_props + 10,
description="Attributes:",
readout=False,
continuous_update=True,
layout=widgets.Layout(width="135px", padding=padding),
style=style,
)

int_slider_label = widgets.Label()
widgets.jslink((int_slider, "value"), (int_slider_label, "value"))

def int_slider_changed(change):
if change["new"]:
with output:
output.clear_output()
sheet = ipysheet.from_dataframe(
m.get_draw_props(n=int_slider.value, return_df=True)
)
display(sheet)

int_slider.observe(int_slider_changed, "value")

buttons = widgets.ToggleButtons(
value=None,
options=["Apply", "Reset", "Close"],
tooltips=["Apply", "Reset", "Close"],
button_style="primary",
)
buttons.style.button_width = "60px"

output = widgets.Output(layout=widgets.Layout(width=widget_width, padding=padding))
m.edit_output = output

with output:
sheet = ipysheet.from_dataframe(
m.get_draw_props(n=int_slider.value, return_df=True)
)
output.clear_output()
display(sheet)

toolbar_widget = widgets.VBox()
toolbar_widget.children = [toolbar_button]
toolbar_header = widgets.HBox()
toolbar_header.children = [
close_button,
toolbar_button,
save_button,
int_slider,
int_slider_label,
]
toolbar_footer = widgets.VBox()
toolbar_footer.children = [
output,
buttons,
]

toolbar_event = ipyevents.Event(
source=toolbar_widget, watched_events=["mouseenter", "mouseleave"]
)

def handle_toolbar_event(event):

if event["type"] == "mouseenter":
toolbar_widget.children = [toolbar_header, toolbar_footer]
elif event["type"] == "mouseleave":
if not toolbar_button.value:
toolbar_widget.children = [toolbar_button]
toolbar_button.value = False
close_button.value = False

toolbar_event.on_dom_event(handle_toolbar_event)

def toolbar_btn_click(change):
if change["new"]:
close_button.value = False
toolbar_widget.children = [toolbar_header, toolbar_footer]
else:
if not close_button.value:
toolbar_widget.children = [toolbar_button]

toolbar_button.observe(toolbar_btn_click, "value")

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
m.edit_mode = False
toolbar_widget.close()

close_button.observe(close_btn_click, "value")

def save_btn_click(change):
if change["new"]:
save_button.value = False
m.save_draw_features("roi.geojson", indent=None)
with output:
output.clear_output()
print("Saved to 'roi.geojson'")

save_button.observe(save_btn_click, "value")

def button_clicked(change):
if change["new"] == "Apply":
with output:
output.clear_output()
sheet = ipysheet.sheet(
rows=m.num_attributes, columns=2, column_headers=["Key", "Value"]
)
display(sheet)
elif change["new"] == "Reset":
output.clear_output()
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
m.edit_mode = False
toolbar_widget.close()

buttons.value = None

buttons.observe(button_clicked, "value")

toolbar_button.value = True
if m is not None:
toolbar_control = ipyleaflet.WidgetControl(
widget=toolbar_widget, position="topright"
)

if toolbar_control not in m.controls:
m.add_control(toolbar_control)
m.tool_control = toolbar_control
else:
return toolbar_widget

0 comments on commit fb45fd9

Please sign in to comment.