88 lines
2.7 KiB
Python
88 lines
2.7 KiB
Python
"""FEMA NFHL flood zone lookup por lat/lng.
|
|
|
|
API publica: https://hazards.fema.gov/gis/nfhl/rest/services/public/NFHL/MapServer
|
|
Layer 28 = "S_FLD_HAZ_AR" (Special Flood Hazard Areas).
|
|
|
|
Sin key requerida. Sin rate limits estrictos.
|
|
|
|
Devuelve dict con:
|
|
zone: "X" / "X (shaded)" / "A" / "AE" / "AH" / "AO" / "V" / "VE" / etc.
|
|
bfe: Base Flood Elevation (ft) o None
|
|
sfha: bool - True si esta en Special Flood Hazard Area
|
|
subtype: subzone description o None
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import requests
|
|
|
|
from .base import FetcherError, DEFAULT_TIMEOUT
|
|
|
|
|
|
FEMA_URL = "https://hazards.fema.gov/arcgis/rest/services/public/NFHL/MapServer/28/query"
|
|
|
|
# Zonas que son SFHA (Special Flood Hazard Area) segun FEMA
|
|
SFHA_ZONES = {"A", "AE", "AH", "AO", "AR", "A99", "V", "VE", "VO"}
|
|
|
|
|
|
def fetch_flood(lat: float, lng: float) -> dict:
|
|
"""Consulta FEMA NFHL para flood zone en (lat, lng).
|
|
|
|
Si el punto NO esta en ninguna SFHA, FEMA devuelve features vacio
|
|
y se interpreta como zona X (low risk, default outside SFHA).
|
|
"""
|
|
if lat is None or lng is None:
|
|
raise FetcherError("lat/lng requeridos")
|
|
|
|
params = {
|
|
"geometry": f"{lng},{lat}", # FEMA usa lng,lat (x,y)
|
|
"geometryType": "esriGeometryPoint",
|
|
"inSR": "4326", # WGS84
|
|
"spatialRel": "esriSpatialRelIntersects",
|
|
"outFields": "FLD_ZONE,STATIC_BFE,ZONE_SUBTY",
|
|
"returnGeometry": "false",
|
|
"f": "json",
|
|
}
|
|
|
|
try:
|
|
r = requests.get(FEMA_URL, params=params, timeout=DEFAULT_TIMEOUT)
|
|
r.raise_for_status()
|
|
except requests.RequestException as e:
|
|
raise FetcherError(f"HTTP error: {e}") from e
|
|
|
|
try:
|
|
data = r.json()
|
|
except ValueError as e:
|
|
raise FetcherError(f"JSON parse error: {e}") from e
|
|
|
|
# FEMA puede devolver "error" si la query es invalida
|
|
if "error" in data:
|
|
raise FetcherError(f"FEMA API error: {data['error']}")
|
|
|
|
features = data.get("features", [])
|
|
if not features:
|
|
# Punto fuera de SFHA → low-risk zone X
|
|
return {
|
|
"zone": "X",
|
|
"bfe": None,
|
|
"sfha": False,
|
|
"subtype": None,
|
|
"source": "FEMA NFHL (outside SFHA)",
|
|
}
|
|
|
|
attrs = features[0].get("attributes", {}) or {}
|
|
zone = (attrs.get("FLD_ZONE") or "unknown").strip()
|
|
subtype = attrs.get("ZONE_SUBTY")
|
|
|
|
# BFE: FEMA usa -9999 para "no aplica"
|
|
bfe_raw = attrs.get("STATIC_BFE")
|
|
bfe = bfe_raw if (bfe_raw is not None and bfe_raw != -9999) else None
|
|
|
|
return {
|
|
"zone": zone,
|
|
"bfe": bfe,
|
|
"sfha": zone in SFHA_ZONES,
|
|
"subtype": subtype,
|
|
"source": "FEMA NFHL",
|
|
}
|