Spaces:
Configuration error
Configuration error
File size: 7,741 Bytes
e8a6c67 41a93a2 e8a6c67 41a93a2 e8a6c67 41a93a2 e8a6c67 d43cf2b e8a6c67 d43cf2b e8a6c67 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | /**
* 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_<assets>: 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<AssetKind, string> = {
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<AssetKind, string> = {
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<string, unknown> | 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;
}
|