From fdd59052c56c1b617c7774cb7cfd32d33dd14290 Mon Sep 17 00:00:00 2001 From: Sirius DevOps Date: Thu, 27 Aug 2026 21:16:16 -0400 Subject: [PATCH] perf: cache IEM SBW globally, clip alerts to viewport, slim properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit National storm-based warnings are fetched once (45s TTL) and clipped to the quantized bbox. Popup fields only — NWS descriptions stay off the wire. --- app/live_layers.py | 118 +++++++++++++++++++++++++++++++------- tests/test_live_layers.py | 64 +++++++++++++++++++++ 2 files changed, 162 insertions(+), 20 deletions(-) diff --git a/app/live_layers.py b/app/live_layers.py index fc24256..9dce0b5 100644 --- a/app/live_layers.py +++ b/app/live_layers.py @@ -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) diff --git a/tests/test_live_layers.py b/tests/test_live_layers.py index ba999d1..bb6f183 100644 --- a/tests/test_live_layers.py +++ b/tests/test_live_layers.py @@ -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", + }