/** * Map the FSM register specialists' `final` event payload into the * `RegisterData` shape consumed by `evidence/RegisterCard.svelte` * (v0.4.2 §15). * * Each register specialist (`mta_entrances`, `nycha_developments`, * `doe_schools`, `doh_hospitals`) returns a Python dict like: * { available: true, n_: int, radius_m: int, * entrances|developments|schools|hospitals: [ * { station_name|development|school_name|facility_name, ... } * ] } * * The shapes diverge a bit per asset class (NYCHA polygons have * `pct_inside_sandy_2012` percentages; subway entrances have boolean * `inside_sandy_2012` per entrance). We normalise each one into a * common row shape that matches the spec page's `SUBWAY_REGISTER` * worked example so the table renders consistently across asset classes. */ import type { AssetKind, RegisterData, RegisterRow } from '$lib/types/states'; interface BaseFinding { [k: string]: unknown; } const SOURCE_LABEL: Record = { subway: 'MTA · USGS · FEMA · NYC OEM · NYC DEP', nycha: 'NYC HA · USGS · NYC OEM · NYC DEP', school: 'NYC DOE · USGS · NYC OEM · NYC DEP', hospital: 'NYS DOH · USGS · NYC OEM · NYC DEP' }; const TYPE_LABEL: Record = { subway: 'subway entrances', nycha: 'NYCHA developments', school: 'public schools', hospital: 'hospitals' }; function metersToLabel(m: number | undefined): string { if (!m || !Number.isFinite(m)) return '—'; return `${Math.round(m)}m`; } function feetLabel(elev_m: number | null | undefined): string { if (elev_m == null || !Number.isFinite(elev_m)) return '—'; return `${(elev_m * 3.28084).toFixed(1)} ft`; } function inundLabel(inside: boolean | undefined, pct?: number | null | undefined): string { if (typeof pct === 'number') { if (pct >= 0.5) return `Inundated 2012 (${Math.round(pct * 100)}%)`; if (pct > 0) return `Edge (${Math.round(pct * 100)}%)`; return '—'; } return inside ? 'Inundated 2012' : '—'; } function depLabel(label: string | null | undefined, classNum?: number | null | undefined, pct?: number | null | undefined): string { if (typeof pct === 'number') { if (pct >= 0.5) return `≥${Math.round(pct * 100)}% in scenario`; if (pct > 0) return `${Math.round(pct * 100)}% edge`; return 'minimal'; } if (label && label.length) return label; if (classNum && classNum > 0) return `class ${classNum}`; return 'minimal'; } function adaFromEntranceType(t: string | undefined): boolean { if (!t) return false; // Same set as ADA_ACCESSIBLE_TYPES in app/registers/mta_entrances.py. return /elevator|easement|stair.*ramp/i.test(t); } /* ── per-asset adapters ───────────────────────────────────────────────── */ function adaptSubway(s: BaseFinding): RegisterData | null { if (!s.available) return null; const list = (s.entrances ?? []) as BaseFinding[]; const rows: RegisterRow[] = list.map((e) => { const ada = adaFromEntranceType(e.entrance_type as string | undefined); return { name: `${e.station_name ?? '?'}${e.daytime_routes ? ` (${String(e.daytime_routes).split(/\s+/).slice(0, 3).join('/')})` : ''}`, elev: feetLabel(e.elev_m as number | null | undefined), ada, fema: 'Zone X', sandy: inundLabel(e.inside_sandy_2012 as boolean | undefined), dep: depLabel( e.dep_extreme_2080_label as string | null | undefined, e.dep_extreme_2080_class as number | null | undefined ), asset: 'subway', primaryTier: e.inside_sandy_2012 ? 'empirical' : 'modeled' }; }); return { type: TYPE_LABEL.subway, radius: metersToLabel(s.radius_m as number | undefined), count: (s.n_entrances as number | undefined) ?? rows.length, rows, sourceLabel: SOURCE_LABEL.subway }; } function adaptNycha(s: BaseFinding): RegisterData | null { if (!s.available) return null; const list = (s.developments ?? []) as BaseFinding[]; const rows: RegisterRow[] = list.map((d) => { const inSandy = d.inside_sandy_2012 as boolean | undefined; const depLbl = d.dep_extreme_2080_label as string | null | undefined; const depCls = d.dep_extreme_2080_class as number | null | undefined; return { name: `${d.development ?? '?'}${d.borough ? ` · ${d.borough}` : ''}`, elev: feetLabel(d.rep_elevation_m as number | null | undefined), ada: false, // NYCHA developments don't carry an ADA flag fema: '—', sandy: inundLabel(inSandy), dep: depLabel(depLbl, depCls), asset: 'nycha', primaryTier: inSandy ? 'empirical' : 'modeled' }; }); return { type: TYPE_LABEL.nycha, radius: metersToLabel(s.radius_m as number | undefined), count: (s.n_developments as number | undefined) ?? rows.length, rows, sourceLabel: SOURCE_LABEL.nycha }; } function adaptSchools(s: BaseFinding): RegisterData | null { if (!s.available) return null; const list = (s.schools ?? []) as BaseFinding[]; const rows: RegisterRow[] = list.map((sc) => ({ name: `${sc.loc_name ?? sc.school_name ?? sc.name ?? '?'}${sc.borough ? ` · ${sc.borough}` : ''}`, elev: feetLabel((sc.elevation_m ?? sc.elev_m) as number | null | undefined), ada: false, fema: '—', sandy: inundLabel(sc.inside_sandy_2012 as boolean | undefined), dep: depLabel( sc.dep_extreme_2080_label as string | null | undefined, sc.dep_extreme_2080_class as number | null | undefined ), asset: 'school', primaryTier: sc.inside_sandy_2012 ? 'empirical' : 'modeled' })); return { type: TYPE_LABEL.school, radius: metersToLabel(s.radius_m as number | undefined), count: (s.n_schools as number | undefined) ?? rows.length, rows, sourceLabel: SOURCE_LABEL.school }; } function adaptHospitals(s: BaseFinding): RegisterData | null { if (!s.available) return null; const list = (s.hospitals ?? []) as BaseFinding[]; const rows: RegisterRow[] = list.map((h) => ({ name: `${h.facility_name ?? h.name ?? '?'}${h.borough ? ` · ${h.borough}` : ''}`, elev: feetLabel((h.elevation_m ?? h.elev_m) as number | null | undefined), ada: true, // hospitals are ADA-required fema: '—', sandy: inundLabel(h.inside_sandy_2012 as boolean | undefined), dep: depLabel( h.dep_extreme_2080_label as string | null | undefined, h.dep_extreme_2080_class as number | null | undefined ), asset: 'hospital', primaryTier: h.inside_sandy_2012 ? 'empirical' : 'modeled' })); return { type: TYPE_LABEL.hospital, radius: metersToLabel(s.radius_m as number | undefined), count: (s.n_hospitals as number | undefined) ?? rows.length, rows, sourceLabel: SOURCE_LABEL.hospital }; } /** * Pull every available register out of the FSM `final` payload. * Order matches the FSM specialist sequence so trace UI and register * cards line up. */ export function extractRegisters(final: Record | null): RegisterData[] { if (!final) return []; const out: RegisterData[] = []; const mta = adaptSubway(final.mta_entrances as BaseFinding | null ?? {}); if (mta && mta.rows.length) out.push(mta); const nycha = adaptNycha(final.nycha_developments as BaseFinding | null ?? {}); if (nycha && nycha.rows.length) out.push(nycha); const schools = adaptSchools(final.doe_schools as BaseFinding | null ?? {}); if (schools && schools.rows.length) out.push(schools); const hospitals = adaptHospitals(final.doh_hospitals as BaseFinding | null ?? {}); if (hospitals && hospitals.rows.length) out.push(hospitals); return out; }