Weather map layers: NWS warning polygons, n0q radar with valid time/legend/loop, wind at incident (#237)

- Weather Warnings overlay (layer key `wx`, saved in the view hash): NWS alert
  polygons coloured by event (NWS hazard-map colours), outline dash by kind,
  popup with event/headline/expiry/office and National Weather Service credit.
  Polls GET /weather/alerts every 2 min while on and visible, refetches on pan.
  A status chip always says what the layer shows; feed errors use the 511
  FeedProblem wording; zone-only alerts are listed, never dropped.
- Radar: IEM nexrad-n0r -> n0q. Valid-time badge from IEM's n0q_0.json,
  STALE past 15 min or when the time can't be read; dBZ legend; optional 1 h
  loop (nexrad-n0q-900913-m05m..m55m), off by default. Still refreshed in
  place, never remounted.
- liveWall: sampleRadarDbz reads the n0q tile with IEM's published 255-colour
  ramp (verified against live tiles); pointInPolygon/alertsAt, wxStyle,
  wantsWind helpers.
- Wall incident panel: "inside <warning>" chips, wind arrow + speed/gust +
  station/obs time on fire/hazmat incidents; region Feeds gains an NWS row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Logan Cusano
2026-10-05 03:46:05 -04:00
co-authored by Claude Opus 5.5
parent 1d6e79ba25
commit 1a84bf46d7
6 changed files with 864 additions and 44 deletions
+71
View File
@@ -319,6 +319,77 @@ export interface Ny511Response {
credit?: CameraCredit;
}
// ── Weather context (server-26#237) — GET /weather/alerts, GET /weather/wind ──
/** GeoJSON (Multi)Polygon, [lon, lat] order. */
export type WeatherGeometry =
| { type: "Polygon"; coordinates: number[][][] }
| { type: "MultiPolygon"; coordinates: number[][][][] };
/** One active NWS alert as c2-core normalises it (app/internal/weather.py normalize_alert). */
export interface WeatherAlertBase {
id: string;
event: string;
severity: string | null;
urgency: string | null;
headline: string | null;
onset: string | null;
expires: string | null;
ends: string | null;
/** An NWS office ("NWS Upton NY"), never a person. */
sender_name: string | null;
area_desc: string | null;
zones: string[];
}
export interface WeatherAlert extends WeatherAlertBase {
geometry: WeatherGeometry;
/** "alert": NWS drew the polygon; "zones": assembled from the affected forecast/county zones. */
geometry_source: "alert" | "zones";
/** [south, west, north, east]. */
bbox: [number, number, number, number] | null;
/** >0: some zones could not be fetched, so the drawn area is smaller than the real one. */
zones_unresolved?: number;
}
export interface WeatherAlertsResponse {
alerts: WeatherAlert[];
/** Alerts with no resolvable shape — listed, never dropped, never bbox-filtered. */
zone_only: WeatherAlertBase[];
/** Epoch s of the server's last SUCCESSFUL NWS fetch; null = never. */
fetched_at: number | null;
/** Set when the latest refresh failed; `alerts` is then the last good data. */
error: string | null;
credit: string;
}
export interface WindObs {
station: string;
station_name: string;
lat: number;
lon: number;
obs_time: string | null;
/** Direction the wind blows FROM, degrees true; null when variable or calm. */
wind_dir_deg: number | null;
variable: boolean;
wind_kt: number;
gust_kt: number | null;
raw: string | null;
distance_km: number;
/** Report older than 2 h. */
stale: boolean;
}
export interface WindResponse {
available: boolean;
/** Why there is no wind, when `available` is false. */
reason: string | null;
wind: WindObs | null;
fetched_at: number | null;
error: string | null;
credit: string;
}
export interface VocabularyPendingTerm {
term: string;
source: "induction" | "correction";