Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
giswqs
GitHub Repository: giswqs/geemap
Path: blob/master/tests/test_map_widgets.py
2313 views
1
#!/usr/bin/env python
2
"""Tests for `map_widgets` module."""
3
4
import unittest
5
from unittest import mock
6
7
import ee
8
import ipywidgets
9
from matplotlib import colorbar
10
from matplotlib import colors
11
from matplotlib import pyplot
12
13
from geemap import coreutils
14
from geemap import legends
15
from geemap import map_widgets
16
from tests import fake_ee
17
from tests import fake_map
18
19
20
def _get_colormaps() -> list[str]:
21
"""Gets the list of available colormaps."""
22
colormap_options = pyplot.colormaps()
23
colormap_options.sort()
24
return ["Custom"] + colormap_options
25
26
27
class TestColorbar(unittest.TestCase):
28
"""Tests for the Colorbar class in the `map_widgets` module."""
29
30
TEST_COLORS = ["blue", "red", "green"]
31
TEST_COLORS_HEX = ["#0000ff", "#ff0000", "#008000"]
32
33
def setUp(self):
34
super().setUp()
35
self.fig_mock = mock.MagicMock()
36
self.ax_mock = mock.MagicMock()
37
self.subplots_mock = mock.patch.object(pyplot, "subplots").start()
38
self.subplots_mock.return_value = (self.fig_mock, self.ax_mock)
39
40
self.colorbar_base_mock = mock.MagicMock()
41
self.colorbar_base_class_mock = mock.patch.object(
42
colorbar, "ColorbarBase"
43
).start()
44
self.colorbar_base_class_mock.return_value = self.colorbar_base_mock
45
46
self.normalize_mock = mock.MagicMock()
47
self.normalize_class_mock = mock.patch.object(colors, "Normalize").start()
48
self.normalize_class_mock.return_value = self.normalize_mock
49
50
self.boundary_norm_mock = mock.MagicMock()
51
self.boundary_norm_class_mock = mock.patch.object(
52
colors, "BoundaryNorm"
53
).start()
54
self.boundary_norm_class_mock.return_value = self.boundary_norm_mock
55
56
self.listed_colormap = mock.MagicMock()
57
self.listed_colormap_class_mock = mock.patch.object(
58
colors, "ListedColormap"
59
).start()
60
self.listed_colormap_class_mock.return_value = self.listed_colormap
61
62
self.linear_segmented_colormap_mock = mock.MagicMock()
63
self.colormap_from_list_mock = mock.patch.object(
64
colors.LinearSegmentedColormap, "from_list"
65
).start()
66
self.colormap_from_list_mock.return_value = self.linear_segmented_colormap_mock
67
68
check_cmap_mock = mock.patch.object(coreutils, "check_cmap").start()
69
check_cmap_mock.side_effect = lambda x: x
70
71
self.cmap_mock = mock.MagicMock()
72
self.get_cmap_mock = mock.patch.object(pyplot, "get_cmap").start()
73
self.get_cmap_mock.return_value = self.cmap_mock
74
75
def tearDown(self):
76
mock.patch.stopall()
77
super().tearDown()
78
79
def test_colorbar_no_args(self):
80
map_widgets.Colorbar()
81
self.normalize_class_mock.assert_called_with(vmin=0, vmax=1)
82
self.get_cmap_mock.assert_called_with("gray")
83
self.subplots_mock.assert_called_with(figsize=(3.0, 0.3))
84
self.ax_mock.set_axis_off.assert_not_called()
85
self.ax_mock.tick_params.assert_called_with(labelsize=9)
86
self.fig_mock.patch.set_alpha.assert_not_called()
87
self.colorbar_base_mock.set_label.assert_not_called()
88
self.colorbar_base_class_mock.assert_called_with(
89
self.ax_mock,
90
norm=self.normalize_mock,
91
alpha=1,
92
cmap=self.cmap_mock,
93
orientation="horizontal",
94
)
95
96
def test_colorbar_orientation_horizontal(self):
97
map_widgets.Colorbar(orientation="horizontal")
98
self.subplots_mock.assert_called_with(figsize=(3.0, 0.3))
99
100
def test_colorbar_orientation_vertical(self):
101
map_widgets.Colorbar(orientation="vertical")
102
self.subplots_mock.assert_called_with(figsize=(0.3, 3.0))
103
104
def test_colorbar_orientation_override(self):
105
map_widgets.Colorbar(orientation="horizontal", width=2.0)
106
self.subplots_mock.assert_called_with(figsize=(2.0, 0.3))
107
108
def test_colorbar_invalid_orientation(self):
109
with self.assertRaisesRegex(ValueError, "orientation must be one of"):
110
map_widgets.Colorbar(orientation="not an orientation")
111
112
def test_colorbar_label(self):
113
map_widgets.Colorbar(label="Colorbar lbl", font_size=42)
114
self.colorbar_base_mock.set_label.assert_called_with(
115
"Colorbar lbl", fontsize=42
116
)
117
118
def test_colorbar_label_as_bands(self):
119
map_widgets.Colorbar(vis_params={"bands": "b1"})
120
self.colorbar_base_mock.set_label.assert_called_with("b1", fontsize=9)
121
122
def test_colorbar_label_with_caption(self):
123
map_widgets.Colorbar(caption="Colorbar caption")
124
self.colorbar_base_mock.set_label.assert_called_with(
125
"Colorbar caption", fontsize=9
126
)
127
128
def test_colorbar_label_precedence(self):
129
map_widgets.Colorbar(
130
label="Colorbar lbl",
131
vis_params={"bands": "b1"},
132
caption="Colorbar caption",
133
font_size=21,
134
)
135
self.colorbar_base_mock.set_label.assert_called_with(
136
"Colorbar lbl", fontsize=21
137
)
138
139
def test_colorbar_axis(self):
140
map_widgets.Colorbar(axis_off=True, font_size=24)
141
self.ax_mock.set_axis_off.assert_called()
142
self.ax_mock.tick_params.assert_called_with(labelsize=24)
143
144
def test_colorbar_transparent_bg(self):
145
map_widgets.Colorbar(transparent_bg=True)
146
self.fig_mock.patch.set_alpha.assert_called_with(0.0)
147
148
def test_colorbar_vis_params_palette(self):
149
map_widgets.Colorbar(
150
vis_params={
151
"palette": self.TEST_COLORS,
152
"min": 11,
153
"max": 21,
154
"opacity": 0.2,
155
}
156
)
157
self.normalize_class_mock.assert_called_with(vmin=11, vmax=21)
158
self.colormap_from_list_mock.assert_called_with(
159
"custom", self.TEST_COLORS_HEX, N=256
160
)
161
self.colorbar_base_class_mock.assert_called_with(
162
self.ax_mock,
163
norm=self.normalize_mock,
164
alpha=0.2,
165
cmap=self.linear_segmented_colormap_mock,
166
orientation="horizontal",
167
)
168
169
def test_colorbar_vis_params_discrete_palette(self):
170
map_widgets.Colorbar(
171
vis_params={"palette": self.TEST_COLORS, "min": -1}, discrete=True
172
)
173
self.boundary_norm_class_mock.assert_called_with([-1], mock.ANY)
174
self.listed_colormap_class_mock.assert_called_with(self.TEST_COLORS_HEX)
175
self.colorbar_base_class_mock.assert_called_with(
176
self.ax_mock,
177
norm=self.boundary_norm_mock,
178
alpha=1,
179
cmap=self.listed_colormap,
180
orientation="horizontal",
181
)
182
183
def test_colorbar_vis_params_palette_as_list(self):
184
map_widgets.Colorbar(vis_params=self.TEST_COLORS, discrete=True)
185
self.boundary_norm_class_mock.assert_called_with([0], mock.ANY)
186
self.listed_colormap_class_mock.assert_called_with(self.TEST_COLORS_HEX)
187
self.colorbar_base_class_mock.assert_called_with(
188
self.ax_mock,
189
norm=self.boundary_norm_mock,
190
alpha=1,
191
cmap=self.listed_colormap,
192
orientation="horizontal",
193
)
194
195
def test_colorbar_kwargs_colors(self):
196
map_widgets.Colorbar(colors=self.TEST_COLORS, discrete=True)
197
self.boundary_norm_class_mock.assert_called_with([0], mock.ANY)
198
self.listed_colormap_class_mock.assert_called_with(self.TEST_COLORS_HEX)
199
self.colorbar_base_class_mock.assert_called_with(
200
self.ax_mock,
201
norm=self.boundary_norm_mock,
202
alpha=1,
203
cmap=self.listed_colormap,
204
orientation="horizontal",
205
colors=self.TEST_COLORS,
206
)
207
208
def test_colorbar_min_max(self):
209
map_widgets.Colorbar(
210
vis_params={"palette": self.TEST_COLORS, "min": -1.5}, vmin=-1, vmax=2
211
)
212
self.normalize_class_mock.assert_called_with(vmin=-1.5, vmax=2)
213
214
def test_colorbar_invalid_min(self):
215
with self.assertRaisesRegex(ValueError, "min value must be scalar type"):
216
map_widgets.Colorbar(vis_params={"min": "invalid_min"})
217
218
def test_colorbar_invalid_max(self):
219
with self.assertRaisesRegex(ValueError, "max value must be scalar type"):
220
map_widgets.Colorbar(vis_params={"max": "invalid_max"})
221
222
def test_colorbar_opacity(self):
223
map_widgets.Colorbar(vis_params={"opacity": 0.5}, colors=self.TEST_COLORS)
224
self.colorbar_base_class_mock.assert_called_with(
225
mock.ANY,
226
norm=mock.ANY,
227
alpha=0.5,
228
cmap=mock.ANY,
229
orientation=mock.ANY,
230
colors=mock.ANY,
231
)
232
233
def test_colorbar_alpha(self):
234
map_widgets.Colorbar(alpha=0.5, colors=self.TEST_COLORS)
235
self.colorbar_base_class_mock.assert_called_with(
236
mock.ANY,
237
norm=mock.ANY,
238
alpha=0.5,
239
cmap=mock.ANY,
240
orientation=mock.ANY,
241
colors=mock.ANY,
242
)
243
244
def test_colorbar_invalid_alpha(self):
245
with self.assertRaisesRegex(
246
ValueError, "opacity or alpha value must be scalar type"
247
):
248
map_widgets.Colorbar(alpha="invalid_alpha", colors=self.TEST_COLORS)
249
250
def test_colorbar_vis_params_throws_for_not_dict(self):
251
with self.assertRaisesRegex(TypeError, "vis_params must be a dictionary"):
252
map_widgets.Colorbar(vis_params="NOT a dict")
253
254
255
class TestLegend(unittest.TestCase):
256
"""Tests for the Legend class in the `map_widgets` module."""
257
258
TEST_KEYS = ["developed", "forest", "open water"]
259
TEST_COLORS_HEX = ["#ff0000", "#00ff00", "#0000ff"]
260
TEST_COLORS_RGB = [(255, 0, 0), (0, 255, 0), (0, 0, 255)]
261
262
def test_legend_initializes(self):
263
legend = map_widgets.Legend(
264
title="My Legend",
265
keys=self.TEST_KEYS,
266
colors=self.TEST_COLORS_HEX,
267
position="bottomleft",
268
add_header=True,
269
widget_args={"show_close_button": True},
270
)
271
self.assertEqual(legend.title, "My Legend")
272
self.assertEqual(legend.position, "bottomleft")
273
self.assertTrue(legend.add_header)
274
self.assertTrue(legend.show_close_button)
275
276
def test_legend_with_keys_and_colors(self):
277
# Normal hex colors.
278
legend = map_widgets.Legend(keys=self.TEST_KEYS, colors=self.TEST_COLORS_HEX)
279
self.assertListEqual(legend.legend_keys, self.TEST_KEYS)
280
self.assertListEqual(legend.legend_colors, self.TEST_COLORS_HEX)
281
282
# Adds # when there are none.
283
legend = map_widgets.Legend(
284
keys=self.TEST_KEYS, colors=["ff0000", "00ff00", "0000ff"]
285
)
286
self.assertListEqual(legend.legend_colors, self.TEST_COLORS_HEX)
287
288
# Three characters.
289
legend = map_widgets.Legend(keys=self.TEST_KEYS, colors=["f00", "0f0", "00f"])
290
self.assertListEqual(legend.legend_colors, ["#f00", "#0f0", "#00f"])
291
292
# With alpha.
293
legend = map_widgets.Legend(
294
keys=self.TEST_KEYS, colors=["#ff0000ff", "#00ff00ff", "#0000ffff"]
295
)
296
self.assertListEqual(
297
legend.legend_colors, ["#ff0000ff", "#00ff00ff", "#0000ffff"]
298
)
299
300
# CSS colors.
301
legend = map_widgets.Legend(
302
keys=self.TEST_KEYS, colors=["red", "green", "blue"]
303
)
304
self.assertListEqual(legend.legend_colors, ["red", "green", "blue"])
305
306
# RGB tuples.
307
legend = map_widgets.Legend(keys=self.TEST_KEYS, colors=self.TEST_COLORS_RGB)
308
self.assertListEqual(legend.legend_colors, self.TEST_COLORS_HEX)
309
310
# Mix of hex and tuples.
311
legend = map_widgets.Legend(
312
keys=self.TEST_KEYS * 2, colors=self.TEST_COLORS_HEX + self.TEST_COLORS_RGB
313
)
314
self.assertListEqual(legend.legend_keys, self.TEST_KEYS * 2)
315
self.assertListEqual(legend.legend_colors, self.TEST_COLORS_HEX * 2)
316
317
def test_legend_with_dictionary(self):
318
legend = map_widgets.Legend(
319
legend_dict=dict(zip(self.TEST_KEYS, self.TEST_COLORS_HEX))
320
)
321
self.assertListEqual(legend.legend_keys, self.TEST_KEYS)
322
self.assertListEqual(legend.legend_colors, self.TEST_COLORS_HEX)
323
324
legend = map_widgets.Legend(
325
legend_dict=dict(zip(self.TEST_KEYS, self.TEST_COLORS_RGB))
326
)
327
self.assertListEqual(legend.legend_keys, self.TEST_KEYS)
328
self.assertListEqual(legend.legend_colors, self.TEST_COLORS_HEX)
329
330
def test_legend_with_builtin_legends(self):
331
legend = map_widgets.Legend(builtin_legend="NLCD")
332
self.assertListEqual(
333
legend.legend_keys, list(legends.builtin_legends["NLCD"].keys())
334
)
335
self.assertListEqual(
336
legend.legend_colors,
337
[f"#{color}" for color in legends.builtin_legends["NLCD"].values()],
338
)
339
340
def test_legend_unable_to_convert_rgb_to_hex(self):
341
with self.assertRaisesRegex(ValueError, "Unable to convert rgb value to hex."):
342
test_keys = ["Key 1"]
343
test_colors = [("invalid", "invalid")]
344
map_widgets.Legend(keys=test_keys, colors=test_colors)
345
346
def test_legend_keys_and_colors_not_same_length(self):
347
with self.assertRaisesRegex(
348
ValueError, ("The legend keys and colors must be the " + "same length.")
349
):
350
test_keys = ["one", "two", "three", "four"]
351
map_widgets.Legend(keys=test_keys, colors=TestLegend.TEST_COLORS_HEX)
352
353
def test_legend_builtin_legend_not_allowed(self):
354
expected_regex = "The builtin legend must be one of the following: {}".format(
355
", ".join(legends.builtin_legends)
356
)
357
with self.assertRaisesRegex(ValueError, expected_regex):
358
map_widgets.Legend(builtin_legend="invalid_builtin_legend")
359
360
def test_legend_position_not_allowed(self):
361
expected_regex = (
362
"The position must be one of the following: "
363
+ "topleft, topright, bottomleft, bottomright"
364
)
365
with self.assertRaisesRegex(ValueError, expected_regex):
366
map_widgets.Legend(position="invalid_position")
367
368
def test_legend_keys_not_a_dict(self):
369
with self.assertRaisesRegex(TypeError, "The legend keys must be a list."):
370
map_widgets.Legend(keys="invalid_keys")
371
372
def test_legend_colors_not_a_list(self):
373
with self.assertRaisesRegex(TypeError, "The legend colors must be a list."):
374
map_widgets.Legend(keys=["test_key"], colors="invalid_colors")
375
376
377
@mock.patch.object(ee, "Algorithms", fake_ee.Algorithms)
378
@mock.patch.object(ee, "FeatureCollection", fake_ee.FeatureCollection)
379
@mock.patch.object(ee, "Feature", fake_ee.Feature)
380
@mock.patch.object(ee, "Geometry", fake_ee.Geometry)
381
@mock.patch.object(ee, "Image", fake_ee.Image)
382
@mock.patch.object(ee, "String", fake_ee.String)
383
class TestInspector(unittest.TestCase):
384
"""Tests for the Inspector class in the `map_widgets` module."""
385
386
def setUp(self):
387
super().setUp()
388
# ee.Reducer is dynamically initialized (can't use @patch.object).
389
ee.Reducer = fake_ee.Reducer
390
391
self.map_fake = fake_map.FakeMap()
392
self.inspector = map_widgets.Inspector(self.map_fake)
393
394
def test_inspector_no_map(self):
395
"""Tests that a valid map must be passed in."""
396
with self.assertRaisesRegex(ValueError, "valid map"):
397
map_widgets.Inspector(None)
398
399
def test_inspector(self):
400
"""Tests that the inspector's initial UI is set up properly."""
401
self.assertEqual(self.map_fake.cursor_style, "crosshair")
402
self.assertFalse(self.inspector.hide_close_button)
403
404
self.assertFalse(self.inspector.expand_points)
405
self.assertTrue(self.inspector.expand_pixels)
406
self.assertFalse(self.inspector.expand_objects)
407
408
self.assertEqual(self.inspector.point_info, {})
409
self.assertEqual(self.inspector.pixel_info, {})
410
self.assertEqual(self.inspector.object_info, {})
411
412
def test_map_empty_click(self):
413
"""Tests that clicking the map triggers inspection."""
414
self.map_fake.click((1, 2), "click")
415
416
self.assertEqual(self.map_fake.cursor_style, "crosshair")
417
418
expected_point_info = coreutils.new_tree_node(
419
"Point (2.00, 1.00) at 1024m/px",
420
[
421
coreutils.new_tree_node("Longitude: 2"),
422
coreutils.new_tree_node("Latitude: 1"),
423
coreutils.new_tree_node("Zoom Level: 7"),
424
coreutils.new_tree_node("Scale (approx. m/px): 1024"),
425
],
426
top_level=True,
427
)
428
self.assertEqual(self.inspector.point_info, expected_point_info)
429
430
expected_pixel_info = coreutils.new_tree_node(
431
"Pixels", top_level=True, expanded=True
432
)
433
self.assertEqual(self.inspector.pixel_info, expected_pixel_info)
434
435
expected_object_info = coreutils.new_tree_node(
436
"Objects", top_level=True, expanded=True
437
)
438
self.assertEqual(self.inspector.object_info, expected_object_info)
439
440
def test_map_click(self):
441
"""Tests that clicking the map triggers inspection."""
442
self.map_fake.ee_layers = {
443
"test-map-1": {
444
"ee_object": ee.Image(1),
445
"ee_layer": fake_map.FakeEeTileLayer(visible=True),
446
"vis_params": None,
447
},
448
"test-map-2": {
449
"ee_object": ee.Image(2),
450
"ee_layer": fake_map.FakeEeTileLayer(visible=False),
451
"vis_params": None,
452
},
453
"test-map-3": {
454
"ee_object": ee.FeatureCollection([]),
455
"ee_layer": fake_map.FakeEeTileLayer(visible=True),
456
"vis_params": None,
457
},
458
}
459
self.map_fake.click((1, 2), "click")
460
461
expected_point_info = coreutils.new_tree_node(
462
"Point (2.00, 1.00) at 1024m/px",
463
[
464
coreutils.new_tree_node("Longitude: 2"),
465
coreutils.new_tree_node("Latitude: 1"),
466
coreutils.new_tree_node("Zoom Level: 7"),
467
coreutils.new_tree_node("Scale (approx. m/px): 1024"),
468
],
469
top_level=True,
470
)
471
self.assertEqual(self.inspector.point_info, expected_point_info)
472
473
expected_pixel_info = coreutils.new_tree_node(
474
"Pixels",
475
[
476
coreutils.new_tree_node(
477
"test-map-1: Image (2 bands)",
478
[
479
coreutils.new_tree_node("B1: 42", expanded=True),
480
coreutils.new_tree_node("B2: 3.14", expanded=True),
481
],
482
expanded=True,
483
),
484
],
485
top_level=True,
486
expanded=True,
487
)
488
self.assertEqual(self.inspector.pixel_info, expected_pixel_info)
489
490
expected_object_info = coreutils.new_tree_node(
491
"Objects",
492
[
493
coreutils.new_tree_node(
494
"test-map-3: Feature",
495
[
496
coreutils.new_tree_node("type: Feature"),
497
coreutils.new_tree_node("id: 00000000000000000001"),
498
coreutils.new_tree_node(
499
"properties: Object (4 properties)",
500
[
501
coreutils.new_tree_node("fullname: some-full-name"),
502
coreutils.new_tree_node("linearid: 110469267091"),
503
coreutils.new_tree_node("mtfcc: S1400"),
504
coreutils.new_tree_node("rttyp: some-rttyp"),
505
],
506
),
507
],
508
),
509
],
510
top_level=True,
511
expanded=True,
512
)
513
self.assertEqual(self.inspector.object_info, expected_object_info)
514
515
def test_map_click_twice(self):
516
"""Tests that clicking the map a second time resets the point info."""
517
self.map_fake.ee_layers = {
518
"test-map-1": {
519
"ee_object": ee.Image(1),
520
"ee_layer": fake_map.FakeEeTileLayer(visible=True),
521
"vis_params": None,
522
},
523
}
524
self.map_fake.scale = 32
525
self.map_fake.click((1, 2), "click")
526
self.map_fake.click((4, 1), "click")
527
528
expected_point_info = coreutils.new_tree_node(
529
"Point (1.00, 4.00) at 32m/px",
530
[
531
coreutils.new_tree_node("Longitude: 1"),
532
coreutils.new_tree_node("Latitude: 4"),
533
coreutils.new_tree_node("Zoom Level: 7"),
534
coreutils.new_tree_node("Scale (approx. m/px): 32"),
535
],
536
top_level=True,
537
)
538
self.assertEqual(self.inspector.point_info, expected_point_info)
539
540
def test_map_click_expanded(self):
541
"""Tests that nodes are expanded when the expand boolean is true."""
542
self.inspector.expand_points = True
543
544
self.map_fake.click((4, 1), "click")
545
546
expected_point_info = coreutils.new_tree_node(
547
"Point (1.00, 4.00) at 1024m/px",
548
[
549
coreutils.new_tree_node("Longitude: 1"),
550
coreutils.new_tree_node("Latitude: 4"),
551
coreutils.new_tree_node("Zoom Level: 7"),
552
coreutils.new_tree_node("Scale (approx. m/px): 1024"),
553
],
554
top_level=True,
555
expanded=True,
556
)
557
self.assertEqual(self.inspector.point_info, expected_point_info)
558
559
560
def _create_fake_map() -> fake_map.FakeMap:
561
ret = fake_map.FakeMap()
562
ret.layers = [
563
fake_map.FakeTileLayer("OpenStreetMap"), # Basemap
564
fake_map.FakeTileLayer("GMaps", False, 0.5), # Extra basemap
565
fake_map.FakeEeTileLayer("test-layer", True, 0.8),
566
fake_map.FakeGeoJSONLayer(
567
"test-geojson-layer",
568
False,
569
{"some-style": "red", "opacity": 0.3, "fillOpacity": 0.2},
570
),
571
]
572
ret.ee_layers = {
573
"test-layer": {"ee_object": None, "ee_layer": ret.layers[2], "vis_params": None}
574
}
575
ret.geojson_layers = [ret.layers[3]]
576
return ret
577
578
579
@mock.patch.object(
580
map_widgets.LayerManagerRow,
581
"_traitlet_link_type",
582
new=mock.Mock(return_value=ipywidgets.link),
583
) # jslink isn't supported in ipywidgets
584
class TestLayerManagerRow(unittest.TestCase):
585
"""Tests for the LayerManagerRow class in the `layer_manager` module."""
586
587
def setUp(self):
588
super().setUp()
589
self.fake_map = _create_fake_map()
590
591
def test_row_invalid_map_or_layer(self):
592
"""Tests that a valid map and layer must be passed in."""
593
with self.assertRaisesRegex(ValueError, "valid map and layer"):
594
map_widgets.LayerManagerRow(None, None)
595
596
def test_row(self):
597
"""Tests LayerManagerRow is initialized correctly for a standard layer."""
598
layer = fake_map.FakeTileLayer(name="layer-name", visible=False, opacity=0.2)
599
row = map_widgets.LayerManagerRow(self.fake_map, layer)
600
601
self.assertFalse(row.is_loading)
602
self.assertEqual(row.name, layer.name)
603
self.assertEqual(row.visible, layer.visible)
604
self.assertEqual(row.opacity, layer.opacity)
605
606
def test_geojson_row(self):
607
"""Tests LayerManagerRow is initialized correctly for a GeoJSON layer."""
608
layer = fake_map.FakeGeoJSONLayer(
609
name="layer-name", visible=True, style={"opacity": 0.2, "fillOpacity": 0.4}
610
)
611
self.fake_map.geojson_layers.append(layer)
612
row = map_widgets.LayerManagerRow(self.fake_map, layer)
613
614
self.assertEqual(row.name, layer.name)
615
self.assertTrue(row.visible)
616
self.assertEqual(row.opacity, 0.4)
617
618
def test_layer_update_row_properties(self):
619
"""Tests layer updates update row traitlets."""
620
layer = fake_map.FakeTileLayer(name="layer-name", visible=False, opacity=0.2)
621
row = map_widgets.LayerManagerRow(self.fake_map, layer)
622
623
layer.loading = True
624
layer.opacity = 0.42
625
layer.visible = True
626
self.assertTrue(row.is_loading)
627
self.assertEqual(row.opacity, 0.42)
628
self.assertTrue(row.visible)
629
630
def test_row_update_layer_properties(self):
631
"""Tests row updates update layer traitlets."""
632
layer = fake_map.FakeTileLayer(name="layer-name", visible=False, opacity=0.2)
633
row = map_widgets.LayerManagerRow(self.fake_map, layer)
634
635
row.opacity = 0.42
636
row.visible = True
637
self.assertEqual(layer.opacity, 0.42)
638
self.assertTrue(layer.visible)
639
640
def test_geojson_row_update_layer_properties(self):
641
"""Tests GeoJSON row updates update layer traitlets."""
642
layer = fake_map.FakeGeoJSONLayer(
643
name="layer-name", visible=True, style={"opacity": 0.2, "fillOpacity": 0.4}
644
)
645
self.fake_map.geojson_layers.append(layer)
646
row = map_widgets.LayerManagerRow(self.fake_map, layer)
647
648
row.opacity = 0.42
649
row.visible = True
650
self.assertEqual(layer.style["opacity"], 0.42)
651
self.assertEqual(layer.style["fillOpacity"], 0.42)
652
self.assertTrue(layer.visible)
653
654
def test_settings_button_clicked_non_ee_layer(self):
655
"""Tests that the layer vis editor is opened when settings is clicked."""
656
row = map_widgets.LayerManagerRow(self.fake_map, self.fake_map.layers[0])
657
658
msg = {"type": "click", "id": "settings"}
659
row._handle_custom_msg(msg, []) # pylint: disable=protected-access
660
661
self.fake_map.add.assert_called_once_with(
662
"layer_editor", position="bottomright", layer_dict=None
663
)
664
665
def test_settings_button_clicked_ee_layer(self):
666
"""Tests that the layer vis editor is opened when settings is clicked."""
667
row = map_widgets.LayerManagerRow(self.fake_map, self.fake_map.layers[2])
668
669
msg = {"type": "click", "id": "settings"}
670
row._handle_custom_msg(msg, []) # pylint: disable=protected-access
671
672
self.fake_map.add.assert_called_once_with(
673
"layer_editor",
674
position="bottomright",
675
layer_dict={
676
"ee_object": None,
677
"ee_layer": self.fake_map.layers[2],
678
"vis_params": None,
679
},
680
)
681
682
def test_delete_button_clicked(self):
683
"""Tests that the layer is removed when delete is clicked."""
684
row = map_widgets.LayerManagerRow(self.fake_map, self.fake_map.layers[0])
685
686
msg = {"type": "click", "id": "delete"}
687
row._handle_custom_msg(msg, []) # pylint: disable=protected-access
688
689
self.assertEqual(len(self.fake_map.layers), 3)
690
self.assertEqual(self.fake_map.layers[0].name, "GMaps")
691
self.assertEqual(self.fake_map.layers[1].name, "test-layer")
692
self.assertEqual(self.fake_map.layers[2].name, "test-geojson-layer")
693
694
695
class TestLayerManager(unittest.TestCase):
696
"""Tests for the LayerManager class in the `layer_manager` module."""
697
698
def setUp(self):
699
super().setUp()
700
self.fake_map = _create_fake_map()
701
self.layer_manager = map_widgets.LayerManager(self.fake_map)
702
703
def test_layer_manager_no_map(self):
704
"""Tests that a valid map must be passed in."""
705
with self.assertRaisesRegex(ValueError, "valid map"):
706
map_widgets.LayerManager(None)
707
708
def _validate_row(
709
self, index: int, name: str, visible: bool, opacity: float
710
) -> None:
711
child = self.layer_manager.children[index]
712
self.assertEqual(child.host_map, self.fake_map)
713
self.assertEqual(child.layer, self.fake_map.layers[index])
714
self.assertEqual(child.name, name)
715
self.assertEqual(child.visible, visible)
716
self.assertAlmostEqual(child.opacity, opacity)
717
718
def test_refresh_layers_updates_children(self):
719
"""Tests that refresh layers updates children."""
720
self.layer_manager.refresh_layers()
721
722
self.assertEqual(len(self.layer_manager.children), len(self.fake_map.layers))
723
self._validate_row(0, name="OpenStreetMap", visible=True, opacity=1.0)
724
self._validate_row(1, name="GMaps", visible=False, opacity=0.5)
725
self._validate_row(2, name="test-layer", visible=True, opacity=0.8)
726
self._validate_row(3, name="test-geojson-layer", visible=False, opacity=0.3)
727
728
def test_visibility_updates_children(self):
729
"""Tests that tweaking the visibility updates children visibilities."""
730
self.layer_manager.refresh_layers()
731
self.assertTrue(self.layer_manager.visible)
732
733
self.layer_manager.visible = False
734
for child in self.layer_manager.children:
735
self.assertFalse(child.visible)
736
737
self.layer_manager.visible = True
738
for child in self.layer_manager.children:
739
self.assertTrue(child.visible)
740
741
742
class TestBasemapSelector(unittest.TestCase):
743
"""Tests for the BasemapSelector class in the `map_widgets` module."""
744
745
def setUp(self):
746
super().setUp()
747
self.basemap_list = [
748
"DEFAULT",
749
"provider.resource-1",
750
"provider.resource-2",
751
"another-provider",
752
]
753
754
def test_basemap_default(self):
755
"""Tests that the default values are set."""
756
widget = map_widgets.BasemapSelector(self.basemap_list, "provider.resource-1")
757
self.assertEqual(
758
widget.basemaps,
759
{
760
"DEFAULT": [],
761
"provider": ["resource-1", "resource-2"],
762
"another-provider": [],
763
},
764
)
765
self.assertEqual(widget.provider, "provider")
766
self.assertEqual(widget.resource, "resource-1")
767
768
def test_basemap_default_no_resource(self):
769
"""Tests that the default values are set for no resource."""
770
widget = map_widgets.BasemapSelector(self.basemap_list, "DEFAULT")
771
self.assertEqual(widget.provider, "DEFAULT")
772
self.assertEqual(widget.resource, "")
773
774
def test_basemap_close(self):
775
"""Tests that triggering the closing button fires the close callback."""
776
widget = map_widgets.BasemapSelector(self.basemap_list, "DEFAULT")
777
on_close_mock = mock.Mock()
778
widget.on_close = on_close_mock
779
msg = {"type": "click", "id": "close"}
780
widget._handle_custom_msg(msg, []) # pylint: disable=protected-access
781
on_close_mock.assert_called_once()
782
783
def test_basemap_change(self):
784
"""Tests that value change fires the basemap_changed callback."""
785
widget = map_widgets.BasemapSelector(self.basemap_list, "provider.resource-2")
786
on_apply_mock = mock.Mock()
787
widget.on_basemap_changed = on_apply_mock
788
msg = {"type": "click", "id": "apply"}
789
widget._handle_custom_msg(msg, []) # pylint: disable=protected-access
790
on_apply_mock.assert_called_once_with("provider.resource-2")
791
792
793
@mock.patch.object(ee, "Feature", fake_ee.Feature)
794
@mock.patch.object(ee, "FeatureCollection", fake_ee.FeatureCollection)
795
@mock.patch.object(ee, "Geometry", fake_ee.Geometry)
796
@mock.patch.object(ee, "Image", fake_ee.Image)
797
class TestLayerEditor(unittest.TestCase):
798
"""Tests for the `LayerEditor` class in the `map_widgets` module."""
799
800
def _fake_layer_dict(self, ee_object):
801
return {
802
"ee_object": ee_object,
803
"ee_layer": fake_map.FakeEeTileLayer(name="fake-ee-layer-name"),
804
"vis_params": {},
805
}
806
807
def setUp(self):
808
super().setUp()
809
self._fake_map = fake_map.FakeMap()
810
pyplot.show = mock.Mock() # Plotting isn't captured by output widgets.
811
812
def test_layer_editor_no_map(self):
813
"""Tests that a valid map must be passed in."""
814
with self.assertRaisesRegex(
815
ValueError, "valid map when creating a LayerEditor widget"
816
):
817
map_widgets.LayerEditor(None, {})
818
819
def test_layer_editor_feature(self):
820
"""Tests that an ee.Feature can be passed in."""
821
widget = map_widgets.LayerEditor(
822
self._fake_map, self._fake_layer_dict(ee.Feature())
823
)
824
self.assertEqual(widget.layer_name, "fake-ee-layer-name")
825
self.assertEqual(widget.layer_type, "vector")
826
self.assertEqual(widget.band_names, [])
827
self.assertEqual(widget.colormaps, _get_colormaps())
828
829
def test_layer_editor_geometry(self):
830
"""Tests that an ee.Geometry can be passed in."""
831
widget = map_widgets.LayerEditor(
832
self._fake_map, self._fake_layer_dict(ee.Geometry())
833
)
834
self.assertEqual(widget.layer_name, "fake-ee-layer-name")
835
self.assertEqual(widget.layer_type, "vector")
836
self.assertEqual(widget.band_names, [])
837
self.assertEqual(widget.colormaps, _get_colormaps())
838
839
def test_layer_editor_feature_collection(self):
840
"""Tests that an ee.FeatureCollection can be passed in."""
841
widget = map_widgets.LayerEditor(
842
self._fake_map, self._fake_layer_dict(ee.FeatureCollection())
843
)
844
self.assertEqual(widget.layer_name, "fake-ee-layer-name")
845
self.assertEqual(widget.layer_type, "vector")
846
self.assertEqual(widget.band_names, [])
847
self.assertEqual(widget.colormaps, _get_colormaps())
848
849
def test_layer_editor_image(self):
850
"""Tests that an ee.Image can be passed in."""
851
widget = map_widgets.LayerEditor(
852
self._fake_map, self._fake_layer_dict(ee.Image())
853
)
854
self.assertEqual(widget.layer_name, "fake-ee-layer-name")
855
self.assertEqual(widget.layer_type, "raster")
856
self.assertEqual(widget.band_names, ["B1", "B2"])
857
self.assertEqual(widget.colormaps, _get_colormaps())
858
859
def test_layer_editor_handle_calculate_band_stats(self):
860
"""Tests that calculate band stats works."""
861
send_mock = mock.MagicMock()
862
widget = map_widgets.LayerEditor(
863
self._fake_map, self._fake_layer_dict(ee.Image())
864
)
865
widget.send = send_mock
866
event = {
867
"id": "band-stats",
868
"type": "calculate",
869
"detail": {"bands": ["B1"], "stretch": "sigma-1"},
870
}
871
widget._handle_message_event(None, event, None)
872
873
response = {
874
"type": "calculate",
875
"id": "band-stats",
876
"response": {"stretch": "sigma-1", "min": 21, "max": 42},
877
}
878
widget.send.assert_called_once_with(response)
879
880
def test_layer_editor_handle_calculate_palette(self):
881
"""Tests that calculate palette works."""
882
send_mock = mock.MagicMock()
883
widget = map_widgets.LayerEditor(
884
self._fake_map, self._fake_layer_dict(ee.Image())
885
)
886
widget.send = send_mock
887
event = {
888
"detail": {
889
"bandMax": 0.742053968164132,
890
"bandMin": 0.14069852859491755,
891
"classes": "3",
892
"colormap": "Blues",
893
"palette": "",
894
},
895
"id": "palette",
896
"type": "calculate",
897
}
898
widget._handle_message_event(None, event, None)
899
900
response = {
901
"type": "calculate",
902
"id": "palette",
903
"response": {"palette": "#f7fbff, #6baed6, #08306b"},
904
}
905
widget.send.assert_called_once_with(response)
906
907
def test_layer_editor_handle_calculate_field(self):
908
"""Tests that calculate fields works."""
909
send_mock = mock.MagicMock()
910
widget = map_widgets.LayerEditor(
911
self._fake_map, self._fake_layer_dict(ee.FeatureCollection())
912
)
913
widget.send = send_mock
914
event = {"detail": {}, "id": "fields", "type": "calculate"}
915
widget._handle_message_event(None, event, None)
916
917
response = {
918
"type": "calculate",
919
"id": "fields",
920
"response": {
921
"fields": ["prop-1", "prop-2"],
922
"field-values": ["aggregation-one", "aggregation-two"],
923
},
924
}
925
widget.send.assert_called_once_with(response)
926
927
def test_layer_editor_handle_calculate_field_values(self):
928
"""Tests that calculate field values works."""
929
send_mock = mock.MagicMock()
930
widget = map_widgets.LayerEditor(
931
self._fake_map, self._fake_layer_dict(ee.FeatureCollection())
932
)
933
widget.send = send_mock
934
event = {
935
"detail": {"field": "prop-1"},
936
"id": "field-values",
937
"type": "calculate",
938
}
939
widget._handle_message_event(None, event, None)
940
941
response = {
942
"type": "calculate",
943
"id": "field-values",
944
"response": {"field-values": ["aggregation-one", "aggregation-two"]},
945
}
946
widget.send.assert_called_once_with(response)
947
948
def test_layer_editor_handle_close_click(self):
949
"""Tests that close click events are handled."""
950
on_close_mock = mock.MagicMock()
951
widget = map_widgets.LayerEditor(
952
self._fake_map, self._fake_layer_dict(ee.FeatureCollection())
953
)
954
widget.on_close = on_close_mock
955
event = {"id": "close", "type": "click"}
956
widget._handle_message_event(None, event, None)
957
958
on_close_mock.assert_called_once_with()
959
960
961
if __name__ == "__main__":
962
unittest.main()
963
964