// Riprap — Bulk mode (schools register). const SOURCE_LABELS = { geocode: "NYC DCP Geosearch", sandy: "NYC OpenData 5xsi-dfpx — Sandy 2012 inundation", dep_extreme_2080: "NYC DEP Stormwater — Extreme 3.66 in/hr + 2080 SLR", dep_moderate_2050: "NYC DEP Stormwater — Moderate 2.13 in/hr + 2050 SLR", dep_moderate_current: "NYC DEP Stormwater — Moderate 2.13 in/hr current", floodnet: "FloodNet NYC — live ultrasonic sensor network", nyc311: "NYC 311 (Socrata erm2-nwe9) — flood descriptors", microtopo: "USGS 3DEP 30 m DEM via py3dep", ida_hwm: "USGS STN — Hurricane Ida 2021 HWMs (Event 312, NY)", prithvi_water: "Prithvi-EO 2.0 (300M, NASA/IBM) Sen1Floods11 — satellite water segmentation", rag_dep_2013: "NYC DEP Wastewater Resiliency Plan (2013)", rag_nycha: "NYCHA — Flood Resilience: Lessons Learned", rag_coned: "Con Edison Climate Change Resilience Plan (Case 22-E-0222)", rag_mta: "MTA Climate Resilience Roadmap (Oct 2025)", rag_comptroller: "NYC Comptroller — \"Is NYC Ready for Rain?\" (2024)", }; const $ = (s) => document.querySelector(s); let allRows = []; let filteredRows = []; let selected = null; let detailMap = null; function tierBadge(t) { const cls = "tier-badge tier-" + t; return `${t}`; } function yn(b) { return b ? '' : ''; } function renderTable() { const tbody = $("#regBody"); tbody.innerHTML = filteredRows.map((r, i) => { const snap = r.snap || {}; const sandy = snap.sandy ? yn(true) : yn(false); const dep80 = snap.dep && snap.dep.dep_extreme_2080 && snap.dep.dep_extreme_2080.depth_class > 0 ? yn(true) : yn(false); const c311 = (snap.nyc311 && snap.nyc311.n) || 0; const fnEv = (snap.floodnet && snap.floodnet.n_flood_events_3y) || 0; const idaN = (snap.ida_hwm && snap.ida_hwm.n_within_radius) || 0; return ` ${tierBadge(r.tier)} ${r.score}
${r.name}
${r.address || ""}
${r.borough || ""} ${sandy} ${dep80} ${c311 || ""} ${fnEv || ""} ${idaN || ""} `; }).join(""); tbody.querySelectorAll("tr").forEach((tr) => { tr.addEventListener("click", () => selectRow(parseInt(tr.dataset.idx, 10))); }); } function applyFilters() { const q = ($("#filter").value || "").toLowerCase(); const boro = $("#boroughFilter").value || ""; filteredRows = allRows.filter((r) => { if (boro && r.borough !== boro) return false; if (!q) return true; return ( (r.name || "").toLowerCase().includes(q) || (r.address || "").toLowerCase().includes(q) || (r.borough || "").toLowerCase().includes(q) || (r.bbl || "").toString().includes(q) ); }); renderTable(); } function selectRow(idx) { selected = filteredRows[idx]; if (!selected) return; $("#detailEmpty").classList.add("hidden"); $("#detailBody").classList.remove("hidden"); $("#detailHeader").innerHTML = `
${selected.name}
${selected.address}, ${selected.borough}
BBL: ${selected.bbl || "—"} · Tier ${selected.tier} · Score ${selected.score}
`; renderDetail(selected); } function renderDetail(row) { const snap = row.snap || {}; // glance const rows = []; if (snap.sandy) rows.push({c:"hit", mark:"■", html:"Inside Sandy 2012 inundation extent"}); else rows.push({c:"miss",mark:"□", html:"Outside Sandy 2012 inundation extent"}); const dep = snap.dep || {}; const depHits = Object.entries(dep).filter(([_,v]) => (v.depth_class || 0) > 0); if (depHits.length) { for (const [scen, v] of depHits) { const lbl = scen.replace("dep_","").replace(/_/g, " "); rows.push({c:"hit", mark:"■", html:`Inside DEP ${lbl} — ${v.depth_label}`}); } } else { rows.push({c:"miss", mark:"□", html:"Outside all DEP stormwater scenarios"}); } const fn = snap.floodnet; if (fn && fn.n_sensors) { if (fn.n_flood_events_3y > 0) { const peak = fn.peak_event; const peakStr = peak && peak.max_depth_mm ? `, peak ${peak.max_depth_mm} mm` : ""; rows.push({c:"hit", mark:"■", html:`${fn.n_flood_events_3y} FloodNet events (3 yr)${peakStr}`}); } } const ida = snap.ida_hwm; if (ida && ida.n_within_radius > 0) { const ht = ida.max_height_above_gnd_ft != null ? `up to ${ida.max_height_above_gnd_ft} ft above ground` : ""; rows.push({c:"hit", mark:"■", html:`${ida.n_within_radius} Ida HWMs ≤${ida.radius_m} m${ht ? ", " + ht : ""}`}); } const mt = snap.microtopo; if (mt) { rows.push({c:"note", mark:"◆", html:`Elevation ${mt.point_elev_m} m; lower than ${mt.rel_elev_pct_200m}% of nearby (200 m)`}); } const c311 = snap.nyc311; if (c311 && c311.n > 0) { rows.push({c:"note", mark:"◆", html:`${c311.n} 311 flood complaints ≤${c311.radius_m} m, ${c311.years} yr`}); } $("#detailGlance").innerHTML = rows.map(r => `
  • ${r.mark}${r.html}
  • `).join(""); // paragraph const para = snap.paragraph; const noPara = $("#detailNoPara"); if (para) { $("#detailParagraph").innerHTML = (para || "").replace(/\[([a-z0-9_]+)\]/gi, (_, d) => `[${d}]`); noPara.classList.add("hidden"); } else { $("#detailParagraph").innerHTML = ""; noPara.classList.remove("hidden"); } // sources const fired = new Set([...(para || "").matchAll(/\[([a-z0-9_]+)\]/g)].map(m => m[1])); const order = [ "sandy", "dep_extreme_2080", "dep_moderate_2050", "dep_moderate_current", "floodnet", "ida_hwm", "microtopo", "nyc311", "rag_dep_2013", "rag_nycha", "rag_coned", "rag_mta", "rag_comptroller", ]; const present = new Set(); if (snap.sandy) present.add("sandy"); for (const [k, v] of Object.entries(snap.dep || {})) { if ((v.depth_class || 0) > 0) present.add(k); } if (snap.floodnet && snap.floodnet.n_sensors > 0) present.add("floodnet"); if (snap.ida_hwm && snap.ida_hwm.n_within_radius > 0) present.add("ida_hwm"); if (snap.microtopo) present.add("microtopo"); if (snap.nyc311 && snap.nyc311.n > 0) present.add("nyc311"); if (snap.rag) for (const h of snap.rag) present.add(h.doc_id); $("#detailSources").innerHTML = order.filter(d => present.has(d)).map(d => { const dim = fired.has(d) ? "" : ' style="opacity:0.5"'; return `${d}${SOURCE_LABELS[d] || d}`; }).join(""); showDetailMap(row); } function showDetailMap(row) { const div = $("#detailMap"); if (!detailMap) { detailMap = new maplibregl.Map({ container: "detailMap", style: { version: 8, sources: { carto: { type: "raster", tiles: ["https://a.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png"], tileSize: 256, attribution: "© OSM · CARTO" }, }, layers: [{ id: "bg", type: "raster", source: "carto" }], }, center: [row.lon, row.lat], zoom: 14, attributionControl: { compact: true }, }); detailMap.on("load", () => { detailMap.addSource("addr", { type: "geojson", data: { type:"FeatureCollection", features:[] }}); detailMap.addLayer({ id:"addr-marker", type:"circle", source:"addr", paint: { "circle-radius": 9, "circle-color": "#1642DF", "circle-stroke-color":"#fff", "circle-stroke-width": 2.5 }}); }); } const setMarker = () => { detailMap.getSource("addr").setData({ type:"FeatureCollection", features:[ { type:"Feature", geometry:{ type:"Point", coordinates:[row.lon, row.lat] }, properties:{} } ]}); detailMap.flyTo({ center: [row.lon, row.lat], zoom: 14 }); }; if (detailMap.loaded()) setMarker(); else detailMap.once("load", setMarker); } async function generateLiveParagraph() { if (!selected) return; const btn = $("#livePara"); btn.disabled = true; btn.textContent = "Generating…"; try { const u = `/api/stream?q=${encodeURIComponent(selected.address + ", " + selected.borough)}`; // collect SSE final event const text = await new Promise((resolve, reject) => { const es = new EventSource(u); es.addEventListener("final", (m) => { resolve(JSON.parse(m.data)); es.close(); }); es.addEventListener("error", () => { reject(new Error("stream error")); es.close(); }); setTimeout(() => { reject(new Error("timeout")); es.close(); }, 90000); }); if (text.paragraph) { selected.snap.paragraph = text.paragraph; selected.snap.rag = text.rag || selected.snap.rag; renderDetail(selected); } } catch (e) { btn.textContent = "Failed: " + e.message; btn.disabled = false; } } function exportCsv() { const cols = ["tier","score","name","address","borough","bbl","bin", "lat","lon","sandy","dep_extreme_2080","floodnet_events_3y", "ida_hwms_800m","nyc311_5y"]; const lines = [cols.join(",")]; for (const r of filteredRows) { const s = r.snap || {}; const row = [ r.tier, r.score, JSON.stringify(r.name), JSON.stringify(r.address || ""), r.borough || "", r.bbl || "", r.bin || "", r.lat, r.lon, s.sandy ? 1 : 0, (s.dep && s.dep.dep_extreme_2080 && s.dep.dep_extreme_2080.depth_class) || 0, (s.floodnet && s.floodnet.n_flood_events_3y) || 0, (s.ida_hwm && s.ida_hwm.n_within_radius) || 0, (s.nyc311 && s.nyc311.n) || 0, ]; lines.push(row.join(",")); } const blob = new Blob([lines.join("\n")], { type: "text/csv" }); download(blob, "riprap_schools_register.csv"); } function exportGeojson() { const features = filteredRows.map((r) => ({ type: "Feature", geometry: { type: "Point", coordinates: [r.lon, r.lat] }, properties: { tier: r.tier, score: r.score, name: r.name, address: r.address, borough: r.borough, bbl: r.bbl, bin: r.bin, sandy: !!(r.snap && r.snap.sandy), dep_extreme_2080: (r.snap && r.snap.dep && r.snap.dep.dep_extreme_2080 && r.snap.dep.dep_extreme_2080.depth_class) || 0, }, })); const blob = new Blob([JSON.stringify({ type: "FeatureCollection", features })], { type: "application/geo+json" }); download(blob, "riprap_schools_register.geojson"); } function download(blob, filename) { const u = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = u; a.download = filename; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(u); }, 200); } // Asset class is parsed from URL path: /register/ const ASSET_CLASS = (location.pathname.match(/\/register\/([^\/?]+)/) || [])[1] || "schools"; const ASSET_TITLES = { schools: "NYC public schools — flood exposure register", nycha: "NYCHA developments — flood exposure register", mta_entrances: "MTA subway entrances — flood exposure register", }; document.title = `Riprap — ${ASSET_TITLES[ASSET_CLASS] || ASSET_CLASS}`; const tagSpan = document.querySelector(".brand-tag"); if (tagSpan) tagSpan.textContent = ASSET_TITLES[ASSET_CLASS] || ASSET_CLASS; const classPicker = document.getElementById("classPicker"); if (classPicker) { classPicker.value = ASSET_CLASS; classPicker.addEventListener("change", () => { location.href = `/register/${classPicker.value}`; }); } (async function init() { const r = await fetch(`/api/register/${ASSET_CLASS}`); if (!r.ok) { const script = `python scripts/build_${ASSET_CLASS}_register.py`; $("#regBody").innerHTML = `Register not built. Run ${script}.`; return; } const data = await r.json(); allRows = data.rows || []; filteredRows = allRows.slice(); // tier counts $("#totalCount").textContent = allRows.length; $("#tier1Count").textContent = allRows.filter(r => r.tier === 1).length; $("#tier2Count").textContent = allRows.filter(r => r.tier === 2).length; $("#tier3Count").textContent = allRows.filter(r => r.tier === 3).length; renderTable(); $("#filter").addEventListener("input", applyFilters); $("#boroughFilter").addEventListener("change", applyFilters); $("#exportCsv").addEventListener("click", exportCsv); $("#exportGeojson").addEventListener("click", exportGeojson); $("#livePara").addEventListener("click", generateLiveParagraph); })();