From a48e8e4f382972753027546400eba4343b84ece7 Mon Sep 17 00:00:00 2001 From: Nathaniel Schmitz Date: Wed, 23 Aug 2023 14:27:41 -0400 Subject: [PATCH 1/4] Refactor layer manager into an ipywidgets subclass (#1671) Co-authored-by: Qiusheng Wu --- geemap/geemap.py | 55 +++++++- geemap/map_widgets.py | 173 +++++++++++++++++++++++++ geemap/toolbar.py | 263 ++++---------------------------------- tests/fake_map.py | 20 ++- tests/test_map_widgets.py | 239 +++++++++++++++++++++++++++++++--- 5 files changed, 488 insertions(+), 262 deletions(-) diff --git a/geemap/geemap.py b/geemap/geemap.py index 104f6e48333..814a4214921 100644 --- a/geemap/geemap.py +++ b/geemap/geemap.py @@ -188,6 +188,8 @@ def __init__(self, **kwargs): zoom = 2 self.inspector_control = None + self.layer_manager_widget = None + self.layer_manager_control = None # Set map width and height if "height" not in kwargs: @@ -1202,7 +1204,29 @@ def create_vis_widget(self, layer_dict): Returns: object: An ipywidget. """ + has_vis_widget = hasattr(self, "_vis_widget") and self._vis_widget is not None + if layer_dict: + if has_vis_widget: + self._vis_widget = None + self._vis_widget = self._render_vis_widget(layer_dict) + if hasattr(self, "_vis_control") and self._vis_control in self.controls: + self.remove_control(self._vis_control) + self._vis_control = None + vis_control = ipyleaflet.WidgetControl( + widget=self._vis_widget, position="topright" + ) + self.add((vis_control)) + self._vis_control = vis_control + else: + if has_vis_widget: + self._vis_widget = None + if hasattr(self, "_vis_control") and self._vis_control is not None: + if self._vis_control in self.controls: + self.remove_control(self._vis_control) + self._vis_control = None + def _render_vis_widget(self, layer_dict): + """Returns the vis widget.""" import matplotlib as mpl import matplotlib.pyplot as plt @@ -2582,15 +2606,34 @@ def add_layer_manager( opened (bool, optional): Whether the control is opened. Defaults to True. show_close_button (bool, optional): Whether to show the close button. Defaults to True. """ - from .toolbar import layer_manager_gui + if self.layer_manager_control: + return - layer_manager_gui(self, position, opened, show_close_button=show_close_button) + def _on_close(): + self.toolbar_reset() + if self.layer_manager_control: + if self.layer_manager_control in self.controls: + self.remove_control(self.layer_manager_control) + self.layer_manager_control.close() + self.layer_manager_control = None + + def _on_open_vis(layer_name): + self.create_vis_widget(self.ee_layer_dict.get(layer_name, None)) + + self.layer_manager_widget = map_widgets.LayerManager(self) + self.layer_manager_widget.collapsed = not opened + self.layer_manager_widget.close_button_hidden = not show_close_button + self.layer_manager_widget.on_close = _on_close + self.layer_manager_widget.on_open_vis = _on_open_vis + self.layer_manager_control = ipyleaflet.WidgetControl( + widget=self.layer_manager_widget, position=position + ) + self.add(self.layer_manager_control) def update_layer_manager(self): """Update the Layer Manager.""" - from .toolbar import layer_manager_gui - - self.layer_manager_widget.children = layer_manager_gui(self, return_widget=True) + if self.layer_manager_widget: + self.layer_manager_widget.refresh_layers() def add_draw_control(self, position="topleft"): """Add a draw control to the map @@ -3358,7 +3401,7 @@ def close_btn_click(change): if right_label is not None: self.remove_control(right_control) - + self.dragging = True close_button.observe(close_btn_click, "value") diff --git a/geemap/map_widgets.py b/geemap/map_widgets.py index 4d6ecf9a384..68c6e4ab71f 100644 --- a/geemap/map_widgets.py +++ b/geemap/map_widgets.py @@ -606,3 +606,176 @@ def _handle_geometry_deleted(self, geo_json): del self.properties[i] self._redraw_layer() self._geometry_delete_dispatcher(self, geometry=geometry) + + +class LayerManager(ipywidgets.VBox): + def __init__(self, host_map): + """Initializes a layer manager widget. + Args: + host_map (geemap.Map): The geemap.Map object. + """ + self._host_map = host_map + if not host_map: + raise ValueError("Must pass a valid map when creating a layer manager.") + + self._collapse_button = ipywidgets.ToggleButton( + value=False, + tooltip="Layer Manager", + icon="server", + layout=ipywidgets.Layout( + width="28px", height="28px", padding="0px 0px 0px 4px" + ), + ) + self._close_button = ipywidgets.Button( + tooltip="Close the tool", + icon="times", + button_style="primary", + layout=ipywidgets.Layout(width="28px", height="28px", padding="0px"), + ) + + self._toolbar_header = ipywidgets.HBox( + children=[self._close_button, self._collapse_button] + ) + self._toolbar_footer = ipywidgets.VBox(children=[]) + + self._collapse_button.observe(self._on_collapse_click, "value") + self._close_button.on_click(self._on_close_click) + + self.on_close = None + self.on_open_vis = None + + self.collapsed = False + self.header_hidden = False + self.close_button_hidden = False + + super().__init__([self._toolbar_header, self._toolbar_footer]) + + @property + def collapsed(self): + return not self._collapse_button.value + + @collapsed.setter + def collapsed(self, value): + self._collapse_button.value = not value + + @property + def header_hidden(self): + return self._toolbar_header.layout.display == "none" + + @header_hidden.setter + def header_hidden(self, value): + self._toolbar_header.layout.display = "none" if value else "block" + + @property + def close_button_hidden(self): + return self._close_button.style.display == "none" + + @close_button_hidden.setter + def close_button_hidden(self, value): + self._close_button.style.display = "none" if value else "inline-block" + + def refresh_layers(self): + """Recreates all the layer widgets.""" + toggle_all_layout = ipywidgets.Layout( + height="18px", width="30ex", padding="0px 8px 25px 8px" + ) + toggle_all_checkbox = ipywidgets.Checkbox( + value=False, + description="All layers on/off", + indent=False, + layout=toggle_all_layout, + ) + toggle_all_checkbox.observe(self._on_all_layers_visibility_toggled, "value") + + layer_rows = [toggle_all_checkbox] + for layer in self._host_map.layers[1:]: + layer_rows.append(self._render_layer_row(layer)) + self._toolbar_footer.children = layer_rows + + def _on_close_click(self, _): + if self.on_close: + self.on_close() + + def _on_collapse_click(self, change): + if change["new"]: + self.refresh_layers() + self.children = [self._toolbar_header, self._toolbar_footer] + else: + self.children = [self._collapse_button] + + def _render_layer_row(self, layer): + visibility_checkbox = ipywidgets.Checkbox( + value=self._compute_layer_visibility(layer), + description=layer.name, + indent=False, + layout=ipywidgets.Layout(height="18px", width="140px"), + ) + visibility_checkbox.observe( + lambda change: self._on_layer_visibility_changed(change, layer), "value" + ) + + opacity_slider = ipywidgets.FloatSlider( + value=self._compute_layer_opacity(layer), + min=0, + max=1, + step=0.01, + readout=False, + layout=ipywidgets.Layout(width="80px"), + ) + opacity_slider.observe( + lambda change: self._on_layer_opacity_changed(change, layer), "value" + ) + + settings_button = ipywidgets.Button( + icon="gear", + layout=ipywidgets.Layout(width="25px", height="25px", padding="0px"), + ) + settings_button.on_click(self._on_layer_settings_click) + + return ipywidgets.HBox( + [visibility_checkbox, settings_button, opacity_slider], + layout=ipywidgets.Layout(padding="0px 8px 0px 8px"), + ) + + def _compute_layer_opacity(self, layer): + if layer in self._host_map.geojson_layers: + opacity = layer.style.get("opacity", 1.0) + fill_opacity = layer.style.get("fillOpacity", 1.0) + return max(opacity, fill_opacity) + return layer.opacity if hasattr(layer, "opacity") else 1.0 + + def _compute_layer_visibility(self, layer): + return layer.visible if hasattr(layer, "visible") else True + + def _on_layer_settings_click(self, button): + if self.on_open_vis: + self.on_open_vis(button.tooltip) + + def _on_all_layers_visibility_toggled(self, change): + for layer in self._host_map.layers: + if hasattr(layer, "visible"): + layer.visible = change["new"] + + def _on_layer_opacity_changed(self, change, layer): + if layer in self._host_map.geojson_layers: + # For non-TileLayer, use layer.style.opacity and layer.style.fillOpacity. + layer.style.update({"opacity": change["new"], "fillOpacity": change["new"]}) + elif hasattr(layer, "opacity"): + layer.opacity = change["new"] + + def _on_layer_visibility_changed(self, change, layer): + if hasattr(layer, "visible"): + layer.visible = change["new"] + + layer_name = change["owner"].description + if layer_name not in self._host_map.ee_layer_names: + return + + layer_dict = self._host_map.ee_layer_dict[layer_name] + for attachment_name in ["legend", "colorbar"]: + attachment = layer_dict.get(attachment_name, None) + attachment_on_map = attachment in self._host_map.controls + if change["new"] and not attachment_on_map: + self._host_map.add(attachment) + elif not change["new"] and attachment_on_map: + self._host_map.remove_control(attachment) diff --git a/geemap/toolbar.py b/geemap/toolbar.py index 7820b3a67fa..e32fc24bbfa 100644 --- a/geemap/toolbar.py +++ b/geemap/toolbar.py @@ -22,6 +22,7 @@ from .common import * from .timelapse import * from .geemap import MapDrawControl +from . import map_widgets class Toolbar(widgets.VBox): @@ -198,12 +199,32 @@ def _toolbar_btn_click(self, change): def _layers_btn_click(self, change): if change["new"]: # Create Layer Manager Widget - self.toolbar_footer.children = layer_manager_gui( - self.host_map, return_widget=True + if self.host_map.layer_manager_control: + return + + def _on_open_vis(layer_name): + self.host_map.create_vis_widget( + self.host_map.ee_layer_dict.get(layer_name, None) + ) + + self.host_map.layer_manager_widget = map_widgets.LayerManager(self.host_map) + self.host_map.layer_manager_widget.header_hidden = True + self.host_map.layer_manager_widget.close_button_hidden = True + self.host_map.layer_manager_widget.on_open_vis = _on_open_vis + self.host_map.layer_manager_control = ipyleaflet.WidgetControl( + widget=self.host_map.layer_manager_widget ) + self.toolbar_footer.children = [self.host_map.layer_manager_widget] else: self.toolbar_footer.children = [self.grid] + self.host_map.toolbar_reset() + if self.host_map.layer_manager_control: + if self.host_map.layer_manager_control in self.host_map.controls: + self.host_map.remove_control(self.host_map.layer_manager_control) + self.host_map.layer_manager_control.close() + self.host_map.layer_manager_control = None + def inspector_gui(m=None): """Generates a tool GUI template using ipywidgets. @@ -589,244 +610,6 @@ def handle_interaction(**kwargs): return toolbar_widget -def layer_manager_gui( - m, position="topright", opened=True, return_widget=False, show_close_button=True -): - """Creates a layer manager widget. - - Args: - m (geemap.Map): The geemap.Map object. - position (str, optional): The position of the widget. Defaults to "topright". - return_widget (bool, optional): Whether to return the widget. Defaults to False. - """ - - layers_button = widgets.ToggleButton( - value=False, - tooltip="Layer Manager", - icon="server", - layout=widgets.Layout(width="28px", height="28px", padding="0px 0px 0px 4px"), - ) - - close_button = widgets.ToggleButton( - value=False, - tooltip="Close the tool", - icon="times", - button_style="primary", - layout=widgets.Layout(height="28px", width="28px", padding="0px 0px 0px 4px"), - ) - - toolbar_header = widgets.HBox() - toolbar_header.children = [layers_button] - toolbar_footer = widgets.VBox() - toolbar_footer.children = [] - toolbar_widget = widgets.VBox() - toolbar_widget.children = [toolbar_header] - - def toolbar_btn_click(change): - if change["new"]: - close_button.value = False - toolbar_widget.children = [toolbar_header, toolbar_footer] - else: - if not close_button.value: - toolbar_widget.children = [layers_button] - - layers_button.observe(toolbar_btn_click, "value") - - def close_btn_click(change): - if change["new"]: - layers_button.value = False - m.toolbar_reset() - if m.layer_manager is not None and m.layer_manager in m.controls: - m.remove_control(m.layer_manager) - m.layer_manager = None - toolbar_widget.close() - - close_button.observe(close_btn_click, "value") - - def layers_btn_click(change): - if change["new"]: - layers_hbox = [] - all_layers_chk = widgets.Checkbox( - value=False, - description="All layers on/off", - indent=False, - layout=widgets.Layout(height="18px", padding="0px 8px 25px 8px"), - ) - all_layers_chk.layout.width = "30ex" - layers_hbox.append(all_layers_chk) - - def all_layers_chk_changed(change): - if change["new"]: - for layer in m.layers: - if hasattr(layer, "visible"): - layer.visible = True - else: - for layer in m.layers: - if hasattr(layer, "visible"): - layer.visible = False - - all_layers_chk.observe(all_layers_chk_changed, "value") - - layers = [lyr for lyr in m.layers[1:]] - - # if the layers contain unsupported layers (e.g., GeoJSON, GeoData), adds the ipyleaflet built-in LayerControl - if len(layers) < (len(m.layers) - 1): - if m.layer_control is None: - layer_control = ipyleaflet.LayersControl(position="topright") - m.layer_control = layer_control - if m.layer_control not in m.controls: - m.add(m.layer_control) - - # for non-TileLayer, use layer.style={'opacity':0, 'fillOpacity': 0} to turn layer off. - for layer in layers: - visible = True - if hasattr(layer, "visible"): - visible = layer.visible - layer_chk = widgets.Checkbox( - value=visible, - description=layer.name, - indent=False, - layout=widgets.Layout(height="18px"), - ) - layer_chk.layout.width = "140px" - - if layer in m.geojson_layers: - try: - opacity = max( - layer.style["opacity"], layer.style["fillOpacity"] - ) - except KeyError: - opacity = 1.0 - else: - if hasattr(layer, "opacity"): - opacity = layer.opacity - - layer_opacity = widgets.FloatSlider( - value=opacity, - min=0, - max=1, - step=0.01, - readout=False, - layout=widgets.Layout(width="80px"), - ) - layer_settings = widgets.ToggleButton( - icon="gear", - tooltip=layer.name, - layout=widgets.Layout( - width="25px", height="25px", padding="0px 0px 0px 5px" - ), - ) - - def layer_opacity_changed(change): - if change["new"]: - layer.style = { - "opacity": change["new"], - "fillOpacity": change["new"], - } - - def layer_vis_on_click(change): - if change["new"]: - layer_name = change["owner"].tooltip - # if layer_name in m.ee_raster_layer_names: - if layer_name in m.ee_layer_names: - layer_dict = m.ee_layer_dict[layer_name] - - if hasattr(m, "_vis_widget") and m._vis_widget is not None: - m._vis_widget = None - m._vis_widget = m.create_vis_widget(layer_dict) - if ( - hasattr(m, "_vis_control") - and m._vis_control in m.controls - ): - m.remove_control(m._vis_control) - m._vis_control = None - vis_control = ipyleaflet.WidgetControl( - widget=m._vis_widget, position="topright" - ) - m.add((vis_control)) - m._vis_control = vis_control - else: - if hasattr(m, "_vis_widget") and m._vis_widget is not None: - m._vis_widget = None - if ( - hasattr(m, "_vis_control") - and m._vis_control is not None - ): - if m._vis_control in m.controls: - m.remove_control(m._vis_control) - m._vis_control = None - change["owner"].value = False - - layer_settings.observe(layer_vis_on_click, "value") - - def layer_chk_changed(change): - layer_name = change["owner"].description - if layer_name in m.ee_layer_names: - if change["new"]: - if "legend" in m.ee_layer_dict[layer_name].keys(): - legend = m.ee_layer_dict[layer_name]["legend"] - if legend not in m.controls: - m.add(legend) - if "colorbar" in m.ee_layer_dict[layer_name].keys(): - colorbar = m.ee_layer_dict[layer_name]["colorbar"] - if colorbar not in m.controls: - m.add(colorbar) - else: - if "legend" in m.ee_layer_dict[layer_name].keys(): - legend = m.ee_layer_dict[layer_name]["legend"] - if legend in m.controls: - m.remove_control(legend) - if "colorbar" in m.ee_layer_dict[layer_name].keys(): - colorbar = m.ee_layer_dict[layer_name]["colorbar"] - if colorbar in m.controls: - m.remove_control(colorbar) - - layer_chk.observe(layer_chk_changed, "value") - - if hasattr(layer, "visible"): - widgets.jslink((layer_chk, "value"), (layer, "visible")) - - if layer in m.geojson_layers: - layer_opacity.observe(layer_opacity_changed, "value") - elif hasattr(layer, "opacity"): - widgets.jsdlink((layer_opacity, "value"), (layer, "opacity")) - hbox = widgets.HBox( - [layer_chk, layer_settings, layer_opacity], - layout=widgets.Layout(padding="0px 8px 0px 8px"), - ) - layers_hbox.append(hbox) - m.layer_widget = layers_hbox - - if show_close_button: - toolbar_header.children = [close_button, layers_button] - else: - toolbar_header.children = [layers_button] - toolbar_footer.children = layers_hbox - - else: - toolbar_header.children = [layers_button] - - layers_button.observe(layers_btn_click, "value") - layers_button.value = opened - - if not hasattr(m, "layer_manager_widget"): - m.layer_manager_widget = toolbar_footer - - if return_widget: - if hasattr(m, "layer_widget"): - return m.layer_widget - else: - return - else: - layer_control = ipyleaflet.WidgetControl( - widget=toolbar_widget, position=position - ) - - if layer_control not in m.controls: - m.add_control(layer_control) - m.layer_manager = layer_control - - def _plotting_tool_callback(map, selected): if selected: ee_plot_gui(map) diff --git a/tests/fake_map.py b/tests/fake_map.py index dba3af3676d..83fde6d91f2 100644 --- a/tests/fake_map.py +++ b/tests/fake_map.py @@ -6,7 +6,9 @@ def __init__(self): self.interaction_handlers = set() self.scale = 1024 self.zoom = 7 + self.layers = [] self.ee_layer_dict = {} + self.geojson_layers = [] self._recognized_attrs = self.__dict__.keys() @@ -40,5 +42,21 @@ def cursor_style(self): class FakeEeTileLayer: - def __init__(self, visible): + def __init__(self, name="test-layer", visible=True, opacity=1.0): + self.name = name self.visible = visible + self.opacity = opacity + + +class FakeTileLayer: + def __init__(self, name="test-layer", visible=True, opacity=1.0): + self.name = name + self.visible = visible + self.opacity = opacity + + +class FakeGeoJSONLayer: + def __init__(self, name="test-layer", visible=True, style=None): + self.name = name + self.visible = visible + self.style = style or {} diff --git a/tests/test_map_widgets.py b/tests/test_map_widgets.py index 392701880ac..2cc285f76c9 100644 --- a/tests/test_map_widgets.py +++ b/tests/test_map_widgets.py @@ -12,6 +12,28 @@ from tests import fake_ee, fake_map +def _query_widget(node, type_matcher, matcher): + """Recursively searches the widget hierarchy for the widget.""" + if hasattr(node, "layout"): + if hasattr(node.layout, "display"): + if node.layout.display == "none": + return None + if hasattr(node, "style"): + if hasattr(node.style, "display"): + if node.style.display == "none": + return None + + children = getattr(node, "children", getattr(node, "nodes", None)) + if children is not None: + for child in children: + result = _query_widget(child, type_matcher, matcher) + if result: + return result + if isinstance(node, type_matcher) and matcher(node): + return node + return None + + class TestColorbar(unittest.TestCase): """Tests for the Colorbar class in the `map_widgets` module.""" @@ -246,23 +268,12 @@ def tearDown(self): pass def _query_checkbox(self, description): - return self._query_widget( + return _query_widget( self.inspector, ipywidgets.Checkbox, lambda c: c.description == description ) def _query_node(self, root, name): - return self._query_widget(root, ipytree.Node, lambda c: c.name == name) - - def _query_widget(self, node, type_matcher, matcher): - children = getattr(node, "children", getattr(node, "nodes", None)) - if children is not None: - for child in children: - result = self._query_widget(child, type_matcher, matcher) - if result: - return result - if isinstance(node, type_matcher) and matcher(node): - return node - return None + return _query_widget(root, ipytree.Node, lambda c: c.name == name) @property def _point_checkbox(self): @@ -278,13 +289,13 @@ def _objects_checkbox(self): @property def _inspector_toggle(self): - return self._query_widget( + return _query_widget( self.inspector, ipywidgets.ToggleButton, lambda c: c.tooltip == "Inspector" ) @property def _close_toggle(self): - return self._query_widget( + return _query_widget( self.inspector, ipywidgets.ToggleButton, lambda c: c.tooltip == "Close the tool", @@ -416,3 +427,201 @@ def test_map_click_twice(self): self.assertIsNone( self._query_node(self.inspector, "Point (2.00, 1.00) at 1024m/px") ) + + +class TestLayerManager(unittest.TestCase): + """Tests for the LayerManager class in the `map_widgets` module.""" + + @property + def collapse_button(self): + """Returns the collapse button on layer_manager or None.""" + return _query_widget( + self.layer_manager, + ipywidgets.ToggleButton, + lambda c: c.tooltip == "Layer Manager", + ) + + @property + def close_button(self): + """Returns the close button on layer_manager or None.""" + return _query_widget( + self.layer_manager, + ipywidgets.Button, + lambda c: c.tooltip == "Close the tool", + ) + + @property + def toggle_all_checkbox(self): + """Returns the toggle all checkbox on layer_manager or None.""" + return _query_widget( + self.layer_manager, + ipywidgets.Checkbox, + lambda c: c.description == "All layers on/off", + ) + + @property + def layer_rows(self): + """Returns the ipywidgets rows on layer_manager.""" + return _query_widget( + self.layer_manager, ipywidgets.VBox, lambda c: True + ).children[1:] + + def _query_checkbox_on_row(self, row, name): + return _query_widget(row, ipywidgets.Checkbox, lambda c: c.description == name) + + def _query_slider_on_row(self, row): + return _query_widget(row, ipywidgets.FloatSlider, lambda _: True) + + def _query_button_on_row(self, row): + return _query_widget(row, ipywidgets.Button, lambda _: True) + + def _validate_row(self, row, name, checked, opacity): + self.assertEqual(self._query_checkbox_on_row(row, name).value, checked) + self.assertEqual(self._query_slider_on_row(row).value, opacity) + self.assertIsNotNone(self._query_button_on_row(row)) + + def setUp(self): + self.fake_map = fake_map.FakeMap() + self.fake_map.layers = [ + fake_map.FakeTileLayer(name=None), # Basemap + fake_map.FakeTileLayer( + name="GMaps", visible=False, opacity=0.5 + ), # Extra basemap + fake_map.FakeEeTileLayer(name="test-layer", visible=True, opacity=0.8), + fake_map.FakeGeoJSONLayer( + name="test-geojson-layer", + visible=False, + style={"some-style": "red", "opacity": 0.3, "fillOpacity": 0.2}, + ), + ] + self.fake_map.ee_layer_dict = { + "test-layer": { + "ee_object": None, + "ee_layer": self.fake_map.layers[2], + "vis_params": None, + }, + } + self.fake_map.geojson_layers = [self.fake_map.layers[3]] + + self.layer_manager = map_widgets.LayerManager(self.fake_map) + + def test_layer_manager_no_map(self): + """Tests that a valid map must be passed in.""" + with self.assertRaisesRegex(ValueError, "valid map"): + map_widgets.LayerManager(None) + + def test_layer_manager(self): + self.assertIsNotNone(self.collapse_button) + self.assertIsNotNone(self.close_button) + self.assertIsNotNone(self.toggle_all_checkbox) + + # Verify computed properties are correct. + self.assertFalse(self.layer_manager.collapsed) + self.assertFalse(self.layer_manager.header_hidden) + self.assertFalse(self.layer_manager.close_button_hidden) + + self.assertEqual(len(self.layer_rows), 3) + self._validate_row(self.layer_rows[0], "GMaps", False, 0.5) + self._validate_row(self.layer_rows[1], "test-layer", True, 0.8) + self._validate_row(self.layer_rows[2], "test-geojson-layer", False, 0.3) + + def test_layer_manager_toggle_all_visibility(self): + """Tests that the toggle all checkbox changes visibilities.""" + # True then False because the event doesn't fire if the value doesn't change. + self.toggle_all_checkbox.value = True + self.toggle_all_checkbox.value = False + + for layer in self.fake_map.layers: + self.assertEqual( + layer.visible, False, f"{layer.name} should not be visible" + ) + + self.toggle_all_checkbox.value = True + + for layer in self.fake_map.layers: + self.assertEqual(layer.visible, True, f"{layer.name} should be visible") + + def test_layer_manager_opacity_changed(self): + """Tests that the opacity slider changes opacities.""" + ee_layer = self.layer_rows[1] + ee_layer_slider = self._query_slider_on_row(ee_layer) + ee_layer_slider.value = 0.01 + self.assertEqual(self.fake_map.layers[2].opacity, 0.01) + + geojson_layer = self.layer_rows[2] + geojson_layer_slider = self._query_slider_on_row(geojson_layer) + geojson_layer_slider.value = 0.02 + self.assertEqual( + self.fake_map.layers[3].style, + {"some-style": "red", "opacity": 0.02, "fillOpacity": 0.02}, + ) + + def test_layer_manager_click_settings(self): + """Tests that the settings button fires an event.""" + on_open_vis_mock = Mock() + self.layer_manager.on_open_vis = on_open_vis_mock + ee_layer_button = self._query_button_on_row(self.layer_rows[1]) + + ee_layer_button.click() + + on_open_vis_mock.assert_called_once() + + def test_layer_manager_click_close(self): + """Tests that the close button fires an event.""" + on_close_mock = Mock() + self.layer_manager.on_close = on_close_mock + + self.close_button.click() + + on_close_mock.assert_called_once() + + def test_layer_manager_refresh_layers(self): + """Tests that refresh_layers refreshes the layers.""" + self.fake_map.layers = [] + self.layer_manager.refresh_layers() + + self.assertEqual(len(self.layer_rows), 0) + + def test_layer_manager_collapsed(self): + """Tests that setting the collapsed property collapses the widget.""" + self.layer_manager.collapsed = True + + self.assertIsNotNone(self.collapse_button) + self.assertIsNone(self.close_button) + self.assertIsNone(self.toggle_all_checkbox) + self.assertEqual(len(self.layer_rows), 0) + + self.layer_manager.collapsed = False + + self.assertIsNotNone(self.collapse_button) + self.assertIsNotNone(self.close_button) + self.assertIsNotNone(self.toggle_all_checkbox) + self.assertEqual(len(self.layer_rows), 3) + + def test_layer_manager_header_hidden(self): + """Tests that setting the header_hidden property hides the header.""" + self.layer_manager.header_hidden = True + + self.assertIsNone(self.collapse_button) + self.assertIsNone(self.close_button) + self.assertIsNotNone(self.toggle_all_checkbox) + + self.layer_manager.header_hidden = False + + self.assertIsNotNone(self.collapse_button) + self.assertIsNotNone(self.close_button) + self.assertIsNotNone(self.toggle_all_checkbox) + + def test_layer_manager_close_button_hidden(self): + """Tests that setting the close_button_hidden property hides the close button.""" + self.layer_manager.close_button_hidden = True + + self.assertIsNotNone(self.collapse_button) + self.assertIsNone(self.close_button) + self.assertIsNotNone(self.toggle_all_checkbox) + + self.layer_manager.close_button_hidden = False + + self.assertIsNotNone(self.collapse_button) + self.assertIsNotNone(self.close_button) + self.assertIsNotNone(self.toggle_all_checkbox) From 06e04996a7620c4b1cc5a01620539858b635c27e Mon Sep 17 00:00:00 2001 From: sufyanAbbasi Date: Wed, 23 Aug 2023 20:12:51 -0700 Subject: [PATCH 2/4] Added unit tests for draw control. (#1674) Co-authored-by: Sufyan Abbasi --- geemap/geemap.py | 3 +- geemap/map_widgets.py | 24 ++-- tests/fake_ee.py | 36 +++++- tests/fake_map.py | 22 ++++ tests/test_map_widgets.py | 255 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 324 insertions(+), 16 deletions(-) diff --git a/geemap/geemap.py b/geemap/geemap.py index 814a4214921..005b3a8cb91 100644 --- a/geemap/geemap.py +++ b/geemap/geemap.py @@ -37,7 +37,7 @@ class MapDrawControl(ipyleaflet.DrawControl, map_widgets.AbstractDrawControl): - """ "Implements the AbstractDrawControl for the map.""" + """Implements the AbstractDrawControl for the map.""" _roi_start = False _roi_end = False @@ -115,6 +115,7 @@ def _remove_geometry_at_index_on_draw_control(self, index): pass def _clear_draw_control(self): + self.data = [] # Remove all drawn features from the map. return self.clear() diff --git a/geemap/map_widgets.py b/geemap/map_widgets.py index 68c6e4ab71f..3c143f4409a 100644 --- a/geemap/map_widgets.py +++ b/geemap/map_widgets.py @@ -466,11 +466,10 @@ def last_feature(self): def count(self): return len(self.geometries) - def reset(self, clear_draw_control=False): + def reset(self, clear_draw_control=True): """Resets the draw controls.""" if self.layer is not None: self.host_map.remove_layer(self.layer) - self.data = [] # Remove all drawn features from the map. self.geometries = [] self.properties = [] self.last_geometry = None @@ -492,7 +491,10 @@ def remove_geometry(self, geometry): self._remove_geometry_at_index_on_draw_control(index) if index == self.count and geometry == self.last_geometry: # Treat this like an "undo" of the last drawn geometry. - self.last_geometry = self.geometries[-1] + if len(self.geometries): + self.last_geometry = self.geometries[-1] + else: + self.last_geometry = geometry self.last_draw_action = DrawActions.REMOVED_LAST if self.layer is not None: self._redraw_layer() @@ -543,7 +545,7 @@ def _clear_draw_control(self): raise NotImplementedError() def _get_synced_geojson_from_draw_control(self): - """Returns an up-to-date of GeoJSON from the draw control.""" + """Returns an up-to-date list of GeoJSON from the draw control.""" raise NotImplementedError() def _sync_geometries(self): @@ -601,11 +603,15 @@ def _handle_geometry_deleted(self, geo_json): geometry = common.geojson_to_ee(geo_json, False) self.last_geometry = geometry self.last_draw_action = DrawActions.DELETED - i = self.geometries.index(geometry) - del self.geometries[i] - del self.properties[i] - self._redraw_layer() - self._geometry_delete_dispatcher(self, geometry=geometry) + try: + index = self.geometries.index(geometry) + except ValueError: + return + if index >= 0: + del self.geometries[index] + del self.properties[index] + self._redraw_layer() + self._geometry_delete_dispatcher(self, geometry=geometry) class LayerManager(ipywidgets.VBox): diff --git a/tests/fake_ee.py b/tests/fake_ee.py index 33890c03c8a..e23b1e35b0e 100644 --- a/tests/fake_ee.py +++ b/tests/fake_ee.py @@ -38,12 +38,16 @@ def getInfo(self): class Geometry: - def __init__(self, *_, **kwargs): + geometry = None + + def __init__(self, *args, **kwargs): + if len(args): + self.geometry = args[0] if kwargs.get("type"): self.geom_type = kwargs.get("type") @classmethod - def Point(self, *_, **__): + def Point(self, lat, **__): return Geometry(type=String("Point")) @classmethod @@ -53,6 +57,9 @@ def BBox(self, *_, **__): def type(self, *_, **__): return self.geom_type + def __eq__(self, other: object): + return self.geometry == getattr(other, 'geometry') + class String: def __init__(self, value): @@ -63,8 +70,11 @@ def compareTo(self, other_str): class FeatureCollection: - def __init__(self, *_, **__): - pass + features = [] + + def __init__(self, *args, **_): + if len(args): + self.features = args[0] def style(self, *_, **__): return Image() @@ -75,10 +85,19 @@ def first(self, *_, **__): def filterBounds(self, *_, **__): return FeatureCollection() + def __eq__(self, other: object): + return self.features == getattr(other, 'features') + class Feature: - def __init__(self, *_, **__): - pass + feature = None + properties = None + + def __init__(self, *args, **_): + if len(args) > 0: + self.feature = args[0] + if len(args) >= 2: + self.properties = args[1] def geometry(self, *_, **__): return Geometry(type=String("Polygon")) @@ -99,6 +118,11 @@ def getInfo(self, *_, **__): }, } + def __eq__(self, other: object): + featuresEqual = self.feature == getattr(other, 'feature') + propertiesEqual = self.properties == getattr(other, 'properties') + return featuresEqual and propertiesEqual + class ImageCollection: def __init__(self, *_, **__): diff --git a/tests/fake_map.py b/tests/fake_map.py index 83fde6d91f2..b3c1d3562b0 100644 --- a/tests/fake_map.py +++ b/tests/fake_map.py @@ -8,6 +8,7 @@ def __init__(self): self.zoom = 7 self.layers = [] self.ee_layer_dict = {} + self.layers = [] self.geojson_layers = [] self._recognized_attrs = self.__dict__.keys() @@ -36,6 +37,27 @@ def click(self, coordinates, event_type): def get_scale(self): return self.scale + def find_layer_index(self, name): + layers = self.layers + + for index, layer in enumerate(layers): + if layer.name == name: + return index + + return -1 + + def add_layer(self, layer): + self.layers.append(layer) + + def remove_layer(self, layer): + self.layers.remove(layer) + + def substitute(self, old_layer, new_layer): + i = self.find_layer_index(old_layer) + if i >= 0: + self.layers[i] = new_layer + pass + @property def cursor_style(self): return self.default_style.get("cursor") diff --git a/tests/test_map_widgets.py b/tests/test_map_widgets.py index 2cc285f76c9..b22363dcfbd 100644 --- a/tests/test_map_widgets.py +++ b/tests/test_map_widgets.py @@ -625,3 +625,258 @@ def test_layer_manager_close_button_hidden(self): self.assertIsNotNone(self.collapse_button) self.assertIsNotNone(self.close_button) self.assertIsNotNone(self.toggle_all_checkbox) + + +@patch.object(ee, "FeatureCollection", fake_ee.FeatureCollection) +@patch.object(ee, "Feature", fake_ee.Feature) +@patch.object(ee, "Geometry", fake_ee.Geometry) +@patch.object(ee, "Image", fake_ee.Image) +class TestAbstractDrawControl(unittest.TestCase): + """Tests for the draw control interface in the `map_widgets` module.""" + geo_json = { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [0, 1], + [0, -1], + [1, -1], + [1, 1], + [0, 1], + ] + ], + }, + "properties": { + "name": "Null Island" + } + } + geo_json2 = { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [0, 2], + [0, -2], + [2, -2], + [2, 2], + [0, 2], + ] + ], + }, + "properties": { + "name": "Null Island 2x" + } + } + + def setUp(self): + map = fake_map.FakeMap() + self.draw_control = TestAbstractDrawControl.TestDrawControl(map) + + def tearDown(self): + pass + + def test_initialization(self): + # Initialized is set by the `_bind_draw_controls` method. + self.assertTrue(self.draw_control.initialized) + self.assertIsNone(self.draw_control.layer) + self.assertEquals(self.draw_control.geometries, []) + self.assertEquals(self.draw_control.properties, []) + self.assertIsNone(self.draw_control.last_geometry) + self.assertIsNone(self.draw_control.last_draw_action) + self.assertEquals(self.draw_control.features, []) + self.assertEquals(self.draw_control.collection, fake_ee.FeatureCollection([])) + self.assertIsNone(self.draw_control.last_feature) + self.assertEquals(self.draw_control.count, 0) + + def test_handles_creation(self): + self.draw_control.create(self.geo_json) + self.assertEquals( + self.draw_control.geometries, + [fake_ee.Geometry(self.geo_json["geometry"])], + ) + + def test_handles_deletion(self): + self.draw_control.create(self.geo_json) + self.assertEquals(len(self.draw_control.geometries), 1) + self.draw_control.delete(0) + self.assertEquals(len(self.draw_control.geometries), 0) + + def test_handles_edit(self): + self.draw_control.create(self.geo_json) + self.assertEquals(len(self.draw_control.geometries), 1) + + self.draw_control.edit(0, self.geo_json2) + self.assertEquals(len(self.draw_control.geometries), 1) + self.assertEquals( + self.draw_control.geometries[0], + fake_ee.Geometry(self.geo_json2["geometry"]), + ) + + def test_property_accessors(self): + self.draw_control.create(self.geo_json) + + # Test layer accessor. + self.assertIsNotNone(self.draw_control.layer) + # Test geometries accessor. + geometry = fake_ee.Geometry(self.geo_json["geometry"]) + self.assertEquals(len(self.draw_control.geometries), 1) + self.assertEquals(self.draw_control.geometries, [geometry]) + # Test properties accessor. + self.assertEquals(self.draw_control.properties, [None]) + # Test last_geometry accessor. + self.assertEquals(self.draw_control.last_geometry, geometry) + # Test last_draw_action accessor. + self.assertEquals( + self.draw_control.last_draw_action, + map_widgets.DrawActions.CREATED + ) + # Test features accessor. + feature = fake_ee.Feature(geometry, None) + self.assertEquals(self.draw_control.features, [feature]) + # Test collection accessor. + self.assertEquals( + self.draw_control.collection, + fake_ee.FeatureCollection([feature]) + ) + # Test last_feature accessor. + self.assertEquals(self.draw_control.last_feature, feature) + # Test count accessor. + self.assertEquals(self.draw_control.count, 1) + + def test_feature_property_access(self): + self.draw_control.create(self.geo_json) + geometry = self.draw_control.geometries[0] + self.assertIsNone( + self.draw_control.get_geometry_properties(geometry) + ) + self.assertEquals( + self.draw_control.features, + [fake_ee.Feature(geometry, None)] + ) + self.draw_control.set_geometry_properties(geometry, {"test": 1}) + self.assertEquals( + self.draw_control.features, + [fake_ee.Feature(geometry, {"test": 1})] + ) + + def test_reset(self): + self.draw_control.create(self.geo_json) + self.assertEquals(len(self.draw_control.geometries), 1) + + # When clear_draw_control is True, deletes the underlying geometries. + self.draw_control.reset(clear_draw_control=True) + self.assertEquals(len(self.draw_control.geometries), 0) + self.assertEquals(len(self.draw_control.geo_jsons), 0) + + self.draw_control.create(self.geo_json) + self.assertEquals(len(self.draw_control.geometries), 1) + # When clear_draw_control is False, does not delete the underlying + # geometries. + self.draw_control.reset(clear_draw_control=False) + self.assertEquals(len(self.draw_control.geometries), 0) + self.assertEquals(len(self.draw_control.geo_jsons), 1) + + def test_remove_geometry(self): + self.draw_control.create(self.geo_json) + self.draw_control.create(self.geo_json2) + geometry1 = self.draw_control.geometries[0] + geometry2 = self.draw_control.geometries[1] + self.assertEquals(len(self.draw_control.geometries), 2) + self.assertEquals(len(self.draw_control.properties), 2) + self.assertEquals( + self.draw_control.last_draw_action, + map_widgets.DrawActions.CREATED + ) + self.assertEquals(self.draw_control.last_geometry, geometry2) + + # When there are two geometries and the removed geometry is the last + # one, then we treat it like an undo. + self.draw_control.remove_geometry(geometry2) + self.assertEquals(len(self.draw_control.geometries), 1) + self.assertEquals(len(self.draw_control.properties), 1) + self.assertEquals( + self.draw_control.last_draw_action, + map_widgets.DrawActions.REMOVED_LAST + ) + self.assertEquals(self.draw_control.last_geometry, geometry1) + + # When there's only one geometry, last_geometry is the removed geometry. + self.draw_control.remove_geometry(geometry1) + self.assertEquals(len(self.draw_control.geometries), 0) + self.assertEquals(len(self.draw_control.properties), 0) + self.assertEquals( + self.draw_control.last_draw_action, + map_widgets.DrawActions.REMOVED_LAST + ) + self.assertEquals(self.draw_control.last_geometry, geometry1) + + # When there are two geometries and the removed geometry is the first + # one, then treat it like a normal delete. + self.draw_control.create(self.geo_json) + self.draw_control.create(self.geo_json2) + geometry1 = self.draw_control.geometries[0] + geometry2 = self.draw_control.geometries[1] + self.draw_control.remove_geometry(geometry1) + self.assertEquals(len(self.draw_control.geometries), 1) + self.assertEquals(len(self.draw_control.properties), 1) + self.assertEquals( + self.draw_control.last_draw_action, + map_widgets.DrawActions.DELETED + ) + self.assertEquals(self.draw_control.last_geometry, geometry1) + + class TestDrawControl(map_widgets.AbstractDrawControl): + """Implements an AbstractDrawControl for tests.""" + geo_jsons = [] + initialized = False + + def __init__(self, host_map, **kwargs): + """Initialize the test draw control. + + Args: + host_map (geemap.Map): The geemap.Map object + """ + super(TestAbstractDrawControl.TestDrawControl, self).__init__( + host_map=host_map, + **kwargs + ) + self.geo_jsons = [] + + def _get_synced_geojson_from_draw_control(self): + return [data.copy() for data in self.geo_jsons] + + def _bind_to_draw_control(self): + # In a non-test environment, `_on_draw` would be used here. + self.initialized = True + + def _remove_geometry_at_index_on_draw_control(self, index): + geo_json = self.geo_jsons[index] + del self.geo_jsons[index] + self._on_draw("deleted", geo_json) + + def _clear_draw_control(self): + self.geo_jsons = [] + + def _on_draw(self, action, geo_json): + """Mimics the ipyleaflet DrawControl handler.""" + if action == "created": + self._handle_geometry_created(geo_json) + elif action == "edited": + self._handle_geometry_edited(geo_json) + elif action == "deleted": + self._handle_geometry_deleted(geo_json) + + def create(self, geo_json): + self.geo_jsons.append(geo_json) + self._on_draw("created", geo_json) + + def edit(self, i, geo_json): + self.geo_jsons[i] = geo_json + self._on_draw("edited", geo_json) + + def delete(self, i): + geo_json = self.geo_jsons[i] + del self.geo_jsons[i] + self._on_draw("deleted", geo_json) From e665000deaef07c697dbb64eedc1d9fa3c55c9a4 Mon Sep 17 00:00:00 2001 From: Qiusheng Wu Date: Wed, 23 Aug 2023 23:45:12 -0400 Subject: [PATCH 3/4] Added G4G workshop notebook (#1676) --- docs/workshops/G4G_2023.ipynb | 3147 ++++++++++++++++++++++++++++ examples/workshops/G4G_2023.ipynb | 3148 +++++++++++++++++++++++++++++ mkdocs.yml | 1 + 3 files changed, 6296 insertions(+) create mode 100644 docs/workshops/G4G_2023.ipynb create mode 100644 examples/workshops/G4G_2023.ipynb diff --git a/docs/workshops/G4G_2023.ipynb b/docs/workshops/G4G_2023.ipynb new file mode 100644 index 00000000000..69cd9c06503 --- /dev/null +++ b/docs/workshops/G4G_2023.ipynb @@ -0,0 +1,3147 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[![image](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/gee-community/geemap/blob/master/docs/workshops/G4G_2023.ipynb)\n", + "\n", + "**Introduction to Google Earth Engine in Python**\n", + "\n", + "* Notebook: \n", + "* Earth Engine: \n", + "* Geemap: \n", + "\n", + "## Introduction (10 mins)\n", + "\n", + "This notebook is for the workshop presented at the [Geo for Good Summit 2023](https://earthoutreachonair.withgoogle.com/events/geoforgood23). Check out [this link](https://earthoutreachonair.withgoogle.com/events/geoforgood23?tab=g4gagenda&expand=module:agenda) for more information about the workshop.\n", + "\n", + "### Abstract\n", + "\n", + "This workshop provides an introduction to cloud-based geospatial analysis using the Earth Engine Python API. Attendees will learn the basics of Earth Engine data types and how to visualize, analyze, and export Earth Engine data in a Jupyter environment with geemap. In addition, attendees will learn how to develop and deploy interactive Earth Engine web apps with Python. Through practical examples and hands-on exercises, attendees will enhance their learning experience. During each hands-on session, attendees will walk through Jupyter Notebook examples on Google Colab with the instructors. At the end of each session, they will complete a hands-on exercise to apply the knowledge they have learned.\n", + "\n", + "### Prerequisites\n", + "\n", + "- To use geemap and the Earth Engine Python API, you must [register](https://code.earthengine.google.com/register) for an Earth Engine account and follow the instructions [here](https://docs.google.com/document/d/1ZGSmrNm6_baqd8CHt33kIBWOlvkh-HLr46bODgJN1h0/edit?usp=sharing) to create a Cloud Project. Earth Engine is free for [noncommercial and research use](https://earthengine.google.com/noncommercial). To test whether you can use authenticate the Earth Engine Python API, please run [this notebook](https://colab.research.google.com/github/giswqs/geemap/blob/master/examples/notebooks/geemap_colab.ipynb) on Google Colab.\n", + "\n", + "- It is recommended that attendees have a basic understanding of Python and Jupyter Notebook. \n", + "- Familiarity with the Earth Engine JavaScript API is not required but will be helpful. \n", + "- Attendees can use Google Colab to follow this workshop without installing anything on their computer.\n", + "\n", + "## Introduction to Earth Engine and geemap (15 mins)\n", + "\n", + "Earth Engine is free for [noncommercial and research use](https://earthengine.google.com/noncommercial). For more than a decade, Earth Engine has enabled planetary-scale Earth data science and analysis by nonprofit organizations, research scientists, and other impact users.\n", + "\n", + "With the launch of Earth Engine for [commercial use](https://earthengine.google.com/commercial), commercial customers will be charged for Earth Engine services. However, Earth Engine will remain free of charge for noncommercial use and research projects. Nonprofit organizations, academic institutions, educators, news media, Indigenous governments, and government researchers are eligible to use Earth Engine free of charge, just as they have done for over a decade.\n", + "\n", + "The geemap Python package is built upon the Earth Engine Python API and open-source mapping libraries. It allows Earth Engine users to interactively manipulate, analyze, and visualize geospatial big data in a Jupyter environment. Since its creation in April 2020, geemap has received over 2,800 GitHub stars and is being used by over 1,000 projects on GitHub. \n", + "\n", + "## Google Colab and Earth Engine Python API authentication (5 mins)\n", + "\n", + "[![image](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/gee-community/geemap/blob/master/docs/workshops/G4G_2023.ipynb)\n", + "\n", + "### Install geemap\n", + "\n", + "Uncomment the following line to install geemap if you are running this notebook in Google Colab." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# %pip install geemap[workshop]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import ee\n", + "import geemap" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Authenticate and initialize Earth Engine\n", + "\n", + "You will need to create a [Google Cloud Project](https://console.cloud.google.com/projectcreate) and enable the [Earth Engine API](https://console.cloud.google.com/apis/api/earthengine.googleapis.com) for the project. You can find detailed instructions [here](https://book.geemap.org/chapters/01_introduction.html#earth-engine-authentication)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_initialize()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Creating interactive maps\n", + "\n", + "Let's create an interactive map using the `ipyleaflet` plotting backend. The [`geemap.Map`](https://geemap.org/geemap/#geemap.geemap.Map) class inherits the [`ipyleaflet.Map`](https://ipyleaflet.readthedocs.io/en/latest/map_and_basemaps/map.html) class. Therefore, you can use the same syntax to create an interactive map as you would with `ipyleaflet.Map`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To display it in a Jupyter notebook, simply ask for the object representation:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To customize the map, you can specify various keyword arguments, such as `center` ([lat, lon]), `zoom`, `width`, and `height`. The default `width` is `100%`, which takes up the entire cell width of the Jupyter notebook. The `height` argument accepts a number or a string. If a number is provided, it represents the height of the map in pixels. If a string is provided, the string must be in the format of a number followed by `px`, e.g., `600px`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4, height=600)\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To hide a control, set `control_name` to `False`, e.g., `draw_ctrl=False`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(data_ctrl=False, toolbar_ctrl=False, draw_ctrl=False)\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Adding basemaps\n", + "\n", + "There are several ways to add basemaps to a map. You can specify the basemap to use in the `basemap` keyword argument when creating the map. Alternatively, you can add basemap layers to the map using the `add_basemap` method. Geemap has hundreds of built-in basemaps available that can be easily added to the map with only one line of code.\n", + "\n", + "Create a map by specifying the basemap to use as follows. For example, the `HYBRID` basemap represents the Google Satellite Hybrid basemap." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(basemap='HYBRID')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can add as many basemaps as you like to the map. For example, the following code adds the `OpenTopoMap` basemap to the map above:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map.add_basemap('OpenTopoMap')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Print out the first 10 basemaps:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "basemaps = list(geemap.basemaps.keys())\n", + "len(geemap.basemaps)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "basemaps[:10]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Using Earth Engine data (30 mins)\n", + "\n", + "### Earth Engine data types (Image, ImageCollection, Geometry, Feature, FeatureCollection)\n", + "\n", + "Earth Engine objects are server-side objects rather than client-side objects, which means that they are not stored locally on your computer. Similar to video streaming services (e.g., YouTube, Netflix, and Hulu), which store videos/movies on their servers, Earth Engine data are stored on the Earth Engine servers. We can stream geospatial data from Earth Engine on-the-fly without having to download the data just like we can watch videos from streaming services using a web browser without having to download the entire video to your computer.\n", + "\n", + "- **Image**: the fundamental raster data type in Earth Engine.\n", + "- **ImageCollection**: a stack or time-series of images.\n", + "- **Geometry**: the fundamental vector data type in Earth Engine.\n", + "- **Feature**: a Geometry with attributes.\n", + "- **FeatureCollection**: a set of features.\n", + "\n", + "### Image\n", + "\n", + "Raster data in Earth Engine are represented as **Image** objects. Images are composed of one or more bands and each band has its own name, data type, scale, mask and projection. Each image has metadata stored as a set of properties.\n", + "\n", + "#### Loading Earth Engine images" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "image = ee.Image('USGS/SRTMGL1_003')\n", + "image" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Visualizing Earth Engine images" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[21.79, 70.87], zoom=3)\n", + "image = ee.Image('USGS/SRTMGL1_003')\n", + "vis_params = {\n", + " 'min': 0,\n", + " 'max': 6000,\n", + " 'palette': ['006633', 'E5FFCC', '662A00', 'D8D8D8', 'F5F5F5'],\n", + "}\n", + "Map.addLayer(image, vis_params, 'SRTM')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### ImageCollection\n", + "\n", + "An `ImageCollection` is a stack or sequence of images. An `ImageCollection` can be loaded by passing an Earth Engine asset ID into the `ImageCollection` constructor. You can find `ImageCollection` IDs in the [Earth Engine Data Catalog](https://developers.google.com/earth-engine/datasets).\n", + "\n", + "#### Loading image collections\n", + "\n", + "For example, to load the image collection of the [Sentinel-2 surface reflectance](https://developers.google.com/earth-engine/datasets/catalog/COPERNICUS_S2_SR):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "collection = ee.ImageCollection('COPERNICUS/S2_SR')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Visualizing image collections\n", + "\n", + "To visualize an Earth Engine **ImageCollection**, we need to convert an **ImageCollection** to an **Image** by compositing all the images in the collection to a single image representing, for example, the min, max, median, mean or standard deviation of the images. For example, to create a median value image from a collection, use the `collection.median()` method. Let's create a median image from the Sentinel-2 surface reflectance collection:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "collection = ee.ImageCollection('COPERNICUS/S2_SR')\n", + "image = collection.median()\n", + "\n", + "vis = {\n", + " 'min': 0.0,\n", + " 'max': 3000,\n", + " 'bands': ['B4', 'B3', 'B2'],\n", + "}\n", + "\n", + "Map.setCenter(83.277, 17.7009, 12)\n", + "Map.addLayer(image, vis, 'Sentinel-2')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Filtering image collections" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "collection = (\n", + " ee.ImageCollection('COPERNICUS/S2_SR')\n", + " .filterDate('2021-01-01', '2022-01-01')\n", + " .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 5))\n", + ")\n", + "image = collection.median()\n", + "\n", + "vis = {\n", + " 'min': 0.0,\n", + " 'max': 3000,\n", + " 'bands': ['B4', 'B3', 'B2'],\n", + "}\n", + "\n", + "Map.setCenter(83.277, 17.7009, 12)\n", + "Map.addLayer(image, vis, 'Sentinel-2')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### FeatureCollection\n", + "\n", + "A **FeatureCollection** is a collection of Features. A FeatureCollection is analogous to a GeoJSON FeatureCollection object, i.e., a collection of features with associated properties/attributes. Data contained in a shapefile can be represented as a FeatureCollection.\n", + "\n", + "#### Loading feature collections\n", + "\n", + "The [Earth Engine Data Catalog](https://developers.google.com/earth-engine/datasets) hosts a variety of vector datasets (e.g,, US Census data, country boundaries, and more) as feature collections. You can find feature collection IDs by searching the data catalog. For example, to load the [TIGER roads data](https://developers.google.com/earth-engine/datasets/catalog/TIGER_2016_Roads) by the U.S. Census Bureau:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "fc = ee.FeatureCollection('TIGER/2016/Roads')\n", + "Map.setCenter(-73.9596, 40.7688, 12)\n", + "Map.addLayer(fc, {}, 'Census roads')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Filtering feature collections" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "states = ee.FeatureCollection('TIGER/2018/States')\n", + "fc = states.filter(ee.Filter.eq('NAME', 'Louisiana'))\n", + "Map.addLayer(fc, {}, 'Louisiana')\n", + "Map.centerObject(fc, 7)\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "feat = fc.first()\n", + "feat.toDictionary()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "states = ee.FeatureCollection('TIGER/2018/States')\n", + "fc = states.filter(ee.Filter.inList('NAME', ['California', 'Oregon', 'Washington']))\n", + "Map.addLayer(fc, {}, 'West Coast')\n", + "Map.centerObject(fc, 5)\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "region = Map.user_roi\n", + "if region is None:\n", + " region = ee.Geometry.BBox(-88.40, 29.88, -77.90, 35.39)\n", + "\n", + "fc = ee.FeatureCollection('TIGER/2018/States').filterBounds(region)\n", + "Map.addLayer(fc, {}, 'Southeastern U.S.')\n", + "Map.centerObject(fc, 6)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Visualizing feature collections" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4)\n", + "states = ee.FeatureCollection(\"TIGER/2018/States\")\n", + "Map.addLayer(states, {}, \"US States\")\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4)\n", + "states = ee.FeatureCollection(\"TIGER/2018/States\")\n", + "style = {'color': '0000ffff', 'width': 2, 'lineType': 'solid', 'fillColor': 'FF000080'}\n", + "Map.addLayer(states.style(**style), {}, \"US States\")\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4)\n", + "states = ee.FeatureCollection(\"TIGER/2018/States\")\n", + "vis_params = {\n", + " 'color': '000000',\n", + " 'colorOpacity': 1,\n", + " 'pointSize': 3,\n", + " 'pointShape': 'circle',\n", + " 'width': 2,\n", + " 'lineType': 'solid',\n", + " 'fillColorOpacity': 0.66,\n", + "}\n", + "palette = ['006633', 'E5FFCC', '662A00', 'D8D8D8', 'F5F5F5']\n", + "Map.add_styled_vector(\n", + " states, column=\"NAME\", palette=palette, layer_name=\"Styled vector\", **vis_params\n", + ")\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Earth Engine Data Catalog\n", + "\n", + "The [Earth Engine Data Catalog](https://developers.google.com/earth-engine/datasets) hosts a variety of geospatial datasets. As of March 2023, the catalog contains over [1,000 datasets](https://github.com/samapriya/Earth-Engine-Datasets-List) with a total size of over 80 petabytes. Some notable datasets include: Landsat, Sentinel, MODIS, NAIP, etc. For a complete list of datasets in CSV or JSON formats, see the [Earth Engine Datasets List](https://github.com/giswqs/Earth-Engine-Catalog/blob/master/gee_catalog.tsv).\n", + "\n", + "#### Searching for datasets\n", + "\n", + "The [Earth Engine Data Catalog](https://developers.google.com/earth-engine/datasets/catalog) is searchable. You can search datasets by name, keyword, or tag. For example, enter \"elevation\" in the search box will filter the catalog to show only datasets containing \"elevation\" in their name, description, or tags. 52 datasets are returned for this search query. Scroll down the list to find the [NASA SRTM Digital Elevation 30m](https://developers.google.com/earth-engine/datasets/catalog/USGS_SRTMGL1_003#description) dataset. On each dataset page, you can find the following information, including Dataset Availability, Dataset Provider, Earth Engine Snippet, Tags, Description, Code Example, and more (see {numref}`ch03_gee_srtm`). One important piece of information is the Image/ImageCollection/FeatureCollection ID of each dataset, which is essential for accessing the dataset through the Earth Engine JavaScript or Python APIs.\n", + "\n", + "![](https://i.imgur.com/B3rf4QN.jpg)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dataset_xyz = ee.Image('USGS/SRTMGL1_003')\n", + "Map.addLayer(dataset_xyz, {}, \"USGS/SRTMGL1_003\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "dem = ee.Image('USGS/SRTMGL1_003')\n", + "vis_params = {\n", + " 'min': 0,\n", + " 'max': 4000,\n", + " 'palette': ['006633', 'E5FFCC', '662A00', 'D8D8D8', 'F5F5F5'],\n", + "}\n", + "Map.addLayer(dem, vis_params, 'SRTM DEM')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Using the datasets module" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from geemap.datasets import DATA" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4)\n", + "dataset = ee.Image(DATA.USGS_GAP_CONUS_2011)\n", + "Map.addLayer(dataset, {}, 'GAP CONUS')\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from geemap.datasets import get_metadata\n", + "\n", + "get_metadata(DATA.USGS_GAP_CONUS_2011)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Converting Earth Engine JavaScripts to Python\n", + "\n", + "Find some Earth Engine JavaScript code that you want to convert to Python. For example, you can grab some sample code from the [Earth Engine Documentation](https://developers.google.com/earth-engine/guides/image_visualization)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Load an image.\n", + "image = ee.Image('LANDSAT/LC08/C02/T1_TOA/LC08_044034_20140318')\n", + "\n", + "# Define the visualization parameters.\n", + "vizParams = {'bands': ['B5', 'B4', 'B3'], 'min': 0, 'max': 0.5, 'gamma': [0.95, 1.1, 1]}\n", + "\n", + "# Center the map and display the image.\n", + "Map.setCenter(-122.1899, 37.5010, 10)\n", + "# San Francisco Bay\n", + "Map.addLayer(image, vizParams, 'False color composite')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Exercise 1 - Creating cloud-free imagery\n", + "\n", + "Create a cloud-free imagery of Texas for the year of 2022. You can use either Landsat 9 or Sentinel-2 imagery. Relevant Earth Engine assets:\n", + "- [ee.FeatureCollection(\"TIGER/2018/States\")](https://developers.google.com/earth-engine/datasets/catalog/TIGER_2018_States)\n", + "- [ee.ImageCollection(\"COPERNICUS/S2_SR\")](https://developers.google.com/earth-engine/datasets/catalog/COPERNICUS_S2_SR)\n", + "- [ee.ImageCollection(\"LANDSAT/LC09/C02/T1_L2\")](https://developers.google.com/earth-engine/datasets/catalog/LANDSAT_LC09_C02_T1_L2)\n", + "\n", + "![](https://i.imgur.com/DNIqGPY.png)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Break 1 (10 mins)\n", + "\n", + "## Visualizing Earth Engine data (30 mins)\n", + "\n", + "### Geemap Inspector tool, plotting tool, interactive GUI for data visualization\n", + "\n", + "### Using the inspector tool" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=(40, -100), zoom=4)\n", + "\n", + "dem = ee.Image('USGS/SRTMGL1_003')\n", + "landsat7 = ee.Image('LANDSAT/LE7_TOA_5YEAR/1999_2003').select(\n", + " ['B1', 'B2', 'B3', 'B4', 'B5', 'B7']\n", + ")\n", + "states = ee.FeatureCollection(\"TIGER/2018/States\")\n", + "\n", + "vis_params = {\n", + " 'min': 0,\n", + " 'max': 4000,\n", + " 'palette': ['006633', 'E5FFCC', '662A00', 'D8D8D8', 'F5F5F5'],\n", + "}\n", + "\n", + "Map.addLayer(dem, vis_params, 'SRTM DEM')\n", + "Map.addLayer(\n", + " landsat7,\n", + " {'bands': ['B4', 'B3', 'B2'], 'min': 20, 'max': 200, 'gamma': 2.0},\n", + " 'Landsat 7',\n", + ")\n", + "Map.addLayer(states, {}, \"US States\")\n", + "Map.add_inspector()\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Using the plotting tool" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4)\n", + "\n", + "landsat7 = ee.Image('LANDSAT/LE7_TOA_5YEAR/1999_2003').select(\n", + " ['B1', 'B2', 'B3', 'B4', 'B5', 'B7']\n", + ")\n", + "\n", + "landsat_vis = {'bands': ['B4', 'B3', 'B2'], 'gamma': 1.4}\n", + "Map.addLayer(landsat7, landsat_vis, \"Landsat\")\n", + "\n", + "hyperion = ee.ImageCollection('EO1/HYPERION').filter(\n", + " ee.Filter.date('2016-01-01', '2017-03-01')\n", + ")\n", + "\n", + "hyperion_vis = {\n", + " 'min': 1000.0,\n", + " 'max': 14000.0,\n", + " 'gamma': 2.5,\n", + "}\n", + "Map.addLayer(hyperion, hyperion_vis, 'Hyperion')\n", + "Map.add_plot_gui()\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map.set_plot_options(add_marker_cluster=True, overlay=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Legends, color bars, and labels\n", + "\n", + "#### Built-in legends" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from geemap.legends import builtin_legends" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for legend in builtin_legends:\n", + " print(legend)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Add NLCD WMS layer and legend to the map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4)\n", + "Map.add_basemap('Google Hybrid')\n", + "Map.add_basemap('NLCD 2019 CONUS Land Cover')\n", + "Map.add_legend(builtin_legend='NLCD', max_width='100px')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Add NLCD Earth Engine layer and legend to the map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4)\n", + "Map.add_basemap('HYBRID')\n", + "\n", + "nlcd = ee.Image('USGS/NLCD_RELEASES/2019_REL/NLCD/2019')\n", + "landcover = nlcd.select('landcover')\n", + "\n", + "Map.addLayer(landcover, {}, 'NLCD Land Cover 2019')\n", + "Map.add_legend(\n", + " title=\"NLCD Land Cover Classification\", builtin_legend='NLCD', height='460px'\n", + ")\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Custom legends" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Add a custom legend by specifying the colors and labels." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(add_google_map=False)\n", + "\n", + "labels = ['One', 'Two', 'Three', 'Four', 'ect']\n", + "\n", + "# colors can be defined using either hex code or RGB (0-255, 0-255, 0-255)\n", + "colors = ['#8DD3C7', '#FFFFB3', '#BEBADA', '#FB8072', '#80B1D3']\n", + "# legend_colors = [(255, 0, 0), (127, 255, 0), (127, 18, 25), (36, 70, 180), (96, 68 123)]\n", + "\n", + "Map.add_legend(labels=labels, colors=colors, position='bottomright')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Add a custom legend by specifying a dictionary of colors and labels." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4)\n", + "Map.add_basemap('Google Hybrid')\n", + "\n", + "legend_dict = {\n", + " '11 Open Water': '466b9f',\n", + " '12 Perennial Ice/Snow': 'd1def8',\n", + " '21 Developed, Open Space': 'dec5c5',\n", + " '22 Developed, Low Intensity': 'd99282',\n", + " '23 Developed, Medium Intensity': 'eb0000',\n", + " '24 Developed High Intensity': 'ab0000',\n", + " '31 Barren Land (Rock/Sand/Clay)': 'b3ac9f',\n", + " '41 Deciduous Forest': '68ab5f',\n", + " '42 Evergreen Forest': '1c5f2c',\n", + " '43 Mixed Forest': 'b5c58f',\n", + " '51 Dwarf Scrub': 'af963c',\n", + " '52 Shrub/Scrub': 'ccb879',\n", + " '71 Grassland/Herbaceous': 'dfdfc2',\n", + " '72 Sedge/Herbaceous': 'd1d182',\n", + " '73 Lichens': 'a3cc51',\n", + " '74 Moss': '82ba9e',\n", + " '81 Pasture/Hay': 'dcd939',\n", + " '82 Cultivated Crops': 'ab6c28',\n", + " '90 Woody Wetlands': 'b8d9eb',\n", + " '95 Emergent Herbaceous Wetlands': '6c9fb8',\n", + "}\n", + "\n", + "nlcd = ee.Image('USGS/NLCD_RELEASES/2019_REL/NLCD/2019')\n", + "landcover = nlcd.select('landcover')\n", + "\n", + "Map.addLayer(landcover, {}, 'NLCD Land Cover 2019')\n", + "Map.add_legend(title=\"NLCD Land Cover Classification\", legend_dict=legend_dict)\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Creating color bars\n", + "\n", + "Add a horizontal color bar." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "\n", + "dem = ee.Image('USGS/SRTMGL1_003')\n", + "vis_params = {\n", + " 'min': 0,\n", + " 'max': 4000,\n", + " 'palette': ['006633', 'E5FFCC', '662A00', 'D8D8D8', 'F5F5F5'],\n", + "}\n", + "\n", + "Map.addLayer(dem, vis_params, 'SRTM DEM')\n", + "Map.add_colorbar(vis_params, label=\"Elevation (m)\", layer_name=\"SRTM DEM\")\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Add a vertical color bar." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map.add_colorbar(\n", + " vis_params, \n", + " label=\"Elevation (m)\", \n", + " layer_name=\"SRTM DEM\", \n", + " orientation=\"vertical\",\n", + " max_width=\"100px\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Split-panel map and linked maps\n", + "\n", + "### Split-panel maps\n", + "\n", + "Create a split map with basemaps." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "Map.split_map(left_layer='Google Terrain', right_layer='OpenTopoMap')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a split map with Earth Engine layers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=(40, -100), zoom=4, height=600)\n", + "\n", + "nlcd_2001 = ee.Image('USGS/NLCD_RELEASES/2019_REL/NLCD/2001').select('landcover')\n", + "nlcd_2019 = ee.Image('USGS/NLCD_RELEASES/2019_REL/NLCD/2019').select('landcover')\n", + "\n", + "left_layer = geemap.ee_tile_layer(nlcd_2001, {}, 'NLCD 2001')\n", + "right_layer = geemap.ee_tile_layer(nlcd_2019, {}, 'NLCD 2019')\n", + "\n", + "Map.split_map(left_layer, right_layer)\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Linked maps\n", + "\n", + "Create a 2x2 linked map for visualizing Sentinel-2 imagery with different band combinations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "image = (\n", + " ee.ImageCollection('COPERNICUS/S2')\n", + " .filterDate('2018-09-01', '2018-09-30')\n", + " .map(lambda img: img.divide(10000))\n", + " .median()\n", + ")\n", + "\n", + "vis_params = [\n", + " {'bands': ['B4', 'B3', 'B2'], 'min': 0, 'max': 0.3, 'gamma': 1.3},\n", + " {'bands': ['B8', 'B11', 'B4'], 'min': 0, 'max': 0.3, 'gamma': 1.3},\n", + " {'bands': ['B8', 'B4', 'B3'], 'min': 0, 'max': 0.3, 'gamma': 1.3},\n", + " {'bands': ['B12', 'B12', 'B4'], 'min': 0, 'max': 0.3, 'gamma': 1.3},\n", + "]\n", + "\n", + "labels = [\n", + " 'Natural Color (B4/B3/B2)',\n", + " 'Land/Water (B8/B11/B4)',\n", + " 'Color Infrared (B8/B4/B3)',\n", + " 'Vegetation (B12/B11/B4)',\n", + "]\n", + "\n", + "geemap.linked_maps(\n", + " rows=2,\n", + " cols=2,\n", + " height=\"300px\",\n", + " center=[38.4151, 21.2712],\n", + " zoom=12,\n", + " ee_objects=[image],\n", + " vis_params=vis_params,\n", + " labels=labels,\n", + " label_position=\"topright\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Timeseries inspector and time slider\n", + "\n", + "#### Timeseries inspector\n", + "\n", + "Check the available years of NLCD." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4)\n", + "collection = ee.ImageCollection('USGS/NLCD_RELEASES/2019_REL/NLCD').select('landcover')\n", + "vis_params = {'bands': ['landcover']}\n", + "years = collection.aggregate_array('system:index').getInfo()\n", + "years" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a timeseries inspector for NLCD." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map.ts_inspector(\n", + " left_ts=collection,\n", + " right_ts=collection,\n", + " left_names=years,\n", + " right_names=years,\n", + " left_vis=vis_params,\n", + " right_vis=vis_params,\n", + " width='80px',\n", + ")\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Time slider\n", + "\n", + "Create a map for visualizing MODIS vegetation data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "\n", + "collection = (\n", + " ee.ImageCollection('MODIS/MCD43A4_006_NDVI')\n", + " .filter(ee.Filter.date('2018-06-01', '2018-07-01'))\n", + " .select(\"NDVI\")\n", + ")\n", + "vis_params = {\n", + " 'min': 0.0,\n", + " 'max': 1.0,\n", + " 'palette': 'ndvi',\n", + "}\n", + "\n", + "Map.add_time_slider(collection, vis_params, time_interval=2)\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a map for visualizing weather data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "\n", + "collection = (\n", + " ee.ImageCollection('NOAA/GFS0P25')\n", + " .filterDate('2018-12-22', '2018-12-23')\n", + " .limit(24)\n", + " .select('temperature_2m_above_ground')\n", + ")\n", + "\n", + "vis_params = {\n", + " 'min': -40.0,\n", + " 'max': 35.0,\n", + " 'palette': ['blue', 'purple', 'cyan', 'green', 'yellow', 'red'],\n", + "}\n", + "\n", + "labels = [str(n).zfill(2) + \":00\" for n in range(0, 24)]\n", + "Map.add_time_slider(collection, vis_params, labels=labels, time_interval=1, opacity=0.8)\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Visualizing Sentinel-2 imagery" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[37.75, -122.45], zoom=12)\n", + "\n", + "collection = (\n", + " ee.ImageCollection('COPERNICUS/S2_SR')\n", + " .filterBounds(ee.Geometry.Point([-122.45, 37.75]))\n", + " .filterMetadata('CLOUDY_PIXEL_PERCENTAGE', 'less_than', 10)\n", + ")\n", + "\n", + "vis_params = {\"min\": 0, \"max\": 4000, \"bands\": [\"B8\", \"B4\", \"B3\"]}\n", + "\n", + "Map.add_time_slider(collection, vis_params)\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Exercise 2 - Creating land cover maps with a legend\n", + "\n", + "Create a split map for visualizing NLCD land cover change in Texas between 2001 and 2019. Add the NLCD legend to the map. Relevant Earth Engine assets:\n", + "- [ee.FeatureCollection(\"TIGER/2018/States\")](https://developers.google.com/earth-engine/datasets/catalog/TIGER_2018_States)\n", + "- [ee.ImageCollection(\"USGS/NLCD_RELEASES/2019_REL/NLCD\")](https://developers.google.com/earth-engine/datasets/catalog/USGS_NLCD_RELEASES_2019_REL_NLCD)\n", + "\n", + "\n", + "![](https://i.imgur.com/LJmztTd.png)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Analyzing Earth Engine data (30 mins)\n", + "\n", + "### Image descriptive statistics\n", + "\n", + "Use a sample Landsat image." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "\n", + "centroid = ee.Geometry.Point([-122.4439, 37.7538])\n", + "image = ee.ImageCollection('LANDSAT/LC08/C01/T1_SR').filterBounds(centroid).first()\n", + "vis = {'min': 0, 'max': 3000, 'bands': ['B5', 'B4', 'B3']}\n", + "\n", + "Map.centerObject(centroid, 8)\n", + "Map.addLayer(image, vis, \"Landsat-8\")\n", + "Map.addLayer(centroid, {}, 'Centroid')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Check image properties." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "image.propertyNames()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Check image property values." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "image.toDictionary()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Get specific image properties." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "image.get('CLOUD_COVER') # 0.05" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Get image properties with easy-to-read time format." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "props = geemap.image_props(image)\n", + "props" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Compute image descriptive statistics." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "stats = geemap.image_stats(image, scale=30)\n", + "stats" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Zonal statistics\n", + "\n", + "#### Zonal statistics\n", + "\n", + "Add Earth Engine data to the map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4)\n", + "\n", + "# Add NASA SRTM\n", + "dem = ee.Image('USGS/SRTMGL1_003')\n", + "dem_vis = {\n", + " 'min': 0,\n", + " 'max': 4000,\n", + " 'palette': ['006633', 'E5FFCC', '662A00', 'D8D8D8', 'F5F5F5'],\n", + "}\n", + "Map.addLayer(dem, dem_vis, 'SRTM DEM')\n", + "\n", + "# Add 5-year Landsat TOA composite\n", + "landsat = ee.Image('LANDSAT/LE7_TOA_5YEAR/1999_2003')\n", + "landsat_vis = {'bands': ['B4', 'B3', 'B2'], 'gamma': 1.4}\n", + "Map.addLayer(landsat, landsat_vis, \"Landsat\", False)\n", + "\n", + "# Add US Census States\n", + "states = ee.FeatureCollection(\"TIGER/2018/States\")\n", + "style = {'fillColor': '00000000'}\n", + "Map.addLayer(states.style(**style), {}, 'US States')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Compute zonal statistics. In this case, we compute the mean elevation of each state and export the results to a CSV file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "out_dem_stats = 'dem_stats.csv'\n", + "geemap.zonal_stats(\n", + " dem, states, out_dem_stats, statistics_type='MEAN', scale=1000, return_fc=False\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Display the csv file as a table." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.csv_to_df(out_dem_stats).sort_values(by=['mean'], ascending=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Compute zonal statistics of mean spectral values of each state." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "out_landsat_stats = 'landsat_stats.csv'\n", + "geemap.zonal_stats(\n", + " landsat,\n", + " states,\n", + " out_landsat_stats,\n", + " statistics_type='MEAN',\n", + " scale=1000,\n", + " return_fc=False,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.csv_to_df(out_landsat_stats)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Zonal statistics by group\n", + "\n", + "Compute zonal statistics by group. In this case, we compute the area of each land cover type in each state and export the results to a CSV file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4)\n", + "\n", + "# Add NLCD data\n", + "dataset = ee.Image('USGS/NLCD_RELEASES/2019_REL/NLCD/2019')\n", + "landcover = dataset.select('landcover')\n", + "Map.addLayer(landcover, {}, 'NLCD 2019')\n", + "\n", + "# Add US census states\n", + "states = ee.FeatureCollection(\"TIGER/2018/States\")\n", + "style = {'fillColor': '00000000'}\n", + "Map.addLayer(states.style(**style), {}, 'US States')\n", + "\n", + "# Add NLCD legend\n", + "Map.add_legend(title='NLCD Land Cover', builtin_legend='NLCD')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Compute zonal statistics by group and convert the area unit from square meters to square kilometers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "nlcd_stats = 'nlcd_stats.csv'\n", + "\n", + "geemap.zonal_stats_by_group(\n", + " landcover,\n", + " states,\n", + " nlcd_stats,\n", + " statistics_type='SUM',\n", + " denominator=1e6,\n", + " decimal_places=2,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.csv_to_df(nlcd_stats)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Calculate the percentage of each land cover type in each state." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "nlcd_stats = 'nlcd_stats_pct.csv'\n", + "\n", + "geemap.zonal_stats_by_group(\n", + " landcover,\n", + " states,\n", + " nlcd_stats,\n", + " statistics_type='PERCENTAGE',\n", + " denominator=1e6,\n", + " decimal_places=2,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Zonal statistics with two images" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.csv_to_df(nlcd_stats)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Zonal statistics by zone\n", + "\n", + "The zonal statistics by zone algorithm is similar to the zonal statistics by group algorithm, but it takes an image as the zone input instead of a feature collection. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4)\n", + "dem = ee.Image('USGS/3DEP/10m')\n", + "vis = {'min': 0, 'max': 4000, 'palette': 'terrain'}\n", + "Map.addLayer(dem, vis, 'DEM')\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "landcover = ee.Image(\"USGS/NLCD_RELEASES/2019_REL/NLCD/2019\").select('landcover')\n", + "Map.addLayer(landcover, {}, 'NLCD 2019')\n", + "Map.add_legend(title='NLCD Land Cover Classification', builtin_legend='NLCD')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Computer the mean elevation of each land cover type." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "stats = geemap.image_stats_by_zone(dem, landcover, reducer='MEAN')\n", + "stats" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "stats.to_csv('mean.csv', index=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Compute the standard deviation of each land cover type." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.image_stats_by_zone(dem, landcover, out_csv=\"std.csv\", reducer='STD')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.csv_to_df('std.csv')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Exercise 3 - Zonal statistics\n", + "\n", + "Find out which state has the highest mean temperature on in the United States on June 28, 2023. Relevant Earth Engine assets:\n", + "\n", + "- [ee.FeatureCollection(\"TIGER/2018/States\")](https://developers.google.com/earth-engine/datasets/catalog/TIGER_2018_States)\n", + "- [ee.ImageCollection(\"NOAA/GFS0P25\")](https://developers.google.com/earth-engine/datasets/catalog/NOAA_GFS0P25)\n", + "\n", + "![](https://i.imgur.com/GZCHHz3.png)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Coordinate grids and fishnets\n", + "\n", + "#### Creating coordinate grids\n", + "\n", + "Create a latitudinal grid with a 5-degree interval." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "lat_grid = geemap.latitude_grid(step=5.0, west=-180, east=180, south=-85, north=85)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "style = {'fillColor': '00000000'}\n", + "Map.addLayer(lat_grid.style(**style), {}, 'Latitude Grid')\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df = geemap.ee_to_df(lat_grid)\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a longitudinal grid with a 5-degree interval." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "lon_grid = geemap.longitude_grid(step=5.0, west=-180, east=180, south=-85, north=85)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "style = {'fillColor': '00000000'}\n", + "Map.addLayer(lon_grid.style(**style), {}, 'Longitude Grid')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a rectangular grid with a 10-degree interval." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "grid = geemap.latlon_grid(\n", + " lat_step=10, lon_step=10, west=-180, east=180, south=-85, north=85\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "style = {'fillColor': '00000000'}\n", + "Map.addLayer(grid.style(**style), {}, 'Coordinate Grid')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Creating fishnets\n", + "\n", + "Create a fishnet based on an Earth Engine geometry." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Use the drawing tools to draw a polygon on the map above. If no polygon is drawn, the default polygon will be used." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "roi = Map.user_roi\n", + "\n", + "if roi is None:\n", + " roi = ee.Geometry.BBox(-112.8089, 33.7306, -88.5951, 46.6244)\n", + " Map.addLayer(roi, {}, 'ROI')\n", + " Map.user_roi = None\n", + "\n", + "Map.centerObject(roi)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a fishnet based on a user-drawn polygon with a 2-degree interval." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fishnet = geemap.fishnet(roi, h_interval=2.0, v_interval=2.0, delta=1)\n", + "style = {'color': 'blue', 'fillColor': '00000000'}\n", + "Map.addLayer(fishnet.style(**style), {}, 'Fishnet')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a new map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Draw a polygon on the map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "roi = Map.user_roi\n", + "\n", + "if roi is None:\n", + " roi = ee.Geometry.Polygon(\n", + " [\n", + " [\n", + " [-64.602356, -1.127399],\n", + " [-68.821106, -12.625598],\n", + " [-60.647278, -22.498601],\n", + " [-47.815247, -21.111406],\n", + " [-43.860168, -8.913564],\n", + " [-54.582825, -0.775886],\n", + " [-60.823059, 0.454555],\n", + " [-64.602356, -1.127399],\n", + " ]\n", + " ]\n", + " )\n", + " Map.addLayer(roi, {}, 'ROI')\n", + "\n", + "Map.centerObject(roi)\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a fishnet based on a user-drawn polygon with specified number of rows and columns." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fishnet = geemap.fishnet(roi, rows=6, cols=8, delta=1)\n", + "style = {'color': 'blue', 'fillColor': '00000000'}\n", + "Map.addLayer(fishnet.style(**style), {}, 'Fishnet')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Land use and land cover change analysis\n", + "\n", + "#### Forest cover mapping\n", + "\n", + "We will use the [Hansen Global Forest Change v1.10 (2000-2022) dataset](https://developers.google.com/earth-engine/datasets/catalog/UMD_hansen_global_forest_change_2022_v1_10)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dataset = ee.Image('UMD/hansen/global_forest_change_2022_v1_10')\n", + "dataset.bandNames()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Select the imagery for 2000." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "first_bands = ['first_b50', 'first_b40', 'first_b30']\n", + "first_image = dataset.select(first_bands)\n", + "Map.addLayer(first_image, {'bands': first_bands, 'gamma': 1.5}, 'Year 2000 Bands 5/4/3')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Select the imagery for 2022." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "last_bands = ['last_b50', 'last_b40', 'last_b30']\n", + "last_image = dataset.select(last_bands)\n", + "Map.addLayer(last_image, {'bands': last_bands, 'gamma': 1.5}, 'Year 2022 Bands 5/4/3')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Select the tree cover imagery for 2000." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "treecover = dataset.select(['treecover2000'])\n", + "treeCoverVisParam = {'min': 0, 'max': 100, 'palette': ['black', 'green']}\n", + "name = 'Tree cover (%)'\n", + "Map.addLayer(treecover, treeCoverVisParam, name)\n", + "Map.add_colorbar(treeCoverVisParam, label=name, layer_name=name)\n", + "Map.add_layer_manager()\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Extract tree cover 2000 by using the threshold of 10%." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "threshold = 10\n", + "treecover_bin = treecover.gte(threshold).selfMask()\n", + "treeVisParam = {'palette': ['green']}\n", + "Map.addLayer(treecover_bin, treeVisParam, 'Tree cover bin')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Forest loss and gain mapping\n", + "\n", + "Visualize forest loss." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "Map.add_basemap('Google Hybrid')\n", + "treeloss_year = dataset.select(['lossyear'])\n", + "treeLossVisParam = {'min': 0, 'max': 22, 'palette': ['yellow', 'red']}\n", + "layer_name = 'Tree loss year since 2000'\n", + "Map.addLayer(treeloss_year, treeLossVisParam, layer_name)\n", + "Map.add_colorbar(treeLossVisParam, label=layer_name, layer_name=layer_name)\n", + "Map.add_layer_manager()\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Compare forest loss and gain." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "Map.add_basemap('Google Hybrid')\n", + "treeloss = dataset.select(['loss']).selfMask()\n", + "treegain = dataset.select(['gain']).selfMask()\n", + "Map.addLayer(treeloss, {'palette': 'red'}, 'Tree loss')\n", + "Map.addLayer(treegain, {'palette': 'yellow'}, 'Tree gain')\n", + "Map.add_layer_manager()\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Zonal statistics by country\n", + "\n", + "Compute zonal statistics to find out which country has the largest forest area in 2000.\n", + "\n", + "Add a country boundary layer to the map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "countries = ee.FeatureCollection(geemap.examples.get_ee_path('countries'))\n", + "style = {'color': '#000000ff', 'fillColor': '#00000000'}\n", + "Map.addLayer(countries.style(**style), {}, 'Countries')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Compute zonal statistics by country." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.zonal_stats(\n", + " treecover_bin,\n", + " countries,\n", + " 'forest_cover.csv',\n", + " statistics_type='SUM',\n", + " denominator=1e6,\n", + " scale=1000,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a pie chart to visualize the forest area by country." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.pie_chart(\n", + " 'forest_cover.csv', names='NAME', values='sum', max_rows=20, height=400\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a bar chart to visualize the forest area by country." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.bar_chart(\n", + " 'forest_cover.csv',\n", + " x='NAME',\n", + " y='sum',\n", + " max_rows=20,\n", + " x_label='Country',\n", + " y_label='Forest area (km2)',\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Calculate the forest loss area by country." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.zonal_stats(\n", + " treeloss.gt(0),\n", + " countries,\n", + " 'treeloss.csv',\n", + " statistics_type='SUM',\n", + " denominator=1e6,\n", + " scale=1000,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a bar chart to visualize the forest loss area by country." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.pie_chart('treeloss.csv', names='NAME', values='sum', max_rows=20, height=600)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a bar chart to visualize the forest loss area by country." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.bar_chart(\n", + " 'treeloss.csv',\n", + " x='NAME',\n", + " y='sum',\n", + " max_rows=20,\n", + " x_label='Country',\n", + " y_label='Forest loss area (km2)',\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Exercise 4 - Analyzing forest cover gain and loss\n", + "\n", + "Find out which US state has the largest forest gain and loss between 2000 and 2022. Create pie charts and bar charts to show the results. Relevant Earth Engine assets:\n", + "\n", + "- [ee.FeatureCollection(\"TIGER/2018/States\")](https://developers.google.com/earth-engine/datasets/catalog/TIGER_2018_States)\n", + "- [ee.Image(\"UMD/hansen/global_forest_change_2022_v1_10\")](https://developers.google.com/earth-engine/datasets/catalog/UMD_hansen_global_forest_change_2022_v1_10)\n", + "\n", + "![](https://i.imgur.com/t5sH5ku.png)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Break 2 (10 mins)\n", + "\n", + "## Exporting Earth Engine data (30 mins)\n", + "\n", + "### Exporting images\n", + "\n", + "Add a Landsat image to the map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "\n", + "image = ee.Image('LANDSAT/LC08/C02/T1_TOA/LC08_044034_20140318').select(\n", + " ['B5', 'B4', 'B3']\n", + ")\n", + "\n", + "vis_params = {'min': 0, 'max': 0.5, 'gamma': [0.95, 1.1, 1]}\n", + "\n", + "Map.centerObject(image)\n", + "Map.addLayer(image, vis_params, 'Landsat')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Add a rectangle to the map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "region = ee.Geometry.BBox(-122.5955, 37.5339, -122.0982, 37.8252)\n", + "fc = ee.FeatureCollection(region)\n", + "style = {'color': 'ffff00ff', 'fillColor': '00000000'}\n", + "Map.addLayer(fc.style(**style), {}, 'ROI')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To local drive" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_export_image(image, filename=\"landsat.tif\", scale=30, region=region)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Check image projection." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "projection = image.select(0).projection().getInfo()\n", + "projection" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "crs = projection['crs']\n", + "crs_transform = projection['transform']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Specify region, crs, and crs_transform." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_export_image(\n", + " image,\n", + " filename=\"landsat_crs.tif\",\n", + " crs=crs,\n", + " crs_transform=crs_transform,\n", + " region=region,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To Google Drive" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_export_image_to_drive(\n", + " image, description='landsat', folder='export', region=region, scale=30\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Exporting image collections" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "point = ee.Geometry.Point(-99.2222, 46.7816)\n", + "collection = (\n", + " ee.ImageCollection('USDA/NAIP/DOQQ')\n", + " .filterBounds(point)\n", + " .filterDate('2008-01-01', '2018-01-01')\n", + " .filter(ee.Filter.listContains(\"system:band_names\", \"N\"))\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "collection.aggregate_array('system:index')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To local drive" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_export_image_collection(collection, out_dir='.', scale=10)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To Google Drive" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_export_image_collection_to_drive(collection, folder='export', scale=10)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Exporting feature collections" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "fc = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017').filter(\n", + " ee.Filter.eq('country_na', 'Germany')\n", + ")\n", + "Map.addLayer(fc, {}, \"Germany\")\n", + "Map.centerObject(fc)\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To local drive" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_to_shp(fc, filename='Germany.shp', selectors=None)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_export_vector(fc, filename='Germany2.shp')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_to_geojson(fc, filename='Germany.geojson')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_to_csv(fc, filename='Germany.csv')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "gdf = geemap.ee_to_gdf(fc)\n", + "gdf" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df = geemap.ee_to_df(fc)\n", + "df" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To Google Drive" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_export_vector_to_drive(\n", + " fc, description=\"germany\", fileFormat='SHP', folder=\"export\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Exercise 5 - Exporting images by a fishnet\n", + "\n", + "Create a fishnet with a 4-degree interval based on the extent of `[-112.5439, 34.0891, -85.0342, 49.6858]`. Use the fishnet to download the Landsat 7 image tiles by the fishnet using the `geemap.download_ee_image_tiles()` and `geemap.download_ee_image_tiles_parallel()` functions. Relevant Earth Engine assets:\n", + "\n", + "- `ee.Image('LANDSAT/LE7_TOA_5YEAR/1999_2003')`\n", + "\n", + "![](https://i.imgur.com/L1IH3fq.png)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4)\n", + "image = ee.Image('LANDSAT/LE7_TOA_5YEAR/1999_2003').select(['B4', 'B3', 'B2'])\n", + "Map.addLayer(image, {'min': 20, 'max': 200, 'gamma': 2.0}, 'Landsat')\n", + "region = ee.Geometry.BBox(-112.5439, 34.0891, -85.0342, 49.6858)\n", + "Map.addLayer(region, {}, 'ROI')\n", + "Map.centerObject(region)\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fishnet = geemap.fishnet(region, h_interval=4.0, v_interval=4.0, delta=0.5)\n", + "style = {'color': 'ffff00ff', 'fillColor': '00000000'}\n", + "Map.addLayer(fishnet.style(**style), {}, 'Fishnet')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.download_ee_image_tiles(\n", + " image, fishnet, out_dir='tiles', scale=1000, crs='EPSG:3857'\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.download_ee_image_tiles_parallel(\n", + " image, fishnet, out_dir='tiles', scale=1000, crs='EPSG:3857'\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "## Creating Satellite timelapse animations (30 mins)\n", + "\n", + "### Creating satellite timeseries\n", + "\n", + "Use the [Harmonized Sentinel-2 MSI](https://developers.google.com/earth-engine/datasets/catalog/COPERNICUS_S2_HARMONIZED) dataset to create a timeseries for a given location. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "collection = ee.ImageCollection(\"COPERNICUS/S2_HARMONIZED\").filterMetadata(\n", + " 'CLOUDY_PIXEL_PERCENTAGE', 'less_than', 10\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Specify the location of interest and date range." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "start_date = '2016-01-01'\n", + "end_date = '2022-12-31'\n", + "region = ee.Geometry.BBox(-122.5549, 37.6968, -122.3446, 37.8111)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create an annual composite." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "images = geemap.create_timeseries(\n", + " collection, start_date, end_date, region, frequency='year', reducer='median'\n", + ")\n", + "images" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Display the timeseries." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "\n", + "vis_params = {\"min\": 0, \"max\": 4000, \"bands\": [\"B8\", \"B4\", \"B3\"]}\n", + "labels = [str(y) for y in range(2016, 2023)]\n", + "\n", + "Map.addLayer(images, vis_params, \"Sentinel-2\", False)\n", + "Map.add_time_slider(images, vis_params, time_interval=2, labels=labels)\n", + "Map.centerObject(region)\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Creating satellite timelapse animations\n", + "\n", + "#### NAIP timelapse" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4)\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "roi = Map.user_roi\n", + "if roi is None:\n", + " roi = ee.Geometry.BBox(-99.1019, 47.1274, -99.0334, 47.1562)\n", + " Map.addLayer(roi, {}, 'ROI')\n", + " Map.centerObject(roi)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "collection = geemap.naip_timeseries(roi, start_year=2009, end_year=2022, RGBN=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "years = geemap.image_dates(collection, date_format='YYYY').getInfo()\n", + "print(years)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "size = len(years)\n", + "images = collection.toList(size)\n", + "for i in range(size):\n", + " image = ee.Image(images.get(i))\n", + " Map.addLayer(image, {'bands': ['N', 'R', 'G']}, years[i])\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "timelapse = geemap.naip_timelapse(\n", + " roi,\n", + " out_gif=\"naip.gif\",\n", + " bands=['N', 'R', 'G'],\n", + " frames_per_second=3,\n", + " title='NAIP Timelapse',\n", + ")\n", + "geemap.show_image(timelapse)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Landsat timelapse" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Pan and zoom the map to an area of interest. Use the drawing tools to draw a rectangle on the map. If no rectangle is drawn, the default rectangle shown below will be used." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "roi = Map.user_roi\n", + "if roi is None:\n", + " roi = ee.Geometry.BBox(-74.7222, -8.5867, -74.1596, -8.2824)\n", + " Map.addLayer(roi)\n", + " Map.centerObject(roi)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "timelapse = geemap.landsat_timelapse(\n", + " roi,\n", + " out_gif='landsat.gif',\n", + " start_year=1984,\n", + " end_year=2022,\n", + " start_date='01-01',\n", + " end_date='12-31',\n", + " bands=['SWIR1', 'NIR', 'Red'],\n", + " frames_per_second=5,\n", + " title='Landsat Timelapse',\n", + " progress_bar_color='blue',\n", + " mp4=True,\n", + ")\n", + "geemap.show_image(timelapse)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "roi = ee.Geometry.BBox(-115.5541, 35.8044, -113.9035, 36.5581)\n", + "Map.addLayer(roi)\n", + "Map.centerObject(roi)\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "timelapse = geemap.landsat_timelapse(\n", + " roi,\n", + " out_gif='las_vegas.gif',\n", + " start_year=1984,\n", + " end_year=2022,\n", + " bands=['NIR', 'Red', 'Green'],\n", + " frames_per_second=5,\n", + " title='Las Vegas, NV',\n", + " font_color='blue',\n", + ")\n", + "geemap.show_image(timelapse)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "roi = ee.Geometry.BBox(113.8252, 22.1988, 114.0851, 22.3497)\n", + "Map.addLayer(roi)\n", + "Map.centerObject(roi)\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "timelapse = geemap.landsat_timelapse(\n", + " roi,\n", + " out_gif='hong_kong.gif',\n", + " start_year=1990,\n", + " end_year=2022,\n", + " start_date='01-01',\n", + " end_date='12-31',\n", + " bands=['SWIR1', 'NIR', 'Red'],\n", + " frames_per_second=3,\n", + " title='Hong Kong',\n", + ")\n", + "geemap.show_image(timelapse)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Sentinel-2 timelapse" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Pan and zoom the map to an area of interest. Use the drawing tools to draw a rectangle on the map. If no rectangle is drawn, the default rectangle shown below will be used." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "roi = Map.user_roi\n", + "if roi is None:\n", + " roi = ee.Geometry.BBox(-74.7222, -8.5867, -74.1596, -8.2824)\n", + " Map.addLayer(roi)\n", + " Map.centerObject(roi)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "timelapse = geemap.sentinel2_timelapse(\n", + " roi,\n", + " out_gif='sentinel2.gif',\n", + " start_year=2016,\n", + " end_year=2021,\n", + " start_date='01-01',\n", + " end_date='12-31',\n", + " frequency='year',\n", + " bands=['SWIR1', 'NIR', 'Red'],\n", + " frames_per_second=3,\n", + " title='Sentinel-2 Timelapse',\n", + ")\n", + "geemap.show_image(timelapse)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### MODIS timelapse\n", + "\n", + "MODIS vegetation indices" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "roi = Map.user_roi\n", + "if roi is None:\n", + " roi = ee.Geometry.BBox(-18.6983, -36.1630, 52.2293, 38.1446)\n", + " Map.addLayer(roi)\n", + " Map.centerObject(roi)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "timelapse = geemap.modis_ndvi_timelapse(\n", + " roi,\n", + " out_gif='ndvi.gif',\n", + " data='Terra',\n", + " band='NDVI',\n", + " start_date='2000-01-01',\n", + " end_date='2022-12-31',\n", + " frames_per_second=3,\n", + " title='MODIS NDVI Timelapse',\n", + " overlay_data='countries',\n", + ")\n", + "geemap.show_image(timelapse)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "MODIS temperature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "roi = Map.user_roi\n", + "if roi is None:\n", + " roi = ee.Geometry.BBox(-171.21, -57.13, 177.53, 79.99)\n", + " Map.addLayer(roi)\n", + " Map.centerObject(roi)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "timelapse = geemap.modis_ocean_color_timelapse(\n", + " satellite='Aqua',\n", + " start_date='2018-01-01',\n", + " end_date='2020-12-31',\n", + " roi=roi,\n", + " frequency='month',\n", + " out_gif='temperature.gif',\n", + " overlay_data='continents',\n", + " overlay_color='yellow',\n", + " overlay_opacity=0.5,\n", + ")\n", + "geemap.show_image(timelapse)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### GOES timelapse" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "roi = ee.Geometry.BBox(167.1898, -28.5757, 202.6258, -12.4411)\n", + "start_date = \"2022-01-15T03:00:00\"\n", + "end_date = \"2022-01-15T07:00:00\"\n", + "data = \"GOES-17\"\n", + "scan = \"full_disk\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "timelapse = geemap.goes_timelapse(\n", + " roi, \"goes.gif\", start_date, end_date, data, scan, framesPerSecond=5\n", + ")\n", + "geemap.show_image(timelapse)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "roi = ee.Geometry.BBox(-159.5954, 24.5178, -114.2438, 60.4088)\n", + "start_date = \"2021-10-24T14:00:00\"\n", + "end_date = \"2021-10-25T01:00:00\"\n", + "data = \"GOES-17\"\n", + "scan = \"full_disk\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "timelapse = geemap.goes_timelapse(\n", + " roi, \"hurricane.gif\", start_date, end_date, data, scan, framesPerSecond=5\n", + ")\n", + "geemap.show_image(timelapse)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "roi = ee.Geometry.BBox(-121.0034, 36.8488, -117.9052, 39.0490)\n", + "start_date = \"2020-09-05T15:00:00\"\n", + "end_date = \"2020-09-06T02:00:00\"\n", + "data = \"GOES-17\"\n", + "scan = \"full_disk\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "timelapse = geemap.goes_fire_timelapse(\n", + " roi, \"fire.gif\", start_date, end_date, data, scan, framesPerSecond=5\n", + ")\n", + "geemap.show_image(timelapse)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Exercise 6 - Creating timelapse animations\n", + "\n", + "Use the geemap timelapse GUI to create a timelapse animation for any location of your choice. Share the timelapse on social media and use the hashtagd such as #EarthEngine and #geemap. See [this](https://i.imgur.com/uFaXD9s.gif) example.\n", + "\n", + "![](https://i.imgur.com/vecPydF.png)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "Map.add_gui(\"timelapse\")\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "\n", + "## Break 3 (10 mins)\n", + "\n", + "## Building interactive web apps (30 mins)\n", + "\n", + "This section is optional. We might not have enough time to cover this section.\n", + "\n", + "Follow the instructions [here](https://huggingface.co/spaces/giswqs/solara-geemap) to build an interactive Earth Engine web app with [Solara](https://github.com/widgetti/solara) and geemap. You need to [sign up](https://huggingface.co/join) for a free Hugging Face account to create the web app. It is free and easy to sign up.\n", + "\n", + "### Exercise 7 - Building an interactive web app for visualizing land cover change\n", + "\n", + "After following the instructions above, you should have a web app that looks like this:\n", + "\n", + "![](https://i.imgur.com/uYDUPl0.png)\n", + "\n", + "The web app URL should look like this: https://giswqs-solara-geemap.hf.space/split-map." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/workshops/G4G_2023.ipynb b/examples/workshops/G4G_2023.ipynb new file mode 100644 index 00000000000..8e33840580d --- /dev/null +++ b/examples/workshops/G4G_2023.ipynb @@ -0,0 +1,3148 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "89a76753", + "metadata": {}, + "source": [ + "[![image](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/gee-community/geemap/blob/master/docs/workshops/G4G_2023.ipynb)\n", + "\n", + "**Introduction to Google Earth Engine in Python**\n", + "\n", + "* Notebook: \n", + "* Earth Engine: \n", + "* Geemap: \n", + "\n", + "## Introduction (10 mins)\n", + "\n", + "This notebook is for the workshop presented at the [Geo for Good Summit 2023](https://earthoutreachonair.withgoogle.com/events/geoforgood23). Check out [this link](https://earthoutreachonair.withgoogle.com/events/geoforgood23?tab=g4gagenda&expand=module:agenda) for more information about the workshop.\n", + "\n", + "### Abstract\n", + "\n", + "This workshop provides an introduction to cloud-based geospatial analysis using the Earth Engine Python API. Attendees will learn the basics of Earth Engine data types and how to visualize, analyze, and export Earth Engine data in a Jupyter environment with geemap. In addition, attendees will learn how to develop and deploy interactive Earth Engine web apps with Python. Through practical examples and hands-on exercises, attendees will enhance their learning experience. During each hands-on session, attendees will walk through Jupyter Notebook examples on Google Colab with the instructors. At the end of each session, they will complete a hands-on exercise to apply the knowledge they have learned.\n", + "\n", + "### Prerequisites\n", + "\n", + "- To use geemap and the Earth Engine Python API, you must [register](https://code.earthengine.google.com/register) for an Earth Engine account and follow the instructions [here](https://docs.google.com/document/d/1ZGSmrNm6_baqd8CHt33kIBWOlvkh-HLr46bODgJN1h0/edit?usp=sharing) to create a Cloud Project. Earth Engine is free for [noncommercial and research use](https://earthengine.google.com/noncommercial). To test whether you can use authenticate the Earth Engine Python API, please run [this notebook](https://colab.research.google.com/github/giswqs/geemap/blob/master/examples/notebooks/geemap_colab.ipynb) on Google Colab.\n", + "\n", + "- It is recommended that attendees have a basic understanding of Python and Jupyter Notebook. \n", + "- Familiarity with the Earth Engine JavaScript API is not required but will be helpful. \n", + "- Attendees can use Google Colab to follow this workshop without installing anything on their computer.\n", + "\n", + "## Introduction to Earth Engine and geemap (15 mins)\n", + "\n", + "Earth Engine is free for [noncommercial and research use](https://earthengine.google.com/noncommercial). For more than a decade, Earth Engine has enabled planetary-scale Earth data science and analysis by nonprofit organizations, research scientists, and other impact users.\n", + "\n", + "With the launch of Earth Engine for [commercial use](https://earthengine.google.com/commercial), commercial customers will be charged for Earth Engine services. However, Earth Engine will remain free of charge for noncommercial use and research projects. Nonprofit organizations, academic institutions, educators, news media, Indigenous governments, and government researchers are eligible to use Earth Engine free of charge, just as they have done for over a decade.\n", + "\n", + "The geemap Python package is built upon the Earth Engine Python API and open-source mapping libraries. It allows Earth Engine users to interactively manipulate, analyze, and visualize geospatial big data in a Jupyter environment. Since its creation in April 2020, geemap has received over 2,800 GitHub stars and is being used by over 1,000 projects on GitHub. \n", + "\n", + "## Google Colab and Earth Engine Python API authentication (5 mins)\n", + "\n", + "[![image](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/gee-community/geemap/blob/master/docs/workshops/G4G_2023.ipynb)\n", + "\n", + "### Install geemap\n", + "\n", + "Uncomment the following line to install geemap if you are running this notebook in Google Colab." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# %pip install geemap[workshop]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import ee\n", + "import geemap" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Authenticate and initialize Earth Engine\n", + "\n", + "You will need to create a [Google Cloud Project](https://console.cloud.google.com/projectcreate) and enable the [Earth Engine API](https://console.cloud.google.com/apis/api/earthengine.googleapis.com) for the project. You can find detailed instructions [here](https://book.geemap.org/chapters/01_introduction.html#earth-engine-authentication)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_initialize()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Creating interactive maps\n", + "\n", + "Let's create an interactive map using the `ipyleaflet` plotting backend. The [`geemap.Map`](https://geemap.org/geemap/#geemap.geemap.Map) class inherits the [`ipyleaflet.Map`](https://ipyleaflet.readthedocs.io/en/latest/map_and_basemaps/map.html) class. Therefore, you can use the same syntax to create an interactive map as you would with `ipyleaflet.Map`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To display it in a Jupyter notebook, simply ask for the object representation:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To customize the map, you can specify various keyword arguments, such as `center` ([lat, lon]), `zoom`, `width`, and `height`. The default `width` is `100%`, which takes up the entire cell width of the Jupyter notebook. The `height` argument accepts a number or a string. If a number is provided, it represents the height of the map in pixels. If a string is provided, the string must be in the format of a number followed by `px`, e.g., `600px`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4, height=600)\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To hide a control, set `control_name` to `False`, e.g., `draw_ctrl=False`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(data_ctrl=False, toolbar_ctrl=False, draw_ctrl=False)\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Adding basemaps\n", + "\n", + "There are several ways to add basemaps to a map. You can specify the basemap to use in the `basemap` keyword argument when creating the map. Alternatively, you can add basemap layers to the map using the `add_basemap` method. Geemap has hundreds of built-in basemaps available that can be easily added to the map with only one line of code.\n", + "\n", + "Create a map by specifying the basemap to use as follows. For example, the `HYBRID` basemap represents the Google Satellite Hybrid basemap." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(basemap='HYBRID')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can add as many basemaps as you like to the map. For example, the following code adds the `OpenTopoMap` basemap to the map above:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map.add_basemap('OpenTopoMap')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Print out the first 10 basemaps:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "basemaps = list(geemap.basemaps.keys())\n", + "len(geemap.basemaps)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "basemaps[:10]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Using Earth Engine data (30 mins)\n", + "\n", + "### Earth Engine data types (Image, ImageCollection, Geometry, Feature, FeatureCollection)\n", + "\n", + "Earth Engine objects are server-side objects rather than client-side objects, which means that they are not stored locally on your computer. Similar to video streaming services (e.g., YouTube, Netflix, and Hulu), which store videos/movies on their servers, Earth Engine data are stored on the Earth Engine servers. We can stream geospatial data from Earth Engine on-the-fly without having to download the data just like we can watch videos from streaming services using a web browser without having to download the entire video to your computer.\n", + "\n", + "- **Image**: the fundamental raster data type in Earth Engine.\n", + "- **ImageCollection**: a stack or time-series of images.\n", + "- **Geometry**: the fundamental vector data type in Earth Engine.\n", + "- **Feature**: a Geometry with attributes.\n", + "- **FeatureCollection**: a set of features.\n", + "\n", + "### Image\n", + "\n", + "Raster data in Earth Engine are represented as **Image** objects. Images are composed of one or more bands and each band has its own name, data type, scale, mask and projection. Each image has metadata stored as a set of properties.\n", + "\n", + "#### Loading Earth Engine images" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "image = ee.Image('USGS/SRTMGL1_003')\n", + "image" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Visualizing Earth Engine images" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[21.79, 70.87], zoom=3)\n", + "image = ee.Image('USGS/SRTMGL1_003')\n", + "vis_params = {\n", + " 'min': 0,\n", + " 'max': 6000,\n", + " 'palette': ['006633', 'E5FFCC', '662A00', 'D8D8D8', 'F5F5F5'],\n", + "}\n", + "Map.addLayer(image, vis_params, 'SRTM')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### ImageCollection\n", + "\n", + "An `ImageCollection` is a stack or sequence of images. An `ImageCollection` can be loaded by passing an Earth Engine asset ID into the `ImageCollection` constructor. You can find `ImageCollection` IDs in the [Earth Engine Data Catalog](https://developers.google.com/earth-engine/datasets).\n", + "\n", + "#### Loading image collections\n", + "\n", + "For example, to load the image collection of the [Sentinel-2 surface reflectance](https://developers.google.com/earth-engine/datasets/catalog/COPERNICUS_S2_SR):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "collection = ee.ImageCollection('COPERNICUS/S2_SR')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Visualizing image collections\n", + "\n", + "To visualize an Earth Engine **ImageCollection**, we need to convert an **ImageCollection** to an **Image** by compositing all the images in the collection to a single image representing, for example, the min, max, median, mean or standard deviation of the images. For example, to create a median value image from a collection, use the `collection.median()` method. Let's create a median image from the Sentinel-2 surface reflectance collection:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "collection = ee.ImageCollection('COPERNICUS/S2_SR')\n", + "image = collection.median()\n", + "\n", + "vis = {\n", + " 'min': 0.0,\n", + " 'max': 3000,\n", + " 'bands': ['B4', 'B3', 'B2'],\n", + "}\n", + "\n", + "Map.setCenter(83.277, 17.7009, 12)\n", + "Map.addLayer(image, vis, 'Sentinel-2')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Filtering image collections" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "collection = (\n", + " ee.ImageCollection('COPERNICUS/S2_SR')\n", + " .filterDate('2021-01-01', '2022-01-01')\n", + " .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 5))\n", + ")\n", + "image = collection.median()\n", + "\n", + "vis = {\n", + " 'min': 0.0,\n", + " 'max': 3000,\n", + " 'bands': ['B4', 'B3', 'B2'],\n", + "}\n", + "\n", + "Map.setCenter(83.277, 17.7009, 12)\n", + "Map.addLayer(image, vis, 'Sentinel-2')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### FeatureCollection\n", + "\n", + "A **FeatureCollection** is a collection of Features. A FeatureCollection is analogous to a GeoJSON FeatureCollection object, i.e., a collection of features with associated properties/attributes. Data contained in a shapefile can be represented as a FeatureCollection.\n", + "\n", + "#### Loading feature collections\n", + "\n", + "The [Earth Engine Data Catalog](https://developers.google.com/earth-engine/datasets) hosts a variety of vector datasets (e.g,, US Census data, country boundaries, and more) as feature collections. You can find feature collection IDs by searching the data catalog. For example, to load the [TIGER roads data](https://developers.google.com/earth-engine/datasets/catalog/TIGER_2016_Roads) by the U.S. Census Bureau:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "fc = ee.FeatureCollection('TIGER/2016/Roads')\n", + "Map.setCenter(-73.9596, 40.7688, 12)\n", + "Map.addLayer(fc, {}, 'Census roads')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Filtering feature collections" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "states = ee.FeatureCollection('TIGER/2018/States')\n", + "fc = states.filter(ee.Filter.eq('NAME', 'Louisiana'))\n", + "Map.addLayer(fc, {}, 'Louisiana')\n", + "Map.centerObject(fc, 7)\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "feat = fc.first()\n", + "feat.toDictionary()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "states = ee.FeatureCollection('TIGER/2018/States')\n", + "fc = states.filter(ee.Filter.inList('NAME', ['California', 'Oregon', 'Washington']))\n", + "Map.addLayer(fc, {}, 'West Coast')\n", + "Map.centerObject(fc, 5)\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "region = Map.user_roi\n", + "if region is None:\n", + " region = ee.Geometry.BBox(-88.40, 29.88, -77.90, 35.39)\n", + "\n", + "fc = ee.FeatureCollection('TIGER/2018/States').filterBounds(region)\n", + "Map.addLayer(fc, {}, 'Southeastern U.S.')\n", + "Map.centerObject(fc, 6)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Visualizing feature collections" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4)\n", + "states = ee.FeatureCollection(\"TIGER/2018/States\")\n", + "Map.addLayer(states, {}, \"US States\")\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4)\n", + "states = ee.FeatureCollection(\"TIGER/2018/States\")\n", + "style = {'color': '0000ffff', 'width': 2, 'lineType': 'solid', 'fillColor': 'FF000080'}\n", + "Map.addLayer(states.style(**style), {}, \"US States\")\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4)\n", + "states = ee.FeatureCollection(\"TIGER/2018/States\")\n", + "vis_params = {\n", + " 'color': '000000',\n", + " 'colorOpacity': 1,\n", + " 'pointSize': 3,\n", + " 'pointShape': 'circle',\n", + " 'width': 2,\n", + " 'lineType': 'solid',\n", + " 'fillColorOpacity': 0.66,\n", + "}\n", + "palette = ['006633', 'E5FFCC', '662A00', 'D8D8D8', 'F5F5F5']\n", + "Map.add_styled_vector(\n", + " states, column=\"NAME\", palette=palette, layer_name=\"Styled vector\", **vis_params\n", + ")\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Earth Engine Data Catalog\n", + "\n", + "The [Earth Engine Data Catalog](https://developers.google.com/earth-engine/datasets) hosts a variety of geospatial datasets. As of March 2023, the catalog contains over [1,000 datasets](https://github.com/samapriya/Earth-Engine-Datasets-List) with a total size of over 80 petabytes. Some notable datasets include: Landsat, Sentinel, MODIS, NAIP, etc. For a complete list of datasets in CSV or JSON formats, see the [Earth Engine Datasets List](https://github.com/giswqs/Earth-Engine-Catalog/blob/master/gee_catalog.tsv).\n", + "\n", + "#### Searching for datasets\n", + "\n", + "The [Earth Engine Data Catalog](https://developers.google.com/earth-engine/datasets/catalog) is searchable. You can search datasets by name, keyword, or tag. For example, enter \"elevation\" in the search box will filter the catalog to show only datasets containing \"elevation\" in their name, description, or tags. 52 datasets are returned for this search query. Scroll down the list to find the [NASA SRTM Digital Elevation 30m](https://developers.google.com/earth-engine/datasets/catalog/USGS_SRTMGL1_003#description) dataset. On each dataset page, you can find the following information, including Dataset Availability, Dataset Provider, Earth Engine Snippet, Tags, Description, Code Example, and more (see {numref}`ch03_gee_srtm`). One important piece of information is the Image/ImageCollection/FeatureCollection ID of each dataset, which is essential for accessing the dataset through the Earth Engine JavaScript or Python APIs.\n", + "\n", + "![](https://i.imgur.com/B3rf4QN.jpg)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dataset_xyz = ee.Image('USGS/SRTMGL1_003')\n", + "Map.addLayer(dataset_xyz, {}, \"USGS/SRTMGL1_003\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "dem = ee.Image('USGS/SRTMGL1_003')\n", + "vis_params = {\n", + " 'min': 0,\n", + " 'max': 4000,\n", + " 'palette': ['006633', 'E5FFCC', '662A00', 'D8D8D8', 'F5F5F5'],\n", + "}\n", + "Map.addLayer(dem, vis_params, 'SRTM DEM')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Using the datasets module" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from geemap.datasets import DATA" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4)\n", + "dataset = ee.Image(DATA.USGS_GAP_CONUS_2011)\n", + "Map.addLayer(dataset, {}, 'GAP CONUS')\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from geemap.datasets import get_metadata\n", + "\n", + "get_metadata(DATA.USGS_GAP_CONUS_2011)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Converting Earth Engine JavaScripts to Python\n", + "\n", + "Find some Earth Engine JavaScript code that you want to convert to Python. For example, you can grab some sample code from the [Earth Engine Documentation](https://developers.google.com/earth-engine/guides/image_visualization)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Load an image.\n", + "image = ee.Image('LANDSAT/LC08/C02/T1_TOA/LC08_044034_20140318')\n", + "\n", + "# Define the visualization parameters.\n", + "vizParams = {'bands': ['B5', 'B4', 'B3'], 'min': 0, 'max': 0.5, 'gamma': [0.95, 1.1, 1]}\n", + "\n", + "# Center the map and display the image.\n", + "Map.setCenter(-122.1899, 37.5010, 10)\n", + "# San Francisco Bay\n", + "Map.addLayer(image, vizParams, 'False color composite')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Exercise 1 - Creating cloud-free imagery\n", + "\n", + "Create a cloud-free imagery of Texas for the year of 2022. You can use either Landsat 9 or Sentinel-2 imagery. Relevant Earth Engine assets:\n", + "- [ee.FeatureCollection(\"TIGER/2018/States\")](https://developers.google.com/earth-engine/datasets/catalog/TIGER_2018_States)\n", + "- [ee.ImageCollection(\"COPERNICUS/S2_SR\")](https://developers.google.com/earth-engine/datasets/catalog/COPERNICUS_S2_SR)\n", + "- [ee.ImageCollection(\"LANDSAT/LC09/C02/T1_L2\")](https://developers.google.com/earth-engine/datasets/catalog/LANDSAT_LC09_C02_T1_L2)\n", + "\n", + "![](https://i.imgur.com/DNIqGPY.png)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Break 1 (10 mins)\n", + "\n", + "## Visualizing Earth Engine data (30 mins)\n", + "\n", + "### Geemap Inspector tool, plotting tool, interactive GUI for data visualization\n", + "\n", + "### Using the inspector tool" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=(40, -100), zoom=4)\n", + "\n", + "dem = ee.Image('USGS/SRTMGL1_003')\n", + "landsat7 = ee.Image('LANDSAT/LE7_TOA_5YEAR/1999_2003').select(\n", + " ['B1', 'B2', 'B3', 'B4', 'B5', 'B7']\n", + ")\n", + "states = ee.FeatureCollection(\"TIGER/2018/States\")\n", + "\n", + "vis_params = {\n", + " 'min': 0,\n", + " 'max': 4000,\n", + " 'palette': ['006633', 'E5FFCC', '662A00', 'D8D8D8', 'F5F5F5'],\n", + "}\n", + "\n", + "Map.addLayer(dem, vis_params, 'SRTM DEM')\n", + "Map.addLayer(\n", + " landsat7,\n", + " {'bands': ['B4', 'B3', 'B2'], 'min': 20, 'max': 200, 'gamma': 2.0},\n", + " 'Landsat 7',\n", + ")\n", + "Map.addLayer(states, {}, \"US States\")\n", + "Map.add_inspector()\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Using the plotting tool" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4)\n", + "\n", + "landsat7 = ee.Image('LANDSAT/LE7_TOA_5YEAR/1999_2003').select(\n", + " ['B1', 'B2', 'B3', 'B4', 'B5', 'B7']\n", + ")\n", + "\n", + "landsat_vis = {'bands': ['B4', 'B3', 'B2'], 'gamma': 1.4}\n", + "Map.addLayer(landsat7, landsat_vis, \"Landsat\")\n", + "\n", + "hyperion = ee.ImageCollection('EO1/HYPERION').filter(\n", + " ee.Filter.date('2016-01-01', '2017-03-01')\n", + ")\n", + "\n", + "hyperion_vis = {\n", + " 'min': 1000.0,\n", + " 'max': 14000.0,\n", + " 'gamma': 2.5,\n", + "}\n", + "Map.addLayer(hyperion, hyperion_vis, 'Hyperion')\n", + "Map.add_plot_gui()\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map.set_plot_options(add_marker_cluster=True, overlay=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Legends, color bars, and labels\n", + "\n", + "#### Built-in legends" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from geemap.legends import builtin_legends" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for legend in builtin_legends:\n", + " print(legend)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Add NLCD WMS layer and legend to the map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4)\n", + "Map.add_basemap('Google Hybrid')\n", + "Map.add_basemap('NLCD 2019 CONUS Land Cover')\n", + "Map.add_legend(builtin_legend='NLCD', max_width='100px')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Add NLCD Earth Engine layer and legend to the map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4)\n", + "Map.add_basemap('HYBRID')\n", + "\n", + "nlcd = ee.Image('USGS/NLCD_RELEASES/2019_REL/NLCD/2019')\n", + "landcover = nlcd.select('landcover')\n", + "\n", + "Map.addLayer(landcover, {}, 'NLCD Land Cover 2019')\n", + "Map.add_legend(\n", + " title=\"NLCD Land Cover Classification\", builtin_legend='NLCD', height='460px'\n", + ")\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Custom legends" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Add a custom legend by specifying the colors and labels." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(add_google_map=False)\n", + "\n", + "labels = ['One', 'Two', 'Three', 'Four', 'ect']\n", + "\n", + "# colors can be defined using either hex code or RGB (0-255, 0-255, 0-255)\n", + "colors = ['#8DD3C7', '#FFFFB3', '#BEBADA', '#FB8072', '#80B1D3']\n", + "# legend_colors = [(255, 0, 0), (127, 255, 0), (127, 18, 25), (36, 70, 180), (96, 68 123)]\n", + "\n", + "Map.add_legend(labels=labels, colors=colors, position='bottomright')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Add a custom legend by specifying a dictionary of colors and labels." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4)\n", + "Map.add_basemap('Google Hybrid')\n", + "\n", + "legend_dict = {\n", + " '11 Open Water': '466b9f',\n", + " '12 Perennial Ice/Snow': 'd1def8',\n", + " '21 Developed, Open Space': 'dec5c5',\n", + " '22 Developed, Low Intensity': 'd99282',\n", + " '23 Developed, Medium Intensity': 'eb0000',\n", + " '24 Developed High Intensity': 'ab0000',\n", + " '31 Barren Land (Rock/Sand/Clay)': 'b3ac9f',\n", + " '41 Deciduous Forest': '68ab5f',\n", + " '42 Evergreen Forest': '1c5f2c',\n", + " '43 Mixed Forest': 'b5c58f',\n", + " '51 Dwarf Scrub': 'af963c',\n", + " '52 Shrub/Scrub': 'ccb879',\n", + " '71 Grassland/Herbaceous': 'dfdfc2',\n", + " '72 Sedge/Herbaceous': 'd1d182',\n", + " '73 Lichens': 'a3cc51',\n", + " '74 Moss': '82ba9e',\n", + " '81 Pasture/Hay': 'dcd939',\n", + " '82 Cultivated Crops': 'ab6c28',\n", + " '90 Woody Wetlands': 'b8d9eb',\n", + " '95 Emergent Herbaceous Wetlands': '6c9fb8',\n", + "}\n", + "\n", + "nlcd = ee.Image('USGS/NLCD_RELEASES/2019_REL/NLCD/2019')\n", + "landcover = nlcd.select('landcover')\n", + "\n", + "Map.addLayer(landcover, {}, 'NLCD Land Cover 2019')\n", + "Map.add_legend(title=\"NLCD Land Cover Classification\", legend_dict=legend_dict)\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Creating color bars\n", + "\n", + "Add a horizontal color bar." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "\n", + "dem = ee.Image('USGS/SRTMGL1_003')\n", + "vis_params = {\n", + " 'min': 0,\n", + " 'max': 4000,\n", + " 'palette': ['006633', 'E5FFCC', '662A00', 'D8D8D8', 'F5F5F5'],\n", + "}\n", + "\n", + "Map.addLayer(dem, vis_params, 'SRTM DEM')\n", + "Map.add_colorbar(vis_params, label=\"Elevation (m)\", layer_name=\"SRTM DEM\")\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Add a vertical color bar." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map.add_colorbar(\n", + " vis_params, \n", + " label=\"Elevation (m)\", \n", + " layer_name=\"SRTM DEM\", \n", + " orientation=\"vertical\",\n", + " max_width=\"100px\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Split-panel map and linked maps\n", + "\n", + "### Split-panel maps\n", + "\n", + "Create a split map with basemaps." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "Map.split_map(left_layer='Google Terrain', right_layer='OpenTopoMap')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a split map with Earth Engine layers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=(40, -100), zoom=4, height=600)\n", + "\n", + "nlcd_2001 = ee.Image('USGS/NLCD_RELEASES/2019_REL/NLCD/2001').select('landcover')\n", + "nlcd_2019 = ee.Image('USGS/NLCD_RELEASES/2019_REL/NLCD/2019').select('landcover')\n", + "\n", + "left_layer = geemap.ee_tile_layer(nlcd_2001, {}, 'NLCD 2001')\n", + "right_layer = geemap.ee_tile_layer(nlcd_2019, {}, 'NLCD 2019')\n", + "\n", + "Map.split_map(left_layer, right_layer)\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Linked maps\n", + "\n", + "Create a 2x2 linked map for visualizing Sentinel-2 imagery with different band combinations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "image = (\n", + " ee.ImageCollection('COPERNICUS/S2')\n", + " .filterDate('2018-09-01', '2018-09-30')\n", + " .map(lambda img: img.divide(10000))\n", + " .median()\n", + ")\n", + "\n", + "vis_params = [\n", + " {'bands': ['B4', 'B3', 'B2'], 'min': 0, 'max': 0.3, 'gamma': 1.3},\n", + " {'bands': ['B8', 'B11', 'B4'], 'min': 0, 'max': 0.3, 'gamma': 1.3},\n", + " {'bands': ['B8', 'B4', 'B3'], 'min': 0, 'max': 0.3, 'gamma': 1.3},\n", + " {'bands': ['B12', 'B12', 'B4'], 'min': 0, 'max': 0.3, 'gamma': 1.3},\n", + "]\n", + "\n", + "labels = [\n", + " 'Natural Color (B4/B3/B2)',\n", + " 'Land/Water (B8/B11/B4)',\n", + " 'Color Infrared (B8/B4/B3)',\n", + " 'Vegetation (B12/B11/B4)',\n", + "]\n", + "\n", + "geemap.linked_maps(\n", + " rows=2,\n", + " cols=2,\n", + " height=\"300px\",\n", + " center=[38.4151, 21.2712],\n", + " zoom=12,\n", + " ee_objects=[image],\n", + " vis_params=vis_params,\n", + " labels=labels,\n", + " label_position=\"topright\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Timeseries inspector and time slider\n", + "\n", + "#### Timeseries inspector\n", + "\n", + "Check the available years of NLCD." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4)\n", + "collection = ee.ImageCollection('USGS/NLCD_RELEASES/2019_REL/NLCD').select('landcover')\n", + "vis_params = {'bands': ['landcover']}\n", + "years = collection.aggregate_array('system:index').getInfo()\n", + "years" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a timeseries inspector for NLCD." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map.ts_inspector(\n", + " left_ts=collection,\n", + " right_ts=collection,\n", + " left_names=years,\n", + " right_names=years,\n", + " left_vis=vis_params,\n", + " right_vis=vis_params,\n", + " width='80px',\n", + ")\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Time slider\n", + "\n", + "Create a map for visualizing MODIS vegetation data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "\n", + "collection = (\n", + " ee.ImageCollection('MODIS/MCD43A4_006_NDVI')\n", + " .filter(ee.Filter.date('2018-06-01', '2018-07-01'))\n", + " .select(\"NDVI\")\n", + ")\n", + "vis_params = {\n", + " 'min': 0.0,\n", + " 'max': 1.0,\n", + " 'palette': 'ndvi',\n", + "}\n", + "\n", + "Map.add_time_slider(collection, vis_params, time_interval=2)\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a map for visualizing weather data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "\n", + "collection = (\n", + " ee.ImageCollection('NOAA/GFS0P25')\n", + " .filterDate('2018-12-22', '2018-12-23')\n", + " .limit(24)\n", + " .select('temperature_2m_above_ground')\n", + ")\n", + "\n", + "vis_params = {\n", + " 'min': -40.0,\n", + " 'max': 35.0,\n", + " 'palette': ['blue', 'purple', 'cyan', 'green', 'yellow', 'red'],\n", + "}\n", + "\n", + "labels = [str(n).zfill(2) + \":00\" for n in range(0, 24)]\n", + "Map.add_time_slider(collection, vis_params, labels=labels, time_interval=1, opacity=0.8)\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Visualizing Sentinel-2 imagery" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[37.75, -122.45], zoom=12)\n", + "\n", + "collection = (\n", + " ee.ImageCollection('COPERNICUS/S2_SR')\n", + " .filterBounds(ee.Geometry.Point([-122.45, 37.75]))\n", + " .filterMetadata('CLOUDY_PIXEL_PERCENTAGE', 'less_than', 10)\n", + ")\n", + "\n", + "vis_params = {\"min\": 0, \"max\": 4000, \"bands\": [\"B8\", \"B4\", \"B3\"]}\n", + "\n", + "Map.add_time_slider(collection, vis_params)\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Exercise 2 - Creating land cover maps with a legend\n", + "\n", + "Create a split map for visualizing NLCD land cover change in Texas between 2001 and 2019. Add the NLCD legend to the map. Relevant Earth Engine assets:\n", + "- [ee.FeatureCollection(\"TIGER/2018/States\")](https://developers.google.com/earth-engine/datasets/catalog/TIGER_2018_States)\n", + "- [ee.ImageCollection(\"USGS/NLCD_RELEASES/2019_REL/NLCD\")](https://developers.google.com/earth-engine/datasets/catalog/USGS_NLCD_RELEASES_2019_REL_NLCD)\n", + "\n", + "\n", + "![](https://i.imgur.com/LJmztTd.png)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Analyzing Earth Engine data (30 mins)\n", + "\n", + "### Image descriptive statistics\n", + "\n", + "Use a sample Landsat image." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "\n", + "centroid = ee.Geometry.Point([-122.4439, 37.7538])\n", + "image = ee.ImageCollection('LANDSAT/LC08/C01/T1_SR').filterBounds(centroid).first()\n", + "vis = {'min': 0, 'max': 3000, 'bands': ['B5', 'B4', 'B3']}\n", + "\n", + "Map.centerObject(centroid, 8)\n", + "Map.addLayer(image, vis, \"Landsat-8\")\n", + "Map.addLayer(centroid, {}, 'Centroid')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Check image properties." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "image.propertyNames()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Check image property values." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "image.toDictionary()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Get specific image properties." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "image.get('CLOUD_COVER') # 0.05" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Get image properties with easy-to-read time format." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "props = geemap.image_props(image)\n", + "props" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Compute image descriptive statistics." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "stats = geemap.image_stats(image, scale=30)\n", + "stats" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Zonal statistics\n", + "\n", + "#### Zonal statistics\n", + "\n", + "Add Earth Engine data to the map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4)\n", + "\n", + "# Add NASA SRTM\n", + "dem = ee.Image('USGS/SRTMGL1_003')\n", + "dem_vis = {\n", + " 'min': 0,\n", + " 'max': 4000,\n", + " 'palette': ['006633', 'E5FFCC', '662A00', 'D8D8D8', 'F5F5F5'],\n", + "}\n", + "Map.addLayer(dem, dem_vis, 'SRTM DEM')\n", + "\n", + "# Add 5-year Landsat TOA composite\n", + "landsat = ee.Image('LANDSAT/LE7_TOA_5YEAR/1999_2003')\n", + "landsat_vis = {'bands': ['B4', 'B3', 'B2'], 'gamma': 1.4}\n", + "Map.addLayer(landsat, landsat_vis, \"Landsat\", False)\n", + "\n", + "# Add US Census States\n", + "states = ee.FeatureCollection(\"TIGER/2018/States\")\n", + "style = {'fillColor': '00000000'}\n", + "Map.addLayer(states.style(**style), {}, 'US States')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Compute zonal statistics. In this case, we compute the mean elevation of each state and export the results to a CSV file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "out_dem_stats = 'dem_stats.csv'\n", + "geemap.zonal_stats(\n", + " dem, states, out_dem_stats, statistics_type='MEAN', scale=1000, return_fc=False\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Display the csv file as a table." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.csv_to_df(out_dem_stats).sort_values(by=['mean'], ascending=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Compute zonal statistics of mean spectral values of each state." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "out_landsat_stats = 'landsat_stats.csv'\n", + "geemap.zonal_stats(\n", + " landsat,\n", + " states,\n", + " out_landsat_stats,\n", + " statistics_type='MEAN',\n", + " scale=1000,\n", + " return_fc=False,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.csv_to_df(out_landsat_stats)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Zonal statistics by group\n", + "\n", + "Compute zonal statistics by group. In this case, we compute the area of each land cover type in each state and export the results to a CSV file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4)\n", + "\n", + "# Add NLCD data\n", + "dataset = ee.Image('USGS/NLCD_RELEASES/2019_REL/NLCD/2019')\n", + "landcover = dataset.select('landcover')\n", + "Map.addLayer(landcover, {}, 'NLCD 2019')\n", + "\n", + "# Add US census states\n", + "states = ee.FeatureCollection(\"TIGER/2018/States\")\n", + "style = {'fillColor': '00000000'}\n", + "Map.addLayer(states.style(**style), {}, 'US States')\n", + "\n", + "# Add NLCD legend\n", + "Map.add_legend(title='NLCD Land Cover', builtin_legend='NLCD')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Compute zonal statistics by group and convert the area unit from square meters to square kilometers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "nlcd_stats = 'nlcd_stats.csv'\n", + "\n", + "geemap.zonal_stats_by_group(\n", + " landcover,\n", + " states,\n", + " nlcd_stats,\n", + " statistics_type='SUM',\n", + " denominator=1e6,\n", + " decimal_places=2,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.csv_to_df(nlcd_stats)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Calculate the percentage of each land cover type in each state." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "nlcd_stats = 'nlcd_stats_pct.csv'\n", + "\n", + "geemap.zonal_stats_by_group(\n", + " landcover,\n", + " states,\n", + " nlcd_stats,\n", + " statistics_type='PERCENTAGE',\n", + " denominator=1e6,\n", + " decimal_places=2,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Zonal statistics with two images" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.csv_to_df(nlcd_stats)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Zonal statistics by zone\n", + "\n", + "The zonal statistics by zone algorithm is similar to the zonal statistics by group algorithm, but it takes an image as the zone input instead of a feature collection. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4)\n", + "dem = ee.Image('USGS/3DEP/10m')\n", + "vis = {'min': 0, 'max': 4000, 'palette': 'terrain'}\n", + "Map.addLayer(dem, vis, 'DEM')\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "landcover = ee.Image(\"USGS/NLCD_RELEASES/2019_REL/NLCD/2019\").select('landcover')\n", + "Map.addLayer(landcover, {}, 'NLCD 2019')\n", + "Map.add_legend(title='NLCD Land Cover Classification', builtin_legend='NLCD')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Computer the mean elevation of each land cover type." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "stats = geemap.image_stats_by_zone(dem, landcover, reducer='MEAN')\n", + "stats" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "stats.to_csv('mean.csv', index=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Compute the standard deviation of each land cover type." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.image_stats_by_zone(dem, landcover, out_csv=\"std.csv\", reducer='STD')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.csv_to_df('std.csv')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Exercise 3 - Zonal statistics\n", + "\n", + "Find out which state has the highest mean temperature on in the United States on June 28, 2023. Relevant Earth Engine assets:\n", + "\n", + "- [ee.FeatureCollection(\"TIGER/2018/States\")](https://developers.google.com/earth-engine/datasets/catalog/TIGER_2018_States)\n", + "- [ee.ImageCollection(\"NOAA/GFS0P25\")](https://developers.google.com/earth-engine/datasets/catalog/NOAA_GFS0P25)\n", + "\n", + "![](https://i.imgur.com/GZCHHz3.png)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Coordinate grids and fishnets\n", + "\n", + "#### Creating coordinate grids\n", + "\n", + "Create a latitudinal grid with a 5-degree interval." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "lat_grid = geemap.latitude_grid(step=5.0, west=-180, east=180, south=-85, north=85)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "style = {'fillColor': '00000000'}\n", + "Map.addLayer(lat_grid.style(**style), {}, 'Latitude Grid')\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df = geemap.ee_to_df(lat_grid)\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a longitudinal grid with a 5-degree interval." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "lon_grid = geemap.longitude_grid(step=5.0, west=-180, east=180, south=-85, north=85)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "style = {'fillColor': '00000000'}\n", + "Map.addLayer(lon_grid.style(**style), {}, 'Longitude Grid')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a rectangular grid with a 10-degree interval." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "grid = geemap.latlon_grid(\n", + " lat_step=10, lon_step=10, west=-180, east=180, south=-85, north=85\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "style = {'fillColor': '00000000'}\n", + "Map.addLayer(grid.style(**style), {}, 'Coordinate Grid')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Creating fishnets\n", + "\n", + "Create a fishnet based on an Earth Engine geometry." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Use the drawing tools to draw a polygon on the map above. If no polygon is drawn, the default polygon will be used." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "roi = Map.user_roi\n", + "\n", + "if roi is None:\n", + " roi = ee.Geometry.BBox(-112.8089, 33.7306, -88.5951, 46.6244)\n", + " Map.addLayer(roi, {}, 'ROI')\n", + " Map.user_roi = None\n", + "\n", + "Map.centerObject(roi)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a fishnet based on a user-drawn polygon with a 2-degree interval." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fishnet = geemap.fishnet(roi, h_interval=2.0, v_interval=2.0, delta=1)\n", + "style = {'color': 'blue', 'fillColor': '00000000'}\n", + "Map.addLayer(fishnet.style(**style), {}, 'Fishnet')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a new map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Draw a polygon on the map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "roi = Map.user_roi\n", + "\n", + "if roi is None:\n", + " roi = ee.Geometry.Polygon(\n", + " [\n", + " [\n", + " [-64.602356, -1.127399],\n", + " [-68.821106, -12.625598],\n", + " [-60.647278, -22.498601],\n", + " [-47.815247, -21.111406],\n", + " [-43.860168, -8.913564],\n", + " [-54.582825, -0.775886],\n", + " [-60.823059, 0.454555],\n", + " [-64.602356, -1.127399],\n", + " ]\n", + " ]\n", + " )\n", + " Map.addLayer(roi, {}, 'ROI')\n", + "\n", + "Map.centerObject(roi)\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a fishnet based on a user-drawn polygon with specified number of rows and columns." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fishnet = geemap.fishnet(roi, rows=6, cols=8, delta=1)\n", + "style = {'color': 'blue', 'fillColor': '00000000'}\n", + "Map.addLayer(fishnet.style(**style), {}, 'Fishnet')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Land use and land cover change analysis\n", + "\n", + "#### Forest cover mapping\n", + "\n", + "We will use the [Hansen Global Forest Change v1.10 (2000-2022) dataset](https://developers.google.com/earth-engine/datasets/catalog/UMD_hansen_global_forest_change_2022_v1_10)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dataset = ee.Image('UMD/hansen/global_forest_change_2022_v1_10')\n", + "dataset.bandNames()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Select the imagery for 2000." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "first_bands = ['first_b50', 'first_b40', 'first_b30']\n", + "first_image = dataset.select(first_bands)\n", + "Map.addLayer(first_image, {'bands': first_bands, 'gamma': 1.5}, 'Year 2000 Bands 5/4/3')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Select the imagery for 2022." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "last_bands = ['last_b50', 'last_b40', 'last_b30']\n", + "last_image = dataset.select(last_bands)\n", + "Map.addLayer(last_image, {'bands': last_bands, 'gamma': 1.5}, 'Year 2022 Bands 5/4/3')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Select the tree cover imagery for 2000." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "treecover = dataset.select(['treecover2000'])\n", + "treeCoverVisParam = {'min': 0, 'max': 100, 'palette': ['black', 'green']}\n", + "name = 'Tree cover (%)'\n", + "Map.addLayer(treecover, treeCoverVisParam, name)\n", + "Map.add_colorbar(treeCoverVisParam, label=name, layer_name=name)\n", + "Map.add_layer_manager()\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Extract tree cover 2000 by using the threshold of 10%." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "threshold = 10\n", + "treecover_bin = treecover.gte(threshold).selfMask()\n", + "treeVisParam = {'palette': ['green']}\n", + "Map.addLayer(treecover_bin, treeVisParam, 'Tree cover bin')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Forest loss and gain mapping\n", + "\n", + "Visualize forest loss." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "Map.add_basemap('Google Hybrid')\n", + "treeloss_year = dataset.select(['lossyear'])\n", + "treeLossVisParam = {'min': 0, 'max': 22, 'palette': ['yellow', 'red']}\n", + "layer_name = 'Tree loss year since 2000'\n", + "Map.addLayer(treeloss_year, treeLossVisParam, layer_name)\n", + "Map.add_colorbar(treeLossVisParam, label=layer_name, layer_name=layer_name)\n", + "Map.add_layer_manager()\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Compare forest loss and gain." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "Map.add_basemap('Google Hybrid')\n", + "treeloss = dataset.select(['loss']).selfMask()\n", + "treegain = dataset.select(['gain']).selfMask()\n", + "Map.addLayer(treeloss, {'palette': 'red'}, 'Tree loss')\n", + "Map.addLayer(treegain, {'palette': 'yellow'}, 'Tree gain')\n", + "Map.add_layer_manager()\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Zonal statistics by country\n", + "\n", + "Compute zonal statistics to find out which country has the largest forest area in 2000.\n", + "\n", + "Add a country boundary layer to the map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "countries = ee.FeatureCollection(geemap.examples.get_ee_path('countries'))\n", + "style = {'color': '#000000ff', 'fillColor': '#00000000'}\n", + "Map.addLayer(countries.style(**style), {}, 'Countries')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Compute zonal statistics by country." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.zonal_stats(\n", + " treecover_bin,\n", + " countries,\n", + " 'forest_cover.csv',\n", + " statistics_type='SUM',\n", + " denominator=1e6,\n", + " scale=1000,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a pie chart to visualize the forest area by country." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.pie_chart(\n", + " 'forest_cover.csv', names='NAME', values='sum', max_rows=20, height=400\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a bar chart to visualize the forest area by country." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.bar_chart(\n", + " 'forest_cover.csv',\n", + " x='NAME',\n", + " y='sum',\n", + " max_rows=20,\n", + " x_label='Country',\n", + " y_label='Forest area (km2)',\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Calculate the forest loss area by country." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.zonal_stats(\n", + " treeloss.gt(0),\n", + " countries,\n", + " 'treeloss.csv',\n", + " statistics_type='SUM',\n", + " denominator=1e6,\n", + " scale=1000,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a bar chart to visualize the forest loss area by country." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.pie_chart('treeloss.csv', names='NAME', values='sum', max_rows=20, height=600)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a bar chart to visualize the forest loss area by country." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.bar_chart(\n", + " 'treeloss.csv',\n", + " x='NAME',\n", + " y='sum',\n", + " max_rows=20,\n", + " x_label='Country',\n", + " y_label='Forest loss area (km2)',\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Exercise 4 - Analyzing forest cover gain and loss\n", + "\n", + "Find out which US state has the largest forest gain and loss between 2000 and 2022. Create pie charts and bar charts to show the results. Relevant Earth Engine assets:\n", + "\n", + "- [ee.FeatureCollection(\"TIGER/2018/States\")](https://developers.google.com/earth-engine/datasets/catalog/TIGER_2018_States)\n", + "- [ee.Image(\"UMD/hansen/global_forest_change_2022_v1_10\")](https://developers.google.com/earth-engine/datasets/catalog/UMD_hansen_global_forest_change_2022_v1_10)\n", + "\n", + "![](https://i.imgur.com/t5sH5ku.png)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Break 2 (10 mins)\n", + "\n", + "## Exporting Earth Engine data (30 mins)\n", + "\n", + "### Exporting images\n", + "\n", + "Add a Landsat image to the map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "\n", + "image = ee.Image('LANDSAT/LC08/C02/T1_TOA/LC08_044034_20140318').select(\n", + " ['B5', 'B4', 'B3']\n", + ")\n", + "\n", + "vis_params = {'min': 0, 'max': 0.5, 'gamma': [0.95, 1.1, 1]}\n", + "\n", + "Map.centerObject(image)\n", + "Map.addLayer(image, vis_params, 'Landsat')\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Add a rectangle to the map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "region = ee.Geometry.BBox(-122.5955, 37.5339, -122.0982, 37.8252)\n", + "fc = ee.FeatureCollection(region)\n", + "style = {'color': 'ffff00ff', 'fillColor': '00000000'}\n", + "Map.addLayer(fc.style(**style), {}, 'ROI')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To local drive" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_export_image(image, filename=\"landsat.tif\", scale=30, region=region)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Check image projection." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "projection = image.select(0).projection().getInfo()\n", + "projection" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "crs = projection['crs']\n", + "crs_transform = projection['transform']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Specify region, crs, and crs_transform." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_export_image(\n", + " image,\n", + " filename=\"landsat_crs.tif\",\n", + " crs=crs,\n", + " crs_transform=crs_transform,\n", + " region=region,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To Google Drive" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_export_image_to_drive(\n", + " image, description='landsat', folder='export', region=region, scale=30\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Exporting image collections" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "point = ee.Geometry.Point(-99.2222, 46.7816)\n", + "collection = (\n", + " ee.ImageCollection('USDA/NAIP/DOQQ')\n", + " .filterBounds(point)\n", + " .filterDate('2008-01-01', '2018-01-01')\n", + " .filter(ee.Filter.listContains(\"system:band_names\", \"N\"))\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "collection.aggregate_array('system:index')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To local drive" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_export_image_collection(collection, out_dir='.', scale=10)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To Google Drive" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_export_image_collection_to_drive(collection, folder='export', scale=10)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Exporting feature collections" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "fc = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017').filter(\n", + " ee.Filter.eq('country_na', 'Germany')\n", + ")\n", + "Map.addLayer(fc, {}, \"Germany\")\n", + "Map.centerObject(fc)\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To local drive" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_to_shp(fc, filename='Germany.shp', selectors=None)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_export_vector(fc, filename='Germany2.shp')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_to_geojson(fc, filename='Germany.geojson')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_to_csv(fc, filename='Germany.csv')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "gdf = geemap.ee_to_gdf(fc)\n", + "gdf" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df = geemap.ee_to_df(fc)\n", + "df" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To Google Drive" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.ee_export_vector_to_drive(\n", + " fc, description=\"germany\", fileFormat='SHP', folder=\"export\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Exercise 5 - Exporting images by a fishnet\n", + "\n", + "Create a fishnet with a 4-degree interval based on the extent of `[-112.5439, 34.0891, -85.0342, 49.6858]`. Use the fishnet to download the Landsat 7 image tiles by the fishnet using the `geemap.download_ee_image_tiles()` and `geemap.download_ee_image_tiles_parallel()` functions. Relevant Earth Engine assets:\n", + "\n", + "- `ee.Image('LANDSAT/LE7_TOA_5YEAR/1999_2003')`\n", + "\n", + "![](https://i.imgur.com/L1IH3fq.png)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4)\n", + "image = ee.Image('LANDSAT/LE7_TOA_5YEAR/1999_2003').select(['B4', 'B3', 'B2'])\n", + "Map.addLayer(image, {'min': 20, 'max': 200, 'gamma': 2.0}, 'Landsat')\n", + "region = ee.Geometry.BBox(-112.5439, 34.0891, -85.0342, 49.6858)\n", + "Map.addLayer(region, {}, 'ROI')\n", + "Map.centerObject(region)\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fishnet = geemap.fishnet(region, h_interval=4.0, v_interval=4.0, delta=0.5)\n", + "style = {'color': 'ffff00ff', 'fillColor': '00000000'}\n", + "Map.addLayer(fishnet.style(**style), {}, 'Fishnet')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.download_ee_image_tiles(\n", + " image, fishnet, out_dir='tiles', scale=1000, crs='EPSG:3857'\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geemap.download_ee_image_tiles_parallel(\n", + " image, fishnet, out_dir='tiles', scale=1000, crs='EPSG:3857'\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "## Creating Satellite timelapse animations (30 mins)\n", + "\n", + "### Creating satellite timeseries\n", + "\n", + "Use the [Harmonized Sentinel-2 MSI](https://developers.google.com/earth-engine/datasets/catalog/COPERNICUS_S2_HARMONIZED) dataset to create a timeseries for a given location. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "collection = ee.ImageCollection(\"COPERNICUS/S2_HARMONIZED\").filterMetadata(\n", + " 'CLOUDY_PIXEL_PERCENTAGE', 'less_than', 10\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Specify the location of interest and date range." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "start_date = '2016-01-01'\n", + "end_date = '2022-12-31'\n", + "region = ee.Geometry.BBox(-122.5549, 37.6968, -122.3446, 37.8111)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create an annual composite." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "images = geemap.create_timeseries(\n", + " collection, start_date, end_date, region, frequency='year', reducer='median'\n", + ")\n", + "images" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Display the timeseries." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "\n", + "vis_params = {\"min\": 0, \"max\": 4000, \"bands\": [\"B8\", \"B4\", \"B3\"]}\n", + "labels = [str(y) for y in range(2016, 2023)]\n", + "\n", + "Map.addLayer(images, vis_params, \"Sentinel-2\", False)\n", + "Map.add_time_slider(images, vis_params, time_interval=2, labels=labels)\n", + "Map.centerObject(region)\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Creating satellite timelapse animations\n", + "\n", + "#### NAIP timelapse" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map(center=[40, -100], zoom=4)\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "roi = Map.user_roi\n", + "if roi is None:\n", + " roi = ee.Geometry.BBox(-99.1019, 47.1274, -99.0334, 47.1562)\n", + " Map.addLayer(roi, {}, 'ROI')\n", + " Map.centerObject(roi)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "collection = geemap.naip_timeseries(roi, start_year=2009, end_year=2022, RGBN=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "years = geemap.image_dates(collection, date_format='YYYY').getInfo()\n", + "print(years)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "size = len(years)\n", + "images = collection.toList(size)\n", + "for i in range(size):\n", + " image = ee.Image(images.get(i))\n", + " Map.addLayer(image, {'bands': ['N', 'R', 'G']}, years[i])\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "timelapse = geemap.naip_timelapse(\n", + " roi,\n", + " out_gif=\"naip.gif\",\n", + " bands=['N', 'R', 'G'],\n", + " frames_per_second=3,\n", + " title='NAIP Timelapse',\n", + ")\n", + "geemap.show_image(timelapse)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Landsat timelapse" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Pan and zoom the map to an area of interest. Use the drawing tools to draw a rectangle on the map. If no rectangle is drawn, the default rectangle shown below will be used." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "roi = Map.user_roi\n", + "if roi is None:\n", + " roi = ee.Geometry.BBox(-74.7222, -8.5867, -74.1596, -8.2824)\n", + " Map.addLayer(roi)\n", + " Map.centerObject(roi)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "timelapse = geemap.landsat_timelapse(\n", + " roi,\n", + " out_gif='landsat.gif',\n", + " start_year=1984,\n", + " end_year=2022,\n", + " start_date='01-01',\n", + " end_date='12-31',\n", + " bands=['SWIR1', 'NIR', 'Red'],\n", + " frames_per_second=5,\n", + " title='Landsat Timelapse',\n", + " progress_bar_color='blue',\n", + " mp4=True,\n", + ")\n", + "geemap.show_image(timelapse)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "roi = ee.Geometry.BBox(-115.5541, 35.8044, -113.9035, 36.5581)\n", + "Map.addLayer(roi)\n", + "Map.centerObject(roi)\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "timelapse = geemap.landsat_timelapse(\n", + " roi,\n", + " out_gif='las_vegas.gif',\n", + " start_year=1984,\n", + " end_year=2022,\n", + " bands=['NIR', 'Red', 'Green'],\n", + " frames_per_second=5,\n", + " title='Las Vegas, NV',\n", + " font_color='blue',\n", + ")\n", + "geemap.show_image(timelapse)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "roi = ee.Geometry.BBox(113.8252, 22.1988, 114.0851, 22.3497)\n", + "Map.addLayer(roi)\n", + "Map.centerObject(roi)\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "timelapse = geemap.landsat_timelapse(\n", + " roi,\n", + " out_gif='hong_kong.gif',\n", + " start_year=1990,\n", + " end_year=2022,\n", + " start_date='01-01',\n", + " end_date='12-31',\n", + " bands=['SWIR1', 'NIR', 'Red'],\n", + " frames_per_second=3,\n", + " title='Hong Kong',\n", + ")\n", + "geemap.show_image(timelapse)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Sentinel-2 timelapse" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Pan and zoom the map to an area of interest. Use the drawing tools to draw a rectangle on the map. If no rectangle is drawn, the default rectangle shown below will be used." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "roi = Map.user_roi\n", + "if roi is None:\n", + " roi = ee.Geometry.BBox(-74.7222, -8.5867, -74.1596, -8.2824)\n", + " Map.addLayer(roi)\n", + " Map.centerObject(roi)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "timelapse = geemap.sentinel2_timelapse(\n", + " roi,\n", + " out_gif='sentinel2.gif',\n", + " start_year=2016,\n", + " end_year=2021,\n", + " start_date='01-01',\n", + " end_date='12-31',\n", + " frequency='year',\n", + " bands=['SWIR1', 'NIR', 'Red'],\n", + " frames_per_second=3,\n", + " title='Sentinel-2 Timelapse',\n", + ")\n", + "geemap.show_image(timelapse)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### MODIS timelapse\n", + "\n", + "MODIS vegetation indices" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "roi = Map.user_roi\n", + "if roi is None:\n", + " roi = ee.Geometry.BBox(-18.6983, -36.1630, 52.2293, 38.1446)\n", + " Map.addLayer(roi)\n", + " Map.centerObject(roi)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "timelapse = geemap.modis_ndvi_timelapse(\n", + " roi,\n", + " out_gif='ndvi.gif',\n", + " data='Terra',\n", + " band='NDVI',\n", + " start_date='2000-01-01',\n", + " end_date='2022-12-31',\n", + " frames_per_second=3,\n", + " title='MODIS NDVI Timelapse',\n", + " overlay_data='countries',\n", + ")\n", + "geemap.show_image(timelapse)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "MODIS temperature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "roi = Map.user_roi\n", + "if roi is None:\n", + " roi = ee.Geometry.BBox(-171.21, -57.13, 177.53, 79.99)\n", + " Map.addLayer(roi)\n", + " Map.centerObject(roi)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "timelapse = geemap.modis_ocean_color_timelapse(\n", + " satellite='Aqua',\n", + " start_date='2018-01-01',\n", + " end_date='2020-12-31',\n", + " roi=roi,\n", + " frequency='month',\n", + " out_gif='temperature.gif',\n", + " overlay_data='continents',\n", + " overlay_color='yellow',\n", + " overlay_opacity=0.5,\n", + ")\n", + "geemap.show_image(timelapse)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### GOES timelapse" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "roi = ee.Geometry.BBox(167.1898, -28.5757, 202.6258, -12.4411)\n", + "start_date = \"2022-01-15T03:00:00\"\n", + "end_date = \"2022-01-15T07:00:00\"\n", + "data = \"GOES-17\"\n", + "scan = \"full_disk\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "timelapse = geemap.goes_timelapse(\n", + " roi, \"goes.gif\", start_date, end_date, data, scan, framesPerSecond=5\n", + ")\n", + "geemap.show_image(timelapse)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "roi = ee.Geometry.BBox(-159.5954, 24.5178, -114.2438, 60.4088)\n", + "start_date = \"2021-10-24T14:00:00\"\n", + "end_date = \"2021-10-25T01:00:00\"\n", + "data = \"GOES-17\"\n", + "scan = \"full_disk\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "timelapse = geemap.goes_timelapse(\n", + " roi, \"hurricane.gif\", start_date, end_date, data, scan, framesPerSecond=5\n", + ")\n", + "geemap.show_image(timelapse)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "roi = ee.Geometry.BBox(-121.0034, 36.8488, -117.9052, 39.0490)\n", + "start_date = \"2020-09-05T15:00:00\"\n", + "end_date = \"2020-09-06T02:00:00\"\n", + "data = \"GOES-17\"\n", + "scan = \"full_disk\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "timelapse = geemap.goes_fire_timelapse(\n", + " roi, \"fire.gif\", start_date, end_date, data, scan, framesPerSecond=5\n", + ")\n", + "geemap.show_image(timelapse)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Exercise 6 - Creating timelapse animations\n", + "\n", + "Use the geemap timelapse GUI to create a timelapse animation for any location of your choice. Share the timelapse on social media and use the hashtagd such as #EarthEngine and #geemap. See [this](https://i.imgur.com/uFaXD9s.gif) example.\n", + "\n", + "![](https://i.imgur.com/vecPydF.png)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "Map.add_gui(\"timelapse\")\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "\n", + "## Break 3 (10 mins)\n", + "\n", + "## Building interactive web apps (30 mins)\n", + "\n", + "This section is optional. We might not have enough time to cover this section.\n", + "\n", + "Follow the instructions [here](https://huggingface.co/spaces/giswqs/solara-geemap) to build an interactive Earth Engine web app with [Solara](https://github.com/widgetti/solara) and geemap. You need to [sign up](https://huggingface.co/join) for a free Hugging Face account to create the web app. It is free and easy to sign up.\n", + "\n", + "### Exercise 7 - Building an interactive web app for visualizing land cover change\n", + "\n", + "After following the instructions above, you should have a web app that looks like this:\n", + "\n", + "![](https://i.imgur.com/uYDUPl0.png)\n", + "\n", + "The web app URL should look like this: https://giswqs-solara-geemap.hf.space/split-map." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/mkdocs.yml b/mkdocs.yml index b72e1d4e980..fd1cab42e70 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -133,6 +133,7 @@ nav: - workshops/GEE_Workshop_2022_Part2.ipynb - workshops/AmericaView_2023.ipynb - workshops/SciPy_2023.ipynb + - workshops/G4G_2023.ipynb - Notebooks: - notebooks/00_geemap_key_features.ipynb - notebooks/01_geemap_intro.ipynb From 4b13f9963c36e6557d4b8f7ceafd9cdc7e15dee5 Mon Sep 17 00:00:00 2001 From: Qiusheng Wu Date: Sat, 26 Aug 2023 08:21:24 -0400 Subject: [PATCH 4/4] Added NLCD 2021 basemap (#1680) --- geemap/basemaps.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/geemap/basemaps.py b/geemap/basemaps.py index 317d5feb22c..f5a89894787 100644 --- a/geemap/basemaps.py +++ b/geemap/basemaps.py @@ -78,6 +78,14 @@ "format": "image/png", "transparent": True, }, + "NLCD 2021 CONUS Land Cover": { + "url": "https://www.mrlc.gov/geoserver/mrlc_display/NLCD_2021_Land_Cover_L48/wms?", + "layers": "NLCD_2021_Land_Cover_L48", + "name": "NLCD 2021 CONUS Land Cover", + "attribution": "MRLC", + "format": "image/png", + "transparent": True, + }, "NLCD 2019 CONUS Land Cover": { "url": "https://www.mrlc.gov/geoserver/mrlc_display/NLCD_2019_Land_Cover_L48/wms?", "layers": "NLCD_2019_Land_Cover_L48",