From 0689a227c87ef13e09479739cd79dbbb36818f2b Mon Sep 17 00:00:00 2001 From: Carl Date: Tue, 19 Dec 2023 22:27:51 +0000 Subject: [PATCH] :broom: --- README.md | 2 +- pages/00_home.py | 20 -- pages/{01_leafmap.py => 00_leafmap.py} | 31 ++- pages/01_docs.py | 29 +++ solara-app.ipynb | 26 ++- solara-test.ipynb | 249 ------------------------- 6 files changed, 60 insertions(+), 297 deletions(-) delete mode 100644 pages/00_home.py rename pages/{01_leafmap.py => 00_leafmap.py} (60%) create mode 100644 pages/01_docs.py delete mode 100644 solara-test.ipynb diff --git a/README.md b/README.md index f41f451..35be792 100644 --- a/README.md +++ b/README.md @@ -24,4 +24,4 @@ This application accesses boundary polygons from the National Parks Service and ### Code -The required code for analysis is in `fire.py`, with solara visualization in `pages/01_leafmap.py`. (For interactive ) \ No newline at end of file +The required code for analysis is in `fire.py`, with solara visualization in `pages/01_leafmap.py`. (For interactive use, see `solar-app.pynb`) \ No newline at end of file diff --git a/pages/00_home.py b/pages/00_home.py deleted file mode 100644 index 2ccc7b6..0000000 --- a/pages/00_home.py +++ /dev/null @@ -1,20 +0,0 @@ -import solara - - -@solara.component -def Page(): - with solara.Column(align="center"): - markdown = """ - ## A Solara Template for Geospatial Applications - - ### Introduction - - **A collection of [Solara](https://github.com/widgetti/solara) web apps for geospatial applications.** - - - Web App: - - GitHub: - - Hugging Face: - - """ - - solara.Markdown(markdown) diff --git a/pages/01_leafmap.py b/pages/00_leafmap.py similarity index 60% rename from pages/01_leafmap.py rename to pages/00_leafmap.py index 2764cd6..3c8c867 100644 --- a/pages/01_leafmap.py +++ b/pages/00_leafmap.py @@ -1,32 +1,23 @@ import leafmap import solara -import pystac_client -import planetary_computer -import odc.stac -import geopandas as gpd -import dask.distributed -import matplotlib.pyplot as plt -import rioxarray -from osgeo import gdal -import matplotlib.pyplot as plt + +# external script defines polygons, etc +from fire import * zoom = solara.reactive(14) center = solara.reactive((34, -116)) -nps = gpd.read_file("/vsicurl/https://huggingface.co/datasets/cboettig/biodiversity/resolve/main/data/NPS.gdb") -calfire = gpd.read_file("/vsicurl/https://huggingface.co/datasets/cboettig/biodiversity/resolve/main/data/fire22_1.gdb", layer = "firep22_1") -jtree = nps[nps.PARKNAME == "Joshua Tree"].to_crs(calfire.crs) -jtree_fires = jtree.overlay(calfire, how="intersection") - -recent = jtree_fires[jtree_fires.YEAR_ > "2015"] -big = recent[recent.Shape_Area == recent.Shape_Area.max()].to_crs("EPSG:4326") -datetime = big.ALARM_DATE.item() + "/" + big.CONT_DATE.item() -box = big.buffer(0.01).bounds.to_numpy()[0] # Fire bbox + buffer #box = jtree.to_crs("EPSG:4326").bounds.to_numpy()[0] # Park bbox - +# location of cached COGs before_url = "https://huggingface.co/datasets/cboettig/solara-data/resolve/main/before.tif" after_url = "https://huggingface.co/datasets/cboettig/solara-data/resolve/main/after.tif" - +# custom polygon appearance +style = { + "stroke": False, + "fill": True, + "fillColor": "#ff6666", + "fillOpacity": 0.5, +} class Map(leafmap.Map): def __init__(self, **kwargs): diff --git a/pages/01_docs.py b/pages/01_docs.py new file mode 100644 index 0000000..0b884ef --- /dev/null +++ b/pages/01_docs.py @@ -0,0 +1,29 @@ +import solara + + +@solara.component +def Page(): + with solara.Column(align="center"): + markdown = """ + ## A Solara Template for Geospatial Applications + + ### Introduction + + **A collection of [Solara](https://github.com/widgetti/solara) web apps for geospatial applications.** + + - Web App: + - GitHub Source code: + - Docker container: `ghcr.io/boettiger-lab/solara-geospatial:latest` + + This application accesses boundary polygons from the National Parks Service + and fire polygon data from CalFire to determine the location of all recorded + fires in Joshua Tree national park. We select the largest area fire since + 2015 in the database (currently turns out to be Elk Trail Fire) and access + all Sentinel-2 imagery from the two weeks before and after the fire alarm date. + From this imagery, we compute the Normalized Burn Severity metric (NBS) + around the fire polygon before and after the fire (using cloud-native approach + of `pystac`, `odc.stac`, and dask), and plot this on a leaflet map overlay with + splitmap and fire polygons. + """ + + solara.Markdown(markdown) diff --git a/solara-app.ipynb b/solara-app.ipynb index b62292e..6adbb5e 100644 --- a/solara-app.ipynb +++ b/solara-app.ipynb @@ -1,8 +1,17 @@ { "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Solara Demo Notebook\n", + "\n", + "This notebook is identical to `fire.py` + `pages/01_leafmap.py`. Running this notebook interactively (e.g. in Jupyter or VSCode) should render the solara page produced by `pages/01_leafmap.py`, and can be used for testing/developing the solara interface." + ] + }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -13,16 +22,18 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ + "# generate COGs for the selected polygon.\n", + "# This task is run by GitHub Actions and cached in HuggingFace Datasets, so can be skipped\n", "run()" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -32,6 +43,7 @@ "before_url = \"https://huggingface.co/datasets/cboettig/solara-data/resolve/main/before.tif\"\n", "after_url = \"https://huggingface.co/datasets/cboettig/solara-data/resolve/main/after.tif\"\n", "\n", + "# customize the style!\n", "style = {\n", " \"stroke\": False,\n", " \"fill\": True,\n", @@ -42,7 +54,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -64,7 +76,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -91,13 +103,13 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "40a3f4affe8749f6882c35fe09663017", + "model_id": "2c176da4d4594b4d879b29410747b282", "version_major": 2, "version_minor": 0 }, diff --git a/solara-test.ipynb b/solara-test.ipynb deleted file mode 100644 index fe53248..0000000 --- a/solara-test.ipynb +++ /dev/null @@ -1,249 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import leafmap\n", - "import solara\n", - "import pystac_client\n", - "import planetary_computer\n", - "import odc.stac\n", - "import geopandas as gpd\n", - "import dask.distributed\n", - "import matplotlib.pyplot as plt\n", - "import rioxarray\n", - "from osgeo import gdal\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "# Stashed public copies of NPS polygons and CalFire polygons\n", - "\n", - "\n", - "zoom = solara.reactive(14)\n", - "center = solara.reactive((34, -116))\n", - "nps = gpd.read_file(\"/vsicurl/https://huggingface.co/datasets/cboettig/biodiversity/resolve/main/data/NPS.gdb\")\n", - "calfire = gpd.read_file(\"/vsicurl/https://huggingface.co/datasets/cboettig/biodiversity/resolve/main/data/fire22_1.gdb\", layer = \"firep22_1\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "\n", - "\n", - "# fire = gpd.read_file(\"/vsizip/vsicurl/https://edcintl.cr.usgs.gov/downloads/sciweb1/shared/MTBS_Fire/data/composite_data/burned_area_extent_shapefile/mtbs_perimeter_data.zip\"\n", - "\n", - "# extract and reproject the Joshua Tree NP Polygon\n", - "jtree = nps[nps.PARKNAME == \"Joshua Tree\"].to_crs(calfire.crs)\n", - "# All Fires in the DB that intersect the Park\n", - "jtree_fires = jtree.overlay(calfire, how=\"intersection\")\n", - "\n", - "# Extract a polygon if interest. > 2015 for Sentinel, otherwise we can use LandSat\n", - "recent = jtree_fires[jtree_fires.YEAR_ > \"2015\"]\n", - "big = recent[recent.Shape_Area == recent.Shape_Area.max()].to_crs(\"EPSG:4326\")\n", - "box = big.buffer(0.01).bounds.to_numpy()[0] # Fire bbox + buffer\n", - "#box = jtree.to_crs(\"EPSG:4326\").bounds.to_numpy()[0] # Park bbox\n" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "from datetime import datetime, timedelta\n", - "alarm_date = datetime.strptime(big.ALARM_DATE.item(), \"%Y-%m-%dT%H:%M:%S+00:00\") \n", - "before_date = alarm_date - timedelta(days=14)\n", - "after_date = alarm_date + timedelta(days=14)" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "search_dates = big.ALARM_DATE.item() + \"/\" + big.CONT_DATE.item()\n", - "search_dates = before_date.strftime(\"%Y-%m-%d\") + \"/\" + after_date.strftime(\"%Y-%m-%d\")" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "def stac_search(box, datetime): \n", - " # STAC Search for this imagery in space/time window\n", - " items = (\n", - " pystac_client.Client.\n", - " open(\"https://planetarycomputer.microsoft.com/api/stac/v1\",\n", - " modifier=planetary_computer.sign_inplace).\n", - " search(collections=[\"sentinel-2-l2a\"],\n", - " bbox=box,\n", - " datetime=datetime,\n", - " query={\"eo:cloud_cover\": {\"lt\": 10}}).\n", - " item_collection())\n", - " return items\n", - "\n", - "def compute_nbs(items, box):\n", - " # Time to compute:\n", - " client = dask.distributed.Client()\n", - " # landsat_bands = [\"nir08\", \"swir16\"]\n", - " sentinel_bands = [\"B08\", \"B12\", \"SCL\"] # NIR, SWIR, and Cloud Mask\n", - "\n", - " # The magic of gdalwarper. Can also resample, reproject, and aggregate on the fly\n", - " data = odc.stac.load(items,\n", - " bands=sentinel_bands,\n", - " bbox=box\n", - " )\n", - " # Compute the Normalized Burn Ratio, must be float\n", - " swir = data[\"B12\"].astype(\"float\")\n", - " nir = data[\"B08\"].astype(\"float\")\n", - " # can resample and aggregate in xarray. compute with dask\n", - " nbs = (((nir - swir) / (nir + swir)).\n", - " # resample(time=\"MS\").\n", - " # median(\"time\", keep_attrs=True).\n", - " compute()\n", - " )\n", - " return nbs\n", - "\n", - "items = stac_search(box, search_dates)\n", - "nbs = compute_nbs(items, box)\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "nbs.isel(time=0).rio.to_raster(raster_path=\"before.tif\", driver=\"COG\")\n", - "nbs.isel(time=(nbs.time.size-1)).rio.to_raster(raster_path=\"after.tif\", driver=\"COG\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "before_url = \"https://huggingface.co/datasets/cboettig/solara-data/resolve/main/before.tif\"\n", - "after_url = \"https://huggingface.co/datasets/cboettig/solara-data/resolve/main/after.tif\"\n", - "\n", - "style = {\n", - " \"stroke\": False,\n", - " \"fill\": True,\n", - " \"fillColor\": \"#ff6666\",\n", - " \"fillOpacity\": 0.5,\n", - "}" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "class Map(leafmap.Map):\n", - " def __init__(self, **kwargs):\n", - " super().__init__(**kwargs)\n", - " # Add what you want below\n", - " # self.add_gdf(jtree, layer_name = \"Joshua Tree NP\")\n", - " self.add_gdf(jtree_fires, layer_name = \"All Fires\", style=style)\n", - " self.add_gdf(big, layer_name = big.FIRE_NAME.item())\n", - " #self.add_raster(\"before.tif\", layer_name = \"before\", colormap=\"viridis\")\n", - " #self.add_raster(\"after.tif\", layer_name = \"after\", colormap=\"viridis\")\n", - " self.split_map(before_url, after_url, \n", - " left_label= \"before fire\", \n", - " right_label = \"after fire\")\n", - " #self.add_stac_gui()\n", - "\n", - "\n", - "@solara.component\n", - "def Page():\n", - " with solara.Column(style={\"min-width\": \"500px\"}):\n", - " # solara components support reactive variables\n", - " # solara.SliderInt(label=\"Zoom level\", value=zoom, min=1, max=20)\n", - " # using 3rd party widget library require wiring up the events manually\n", - " # using zoom.value and zoom.set\n", - " Map.element( # type: ignore\n", - " zoom=zoom.value,\n", - " on_zoom=zoom.set,\n", - " center=center.value,\n", - " on_center=center.set,\n", - " scroll_wheel_zoom=True,\n", - " toolbar_ctrl=False,\n", - " data_ctrl=False,\n", - " height=\"780px\",\n", - " )\n", - " solara.Text(f\"Zoom: {zoom.value}\")\n", - " solara.Text(f\"Center: {center.value}\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "822080e3202b494888a05630fb5bf798", - "version_major": 2, - "version_minor": 0 - }, - "text/html": [ - "Cannot show widget. You probably want to rerun the code cell above (Click in the code cell, and press Shift+Enter +)." - ], - "text/plain": [ - "Cannot show ipywidgets in text" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "Page()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "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.11.6" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -}