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;
}