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/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