Spaces:
Sleeping
Sleeping
✨ feat(flood): add theme-colored vector flood zones + FEMA proxy
Browse filesReplace raster tile overlay with GeoJSON vector polygons that adapt
colors to the active theme. Add FEMA proxy endpoint to backend
(bypasses CORS). Add loading indicator and zone count in flood panel.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- surgeink/api/fema.py +72 -0
- surgeink/api/router.py +2 -1
surgeink/api/fema.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import httpx
|
| 2 |
+
from fastapi import APIRouter, HTTPException, Query
|
| 3 |
+
|
| 4 |
+
router = APIRouter()
|
| 5 |
+
|
| 6 |
+
FEMA_QUERY_URL = (
|
| 7 |
+
"https://hazards.fema.gov/arcgis/rest/services/public/NFHL/MapServer/28/query"
|
| 8 |
+
)
|
| 9 |
+
MAX_RECORDS = 100
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@router.get("/fema/zones")
|
| 13 |
+
async def get_fema_zones(
|
| 14 |
+
bbox: str = Query(..., description="west,south,east,north"),
|
| 15 |
+
):
|
| 16 |
+
parts = bbox.split(",")
|
| 17 |
+
if len(parts) != 4:
|
| 18 |
+
raise HTTPException(422, detail="bbox must have 4 comma-separated values")
|
| 19 |
+
|
| 20 |
+
all_features = []
|
| 21 |
+
offset = 0
|
| 22 |
+
has_more = True
|
| 23 |
+
|
| 24 |
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
| 25 |
+
while has_more:
|
| 26 |
+
params = {
|
| 27 |
+
"geometry": bbox,
|
| 28 |
+
"geometryType": "esriGeometryEnvelope",
|
| 29 |
+
"inSR": "4326",
|
| 30 |
+
"outSR": "4326",
|
| 31 |
+
"spatialRel": "esriSpatialRelIntersects",
|
| 32 |
+
"outFields": "FLD_ZONE,ZONE_SUBTY",
|
| 33 |
+
"returnGeometry": "true",
|
| 34 |
+
"resultOffset": str(offset),
|
| 35 |
+
"resultRecordCount": str(MAX_RECORDS),
|
| 36 |
+
"f": "json",
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
resp = await client.get(FEMA_QUERY_URL, params=params)
|
| 40 |
+
if resp.status_code != 200:
|
| 41 |
+
raise HTTPException(502, detail=f"FEMA returned {resp.status_code}")
|
| 42 |
+
|
| 43 |
+
data = resp.json()
|
| 44 |
+
|
| 45 |
+
if "error" in data:
|
| 46 |
+
raise HTTPException(502, detail=f"FEMA error: {data['error']}")
|
| 47 |
+
|
| 48 |
+
features = data.get("features", [])
|
| 49 |
+
|
| 50 |
+
for f in features:
|
| 51 |
+
geom = f.get("geometry", {})
|
| 52 |
+
rings = geom.get("rings")
|
| 53 |
+
if not rings:
|
| 54 |
+
continue
|
| 55 |
+
all_features.append({
|
| 56 |
+
"type": "Feature",
|
| 57 |
+
"properties": f.get("attributes", {}),
|
| 58 |
+
"geometry": {
|
| 59 |
+
"type": "Polygon",
|
| 60 |
+
"coordinates": rings,
|
| 61 |
+
},
|
| 62 |
+
})
|
| 63 |
+
|
| 64 |
+
if len(features) < MAX_RECORDS:
|
| 65 |
+
has_more = False
|
| 66 |
+
else:
|
| 67 |
+
offset += MAX_RECORDS
|
| 68 |
+
|
| 69 |
+
return {
|
| 70 |
+
"type": "FeatureCollection",
|
| 71 |
+
"features": all_features,
|
| 72 |
+
}
|
surgeink/api/router.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
from fastapi import APIRouter
|
| 2 |
|
| 3 |
-
from surgeink.api import geocode, forecast, layers, risk, tiles, predict, interpret
|
| 4 |
|
| 5 |
router = APIRouter(prefix="/api/v1")
|
| 6 |
|
|
@@ -11,3 +11,4 @@ router.include_router(risk.router, tags=["risk"])
|
|
| 11 |
router.include_router(tiles.router, tags=["tiles"])
|
| 12 |
router.include_router(predict.router, tags=["predict"])
|
| 13 |
router.include_router(interpret.router, tags=["interpret"])
|
|
|
|
|
|
| 1 |
from fastapi import APIRouter
|
| 2 |
|
| 3 |
+
from surgeink.api import geocode, forecast, layers, risk, tiles, predict, interpret, fema
|
| 4 |
|
| 5 |
router = APIRouter(prefix="/api/v1")
|
| 6 |
|
|
|
|
| 11 |
router.include_router(tiles.router, tags=["tiles"])
|
| 12 |
router.include_router(predict.router, tags=["predict"])
|
| 13 |
router.include_router(interpret.router, tags=["interpret"])
|
| 14 |
+
router.include_router(fema.router, tags=["fema"])
|