Power Outages map layer + wires-down cross-match on the live wall (#184)
- lib/useOutages.ts: GET /outages for the map view (pan/zoom debounced) or a fixed bbox; polls every 3 min while visible, refetches when a hidden tab returns. - components/map/OutageLayer.tsx: county markers at the label point sized by meters out, stale-only counties dimmed/dashed; popup with count, % served when known, since/ETR, per-utility counts, SourceCredit + fetch time, and "county centre, not the outage location". An always-on status chip says what the layer shows: loading, N counties, none reported in view (with the source's coverage note), no source enabled (admin-only link to /admin#outages), no licensed source covers this area, or feed error. - MapView: "Power Outages" overlay, off by default, plus a legend entry while it is on. Not hash-persisted yet: LAYER_NAMES lives in lib/liveWall.ts, which #235 owns this batch. - lib/outageMatch.ts (pure): county data matches by county bbox and says "county has N out" ("in or near" when two bboxes hold the point); point data matches within 500 m; stale rows never count as live; flags wires-down/transformer/manhole/pole/tree-on-wires incidents. - WallPanels: OUTAGE lines under the incident's situational context (hazard chip, source credit, fetch time) and an OUTAGE row in the region Feeds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f3b21cd432
commit
d10c2f732a
@@ -33,6 +33,7 @@ import {
|
||||
incidentPoints, layerKeyForName, normaliseLayers, roundViewport, viewshedWedge, worstFirst,
|
||||
type BaseKey, type LayerKey, type MapViewport,
|
||||
} from "@/lib/liveWall";
|
||||
import { OutageLayer, outageGlyphSvg } from "@/components/map/OutageLayer";
|
||||
import { PlayButton } from "@/components/InlinePlayer";
|
||||
import { hasAudio, useAudio } from "@/lib/audioQueue";
|
||||
|
||||
@@ -353,6 +354,11 @@ function RadarLayer() {
|
||||
const OVERLAY_DOT_CAMERAS = LAYER_NAMES.dotcam;
|
||||
const OVERLAY_TRAFFIC_EVENTS = LAYER_NAMES.traffic;
|
||||
const OVERLAY_VIEWSHEDS = LAYER_NAMES.vs;
|
||||
// Power outages (server-26#184). Not in LAYER_NAMES yet — lib/liveWall.ts was
|
||||
// owned by #235 when this landed — so it isn't saved in the URL hash and always
|
||||
// starts off. Adding `outage: "Power Outages"` to LAYER_NAMES and `checked=
|
||||
// {layerOn("outage")}` below makes it persist like the rest.
|
||||
const OVERLAY_OUTAGES = "Power Outages";
|
||||
|
||||
/**
|
||||
* Overlay names checked when the map mounts (server-26#234 saved views).
|
||||
@@ -1171,13 +1177,20 @@ export default function MapView({
|
||||
const startOverlayNames = useMemo(() => new Set(Array.from(startLayers).map((k) => LAYER_NAMES[k])), [startLayers]);
|
||||
const layerOn = (k: LayerKey) => startLayers.has(k);
|
||||
const [aircraftShown, setAircraftShown] = useState(() => startLayers.has("air"));
|
||||
const [outagesShown, setOutagesShown] = useState(false);
|
||||
|
||||
// The altitude key only belongs in the legend while the opt-in Aircraft
|
||||
// overlay is actually on (server-26#185).
|
||||
// overlay is actually on (server-26#185); same for the outage key (#184).
|
||||
useEffect(() => {
|
||||
if (!mapInstance) return;
|
||||
const on = (e: L.LayersControlEvent) => e.name === LAYER_NAMES.air && setAircraftShown(true);
|
||||
const off = (e: L.LayersControlEvent) => e.name === LAYER_NAMES.air && setAircraftShown(false);
|
||||
const on = (e: L.LayersControlEvent) => {
|
||||
if (e.name === LAYER_NAMES.air) setAircraftShown(true);
|
||||
if (e.name === OVERLAY_OUTAGES) setOutagesShown(true);
|
||||
};
|
||||
const off = (e: L.LayersControlEvent) => {
|
||||
if (e.name === LAYER_NAMES.air) setAircraftShown(false);
|
||||
if (e.name === OVERLAY_OUTAGES) setOutagesShown(false);
|
||||
};
|
||||
mapInstance.on("overlayadd", on);
|
||||
mapInstance.on("overlayremove", off);
|
||||
return () => {
|
||||
@@ -1393,6 +1406,13 @@ export default function MapView({
|
||||
</FeatureGroup>
|
||||
</LayersControl.Overlay>
|
||||
|
||||
{/* Overlay: Power outages (server-26#184) — licence-gated providers, opt-in */}
|
||||
<LayersControl.Overlay checked={false} name={OVERLAY_OUTAGES}>
|
||||
<FeatureGroup>
|
||||
<OutageLayer shown={outagesShown} />
|
||||
</FeatureGroup>
|
||||
</LayersControl.Overlay>
|
||||
|
||||
{/* Overlay: Weather Radar — NEXRAD via Iowa Env Mesonet */}
|
||||
<LayersControl.Overlay checked={layerOn("radar")} name={LAYER_NAMES.radar}>
|
||||
<RadarLayer />
|
||||
@@ -1463,6 +1483,19 @@ export default function MapView({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{outagesShown && (
|
||||
<div className="border-t border-line pt-1.5 space-y-1">
|
||||
<p className="text-ink-muted font-medium text-[10px] uppercase tracking-wide">Power outages</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<span style={{ width: 16, display: "inline-flex", justifyContent: "center" }} dangerouslySetInnerHTML={{ __html: outageGlyphSvg(14) }} />
|
||||
<span className="text-ink-2">County with outages · size = meters out</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span style={{ width: 16, display: "inline-flex", justifyContent: "center", opacity: 0.6 }} dangerouslySetInnerHTML={{ __html: outageGlyphSvg(14, true) }} />
|
||||
<span className="text-ink-2">Stale upstream rows only</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="border-t border-line pt-1.5 space-y-1">
|
||||
<p className="text-ink-muted font-medium text-[10px] uppercase tracking-wide">Nodes</p>
|
||||
{([
|
||||
|
||||
Reference in New Issue
Block a user