Skip to content

Commit 761afaa

Browse files
sufyanAbbasigiswqs
andauthored
Refactor the draw control (#1666)
* First pass implementation of AbstractDrawControl * Consolidate reset functionality * Better `None` checks on properties lookup. * Fix linter issues, safer property indexing. * Add docstrings to functions and classes * Fix remove drawn features bug --------- Co-authored-by: Sufyan Abbasi <[email protected]> Co-authored-by: Qiusheng Wu <[email protected]>
1 parent 1078bae commit 761afaa

File tree

3 files changed

+379
-149
lines changed

3 files changed

+379
-149
lines changed

geemap/geemap.py

Lines changed: 137 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,88 @@
3636
basemaps = Box(xyz_to_leaflet(), frozen_box=True)
3737

3838

39+
class MapDrawControl(ipyleaflet.DrawControl, map_widgets.AbstractDrawControl):
40+
""" "Implements the AbstractDrawControl for the map."""
41+
42+
_roi_start = False
43+
_roi_end = False
44+
45+
def __init__(self, host_map, **kwargs):
46+
"""Initialize the map draw control.
47+
48+
Args:
49+
host_map (geemap.Map): The geemap.Map object that the control will be added to.
50+
"""
51+
super(MapDrawControl, self).__init__(host_map=host_map, **kwargs)
52+
53+
@property
54+
def user_roi(self):
55+
"""Returns the last drawn geometry.
56+
57+
Returns:
58+
ee.Geometry: Last drawn geometry.
59+
"""
60+
return self.last_geometry
61+
62+
@property
63+
def user_rois(self):
64+
"""Returns all drawn geometries as an ee.FeatureCollection.
65+
66+
Returns:
67+
ee.FeatureCollection: All drawn geometries.
68+
"""
69+
return self.collection
70+
71+
# NOTE: Overridden for backwards compatibility, where edited geometries are
72+
# added to the layer instead of modified in place. Remove when
73+
# https://github.com/jupyter-widgets/ipyleaflet/issues/1119 is fixed to
74+
# allow geometry edits to be reflected on the tile layer.
75+
def _handle_geometry_edited(self, geo_json):
76+
return self._handle_geometry_created(geo_json)
77+
78+
def _get_synced_geojson_from_draw_control(self):
79+
return [data.copy() for data in self.data]
80+
81+
def _bind_to_draw_control(self):
82+
# Handles draw events
83+
def handle_draw(_, action, geo_json):
84+
try:
85+
self._roi_start = True
86+
if action == "created":
87+
self._handle_geometry_created(geo_json)
88+
elif action == "edited":
89+
self._handle_geometry_edited(geo_json)
90+
elif action == "deleted":
91+
self._handle_geometry_deleted(geo_json)
92+
self._roi_end = True
93+
self._roi_start = False
94+
except Exception as e:
95+
self.reset(clear_draw_control=False)
96+
self._roi_start = False
97+
self._roi_end = False
98+
print("There was an error creating Earth Engine Feature.")
99+
raise Exception(e)
100+
101+
self.on_draw(handle_draw)
102+
# NOTE: Uncomment the following code once
103+
# https://github.com/jupyter-widgets/ipyleaflet/issues/1119 is fixed
104+
# to allow edited geometries to be reflected instead of added.
105+
# def handle_data_update(_):
106+
# self._sync_geometries()
107+
# self.observe(handle_data_update, 'data')
108+
109+
def _remove_geometry_at_index_on_draw_control(self, index):
110+
# NOTE: Uncomment the following code once
111+
# https://github.com/jupyter-widgets/ipyleaflet/issues/1119 is fixed to
112+
# remove drawn geometries with `remove_last_drawn()`.
113+
# del self.data[index]
114+
# self.send_state(key='data')
115+
pass
116+
117+
def _clear_draw_control(self):
118+
return self.clear()
119+
120+
39121
class Map(ipyleaflet.Map):
40122
"""The Map class inherits the ipyleaflet Map class. The arguments you can pass to the Map initialization
41123
can be found at https://ipyleaflet.readthedocs.io/en/latest/map_and_basemaps/map.html.
@@ -46,6 +128,27 @@ class Map(ipyleaflet.Map):
46128
object: ipyleaflet map object.
47129
"""
48130

131+
# Map attributes for drawing features
132+
@property
133+
def draw_features(self):
134+
return self.draw_control.features if self.draw_control else []
135+
136+
@property
137+
def draw_last_feature(self):
138+
return self.draw_control.last_feature if self.draw_control else None
139+
140+
@property
141+
def draw_layer(self):
142+
return self.draw_control.layer if self.draw_control else None
143+
144+
@property
145+
def user_roi(self):
146+
return self.draw_control.user_roi if self.draw_control else None
147+
148+
@property
149+
def user_rois(self):
150+
return self.draw_control.user_rois if self.draw_control else None
151+
49152
def __init__(self, **kwargs):
50153
"""Initialize a map object. The following additional parameters can be passed in addition to the ipyleaflet.Map parameters:
51154
@@ -170,13 +273,6 @@ def __init__(self, **kwargs):
170273
if kwargs.get(control, True):
171274
self.add_controls(control, position="bottomright")
172275

173-
# Map attributes for drawing features
174-
self.draw_features = []
175-
self.draw_last_feature = None
176-
self.draw_layer = None
177-
self.user_roi = None
178-
self.user_rois = None
179-
180276
# Map attributes for layers
181277
self.geojson_layers = []
182278
self.ee_layers = []
@@ -2467,7 +2563,9 @@ def _on_close():
24672563
self.inspector_control.close()
24682564
self.inspector_control = None
24692565

2470-
inspector = map_widgets.Inspector(self, names, visible, decimals, opened, show_close_button)
2566+
inspector = map_widgets.Inspector(
2567+
self, names, visible, decimals, opened, show_close_button
2568+
)
24712569
inspector.on_close = _on_close
24722570
self.inspector_control = ipyleaflet.WidgetControl(
24732571
widget=inspector, position=position
@@ -2500,8 +2598,8 @@ def add_draw_control(self, position="topleft"):
25002598
Args:
25012599
position (str, optional): The position of the draw control. Defaults to "topleft".
25022600
"""
2503-
2504-
draw_control = ipyleaflet.DrawControl(
2601+
draw_control = MapDrawControl(
2602+
host_map=self,
25052603
marker={"shapeOptions": {"color": "#3388ff"}},
25062604
rectangle={"shapeOptions": {"color": "#3388ff"}},
25072605
# circle={"shapeOptions": {"color": "#3388ff"}},
@@ -2510,50 +2608,6 @@ def add_draw_control(self, position="topleft"):
25102608
remove=True,
25112609
position=position,
25122610
)
2513-
2514-
# Handles draw events
2515-
def handle_draw(target, action, geo_json):
2516-
try:
2517-
self._roi_start = True
2518-
geom = geojson_to_ee(geo_json, False)
2519-
self.user_roi = geom
2520-
feature = ee.Feature(geom)
2521-
self.draw_last_feature = feature
2522-
if not hasattr(self, "_draw_count"):
2523-
self._draw_count = 0
2524-
if action == "deleted" and len(self.draw_features) > 0:
2525-
self.draw_features.remove(feature)
2526-
self._draw_count -= 1
2527-
else:
2528-
self.draw_features.append(feature)
2529-
self._draw_count += 1
2530-
collection = ee.FeatureCollection(self.draw_features)
2531-
self.user_rois = collection
2532-
ee_draw_layer = EELeafletTileLayer(
2533-
collection, {"color": "blue"}, "Drawn Features", False, 0.5
2534-
)
2535-
draw_layer_index = self.find_layer_index("Drawn Features")
2536-
2537-
if draw_layer_index == -1:
2538-
self.add(ee_draw_layer)
2539-
self.draw_layer = ee_draw_layer
2540-
else:
2541-
self.substitute_layer(self.draw_layer, ee_draw_layer)
2542-
self.draw_layer = ee_draw_layer
2543-
self._roi_end = True
2544-
self._roi_start = False
2545-
except Exception as e:
2546-
self._draw_count = 0
2547-
self.draw_features = []
2548-
self.draw_last_feature = None
2549-
self.draw_layer = None
2550-
self.user_roi = None
2551-
self._roi_start = False
2552-
self._roi_end = False
2553-
print("There was an error creating Earth Engine Feature.")
2554-
raise Exception(e)
2555-
2556-
draw_control.on_draw(handle_draw)
25572611
self.add(draw_control)
25582612
self.draw_control = draw_control
25592613

@@ -2585,8 +2639,11 @@ def add_toolbar(self, position="topright", **kwargs):
25852639
"""
25862640

25872641
from .toolbar import Toolbar, main_tools, extra_tools
2642+
25882643
self._toolbar = Toolbar(self, main_tools, extra_tools)
2589-
toolbar_control = ipyleaflet.WidgetControl(widget=self._toolbar, position=position)
2644+
toolbar_control = ipyleaflet.WidgetControl(
2645+
widget=self._toolbar, position=position
2646+
)
25902647
self.add(toolbar_control)
25912648

25922649
def add_plot_gui(self, position="topright", **kwargs):
@@ -4060,46 +4117,37 @@ def add_remote_tile(
40604117
else:
40614118
raise Exception("The source must be a URL.")
40624119

4120+
def remove_draw_control(self):
4121+
"""Removes the draw control from the map"""
4122+
controls = []
4123+
old_draw_control = None
4124+
for control in self.controls:
4125+
if isinstance(control, MapDrawControl):
4126+
old_draw_control = control
4127+
4128+
else:
4129+
controls.append(control)
4130+
4131+
self.controls = tuple(controls)
4132+
if old_draw_control:
4133+
old_draw_control.close()
4134+
40634135
def remove_drawn_features(self):
40644136
"""Removes user-drawn geometries from the map"""
4065-
if self.draw_layer is not None:
4066-
self.remove_layer(self.draw_layer)
4067-
self._draw_count = 0
4068-
self.draw_features = []
4069-
self.draw_last_feature = None
4070-
self.draw_layer = None
4071-
self.user_roi = None
4072-
self.user_rois = None
4073-
self._chart_values = []
4074-
self._chart_points = []
4075-
self._chart_labels = None
40764137
if self.draw_control is not None:
4077-
self.draw_control.clear()
4138+
self.draw_control.reset()
40784139

40794140
def remove_last_drawn(self):
4080-
"""Removes user-drawn geometries from the map"""
4081-
if self.draw_layer is not None:
4082-
collection = ee.FeatureCollection(self.draw_features[:-1])
4083-
ee_draw_layer = EELeafletTileLayer(
4084-
collection, {"color": "blue"}, "Drawn Features", True, 0.5
4085-
)
4086-
if self._draw_count == 1:
4141+
"""Removes last user-drawn geometry from the map"""
4142+
if self.draw_control is not None:
4143+
if self.draw_control.count == 1:
40874144
self.remove_drawn_features()
4088-
else:
4089-
self.substitute_layer(self.draw_layer, ee_draw_layer)
4090-
self.draw_layer = ee_draw_layer
4091-
self._draw_count -= 1
4092-
self.draw_features = self.draw_features[:-1]
4093-
self.draw_last_feature = self.draw_features[-1]
4094-
self.draw_layer = ee_draw_layer
4095-
self.user_roi = ee.Feature(
4096-
collection.toList(collection.size()).get(
4097-
collection.size().subtract(1)
4098-
)
4099-
).geometry()
4100-
self.user_rois = collection
4101-
self._chart_values = self._chart_values[:-1]
4102-
self._chart_points = self._chart_points[:-1]
4145+
elif self.draw_control.count:
4146+
self.draw_control.remove_geometry(self.draw_control.geometries[-1])
4147+
if hasattr(self, "_chart_values"):
4148+
self._chart_values = self._chart_values[:-1]
4149+
if hasattr(self, "_chart_points"):
4150+
self._chart_points = self._chart_points[:-1]
41034151
# self._chart_labels = None
41044152

41054153
def extract_values_to_points(self, filename):

0 commit comments

Comments
 (0)