cboettig commited on
Commit
0689a22
Β·
1 Parent(s): 831b99f
README.md CHANGED
@@ -24,4 +24,4 @@ This application accesses boundary polygons from the National Parks Service and
24
 
25
  ### Code
26
 
27
- The required code for analysis is in `fire.py`, with solara visualization in `pages/01_leafmap.py`. (For interactive )
 
24
 
25
  ### Code
26
 
27
+ The required code for analysis is in `fire.py`, with solara visualization in `pages/01_leafmap.py`. (For interactive use, see `solar-app.pynb`)
pages/{01_leafmap.py β†’ 00_leafmap.py} RENAMED
@@ -1,32 +1,23 @@
1
  import leafmap
2
  import solara
3
- import pystac_client
4
- import planetary_computer
5
- import odc.stac
6
- import geopandas as gpd
7
- import dask.distributed
8
- import matplotlib.pyplot as plt
9
- import rioxarray
10
- from osgeo import gdal
11
- import matplotlib.pyplot as plt
12
 
13
  zoom = solara.reactive(14)
14
  center = solara.reactive((34, -116))
15
 
16
- nps = gpd.read_file("/vsicurl/https://huggingface.co/datasets/cboettig/biodiversity/resolve/main/data/NPS.gdb")
17
- calfire = gpd.read_file("/vsicurl/https://huggingface.co/datasets/cboettig/biodiversity/resolve/main/data/fire22_1.gdb", layer = "firep22_1")
18
- jtree = nps[nps.PARKNAME == "Joshua Tree"].to_crs(calfire.crs)
19
- jtree_fires = jtree.overlay(calfire, how="intersection")
20
-
21
- recent = jtree_fires[jtree_fires.YEAR_ > "2015"]
22
- big = recent[recent.Shape_Area == recent.Shape_Area.max()].to_crs("EPSG:4326")
23
- datetime = big.ALARM_DATE.item() + "/" + big.CONT_DATE.item()
24
- box = big.buffer(0.01).bounds.to_numpy()[0] # Fire bbox + buffer #box = jtree.to_crs("EPSG:4326").bounds.to_numpy()[0] # Park bbox
25
-
26
  before_url = "https://huggingface.co/datasets/cboettig/solara-data/resolve/main/before.tif"
27
  after_url = "https://huggingface.co/datasets/cboettig/solara-data/resolve/main/after.tif"
28
 
29
-
 
 
 
 
 
 
30
 
31
  class Map(leafmap.Map):
32
  def __init__(self, **kwargs):
 
1
  import leafmap
2
  import solara
3
+
4
+ # external script defines polygons, etc
5
+ from fire import *
 
 
 
 
 
 
6
 
7
  zoom = solara.reactive(14)
8
  center = solara.reactive((34, -116))
9
 
10
+ # location of cached COGs
 
 
 
 
 
 
 
 
 
11
  before_url = "https://huggingface.co/datasets/cboettig/solara-data/resolve/main/before.tif"
12
  after_url = "https://huggingface.co/datasets/cboettig/solara-data/resolve/main/after.tif"
13
 
14
+ # custom polygon appearance
15
+ style = {
16
+ "stroke": False,
17
+ "fill": True,
18
+ "fillColor": "#ff6666",
19
+ "fillOpacity": 0.5,
20
+ }
21
 
22
  class Map(leafmap.Map):
23
  def __init__(self, **kwargs):
pages/{00_home.py β†’ 01_docs.py} RENAMED
@@ -12,9 +12,18 @@ def Page():
12
  **A collection of [Solara](https://github.com/widgetti/solara) web apps for geospatial applications.**
13
 
14
  - Web App: <https://cboettig-solara-test.hf.space>
15
- - GitHub: <https://github.com/boettiger-lab/solara-test>
16
- - Hugging Face: <https://huggingface.co/spaces/cboettig/solara-test>
17
 
 
 
 
 
 
 
 
 
 
18
  """
19
 
20
  solara.Markdown(markdown)
 
12
  **A collection of [Solara](https://github.com/widgetti/solara) web apps for geospatial applications.**
13
 
14
  - Web App: <https://cboettig-solara-test.hf.space>
15
+ - GitHub Source code: <https://github.com/boettiger-lab/solara-test>
16
+ - Docker container: `ghcr.io/boettiger-lab/solara-geospatial:latest`
17
 
18
+ This application accesses boundary polygons from the National Parks Service
19
+ and fire polygon data from CalFire to determine the location of all recorded
20
+ fires in Joshua Tree national park. We select the largest area fire since
21
+ 2015 in the database (currently turns out to be Elk Trail Fire) and access
22
+ all Sentinel-2 imagery from the two weeks before and after the fire alarm date.
23
+ From this imagery, we compute the Normalized Burn Severity metric (NBS)
24
+ around the fire polygon before and after the fire (using cloud-native approach
25
+ of `pystac`, `odc.stac`, and dask), and plot this on a leaflet map overlay with
26
+ splitmap and fire polygons.
27
  """
28
 
29
  solara.Markdown(markdown)
solara-app.ipynb CHANGED
@@ -1,8 +1,17 @@
1
  {
2
  "cells": [
 
 
 
 
 
 
 
 
 
3
  {
4
  "cell_type": "code",
5
- "execution_count": 6,
6
  "metadata": {},
7
  "outputs": [],
8
  "source": [
@@ -13,16 +22,18 @@
13
  },
14
  {
15
  "cell_type": "code",
16
- "execution_count": 3,
17
  "metadata": {},
18
  "outputs": [],
19
  "source": [
 
 
20
  "run()"
21
  ]
22
  },
23
  {
24
  "cell_type": "code",
25
- "execution_count": 10,
26
  "metadata": {},
27
  "outputs": [],
28
  "source": [
@@ -32,6 +43,7 @@
32
  "before_url = \"https://huggingface.co/datasets/cboettig/solara-data/resolve/main/before.tif\"\n",
33
  "after_url = \"https://huggingface.co/datasets/cboettig/solara-data/resolve/main/after.tif\"\n",
34
  "\n",
 
35
  "style = {\n",
36
  " \"stroke\": False,\n",
37
  " \"fill\": True,\n",
@@ -42,7 +54,7 @@
42
  },
43
  {
44
  "cell_type": "code",
45
- "execution_count": 11,
46
  "metadata": {},
47
  "outputs": [],
48
  "source": [
@@ -64,7 +76,7 @@
64
  },
65
  {
66
  "cell_type": "code",
67
- "execution_count": null,
68
  "metadata": {},
69
  "outputs": [],
70
  "source": [
@@ -91,13 +103,13 @@
91
  },
92
  {
93
  "cell_type": "code",
94
- "execution_count": 12,
95
  "metadata": {},
96
  "outputs": [
97
  {
98
  "data": {
99
  "application/vnd.jupyter.widget-view+json": {
100
- "model_id": "40a3f4affe8749f6882c35fe09663017",
101
  "version_major": 2,
102
  "version_minor": 0
103
  },
 
1
  {
2
  "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# Solara Demo Notebook\n",
8
+ "\n",
9
+ "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."
10
+ ]
11
+ },
12
  {
13
  "cell_type": "code",
14
+ "execution_count": 1,
15
  "metadata": {},
16
  "outputs": [],
17
  "source": [
 
22
  },
23
  {
24
  "cell_type": "code",
25
+ "execution_count": null,
26
  "metadata": {},
27
  "outputs": [],
28
  "source": [
29
+ "# generate COGs for the selected polygon.\n",
30
+ "# This task is run by GitHub Actions and cached in HuggingFace Datasets, so can be skipped\n",
31
  "run()"
32
  ]
33
  },
34
  {
35
  "cell_type": "code",
36
+ "execution_count": 3,
37
  "metadata": {},
38
  "outputs": [],
39
  "source": [
 
43
  "before_url = \"https://huggingface.co/datasets/cboettig/solara-data/resolve/main/before.tif\"\n",
44
  "after_url = \"https://huggingface.co/datasets/cboettig/solara-data/resolve/main/after.tif\"\n",
45
  "\n",
46
+ "# customize the style!\n",
47
  "style = {\n",
48
  " \"stroke\": False,\n",
49
  " \"fill\": True,\n",
 
54
  },
55
  {
56
  "cell_type": "code",
57
+ "execution_count": 4,
58
  "metadata": {},
59
  "outputs": [],
60
  "source": [
 
76
  },
77
  {
78
  "cell_type": "code",
79
+ "execution_count": 5,
80
  "metadata": {},
81
  "outputs": [],
82
  "source": [
 
103
  },
104
  {
105
  "cell_type": "code",
106
+ "execution_count": 6,
107
  "metadata": {},
108
  "outputs": [
109
  {
110
  "data": {
111
  "application/vnd.jupyter.widget-view+json": {
112
+ "model_id": "2c176da4d4594b4d879b29410747b282",
113
  "version_major": 2,
114
  "version_minor": 0
115
  },
solara-test.ipynb DELETED
@@ -1,249 +0,0 @@
1
- {
2
- "cells": [
3
- {
4
- "cell_type": "code",
5
- "execution_count": 1,
6
- "metadata": {},
7
- "outputs": [],
8
- "source": [
9
- "import leafmap\n",
10
- "import solara\n",
11
- "import pystac_client\n",
12
- "import planetary_computer\n",
13
- "import odc.stac\n",
14
- "import geopandas as gpd\n",
15
- "import dask.distributed\n",
16
- "import matplotlib.pyplot as plt\n",
17
- "import rioxarray\n",
18
- "from osgeo import gdal\n"
19
- ]
20
- },
21
- {
22
- "cell_type": "code",
23
- "execution_count": 2,
24
- "metadata": {},
25
- "outputs": [],
26
- "source": [
27
- "\n",
28
- "# Stashed public copies of NPS polygons and CalFire polygons\n",
29
- "\n",
30
- "\n",
31
- "zoom = solara.reactive(14)\n",
32
- "center = solara.reactive((34, -116))\n",
33
- "nps = gpd.read_file(\"/vsicurl/https://huggingface.co/datasets/cboettig/biodiversity/resolve/main/data/NPS.gdb\")\n",
34
- "calfire = gpd.read_file(\"/vsicurl/https://huggingface.co/datasets/cboettig/biodiversity/resolve/main/data/fire22_1.gdb\", layer = \"firep22_1\")\n"
35
- ]
36
- },
37
- {
38
- "cell_type": "code",
39
- "execution_count": 3,
40
- "metadata": {},
41
- "outputs": [],
42
- "source": [
43
- "\n",
44
- "\n",
45
- "\n",
46
- "# 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",
47
- "\n",
48
- "# extract and reproject the Joshua Tree NP Polygon\n",
49
- "jtree = nps[nps.PARKNAME == \"Joshua Tree\"].to_crs(calfire.crs)\n",
50
- "# All Fires in the DB that intersect the Park\n",
51
- "jtree_fires = jtree.overlay(calfire, how=\"intersection\")\n",
52
- "\n",
53
- "# Extract a polygon if interest. > 2015 for Sentinel, otherwise we can use LandSat\n",
54
- "recent = jtree_fires[jtree_fires.YEAR_ > \"2015\"]\n",
55
- "big = recent[recent.Shape_Area == recent.Shape_Area.max()].to_crs(\"EPSG:4326\")\n",
56
- "box = big.buffer(0.01).bounds.to_numpy()[0] # Fire bbox + buffer\n",
57
- "#box = jtree.to_crs(\"EPSG:4326\").bounds.to_numpy()[0] # Park bbox\n"
58
- ]
59
- },
60
- {
61
- "cell_type": "code",
62
- "execution_count": 4,
63
- "metadata": {},
64
- "outputs": [],
65
- "source": [
66
- "from datetime import datetime, timedelta\n",
67
- "alarm_date = datetime.strptime(big.ALARM_DATE.item(), \"%Y-%m-%dT%H:%M:%S+00:00\") \n",
68
- "before_date = alarm_date - timedelta(days=14)\n",
69
- "after_date = alarm_date + timedelta(days=14)"
70
- ]
71
- },
72
- {
73
- "cell_type": "code",
74
- "execution_count": 5,
75
- "metadata": {},
76
- "outputs": [],
77
- "source": [
78
- "search_dates = big.ALARM_DATE.item() + \"/\" + big.CONT_DATE.item()\n",
79
- "search_dates = before_date.strftime(\"%Y-%m-%d\") + \"/\" + after_date.strftime(\"%Y-%m-%d\")"
80
- ]
81
- },
82
- {
83
- "cell_type": "code",
84
- "execution_count": 6,
85
- "metadata": {},
86
- "outputs": [],
87
- "source": [
88
- "\n",
89
- "def stac_search(box, datetime): \n",
90
- " # STAC Search for this imagery in space/time window\n",
91
- " items = (\n",
92
- " pystac_client.Client.\n",
93
- " open(\"https://planetarycomputer.microsoft.com/api/stac/v1\",\n",
94
- " modifier=planetary_computer.sign_inplace).\n",
95
- " search(collections=[\"sentinel-2-l2a\"],\n",
96
- " bbox=box,\n",
97
- " datetime=datetime,\n",
98
- " query={\"eo:cloud_cover\": {\"lt\": 10}}).\n",
99
- " item_collection())\n",
100
- " return items\n",
101
- "\n",
102
- "def compute_nbs(items, box):\n",
103
- " # Time to compute:\n",
104
- " client = dask.distributed.Client()\n",
105
- " # landsat_bands = [\"nir08\", \"swir16\"]\n",
106
- " sentinel_bands = [\"B08\", \"B12\", \"SCL\"] # NIR, SWIR, and Cloud Mask\n",
107
- "\n",
108
- " # The magic of gdalwarper. Can also resample, reproject, and aggregate on the fly\n",
109
- " data = odc.stac.load(items,\n",
110
- " bands=sentinel_bands,\n",
111
- " bbox=box\n",
112
- " )\n",
113
- " # Compute the Normalized Burn Ratio, must be float\n",
114
- " swir = data[\"B12\"].astype(\"float\")\n",
115
- " nir = data[\"B08\"].astype(\"float\")\n",
116
- " # can resample and aggregate in xarray. compute with dask\n",
117
- " nbs = (((nir - swir) / (nir + swir)).\n",
118
- " # resample(time=\"MS\").\n",
119
- " # median(\"time\", keep_attrs=True).\n",
120
- " compute()\n",
121
- " )\n",
122
- " return nbs\n",
123
- "\n",
124
- "items = stac_search(box, search_dates)\n",
125
- "nbs = compute_nbs(items, box)\n",
126
- "\n"
127
- ]
128
- },
129
- {
130
- "cell_type": "code",
131
- "execution_count": 7,
132
- "metadata": {},
133
- "outputs": [],
134
- "source": [
135
- "\n",
136
- "nbs.isel(time=0).rio.to_raster(raster_path=\"before.tif\", driver=\"COG\")\n",
137
- "nbs.isel(time=(nbs.time.size-1)).rio.to_raster(raster_path=\"after.tif\", driver=\"COG\")\n"
138
- ]
139
- },
140
- {
141
- "cell_type": "code",
142
- "execution_count": 8,
143
- "metadata": {},
144
- "outputs": [],
145
- "source": [
146
- "\n",
147
- "before_url = \"https://huggingface.co/datasets/cboettig/solara-data/resolve/main/before.tif\"\n",
148
- "after_url = \"https://huggingface.co/datasets/cboettig/solara-data/resolve/main/after.tif\"\n",
149
- "\n",
150
- "style = {\n",
151
- " \"stroke\": False,\n",
152
- " \"fill\": True,\n",
153
- " \"fillColor\": \"#ff6666\",\n",
154
- " \"fillOpacity\": 0.5,\n",
155
- "}"
156
- ]
157
- },
158
- {
159
- "cell_type": "code",
160
- "execution_count": 9,
161
- "metadata": {},
162
- "outputs": [],
163
- "source": [
164
- "class Map(leafmap.Map):\n",
165
- " def __init__(self, **kwargs):\n",
166
- " super().__init__(**kwargs)\n",
167
- " # Add what you want below\n",
168
- " # self.add_gdf(jtree, layer_name = \"Joshua Tree NP\")\n",
169
- " self.add_gdf(jtree_fires, layer_name = \"All Fires\", style=style)\n",
170
- " self.add_gdf(big, layer_name = big.FIRE_NAME.item())\n",
171
- " #self.add_raster(\"before.tif\", layer_name = \"before\", colormap=\"viridis\")\n",
172
- " #self.add_raster(\"after.tif\", layer_name = \"after\", colormap=\"viridis\")\n",
173
- " self.split_map(before_url, after_url, \n",
174
- " left_label= \"before fire\", \n",
175
- " right_label = \"after fire\")\n",
176
- " #self.add_stac_gui()\n",
177
- "\n",
178
- "\n",
179
- "@solara.component\n",
180
- "def Page():\n",
181
- " with solara.Column(style={\"min-width\": \"500px\"}):\n",
182
- " # solara components support reactive variables\n",
183
- " # solara.SliderInt(label=\"Zoom level\", value=zoom, min=1, max=20)\n",
184
- " # using 3rd party widget library require wiring up the events manually\n",
185
- " # using zoom.value and zoom.set\n",
186
- " Map.element( # type: ignore\n",
187
- " zoom=zoom.value,\n",
188
- " on_zoom=zoom.set,\n",
189
- " center=center.value,\n",
190
- " on_center=center.set,\n",
191
- " scroll_wheel_zoom=True,\n",
192
- " toolbar_ctrl=False,\n",
193
- " data_ctrl=False,\n",
194
- " height=\"780px\",\n",
195
- " )\n",
196
- " solara.Text(f\"Zoom: {zoom.value}\")\n",
197
- " solara.Text(f\"Center: {center.value}\")\n"
198
- ]
199
- },
200
- {
201
- "cell_type": "code",
202
- "execution_count": 10,
203
- "metadata": {},
204
- "outputs": [
205
- {
206
- "data": {
207
- "application/vnd.jupyter.widget-view+json": {
208
- "model_id": "822080e3202b494888a05630fb5bf798",
209
- "version_major": 2,
210
- "version_minor": 0
211
- },
212
- "text/html": [
213
- "Cannot show widget. You probably want to rerun the code cell above (<i>Click in the code cell, and press Shift+Enter <kbd>⇧</kbd>+<kbd>↩</kbd></i>)."
214
- ],
215
- "text/plain": [
216
- "Cannot show ipywidgets in text"
217
- ]
218
- },
219
- "metadata": {},
220
- "output_type": "display_data"
221
- }
222
- ],
223
- "source": [
224
- "Page()"
225
- ]
226
- }
227
- ],
228
- "metadata": {
229
- "kernelspec": {
230
- "display_name": ".venv",
231
- "language": "python",
232
- "name": "python3"
233
- },
234
- "language_info": {
235
- "codemirror_mode": {
236
- "name": "ipython",
237
- "version": 3
238
- },
239
- "file_extension": ".py",
240
- "mimetype": "text/x-python",
241
- "name": "python",
242
- "nbconvert_exporter": "python",
243
- "pygments_lexer": "ipython3",
244
- "version": "3.11.6"
245
- }
246
- },
247
- "nbformat": 4,
248
- "nbformat_minor": 2
249
- }