// 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);
})();