{ "cells": [ { "cell_type": "markdown", "id": "612223b1-8edf-4477-9142-e89aa6a33da2", "metadata": {}, "source": [ "# Visualizing multi-dimensional OPERA-DISP with titiler-multidim" ] }, { "cell_type": "markdown", "id": "7ef8a131-64a1-46a6-ad95-c15d7a9e1d19", "metadata": {}, "source": [ "Authors: Henry Rodman(Development Seed), Harshini Girish(UAH), Alex Mandel(Development Seed)\n", "\n", "Date: September 25, 2025\n", "\n", "Description: TiTiler-MultiDim is a TiTiler application designed to serve multidimensional rasters—like OPERA-DISP NetCDF—directly as web map tiles. It lets you pick a variable (e.g., water_mask or displacement layers) and slice along dimensions (time, burst, polarization, etc.), then renders those selections on-the-fly into XYZ/TileJSON that you can drop into Folium/Leaflet without downloading the file. The notebook centers on this workflow: open a multidim asset, choose variable + dims + styling (colormap, range), and stream dynamic tiles for quick, interactive visualization." ] }, { "cell_type": "markdown", "id": "fa15eafd-bca8-4b0b-8fd3-b917c3c11a60", "metadata": {}, "source": [ "## Run This Notebook\n", "To access and run this tutorial within MAAP's Algorithm Development Environment (ADE), please refer to the [\"Getting started with the MAAP\"](https://docs.maap-project.org/en/latest/getting_started/getting_started.html) section of our documentation.\n", "\n", "Disclaimer: it is highly recommended to run a tutorial within MAAP's ADE, which already includes packages specific to MAAP, such as maap-py. Running the tutorial outside of the MAAP ADE may lead to errors." ] }, { "cell_type": "markdown", "id": "f34058ef-2ff5-40d4-80ce-34c71a0f31df", "metadata": {}, "source": [ "## Additional Resources\n", "- [OPERA Surface Displacement (DISP-S1)](https://docs.maap-project.org/en/latest/science/OPERA/OPERA_Surface_Displacement.html) — Overview of the OPERA DISP-S1 product: what it measures, how it’s produced, and how to access/use it on MAAP.\n", "- [Visualizing with TiTiler-PgSTAC (Python)](https://docs.maap-project.org/en/latest/technical_tutorials/visualization/visualizing_titiler-pgstac.html) — Step-by-step guide to stream STAC assets through TiTiler-PgSTAC and visualize them as web tiles in Python.\n", "- [Visualizing with TiTiler-PgSTAC (R)](https://docs.maap-project.org/en/latest/technical_tutorials/working_with_r/visualizing_with_titiler-pgstac.html) — R-focused tutorial for rendering STAC items via TiTiler-PgSTAC and building interactive map visualizations.\n" ] }, { "cell_type": "markdown", "id": "2e139427-f3e7-4ffc-9e73-d995d2b1b687", "metadata": {}, "source": [ "## About the Dataset\n", "\n", "> The Level-3 OPERA Sentinel-1 Surface Displacement (DISP) product is generated through interferometric time-series analysis of Level-2 Coregistered Sentinel-1 Single Look Complex (CSLC) datasets. Using a hybrid Persistent Scatterer (PS) and Distributed Scatterer (DS) approach, this product quantifies Earth's surface displacement in the radar line-of-sight. The DISP products enable the detection of anthropogenic and natural surface changes, including subsidence, tectonic deformation, and landslides. \n", "\n", "> The OPERA DISP suite comprises complementary datasets derived from Sentinel-1 and NISAR inputs, designated as DISP-S1 and DISP-NI, respectively. Each product, created per acquisition, adheres to a consistent structure, HDF5 file format, file-naming convention, and a 30 m spatial posting. This collection specifically includes DISP-S1 products, derived from Sentinel-1 data. For visualization and quick exploration, the Pangeo Image can be used for these datasets. \n", "\n", "Source: [OPERA Surface Displacement from Sentinel-1](https://cmr.earthdata.nasa.gov/search/concepts/C3294057315-ASF.html)" ] }, { "cell_type": "markdown", "id": "6c36cd9f-238c-4087-865a-684f4cfd94a1", "metadata": {}, "source": [ "## Install/Import Packages\n", "Make sure the following libraries are installed before running the notebook" ] }, { "cell_type": "code", "execution_count": 1, "id": "ad3f349c-6286-4f7f-aeaf-84058cc86b9f", "metadata": {}, "outputs": [], "source": [ "import os\n", "import earthaccess\n", "import httpx\n", "from folium import GeoJson, LayerControl, Map, TileLayer\n", "from pprint import pprint\n", "from shapely.geometry import box, mapping\n", "from pathlib import Path\n" ] }, { "cell_type": "markdown", "id": "7db6a652-b34b-483b-a234-4eece3d6d75e", "metadata": {}, "source": [ "## Searching the Data\n", "This performs a granule search using the `earthaccess.granule_query()` function on the OPERA Sentinel-1 displacement product collection." ] }, { "cell_type": "code", "execution_count": 2, "id": "3f8e5c6d-0ce1-4e08-bab1-77d8f2f5c276", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Collection: {'ShortName': 'OPERA_L3_DISP-S1_V1', 'Version': '1'}\n", "Spatial coverage: {'HorizontalSpatialDomain': {'Geometry': {'GPolygons': [{'Boundary': {'Points': [{'Latitude': 39.16866, 'Longitude': -121.23333}, {'Latitude': 38.66165, 'Longitude': -124.06639}, {'Latitude': 37.8195, 'Longitude': -123.86328}, {'Latitude': 37.95784, 'Longitude': -122.92612}, {'Latitude': 37.35143, 'Longitude': -122.78615}, {'Latitude': 37.66388, 'Longitude': -120.92917}, {'Latitude': 39.16866, 'Longitude': -121.23333}]}}]}}}\n", "Temporal coverage: {'RangeDateTime': {'BeginningDateTime': '2016-07-05T02:07:28Z', 'EndingDateTime': '2017-01-07T02:06:47Z'}}\n", "Size(MB): 376.585862159729\n", "Data: ['https://datapool.asf.alaska.edu/DISP/OPERA-S1/OPERA_L3_DISP-S1_IW_F09157_VV_20160705T020728Z_20170107T020647Z_v1.0_20250408T163918Z.nc', 'https://datapool.asf.alaska.edu/DISP/OPERA-S1/OPERA_L3_DISP-S1_IW_F09157_VV_20160705T020728Z_20170107T020647Z_v1.0_20250408T163918Z.zarr.json.gz', 'https://datapool.asf.alaska.edu/DISP/OPERA-S1/OPERA_L3_DISP-S1_IW_F09157_VV_short_wavelength_displacement.zarr.json.gz']\n" ] } ], "source": [ "auth = earthaccess.login()\n", "granule_query = (\n", " earthaccess.granule_query()\n", " .short_name(\"OPERA_L3_DISP-S1_V1\")\n", " .bounding_box(-121, 38, -120, 39)\n", ")\n", "\n", "granules = granule_query.get(1)\n", "granule = granules[0]\n", "\n", "print(granule)\n" ] }, { "cell_type": "markdown", "id": "402cd405-0dd6-4d93-a8b0-767b1197b3d9", "metadata": {}, "source": [ "## Downloading OPERA-DISP granules\n", "\n", "Creates the local folder to fetch the selected OPERA-DISP files.Then the progress indicators for the download tasks.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "f9d87914-882f-4567-a895-7a82043f7f2c", "metadata": {}, "outputs": [], "source": [ "!mkdir -p /projects/my-public-bucket/opera-disp" ] }, { "cell_type": "code", "execution_count": null, "id": "4aad9f3c-1798-4e71-85ce-bf7aa888e0ff", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "cb7b2300d5704ecabd9d721d09c7784f", "version_major": 2, "version_minor": 0 }, "text/plain": [ "QUEUEING TASKS | : 0%| | 0/3 [00:00
Make this Notebook Trusted to load map: File -> Trust Notebook
" ], "text/plain": [ "" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# create an XYZ tile layer for the leaflet map\n", "tiles = TileLayer(\n", " name=\"OPERA-DISP\",\n", " tiles=tilejson[\"tiles\"][0],\n", " min_zoom=tilejson[\"minzoom\"],\n", " opacity=1,\n", " attr=\"NASA\",\n", " overlay=True,\n", " control=True,\n", ")\n", "\n", "# load the granule bounding box so we can plot it on the map\n", "geojson = {\"type\": \"Feature\", \"geometry\": mapping(box(*tilejson[\"bounds\"])), \"properties\": {}}\n", "\n", "geojson_layer = GeoJson(\n", " data=geojson,\n", " style_function=lambda x: {\n", " \"opacity\": 1,\n", " \"dashArray\": \"1\",\n", " \"fillOpacity\": 0,\n", " \"weight\": 2,\n", " \"color\": \"orange\",\n", " },\n", " name=\"granule bounds\",\n", " overlay=True,\n", " control=True,\n", " show=True,\n", ")\n", "\n", "m = Map(\n", " tiles=\"OpenStreetMap\",\n", " location=(\n", " (tilejson[\"bounds\"][1] + tilejson[\"bounds\"][3]) / 2,\n", " (tilejson[\"bounds\"][0] + tilejson[\"bounds\"][2]) / 2\n", " ),\n", " zoom_start=8,\n", ")\n", "tiles.add_to(m)\n", "geojson_layer.add_to(m)\n", "\n", "LayerControl(collapsed=False).add_to(m)\n", "\n", "m" ] }, { "cell_type": "markdown", "id": "3fcb46b4-ceb1-46c0-a692-70d74dc2565f", "metadata": {}, "source": [ "**Note:** The plot may take 20–30 seconds to load." ] } ], "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.12.7" } }, "nbformat": 4, "nbformat_minor": 5 }