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.
@@ -0,0 +1,2222 @@
1
+ """Various ipywidgets that can be added to a map."""
2
+
3
+ import functools
4
+
5
+ import IPython
6
+ from IPython.core.display import HTML, display
7
+
8
+ import ee
9
+ import ipytree
10
+ import ipywidgets
11
+
12
+ from . import common
13
+
14
+ from traceback import format_tb
15
+
16
+
17
+ def _set_css_in_cell_output(info):
18
+ display(
19
+ HTML(
20
+ """
21
+ <style>
22
+ .geemap-dark {
23
+ --jp-widgets-color: white;
24
+ --jp-widgets-label-color: white;
25
+ --jp-ui-font-color1: white;
26
+ --jp-layout-color2: #454545;
27
+ background-color: #383838;
28
+ }
29
+
30
+ .geemap-dark .jupyter-button {
31
+ --jp-layout-color3: #383838;
32
+ }
33
+
34
+ .geemap-colab {
35
+ background-color: var(--colab-primary-surface-color, white);
36
+ }
37
+
38
+ .geemap-colab .jupyter-button {
39
+ --jp-layout-color3: var(--colab-primary-surface-color, white);
40
+ }
41
+ </style>
42
+ """
43
+ )
44
+ )
45
+
46
+
47
+ try:
48
+ IPython.get_ipython().events.register("pre_run_cell", _set_css_in_cell_output)
49
+ except AttributeError:
50
+ pass
51
+
52
+
53
+ class Theme:
54
+ """Applies dynamic theme in Colab, otherwise light."""
55
+
56
+ current_theme = "colab" if common.in_colab_shell() else "light"
57
+
58
+ @staticmethod
59
+ def apply(cls):
60
+ original_init = cls.__init__
61
+
62
+ @functools.wraps(cls.__init__)
63
+ def wrapper(self, *args, **kwargs):
64
+ original_init(self, *args, **kwargs)
65
+ self.add_class("geemap-{}".format(Theme.current_theme))
66
+
67
+ cls.__init__ = wrapper
68
+ return cls
69
+
70
+
71
+ @Theme.apply
72
+ class Colorbar(ipywidgets.Output):
73
+ """A matplotlib colorbar widget that can be added to the map."""
74
+
75
+ def __init__(
76
+ self,
77
+ vis_params=None,
78
+ cmap="gray",
79
+ discrete=False,
80
+ label=None,
81
+ orientation="horizontal",
82
+ transparent_bg=False,
83
+ font_size=9,
84
+ axis_off=False,
85
+ max_width=None,
86
+ **kwargs,
87
+ ):
88
+ """Add a matplotlib colorbar to the map.
89
+
90
+ Args:
91
+ vis_params (dict): Visualization parameters as a dictionary. See
92
+ https://developers.google.com/earth-engine/guides/image_visualization # noqa
93
+ for options.
94
+ cmap (str, optional): Matplotlib colormap. Defaults to "gray". See
95
+ https://matplotlib.org/3.3.4/tutorials/colors/colormaps.html#sphx-glr-tutorials-colors-colormaps-py # noqa
96
+ for options.
97
+ discrete (bool, optional): Whether to create a discrete colorbar.
98
+ Defaults to False.
99
+ label (str, optional): Label for the colorbar. Defaults to None.
100
+ orientation (str, optional): Orientation of the colorbar, such as
101
+ "vertical" and "horizontal". Defaults to "horizontal".
102
+ transparent_bg (bool, optional): Whether to use transparent
103
+ background. Defaults to False.
104
+ font_size (int, optional): Font size for the colorbar. Defaults
105
+ to 9.
106
+ axis_off (bool, optional): Whether to turn off the axis. Defaults
107
+ to False.
108
+ max_width (str, optional): Maximum width of the colorbar in pixels.
109
+ Defaults to None.
110
+
111
+ Raises:
112
+ TypeError: If the vis_params is not a dictionary.
113
+ ValueError: If the orientation is not either horizontal or vertical.
114
+ ValueError: If the provided min value is not convertible to float.
115
+ ValueError: If the provided max value is not convertible to float.
116
+ ValueError: If the provided opacity value is not convertible to float.
117
+ ValueError: If cmap or palette is not provided.
118
+ """
119
+
120
+ import matplotlib # pylint: disable=import-outside-toplevel
121
+ import numpy # pylint: disable=import-outside-toplevel
122
+
123
+ if max_width is None:
124
+ if orientation == "horizontal":
125
+ max_width = "270px"
126
+ else:
127
+ max_width = "100px"
128
+
129
+ if isinstance(vis_params, (list, tuple)):
130
+ vis_params = {"palette": list(vis_params)}
131
+ elif not vis_params:
132
+ vis_params = {}
133
+
134
+ if not isinstance(vis_params, dict):
135
+ raise TypeError("The vis_params must be a dictionary.")
136
+
137
+ if isinstance(kwargs.get("colors"), (list, tuple)):
138
+ vis_params["palette"] = list(kwargs["colors"])
139
+
140
+ width, height = self._get_dimensions(orientation, kwargs)
141
+
142
+ vmin = vis_params.get("min", kwargs.pop("vmin", 0))
143
+ try:
144
+ vmin = float(vmin)
145
+ except ValueError as err:
146
+ raise ValueError("The provided min value must be scalar type.")
147
+
148
+ vmax = vis_params.get("max", kwargs.pop("mvax", 1))
149
+ try:
150
+ vmax = float(vmax)
151
+ except ValueError as err:
152
+ raise ValueError("The provided max value must be scalar type.")
153
+
154
+ alpha = vis_params.get("opacity", kwargs.pop("alpha", 1))
155
+ try:
156
+ alpha = float(alpha)
157
+ except ValueError as err:
158
+ raise ValueError("opacity or alpha value must be scalar type.")
159
+
160
+ if "palette" in vis_params.keys():
161
+ hexcodes = common.to_hex_colors(common.check_cmap(vis_params["palette"]))
162
+ if discrete:
163
+ cmap = matplotlib.colors.ListedColormap(hexcodes)
164
+ linspace = numpy.linspace(vmin, vmax, cmap.N + 1)
165
+ norm = matplotlib.colors.BoundaryNorm(linspace, cmap.N)
166
+ else:
167
+ cmap = matplotlib.colors.LinearSegmentedColormap.from_list(
168
+ "custom", hexcodes, N=256
169
+ )
170
+ norm = matplotlib.colors.Normalize(vmin=vmin, vmax=vmax)
171
+ elif cmap:
172
+ cmap = matplotlib.pyplot.get_cmap(cmap)
173
+ norm = matplotlib.colors.Normalize(vmin=vmin, vmax=vmax)
174
+ else:
175
+ raise ValueError(
176
+ 'cmap keyword or "palette" key in vis_params must be provided.'
177
+ )
178
+
179
+ fig, ax = matplotlib.pyplot.subplots(figsize=(width, height))
180
+ cb = matplotlib.colorbar.ColorbarBase(
181
+ ax,
182
+ norm=norm,
183
+ alpha=alpha,
184
+ cmap=cmap,
185
+ orientation=orientation,
186
+ **kwargs,
187
+ )
188
+
189
+ label = label or vis_params.get("bands") or kwargs.pop("caption", None)
190
+ if label:
191
+ cb.set_label(label, fontsize=font_size)
192
+
193
+ if axis_off:
194
+ ax.set_axis_off()
195
+ ax.tick_params(labelsize=font_size)
196
+
197
+ # Set the background color to transparent.
198
+ if transparent_bg:
199
+ fig.patch.set_alpha(0.0)
200
+
201
+ super().__init__(layout=ipywidgets.Layout(width=max_width))
202
+ with self:
203
+ self.outputs = ()
204
+ matplotlib.pyplot.show()
205
+
206
+ def _get_dimensions(self, orientation, kwargs):
207
+ default_dims = {"horizontal": (3.0, 0.3), "vertical": (0.3, 3.0)}
208
+ if orientation in default_dims:
209
+ default = default_dims[orientation]
210
+ return (
211
+ kwargs.get("width", default[0]),
212
+ kwargs.get("height", default[1]),
213
+ )
214
+ raise ValueError(
215
+ f"orientation must be one of [{', '.join(default_dims.keys())}]."
216
+ )
217
+
218
+
219
+ @Theme.apply
220
+ class Legend(ipywidgets.VBox):
221
+ """A legend widget that can be added to the map."""
222
+
223
+ ALLOWED_POSITIONS = ["topleft", "topright", "bottomleft", "bottomright"]
224
+ DEFAULT_COLORS = ["#8DD3C7", "#FFFFB3", "#BEBADA", "#FB8072", "#80B1D3"]
225
+ DEFAULT_KEYS = ["One", "Two", "Three", "Four", "etc"]
226
+ DEFAULT_MAX_HEIGHT = "400px"
227
+ DEFAULT_MAX_WIDTH = "300px"
228
+
229
+ def __init__(
230
+ self,
231
+ title="Legend",
232
+ legend_dict=None,
233
+ keys=None,
234
+ colors=None,
235
+ position="bottomright",
236
+ builtin_legend=None,
237
+ add_header=True,
238
+ widget_args={},
239
+ **kwargs,
240
+ ):
241
+ """Adds a customized legend to the map.
242
+
243
+ Args:
244
+ title (str, optional): Title of the legend. Defaults to 'Legend'.
245
+ legend_dict (dict, optional): A dictionary containing legend items
246
+ as keys and color as values. If provided, keys and colors will
247
+ be ignored. Defaults to None.
248
+ keys (list, optional): A list of legend keys. Defaults to None.
249
+ colors (list, optional): A list of legend colors. Defaults to None.
250
+ position (str, optional): Position of the legend. Defaults to
251
+ 'bottomright'.
252
+ builtin_legend (str, optional): Name of the builtin legend to add
253
+ to the map. Defaults to None.
254
+ add_header (bool, optional): Whether the legend can be closed or
255
+ not. Defaults to True.
256
+ widget_args (dict, optional): Additional arguments passed to the
257
+ widget_template() function. Defaults to {}.
258
+
259
+ Raises:
260
+ TypeError: If the keys are not a list.
261
+ TypeError: If the colors are not list.
262
+ TypeError: If the colors are not a list of tuples.
263
+ TypeError: If the legend_dict is not a dictionary.
264
+ ValueError: If the legend template does not exist.
265
+ ValueError: If a rgb value cannot to be converted to hex.
266
+ ValueError: If the keys and colors are not the same length.
267
+ ValueError: If the builtin_legend is not allowed.
268
+ ValueError: If the position is not allowed.
269
+
270
+ """
271
+ import os # pylint: disable=import-outside-toplevel
272
+ from IPython.display import display # pylint: disable=import-outside-toplevel
273
+ import pkg_resources # pylint: disable=import-outside-toplevel
274
+ from .legends import builtin_legends # pylint: disable=import-outside-toplevel
275
+
276
+ pkg_dir = os.path.dirname(
277
+ pkg_resources.resource_filename("geemap", "geemap.py")
278
+ )
279
+ legend_template = os.path.join(pkg_dir, "data/template/legend.html")
280
+
281
+ if not os.path.exists(legend_template):
282
+ raise ValueError("The legend template does not exist.")
283
+
284
+ if "labels" in kwargs:
285
+ keys = kwargs["labels"]
286
+ kwargs.pop("labels")
287
+
288
+ if keys is not None:
289
+ if not isinstance(keys, list):
290
+ raise TypeError("The legend keys must be a list.")
291
+ else:
292
+ keys = Legend.DEFAULT_KEYS
293
+
294
+ if colors is not None:
295
+ if not isinstance(colors, list):
296
+ raise TypeError("The legend colors must be a list.")
297
+ elif all(isinstance(item, tuple) for item in colors):
298
+ colors = Legend.__convert_rgb_colors_to_hex(colors)
299
+ elif all((item.startswith("#") and len(item) == 7) for item in colors):
300
+ pass
301
+ elif all((len(item) == 6) for item in colors):
302
+ pass
303
+ else:
304
+ raise TypeError("The legend colors must be a list of tuples.")
305
+ else:
306
+ colors = Legend.DEFAULT_COLORS
307
+
308
+ if len(keys) != len(colors):
309
+ raise ValueError("The legend keys and colors must be the same length.")
310
+
311
+ allowed_builtin_legends = builtin_legends.keys()
312
+ if builtin_legend is not None:
313
+ builtin_legend_allowed = Legend.__check_if_allowed(
314
+ builtin_legend, "builtin legend", allowed_builtin_legends
315
+ )
316
+ if builtin_legend_allowed:
317
+ legend_dict = builtin_legends[builtin_legend]
318
+ keys = list(legend_dict.keys())
319
+ colors = list(legend_dict.values())
320
+
321
+ if legend_dict is not None:
322
+ if not isinstance(legend_dict, dict):
323
+ raise TypeError("The legend dict must be a dictionary.")
324
+ else:
325
+ keys = list(legend_dict.keys())
326
+ colors = list(legend_dict.values())
327
+ if all(isinstance(item, tuple) for item in colors):
328
+ colors = Legend.__convert_rgb_colors_to_hex(colors)
329
+
330
+ Legend.__check_if_allowed(position, "position", Legend.ALLOWED_POSITIONS)
331
+
332
+ header = []
333
+ footer = []
334
+ content = Legend.__create_legend_items(keys, colors)
335
+
336
+ with open(legend_template) as f:
337
+ lines = f.readlines()
338
+ lines[3] = lines[3].replace("Legend", title)
339
+ header = lines[:6]
340
+ footer = lines[11:]
341
+
342
+ legend_html = header + content + footer
343
+ legend_text = "".join(legend_html)
344
+ legend_output = ipywidgets.Output(layout=Legend.__create_layout(**kwargs))
345
+ legend_widget = ipywidgets.HTML(value=legend_text)
346
+
347
+ if add_header:
348
+ if "show_close_button" not in widget_args:
349
+ widget_args["show_close_button"] = False
350
+ if "widget_icon" not in widget_args:
351
+ widget_args["widget_icon"] = "bars"
352
+
353
+ legend_output_widget = common.widget_template(
354
+ legend_output,
355
+ position=position,
356
+ display_widget=legend_widget,
357
+ **widget_args,
358
+ )
359
+ else:
360
+ legend_output_widget = legend_widget
361
+
362
+ super().__init__(children=[legend_output_widget])
363
+
364
+ legend_output.clear_output()
365
+ with legend_output:
366
+ display(legend_widget)
367
+
368
+ def __check_if_allowed(value, value_name, allowed_list):
369
+ if value not in allowed_list:
370
+ raise ValueError(
371
+ "The "
372
+ + value_name
373
+ + " must be one of the following: {}.".format(", ".join(allowed_list))
374
+ )
375
+ return True
376
+
377
+ def __convert_rgb_colors_to_hex(colors):
378
+ try:
379
+ return [common.rgb_to_hex(x) for x in colors]
380
+ except:
381
+ raise ValueError("Unable to convert rgb value to hex.")
382
+
383
+ def __create_legend_items(keys, colors):
384
+ legend_items = []
385
+ for index, key in enumerate(keys):
386
+ color = colors[index]
387
+ if not color.startswith("#"):
388
+ color = "#" + color
389
+ item = "<li><span style='background:{};'></span>{}</li>\n".format(
390
+ color, key
391
+ )
392
+ legend_items.append(item)
393
+ return legend_items
394
+
395
+ def __create_layout(**kwargs):
396
+ height = Legend.__create_layout_property("height", None, **kwargs)
397
+
398
+ min_height = Legend.__create_layout_property("min_height", None, **kwargs)
399
+
400
+ if height is None:
401
+ max_height = Legend.DEFAULT_MAX_HEIGHT
402
+ else:
403
+ max_height = Legend.__create_layout_property("max_height", None, **kwargs)
404
+
405
+ width = Legend.__create_layout_property("width", None, **kwargs)
406
+
407
+ if "min_width" not in kwargs:
408
+ min_width = None
409
+
410
+ if width is None:
411
+ max_width = Legend.DEFAULT_MAX_WIDTH
412
+ else:
413
+ max_width = Legend.__create_layout_property(
414
+ "max_width", Legend.DEFAULT_MAX_WIDTH, **kwargs
415
+ )
416
+
417
+ return {
418
+ "height": height,
419
+ "max_height": max_height,
420
+ "max_width": max_width,
421
+ "min_height": min_height,
422
+ "min_width": min_width,
423
+ "overflow": "scroll",
424
+ "width": width,
425
+ }
426
+
427
+ def __create_layout_property(name, default_value, **kwargs):
428
+ return default_value if name not in kwargs else kwargs[name]
429
+
430
+
431
+ @Theme.apply
432
+ class Inspector(ipywidgets.VBox):
433
+ """Inspector widget for Earth Engine data."""
434
+
435
+ def __init__(
436
+ self,
437
+ host_map,
438
+ names=None,
439
+ visible=True,
440
+ decimals=2,
441
+ opened=True,
442
+ show_close_button=True,
443
+ ):
444
+ """Creates an Inspector widget for Earth Engine data.
445
+
446
+ Args:
447
+ host_map (geemap.Map): The map to add the inspector widget to.
448
+ names (list, optional): The list of layer names to be inspected.
449
+ Defaults to None.
450
+ visible (bool, optional): Whether to inspect visible layers only.
451
+ Defaults to True.
452
+ decimals (int, optional): The number of decimal places to round the
453
+ values. Defaults to 2.
454
+ opened (bool, optional): Whether the inspector is opened. Defaults
455
+ to True.
456
+ show_close_button (bool, optional): Whether to show the close
457
+ button. Defaults to True.
458
+ """
459
+
460
+ self._host_map = host_map
461
+ if not host_map:
462
+ raise ValueError("Must pass a valid map when creating an inspector.")
463
+
464
+ self._names = names
465
+ self._visible = visible
466
+ self._decimals = decimals
467
+ self._opened = opened
468
+
469
+ self.on_close = None
470
+
471
+ self._expand_point_tree = False
472
+ self._expand_pixels_tree = True
473
+ self._expand_objects_tree = False
474
+
475
+ host_map.default_style = {"cursor": "crosshair"}
476
+
477
+ left_padded_square = ipywidgets.Layout(
478
+ width="28px", height="28px", padding="0px 0px 0px 4px"
479
+ )
480
+
481
+ self.toolbar_button = ipywidgets.ToggleButton(
482
+ value=opened, tooltip="Inspector", icon="info", layout=left_padded_square
483
+ )
484
+ self.toolbar_button.observe(self._on_toolbar_btn_click, "value")
485
+
486
+ close_button = ipywidgets.ToggleButton(
487
+ value=False,
488
+ tooltip="Close the tool",
489
+ icon="times",
490
+ button_style="primary",
491
+ layout=left_padded_square,
492
+ )
493
+ close_button.observe(self._on_close_btn_click, "value")
494
+
495
+ point_checkbox = self._create_checkbox("Point", self._expand_point_tree)
496
+ pixels_checkbox = self._create_checkbox("Pixels", self._expand_pixels_tree)
497
+ objects_checkbox = self._create_checkbox("Objects", self._expand_objects_tree)
498
+ point_checkbox.observe(self._on_point_checkbox_changed, "value")
499
+ pixels_checkbox.observe(self._on_pixels_checkbox_changed, "value")
500
+ objects_checkbox.observe(self._on_objects_checkbox_changed, "value")
501
+ self.inspector_checks = ipywidgets.HBox(
502
+ children=[
503
+ ipywidgets.Label(
504
+ "Expand", layout=ipywidgets.Layout(padding="0px 8px 0px 4px")
505
+ ),
506
+ point_checkbox,
507
+ pixels_checkbox,
508
+ objects_checkbox,
509
+ ]
510
+ )
511
+
512
+ if show_close_button:
513
+ self.toolbar_header = ipywidgets.HBox(
514
+ children=[close_button, self.toolbar_button]
515
+ )
516
+ else:
517
+ self.toolbar_header = ipywidgets.HBox(children=[self.toolbar_button])
518
+ self.tree_output = ipywidgets.VBox(
519
+ children=[],
520
+ layout=ipywidgets.Layout(
521
+ max_width="600px", max_height="300px", overflow="auto", display="block"
522
+ ),
523
+ )
524
+ self._clear_inspector_output()
525
+
526
+ host_map.on_interaction(self._on_map_interaction)
527
+ self.toolbar_button.value = opened
528
+
529
+ super().__init__(
530
+ children=[self.toolbar_header, self.inspector_checks, self.tree_output]
531
+ )
532
+
533
+ def cleanup(self):
534
+ """Removes the widget from the map and performs cleanup."""
535
+ if self._host_map:
536
+ self._host_map.default_style = {"cursor": "default"}
537
+ self._host_map.on_interaction(self._on_map_interaction, remove=True)
538
+ if self.on_close is not None:
539
+ self.on_close()
540
+
541
+ def _create_checkbox(self, title, checked):
542
+ layout = ipywidgets.Layout(width="auto", padding="0px 6px 0px 0px")
543
+ return ipywidgets.Checkbox(
544
+ description=title, indent=False, value=checked, layout=layout
545
+ )
546
+
547
+ def _on_map_interaction(self, **kwargs):
548
+ latlon = kwargs.get("coordinates")
549
+ if kwargs.get("type") == "click":
550
+ self._on_map_click(latlon)
551
+
552
+ def _on_map_click(self, latlon):
553
+ if self.toolbar_button.value:
554
+ self._host_map.default_style = {"cursor": "wait"}
555
+ self._clear_inspector_output()
556
+
557
+ nodes = [self._point_info(latlon)]
558
+ pixels_node = self._pixels_info(latlon)
559
+ if pixels_node.nodes:
560
+ nodes.append(pixels_node)
561
+ objects_node = self._objects_info(latlon)
562
+ if objects_node.nodes:
563
+ nodes.append(objects_node)
564
+
565
+ self.tree_output.children = [ipytree.Tree(nodes=nodes)]
566
+ self._host_map.default_style = {"cursor": "crosshair"}
567
+
568
+ def _clear_inspector_output(self):
569
+ self.tree_output.children = []
570
+ self.children = []
571
+ self.children = [self.toolbar_header, self.inspector_checks, self.tree_output]
572
+
573
+ def _on_point_checkbox_changed(self, change):
574
+ self._expand_point_tree = change["new"]
575
+
576
+ def _on_pixels_checkbox_changed(self, change):
577
+ self._expand_pixels_tree = change["new"]
578
+
579
+ def _on_objects_checkbox_changed(self, change):
580
+ self._expand_objects_tree = change["new"]
581
+
582
+ def _on_toolbar_btn_click(self, change):
583
+ if change["new"]:
584
+ self._host_map.default_style = {"cursor": "crosshair"}
585
+ self.children = [
586
+ self.toolbar_header,
587
+ self.inspector_checks,
588
+ self.tree_output,
589
+ ]
590
+ self._clear_inspector_output()
591
+ else:
592
+ self.children = [self.toolbar_button]
593
+ self._host_map.default_style = {"cursor": "default"}
594
+
595
+ def _on_close_btn_click(self, change):
596
+ if change["new"]:
597
+ self.cleanup()
598
+
599
+ def _get_visible_map_layers(self):
600
+ layers = {}
601
+ if self._names is not None:
602
+ names = [names] if isinstance(names, str) else self._names
603
+ for name in names:
604
+ if name in self._host_map.ee_layers:
605
+ layers[name] = self._host_map.ee_layers[name]
606
+ else:
607
+ layers = self._host_map.ee_layers
608
+ return {k: v for k, v in layers.items() if v["ee_layer"].visible}
609
+
610
+ def _root_node(self, title, nodes, **kwargs):
611
+ return ipytree.Node(
612
+ title,
613
+ icon="archive",
614
+ nodes=nodes,
615
+ open_icon="plus-square",
616
+ open_icon_style="success",
617
+ close_icon="minus-square",
618
+ close_icon_style="info",
619
+ **kwargs,
620
+ )
621
+
622
+ def _point_info(self, latlon):
623
+ scale = self._host_map.get_scale()
624
+ label = (
625
+ f"Point ({latlon[1]:.{self._decimals}f}, "
626
+ + f"{latlon[0]:.{self._decimals}f}) at {int(scale)}m/px"
627
+ )
628
+ nodes = [
629
+ ipytree.Node(f"Longitude: {latlon[1]}"),
630
+ ipytree.Node(f"Latitude: {latlon[0]}"),
631
+ ipytree.Node(f"Zoom Level: {self._host_map.zoom}"),
632
+ ipytree.Node(f"Scale (approx. m/px): {scale}"),
633
+ ]
634
+ return self._root_node(label, nodes, opened=self._expand_point_tree)
635
+
636
+ def _query_point(self, latlon, ee_object):
637
+ point = ee.Geometry.Point(latlon[::-1])
638
+ scale = self._host_map.get_scale()
639
+ if isinstance(ee_object, ee.ImageCollection):
640
+ ee_object = ee_object.mosaic()
641
+ if isinstance(ee_object, ee.Image):
642
+ return ee_object.reduceRegion(ee.Reducer.first(), point, scale).getInfo()
643
+ return None
644
+
645
+ def _pixels_info(self, latlon):
646
+ if not self._visible:
647
+ return self._root_node("Pixels", [])
648
+
649
+ layers = self._get_visible_map_layers()
650
+ nodes = []
651
+ for layer_name, layer in layers.items():
652
+ ee_object = layer["ee_object"]
653
+ pixel = self._query_point(latlon, ee_object)
654
+ if not pixel:
655
+ continue
656
+ pluralized_band = "band" if len(pixel) == 1 else "bands"
657
+ ee_obj_type = ee_object.__class__.__name__
658
+ label = f"{layer_name}: {ee_obj_type} ({len(pixel)} {pluralized_band})"
659
+ layer_node = ipytree.Node(label, opened=self._expand_pixels_tree)
660
+ for key, value in sorted(pixel.items()):
661
+ if isinstance(value, float):
662
+ value = round(value, self._decimals)
663
+ layer_node.add_node(ipytree.Node(f"{key}: {value}", icon="file"))
664
+ nodes.append(layer_node)
665
+
666
+ return self._root_node("Pixels", nodes)
667
+
668
+ def _get_bbox(self, latlon):
669
+ lat, lon = latlon
670
+ delta = 0.005
671
+ return ee.Geometry.BBox(lon - delta, lat - delta, lon + delta, lat + delta)
672
+
673
+ def _objects_info(self, latlon):
674
+ if not self._visible:
675
+ return self._root_node("Objects", [])
676
+
677
+ layers = self._get_visible_map_layers()
678
+ point = ee.Geometry.Point(latlon[::-1])
679
+ nodes = []
680
+ for layer_name, layer in layers.items():
681
+ ee_object = layer["ee_object"]
682
+ if isinstance(ee_object, ee.FeatureCollection):
683
+ geom = ee.Feature(ee_object.first()).geometry()
684
+ bbox = self._get_bbox(latlon)
685
+ is_point = ee.Algorithms.If(
686
+ geom.type().compareTo(ee.String("Point")), point, bbox
687
+ )
688
+ ee_object = ee_object.filterBounds(is_point).first()
689
+ tree_node = common.get_info(
690
+ ee_object, layer_name, self._expand_objects_tree, True
691
+ )
692
+ if tree_node:
693
+ nodes.append(tree_node)
694
+
695
+ return self._root_node("Objects", nodes)
696
+
697
+
698
+ @Theme.apply
699
+ class LayerManager(ipywidgets.VBox):
700
+ def __init__(self, host_map):
701
+ """Initializes a layer manager widget.
702
+ Args:
703
+ host_map (geemap.Map): The geemap.Map object.
704
+ """
705
+ self._host_map = host_map
706
+ if not host_map:
707
+ raise ValueError("Must pass a valid map when creating a layer manager.")
708
+
709
+ self._collapse_button = ipywidgets.ToggleButton(
710
+ value=False,
711
+ tooltip="Layer Manager",
712
+ icon="server",
713
+ layout=ipywidgets.Layout(
714
+ width="28px", height="28px", padding="0px 0px 0px 4px"
715
+ ),
716
+ )
717
+ self._close_button = ipywidgets.Button(
718
+ tooltip="Close the tool",
719
+ icon="times",
720
+ button_style="primary",
721
+ layout=ipywidgets.Layout(width="28px", height="28px", padding="0px"),
722
+ )
723
+
724
+ self._toolbar_header = ipywidgets.HBox(
725
+ children=[self._close_button, self._collapse_button]
726
+ )
727
+ self._toolbar_footer = ipywidgets.VBox(children=[])
728
+
729
+ self._collapse_button.observe(self._on_collapse_click, "value")
730
+ self._close_button.on_click(self._on_close_click)
731
+
732
+ self.on_close = None
733
+ self.on_open_vis = None
734
+
735
+ self.collapsed = False
736
+ self.header_hidden = False
737
+ self.close_button_hidden = False
738
+
739
+ super().__init__([self._toolbar_header, self._toolbar_footer])
740
+
741
+ @property
742
+ def collapsed(self):
743
+ return not self._collapse_button.value
744
+
745
+ @collapsed.setter
746
+ def collapsed(self, value):
747
+ self._collapse_button.value = not value
748
+
749
+ @property
750
+ def header_hidden(self):
751
+ return self._toolbar_header.layout.display == "none"
752
+
753
+ @header_hidden.setter
754
+ def header_hidden(self, value):
755
+ self._toolbar_header.layout.display = "none" if value else "block"
756
+
757
+ @property
758
+ def close_button_hidden(self):
759
+ return self._close_button.style.display == "none"
760
+
761
+ @close_button_hidden.setter
762
+ def close_button_hidden(self, value):
763
+ self._close_button.style.display = "none" if value else "inline-block"
764
+
765
+ def refresh_layers(self):
766
+ """Recreates all the layer widgets."""
767
+ toggle_all_layout = ipywidgets.Layout(
768
+ height="18px", width="30ex", padding="0px 8px 25px 8px"
769
+ )
770
+ toggle_all_checkbox = ipywidgets.Checkbox(
771
+ value=False,
772
+ description="All layers on/off",
773
+ indent=False,
774
+ layout=toggle_all_layout,
775
+ )
776
+ toggle_all_checkbox.observe(self._on_all_layers_visibility_toggled, "value")
777
+
778
+ layer_rows = []
779
+ # non_basemap_layers = self._host_map.layers[1:] # Skip the basemap.
780
+ for layer in self._host_map.layers:
781
+ layer_rows.append(self._render_layer_row(layer))
782
+ self._toolbar_footer.children = [toggle_all_checkbox] + layer_rows
783
+
784
+ def _on_close_click(self, _):
785
+ if self.on_close:
786
+ self.on_close()
787
+
788
+ def _on_collapse_click(self, change):
789
+ if change["new"]:
790
+ self.refresh_layers()
791
+ self.children = [self._toolbar_header, self._toolbar_footer]
792
+ else:
793
+ self.children = [self._collapse_button]
794
+
795
+ def _render_layer_row(self, layer):
796
+ visibility_checkbox = ipywidgets.Checkbox(
797
+ value=self._compute_layer_visibility(layer),
798
+ description=layer.name,
799
+ indent=False,
800
+ layout=ipywidgets.Layout(height="18px", width="140px"),
801
+ )
802
+ visibility_checkbox.observe(
803
+ lambda change: self._on_layer_visibility_changed(change, layer), "value"
804
+ )
805
+
806
+ opacity_slider = ipywidgets.FloatSlider(
807
+ value=self._compute_layer_opacity(layer),
808
+ min=0,
809
+ max=1,
810
+ step=0.01,
811
+ readout=False,
812
+ layout=ipywidgets.Layout(width="80px"),
813
+ )
814
+ opacity_slider.observe(
815
+ lambda change: self._on_layer_opacity_changed(change, layer), "value"
816
+ )
817
+
818
+ settings_button = ipywidgets.Button(
819
+ icon="gear",
820
+ layout=ipywidgets.Layout(width="25px", height="25px", padding="0px"),
821
+ tooltip=layer.name,
822
+ )
823
+ settings_button.on_click(self._on_layer_settings_click)
824
+
825
+ return ipywidgets.HBox(
826
+ [visibility_checkbox, settings_button, opacity_slider],
827
+ layout=ipywidgets.Layout(padding="0px 8px 0px 8px"),
828
+ )
829
+
830
+ def _compute_layer_opacity(self, layer):
831
+ if layer in self._host_map.geojson_layers:
832
+ opacity = layer.style.get("opacity", 1.0)
833
+ fill_opacity = layer.style.get("fillOpacity", 1.0)
834
+ return max(opacity, fill_opacity)
835
+ return layer.opacity if hasattr(layer, "opacity") else 1.0
836
+
837
+ def _compute_layer_visibility(self, layer):
838
+ return layer.visible if hasattr(layer, "visible") else True
839
+
840
+ def _on_layer_settings_click(self, button):
841
+ if self.on_open_vis:
842
+ self.on_open_vis(button.tooltip)
843
+
844
+ def _on_all_layers_visibility_toggled(self, change):
845
+ checkboxes = [
846
+ row.children[0] for row in self._toolbar_footer.children[1:]
847
+ ] # Skip the all on/off checkbox.
848
+ for checkbox in checkboxes:
849
+ checkbox.value = change["new"]
850
+
851
+ def _on_layer_opacity_changed(self, change, layer):
852
+ if layer in self._host_map.geojson_layers:
853
+ # For non-TileLayer, use layer.style.opacity and layer.style.fillOpacity.
854
+ layer.style.update({"opacity": change["new"], "fillOpacity": change["new"]})
855
+ elif hasattr(layer, "opacity"):
856
+ layer.opacity = change["new"]
857
+
858
+ def _on_layer_visibility_changed(self, change, layer):
859
+ if hasattr(layer, "visible"):
860
+ layer.visible = change["new"]
861
+
862
+ layer_name = change["owner"].description
863
+ if layer_name not in self._host_map.ee_layers:
864
+ return
865
+
866
+ layer_dict = self._host_map.ee_layers[layer_name]
867
+ for attachment_name in ["legend", "colorbar"]:
868
+ attachment = layer_dict.get(attachment_name, None)
869
+ attachment_on_map = attachment in self._host_map.controls
870
+ if change["new"] and not attachment_on_map:
871
+ try:
872
+ self._host_map.add(attachment)
873
+ except:
874
+ from ipyleaflet import WidgetControl
875
+
876
+ widget = attachment.widget
877
+ position = attachment.position
878
+ control = WidgetControl(widget=widget, position=position)
879
+ self._host_map.add(control)
880
+ layer_dict["colorbar"] = control
881
+
882
+ elif not change["new"] and attachment_on_map:
883
+ self._host_map.remove_control(attachment)
884
+
885
+
886
+ @Theme.apply
887
+ class Basemap(ipywidgets.HBox):
888
+ """Widget for selecting a basemap."""
889
+
890
+ def __init__(self, basemaps, value):
891
+ """Creates a widget for selecting a basemap.
892
+
893
+ Args:
894
+ basemaps (list): The list of basemap names to make available for selection.
895
+ value (str): The default value from basemaps to select.
896
+ """
897
+ self.on_close = None
898
+ self.on_basemap_changed = None
899
+
900
+ self._dropdown = ipywidgets.Dropdown(
901
+ options=list(basemaps),
902
+ value=value,
903
+ layout=ipywidgets.Layout(width="200px"),
904
+ )
905
+ self._dropdown.observe(self._on_dropdown_click, "value")
906
+
907
+ close_button = ipywidgets.Button(
908
+ icon="times",
909
+ tooltip="Close the basemap widget",
910
+ button_style="primary",
911
+ layout=ipywidgets.Layout(width="32px"),
912
+ )
913
+ close_button.on_click(self._on_close_click)
914
+
915
+ super().__init__([self._dropdown, close_button])
916
+
917
+ def _on_dropdown_click(self, change):
918
+ if self.on_basemap_changed and change["new"]:
919
+ self.on_basemap_changed(self._dropdown.value)
920
+
921
+ def cleanup(self):
922
+ if self.on_close:
923
+ self.on_close()
924
+
925
+ def _on_close_click(self, _):
926
+ self.cleanup()
927
+
928
+
929
+ @Theme.apply
930
+ class LayerEditor(ipywidgets.VBox):
931
+ """Widget for displaying and editing layer visualization properties."""
932
+
933
+ def __init__(self, host_map, layer_dict):
934
+ """Initializes a layer editor widget.
935
+
936
+ Args:
937
+ host_map (geemap.Map): The geemap.Map object.
938
+ layer_dict (dict): The layer object to edit.
939
+ """
940
+
941
+ self.on_close = None
942
+
943
+ self._host_map = host_map
944
+ if not host_map:
945
+ raise ValueError(
946
+ f"Must pass a valid map when creating a {self.__class__.__name__} widget."
947
+ )
948
+
949
+ self._toggle_button = ipywidgets.ToggleButton(
950
+ value=True,
951
+ tooltip="Layer editor",
952
+ icon="gear",
953
+ layout=ipywidgets.Layout(
954
+ width="28px", height="28px", padding="0px 0 0 3px"
955
+ ),
956
+ )
957
+ self._toggle_button.observe(self._on_toggle_click, "value")
958
+
959
+ self._close_button = ipywidgets.Button(
960
+ tooltip="Close the vis params dialog",
961
+ icon="times",
962
+ button_style="primary",
963
+ layout=ipywidgets.Layout(width="28px", height="28px", padding="0"),
964
+ )
965
+ self._close_button.on_click(self._on_close_click)
966
+
967
+ layout = ipywidgets.Layout(width="95px")
968
+ self._import_button = ipywidgets.Button(
969
+ description="Import",
970
+ button_style="primary",
971
+ tooltip="Import vis params to notebook",
972
+ layout=layout,
973
+ )
974
+ self._apply_button = ipywidgets.Button(
975
+ description="Apply", tooltip="Apply vis params to the layer", layout=layout
976
+ )
977
+ self._import_button.on_click(self._on_import_click)
978
+ self._apply_button.on_click(self._on_apply_click)
979
+
980
+ self._label = ipywidgets.Label(
981
+ value="Layer name",
982
+ layout=ipywidgets.Layout(max_width="250px", padding="1px 8px 0 4px"),
983
+ )
984
+ self._embedded_widget = ipywidgets.Label(value="Vis params are uneditable")
985
+ if layer_dict is not None:
986
+ self._ee_object = layer_dict["ee_object"]
987
+ if isinstance(self._ee_object, (ee.Feature, ee.Geometry)):
988
+ self._ee_object = ee.FeatureCollection(self._ee_object)
989
+
990
+ self._ee_layer = layer_dict["ee_layer"]
991
+ self._label.value = self._ee_layer.name
992
+ if isinstance(self._ee_object, ee.FeatureCollection):
993
+ self._embedded_widget = _VectorLayerEditor(
994
+ host_map=host_map, layer_dict=layer_dict
995
+ )
996
+ elif isinstance(self._ee_object, ee.Image):
997
+ self._embedded_widget = _RasterLayerEditor(
998
+ host_map=host_map, layer_dict=layer_dict
999
+ )
1000
+
1001
+ super().__init__(children=[])
1002
+ self._on_toggle_click({"new": True})
1003
+
1004
+ def _on_toggle_click(self, change):
1005
+ if change["new"]:
1006
+ self.children = [
1007
+ ipywidgets.HBox([self._close_button, self._toggle_button, self._label]),
1008
+ self._embedded_widget,
1009
+ ipywidgets.HBox([self._import_button, self._apply_button]),
1010
+ ]
1011
+ else:
1012
+ self.children = [
1013
+ ipywidgets.HBox([self._close_button, self._toggle_button, self._label]),
1014
+ ]
1015
+
1016
+ def _on_import_click(self, _):
1017
+ self._embedded_widget.on_import_click()
1018
+
1019
+ def _on_apply_click(self, _):
1020
+ self._embedded_widget.on_apply_click()
1021
+
1022
+ def _on_close_click(self, _):
1023
+ if self.on_close:
1024
+ self.on_close()
1025
+
1026
+
1027
+ @Theme.apply
1028
+ class _RasterLayerEditor(ipywidgets.VBox):
1029
+ """Widget for displaying and editing layer visualization properties for raster layers."""
1030
+
1031
+ def __init__(self, host_map, layer_dict):
1032
+ """Initializes a raster layer editor widget.
1033
+
1034
+ Args:
1035
+ host_map (geemap.Map): The geemap.Map object.
1036
+ layer_dict (dict): The layer object to edit.
1037
+ """
1038
+ self._host_map = host_map
1039
+ self._layer_dict = layer_dict
1040
+
1041
+ self._ee_object = layer_dict["ee_object"]
1042
+ self._ee_layer = layer_dict["ee_layer"]
1043
+ self._vis_params = layer_dict["vis_params"]
1044
+
1045
+ self._layer_name = self._ee_layer.name
1046
+ self._layer_opacity = self._ee_layer.opacity
1047
+
1048
+ self._min_value = 0
1049
+ self._max_value = 100
1050
+ self._sel_bands = None
1051
+ self._layer_palette = []
1052
+ self._layer_gamma = 1
1053
+ self._left_value = 0
1054
+ self._right_value = 10000
1055
+
1056
+ band_names = self._ee_object.bandNames().getInfo()
1057
+ self._band_count = len(band_names)
1058
+
1059
+ if "min" in self._vis_params.keys():
1060
+ self._min_value = self._vis_params["min"]
1061
+ if self._min_value < self._left_value:
1062
+ self._left_value = self._min_value - self._max_value
1063
+ if "max" in self._vis_params.keys():
1064
+ self._max_value = self._vis_params["max"]
1065
+ self._right_value = 2 * self._max_value
1066
+ if "gamma" in self._vis_params.keys():
1067
+ if isinstance(self._vis_params["gamma"], list):
1068
+ self._layer_gamma = self._vis_params["gamma"][0]
1069
+ else:
1070
+ self._layer_gamma = self._vis_params["gamma"]
1071
+ if "bands" in self._vis_params.keys():
1072
+ self._sel_bands = self._vis_params["bands"]
1073
+ if "palette" in self._vis_params.keys():
1074
+ self._layer_palette = [
1075
+ color.replace("#", "") for color in list(self._vis_params["palette"])
1076
+ ]
1077
+
1078
+ # ipywidgets doesn't support horizontal radio buttons
1079
+ # (https://github.com/jupyter-widgets/ipywidgets/issues/1247). Instead,
1080
+ # use two individual radio buttons with some hackery.
1081
+ self._greyscale_radio_button = ipywidgets.RadioButtons(
1082
+ options=["1 band (Grayscale)"],
1083
+ layout={"width": "max-content", "margin": "0 15px 0 0"},
1084
+ )
1085
+ self._rgb_radio_button = ipywidgets.RadioButtons(
1086
+ options=["3 bands (RGB)"], layout={"width": "max-content"}
1087
+ )
1088
+ self._greyscale_radio_button.index = None
1089
+ self._rgb_radio_button.index = None
1090
+
1091
+ band_dropdown_layout = ipywidgets.Layout(width="98px")
1092
+ self._band_1_dropdown = ipywidgets.Dropdown(
1093
+ options=band_names, value=band_names[0], layout=band_dropdown_layout
1094
+ )
1095
+ self._band_2_dropdown = ipywidgets.Dropdown(
1096
+ options=band_names, value=band_names[0], layout=band_dropdown_layout
1097
+ )
1098
+ self._band_3_dropdown = ipywidgets.Dropdown(
1099
+ options=band_names, value=band_names[0], layout=band_dropdown_layout
1100
+ )
1101
+ self._bands_hbox = ipywidgets.HBox(layout=ipywidgets.Layout(margin="0 0 6px 0"))
1102
+
1103
+ self._color_picker = ipywidgets.ColorPicker(
1104
+ concise=False,
1105
+ value="#000000",
1106
+ layout=ipywidgets.Layout(width="116px"),
1107
+ style={"description_width": "initial"},
1108
+ )
1109
+
1110
+ self._add_color_button = ipywidgets.Button(
1111
+ icon="plus",
1112
+ tooltip="Add a hex color string to the palette",
1113
+ layout=ipywidgets.Layout(width="32px"),
1114
+ )
1115
+ self._del_color_button = ipywidgets.Button(
1116
+ icon="minus",
1117
+ tooltip="Remove a hex color string from the palette",
1118
+ layout=ipywidgets.Layout(width="32px"),
1119
+ )
1120
+ self._reset_color_button = ipywidgets.Button(
1121
+ icon="eraser",
1122
+ tooltip="Remove all color strings from the palette",
1123
+ layout=ipywidgets.Layout(width="34px"),
1124
+ )
1125
+ self._add_color_button.on_click(self._add_color_clicked)
1126
+ self._del_color_button.on_click(self._del_color_clicked)
1127
+ self._reset_color_button.on_click(self._reset_color_clicked)
1128
+
1129
+ self._classes_dropdown = ipywidgets.Dropdown(
1130
+ options=["Any"] + [str(i) for i in range(3, 13)],
1131
+ description="Classes:",
1132
+ layout=ipywidgets.Layout(width="115px"),
1133
+ style={"description_width": "initial"},
1134
+ )
1135
+ self._classes_dropdown.observe(self._classes_changed, "value")
1136
+
1137
+ self._colormap_dropdown = ipywidgets.Dropdown(
1138
+ options=self._get_colormaps(),
1139
+ value=None,
1140
+ description="Colormap:",
1141
+ layout=ipywidgets.Layout(width="181px"),
1142
+ style={"description_width": "initial"},
1143
+ )
1144
+ self._colormap_dropdown.observe(self._colormap_changed, "value")
1145
+
1146
+ self._palette_label = ipywidgets.Text(
1147
+ value=", ".join(self._layer_palette),
1148
+ placeholder="List of hex color code (RRGGBB)",
1149
+ description="Palette:",
1150
+ tooltip="Enter a list of hex color code (RRGGBB)",
1151
+ layout=ipywidgets.Layout(width="300px"),
1152
+ style={"description_width": "initial"},
1153
+ )
1154
+
1155
+ self._stretch_dropdown = ipywidgets.Dropdown(
1156
+ options={
1157
+ "Custom": {},
1158
+ "1 σ": {"sigma": 1},
1159
+ "2 σ": {"sigma": 2},
1160
+ "3 σ": {"sigma": 3},
1161
+ "90%": {"percent": 0.90},
1162
+ "98%": {"percent": 0.98},
1163
+ "100%": {"percent": 1.0},
1164
+ },
1165
+ description="Stretch:",
1166
+ layout=ipywidgets.Layout(width="260px"),
1167
+ style={"description_width": "initial"},
1168
+ )
1169
+
1170
+ self._stretch_button = ipywidgets.Button(
1171
+ disabled=True,
1172
+ tooltip="Re-calculate stretch",
1173
+ layout=ipywidgets.Layout(width="36px"),
1174
+ icon="refresh",
1175
+ )
1176
+ self._stretch_dropdown.observe(self._value_stretch_changed, names="value")
1177
+ self._stretch_button.on_click(self._update_stretch)
1178
+
1179
+ self._value_range_slider = ipywidgets.FloatRangeSlider(
1180
+ value=[self._min_value, self._max_value],
1181
+ min=self._left_value,
1182
+ max=self._right_value,
1183
+ step=0.1,
1184
+ description="Range:",
1185
+ disabled=False,
1186
+ continuous_update=False,
1187
+ readout=True,
1188
+ readout_format=".1f",
1189
+ layout=ipywidgets.Layout(width="300px"),
1190
+ style={"description_width": "45px"},
1191
+ )
1192
+
1193
+ self._opacity_slider = ipywidgets.FloatSlider(
1194
+ value=self._layer_opacity,
1195
+ min=0,
1196
+ max=1,
1197
+ step=0.01,
1198
+ description="Opacity:",
1199
+ continuous_update=False,
1200
+ readout=True,
1201
+ readout_format=".2f",
1202
+ layout=ipywidgets.Layout(width="320px"),
1203
+ style={"description_width": "50px"},
1204
+ )
1205
+
1206
+ self._gamma_slider = ipywidgets.FloatSlider(
1207
+ value=self._layer_gamma,
1208
+ min=0.1,
1209
+ max=10,
1210
+ step=0.01,
1211
+ description="Gamma:",
1212
+ continuous_update=False,
1213
+ readout=True,
1214
+ readout_format=".2f",
1215
+ layout=ipywidgets.Layout(width="320px"),
1216
+ style={"description_width": "50px"},
1217
+ )
1218
+
1219
+ self._legend_checkbox = ipywidgets.Checkbox(
1220
+ value=False,
1221
+ description="Legend",
1222
+ indent=False,
1223
+ layout=ipywidgets.Layout(width="70px"),
1224
+ )
1225
+
1226
+ self._linear_checkbox = ipywidgets.Checkbox(
1227
+ value=True,
1228
+ description="Linear colormap",
1229
+ indent=False,
1230
+ layout=ipywidgets.Layout(width="150px"),
1231
+ )
1232
+ self._step_checkbox = ipywidgets.Checkbox(
1233
+ value=False,
1234
+ description="Step colormap",
1235
+ indent=False,
1236
+ layout=ipywidgets.Layout(width="140px"),
1237
+ )
1238
+ self._linear_checkbox.observe(self._linear_checkbox_changed, "value")
1239
+ self._step_checkbox.observe(self._step_checkbox_changed, "value")
1240
+
1241
+ self._legend_title_label = ipywidgets.Text(
1242
+ value="Legend",
1243
+ description="Legend title:",
1244
+ tooltip="Enter a title for the legend",
1245
+ layout=ipywidgets.Layout(width="300px"),
1246
+ style={"description_width": "initial"},
1247
+ )
1248
+
1249
+ self._legend_labels_label = ipywidgets.Text(
1250
+ value="Class 1, Class 2, Class 3",
1251
+ description="Legend labels:",
1252
+ tooltip="Enter a a list of labels for the legend",
1253
+ layout=ipywidgets.Layout(width="300px"),
1254
+ style={"description_width": "initial"},
1255
+ )
1256
+
1257
+ self._stretch_hbox = ipywidgets.HBox(
1258
+ [self._stretch_dropdown, self._stretch_button]
1259
+ )
1260
+ self._colormap_hbox = ipywidgets.HBox(
1261
+ [self._linear_checkbox, self._step_checkbox]
1262
+ )
1263
+ self._legend_vbox = ipywidgets.VBox()
1264
+
1265
+ self._colorbar_output = ipywidgets.Output(
1266
+ layout=ipywidgets.Layout(height="60px", max_width="300px")
1267
+ )
1268
+
1269
+ self._legend_checkbox.observe(self._legend_checkbox_changed, "value")
1270
+
1271
+ children = []
1272
+ if self._band_count < 3:
1273
+ self._greyscale_radio_button.index = 0
1274
+ self._band_1_dropdown.layout.width = "300px"
1275
+ self._bands_hbox.children = [self._band_1_dropdown]
1276
+ children = self._get_tool_layout(grayscale=True)
1277
+ self._legend_checkbox.value = False
1278
+
1279
+ if len(self._palette_label.value) > 0 and "," in self._palette_label.value:
1280
+ colors = common.to_hex_colors(
1281
+ [color.strip() for color in self._palette_label.value.split(",")]
1282
+ )
1283
+ self._render_colorbar(colors)
1284
+ else:
1285
+ self._rgb_radio_button.index = 0
1286
+ sel_bands = self._sel_bands
1287
+ if (sel_bands is None) or (len(sel_bands) < 2):
1288
+ sel_bands = band_names[0:3]
1289
+ self._band_1_dropdown.value = sel_bands[0]
1290
+ self._band_2_dropdown.value = sel_bands[1]
1291
+ self._band_3_dropdown.value = sel_bands[2]
1292
+ self._bands_hbox.children = [
1293
+ self._band_1_dropdown,
1294
+ self._band_2_dropdown,
1295
+ self._band_3_dropdown,
1296
+ ]
1297
+ children = self._get_tool_layout(grayscale=False)
1298
+
1299
+ self._greyscale_radio_button.observe(self._radio1_observer, names=["value"])
1300
+ self._rgb_radio_button.observe(self._radio2_observer, names=["value"])
1301
+
1302
+ super().__init__(
1303
+ layout=ipywidgets.Layout(
1304
+ padding="5px 0px 5px 8px", # top, right, bottom, left
1305
+ # width="330px",
1306
+ max_height="305px",
1307
+ overflow="auto",
1308
+ display="block",
1309
+ ),
1310
+ children=children,
1311
+ )
1312
+
1313
+ def _value_stretch_changed(self, value):
1314
+ """Apply the selected stretch option and update widget states."""
1315
+ stretch_option = value["new"]
1316
+
1317
+ if stretch_option:
1318
+ self._stretch_button.disabled = False
1319
+ self._value_range_slider.disabled = True
1320
+ self._update_stretch()
1321
+ else:
1322
+ self._stretch_button.disabled = True
1323
+ self._value_range_slider.disabled = False
1324
+
1325
+ def _update_stretch(self, *_):
1326
+ """Calculate and set the range slider by applying stretch parameters."""
1327
+ stretch_params = self._stretch_dropdown.value
1328
+
1329
+ (s, w), (n, e) = self._host_map.bounds
1330
+ map_bbox = ee.Geometry.BBox(west=w, south=s, east=e, north=n)
1331
+ vis_bands = set((b.value for b in self._bands_hbox.children))
1332
+ min_val, max_val = self._ee_layer.calculate_vis_minmax(
1333
+ bounds=map_bbox, bands=vis_bands, **stretch_params
1334
+ )
1335
+
1336
+ # Update in the correct order to avoid setting an invalid range
1337
+ if min_val > self._value_range_slider.max:
1338
+ self._value_range_slider.max = max_val
1339
+ self._value_range_slider.min = min_val
1340
+ else:
1341
+ self._value_range_slider.min = min_val
1342
+ self._value_range_slider.max = max_val
1343
+
1344
+ self._value_range_slider.value = [min_val, max_val]
1345
+
1346
+ def _get_tool_layout(self, grayscale):
1347
+ return [
1348
+ ipywidgets.HBox([self._greyscale_radio_button, self._rgb_radio_button]),
1349
+ self._bands_hbox,
1350
+ self._stretch_hbox,
1351
+ self._value_range_slider,
1352
+ self._opacity_slider,
1353
+ self._gamma_slider,
1354
+ ] + (
1355
+ [
1356
+ ipywidgets.HBox([self._classes_dropdown, self._colormap_dropdown]),
1357
+ self._palette_label,
1358
+ self._colorbar_output,
1359
+ ipywidgets.HBox(
1360
+ [
1361
+ self._legend_checkbox,
1362
+ self._color_picker,
1363
+ self._add_color_button,
1364
+ self._del_color_button,
1365
+ self._reset_color_button,
1366
+ ]
1367
+ ),
1368
+ self._legend_vbox,
1369
+ ]
1370
+ if grayscale
1371
+ else []
1372
+ )
1373
+
1374
+ def _get_colormaps(self):
1375
+ from matplotlib import pyplot # pylint: disable=import-outside-toplevel
1376
+
1377
+ colormap_options = pyplot.colormaps()
1378
+ colormap_options.sort()
1379
+ return colormap_options
1380
+
1381
+ def _render_colorbar(self, colors):
1382
+ import matplotlib # pylint: disable=import-outside-toplevel
1383
+ from matplotlib import pyplot # pylint: disable=import-outside-toplevel
1384
+
1385
+ colors = common.to_hex_colors(colors)
1386
+
1387
+ _, ax = pyplot.subplots(figsize=(4, 0.3))
1388
+ cmap = matplotlib.colors.LinearSegmentedColormap.from_list(
1389
+ "custom", colors, N=256
1390
+ )
1391
+ norm = matplotlib.colors.Normalize(
1392
+ vmin=self._value_range_slider.value[0],
1393
+ vmax=self._value_range_slider.value[1],
1394
+ )
1395
+ matplotlib.colorbar.ColorbarBase(
1396
+ ax, norm=norm, cmap=cmap, orientation="horizontal"
1397
+ )
1398
+
1399
+ self._palette_label.value = ", ".join(colors)
1400
+
1401
+ self._colorbar_output.clear_output()
1402
+ with self._colorbar_output:
1403
+ pyplot.show()
1404
+
1405
+ def _classes_changed(self, change):
1406
+ import matplotlib # pylint: disable=import-outside-toplevel
1407
+ from matplotlib import pyplot # pylint: disable=import-outside-toplevel
1408
+
1409
+ if not change["new"]:
1410
+ return
1411
+
1412
+ selected = change["owner"].value
1413
+ if self._colormap_dropdown.value is not None:
1414
+ n_class = None
1415
+ if selected != "Any":
1416
+ n_class = int(self._classes_dropdown.value)
1417
+
1418
+ colors = pyplot.cm.get_cmap(self._colormap_dropdown.value, n_class)
1419
+ cmap_colors = [
1420
+ matplotlib.colors.rgb2hex(colors(i))[1:] for i in range(colors.N)
1421
+ ]
1422
+ self._render_colorbar(cmap_colors)
1423
+
1424
+ if len(self._palette_label.value) > 0 and "," in self._palette_label.value:
1425
+ labels = [
1426
+ f"Class {i+1}"
1427
+ for i in range(len(self._palette_label.value.split(",")))
1428
+ ]
1429
+ self._legend_labels_label.value = ", ".join(labels)
1430
+
1431
+ def _add_color_clicked(self, _):
1432
+ if self._color_picker.value is not None:
1433
+ if len(self._palette_label.value) == 0:
1434
+ self._palette_label.value = self._color_picker.value[1:]
1435
+ else:
1436
+ self._palette_label.value += ", " + self._color_picker.value[1:]
1437
+
1438
+ def _del_color_clicked(self, _):
1439
+ if "," in self._palette_label.value:
1440
+ items = [item.strip() for item in self._palette_label.value.split(",")]
1441
+ self._palette_label.value = ", ".join(items[:-1])
1442
+ else:
1443
+ self._palette_label.value = ""
1444
+
1445
+ def _reset_color_clicked(self, _):
1446
+ self._palette_label.value = ""
1447
+
1448
+ def _linear_checkbox_changed(self, change):
1449
+ if change["new"]:
1450
+ self._step_checkbox.value = False
1451
+ self._legend_vbox.children = [self._colormap_hbox]
1452
+ else:
1453
+ self._step_checkbox.value = True
1454
+
1455
+ def _step_checkbox_changed(self, change):
1456
+ if change["new"]:
1457
+ self._linear_checkbox.value = False
1458
+ if len(self._layer_palette) > 0:
1459
+ self._legend_labels_label.value = ",".join(
1460
+ ["Class " + str(i) for i in range(1, len(self._layer_palette) + 1)]
1461
+ )
1462
+ self._legend_vbox.children = [
1463
+ self._colormap_hbox,
1464
+ self._legend_title_label,
1465
+ self._legend_labels_label,
1466
+ ]
1467
+ else:
1468
+ self._linear_checkbox.value = True
1469
+
1470
+ def _colormap_changed(self, change):
1471
+ import matplotlib # pylint: disable=import-outside-toplevel
1472
+ from matplotlib import pyplot # pylint: disable=import-outside-toplevel
1473
+
1474
+ if change["new"]:
1475
+ n_class = None
1476
+ if self._classes_dropdown.value != "Any":
1477
+ n_class = int(self._classes_dropdown.value)
1478
+
1479
+ colors = pyplot.cm.get_cmap(self._colormap_dropdown.value, n_class)
1480
+ cmap_colors = [
1481
+ matplotlib.colors.rgb2hex(colors(i))[1:] for i in range(colors.N)
1482
+ ]
1483
+ self._render_colorbar(cmap_colors)
1484
+
1485
+ if len(self._palette_label.value) > 0 and "," in self._palette_label.value:
1486
+ labels = [
1487
+ f"Class {i+1}"
1488
+ for i in range(len(self._palette_label.value.split(",")))
1489
+ ]
1490
+ self._legend_labels_label.value = ", ".join(labels)
1491
+
1492
+ def on_import_click(self):
1493
+ vis = {}
1494
+ if self._greyscale_radio_button.index == 0:
1495
+ vis["bands"] = [self._band_1_dropdown.value]
1496
+ if len(self._palette_label.value) > 0:
1497
+ vis["palette"] = self._palette_label.value.split(",")
1498
+ else:
1499
+ vis["bands"] = [
1500
+ self._band_1_dropdown.value,
1501
+ self._band_2_dropdown.value,
1502
+ self._band_3_dropdown.value,
1503
+ ]
1504
+
1505
+ vis["min"] = self._value_range_slider.value[0]
1506
+ vis["max"] = self._value_range_slider.value[1]
1507
+ vis["opacity"] = self._opacity_slider.value
1508
+ vis["gamma"] = self._gamma_slider.value
1509
+
1510
+ common.create_code_cell(f"vis_params = {str(vis)}")
1511
+ print(f"vis_params = {str(vis)}")
1512
+
1513
+ def on_apply_click(self):
1514
+ vis = {}
1515
+ if self._greyscale_radio_button.index == 0:
1516
+ vis["bands"] = [self._band_1_dropdown.value]
1517
+ if len(self._palette_label.value) > 0:
1518
+ vis["palette"] = [
1519
+ c.strip() for c in self._palette_label.value.split(",")
1520
+ ]
1521
+ else:
1522
+ vis["bands"] = [
1523
+ self._band_1_dropdown.value,
1524
+ self._band_2_dropdown.value,
1525
+ self._band_3_dropdown.value,
1526
+ ]
1527
+ vis["gamma"] = self._gamma_slider.value
1528
+
1529
+ vis["min"] = self._value_range_slider.value[0]
1530
+ vis["max"] = self._value_range_slider.value[1]
1531
+
1532
+ self._host_map.add_layer(
1533
+ self._ee_object, vis, self._layer_name, True, self._opacity_slider.value
1534
+ )
1535
+ self._ee_layer.visible = False
1536
+
1537
+ def _remove_control(key):
1538
+ if widget := self._layer_dict.get(key, None):
1539
+ if widget in self._host_map.controls:
1540
+ self._host_map.remove(widget)
1541
+ del self._layer_dict[key]
1542
+
1543
+ if self._legend_checkbox.value:
1544
+ _remove_control("colorbar")
1545
+ if self._linear_checkbox.value:
1546
+ _remove_control("legend")
1547
+
1548
+ if (
1549
+ len(self._palette_label.value) > 0
1550
+ and "," in self._palette_label.value
1551
+ ):
1552
+ colors = common.to_hex_colors(
1553
+ [
1554
+ color.strip()
1555
+ for color in self._palette_label.value.split(",")
1556
+ ]
1557
+ )
1558
+
1559
+ if hasattr(self._host_map, "colorbar"):
1560
+ self._host_map.add_colorbar(
1561
+ vis_params={
1562
+ "palette": colors,
1563
+ "min": self._value_range_slider.value[0],
1564
+ "max": self._value_range_slider.value[1],
1565
+ },
1566
+ layer_name=self._layer_name,
1567
+ )
1568
+ elif self._step_checkbox.value:
1569
+ if (
1570
+ len(self._palette_label.value) > 0
1571
+ and "," in self._palette_label.value
1572
+ ):
1573
+ colors = common.to_hex_colors(
1574
+ [
1575
+ color.strip()
1576
+ for color in self._palette_label.value.split(",")
1577
+ ]
1578
+ )
1579
+ labels = [
1580
+ label.strip()
1581
+ for label in self._legend_labels_label.value.split(",")
1582
+ ]
1583
+
1584
+ if hasattr(self._host_map, "add_legend"):
1585
+ self._host_map.add_legend(
1586
+ title=self._legend_title_label.value,
1587
+ legend_keys=labels,
1588
+ legend_colors=colors,
1589
+ layer_name=self._layer_name,
1590
+ )
1591
+ else:
1592
+ if self._greyscale_radio_button.index == 0 and "palette" in vis:
1593
+ self._render_colorbar(vis["palette"])
1594
+ _remove_control("colorbar")
1595
+ _remove_control("legend")
1596
+
1597
+ def _legend_checkbox_changed(self, change):
1598
+ if change["new"]:
1599
+ self._linear_checkbox.value = True
1600
+ self._legend_vbox.children = [
1601
+ ipywidgets.HBox([self._linear_checkbox, self._step_checkbox]),
1602
+ ]
1603
+ else:
1604
+ self._legend_vbox.children = []
1605
+
1606
+ def _radio1_observer(self, _):
1607
+ self._rgb_radio_button.unobserve(self._radio2_observer, names=["value"])
1608
+ self._rgb_radio_button.index = None
1609
+ self._rgb_radio_button.observe(self._radio2_observer, names=["value"])
1610
+ self._band_1_dropdown.layout.width = "300px"
1611
+ self._bands_hbox.children = [self._band_1_dropdown]
1612
+ self._palette_label.value = ", ".join(self._layer_palette)
1613
+ self._palette_label.disabled = False
1614
+ self._color_picker.disabled = False
1615
+ self._add_color_button.disabled = False
1616
+ self._del_color_button.disabled = False
1617
+ self._reset_color_button.disabled = False
1618
+ self.children = self._get_tool_layout(grayscale=True)
1619
+
1620
+ if len(self._palette_label.value) > 0 and "," in self._palette_label.value:
1621
+ colors = [color.strip() for color in self._palette_label.value.split(",")]
1622
+ self._render_colorbar(colors)
1623
+
1624
+ def _radio2_observer(self, _):
1625
+ dropdown_width = "98px"
1626
+ self._greyscale_radio_button.unobserve(self._radio1_observer, names=["value"])
1627
+ self._greyscale_radio_button.index = None
1628
+ self._greyscale_radio_button.observe(self._radio1_observer, names=["value"])
1629
+ self._band_1_dropdown.layout.width = dropdown_width
1630
+ self._bands_hbox.children = [
1631
+ self._band_1_dropdown,
1632
+ self._band_2_dropdown,
1633
+ self._band_3_dropdown,
1634
+ ]
1635
+ self._palette_label.value = ""
1636
+ self._palette_label.disabled = True
1637
+ self._color_picker.disabled = True
1638
+ self._add_color_button.disabled = True
1639
+ self._del_color_button.disabled = True
1640
+ self._reset_color_button.disabled = True
1641
+ self.children = self._get_tool_layout(grayscale=False)
1642
+ self._colorbar_output.clear_output()
1643
+
1644
+
1645
+ @Theme.apply
1646
+ class _VectorLayerEditor(ipywidgets.VBox):
1647
+ """Widget for displaying and editing layer visualization properties."""
1648
+
1649
+ _POINT_SHAPES = [
1650
+ "circle",
1651
+ "square",
1652
+ "diamond",
1653
+ "cross",
1654
+ "plus",
1655
+ "pentagram",
1656
+ "hexagram",
1657
+ "triangle",
1658
+ "triangle_up",
1659
+ "triangle_down",
1660
+ "triangle_left",
1661
+ "triangle_right",
1662
+ "pentagon",
1663
+ "hexagon",
1664
+ "star5",
1665
+ "star6",
1666
+ ]
1667
+
1668
+ @property
1669
+ def _layer_name(self):
1670
+ return self._ee_layer.name
1671
+
1672
+ @property
1673
+ def _layer_opacity(self):
1674
+ return self._ee_layer.opacity
1675
+
1676
+ def __init__(self, host_map, layer_dict):
1677
+ """Initializes a layer manager widget.
1678
+
1679
+ Args:
1680
+ host_map (geemap.Map): The geemap.Map object.
1681
+ """
1682
+
1683
+ self._host_map = host_map
1684
+ if not host_map:
1685
+ raise ValueError("Must pass a valid map when creating a layer manager.")
1686
+
1687
+ self._layer_dict = layer_dict
1688
+
1689
+ self._ee_object = layer_dict["ee_object"]
1690
+ if isinstance(self._ee_object, (ee.Feature, ee.Geometry)):
1691
+ self._ee_object = ee.FeatureCollection(self._ee_object)
1692
+
1693
+ self._ee_layer = layer_dict["ee_layer"]
1694
+
1695
+ self._new_layer_name = ipywidgets.Text(
1696
+ value=f"{self._layer_name} style",
1697
+ description="New layer name:",
1698
+ style={"description_width": "initial"},
1699
+ )
1700
+
1701
+ self._color_picker = ipywidgets.ColorPicker(
1702
+ concise=False,
1703
+ value="#000000",
1704
+ description="Color:",
1705
+ layout=ipywidgets.Layout(width="140px"),
1706
+ style={"description_width": "initial"},
1707
+ )
1708
+
1709
+ self._color_opacity_slider = ipywidgets.FloatSlider(
1710
+ value=self._layer_opacity,
1711
+ min=0,
1712
+ max=1,
1713
+ step=0.01,
1714
+ description="Opacity:",
1715
+ continuous_update=True,
1716
+ readout=False,
1717
+ layout=ipywidgets.Layout(width="130px"),
1718
+ style={"description_width": "50px"},
1719
+ )
1720
+ self._color_opacity_slider.observe(self._color_opacity_change, names="value")
1721
+
1722
+ self._color_opacity_label = ipywidgets.Label(
1723
+ style={"description_width": "initial"},
1724
+ layout=ipywidgets.Layout(padding="0px"),
1725
+ )
1726
+
1727
+ self._point_size_label = ipywidgets.IntText(
1728
+ value=3,
1729
+ description="Point size:",
1730
+ layout=ipywidgets.Layout(width="110px"),
1731
+ style={"description_width": "initial"},
1732
+ )
1733
+
1734
+ self._point_shape_dropdown = ipywidgets.Dropdown(
1735
+ options=self._POINT_SHAPES,
1736
+ value="circle",
1737
+ description="Point shape:",
1738
+ layout=ipywidgets.Layout(width="185px"),
1739
+ style={"description_width": "initial"},
1740
+ )
1741
+
1742
+ self._line_width_label = ipywidgets.IntText(
1743
+ value=2,
1744
+ description="Line width:",
1745
+ layout=ipywidgets.Layout(width="110px"),
1746
+ style={"description_width": "initial"},
1747
+ )
1748
+
1749
+ self._line_type_label = ipywidgets.Dropdown(
1750
+ options=["solid", "dotted", "dashed"],
1751
+ value="solid",
1752
+ description="Line type:",
1753
+ layout=ipywidgets.Layout(width="185px"),
1754
+ style={"description_width": "initial"},
1755
+ )
1756
+
1757
+ self._fill_color_picker = ipywidgets.ColorPicker(
1758
+ concise=False,
1759
+ value="#000000",
1760
+ description="Fill Color:",
1761
+ layout=ipywidgets.Layout(width="160px"),
1762
+ style={"description_width": "initial"},
1763
+ )
1764
+
1765
+ self._fill_color_opacity_slider = ipywidgets.FloatSlider(
1766
+ value=0.66,
1767
+ min=0,
1768
+ max=1,
1769
+ step=0.01,
1770
+ description="Opacity:",
1771
+ continuous_update=True,
1772
+ readout=False,
1773
+ layout=ipywidgets.Layout(width="110px"),
1774
+ style={"description_width": "50px"},
1775
+ )
1776
+ self._fill_color_opacity_slider.observe(
1777
+ self._fill_color_opacity_change, names="value"
1778
+ )
1779
+
1780
+ self._fill_color_opacity_label = ipywidgets.Label(
1781
+ style={"description_width": "initial"},
1782
+ layout=ipywidgets.Layout(padding="0px"),
1783
+ )
1784
+
1785
+ self._color_picker = ipywidgets.ColorPicker(
1786
+ concise=False,
1787
+ value="#000000",
1788
+ layout=ipywidgets.Layout(width="116px"),
1789
+ style={"description_width": "initial"},
1790
+ )
1791
+
1792
+ self._add_color = ipywidgets.Button(
1793
+ icon="plus",
1794
+ tooltip="Add a hex color string to the palette",
1795
+ layout=ipywidgets.Layout(width="32px"),
1796
+ )
1797
+ self._del_color = ipywidgets.Button(
1798
+ icon="minus",
1799
+ tooltip="Remove a hex color string from the palette",
1800
+ layout=ipywidgets.Layout(width="32px"),
1801
+ )
1802
+ self._reset_color = ipywidgets.Button(
1803
+ icon="eraser",
1804
+ tooltip="Remove all color strings from the palette",
1805
+ layout=ipywidgets.Layout(width="34px"),
1806
+ )
1807
+ self._add_color.on_click(self._add_color_clicked)
1808
+ self._del_color.on_click(self._del_color_clicked)
1809
+ self._reset_color.on_click(self._reset_color_clicked)
1810
+
1811
+ self._palette_label = ipywidgets.Text(
1812
+ value="",
1813
+ placeholder="List of hex code (RRGGBB) separated by comma",
1814
+ description="Palette:",
1815
+ tooltip="Enter a list of hex code (RRGGBB) separated by comma",
1816
+ layout=ipywidgets.Layout(width="300px"),
1817
+ style={"description_width": "initial"},
1818
+ )
1819
+
1820
+ self._legend_title_label = ipywidgets.Text(
1821
+ value="Legend",
1822
+ description="Legend title:",
1823
+ tooltip="Enter a title for the legend",
1824
+ layout=ipywidgets.Layout(width="300px"),
1825
+ style={"description_width": "initial"},
1826
+ )
1827
+
1828
+ self._legend_labels_label = ipywidgets.Text(
1829
+ value="Labels",
1830
+ description="Legend labels:",
1831
+ tooltip="Enter a a list of labels for the legend",
1832
+ layout=ipywidgets.Layout(width="300px"),
1833
+ style={"description_width": "initial"},
1834
+ )
1835
+
1836
+ self._field_dropdown = ipywidgets.Dropdown(
1837
+ options=[],
1838
+ value=None,
1839
+ description="Field:",
1840
+ layout=ipywidgets.Layout(width="140px"),
1841
+ style={"description_width": "initial"},
1842
+ )
1843
+ self._field_dropdown.observe(self._field_changed, "value")
1844
+
1845
+ self._field_values_dropdown = ipywidgets.Dropdown(
1846
+ options=[],
1847
+ value=None,
1848
+ description="Values:",
1849
+ layout=ipywidgets.Layout(width="156px"),
1850
+ style={"description_width": "initial"},
1851
+ )
1852
+
1853
+ self._classes_dropdown = ipywidgets.Dropdown(
1854
+ options=["Any"] + [str(i) for i in range(3, 13)],
1855
+ description="Classes:",
1856
+ layout=ipywidgets.Layout(width="115px"),
1857
+ style={"description_width": "initial"},
1858
+ )
1859
+ self._colormap_dropdown = ipywidgets.Dropdown(
1860
+ options=["viridis"],
1861
+ value="viridis",
1862
+ description="Colormap:",
1863
+ layout=ipywidgets.Layout(width="181px"),
1864
+ style={"description_width": "initial"},
1865
+ )
1866
+ self._classes_dropdown.observe(self._classes_changed, "value")
1867
+ self._colormap_dropdown.observe(self._colormap_changed, "value")
1868
+
1869
+ self._style_chk = ipywidgets.Checkbox(
1870
+ value=False,
1871
+ description="Style by attribute",
1872
+ indent=False,
1873
+ layout=ipywidgets.Layout(width="140px"),
1874
+ )
1875
+ self._legend_checkbox = ipywidgets.Checkbox(
1876
+ value=False,
1877
+ description="Legend",
1878
+ indent=False,
1879
+ layout=ipywidgets.Layout(width="70px"),
1880
+ )
1881
+ self._style_chk.observe(self._style_chk_changed, "value")
1882
+ self._legend_checkbox.observe(self._legend_chk_changed, "value")
1883
+
1884
+ self._compute_label = ipywidgets.Label(value="")
1885
+
1886
+ self._style_vbox = ipywidgets.VBox(
1887
+ [ipywidgets.HBox([self._style_chk, self._compute_label])]
1888
+ )
1889
+
1890
+ self._colorbar_output = ipywidgets.Output(
1891
+ layout=ipywidgets.Layout(height="60px", width="300px")
1892
+ )
1893
+
1894
+ is_point = common.geometry_type(self._ee_object) in ["Point", "MultiPoint"]
1895
+ self._point_size_label.disabled = not is_point
1896
+ self._point_shape_dropdown.disabled = not is_point
1897
+
1898
+ super().__init__(
1899
+ layout=ipywidgets.Layout(
1900
+ padding="5px 5px 5px 8px",
1901
+ # width="330px",
1902
+ max_height="250px",
1903
+ overflow="auto",
1904
+ display="block",
1905
+ ),
1906
+ children=[
1907
+ self._new_layer_name,
1908
+ ipywidgets.HBox(
1909
+ [
1910
+ self._color_picker,
1911
+ self._color_opacity_slider,
1912
+ self._color_opacity_label,
1913
+ ]
1914
+ ),
1915
+ ipywidgets.HBox([self._point_size_label, self._point_shape_dropdown]),
1916
+ ipywidgets.HBox([self._line_width_label, self._line_type_label]),
1917
+ ipywidgets.HBox(
1918
+ [
1919
+ self._fill_color_picker,
1920
+ self._fill_color_opacity_slider,
1921
+ self._fill_color_opacity_label,
1922
+ ]
1923
+ ),
1924
+ self._style_vbox,
1925
+ ],
1926
+ )
1927
+
1928
+ def _get_vis_params(self):
1929
+ vis = {}
1930
+ vis["color"] = self._color_picker.value[1:] + str(
1931
+ hex(int(self._color_opacity_slider.value * 255))
1932
+ )[2:].zfill(2)
1933
+ if common.geometry_type(self._ee_object) in ["Point", "MultiPoint"]:
1934
+ vis["pointSize"] = self._point_size_label.value
1935
+ vis["pointShape"] = self._point_shape_dropdown.value
1936
+ vis["width"] = self._line_width_label.value
1937
+ vis["lineType"] = self._line_type_label.value
1938
+ vis["fillColor"] = self._fill_color_picker.value[1:] + str(
1939
+ hex(int(self._fill_color_opacity_slider.value * 255))
1940
+ )[2:].zfill(2)
1941
+
1942
+ return vis
1943
+
1944
+ def on_apply_click(self):
1945
+ self._compute_label.value = "Computing ..."
1946
+
1947
+ if self._new_layer_name.value in self._host_map.ee_layers:
1948
+ old_layer = self._new_layer_name.value
1949
+ self._host_map.remove(old_layer)
1950
+
1951
+ if not self._style_chk.value:
1952
+ vis = self._get_vis_params()
1953
+ self._host_map.add_layer(
1954
+ self._ee_object.style(**vis), {}, self._new_layer_name.value
1955
+ )
1956
+ self._ee_layer.visible = False
1957
+ self._compute_label.value = ""
1958
+
1959
+ elif (
1960
+ self._style_chk.value
1961
+ and len(self._palette_label.value) > 0
1962
+ and "," in self._palette_label.value
1963
+ ):
1964
+ try:
1965
+ colors = ee.List(
1966
+ [
1967
+ color.strip()
1968
+ + str(hex(int(self._fill_color_opacity_slider.value * 255)))[
1969
+ 2:
1970
+ ].zfill(2)
1971
+ for color in self._palette_label.value.split(",")
1972
+ ]
1973
+ )
1974
+ arr = (
1975
+ self._ee_object.aggregate_array(self._field_dropdown.value)
1976
+ .distinct()
1977
+ .sort()
1978
+ )
1979
+ fc = self._ee_object.map(
1980
+ lambda f: f.set(
1981
+ {"styleIndex": arr.indexOf(f.get(self._field_dropdown.value))}
1982
+ )
1983
+ )
1984
+ step = arr.size().divide(colors.size()).ceil()
1985
+ fc = fc.map(
1986
+ lambda f: f.set(
1987
+ {
1988
+ "style": {
1989
+ "color": self._color_picker.value[1:]
1990
+ + str(hex(int(self._color_opacity_slider.value * 255)))[
1991
+ 2:
1992
+ ].zfill(2),
1993
+ "pointSize": self._point_size_label.value,
1994
+ "pointShape": self._point_shape_dropdown.value,
1995
+ "width": self._line_width_label.value,
1996
+ "lineType": self._line_type_label.value,
1997
+ "fillColor": colors.get(
1998
+ ee.Number(
1999
+ ee.Number(f.get("styleIndex")).divide(step)
2000
+ ).floor()
2001
+ ),
2002
+ }
2003
+ }
2004
+ )
2005
+ )
2006
+
2007
+ self._host_map.add_layer(
2008
+ fc.style(**{"styleProperty": "style"}),
2009
+ {},
2010
+ f"{self._new_layer_name.value}",
2011
+ )
2012
+
2013
+ if (
2014
+ len(self._palette_label.value)
2015
+ and self._legend_checkbox.value
2016
+ and len(self._legend_labels_label.value) > 0
2017
+ and hasattr(self._host_map, "add_legend")
2018
+ ):
2019
+ legend_colors = [
2020
+ color.strip() for color in self._palette_label.value.split(",")
2021
+ ]
2022
+ legend_keys = [
2023
+ label.strip()
2024
+ for label in self._legend_labels_label.value.split(",")
2025
+ ]
2026
+
2027
+ if hasattr(self._host_map, "add_legend"):
2028
+ self._host_map.add_legend(
2029
+ title=self._legend_title_label.value,
2030
+ legend_keys=legend_keys,
2031
+ legend_colors=legend_colors,
2032
+ layer_name=self._new_layer_name.value,
2033
+ )
2034
+ except Exception as exc:
2035
+ self._compute_label.value = "Error: " + str(exc)
2036
+
2037
+ self._ee_layer.visible = False
2038
+ self._compute_label.value = ""
2039
+
2040
+ def _render_colorbar(self, colors):
2041
+ import matplotlib # pylint: disable=import-outside-toplevel
2042
+ from matplotlib import pyplot # pylint: disable=import-outside-toplevel
2043
+
2044
+ colors = common.to_hex_colors(colors)
2045
+
2046
+ _, ax = pyplot.subplots(figsize=(4, 0.3))
2047
+ cmap = matplotlib.colors.LinearSegmentedColormap.from_list(
2048
+ "custom", colors, N=256
2049
+ )
2050
+ norm = matplotlib.colors.Normalize(vmin=0, vmax=1)
2051
+ matplotlib.colorbar.ColorbarBase(
2052
+ ax, norm=norm, cmap=cmap, orientation="horizontal"
2053
+ )
2054
+
2055
+ self._palette_label.value = ", ".join(colors)
2056
+ self._colorbar_output.clear_output()
2057
+ with self._colorbar_output:
2058
+ pyplot.show()
2059
+
2060
+ def _classes_changed(self, change):
2061
+ import matplotlib # pylint: disable=import-outside-toplevel
2062
+ from matplotlib import pyplot # pylint: disable=import-outside-toplevel
2063
+
2064
+ if change["new"]:
2065
+ selected = change["owner"].value
2066
+ if self._colormap_dropdown.value is not None:
2067
+ n_class = None
2068
+ if selected != "Any":
2069
+ n_class = int(self._classes_dropdown.value)
2070
+
2071
+ colors = pyplot.cm.get_cmap(self._colormap_dropdown.value, n_class)
2072
+ cmap_colors = [
2073
+ matplotlib.colors.rgb2hex(colors(i))[1:] for i in range(colors.N)
2074
+ ]
2075
+ self._render_colorbar(cmap_colors)
2076
+
2077
+ if (
2078
+ len(self._palette_label.value) > 0
2079
+ and "," in self._palette_label.value
2080
+ ):
2081
+ labels = [
2082
+ f"Class {i+1}"
2083
+ for i in range(len(self._palette_label.value.split(",")))
2084
+ ]
2085
+ self._legend_labels_label.value = ", ".join(labels)
2086
+
2087
+ def _colormap_changed(self, change):
2088
+ import matplotlib # pylint: disable=import-outside-toplevel
2089
+ from matplotlib import pyplot # pylint: disable=import-outside-toplevel
2090
+
2091
+ if change["new"]:
2092
+ n_class = None
2093
+ if self._classes_dropdown.value != "Any":
2094
+ n_class = int(self._classes_dropdown.value)
2095
+
2096
+ colors = pyplot.cm.get_cmap(self._colormap_dropdown.value, n_class)
2097
+ cmap_colors = [
2098
+ matplotlib.colors.rgb2hex(colors(i))[1:] for i in range(colors.N)
2099
+ ]
2100
+ self._render_colorbar(cmap_colors)
2101
+
2102
+ if len(self._palette_label.value) > 0 and "," in self._palette_label.value:
2103
+ labels = [
2104
+ f"Class {i+1}"
2105
+ for i in range(len(self._palette_label.value.split(",")))
2106
+ ]
2107
+ self._legend_labels_label.value = ", ".join(labels)
2108
+
2109
+ def _fill_color_opacity_change(self, change):
2110
+ self._fill_color_opacity_label.value = str(change["new"])
2111
+
2112
+ def _color_opacity_change(self, change):
2113
+ self._color_opacity_label.value = str(change["new"])
2114
+
2115
+ def _add_color_clicked(self, _):
2116
+ if self._color_picker.value is not None:
2117
+ if len(self._palette_label.value) == 0:
2118
+ self._palette_label.value = self._color_picker.value[1:]
2119
+ else:
2120
+ self._palette_label.value += ", " + self._color_picker.value[1:]
2121
+
2122
+ def _del_color_clicked(self, _):
2123
+ if "," in self._palette_label.value:
2124
+ items = [item.strip() for item in self._palette_label.value.split(",")]
2125
+ self._palette_label.value = ", ".join(items[:-1])
2126
+ else:
2127
+ self._palette_label.value = ""
2128
+
2129
+ def _reset_color_clicked(self, _):
2130
+ self._palette_label.value = ""
2131
+
2132
+ def _style_chk_changed(self, change):
2133
+ from matplotlib import pyplot # pylint: disable=import-outside-toplevel
2134
+
2135
+ if change["new"]:
2136
+ self._colorbar_output.clear_output()
2137
+
2138
+ self._fill_color_picker.disabled = True
2139
+ colormap_options = pyplot.colormaps()
2140
+ colormap_options.sort()
2141
+ self._colormap_dropdown.options = colormap_options
2142
+ self._colormap_dropdown.value = "viridis"
2143
+ self._style_vbox.children = [
2144
+ ipywidgets.HBox([self._style_chk, self._compute_label]),
2145
+ ipywidgets.HBox([self._field_dropdown, self._field_values_dropdown]),
2146
+ ipywidgets.HBox([self._classes_dropdown, self._colormap_dropdown]),
2147
+ self._palette_label,
2148
+ self._colorbar_output,
2149
+ ipywidgets.HBox(
2150
+ [
2151
+ self._legend_checkbox,
2152
+ self._color_picker,
2153
+ self._add_color,
2154
+ self._del_color,
2155
+ self._reset_color,
2156
+ ]
2157
+ ),
2158
+ ]
2159
+ self._compute_label.value = "Computing ..."
2160
+
2161
+ self._field_dropdown.options = (
2162
+ ee.Feature(self._ee_object.first()).propertyNames().getInfo()
2163
+ )
2164
+ self._compute_label.value = ""
2165
+ self._classes_dropdown.value = "Any"
2166
+ self._legend_checkbox.value = False
2167
+
2168
+ else:
2169
+ self._fill_color_picker.disabled = False
2170
+ self._style_vbox.children = [
2171
+ ipywidgets.HBox([self._style_chk, self._compute_label])
2172
+ ]
2173
+ self._compute_label.value = ""
2174
+ self._colorbar_output.clear_output()
2175
+
2176
+ def _legend_chk_changed(self, change):
2177
+ if change["new"]:
2178
+ self._style_vbox.children = list(self._style_vbox.children) + [
2179
+ ipywidgets.VBox([self._legend_title_label, self._legend_labels_label])
2180
+ ]
2181
+
2182
+ if len(self._palette_label.value) > 0 and "," in self._palette_label.value:
2183
+ labels = [
2184
+ f"Class {i+1}"
2185
+ for i in range(len(self._palette_label.value.split(",")))
2186
+ ]
2187
+ self._legend_labels_label.value = ", ".join(labels)
2188
+
2189
+ else:
2190
+ self._style_vbox.children = [
2191
+ ipywidgets.HBox([self._style_chk, self._compute_label]),
2192
+ ipywidgets.HBox([self._field_dropdown, self._field_values_dropdown]),
2193
+ ipywidgets.HBox([self._classes_dropdown, self._colormap_dropdown]),
2194
+ self._palette_label,
2195
+ ipywidgets.HBox(
2196
+ [
2197
+ self._legend_checkbox,
2198
+ self._color_picker,
2199
+ self._add_color,
2200
+ self._del_color,
2201
+ self._reset_color,
2202
+ ]
2203
+ ),
2204
+ ]
2205
+
2206
+ def _field_changed(self, change):
2207
+ if change["new"]:
2208
+ self._compute_label.value = "Computing ..."
2209
+ options = self._ee_object.aggregate_array(
2210
+ self._field_dropdown.value
2211
+ ).getInfo()
2212
+ if options is not None:
2213
+ options = list(set(options))
2214
+ options.sort()
2215
+
2216
+ self._field_values_dropdown.options = options
2217
+ self._compute_label.value = ""
2218
+
2219
+ def on_import_click(self):
2220
+ vis = self._get_vis_params()
2221
+ common.create_code_cell(f"style = {str(vis)}")
2222
+ print(f"style = {str(vis)}")