diff --git a/docs/notebooks/101_nasa_opera.ipynb b/docs/notebooks/101_nasa_opera.ipynb new file mode 100644 index 0000000000..d703539faf --- /dev/null +++ b/docs/notebooks/101_nasa_opera.ipynb @@ -0,0 +1,187 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[![image](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://demo.leafmap.org/lab/index.html?path=notebooks/101_nasa_opera.ipynb)\n", + "[![image](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/opengeos/leafmap/blob/master/docs/notebooks/101_nasa_opera.ipynb)\n", + "[![image](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/opengeos/leafmap/HEAD)\n", + "\n", + "**Searching and Visualizing NASA OPERA Data Products Interactively**\n", + "\n", + "\n", + "Started in April 2021, the Observational Products for End-Users from Remote Sensing Analysis ([OPERA](https://www.jpl.nasa.gov/go/opera)) project at the Jet Propulsion Laboratory collects data from satellite radar and optical instruments to generate six product suites:\n", + "\n", + "* a near-global Surface Water Extent product suite\n", + "* a near-global Surface Disturbance product suite\n", + "* a near-global Radiometric Terrain Corrected product\n", + "* a North America Coregistered Single Look complex product suite\n", + "* a North America Displacement product suite\n", + "* a North America Vertical Land Motion product suite\n", + "\n", + "This notebook demonstrates how to search and visualize NASA OPERA data products interactively using the `leafmap` Python package." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# %pip install -U \"leafmap[raster]\" earthaccess" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import leafmap" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To download and access the data, you will need to create an Earthdata login. You can register for an account at [urs.earthdata.nasa.gov](https://urs.earthdata.nasa.gov)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "leafmap.nasa_data_login()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m = leafmap.Map(center=[36.1711, -114.6581], zoom=10, height=\"700px\")\n", + "m.add_basemap(\"Satellite\")\n", + "m.add(\"NASA_OPERA\")\n", + "m" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Pan and zoom to your area of interest. Select a dataset from the Short Name dropdown list. Click the \"Search\" button to load the available datasets for the region. The footprints of the datasets will be displayed on the map. Click on a footprint to display the metadata of the dataset. \n", + "\n", + "![image](https://github.com/user-attachments/assets/f3b1b42e-d83d-4f40-b9e6-67082d364367)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The footprints of the datasets can be accessed as a GeoPandas GeoDataFrame:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m._NASA_DATA_GDF.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Select a dataset from the Dataset dropdown list. Then, select a layer from the Layer dropdown list. Choose a appropriate colormap, then click on the \"Display\" button to display the selected layer on the map." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The water classification layer:\n", + "\n", + "![image](https://github.com/user-attachments/assets/f9800913-c683-4a9f-a3b5-164c911a4486)\n", + "\n", + "The DEM layer:\n", + "\n", + "![image](https://github.com/user-attachments/assets/15690881-d259-474f-9c77-eb6c40027ccc)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The selected layer added to the map can be accessed as a xarray Dataset:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m._NASA_DATA_DS" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To save the displayed layer as a GeoTIFF file:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m._NASA_DATA_DS[\"band_data\"].rio.to_raster(\"data/DSWx.tif\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To download all the available datasets for the region:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "leafmap.nasa_data_download(m._NASA_DATA_RESULTS[:1], out_dir=\"data\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "geo", + "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.12.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/tutorials.md b/docs/tutorials.md index f0be2aa040..5cc9d692fe 100644 --- a/docs/tutorials.md +++ b/docs/tutorials.md @@ -112,6 +112,7 @@ 98. Retrieving watershed boundaries from the National Hydrography Dataset (NHD) ([notebook](https://leafmap.org/notebooks/98_watershed)) 99. Retrieving wetland boundaries from the National Wetlands Inventory (NWI) ([notebook](https://leafmap.org/notebooks/99_wetlands)) 100. Visualizing the National Land Cover Database (NLCD) data products with Leafmap ([notebook](https://leafmap.org/notebooks/100_nlcd)) +101. Searching and Visualizing NASA OPERA Data Products Interactively ([notebook](https://leafmap.org/notebooks/101_nasa_opera)) ## Demo diff --git a/examples/README.md b/examples/README.md index 2b6ec245c0..163c383637 100644 --- a/examples/README.md +++ b/examples/README.md @@ -119,6 +119,7 @@ 98. Retrieving watershed boundaries from the National Hydrography Dataset (NHD) ([notebook](https://leafmap.org/notebooks/98_watershed)) 99. Retrieving wetland boundaries from the National Wetlands Inventory (NWI) ([notebook](https://leafmap.org/notebooks/99_wetlands)) 100. Visualizing the National Land Cover Database (NLCD) data products with Leafmap ([notebook](https://leafmap.org/notebooks/100_nlcd)) +101. Searching and Visualizing NASA OPERA Data Products Interactively ([notebook](https://leafmap.org/notebooks/101_nasa_opera)) ## Demo diff --git a/leafmap/leafmap.py b/leafmap/leafmap.py index 9eac06d4b1..140d5ee196 100644 --- a/leafmap/leafmap.py +++ b/leafmap/leafmap.py @@ -233,6 +233,10 @@ def add(self, obj, index=None, **kwargs) -> None: from .toolbar import nasa_data_gui nasa_data_gui(self, **kwargs) + elif obj == "NASA_OPERA": + from .toolbar import nasa_opera_gui + + nasa_opera_gui(self, **kwargs) elif obj == "inspector": from .toolbar import inspector_gui diff --git a/leafmap/toolbar.py b/leafmap/toolbar.py index bbc2f906f5..7c32015897 100644 --- a/leafmap/toolbar.py +++ b/leafmap/toolbar.py @@ -6615,3 +6615,431 @@ def button_clicked(change): m.tool_control = toolbar_control else: return toolbar_widget + + +def nasa_opera_gui( + m, + position: Optional[str] = "topright", + opened: Optional[bool] = True, + default_dataset: Optional[str] = "OPERA_L3_DSWX-HLS_V1", + **kwargs, +): + """Search NASA Earth data interactive + + Args: + m (leafmap.Map, optional): The leaflet Map object. Defaults to None. + position (str, optional): The position of the widget. Defaults to "topright". + opened (bool, optional): Whether to open the widget. Defaults to True. + default_dataset (str, optional): The default dataset. Defaults to "OPERA_L3_DSWX-HLS_V1". + + Returns: + ipywidgets: The tool GUI widget. + """ + import pandas as pd + from datetime import datetime + import boto3 + import rasterio as rio + from rasterio.session import AWSSession + import xarray as xr + import matplotlib.pyplot as plt + + widget_width = "400px" + padding = "0px 0px 0px 5px" # upper, right, bottom, left + style = {"description_width": "initial"} + + colormaps = plt.colormaps() + cmap_options = [cmap for cmap in colormaps if (len(cmap) < 20 and cmap.islower())] + cmap_options.sort() + + if not hasattr(m, "_NASA_DATA"): + + data = { + "ShortName": [ + "OPERA_L2_CSLC-S1-STATIC_V1", + "OPERA_L2_CSLC-S1_V1", + "OPERA_L2_RTC-S1-STATIC_V1", + "OPERA_L2_RTC-S1_V1", + "OPERA_L3_DIST-ALERT-HLS_V1", + "OPERA_L3_DIST-ANN-HLS_V1", + "OPERA_L3_DSWX-HLS_V1", + "OPERA_L3_DSWX-S1_V1", + ], + "EntryTitle": [ + "OPERA Coregistered Single-Look Complex from Sentinel-1 Static Layers validated product (Version 1)", + "OPERA Coregistered Single-Look Complex from Sentinel-1 validated product (Version 1)", + "OPERA Radiometric Terrain Corrected SAR Backscatter from Sentinel-1 Static Layers validated product (Version 1)", + "OPERA Radiometric Terrain Corrected SAR Backscatter from Sentinel-1 validated product (Version 1)", + "OPERA Land Surface Disturbance Alert from Harmonized Landsat Sentinel-2 product (Version 1)", + "OPERA Land Surface Disturbance Annual from Harmonized Landsat Sentinel-2 product (Version 1)", + "OPERA Dynamic Surface Water Extent from Harmonized Landsat Sentinel-2 product (Version 1)", + "OPERA Dynamic Surface Water Extent from Sentinel-1 (Version 1)", + ], + } + + df = pd.DataFrame(data) + setattr(m, "_NASA_DATA", df) + names = df["ShortName"].tolist() + setattr(m, "_NASA_DATA_NAMES", names) + + # Generates the temporary + s3_cred_endpoint = "https://archive.podaac.earthdata.nasa.gov/s3credentials" + + def get_temp_creds(): + temp_creds_url = s3_cred_endpoint + return requests.get(temp_creds_url).json() + + temp_creds_req = get_temp_creds() + + session = boto3.Session( + aws_access_key_id=temp_creds_req["accessKeyId"], + aws_secret_access_key=temp_creds_req["secretAccessKey"], + aws_session_token=temp_creds_req["sessionToken"], + region_name="us-west-2", + ) + + rio_env = rio.Env( + AWSSession(session), + GDAL_DISABLE_READDIR_ON_OPEN="EMPTY_DIR", + CPL_VSIL_CURL_ALLOWED_EXTENSIONS="TIF, TIFF", + GDAL_HTTP_COOKIEFILE=os.path.expanduser("~/cookies.txt"), + GDAL_HTTP_COOKIEJAR=os.path.expanduser("~/cookies.txt"), + ) + rio_env.__enter__() + + default_title = m._NASA_DATA[m._NASA_DATA["ShortName"] == default_dataset][ + "EntryTitle" + ].values[0] + + output = widgets.Output( + layout=widgets.Layout(width=widget_width, padding=padding, overflow="auto") + ) + + toolbar_button = widgets.ToggleButton( + value=False, + tooltip="Search NASA Earth data", + icon="search", + 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"), + ) + + short_name = widgets.Dropdown( + options=m._NASA_DATA_NAMES, + value=default_dataset, + description="Short Name:", + style=style, + layout=widgets.Layout(width=widget_width, padding=padding), + ) + + title = widgets.Text( + value=default_title, + description="Title:", + style=style, + disabled=True, + layout=widgets.Layout(width=widget_width, padding=padding), + ) + + max_items = widgets.IntText( + value=50, + description="Max items:", + style=style, + layout=widgets.Layout(width="125px", padding=padding), + ) + + bbox = widgets.Text( + value="", + description="Bounding box:", + placeholder="xmin, ymin, xmax, ymax", + style=style, + layout=widgets.Layout(width="271px", padding=padding), + ) + + start_date = widgets.DatePicker( + description="Start date:", + disabled=False, + style=style, + layout=widgets.Layout(width="198px", padding=padding), + ) + end_date = widgets.DatePicker( + description="End date:", + disabled=False, + style=style, + layout=widgets.Layout(width="198px", padding=padding), + ) + + dataset = widgets.Dropdown( + value=None, + description="Dataset:", + style=style, + layout=widgets.Layout(width=widget_width, padding=padding), + ) + + layer = widgets.Dropdown( + value=None, + description="Layer:", + style=style, + layout=widgets.Layout(width="200px", padding=padding), + ) + + palette = widgets.Dropdown( + options=cmap_options, + value="tab10", + description="Colormap:", + style=style, + layout=widgets.Layout(width="200px", padding=padding), + ) + + buttons = widgets.ToggleButtons( + value=None, + options=["Search", "Display", "Reset", "Close"], + tooltips=["Get Items", "Display Image", "Reset", "Close"], + button_style="primary", + ) + buttons.style.button_width = "65px" + + def change_dataset(change): + title.value = m._NASA_DATA[m._NASA_DATA["ShortName"] == short_name.value][ + "EntryTitle" + ].values[0] + dataset.value = None + dataset.options = [] + layer.value = None + layer.options = [] + + short_name.observe(change_dataset, "value") + + toolbar_widget = widgets.VBox() + toolbar_widget.children = [toolbar_button] + toolbar_header = widgets.HBox() + toolbar_header.children = [close_button, toolbar_button] + toolbar_footer = widgets.VBox() + toolbar_footer.children = [ + short_name, + title, + widgets.HBox([max_items, bbox]), + widgets.HBox([start_date, end_date]), + dataset, + widgets.HBox([layer, palette]), + buttons, + output, + ] + + toolbar_event = ipyevents.Event( + source=toolbar_widget, watched_events=["mouseenter", "mouseleave"] + ) + + def handle_toolbar_event(event): + if event["type"] == "mouseenter": + toolbar_widget.children = [toolbar_header, toolbar_footer] + elif event["type"] == "mouseleave": + if not toolbar_button.value: + toolbar_widget.children = [toolbar_button] + toolbar_button.value = False + close_button.value = False + + toolbar_event.on_dom_event(handle_toolbar_event) + + 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 = [toolbar_button] + + toolbar_button.observe(toolbar_btn_click, "value") + + def close_btn_click(change): + if change["new"]: + toolbar_button.value = False + if m is not None: + m.toolbar_reset() + if m.tool_control is not None and m.tool_control in m.controls: + m.remove_control(m.tool_control) + m.tool_control = None + toolbar_widget.close() + + close_button.observe(close_btn_click, "value") + + def button_clicked(change): + if change["new"] == "Search": + with output: + output.clear_output() + with output: + print("Searching...") + + if bbox.value.strip() == "": + if m.user_roi_bounds() is not None: + bounds = tuple(m.user_roi_bounds()) + else: + bounds = ( + m.bounds[0][1], + m.bounds[0][0], + m.bounds[1][1], + m.bounds[1][0], + ) + else: + bounds = tuple(map(float, bbox.value.split(","))) + if len(bounds) != 4: + print("Please provide a valid bounding box.") + bounds = None + + if start_date.value is not None and end_date.value is not None: + date_range = (str(start_date.value), str(end_date.value)) + elif start_date.value is not None: + date_range = ( + str(start_date.value), + datetime.today().strftime("%Y-%m-%d"), + ) + else: + date_range = None + + output.clear_output(wait=True) + try: + results, gdf = nasa_data_search( + count=max_items.value, + short_name=short_name.value, + bbox=bounds, + temporal=date_range, + return_gdf=True, + ) + + if len(results) > 0: + if "Footprints" in m.get_layer_names(): + m.remove(m.find_layer("Footprints")) + if ( + hasattr(m, "_NASA_DATA_CTRL") + and m._NASA_DATA_CTRL in m.controls + ): + m.remove(m._NASA_DATA_CTRL) + + style = { + # "stroke": True, + "color": "#3388ff", + "weight": 2, + "opacity": 1, + "fill": True, + "fillColor": "#3388ff", + "fillOpacity": 0.1, + } + + hover_style = { + "weight": style["weight"] + 2, + "fillOpacity": 0, + "color": "yellow", + } + + m.add_gdf( + gdf, + layer_name="Footprints", + info_mode="on_click", + zoom_to_layer=False, + style=style, + hover_style=hover_style, + ) + setattr(m, "_NASA_DATA_CTRL", m.controls[-1]) + + dataset.options = gdf["native-id"].values.tolist() + dataset.value = dataset.options[0] + + setattr(m, "_NASA_DATA_GDF", gdf) + setattr(m, "_NASA_DATA_RESULTS", results) + + if len(m._NASA_DATA_RESULTS) > 0: + links = m._NASA_DATA_RESULTS[0].data_links() + layer.options = [link.split("_")[-1] for link in links] + layer.value = layer.options[0] + else: + layer.options = [] + layer.value = None + + output.clear_output() + + except Exception as e: + print(e) + + elif change["new"] == "Display": + output.clear_output() + with output: + print("Loading...") + links = m._NASA_DATA_RESULTS[ + dataset.options.index(dataset.value) + ].data_links() + link = links[layer.index] + try: + if link.endswith(".tif"): + ds = xr.open_dataset(link, engine="rasterio") + setattr(m, "_NASA_DATA_DS", ds) + da = ds["band_data"] + nodata = os.environ.get("NODATA", 0) + da = da.fillna(nodata) + image = array_to_image(da) + setattr(m, "_NASA_DATA_IMAGE", image) + name_prefix = dataset.value.split("_")[4][:8] + name_suffix = layer.value.split(".")[0] + layer_name = f"{name_prefix}_{name_suffix}" + m.add_raster( + image, + zoom_to_layer=True, + colormap=palette.value, + nodata=nodata, + layer_name=layer_name, + ) + output.clear_output() + else: + output.clear_output() + print("Only GeoTIFF files are supported.") + except Exception as e: + output.clear_output() + print(e) + + elif change["new"] == "Reset": + short_name.options = m._NASA_DATA_NAMES + short_name.value = default_dataset + title.value = default_title + max_items.value = 50 + bbox.value = "" + bbox.placeholder = "xmin, ymin, xmax, ymax" + start_date.value = None + end_date.value = None + dataset.options = [] + dataset.value = None + layer.options = [] + layer.value = None + palette.value = "tab10" + output.clear_output() + + if "Footprints" in m.get_layer_names(): + m.remove(m.find_layer("Footprints")) + if hasattr(m, "_NASA_DATA_CTRL") and m._NASA_DATA_CTRL in m.controls: + m.remove(m._NASA_DATA_CTRL) + + elif change["new"] == "Close": + if m is not None: + m.toolbar_reset() + if m.tool_control is not None and m.tool_control in m.controls: + m.remove_control(m.tool_control) + m.tool_control = None + toolbar_widget.close() + + buttons.value = None + + buttons.observe(button_clicked, "value") + + toolbar_button.value = opened + if m is not None: + toolbar_control = ipyleaflet.WidgetControl( + widget=toolbar_widget, position=position + ) + + if toolbar_control not in m.controls: + m.add(toolbar_control) + m.tool_control = toolbar_control + else: + return toolbar_widget diff --git a/mkdocs.yml b/mkdocs.yml index a8b065229d..2d04613a23 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -80,6 +80,7 @@ plugins: "notebooks/88_nasa_earth_data.ipynb", "notebooks/94_mapbox.ipynb", "notebooks/99_wetlands.ipynb", + "notebooks/101_nasa_opera.ipynb", ] markdown_extensions: @@ -335,3 +336,4 @@ nav: - notebooks/98_watershed.ipynb - notebooks/99_wetlands.ipynb - notebooks/100_nlcd.ipynb + - notebooks/101_nasa_opera.ipynb