geeltermap 0.1.0__py3-none-any.whl → 0.1.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- geeltermap/__init__.py +2 -2
- geeltermap/core.py +870 -0
- geeltermap/ee_tile_layers.py +231 -0
- geeltermap/geeltermap.py +2 -0
- geeltermap/map_widgets.py +2222 -0
- geeltermap/toolbar.py +35 -25
- {geeltermap-0.1.0.dist-info → geeltermap-0.1.8.dist-info}/METADATA +5 -5
- {geeltermap-0.1.0.dist-info → geeltermap-0.1.8.dist-info}/RECORD +10 -7
- {geeltermap-0.1.0.dist-info → geeltermap-0.1.8.dist-info}/WHEEL +1 -1
- {geeltermap-0.1.0.dist-info → geeltermap-0.1.8.dist-info}/LICENSE +0 -0
geeltermap/core.py
ADDED
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
"""A generic Map interface and lightweight implementation."""
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
import logging
|
|
5
|
+
import math
|
|
6
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple, Type
|
|
7
|
+
|
|
8
|
+
import ee
|
|
9
|
+
import ipyleaflet
|
|
10
|
+
import ipywidgets
|
|
11
|
+
|
|
12
|
+
from . import basemaps
|
|
13
|
+
from . import common
|
|
14
|
+
from . import ee_tile_layers
|
|
15
|
+
from . import map_widgets
|
|
16
|
+
from . import toolbar
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DrawActions(enum.Enum):
|
|
20
|
+
"""Action types for the draw control.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
enum (str): Action type.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
CREATED = "created"
|
|
27
|
+
EDITED = "edited"
|
|
28
|
+
DELETED = "deleted"
|
|
29
|
+
REMOVED_LAST = "removed-last"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AbstractDrawControl(object):
|
|
33
|
+
"""Abstract class for the draw control."""
|
|
34
|
+
|
|
35
|
+
host_map = None
|
|
36
|
+
layer = None
|
|
37
|
+
geometries = []
|
|
38
|
+
properties = []
|
|
39
|
+
last_geometry = None
|
|
40
|
+
last_draw_action = None
|
|
41
|
+
_geometry_create_dispatcher = ipywidgets.CallbackDispatcher()
|
|
42
|
+
_geometry_edit_dispatcher = ipywidgets.CallbackDispatcher()
|
|
43
|
+
_geometry_delete_dispatcher = ipywidgets.CallbackDispatcher()
|
|
44
|
+
|
|
45
|
+
def __init__(self, host_map):
|
|
46
|
+
"""Initialize the draw control.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
host_map (geemap.Map): The geemap.Map instance to be linked with the draw control.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
self.host_map = host_map
|
|
53
|
+
self.layer = None
|
|
54
|
+
self.geometries = []
|
|
55
|
+
self.properties = []
|
|
56
|
+
self.last_geometry = None
|
|
57
|
+
self.last_draw_action = None
|
|
58
|
+
self._geometry_create_dispatcher = ipywidgets.CallbackDispatcher()
|
|
59
|
+
self._geometry_edit_dispatcher = ipywidgets.CallbackDispatcher()
|
|
60
|
+
self._geometry_delete_dispatcher = ipywidgets.CallbackDispatcher()
|
|
61
|
+
self._bind_to_draw_control()
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def features(self):
|
|
65
|
+
if self.count:
|
|
66
|
+
features = []
|
|
67
|
+
for i, geometry in enumerate(self.geometries):
|
|
68
|
+
if i < len(self.properties):
|
|
69
|
+
property = self.properties[i]
|
|
70
|
+
else:
|
|
71
|
+
property = None
|
|
72
|
+
features.append(ee.Feature(geometry, property))
|
|
73
|
+
return features
|
|
74
|
+
else:
|
|
75
|
+
return []
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def collection(self):
|
|
79
|
+
return ee.FeatureCollection(self.features if self.count else [])
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def last_feature(self):
|
|
83
|
+
property = self.get_geometry_properties(self.last_geometry)
|
|
84
|
+
return ee.Feature(self.last_geometry, property) if self.last_geometry else None
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def count(self):
|
|
88
|
+
return len(self.geometries)
|
|
89
|
+
|
|
90
|
+
def reset(self, clear_draw_control=True):
|
|
91
|
+
"""Resets the draw controls."""
|
|
92
|
+
if self.layer is not None:
|
|
93
|
+
self.host_map.remove_layer(self.layer)
|
|
94
|
+
self.geometries = []
|
|
95
|
+
self.properties = []
|
|
96
|
+
self.last_geometry = None
|
|
97
|
+
self.layer = None
|
|
98
|
+
if clear_draw_control:
|
|
99
|
+
self._clear_draw_control()
|
|
100
|
+
|
|
101
|
+
def remove_geometry(self, geometry):
|
|
102
|
+
"""Removes a geometry from the draw control."""
|
|
103
|
+
if not geometry:
|
|
104
|
+
return
|
|
105
|
+
try:
|
|
106
|
+
index = self.geometries.index(geometry)
|
|
107
|
+
except ValueError:
|
|
108
|
+
return
|
|
109
|
+
if index >= 0:
|
|
110
|
+
del self.geometries[index]
|
|
111
|
+
del self.properties[index]
|
|
112
|
+
self._remove_geometry_at_index_on_draw_control(index)
|
|
113
|
+
if index == self.count and geometry == self.last_geometry:
|
|
114
|
+
# Treat this like an "undo" of the last drawn geometry.
|
|
115
|
+
if len(self.geometries):
|
|
116
|
+
self.last_geometry = self.geometries[-1]
|
|
117
|
+
else:
|
|
118
|
+
self.last_geometry = geometry
|
|
119
|
+
self.last_draw_action = DrawActions.REMOVED_LAST
|
|
120
|
+
if self.layer is not None:
|
|
121
|
+
self._redraw_layer()
|
|
122
|
+
|
|
123
|
+
def get_geometry_properties(self, geometry):
|
|
124
|
+
"""Gets the properties of a geometry."""
|
|
125
|
+
if not geometry:
|
|
126
|
+
return None
|
|
127
|
+
try:
|
|
128
|
+
index = self.geometries.index(geometry)
|
|
129
|
+
except ValueError:
|
|
130
|
+
return None
|
|
131
|
+
if index >= 0:
|
|
132
|
+
return self.properties[index]
|
|
133
|
+
else:
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
def set_geometry_properties(self, geometry, property):
|
|
137
|
+
"""Sets the properties of a geometry."""
|
|
138
|
+
if not geometry:
|
|
139
|
+
return
|
|
140
|
+
try:
|
|
141
|
+
index = self.geometries.index(geometry)
|
|
142
|
+
except ValueError:
|
|
143
|
+
return
|
|
144
|
+
if index >= 0:
|
|
145
|
+
self.properties[index] = property
|
|
146
|
+
|
|
147
|
+
def on_geometry_create(self, callback, remove=False):
|
|
148
|
+
self._geometry_create_dispatcher.register_callback(callback, remove=remove)
|
|
149
|
+
|
|
150
|
+
def on_geometry_edit(self, callback, remove=False):
|
|
151
|
+
self._geometry_edit_dispatcher.register_callback(callback, remove=remove)
|
|
152
|
+
|
|
153
|
+
def on_geometry_delete(self, callback, remove=False):
|
|
154
|
+
self._geometry_delete_dispatcher.register_callback(callback, remove=remove)
|
|
155
|
+
|
|
156
|
+
def _bind_to_draw_control(self):
|
|
157
|
+
"""Set up draw control event handling like create, edit, and delete."""
|
|
158
|
+
raise NotImplementedError()
|
|
159
|
+
|
|
160
|
+
def _remove_geometry_at_index_on_draw_control(self):
|
|
161
|
+
"""Remove the geometry at the given index on the draw control."""
|
|
162
|
+
raise NotImplementedError()
|
|
163
|
+
|
|
164
|
+
def _clear_draw_control(self):
|
|
165
|
+
"""Clears the geometries from the draw control."""
|
|
166
|
+
raise NotImplementedError()
|
|
167
|
+
|
|
168
|
+
def _get_synced_geojson_from_draw_control(self):
|
|
169
|
+
"""Returns an up-to-date list of GeoJSON from the draw control."""
|
|
170
|
+
raise NotImplementedError()
|
|
171
|
+
|
|
172
|
+
def _sync_geometries(self):
|
|
173
|
+
"""Sync the local geometries with those from the draw control."""
|
|
174
|
+
if not self.count:
|
|
175
|
+
return
|
|
176
|
+
# The current geometries from the draw_control.
|
|
177
|
+
test_geojsons = self._get_synced_geojson_from_draw_control()
|
|
178
|
+
i = 0
|
|
179
|
+
while i < self.count and i < len(test_geojsons):
|
|
180
|
+
local_geometry = None
|
|
181
|
+
test_geometry = None
|
|
182
|
+
while i < self.count and i < len(test_geojsons):
|
|
183
|
+
local_geometry = self.geometries[i]
|
|
184
|
+
test_geometry = common.geojson_to_ee(test_geojsons[i], geodesic=False)
|
|
185
|
+
if test_geometry == local_geometry:
|
|
186
|
+
i += 1
|
|
187
|
+
else:
|
|
188
|
+
break
|
|
189
|
+
if i < self.count and test_geometry is not None:
|
|
190
|
+
self.geometries[i] = test_geometry
|
|
191
|
+
if self.layer is not None:
|
|
192
|
+
self._redraw_layer()
|
|
193
|
+
|
|
194
|
+
def _redraw_layer(self):
|
|
195
|
+
if self.host_map:
|
|
196
|
+
self.host_map.add_layer(
|
|
197
|
+
self.collection, {"color": "blue"}, "Drawn Features", False, 0.5
|
|
198
|
+
)
|
|
199
|
+
self.layer = self.host_map.ee_layers.get("Drawn Features", {}).get(
|
|
200
|
+
"ee_layer", None
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def _handle_geometry_created(self, geo_json):
|
|
204
|
+
geometry = common.geojson_to_ee(geo_json, geodesic=False)
|
|
205
|
+
self.last_geometry = geometry
|
|
206
|
+
self.last_draw_action = DrawActions.CREATED
|
|
207
|
+
self.geometries.append(geometry)
|
|
208
|
+
self.properties.append(None)
|
|
209
|
+
self._redraw_layer()
|
|
210
|
+
self._geometry_create_dispatcher(self, geometry=geometry)
|
|
211
|
+
|
|
212
|
+
def _handle_geometry_edited(self, geo_json):
|
|
213
|
+
geometry = common.geojson_to_ee(geo_json, geodesic=False)
|
|
214
|
+
self.last_geometry = geometry
|
|
215
|
+
self.last_draw_action = DrawActions.EDITED
|
|
216
|
+
self._sync_geometries()
|
|
217
|
+
self._redraw_layer()
|
|
218
|
+
self._geometry_edit_dispatcher(self, geometry=geometry)
|
|
219
|
+
|
|
220
|
+
def _handle_geometry_deleted(self, geo_json):
|
|
221
|
+
geometry = common.geojson_to_ee(geo_json, geodesic=False)
|
|
222
|
+
self.last_geometry = geometry
|
|
223
|
+
self.last_draw_action = DrawActions.DELETED
|
|
224
|
+
try:
|
|
225
|
+
index = self.geometries.index(geometry)
|
|
226
|
+
except ValueError:
|
|
227
|
+
return
|
|
228
|
+
if index >= 0:
|
|
229
|
+
del self.geometries[index]
|
|
230
|
+
del self.properties[index]
|
|
231
|
+
self._redraw_layer()
|
|
232
|
+
self._geometry_delete_dispatcher(self, geometry=geometry)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class MapDrawControl(ipyleaflet.DrawControl, AbstractDrawControl):
|
|
236
|
+
"""Implements the AbstractDrawControl for ipleaflet Map."""
|
|
237
|
+
|
|
238
|
+
def __init__(self, host_map, **kwargs):
|
|
239
|
+
"""Initialize the map draw control.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
host_map (geemap.Map): The geemap.Map object that the control will be added to.
|
|
243
|
+
"""
|
|
244
|
+
super(MapDrawControl, self).__init__(host_map=host_map, **kwargs)
|
|
245
|
+
|
|
246
|
+
# NOTE: Overridden for backwards compatibility, where edited geometries are
|
|
247
|
+
# added to the layer instead of modified in place. Remove when
|
|
248
|
+
# https://github.com/jupyter-widgets/ipyleaflet/issues/1119 is fixed to
|
|
249
|
+
# allow geometry edits to be reflected on the tile layer.
|
|
250
|
+
def _handle_geometry_edited(self, geo_json):
|
|
251
|
+
return self._handle_geometry_created(geo_json)
|
|
252
|
+
|
|
253
|
+
def _get_synced_geojson_from_draw_control(self):
|
|
254
|
+
return [data.copy() for data in self.data]
|
|
255
|
+
|
|
256
|
+
def _bind_to_draw_control(self):
|
|
257
|
+
# Handles draw events
|
|
258
|
+
def handle_draw(_, action, geo_json):
|
|
259
|
+
try:
|
|
260
|
+
if action == "created":
|
|
261
|
+
self._handle_geometry_created(geo_json)
|
|
262
|
+
elif action == "edited":
|
|
263
|
+
self._handle_geometry_edited(geo_json)
|
|
264
|
+
elif action == "deleted":
|
|
265
|
+
self._handle_geometry_deleted(geo_json)
|
|
266
|
+
except Exception as e:
|
|
267
|
+
self.reset(clear_draw_control=False)
|
|
268
|
+
print("There was an error creating Earth Engine Feature.")
|
|
269
|
+
raise Exception(e)
|
|
270
|
+
|
|
271
|
+
self.on_draw(handle_draw)
|
|
272
|
+
# NOTE: Uncomment the following code once
|
|
273
|
+
# https://github.com/jupyter-widgets/ipyleaflet/issues/1119 is fixed
|
|
274
|
+
# to allow edited geometries to be reflected instead of added.
|
|
275
|
+
# def handle_data_update(_):
|
|
276
|
+
# self._sync_geometries()
|
|
277
|
+
# self.observe(handle_data_update, 'data')
|
|
278
|
+
|
|
279
|
+
def _remove_geometry_at_index_on_draw_control(self, index):
|
|
280
|
+
# NOTE: Uncomment the following code once
|
|
281
|
+
# https://github.com/jupyter-widgets/ipyleaflet/issues/1119 is fixed to
|
|
282
|
+
# remove drawn geometries with `remove_last_drawn()`.
|
|
283
|
+
# del self.data[index]
|
|
284
|
+
# self.send_state(key='data')
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
def _clear_draw_control(self):
|
|
288
|
+
self.data = [] # Remove all drawn features from the map.
|
|
289
|
+
return self.clear()
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class MapInterface:
|
|
293
|
+
"""Interface for all maps."""
|
|
294
|
+
|
|
295
|
+
# The layers on the map.
|
|
296
|
+
ee_layers: Dict[str, Dict[str, Any]]
|
|
297
|
+
|
|
298
|
+
# The GeoJSON layers on the map.
|
|
299
|
+
geojson_layers: List[Any]
|
|
300
|
+
|
|
301
|
+
def get_zoom(self) -> int:
|
|
302
|
+
"""Returns the current zoom level of the map."""
|
|
303
|
+
raise NotImplementedError()
|
|
304
|
+
|
|
305
|
+
def set_zoom(self, value: int) -> None:
|
|
306
|
+
"""Sets the current zoom level of the map."""
|
|
307
|
+
del value # Unused.
|
|
308
|
+
raise NotImplementedError()
|
|
309
|
+
|
|
310
|
+
def get_center(self) -> Sequence:
|
|
311
|
+
"""Returns the current center of the map (lat, lon)."""
|
|
312
|
+
raise NotImplementedError()
|
|
313
|
+
|
|
314
|
+
def set_center(self, lon: float, lat: float, zoom: Optional[int] = None) -> None:
|
|
315
|
+
"""Centers the map view at a given coordinates with the given zoom level."""
|
|
316
|
+
del lon, lat, zoom # Unused.
|
|
317
|
+
raise NotImplementedError()
|
|
318
|
+
|
|
319
|
+
def center_object(
|
|
320
|
+
self, ee_object: ee.ComputedObject, zoom: Optional[int] = None
|
|
321
|
+
) -> None:
|
|
322
|
+
"""Centers the map view on a given object."""
|
|
323
|
+
del ee_object, zoom # Unused.
|
|
324
|
+
raise NotImplementedError()
|
|
325
|
+
|
|
326
|
+
def get_scale(self) -> float:
|
|
327
|
+
"""Returns the approximate pixel scale of the current map view, in meters."""
|
|
328
|
+
raise NotImplementedError()
|
|
329
|
+
|
|
330
|
+
def get_bounds(self) -> Tuple[float]:
|
|
331
|
+
"""Returns the bounds of the current map view.
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
list: A list in the format [west, south, east, north] in degrees.
|
|
335
|
+
"""
|
|
336
|
+
raise NotImplementedError()
|
|
337
|
+
|
|
338
|
+
@property
|
|
339
|
+
def width(self) -> str:
|
|
340
|
+
"""Returns the current width of the map."""
|
|
341
|
+
raise NotImplementedError()
|
|
342
|
+
|
|
343
|
+
@width.setter
|
|
344
|
+
def width(self, value: str) -> None:
|
|
345
|
+
"""Sets the width of the map."""
|
|
346
|
+
del value # Unused.
|
|
347
|
+
raise NotImplementedError()
|
|
348
|
+
|
|
349
|
+
@property
|
|
350
|
+
def height(self) -> str:
|
|
351
|
+
"""Returns the current height of the map."""
|
|
352
|
+
raise NotImplementedError()
|
|
353
|
+
|
|
354
|
+
@height.setter
|
|
355
|
+
def height(self, value: str) -> None:
|
|
356
|
+
"""Sets the height of the map."""
|
|
357
|
+
del value # Unused.
|
|
358
|
+
raise NotImplementedError()
|
|
359
|
+
|
|
360
|
+
def add(self, widget: str, position: str, **kwargs) -> None:
|
|
361
|
+
"""Adds a widget to the map."""
|
|
362
|
+
del widget, position, kwargs # Unused.
|
|
363
|
+
raise NotImplementedError()
|
|
364
|
+
|
|
365
|
+
def remove(self, widget: str) -> None:
|
|
366
|
+
"""Removes a widget to the map."""
|
|
367
|
+
del widget # Unused.
|
|
368
|
+
raise NotImplementedError()
|
|
369
|
+
|
|
370
|
+
def add_layer(
|
|
371
|
+
self,
|
|
372
|
+
ee_object: ee.ComputedObject,
|
|
373
|
+
vis_params: Optional[Dict[str, Any]] = None,
|
|
374
|
+
name: Optional[str] = None,
|
|
375
|
+
shown: bool = True,
|
|
376
|
+
opacity: float = 1.0,
|
|
377
|
+
) -> None:
|
|
378
|
+
"""Adds a layer to the map."""
|
|
379
|
+
del ee_object, vis_params, name, shown, opacity # Unused.
|
|
380
|
+
raise NotImplementedError()
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class Map(ipyleaflet.Map, MapInterface):
|
|
384
|
+
"""The Map class inherits the ipyleaflet Map class.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
center (list, optional): Center of the map (lat, lon). Defaults to [0, 0].
|
|
388
|
+
zoom (int, optional): Zoom level of the map. Defaults to 2.
|
|
389
|
+
height (str, optional): Height of the map. Defaults to "600px".
|
|
390
|
+
width (str, optional): Width of the map. Defaults to "100%".
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
ipyleaflet: ipyleaflet map object.
|
|
394
|
+
"""
|
|
395
|
+
|
|
396
|
+
_KWARG_DEFAULTS: Dict[str, Any] = {
|
|
397
|
+
"center": [0, 0],
|
|
398
|
+
"zoom": 2,
|
|
399
|
+
"zoom_control": False,
|
|
400
|
+
"attribution_control": False,
|
|
401
|
+
"ee_initialize": True,
|
|
402
|
+
"scroll_wheel_zoom": True,
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
_BASEMAP_ALIASES: Dict[str, str] = {
|
|
406
|
+
"DEFAULT": "OpenStreetMap.Mapnik",
|
|
407
|
+
"ROADMAP": "Esri.WorldStreetMap",
|
|
408
|
+
"SATELLITE": "Esri.WorldImagery",
|
|
409
|
+
"TERRAIN": "Esri.WorldTopoMap",
|
|
410
|
+
"HYBRID": "Esri.WorldImagery",
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
_USER_AGENT_PREFIX = "geemap-core"
|
|
414
|
+
|
|
415
|
+
@property
|
|
416
|
+
def width(self) -> str:
|
|
417
|
+
return self.layout.width
|
|
418
|
+
|
|
419
|
+
@width.setter
|
|
420
|
+
def width(self, value: str) -> None:
|
|
421
|
+
self.layout.width = value
|
|
422
|
+
|
|
423
|
+
@property
|
|
424
|
+
def height(self) -> str:
|
|
425
|
+
return self.layout.height
|
|
426
|
+
|
|
427
|
+
@height.setter
|
|
428
|
+
def height(self, value: str) -> None:
|
|
429
|
+
self.layout.height = value
|
|
430
|
+
|
|
431
|
+
@property
|
|
432
|
+
def _toolbar(self) -> Optional[toolbar.Toolbar]:
|
|
433
|
+
return self._find_widget_of_type(toolbar.Toolbar)
|
|
434
|
+
|
|
435
|
+
@property
|
|
436
|
+
def _inspector(self) -> Optional[map_widgets.Inspector]:
|
|
437
|
+
return self._find_widget_of_type(map_widgets.Inspector)
|
|
438
|
+
|
|
439
|
+
@property
|
|
440
|
+
def _draw_control(self) -> MapDrawControl:
|
|
441
|
+
return self._find_widget_of_type(MapDrawControl)
|
|
442
|
+
|
|
443
|
+
@property
|
|
444
|
+
def _layer_manager(self) -> Optional[map_widgets.LayerManager]:
|
|
445
|
+
if toolbar_widget := self._toolbar:
|
|
446
|
+
if isinstance(toolbar_widget.accessory_widget, map_widgets.LayerManager):
|
|
447
|
+
return toolbar_widget.accessory_widget
|
|
448
|
+
return self._find_widget_of_type(map_widgets.LayerManager)
|
|
449
|
+
|
|
450
|
+
@property
|
|
451
|
+
def _layer_editor(self) -> Optional[map_widgets.LayerEditor]:
|
|
452
|
+
return self._find_widget_of_type(map_widgets.LayerEditor)
|
|
453
|
+
|
|
454
|
+
@property
|
|
455
|
+
def _basemap_selector(self) -> Optional[map_widgets.Basemap]:
|
|
456
|
+
return self._find_widget_of_type(map_widgets.Basemap)
|
|
457
|
+
|
|
458
|
+
def __init__(self, **kwargs):
|
|
459
|
+
self._available_basemaps = self._get_available_basemaps()
|
|
460
|
+
|
|
461
|
+
if "width" in kwargs:
|
|
462
|
+
self.width: str = kwargs.pop("width", "100%")
|
|
463
|
+
self.height: str = kwargs.pop("height", "600px")
|
|
464
|
+
|
|
465
|
+
self.ee_layers: Dict[str, Dict[str, Any]] = {}
|
|
466
|
+
self.geojson_layers: List[Any] = []
|
|
467
|
+
|
|
468
|
+
kwargs = self._apply_kwarg_defaults(kwargs)
|
|
469
|
+
super().__init__(**kwargs)
|
|
470
|
+
|
|
471
|
+
for position, widgets in self._control_config().items():
|
|
472
|
+
for widget in widgets:
|
|
473
|
+
self.add(widget, position=position)
|
|
474
|
+
|
|
475
|
+
# Authenticate and initialize EE.
|
|
476
|
+
if kwargs.get("ee_initialize", True):
|
|
477
|
+
common.ee_initialize(user_agent_prefix=self._USER_AGENT_PREFIX)
|
|
478
|
+
|
|
479
|
+
# Listen for layers being added/removed so we can update the layer manager.
|
|
480
|
+
self.observe(self._on_layers_change, "layers")
|
|
481
|
+
|
|
482
|
+
def get_zoom(self) -> int:
|
|
483
|
+
return self.zoom
|
|
484
|
+
|
|
485
|
+
def set_zoom(self, value: int) -> None:
|
|
486
|
+
self.zoom = value
|
|
487
|
+
|
|
488
|
+
def get_center(self) -> Sequence:
|
|
489
|
+
return self.center
|
|
490
|
+
|
|
491
|
+
def get_bounds(self) -> Sequence:
|
|
492
|
+
bounds = self.bounds
|
|
493
|
+
return [bounds[0][0], bounds[0][1], bounds[1][0], bounds[1][1]]
|
|
494
|
+
|
|
495
|
+
def get_scale(self) -> float:
|
|
496
|
+
# Reference:
|
|
497
|
+
# - https://blogs.bing.com/maps/2006/02/25/map-control-zoom-levels-gt-resolution
|
|
498
|
+
# - https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Resolution_and_Scale
|
|
499
|
+
center_lat = self.center[0]
|
|
500
|
+
center_lat_cos = math.cos(math.radians(center_lat))
|
|
501
|
+
return 156543.04 * center_lat_cos / math.pow(2, self.zoom)
|
|
502
|
+
|
|
503
|
+
def set_center(self, lon: float, lat: float, zoom: Optional[int] = None) -> None:
|
|
504
|
+
self.center = (lat, lon)
|
|
505
|
+
if zoom is not None:
|
|
506
|
+
self.zoom = zoom
|
|
507
|
+
|
|
508
|
+
def _get_geometry(
|
|
509
|
+
self, ee_object: ee.ComputedObject, max_error: float
|
|
510
|
+
) -> ee.Geometry:
|
|
511
|
+
"""Returns the geometry for an arbitrary EE object."""
|
|
512
|
+
if isinstance(ee_object, ee.Geometry):
|
|
513
|
+
return ee_object
|
|
514
|
+
try:
|
|
515
|
+
return ee_object.geometry(maxError=max_error)
|
|
516
|
+
except Exception as exc:
|
|
517
|
+
raise Exception(
|
|
518
|
+
"ee_object must be one of ee.Geometry, ee.FeatureCollection, ee.Image, or ee.ImageCollection."
|
|
519
|
+
) from exc
|
|
520
|
+
|
|
521
|
+
def center_object(
|
|
522
|
+
self, ee_object: ee.ComputedObject, zoom: Optional[int] = None
|
|
523
|
+
) -> None:
|
|
524
|
+
max_error = 0.001
|
|
525
|
+
geometry = self._get_geometry(ee_object, max_error).transform(
|
|
526
|
+
maxError=max_error
|
|
527
|
+
)
|
|
528
|
+
if zoom is None:
|
|
529
|
+
coordinates = geometry.bounds(max_error).getInfo()["coordinates"][0]
|
|
530
|
+
x_vals = [c[0] for c in coordinates]
|
|
531
|
+
y_vals = [c[1] for c in coordinates]
|
|
532
|
+
self.fit_bounds([[min(y_vals), min(x_vals)], [max(y_vals), max(x_vals)]])
|
|
533
|
+
else:
|
|
534
|
+
if not isinstance(zoom, int):
|
|
535
|
+
raise ValueError("Zoom must be an integer.")
|
|
536
|
+
centroid = geometry.centroid(maxError=max_error).getInfo()["coordinates"]
|
|
537
|
+
self.set_center(centroid[0], centroid[1], zoom)
|
|
538
|
+
|
|
539
|
+
def _find_widget_of_type(
|
|
540
|
+
self, widget_type: Type, return_control: bool = False
|
|
541
|
+
) -> Optional[Any]:
|
|
542
|
+
"""Finds a widget in the controls with the passed in type."""
|
|
543
|
+
for widget in self.controls:
|
|
544
|
+
if isinstance(widget, ipyleaflet.WidgetControl):
|
|
545
|
+
if isinstance(widget.widget, widget_type):
|
|
546
|
+
return widget if return_control else widget.widget
|
|
547
|
+
elif isinstance(widget, widget_type):
|
|
548
|
+
return widget
|
|
549
|
+
return None
|
|
550
|
+
|
|
551
|
+
def add(self, obj: Any, position: str = "", **kwargs) -> None:
|
|
552
|
+
if not position:
|
|
553
|
+
for default_position, widgets in self._control_config().items():
|
|
554
|
+
if obj in widgets:
|
|
555
|
+
position = default_position
|
|
556
|
+
if not position:
|
|
557
|
+
position = "topright"
|
|
558
|
+
|
|
559
|
+
# Basic controls:
|
|
560
|
+
# - can only be added to the map once,
|
|
561
|
+
# - have a constructor that takes a position arg, and
|
|
562
|
+
# - don't need to be stored as instance vars.
|
|
563
|
+
basic_controls: Dict[str, Tuple[ipyleaflet.Control, Dict[str, Any]]] = {
|
|
564
|
+
"zoom_control": (ipyleaflet.ZoomControl, {}),
|
|
565
|
+
"fullscreen_control": (ipyleaflet.FullScreenControl, {}),
|
|
566
|
+
"scale_control": (ipyleaflet.ScaleControl, {"metric": True}),
|
|
567
|
+
"attribution_control": (ipyleaflet.AttributionControl, {}),
|
|
568
|
+
}
|
|
569
|
+
if obj in basic_controls:
|
|
570
|
+
basic_control = basic_controls[obj]
|
|
571
|
+
# Check if widget is already on the map.
|
|
572
|
+
if self._find_widget_of_type(basic_control[0]):
|
|
573
|
+
return
|
|
574
|
+
new_kwargs = {**basic_control[1], **kwargs}
|
|
575
|
+
super().add(basic_control[0](position=position, **new_kwargs))
|
|
576
|
+
elif obj == "toolbar":
|
|
577
|
+
self._add_toolbar(position, **kwargs)
|
|
578
|
+
elif obj == "inspector":
|
|
579
|
+
self._add_inspector(position, **kwargs)
|
|
580
|
+
elif obj == "layer_manager":
|
|
581
|
+
self._add_layer_manager(position, **kwargs)
|
|
582
|
+
elif obj == "layer_editor":
|
|
583
|
+
self._add_layer_editor(position, **kwargs)
|
|
584
|
+
elif obj == "draw_control":
|
|
585
|
+
self._add_draw_control(position, **kwargs)
|
|
586
|
+
elif obj == "basemap_selector":
|
|
587
|
+
self._add_basemap_selector(position, **kwargs)
|
|
588
|
+
else:
|
|
589
|
+
super().add(obj)
|
|
590
|
+
|
|
591
|
+
def _on_toggle_toolbar_layers(self, is_open: bool) -> None:
|
|
592
|
+
if is_open:
|
|
593
|
+
if self._layer_manager:
|
|
594
|
+
return
|
|
595
|
+
|
|
596
|
+
def _on_open_vis(layer_name: str) -> None:
|
|
597
|
+
layer = self.ee_layers.get(layer_name, None)
|
|
598
|
+
self._add_layer_editor(position="bottomright", layer_dict=layer)
|
|
599
|
+
|
|
600
|
+
layer_manager = map_widgets.LayerManager(self)
|
|
601
|
+
layer_manager.header_hidden = True
|
|
602
|
+
layer_manager.close_button_hidden = True
|
|
603
|
+
layer_manager.on_open_vis = _on_open_vis
|
|
604
|
+
self._toolbar.accessory_widget = layer_manager
|
|
605
|
+
else:
|
|
606
|
+
self._toolbar.accessory_widget = None
|
|
607
|
+
self.remove("layer_manager")
|
|
608
|
+
|
|
609
|
+
def _add_layer_manager(self, position: str, **kwargs) -> None:
|
|
610
|
+
if self._layer_manager:
|
|
611
|
+
return
|
|
612
|
+
|
|
613
|
+
def _on_open_vis(layer_name: str) -> None:
|
|
614
|
+
layer = self.ee_layers.get(layer_name, None)
|
|
615
|
+
self._add_layer_editor(position="bottomright", layer_dict=layer)
|
|
616
|
+
|
|
617
|
+
layer_manager = map_widgets.LayerManager(self, **kwargs)
|
|
618
|
+
layer_manager.on_close = lambda: self.remove("layer_manager")
|
|
619
|
+
layer_manager.on_open_vis = _on_open_vis
|
|
620
|
+
layer_manager_control = ipyleaflet.WidgetControl(
|
|
621
|
+
widget=layer_manager, position=position
|
|
622
|
+
)
|
|
623
|
+
super().add(layer_manager_control)
|
|
624
|
+
|
|
625
|
+
def _add_toolbar(self, position: str, **kwargs) -> None:
|
|
626
|
+
if self._toolbar:
|
|
627
|
+
return
|
|
628
|
+
|
|
629
|
+
toolbar_val = toolbar.Toolbar(
|
|
630
|
+
self, self._toolbar_main_tools(), self._toolbar_extra_tools(), **kwargs
|
|
631
|
+
)
|
|
632
|
+
toolbar_val.on_layers_toggled = self._on_toggle_toolbar_layers
|
|
633
|
+
toolbar_control = ipyleaflet.WidgetControl(
|
|
634
|
+
widget=toolbar_val, position=position
|
|
635
|
+
)
|
|
636
|
+
super().add(toolbar_control)
|
|
637
|
+
|
|
638
|
+
def _add_inspector(self, position: str, **kwargs) -> None:
|
|
639
|
+
if self._inspector:
|
|
640
|
+
return
|
|
641
|
+
|
|
642
|
+
inspector = map_widgets.Inspector(self, **kwargs)
|
|
643
|
+
inspector.on_close = lambda: self.remove("inspector")
|
|
644
|
+
inspector_control = ipyleaflet.WidgetControl(
|
|
645
|
+
widget=inspector, position=position
|
|
646
|
+
)
|
|
647
|
+
super().add(inspector_control)
|
|
648
|
+
|
|
649
|
+
def _add_layer_editor(self, position: str, **kwargs) -> None:
|
|
650
|
+
if self._layer_editor:
|
|
651
|
+
return
|
|
652
|
+
|
|
653
|
+
widget = map_widgets.LayerEditor(self, **kwargs)
|
|
654
|
+
widget.on_close = lambda: self.remove("layer_editor")
|
|
655
|
+
control = ipyleaflet.WidgetControl(widget=widget, position=position)
|
|
656
|
+
super().add(control)
|
|
657
|
+
|
|
658
|
+
def _add_draw_control(self, position="topleft", **kwargs) -> None:
|
|
659
|
+
"""Add a draw control to the map
|
|
660
|
+
|
|
661
|
+
Args:
|
|
662
|
+
position (str, optional): The position of the draw control. Defaults to "topleft".
|
|
663
|
+
"""
|
|
664
|
+
if self._draw_control:
|
|
665
|
+
return
|
|
666
|
+
default_args = dict(
|
|
667
|
+
marker={"shapeOptions": {"color": "#3388ff"}},
|
|
668
|
+
rectangle={"shapeOptions": {"color": "#3388ff"}},
|
|
669
|
+
circlemarker={},
|
|
670
|
+
edit=True,
|
|
671
|
+
remove=True,
|
|
672
|
+
)
|
|
673
|
+
control = MapDrawControl(
|
|
674
|
+
host_map=self,
|
|
675
|
+
position=position,
|
|
676
|
+
**{**default_args, **kwargs},
|
|
677
|
+
)
|
|
678
|
+
super().add(control)
|
|
679
|
+
|
|
680
|
+
def get_draw_control(self) -> Optional[MapDrawControl]:
|
|
681
|
+
return self._draw_control
|
|
682
|
+
|
|
683
|
+
def _add_basemap_selector(self, position: str, **kwargs) -> None:
|
|
684
|
+
if self._basemap_selector:
|
|
685
|
+
return
|
|
686
|
+
|
|
687
|
+
basemap_names = kwargs.pop("basemaps", list(self._available_basemaps.keys()))
|
|
688
|
+
value = kwargs.pop(
|
|
689
|
+
"value", self._get_preferred_basemap_name(self.layers[0].name)
|
|
690
|
+
)
|
|
691
|
+
basemap = map_widgets.Basemap(basemap_names, value, **kwargs)
|
|
692
|
+
basemap.on_close = lambda: self.remove("basemap_selector")
|
|
693
|
+
basemap.on_basemap_changed = self._replace_basemap
|
|
694
|
+
basemap_control = ipyleaflet.WidgetControl(widget=basemap, position=position)
|
|
695
|
+
super().add(basemap_control)
|
|
696
|
+
|
|
697
|
+
def remove(self, widget: Any) -> None:
|
|
698
|
+
"""Removes a widget to the map."""
|
|
699
|
+
|
|
700
|
+
basic_controls: Dict[str, ipyleaflet.Control] = {
|
|
701
|
+
"zoom_control": ipyleaflet.ZoomControl,
|
|
702
|
+
"fullscreen_control": ipyleaflet.FullScreenControl,
|
|
703
|
+
"scale_control": ipyleaflet.ScaleControl,
|
|
704
|
+
"attribution_control": ipyleaflet.AttributionControl,
|
|
705
|
+
"toolbar": toolbar.Toolbar,
|
|
706
|
+
"inspector": map_widgets.Inspector,
|
|
707
|
+
"layer_manager": map_widgets.LayerManager,
|
|
708
|
+
"layer_editor": map_widgets.LayerEditor,
|
|
709
|
+
"draw_control": MapDrawControl,
|
|
710
|
+
"basemap_selector": map_widgets.Basemap,
|
|
711
|
+
}
|
|
712
|
+
if widget_type := basic_controls.get(widget, None):
|
|
713
|
+
if control := self._find_widget_of_type(widget_type, return_control=True):
|
|
714
|
+
self.remove(control)
|
|
715
|
+
control.close()
|
|
716
|
+
return
|
|
717
|
+
|
|
718
|
+
if hasattr(widget, "name") and widget.name in self.ee_layers:
|
|
719
|
+
self.ee_layers.pop(widget.name)
|
|
720
|
+
|
|
721
|
+
if ee_layer := self.ee_layers.pop(widget, None):
|
|
722
|
+
tile_layer = ee_layer.get("ee_layer", None)
|
|
723
|
+
if tile_layer is not None:
|
|
724
|
+
self.remove_layer(tile_layer)
|
|
725
|
+
if legend := ee_layer.get("legend", None):
|
|
726
|
+
self.remove(legend)
|
|
727
|
+
if colorbar := ee_layer.get("colorbar", None):
|
|
728
|
+
self.remove(colorbar)
|
|
729
|
+
return
|
|
730
|
+
|
|
731
|
+
super().remove(widget)
|
|
732
|
+
if isinstance(widget, ipywidgets.Widget):
|
|
733
|
+
widget.close()
|
|
734
|
+
|
|
735
|
+
def add_layer(
|
|
736
|
+
self,
|
|
737
|
+
ee_object: ee.ComputedObject,
|
|
738
|
+
vis_params: Dict[str, Any] = None,
|
|
739
|
+
name: Optional[str] = None,
|
|
740
|
+
shown: bool = True,
|
|
741
|
+
opacity: float = 1.0,
|
|
742
|
+
) -> None:
|
|
743
|
+
"""Adds a layer to the map."""
|
|
744
|
+
|
|
745
|
+
# Call super if not an EE object.
|
|
746
|
+
if not isinstance(ee_object, ee_tile_layers.EELeafletTileLayer.EE_TYPES):
|
|
747
|
+
super().add_layer(ee_object)
|
|
748
|
+
return
|
|
749
|
+
|
|
750
|
+
if vis_params is None:
|
|
751
|
+
vis_params = {}
|
|
752
|
+
if name is None:
|
|
753
|
+
name = f"Layer {len(self.ee_layers) + 1}"
|
|
754
|
+
tile_layer = ee_tile_layers.EELeafletTileLayer(
|
|
755
|
+
ee_object, vis_params, name, shown, opacity
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
# Remove the layer if it already exists.
|
|
759
|
+
self.remove(name)
|
|
760
|
+
|
|
761
|
+
self.ee_layers[name] = {
|
|
762
|
+
"ee_object": ee_object,
|
|
763
|
+
"ee_layer": tile_layer,
|
|
764
|
+
"vis_params": vis_params,
|
|
765
|
+
}
|
|
766
|
+
super().add(tile_layer)
|
|
767
|
+
|
|
768
|
+
def _open_help_page(
|
|
769
|
+
self, host_map: MapInterface, selected: bool, item: toolbar.Toolbar.Item
|
|
770
|
+
) -> None:
|
|
771
|
+
del host_map, item # Unused.
|
|
772
|
+
if selected:
|
|
773
|
+
common.open_url("https://geemap.org")
|
|
774
|
+
|
|
775
|
+
def _toolbar_main_tools(self) -> List[toolbar.Toolbar.Item]:
|
|
776
|
+
@toolbar._cleanup_toolbar_item
|
|
777
|
+
def inspector_tool_callback(
|
|
778
|
+
map: Map, selected: bool, item: toolbar.Toolbar.Item
|
|
779
|
+
):
|
|
780
|
+
del selected, item # Unused.
|
|
781
|
+
map.add("inspector")
|
|
782
|
+
return map._inspector
|
|
783
|
+
|
|
784
|
+
@toolbar._cleanup_toolbar_item
|
|
785
|
+
def basemap_tool_callback(map: Map, selected: bool, item: toolbar.Toolbar.Item):
|
|
786
|
+
del selected, item # Unused.
|
|
787
|
+
map.add("basemap_selector")
|
|
788
|
+
return map._basemap_selector
|
|
789
|
+
|
|
790
|
+
return [
|
|
791
|
+
toolbar.Toolbar.Item(
|
|
792
|
+
icon="map",
|
|
793
|
+
tooltip="Basemap selector",
|
|
794
|
+
callback=basemap_tool_callback,
|
|
795
|
+
reset=False,
|
|
796
|
+
),
|
|
797
|
+
toolbar.Toolbar.Item(
|
|
798
|
+
icon="info",
|
|
799
|
+
tooltip="Inspector",
|
|
800
|
+
callback=inspector_tool_callback,
|
|
801
|
+
reset=False,
|
|
802
|
+
),
|
|
803
|
+
toolbar.Toolbar.Item(
|
|
804
|
+
icon="question", tooltip="Get help", callback=self._open_help_page
|
|
805
|
+
),
|
|
806
|
+
]
|
|
807
|
+
|
|
808
|
+
def _toolbar_extra_tools(self) -> Optional[List[toolbar.Toolbar.Item]]:
|
|
809
|
+
return None
|
|
810
|
+
|
|
811
|
+
def _control_config(self) -> Dict[str, List[str]]:
|
|
812
|
+
return {
|
|
813
|
+
"topleft": ["zoom_control", "fullscreen_control", "draw_control"],
|
|
814
|
+
"bottomleft": ["scale_control", "measure_control"],
|
|
815
|
+
"topright": ["toolbar"],
|
|
816
|
+
"bottomright": ["attribution_control"],
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
def _apply_kwarg_defaults(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
|
820
|
+
ret_kwargs = {}
|
|
821
|
+
for kwarg, default in self._KWARG_DEFAULTS.items():
|
|
822
|
+
ret_kwargs[kwarg] = kwargs.pop(kwarg, default)
|
|
823
|
+
ret_kwargs.update(kwargs)
|
|
824
|
+
return ret_kwargs
|
|
825
|
+
|
|
826
|
+
def _replace_basemap(self, basemap_name: str) -> None:
|
|
827
|
+
basemap = self._available_basemaps.get(basemap_name, None)
|
|
828
|
+
if basemap is None:
|
|
829
|
+
logging.warning("Invalid basemap selected: %s", basemap_name)
|
|
830
|
+
return
|
|
831
|
+
new_layer = ipyleaflet.TileLayer(
|
|
832
|
+
url=basemap["url"],
|
|
833
|
+
name=basemap["name"],
|
|
834
|
+
max_zoom=basemap.get("max_zoom", 24),
|
|
835
|
+
attribution=basemap.get("attribution", None),
|
|
836
|
+
)
|
|
837
|
+
# substitute_layer is broken when the map has a single layer.
|
|
838
|
+
if len(self.layers) == 1:
|
|
839
|
+
self.clear_layers()
|
|
840
|
+
self.add_layer(new_layer)
|
|
841
|
+
else:
|
|
842
|
+
self.substitute_layer(self.layers[0], new_layer)
|
|
843
|
+
|
|
844
|
+
def _get_available_basemaps(self) -> Dict[str, Any]:
|
|
845
|
+
"""Convert xyz tile services to a dictionary of basemaps."""
|
|
846
|
+
ret_dict = {}
|
|
847
|
+
for tile_info in basemaps.get_xyz_dict().values():
|
|
848
|
+
tile_info["url"] = tile_info.build_url()
|
|
849
|
+
ret_dict[tile_info["name"]] = tile_info
|
|
850
|
+
extra_dict = {k: ret_dict[v] for k, v in self._BASEMAP_ALIASES.items()}
|
|
851
|
+
return {**extra_dict, **ret_dict}
|
|
852
|
+
|
|
853
|
+
def _get_preferred_basemap_name(self, basemap_name: str) -> str:
|
|
854
|
+
"""Returns the aliased basemap name."""
|
|
855
|
+
try:
|
|
856
|
+
return list(self._BASEMAP_ALIASES.keys())[
|
|
857
|
+
list(self._BASEMAP_ALIASES.values()).index(basemap_name)
|
|
858
|
+
]
|
|
859
|
+
except ValueError:
|
|
860
|
+
return basemap_name
|
|
861
|
+
|
|
862
|
+
def _on_layers_change(self, change) -> None:
|
|
863
|
+
del change # Unused.
|
|
864
|
+
if self._layer_manager:
|
|
865
|
+
self._layer_manager.refresh_layers()
|
|
866
|
+
|
|
867
|
+
# Keep the following three camelCase methods for backwards compatibility.
|
|
868
|
+
addLayer = add_layer
|
|
869
|
+
centerObject = center_object
|
|
870
|
+
setCenter = set_center
|