{ "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, ?it/s]" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "7dc3f833c1c945019bbccb9449e88506", "version_major": 2, "version_minor": 0 }, "text/plain": [ "PROCESSING TASKS | : 0%| | 0/3 [00:00, ?it/s]" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "3765435664b14138a4aaaf7cf229cd24", "version_major": 2, "version_minor": 0 }, "text/plain": [ "COLLECTING RESULTS | : 0%| | 0/3 [00:00, ?it/s]" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "download_dir = \"/projects/my-public-bucket/opera-disp/\"\n", "downloaded = earthaccess.download(\n", " granules,\n", " download_dir,\n", ")" ] }, { "cell_type": "markdown", "id": "20882e90-df68-42c5-ba1a-6edcb68e3908", "metadata": {}, "source": [ "## Building nc_path and the S3 nc_key\n", "\n", "Finds the first downloaded NetCDF by extension then constructs a shared-workspace S3 URI so the file can be referenced.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "480e0f8b-3cb6-43b5-a7f2-56111ed7308b", "metadata": {}, "outputs": [], "source": [ "nc_path = next(\n", " (\n", " str(Path(fn).relative_to(download_dir))\n", " for fn in downloaded\n", " if Path(fn).suffix == \".nc\"\n", " ),\n", " None,\n", ")\n", "\n", "assert nc_path is not None, f\"No NetCDF (.nc) file downloaded to {download_dir}\"\n", "\n", "nc_key = (\n", " f\"s3://maap-ops-workspace/shared/{os.getenv('CHE_WORKSPACE_NAMESPACE')}\"\n", " f\"/opera-disp/{nc_path}\"\n", ")" ] }, { "cell_type": "markdown", "id": "1f5b1109-7915-4faf-819f-cf8217e86134", "metadata": {}, "source": [ "## Requesting TileJSON from TiTiler-MultiDim\n", "Send a request to `/WebMercatorQuad/tilejson.json` with `url=nc_key`, the target variable, and styling. TiTiler reads/slices the NetCDF on the fly and returns TileJSON with bounds, center, zoom limits, and a tiles URL template. Then use the returned tiles template to create a Leaflet tileLayer and render the layer in the map without downloading the file.\n" ] }, { "cell_type": "code", "execution_count": 6, "id": "05d2a9ac-d072-4820-ba53-aa893118f399", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'bounds': [-124.14140929951725,\n", " 37.08503078714323,\n", " -120.82130131653709,\n", " 39.24070040130846],\n", " 'center': [-122.48135530802716, 38.16286559422585, 7],\n", " 'maxzoom': 12,\n", " 'minzoom': 7,\n", " 'scheme': 'xyz',\n", " 'tilejson': '2.2.0',\n", " 'tiles': ['https://staging.openveda.cloud/api/titiler-multidim/tiles/WebMercatorQuad/{z}/{x}/{y}@1x?url=s3%3A%2F%2Fmaap-ops-workspace%2Fshared%2Fharshinigirish%2Fopera-disp%2FOPERA_L3_DISP-S1_IW_F09157_VV_20160705T020728Z_20170107T020647Z_v1.0_20250408T163918Z.nc&rescale=%5B-0.1%2C+0.05%5D&colormap_name=rdbu&variable=displacement'],\n", " 'version': '1.0.0'}\n" ] } ], "source": [ "TITILER_MULTIDIM_ENDPOINT = \"https://staging.openveda.cloud/api/titiler-multidim\"\n", "\n", "params = {\n", " \"url\": nc_key,\n", " \"rescale\": [[-0.1, 0.05]],\n", " \"colormap_name\": \"rdbu\",\n", " \"variable\": \"displacement\",\n", " \"minzoom\": 7,\n", "}\n", "\n", "tilejson = httpx.get(\n", " f\"{TITILER_MULTIDIM_ENDPOINT}/WebMercatorQuad/tilejson.json\",\n", " params=params,\n", " timeout=None,\n", ").json()\n", "\n", "pprint(tilejson)" ] }, { "cell_type": "markdown", "id": "a9142417-546f-43b4-ae61-ebcb489c0491", "metadata": {}, "source": [ "## Visualising tile layer and granule bounds in Leaflet" ] }, { "cell_type": "markdown", "id": "958c31e9-6b13-4cc5-9cea-5529929675f9", "metadata": {}, "source": [ "This builds a Leaflet TileLayer from the TileJSON (`tiles[0]`, `minzoom`) so the OPERA-DISP raster streams as XYZ tiles. It also constructs a GeoJSON polygon from `tilejson[\"bounds\"]` and styles it (orange outline) to show the granule footprint. Finally, it centers a map on the bounds midpoint, adds both layers, and enables a `LayerControl` for toggling." ] }, { "cell_type": "code", "execution_count": 12, "id": "4ce73c61-33a1-4ad4-8f84-5df06819ca84", "metadata": {}, "outputs": [ { "data": { "text/html": [ "