geeltermap 0.1.0__py3-none-any.whl → 0.1.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- geeltermap/__init__.py +2 -2
- geeltermap/core.py +870 -0
- geeltermap/ee_tile_layers.py +231 -0
- geeltermap/geeltermap.py +2 -0
- geeltermap/map_widgets.py +2222 -0
- geeltermap/toolbar.py +35 -25
- {geeltermap-0.1.0.dist-info → geeltermap-0.1.8.dist-info}/METADATA +5 -5
- {geeltermap-0.1.0.dist-info → geeltermap-0.1.8.dist-info}/RECORD +10 -7
- {geeltermap-0.1.0.dist-info → geeltermap-0.1.8.dist-info}/WHEEL +1 -1
- {geeltermap-0.1.0.dist-info → geeltermap-0.1.8.dist-info}/LICENSE +0 -0
|
@@ -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)}")
|