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
|
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:
|
def _f(value: object) -> float | None:
|
||||||
if value is None or value == "":
|
if value is None or value == "":
|
||||||
return None
|
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:
|
async def fetch_weather_alerts(area: str | None, bbox: str | None) -> dict:
|
||||||
"""Cached NWS active alerts + IEM storm-based warning polygons."""
|
"""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():
|
async def _load():
|
||||||
nws_params: dict[str, str] = {"status": "actual"}
|
nws_params: dict[str, str] = {"status": "actual"}
|
||||||
|
clip_box = None
|
||||||
if area:
|
if area:
|
||||||
nws_params["area"] = area.upper()
|
nws_params["area"] = area.upper()
|
||||||
elif bbox:
|
elif bbox:
|
||||||
minlon, minlat, maxlon, maxlat = quantize_bbox(*parse_bbox(bbox))
|
clip_box = quantize_bbox(*parse_bbox(bbox))
|
||||||
nws_params["bbox"] = f"{minlon},{minlat},{maxlon},{maxlat}"
|
nws_params["bbox"] = f"{clip_box[0]},{clip_box[1]},{clip_box[2]},{clip_box[3]}"
|
||||||
nws_fc: dict = {"features": []}
|
nws_fc: dict = {"features": []}
|
||||||
sbw_fc: dict = {"features": []}
|
|
||||||
try:
|
try:
|
||||||
nws_fc = await _get_json(NWS_ALERTS, nws_params)
|
nws_fc = await _get_json(NWS_ALERTS, nws_params)
|
||||||
except Exception:
|
except Exception:
|
||||||
nws_fc = {"features": []}
|
nws_fc = {"features": []}
|
||||||
try:
|
sbw_fc = await _ttl_get("iem:sbw", 45.0, _load_iem)
|
||||||
sbw_fc = await _get_json(IEM_SBW)
|
|
||||||
except Exception:
|
|
||||||
sbw_fc = {"features": []}
|
|
||||||
features = []
|
features = []
|
||||||
for feat in nws_fc.get("features") or []:
|
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"
|
props["source"] = "nws"
|
||||||
feat["properties"] = props
|
features.append({**feat, "properties": slim_alert_properties(props)})
|
||||||
features.append(feat)
|
for feat in (sbw_fc or {}).get("features") or []:
|
||||||
for feat in sbw_fc.get("features") or []:
|
if not isinstance(feat, dict):
|
||||||
props = feat.get("properties") or {}
|
continue
|
||||||
|
props = dict(feat.get("properties") or {})
|
||||||
props["source"] = "iem-sbw"
|
props["source"] = "iem-sbw"
|
||||||
# IEM uses `ps` (phenomenon) / `wfo`; map a display event.
|
|
||||||
if "event" not in props:
|
if "event" not in props:
|
||||||
props["event"] = props.get("ps") or "Storm-based warning"
|
props["event"] = props.get("ps") or "Storm-based warning"
|
||||||
feat["properties"] = props
|
features.append({**feat, "properties": slim_alert_properties(props)})
|
||||||
if bbox:
|
merged = {"type": "FeatureCollection", "features": features}
|
||||||
# Cheap reject: skip if no geometry; keep otherwise (polygons).
|
if clip_box:
|
||||||
if not feat.get("geometry"):
|
merged = clip_fc_to_bbox(merged, *clip_box)
|
||||||
continue
|
elif bbox:
|
||||||
features.append(feat)
|
merged = clip_fc_to_bbox(merged, *parse_bbox(bbox))
|
||||||
return {"type": "FeatureCollection", "features": features}
|
return merged
|
||||||
|
|
||||||
key = f"alerts:{area or ''}:{bbox_cell_key(bbox) if bbox else ''}"
|
key = f"alerts:{area or ''}:{bbox_cell_key(bbox) if bbox else ''}"
|
||||||
return await _ttl_get(key, 30.0, _load)
|
return await _ttl_get(key, 30.0, _load)
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,12 @@
|
||||||
from live_layers import (
|
from live_layers import (
|
||||||
MARKER_FIELDS,
|
MARKER_FIELDS,
|
||||||
bbox_center_radius_nm,
|
bbox_center_radius_nm,
|
||||||
|
clip_fc_to_bbox,
|
||||||
filter_points_bbox,
|
filter_points_bbox,
|
||||||
parse_bbox,
|
parse_bbox,
|
||||||
quantize_bbox,
|
quantize_bbox,
|
||||||
rainviewer_tile_url,
|
rainviewer_tile_url,
|
||||||
|
slim_alert_properties,
|
||||||
to_marker,
|
to_marker,
|
||||||
transform_adsb_lol,
|
transform_adsb_lol,
|
||||||
transform_ais_frame,
|
transform_ais_frame,
|
||||||
|
|
@ -15,6 +17,7 @@ from live_layers import (
|
||||||
transform_wfigs_incidents,
|
transform_wfigs_incidents,
|
||||||
_cache,
|
_cache,
|
||||||
_ttl_get,
|
_ttl_get,
|
||||||
|
_wfigs_params,
|
||||||
)
|
)
|
||||||
|
|
||||||
from camera_scraper import parse_caltrans_json
|
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["slow"][1] == "S"
|
||||||
assert _cache["fast"][1] == "F"
|
assert _cache["fast"][1] == "F"
|
||||||
_cache.clear()
|
_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