perf: faster map overlay load #4
2 changed files with 162 additions and 20 deletions
|
|
@ -206,6 +206,78 @@ def filter_points_bbox(
|
|||
return out
|
||||
|
||||
|
||||
_ALERT_KEEP = ("event", "severity", "headline", "areaDesc", "wfo", "source")
|
||||
|
||||
|
||||
def slim_alert_properties(props: dict | None) -> dict:
|
||||
"""Keep only the fields the map popup reads."""
|
||||
src = props or {}
|
||||
return {k: src.get(k) for k in _ALERT_KEEP}
|
||||
|
||||
|
||||
def _walk_coords(coords: Any, acc: list[float]) -> None:
|
||||
if not coords:
|
||||
return
|
||||
first = coords[0]
|
||||
if isinstance(first, (int, float)):
|
||||
lon, lat = float(coords[0]), float(coords[1])
|
||||
acc[0] = min(acc[0], lon)
|
||||
acc[1] = min(acc[1], lat)
|
||||
acc[2] = max(acc[2], lon)
|
||||
acc[3] = max(acc[3], lat)
|
||||
return
|
||||
for child in coords:
|
||||
_walk_coords(child, acc)
|
||||
|
||||
|
||||
def _geom_envelope(geom: dict | None) -> tuple[float, float, float, float] | None:
|
||||
if not geom or not isinstance(geom, dict):
|
||||
return None
|
||||
if geom.get("type") == "GeometryCollection":
|
||||
env: list[float] | None = None
|
||||
for g in geom.get("geometries") or []:
|
||||
e = _geom_envelope(g)
|
||||
if e is None:
|
||||
continue
|
||||
if env is None:
|
||||
env = list(e)
|
||||
else:
|
||||
env[0] = min(env[0], e[0])
|
||||
env[1] = min(env[1], e[1])
|
||||
env[2] = max(env[2], e[2])
|
||||
env[3] = max(env[3], e[3])
|
||||
return tuple(env) if env else None # type: ignore[return-value]
|
||||
coords = geom.get("coordinates")
|
||||
if coords is None:
|
||||
return None
|
||||
acc = [180.0, 90.0, -180.0, -90.0]
|
||||
try:
|
||||
_walk_coords(coords, acc)
|
||||
except (TypeError, ValueError, IndexError):
|
||||
return None
|
||||
if acc[0] > acc[2]:
|
||||
return None
|
||||
return acc[0], acc[1], acc[2], acc[3]
|
||||
|
||||
|
||||
def clip_fc_to_bbox(
|
||||
fc: dict | None,
|
||||
minlon: float, minlat: float, maxlon: float, maxlat: float,
|
||||
) -> dict:
|
||||
"""Drop features whose geometry envelope misses the viewport. No shapely."""
|
||||
box = (minlon, minlat, maxlon, maxlat)
|
||||
out = []
|
||||
for feat in (fc or {}).get("features") or []:
|
||||
if not isinstance(feat, dict):
|
||||
continue
|
||||
env = _geom_envelope(feat.get("geometry"))
|
||||
if env is None:
|
||||
continue
|
||||
if env[0] <= box[2] and env[2] >= box[0] and env[1] <= box[3] and env[3] >= box[1]:
|
||||
out.append(feat)
|
||||
return {"type": "FeatureCollection", "features": out}
|
||||
|
||||
|
||||
def _f(value: object) -> float | None:
|
||||
if value is None or value == "":
|
||||
return None
|
||||
|
|
@ -574,42 +646,48 @@ async def fetch_fire_perimeters(bbox: str | None) -> dict:
|
|||
async def fetch_weather_alerts(area: str | None, bbox: str | None) -> dict:
|
||||
"""Cached NWS active alerts + IEM storm-based warning polygons."""
|
||||
|
||||
async def _load_iem():
|
||||
try:
|
||||
data = await _get_json(IEM_SBW)
|
||||
except Exception:
|
||||
return {"type": "FeatureCollection", "features": []}
|
||||
return data if isinstance(data, dict) else {"type": "FeatureCollection", "features": []}
|
||||
|
||||
async def _load():
|
||||
nws_params: dict[str, str] = {"status": "actual"}
|
||||
clip_box = None
|
||||
if area:
|
||||
nws_params["area"] = area.upper()
|
||||
elif bbox:
|
||||
minlon, minlat, maxlon, maxlat = quantize_bbox(*parse_bbox(bbox))
|
||||
nws_params["bbox"] = f"{minlon},{minlat},{maxlon},{maxlat}"
|
||||
clip_box = quantize_bbox(*parse_bbox(bbox))
|
||||
nws_params["bbox"] = f"{clip_box[0]},{clip_box[1]},{clip_box[2]},{clip_box[3]}"
|
||||
nws_fc: dict = {"features": []}
|
||||
sbw_fc: dict = {"features": []}
|
||||
try:
|
||||
nws_fc = await _get_json(NWS_ALERTS, nws_params)
|
||||
except Exception:
|
||||
nws_fc = {"features": []}
|
||||
try:
|
||||
sbw_fc = await _get_json(IEM_SBW)
|
||||
except Exception:
|
||||
sbw_fc = {"features": []}
|
||||
sbw_fc = await _ttl_get("iem:sbw", 45.0, _load_iem)
|
||||
features = []
|
||||
for feat in nws_fc.get("features") or []:
|
||||
props = feat.get("properties") or {}
|
||||
if not isinstance(feat, dict):
|
||||
continue
|
||||
props = dict(feat.get("properties") or {})
|
||||
props["source"] = "nws"
|
||||
feat["properties"] = props
|
||||
features.append(feat)
|
||||
for feat in sbw_fc.get("features") or []:
|
||||
props = feat.get("properties") or {}
|
||||
features.append({**feat, "properties": slim_alert_properties(props)})
|
||||
for feat in (sbw_fc or {}).get("features") or []:
|
||||
if not isinstance(feat, dict):
|
||||
continue
|
||||
props = dict(feat.get("properties") or {})
|
||||
props["source"] = "iem-sbw"
|
||||
# IEM uses `ps` (phenomenon) / `wfo`; map a display event.
|
||||
if "event" not in props:
|
||||
props["event"] = props.get("ps") or "Storm-based warning"
|
||||
feat["properties"] = props
|
||||
if bbox:
|
||||
# Cheap reject: skip if no geometry; keep otherwise (polygons).
|
||||
if not feat.get("geometry"):
|
||||
continue
|
||||
features.append(feat)
|
||||
return {"type": "FeatureCollection", "features": features}
|
||||
features.append({**feat, "properties": slim_alert_properties(props)})
|
||||
merged = {"type": "FeatureCollection", "features": features}
|
||||
if clip_box:
|
||||
merged = clip_fc_to_bbox(merged, *clip_box)
|
||||
elif bbox:
|
||||
merged = clip_fc_to_bbox(merged, *parse_bbox(bbox))
|
||||
return merged
|
||||
|
||||
key = f"alerts:{area or ''}:{bbox_cell_key(bbox) if bbox else ''}"
|
||||
return await _ttl_get(key, 30.0, _load)
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@
|
|||
from live_layers import (
|
||||
MARKER_FIELDS,
|
||||
bbox_center_radius_nm,
|
||||
clip_fc_to_bbox,
|
||||
filter_points_bbox,
|
||||
parse_bbox,
|
||||
quantize_bbox,
|
||||
rainviewer_tile_url,
|
||||
slim_alert_properties,
|
||||
to_marker,
|
||||
transform_adsb_lol,
|
||||
transform_ais_frame,
|
||||
|
|
@ -15,6 +17,7 @@ from live_layers import (
|
|||
transform_wfigs_incidents,
|
||||
_cache,
|
||||
_ttl_get,
|
||||
_wfigs_params,
|
||||
)
|
||||
|
||||
from camera_scraper import parse_caltrans_json
|
||||
|
|
@ -287,3 +290,64 @@ def test_ttl_get_does_not_block_other_keys():
|
|||
assert _cache["slow"][1] == "S"
|
||||
assert _cache["fast"][1] == "F"
|
||||
_cache.clear()
|
||||
|
||||
|
||||
def test_clip_fc_to_bbox_drops_far_features_and_empty_geometry():
|
||||
fc = {
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {"event": "near"},
|
||||
"geometry": {"type": "Point", "coordinates": [-78.7, 35.8]},
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {"event": "far"},
|
||||
"geometry": {"type": "Point", "coordinates": [-120.0, 45.0]},
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {"event": "nogeom"},
|
||||
"geometry": None,
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {"event": "poly-overlap"},
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [[
|
||||
[-79.0, 35.0], [-78.0, 35.0], [-78.0, 36.0],
|
||||
[-79.0, 36.0], [-79.0, 35.0],
|
||||
]],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
clipped = clip_fc_to_bbox(fc, -79.0, 35.5, -78.0, 36.0)
|
||||
events = [f["properties"]["event"] for f in clipped["features"]]
|
||||
assert events == ["near", "poly-overlap"]
|
||||
|
||||
|
||||
def test_slim_alert_properties_keeps_popup_fields_only():
|
||||
fat = {
|
||||
"event": "Tornado Warning",
|
||||
"severity": "Extreme",
|
||||
"headline": "TORNADO WARNING",
|
||||
"areaDesc": "Wake",
|
||||
"wfo": "RAH",
|
||||
"source": "nws",
|
||||
"parameters": {"WIND": ["70"]},
|
||||
"description": "A long narrative " * 40,
|
||||
"instruction": "Take shelter.",
|
||||
"geocode": {"SAME": ["037183"]},
|
||||
}
|
||||
slim = slim_alert_properties(fat)
|
||||
assert slim == {
|
||||
"event": "Tornado Warning",
|
||||
"severity": "Extreme",
|
||||
"headline": "TORNADO WARNING",
|
||||
"areaDesc": "Wake",
|
||||
"wfo": "RAH",
|
||||
"source": "nws",
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue