Spaces:
Paused
Paused
Add JRC water statistics endpoint (#7)
Browse filesAdd POST /jrc-water-stats endpoint that computes JRC monthly water
history and water occurrence statistics for a given bounding box and
scale. Returns JSON with monthly/yearly water area data and occurrence
stats (mean, min, max, stdDev, histogram) suitable for creating plots.
- README.md +79 -5
- main.py +134 -0
- requirements.txt +1 -1
README.md
CHANGED
|
@@ -67,15 +67,16 @@ docker run -p 7860:7860 -e EARTHENGINE_TOKEN="your_token" ee-tile-request
|
|
| 67 |
|
| 68 |
- **Web UI**: http://localhost:7860
|
| 69 |
- **API Documentation**: http://localhost:7860/docs
|
| 70 |
-
- **
|
|
|
|
| 71 |
|
| 72 |
-
##
|
| 73 |
|
| 74 |
-
### Endpoint
|
| 75 |
|
| 76 |
`POST /tile`
|
| 77 |
|
| 78 |
-
### Request Parameters
|
| 79 |
|
| 80 |
| Parameter | Type | Required | Description |
|
| 81 |
|-----------|------|----------|-------------|
|
|
@@ -159,7 +160,7 @@ curl -X POST "http://localhost:7860/tile" \
|
|
| 159 |
}'
|
| 160 |
```
|
| 161 |
|
| 162 |
-
### Response
|
| 163 |
|
| 164 |
```json
|
| 165 |
{
|
|
@@ -167,6 +168,79 @@ curl -X POST "http://localhost:7860/tile" \
|
|
| 167 |
}
|
| 168 |
```
|
| 169 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
### Using with Web Mapping Libraries
|
| 171 |
|
| 172 |
#### Leaflet
|
|
|
|
| 67 |
|
| 68 |
- **Web UI**: http://localhost:7860
|
| 69 |
- **API Documentation**: http://localhost:7860/docs
|
| 70 |
+
- **Tile Endpoint**: POST http://localhost:7860/tile
|
| 71 |
+
- **JRC Water Stats Endpoint**: POST http://localhost:7860/jrc-water-stats
|
| 72 |
|
| 73 |
+
## Tile URL API
|
| 74 |
|
| 75 |
+
### Tile Endpoint
|
| 76 |
|
| 77 |
`POST /tile`
|
| 78 |
|
| 79 |
+
### Tile Request Parameters
|
| 80 |
|
| 81 |
| Parameter | Type | Required | Description |
|
| 82 |
|-----------|------|----------|-------------|
|
|
|
|
| 160 |
}'
|
| 161 |
```
|
| 162 |
|
| 163 |
+
### Tile Response
|
| 164 |
|
| 165 |
```json
|
| 166 |
{
|
|
|
|
| 168 |
}
|
| 169 |
```
|
| 170 |
|
| 171 |
+
## JRC Water Statistics API
|
| 172 |
+
|
| 173 |
+
### JRC Endpoint
|
| 174 |
+
|
| 175 |
+
`POST /jrc-water-stats`
|
| 176 |
+
|
| 177 |
+
Computes JRC monthly water history and water occurrence statistics for a given bounding box and scale. Returns JSON data suitable for creating plots.
|
| 178 |
+
|
| 179 |
+
### JRC Request Parameters
|
| 180 |
+
|
| 181 |
+
| Parameter | Type | Required | Default | Description |
|
| 182 |
+
|-----------|------|----------|---------|-------------|
|
| 183 |
+
| `bbox` | array | Yes | — | Bounding box [west, south, east, north] in degrees |
|
| 184 |
+
| `scale` | number | No | 30 | Scale in meters for computation |
|
| 185 |
+
| `start_date` | string | No | "1984-03-16" | Start date (format: "YYYY-MM-DD") |
|
| 186 |
+
| `end_date` | string | No | today | End date (format: "YYYY-MM-DD") |
|
| 187 |
+
| `start_month` | integer | No | 1 | Start month for calendar filtering (1-12) |
|
| 188 |
+
| `end_month` | integer | No | 12 | End month for calendar filtering (1-12) |
|
| 189 |
+
| `frequency` | string | No | "year" | Aggregation frequency: "month" or "year" |
|
| 190 |
+
| `denominator` | number | No | 10000 | Area unit conversion (10000 = hectares) |
|
| 191 |
+
|
| 192 |
+
### Example
|
| 193 |
+
|
| 194 |
+
```bash
|
| 195 |
+
curl -X POST "http://localhost:7860/jrc-water-stats" \
|
| 196 |
+
-H "Content-Type: application/json" \
|
| 197 |
+
-d '{
|
| 198 |
+
"bbox": [-90.5, 29.5, -90.0, 30.0],
|
| 199 |
+
"scale": 30,
|
| 200 |
+
"start_month": 5,
|
| 201 |
+
"end_month": 10,
|
| 202 |
+
"frequency": "year"
|
| 203 |
+
}'
|
| 204 |
+
```
|
| 205 |
+
|
| 206 |
+
### JRC Response
|
| 207 |
+
|
| 208 |
+
```json
|
| 209 |
+
{
|
| 210 |
+
"monthly_history": {
|
| 211 |
+
"frequency": "year",
|
| 212 |
+
"unit": "hectares",
|
| 213 |
+
"data": [
|
| 214 |
+
{"Year": "1984", "Area": 123.45},
|
| 215 |
+
{"Year": "1985", "Area": 130.20}
|
| 216 |
+
]
|
| 217 |
+
},
|
| 218 |
+
"water_occurrence": {
|
| 219 |
+
"stats": {
|
| 220 |
+
"mean": 45.2,
|
| 221 |
+
"min": 0,
|
| 222 |
+
"max": 100,
|
| 223 |
+
"stdDev": 28.3
|
| 224 |
+
},
|
| 225 |
+
"histogram": {
|
| 226 |
+
"bin_edges": [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100],
|
| 227 |
+
"counts": [1500, 200, 150, 100, 80, 60, 50, 40, 30, 300]
|
| 228 |
+
}
|
| 229 |
+
},
|
| 230 |
+
"parameters": {
|
| 231 |
+
"bbox": [-90.5, 29.5, -90.0, 30.0],
|
| 232 |
+
"scale": 30,
|
| 233 |
+
"start_date": "1984-03-16",
|
| 234 |
+
"end_date": "2026-03-05",
|
| 235 |
+
"start_month": 5,
|
| 236 |
+
"end_month": 10,
|
| 237 |
+
"frequency": "year"
|
| 238 |
+
}
|
| 239 |
+
}
|
| 240 |
+
```
|
| 241 |
+
|
| 242 |
+
When `frequency` is `"month"`, the `data` array contains `{"Month": "Jan", "Area": ...}` entries instead of `Year`.
|
| 243 |
+
|
| 244 |
### Using with Web Mapping Libraries
|
| 245 |
|
| 246 |
#### Leaflet
|
main.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import os
|
| 2 |
import json
|
|
|
|
| 3 |
import ee
|
| 4 |
import geemap
|
| 5 |
import gradio as gr
|
|
@@ -211,6 +212,19 @@ class TileRequest(BaseModel):
|
|
| 211 |
bbox: list[float] | None = None # [west, south, east, north]
|
| 212 |
|
| 213 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
@app.post("/tile")
|
| 215 |
def get_tile_api(req: TileRequest):
|
| 216 |
result = get_tile(
|
|
@@ -221,6 +235,126 @@ def get_tile_api(req: TileRequest):
|
|
| 221 |
return {"tile_url": result}
|
| 222 |
|
| 223 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
# ---- Gradio UI ----
|
| 225 |
def get_tile_gradio(asset_id, vis_params, start_date, end_date, bbox_str):
|
| 226 |
"""Wrapper for Gradio that converts string inputs to proper types."""
|
|
|
|
| 1 |
import os
|
| 2 |
import json
|
| 3 |
+
import datetime
|
| 4 |
import ee
|
| 5 |
import geemap
|
| 6 |
import gradio as gr
|
|
|
|
| 212 |
bbox: list[float] | None = None # [west, south, east, north]
|
| 213 |
|
| 214 |
|
| 215 |
+
class JRCWaterStatsRequest(BaseModel):
|
| 216 |
+
"""Request model for JRC water statistics endpoint."""
|
| 217 |
+
|
| 218 |
+
bbox: list[float] # [west, south, east, north]
|
| 219 |
+
scale: float | None = 30
|
| 220 |
+
start_date: str | None = "1984-03-16"
|
| 221 |
+
end_date: str | None = None # defaults to today
|
| 222 |
+
start_month: int | None = 1
|
| 223 |
+
end_month: int | None = 12
|
| 224 |
+
frequency: str | None = "year" # "month" or "year"
|
| 225 |
+
denominator: float | None = 10000.0 # m² to hectares
|
| 226 |
+
|
| 227 |
+
|
| 228 |
@app.post("/tile")
|
| 229 |
def get_tile_api(req: TileRequest):
|
| 230 |
result = get_tile(
|
|
|
|
| 235 |
return {"tile_url": result}
|
| 236 |
|
| 237 |
|
| 238 |
+
@app.post("/jrc-water-stats")
|
| 239 |
+
def get_jrc_water_stats(req: JRCWaterStatsRequest):
|
| 240 |
+
"""Compute JRC monthly water history and water occurrence statistics.
|
| 241 |
+
|
| 242 |
+
Args:
|
| 243 |
+
req: Request with bbox, scale, date range, month range, and frequency.
|
| 244 |
+
|
| 245 |
+
Returns:
|
| 246 |
+
dict: Monthly history data and water occurrence statistics.
|
| 247 |
+
"""
|
| 248 |
+
try:
|
| 249 |
+
if len(req.bbox) != 4:
|
| 250 |
+
raise ValueError(
|
| 251 |
+
"bbox must be a list of 4 values: [west, south, east, north]"
|
| 252 |
+
)
|
| 253 |
+
if req.frequency not in ("month", "year"):
|
| 254 |
+
raise ValueError("frequency must be 'month' or 'year'")
|
| 255 |
+
|
| 256 |
+
region = ee.Geometry.BBox(*req.bbox)
|
| 257 |
+
end_date = req.end_date or datetime.date.today().strftime("%Y-%m-%d")
|
| 258 |
+
|
| 259 |
+
# Compute monthly water history from JRC MonthlyHistory
|
| 260 |
+
collection = ee.ImageCollection("JRC/GSW1_4/MonthlyHistory")
|
| 261 |
+
images = (
|
| 262 |
+
collection.filterDate(req.start_date, end_date)
|
| 263 |
+
.filter(ee.Filter.calendarRange(req.start_month, req.end_month, "month"))
|
| 264 |
+
.map(lambda img: img.eq(2).selfMask())
|
| 265 |
+
)
|
| 266 |
+
|
| 267 |
+
def cal_area(img):
|
| 268 |
+
pixel_area = img.multiply(ee.Image.pixelArea()).divide(req.denominator)
|
| 269 |
+
img_area = pixel_area.reduceRegion(
|
| 270 |
+
geometry=region,
|
| 271 |
+
reducer=ee.Reducer.sum(),
|
| 272 |
+
scale=req.scale,
|
| 273 |
+
maxPixels=1e12,
|
| 274 |
+
bestEffort=True,
|
| 275 |
+
)
|
| 276 |
+
return img.set({"area": img_area})
|
| 277 |
+
|
| 278 |
+
areas = images.map(cal_area)
|
| 279 |
+
stats_list = areas.aggregate_array("area").getInfo()
|
| 280 |
+
values = [item["water"] for item in stats_list]
|
| 281 |
+
labels = areas.aggregate_array("system:index").getInfo()
|
| 282 |
+
|
| 283 |
+
if req.frequency == "month":
|
| 284 |
+
history_data = [
|
| 285 |
+
{"Month": label, "Area": area} for label, area in zip(labels, values)
|
| 286 |
+
]
|
| 287 |
+
else:
|
| 288 |
+
# Group by year and compute mean
|
| 289 |
+
year_areas = {}
|
| 290 |
+
for label, area in zip(labels, values):
|
| 291 |
+
year = label[:4]
|
| 292 |
+
year_areas.setdefault(year, []).append(area)
|
| 293 |
+
history_data = [
|
| 294 |
+
{"Year": year, "Area": sum(areas) / len(areas)}
|
| 295 |
+
for year, areas in sorted(year_areas.items())
|
| 296 |
+
]
|
| 297 |
+
|
| 298 |
+
# Compute water occurrence statistics
|
| 299 |
+
occurrence = ee.Image("JRC/GSW1_4/GlobalSurfaceWater").select("occurrence")
|
| 300 |
+
stats = occurrence.reduceRegion(
|
| 301 |
+
reducer=ee.Reducer.mean()
|
| 302 |
+
.combine(ee.Reducer.min(), sharedInputs=True)
|
| 303 |
+
.combine(ee.Reducer.max(), sharedInputs=True)
|
| 304 |
+
.combine(ee.Reducer.stdDev(), sharedInputs=True),
|
| 305 |
+
geometry=region,
|
| 306 |
+
scale=req.scale,
|
| 307 |
+
maxPixels=1e12,
|
| 308 |
+
bestEffort=True,
|
| 309 |
+
).getInfo()
|
| 310 |
+
|
| 311 |
+
# Compute occurrence histogram (10 bins from 0 to 100)
|
| 312 |
+
hist_result = occurrence.reduceRegion(
|
| 313 |
+
reducer=ee.Reducer.fixedHistogram(0, 100, 10),
|
| 314 |
+
geometry=region,
|
| 315 |
+
scale=req.scale,
|
| 316 |
+
maxPixels=1e12,
|
| 317 |
+
bestEffort=True,
|
| 318 |
+
).getInfo()
|
| 319 |
+
|
| 320 |
+
# Parse histogram result
|
| 321 |
+
histogram = {"bin_edges": [], "counts": []}
|
| 322 |
+
if hist_result and "occurrence" in hist_result:
|
| 323 |
+
hist_list = hist_result["occurrence"]
|
| 324 |
+
bin_edges = [row[0] for row in hist_list]
|
| 325 |
+
bin_edges.append(hist_list[-1][0] + 10) # add right edge
|
| 326 |
+
counts = [row[1] for row in hist_list]
|
| 327 |
+
histogram = {"bin_edges": bin_edges, "counts": counts}
|
| 328 |
+
|
| 329 |
+
return {
|
| 330 |
+
"monthly_history": {
|
| 331 |
+
"frequency": req.frequency,
|
| 332 |
+
"unit": "hectares",
|
| 333 |
+
"data": history_data,
|
| 334 |
+
},
|
| 335 |
+
"water_occurrence": {
|
| 336 |
+
"stats": {
|
| 337 |
+
"mean": stats.get("occurrence_mean"),
|
| 338 |
+
"min": stats.get("occurrence_min"),
|
| 339 |
+
"max": stats.get("occurrence_max"),
|
| 340 |
+
"stdDev": stats.get("occurrence_stdDev"),
|
| 341 |
+
},
|
| 342 |
+
"histogram": histogram,
|
| 343 |
+
},
|
| 344 |
+
"parameters": {
|
| 345 |
+
"bbox": req.bbox,
|
| 346 |
+
"scale": req.scale,
|
| 347 |
+
"start_date": req.start_date,
|
| 348 |
+
"end_date": end_date,
|
| 349 |
+
"start_month": req.start_month,
|
| 350 |
+
"end_month": req.end_month,
|
| 351 |
+
"frequency": req.frequency,
|
| 352 |
+
},
|
| 353 |
+
}
|
| 354 |
+
except Exception as e:
|
| 355 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 356 |
+
|
| 357 |
+
|
| 358 |
# ---- Gradio UI ----
|
| 359 |
def get_tile_gradio(asset_id, vis_params, start_date, end_date, bbox_str):
|
| 360 |
"""Wrapper for Gradio that converts string inputs to proper types."""
|
requirements.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
fastapi
|
| 2 |
-
|
| 3 |
gradio
|
| 4 |
uvicorn
|
|
|
|
| 1 |
fastapi
|
| 2 |
+
geemap
|
| 3 |
gradio
|
| 4 |
uvicorn
|