Bulk register + compare modes
Browse filesTwo more user-facing modes on top of the address endpoint:
/compare — side-by-side briefings for two queries, useful
when picking between candidate parcels or
comparing two neighborhoods.
/register/{class} — pre-computed exposure for an entire asset
register: NYC DOE schools, NYCHA developments,
MTA subway entrances. The JSON ships baked
(built offline by register_builder), so the
UI is just paginated rendering — every row
shows the same paragraph + tier the address
mode would produce.
Asset loaders normalise each register's source GeoJSON into the
common (name, lat, lon, props) shape register_builder expects.
- .gitattributes +3 -0
- app/assets/mta_entrances.py +74 -0
- app/assets/nycha.py +28 -0
- app/assets/schools.py +27 -0
- app/register_builder.py +169 -0
- data/mta_entrances.geojson +3 -0
- data/nycha.geojson +3 -0
- data/registers/nycha.json +3 -0
- data/registers/schools.json +3 -0
- data/schools.geojson +3 -0
- web/static/compare.html +131 -0
- web/static/compare.js +323 -0
- web/static/register.html +121 -0
- web/static/register.js +315 -0
|
@@ -3,6 +3,9 @@
|
|
| 3 |
*.tif filter=lfs diff=lfs merge=lfs -text
|
| 4 |
*.pdf filter=lfs diff=lfs merge=lfs -text
|
| 5 |
|
|
|
|
|
|
|
|
|
|
| 6 |
# Esri FileGDB internal binary files (DEP Stormwater scenario data)
|
| 7 |
*.gdbtable filter=lfs diff=lfs merge=lfs -text
|
| 8 |
*.gdbtablx filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 3 |
*.tif filter=lfs diff=lfs merge=lfs -text
|
| 4 |
*.pdf filter=lfs diff=lfs merge=lfs -text
|
| 5 |
|
| 6 |
+
# Pre-computed register paragraphs
|
| 7 |
+
data/registers/*.json filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
|
| 9 |
# Esri FileGDB internal binary files (DEP Stormwater scenario data)
|
| 10 |
*.gdbtable filter=lfs diff=lfs merge=lfs -text
|
| 11 |
*.gdbtablx filter=lfs diff=lfs merge=lfs -text
|
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""MTA Subway Entrances and Exits (NY OpenData i9wp-a4ja).
|
| 2 |
+
|
| 3 |
+
~1,900 subway entrances city-wide. The MTA Climate Resilience Roadmap
|
| 4 |
+
(Oct 2025) names ~1,500 of these as priorities for sealing — this is
|
| 5 |
+
exactly the asset class our RAG corpus has the most to say about, and
|
| 6 |
+
exactly the audience (MTA capital planners, transit advocacy) the
|
| 7 |
+
register is built for.
|
| 8 |
+
"""
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import json
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
import geopandas as gpd
|
| 15 |
+
import httpx
|
| 16 |
+
|
| 17 |
+
from app.spatial import DATA, NYC_CRS
|
| 18 |
+
|
| 19 |
+
URL = "https://data.ny.gov/api/geospatial/i9wp-a4ja?method=export&format=GeoJSON"
|
| 20 |
+
LOCAL = DATA / "mta_entrances.geojson"
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _ensure_fixture() -> Path:
|
| 24 |
+
if LOCAL.exists():
|
| 25 |
+
return LOCAL
|
| 26 |
+
print("downloading MTA Subway Entrances (one-time)...", flush=True)
|
| 27 |
+
r = httpx.get(URL, timeout=60)
|
| 28 |
+
r.raise_for_status()
|
| 29 |
+
LOCAL.write_text(r.text)
|
| 30 |
+
return LOCAL
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def load() -> gpd.GeoDataFrame:
|
| 34 |
+
_ensure_fixture()
|
| 35 |
+
g = gpd.read_file(LOCAL)
|
| 36 |
+
if g.crs is None:
|
| 37 |
+
g.set_crs("EPSG:4326", inplace=True)
|
| 38 |
+
g = g.to_crs(NYC_CRS)
|
| 39 |
+
rename_map = {
|
| 40 |
+
"stop_name": "name",
|
| 41 |
+
"constrained_floor_to_floor_height": None,
|
| 42 |
+
"borough": "borough",
|
| 43 |
+
"entrance_type": "entrance_type",
|
| 44 |
+
"ada": "ada",
|
| 45 |
+
"north_south_street": "ns_street",
|
| 46 |
+
"east_west_street": "ew_street",
|
| 47 |
+
"corner": "corner",
|
| 48 |
+
}
|
| 49 |
+
for k, v in rename_map.items():
|
| 50 |
+
if v and k in g.columns and k != v:
|
| 51 |
+
g = g.rename(columns={k: v})
|
| 52 |
+
|
| 53 |
+
# build a usable address-style label
|
| 54 |
+
def label(row):
|
| 55 |
+
nm = (row.get("name") or "").strip()
|
| 56 |
+
ns = (row.get("ns_street") or "").strip()
|
| 57 |
+
ew = (row.get("ew_street") or "").strip()
|
| 58 |
+
cn = (row.get("corner") or "").strip()
|
| 59 |
+
bits = [nm]
|
| 60 |
+
cross = " & ".join(b for b in [ns, ew] if b)
|
| 61 |
+
if cross: bits.append(cross)
|
| 62 |
+
if cn: bits.append(f"({cn})")
|
| 63 |
+
return ", ".join([b for b in bits if b])
|
| 64 |
+
|
| 65 |
+
g["address"] = g.apply(label, axis=1)
|
| 66 |
+
if "borough" in g.columns:
|
| 67 |
+
boro_map = {"M": "Manhattan", "Bk": "Brooklyn", "B": "Brooklyn",
|
| 68 |
+
"Q": "Queens", "Bx": "Bronx", "SI": "Staten Island"}
|
| 69 |
+
g["borough"] = g["borough"].astype(str).map(lambda v: boro_map.get(v, v.title()))
|
| 70 |
+
|
| 71 |
+
keep = [c for c in ["name", "address", "borough", "entrance_type",
|
| 72 |
+
"ada", "ns_street", "ew_street", "corner", "geometry"]
|
| 73 |
+
if c in g.columns]
|
| 74 |
+
return g[keep].copy()
|
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""NYCHA Developments (NYC OpenData phvi-damg).
|
| 2 |
+
|
| 3 |
+
326 public-housing developments across NYC. Used as an asset class for
|
| 4 |
+
the bulk-mode register; the parent rationale for surfacing this layer
|
| 5 |
+
is that NYCHA was hit hard by Sandy and remains a published Tier-1
|
| 6 |
+
flood-resilience priority in the city's Hazard Mitigation Plan.
|
| 7 |
+
"""
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import geopandas as gpd
|
| 11 |
+
|
| 12 |
+
from app.spatial import DATA, load_layer
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def load() -> gpd.GeoDataFrame:
|
| 16 |
+
g = load_layer(DATA / "nycha.geojson")
|
| 17 |
+
# NYCHA developments come back as polygons; the FSM expects point
|
| 18 |
+
# geometry for spatial joins. Use centroid.
|
| 19 |
+
g = g.copy()
|
| 20 |
+
g["geometry"] = g.geometry.centroid
|
| 21 |
+
|
| 22 |
+
# NYCHA Developments has only `developmen` (truncated label), tds_num, borough.
|
| 23 |
+
g = g.rename(columns={"developmen": "name"})
|
| 24 |
+
g["address"] = g["name"] # the field doubles as both
|
| 25 |
+
g["borough"] = g["borough"].str.title() # "BRONX" -> "Bronx" to match Riprap convention
|
| 26 |
+
|
| 27 |
+
keep = [c for c in ["name", "address", "borough", "tds_num", "geometry"] if c in g.columns]
|
| 28 |
+
return g[keep].copy()
|
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""NYC DOE School Point Locations (Socrata a3nt-yts4)."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import geopandas as gpd
|
| 5 |
+
|
| 6 |
+
from app.spatial import DATA, load_layer
|
| 7 |
+
|
| 8 |
+
BORO = {"1": "Manhattan", "2": "Bronx", "3": "Brooklyn", "4": "Queens", "5": "Staten Island"}
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def load() -> gpd.GeoDataFrame:
|
| 12 |
+
g = load_layer(DATA / "schools.geojson")
|
| 13 |
+
g = g.rename(columns={
|
| 14 |
+
"loc_code": "loc_code",
|
| 15 |
+
"loc_name": "name",
|
| 16 |
+
"address": "address",
|
| 17 |
+
"bbl": "bbl",
|
| 18 |
+
"bin": "bin",
|
| 19 |
+
"boronum": "boro_num",
|
| 20 |
+
"geodistric": "geo_district",
|
| 21 |
+
"adimindist": "admin_district",
|
| 22 |
+
})
|
| 23 |
+
g["borough"] = g["boro_num"].astype(str).map(BORO)
|
| 24 |
+
g["bbl"] = g["bbl"].astype(str).str.replace(r"\.0$", "", regex=True)
|
| 25 |
+
keep = ["loc_code", "name", "address", "borough", "bbl", "bin",
|
| 26 |
+
"geo_district", "admin_district", "geometry"]
|
| 27 |
+
return g[keep].copy()
|
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Generic per-asset register builder.
|
| 2 |
+
|
| 3 |
+
Runs the same FSM specialists over every asset in a class. Tier 1+2
|
| 4 |
+
get full Granite paragraphs; Tier 3 gets signals only (paragraph
|
| 5 |
+
generated on click in the UI).
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import json
|
| 10 |
+
import sys
|
| 11 |
+
import time
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Any, Callable
|
| 14 |
+
|
| 15 |
+
import geopandas as gpd
|
| 16 |
+
from shapely.geometry import Point
|
| 17 |
+
|
| 18 |
+
from app.context import floodnet, microtopo, nyc311
|
| 19 |
+
from app.flood_layers import dep_stormwater, ida_hwm, sandy_inundation
|
| 20 |
+
from app.rag import retrieve as rag_retrieve, warm as rag_warm
|
| 21 |
+
from app.reconcile import reconcile as run_reconcile
|
| 22 |
+
from app.score import score_frame
|
| 23 |
+
|
| 24 |
+
ROOT = Path(__file__).resolve().parent.parent
|
| 25 |
+
REGISTERS_DIR = ROOT / "data" / "registers"
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _build_one(row_meta: dict, geom_2263, lat: float, lon: float,
|
| 29 |
+
with_paragraph: bool) -> dict:
|
| 30 |
+
pt = gpd.GeoDataFrame(geometry=[geom_2263], crs="EPSG:2263")
|
| 31 |
+
sandy = bool(sandy_inundation.join(pt).iloc[0])
|
| 32 |
+
dep = {}
|
| 33 |
+
for scen in ["dep_extreme_2080", "dep_moderate_2050", "dep_moderate_current"]:
|
| 34 |
+
j = dep_stormwater.join(pt, scen).iloc[0]
|
| 35 |
+
dep[scen] = {
|
| 36 |
+
"depth_class": int(j["depth_class"]),
|
| 37 |
+
"depth_label": j["depth_label"],
|
| 38 |
+
"citation": f"NYC DEP Stormwater Flood Map — {dep_stormwater.label(scen)}",
|
| 39 |
+
}
|
| 40 |
+
fn = floodnet.summary_for_point(lat, lon, 600); fn["radius_m"] = 600
|
| 41 |
+
n311 = nyc311.summary_for_point(lat, lon, 200, 5)
|
| 42 |
+
mt_obj = microtopo.microtopo_at(lat, lon)
|
| 43 |
+
mt = vars(mt_obj) if mt_obj else None
|
| 44 |
+
ida_obj = ida_hwm.summary_for_point(lat, lon, 800)
|
| 45 |
+
ida = vars(ida_obj) if ida_obj else None
|
| 46 |
+
|
| 47 |
+
snap = {
|
| 48 |
+
"geocode": {**row_meta, "lat": lat, "lon": lon},
|
| 49 |
+
"sandy": sandy, "dep": dep, "floodnet": fn, "nyc311": n311,
|
| 50 |
+
"microtopo": mt, "ida_hwm": ida,
|
| 51 |
+
}
|
| 52 |
+
if with_paragraph:
|
| 53 |
+
rag_query = (f"flood risk for {row_meta.get('name','')} in "
|
| 54 |
+
f"{row_meta.get('borough','')}, NYC; resilience plan, "
|
| 55 |
+
f"vulnerability, mitigation")
|
| 56 |
+
snap["rag"] = rag_retrieve(rag_query, k=2, min_score=0.55)
|
| 57 |
+
para, audit = run_reconcile(snap, return_audit=True)
|
| 58 |
+
snap["paragraph"] = para
|
| 59 |
+
snap["audit"] = audit
|
| 60 |
+
return snap
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def build_register(asset_class: str, loader: Callable, *,
|
| 64 |
+
tier_with_paragraph: tuple[int, ...] = (1, 2),
|
| 65 |
+
meta_keys: tuple[str, ...] = ("name", "address", "borough"),
|
| 66 |
+
regenerate: bool = False) -> Path:
|
| 67 |
+
"""Build a register JSON for an asset class.
|
| 68 |
+
|
| 69 |
+
Args:
|
| 70 |
+
asset_class: short id (also the output filename)
|
| 71 |
+
loader: zero-arg callable returning a GeoDataFrame in EPSG:2263 with
|
| 72 |
+
point geometry and at least the columns in meta_keys
|
| 73 |
+
tier_with_paragraph: which tiers get full Granite reconciliation
|
| 74 |
+
meta_keys: which row columns to surface as the geocode-style metadata
|
| 75 |
+
"""
|
| 76 |
+
out = REGISTERS_DIR / f"{asset_class}.json"
|
| 77 |
+
if out.exists() and not regenerate:
|
| 78 |
+
print(f"already exists: {out}; pass regenerate=True to rebuild",
|
| 79 |
+
file=sys.stderr)
|
| 80 |
+
return out
|
| 81 |
+
REGISTERS_DIR.mkdir(exist_ok=True, parents=True)
|
| 82 |
+
|
| 83 |
+
print(f"loading asset class {asset_class!r}...", file=sys.stderr)
|
| 84 |
+
g = loader()
|
| 85 |
+
if g.crs is None or g.crs.to_string() != "EPSG:2263":
|
| 86 |
+
g = g.to_crs("EPSG:2263")
|
| 87 |
+
|
| 88 |
+
# tier each asset off the same rubric (sandy + 3 DEP scenarios)
|
| 89 |
+
g["sandy"] = sandy_inundation.join(g).astype(int)
|
| 90 |
+
for scen in ["dep_extreme_2080", "dep_moderate_2050", "dep_moderate_current"]:
|
| 91 |
+
j = dep_stormwater.join(g, scen)
|
| 92 |
+
g[scen] = (j["depth_class"] > 0).astype(int)
|
| 93 |
+
g = score_frame(g)
|
| 94 |
+
g["lat"] = g.geometry.to_crs("EPSG:4326").y
|
| 95 |
+
g["lon"] = g.geometry.to_crs("EPSG:4326").x
|
| 96 |
+
|
| 97 |
+
targets = g[g["tier"].isin([1, 2, 3])].copy()
|
| 98 |
+
print(f" {len(targets)} of {len(g)} assets at Tier 1-3", file=sys.stderr)
|
| 99 |
+
print("warming RAG index...", file=sys.stderr)
|
| 100 |
+
rag_warm()
|
| 101 |
+
|
| 102 |
+
# Resume support: a partial JSON sits next to the final output. We
|
| 103 |
+
# write it after every row, so any blip can be retried without losing
|
| 104 |
+
# work.
|
| 105 |
+
partial = REGISTERS_DIR / f"{asset_class}.partial.json"
|
| 106 |
+
rows: list[dict] = []
|
| 107 |
+
done_keys: set = set()
|
| 108 |
+
if partial.exists():
|
| 109 |
+
try:
|
| 110 |
+
data = json.loads(partial.read_text())
|
| 111 |
+
rows = data.get("rows", [])
|
| 112 |
+
# use lat/lon as the unique key (works for any asset class)
|
| 113 |
+
done_keys = {(round(r["lat"], 5), round(r["lon"], 5)) for r in rows}
|
| 114 |
+
print(f" resuming with {len(rows)} rows already processed",
|
| 115 |
+
file=sys.stderr)
|
| 116 |
+
except Exception as e:
|
| 117 |
+
print(f" failed to read partial, starting fresh: {e}",
|
| 118 |
+
file=sys.stderr)
|
| 119 |
+
|
| 120 |
+
t0 = time.time()
|
| 121 |
+
for i, (_, row) in enumerate(
|
| 122 |
+
targets.sort_values(["score", "name"], ascending=[False, True]).iterrows()):
|
| 123 |
+
key = (round(float(row["lat"]), 5), round(float(row["lon"]), 5))
|
| 124 |
+
if key in done_keys:
|
| 125 |
+
continue
|
| 126 |
+
tier = int(row["tier"])
|
| 127 |
+
with_paragraph = tier in tier_with_paragraph
|
| 128 |
+
meta = {k: row.get(k) for k in meta_keys}
|
| 129 |
+
try:
|
| 130 |
+
snap = _build_one(meta, row["geometry"],
|
| 131 |
+
float(row["lat"]), float(row["lon"]),
|
| 132 |
+
with_paragraph=with_paragraph)
|
| 133 |
+
except Exception as e:
|
| 134 |
+
print(f" [{i+1}/{len(targets)}] FAILED tier-{tier} "
|
| 135 |
+
f"{str(meta.get('name',''))[:50]} -- {type(e).__name__}: {e}",
|
| 136 |
+
file=sys.stderr)
|
| 137 |
+
time.sleep(2) # back off on transient errors
|
| 138 |
+
continue
|
| 139 |
+
|
| 140 |
+
rec: dict[str, Any] = {
|
| 141 |
+
**{k: row.get(k) for k in g.columns if k != "geometry"},
|
| 142 |
+
"lat": float(row["lat"]),
|
| 143 |
+
"lon": float(row["lon"]),
|
| 144 |
+
"score": int(row["score"]),
|
| 145 |
+
"tier": tier,
|
| 146 |
+
"snap": snap,
|
| 147 |
+
}
|
| 148 |
+
rows.append(rec)
|
| 149 |
+
done_keys.add(key)
|
| 150 |
+
# incremental persist
|
| 151 |
+
partial.write_text(json.dumps({
|
| 152 |
+
"asset_class": asset_class,
|
| 153 |
+
"rows": rows,
|
| 154 |
+
}, default=str))
|
| 155 |
+
elapsed = time.time() - t0
|
| 156 |
+
print(f" [{i+1}/{len(targets)}] tier-{tier} "
|
| 157 |
+
f"{str(meta.get('name',''))[:50]:<50} "
|
| 158 |
+
f"({elapsed:.0f}s elapsed)", file=sys.stderr)
|
| 159 |
+
|
| 160 |
+
out.write_text(json.dumps({
|
| 161 |
+
"asset_class": asset_class,
|
| 162 |
+
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
| 163 |
+
"rows": rows,
|
| 164 |
+
}, default=str))
|
| 165 |
+
if partial.exists():
|
| 166 |
+
partial.unlink()
|
| 167 |
+
print(f"\nwrote {len(rows)} rows -> {out} ({out.stat().st_size // 1024} KB)",
|
| 168 |
+
file=sys.stderr)
|
| 169 |
+
return out
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8f5cc2ac27763a44bc7c9166e229f221dd5ffb285bf148bc4c12a326b23b8072
|
| 3 |
+
size 939323
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6f3a4da7938f0e70c00b8497aabdc0e5dede8e9bc4940abbf0c35171ba831fa4
|
| 3 |
+
size 308085
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:429ee990e27b90cb824678eaf7679cd6efc385103c564d1acc97b39766183a0e
|
| 3 |
+
size 160508
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5fb2aa47325d8214453016e0f7813650e4b347b2976edef0590ed9a5af0d6058
|
| 3 |
+
size 448698
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d67aa09b8385d984ed41caf042431c5e86406143e0cc14b52a2fc5d3d308f888
|
| 3 |
+
size 897297
|
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
| 6 |
+
<title>Riprap — compare two NYC addresses</title>
|
| 7 |
+
<link rel="stylesheet" href="/static/style.css">
|
| 8 |
+
<link rel="stylesheet" href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css">
|
| 9 |
+
<script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
|
| 10 |
+
</head>
|
| 11 |
+
<body class="compare-mode">
|
| 12 |
+
|
| 13 |
+
<header class="topbar">
|
| 14 |
+
<div class="topbar-inner">
|
| 15 |
+
<div class="brand">
|
| 16 |
+
<span class="brand-name">Riprap</span>
|
| 17 |
+
<span class="brand-sep">·</span>
|
| 18 |
+
<span class="brand-tag">compare two NYC addresses, side by side</span>
|
| 19 |
+
</div>
|
| 20 |
+
<div class="topbar-right">
|
| 21 |
+
<a href="/" class="modelink">address</a>
|
| 22 |
+
<a href="/register/schools" class="modelink">register</a>
|
| 23 |
+
<span class="local-pill" title="Granite 4.1 inference runs on this machine. Two FSMs run in parallel.">
|
| 24 |
+
<span class="dot"></span>local · 2× Granite 4.1
|
| 25 |
+
</span>
|
| 26 |
+
</div>
|
| 27 |
+
</div>
|
| 28 |
+
</header>
|
| 29 |
+
|
| 30 |
+
<div class="form-bar form-bar-compare">
|
| 31 |
+
<form id="cform">
|
| 32 |
+
<div class="cform-row">
|
| 33 |
+
<label>A</label>
|
| 34 |
+
<input type="text" id="qa" name="a" autocomplete="off"
|
| 35 |
+
placeholder='Address A — e.g. "153-09 90 Avenue, Jamaica, Queens"'
|
| 36 |
+
value="153-09 90 Avenue, Jamaica, Queens" required />
|
| 37 |
+
</div>
|
| 38 |
+
<div class="cform-row">
|
| 39 |
+
<label>B</label>
|
| 40 |
+
<input type="text" id="qb" name="b" autocomplete="off"
|
| 41 |
+
placeholder='Address B — e.g. "350 5 Avenue, Manhattan" (Empire State)'
|
| 42 |
+
value="350 5 Avenue, Manhattan" required />
|
| 43 |
+
</div>
|
| 44 |
+
<button type="submit" id="cgo">Compare</button>
|
| 45 |
+
</form>
|
| 46 |
+
<div class="suggest">
|
| 47 |
+
<span class="suggest-label">try:</span>
|
| 48 |
+
<button class="chip" data-a="153-09 90 Avenue, Jamaica, Queens" data-b="350 5 Avenue, Manhattan">Ida basements vs. Empire State</button>
|
| 49 |
+
<button class="chip" data-a="180 Beach 35 St, Queens" data-b="2950 W 25 St, Brooklyn">Far Rockaway vs. Coney Island NYCHA</button>
|
| 50 |
+
<button class="chip" data-a="Smith and 9 Street, Brooklyn" data-b="220 W 109 St, Manhattan">Gowanus vs. Upper West Side</button>
|
| 51 |
+
</div>
|
| 52 |
+
</div>
|
| 53 |
+
|
| 54 |
+
<div class="workbench compare-workbench">
|
| 55 |
+
|
| 56 |
+
<!-- LEFT pane: address A -->
|
| 57 |
+
<aside class="cpane" id="paneA">
|
| 58 |
+
<div class="cpane-head"><span class="ctag a">A</span><span id="aTitle">Address A</span></div>
|
| 59 |
+
<section class="panel">
|
| 60 |
+
<h2>Trace<span class="hint">Burr FSM · A</span></h2>
|
| 61 |
+
<ul id="stepsA"></ul>
|
| 62 |
+
</section>
|
| 63 |
+
<section class="panel hidden" id="reportA">
|
| 64 |
+
<h2>Cited report<span class="hint">Granite 4.1 · A</span></h2>
|
| 65 |
+
<div class="report-section"><h3>At a glance</h3><ul id="glanceA" class="glance-list"></ul></div>
|
| 66 |
+
<div class="report-section"><h3>Summary</h3><p id="paragraphA"></p></div>
|
| 67 |
+
<div class="report-section"><h3>Sources fired</h3><ol id="sourcesA" class="sources-list"></ol></div>
|
| 68 |
+
</section>
|
| 69 |
+
</aside>
|
| 70 |
+
|
| 71 |
+
<!-- MIDDLE pane: shared map -->
|
| 72 |
+
<section class="col-mid compare-mid">
|
| 73 |
+
<div class="panel panel-map" id="map-card">
|
| 74 |
+
<h2>Map<span class="hint">Both addresses · shared layers</span></h2>
|
| 75 |
+
<div id="map"></div>
|
| 76 |
+
<div class="legend">
|
| 77 |
+
<span><i class="sw sandy"></i>Sandy 2012</span>
|
| 78 |
+
<span><i class="sw dep"></i>DEP Extreme 2080</span>
|
| 79 |
+
<span><i class="sw fnDot"></i>FloodNet (no events)</span>
|
| 80 |
+
<span><i class="sw fnDotHot"></i>FloodNet w/ events</span>
|
| 81 |
+
<span><i class="sw addr"></i>A & B markers</span>
|
| 82 |
+
</div>
|
| 83 |
+
</div>
|
| 84 |
+
</section>
|
| 85 |
+
|
| 86 |
+
<!-- RIGHT pane: address B -->
|
| 87 |
+
<aside class="cpane" id="paneB">
|
| 88 |
+
<div class="cpane-head"><span class="ctag b">B</span><span id="bTitle">Address B</span></div>
|
| 89 |
+
<section class="panel">
|
| 90 |
+
<h2>Trace<span class="hint">Burr FSM · B</span></h2>
|
| 91 |
+
<ul id="stepsB"></ul>
|
| 92 |
+
</section>
|
| 93 |
+
<section class="panel hidden" id="reportB">
|
| 94 |
+
<h2>Cited report<span class="hint">Granite 4.1 · B</span></h2>
|
| 95 |
+
<div class="report-section"><h3>At a glance</h3><ul id="glanceB" class="glance-list"></ul></div>
|
| 96 |
+
<div class="report-section"><h3>Summary</h3><p id="paragraphB"></p></div>
|
| 97 |
+
<div class="report-section"><h3>Sources fired</h3><ol id="sourcesB" class="sources-list"></ol></div>
|
| 98 |
+
</section>
|
| 99 |
+
</aside>
|
| 100 |
+
|
| 101 |
+
</div>
|
| 102 |
+
|
| 103 |
+
<footer>
|
| 104 |
+
<div class="foot-inner">
|
| 105 |
+
<div class="foot-col">
|
| 106 |
+
<h3>Compare mode</h3>
|
| 107 |
+
<p>
|
| 108 |
+
Two addresses, two parallel runs of the same 8-specialist FSM.
|
| 109 |
+
Each side renders its own trace, at-a-glance signals, cited
|
| 110 |
+
summary, and source list. The map shows both markers and shared
|
| 111 |
+
layer overlays. Both Granite 4.1 reconciliations run concurrently
|
| 112 |
+
via Ollama (<code>OLLAMA_NUM_PARALLEL=2</code>); wallclock is
|
| 113 |
+
comparable to a single query.
|
| 114 |
+
</p>
|
| 115 |
+
</div>
|
| 116 |
+
<div class="foot-col">
|
| 117 |
+
<h3>Sources</h3>
|
| 118 |
+
<p>
|
| 119 |
+
<a href="https://data.cityofnewyork.us/" target="_blank">NYC Open Data</a> ·
|
| 120 |
+
<a href="https://api.floodnet.nyc/" target="_blank">FloodNet NYC</a> ·
|
| 121 |
+
<a href="https://geosearch.planninglabs.nyc/" target="_blank">NYC DCP Geosearch</a> ·
|
| 122 |
+
<a href="https://stn.wim.usgs.gov/" target="_blank">USGS STN</a> ·
|
| 123 |
+
<a href="https://www.usgs.gov/3d-elevation-program" target="_blank">USGS 3DEP</a>
|
| 124 |
+
</p>
|
| 125 |
+
</div>
|
| 126 |
+
</div>
|
| 127 |
+
</footer>
|
| 128 |
+
|
| 129 |
+
<script src="/static/compare.js"></script>
|
| 130 |
+
</body>
|
| 131 |
+
</html>
|
|
@@ -0,0 +1,323 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Riprap — Compare mode. Two addresses, parallel FSM runs, shared map.
|
| 2 |
+
|
| 3 |
+
const STEP_LABELS = {
|
| 4 |
+
geocode: ["Geocode (DCP Geosearch)", "address → lat/lon, BBL"],
|
| 5 |
+
sandy_inundation: ["Sandy Inundation (NYC OD)", "empirical 2012 extent"],
|
| 6 |
+
dep_stormwater: ["DEP Stormwater Maps", "pluvial scenarios + 2080 SLR"],
|
| 7 |
+
floodnet: ["FloodNet sensor network", "live ultrasonic depth sensors"],
|
| 8 |
+
nyc311: ["NYC 311 archive", "flood complaints in buffer"],
|
| 9 |
+
microtopo_lidar: ["LiDAR terrain (DEM + TWI + HAND)", "USGS 3DEP DEM + whitebox hydrology"],
|
| 10 |
+
ida_hwm_2021: ["Ida 2021 high-water marks", "USGS empirical post-event extent"],
|
| 11 |
+
prithvi_eo_v2: ["Prithvi-EO 2.0 (300M, NASA/IBM)", "Sen1Floods11 satellite water segmentation"],
|
| 12 |
+
rag_granite_embedding: ["Granite Embedding 278M (RAG)", "policy corpus retrieval"],
|
| 13 |
+
reconcile_granite41: ["Granite 4.1 reconcile (local)", "document-grounded synthesis"],
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
const STEPS_ORDER = [
|
| 17 |
+
"geocode", "sandy_inundation", "dep_stormwater", "floodnet", "nyc311",
|
| 18 |
+
"microtopo_lidar", "ida_hwm_2021", "prithvi_eo_v2",
|
| 19 |
+
"rag_granite_embedding", "reconcile_granite41",
|
| 20 |
+
];
|
| 21 |
+
|
| 22 |
+
const SOURCE_LABELS = {
|
| 23 |
+
geocode: "NYC DCP Geosearch",
|
| 24 |
+
sandy: "NYC OpenData 5xsi-dfpx — Sandy 2012 inundation",
|
| 25 |
+
dep_extreme_2080: "NYC DEP Stormwater — Extreme 3.66 in/hr + 2080 SLR",
|
| 26 |
+
dep_moderate_2050: "NYC DEP Stormwater — Moderate 2.13 in/hr + 2050 SLR",
|
| 27 |
+
dep_moderate_current: "NYC DEP Stormwater — Moderate 2.13 in/hr current",
|
| 28 |
+
floodnet: "FloodNet NYC — live ultrasonic sensor network",
|
| 29 |
+
nyc311: "NYC 311 (Socrata erm2-nwe9) — flood descriptors",
|
| 30 |
+
microtopo: "USGS 3DEP 30 m DEM via py3dep",
|
| 31 |
+
ida_hwm: "USGS STN — Hurricane Ida 2021 HWMs (Event 312, NY)",
|
| 32 |
+
prithvi_water: "Prithvi-EO 2.0 (300M, NASA/IBM) Sen1Floods11 — satellite water segmentation",
|
| 33 |
+
rag_dep_2013: "NYC DEP Wastewater Resiliency Plan (2013)",
|
| 34 |
+
rag_nycha: "NYCHA — Flood Resilience: Lessons Learned",
|
| 35 |
+
rag_coned: "Con Edison Climate Change Resilience Plan (Case 22-E-0222)",
|
| 36 |
+
rag_mta: "MTA Climate Resilience Roadmap (Oct 2025)",
|
| 37 |
+
rag_comptroller: "NYC Comptroller — \"Is NYC Ready for Rain?\" (2024)",
|
| 38 |
+
};
|
| 39 |
+
|
| 40 |
+
const $ = (s) => document.querySelector(s);
|
| 41 |
+
|
| 42 |
+
let evtSrc = null;
|
| 43 |
+
let map = null;
|
| 44 |
+
let mapInit = false;
|
| 45 |
+
|
| 46 |
+
const MAP_STYLE = {
|
| 47 |
+
version: 8,
|
| 48 |
+
sources: {
|
| 49 |
+
carto: {
|
| 50 |
+
type: "raster",
|
| 51 |
+
tiles: ["https://a.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png"],
|
| 52 |
+
tileSize: 256,
|
| 53 |
+
attribution: "© OpenStreetMap contributors © CARTO",
|
| 54 |
+
},
|
| 55 |
+
},
|
| 56 |
+
layers: [
|
| 57 |
+
{ id: "bg", type: "background", paint: { "background-color": "#fafbfd" } },
|
| 58 |
+
{ id: "carto", type: "raster", source: "carto" },
|
| 59 |
+
],
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
function ensureMap() {
|
| 63 |
+
if (mapInit) return;
|
| 64 |
+
mapInit = true;
|
| 65 |
+
map = new maplibregl.Map({
|
| 66 |
+
container: "map",
|
| 67 |
+
style: MAP_STYLE,
|
| 68 |
+
center: [-74.0, 40.72],
|
| 69 |
+
zoom: 10,
|
| 70 |
+
attributionControl: { compact: true },
|
| 71 |
+
});
|
| 72 |
+
map.addControl(new maplibregl.NavigationControl({ visualizePitch: false }), "top-right");
|
| 73 |
+
map.on("load", () => {
|
| 74 |
+
for (const sideKey of ["a", "b"]) {
|
| 75 |
+
map.addSource("sandy_" + sideKey, { type: "geojson", data: { type: "FeatureCollection", features: [] } });
|
| 76 |
+
map.addLayer({ id: "sandy_" + sideKey + "-fill", type: "fill", source: "sandy_" + sideKey,
|
| 77 |
+
paint: { "fill-color": "#fc5d52", "fill-opacity": 0.22 } });
|
| 78 |
+
map.addSource("dep_" + sideKey, { type: "geojson", data: { type: "FeatureCollection", features: [] } });
|
| 79 |
+
map.addLayer({ id: "dep_" + sideKey + "-fill", type: "fill", source: "dep_" + sideKey,
|
| 80 |
+
paint: {
|
| 81 |
+
"fill-color": ["match", ["get", "Flooding_Category"],
|
| 82 |
+
1, "#568adf", 2, "#1642DF", 3, "#031553", "#568adf"],
|
| 83 |
+
"fill-opacity": 0.28 } });
|
| 84 |
+
map.addSource("fn_" + sideKey, { type: "geojson", data: { type: "FeatureCollection", features: [] } });
|
| 85 |
+
map.addLayer({ id: "fn_" + sideKey + "-circles", type: "circle", source: "fn_" + sideKey,
|
| 86 |
+
paint: {
|
| 87 |
+
"circle-radius": 5,
|
| 88 |
+
"circle-color": ["case", [">", ["get", "n_events_3y"], 0], "#fc5d52", "#1a8754"],
|
| 89 |
+
"circle-stroke-color": "#ffffff",
|
| 90 |
+
"circle-stroke-width": 1.5,
|
| 91 |
+
} });
|
| 92 |
+
}
|
| 93 |
+
map.addSource("addr_a", { type: "geojson", data: { type: "FeatureCollection", features: [] } });
|
| 94 |
+
map.addLayer({ id: "addr_a-marker", type: "circle", source: "addr_a",
|
| 95 |
+
paint: { "circle-radius": 9, "circle-color": "#1642DF", "circle-stroke-color": "#fff", "circle-stroke-width": 2.5 } });
|
| 96 |
+
map.addSource("addr_b", { type: "geojson", data: { type: "FeatureCollection", features: [] } });
|
| 97 |
+
map.addLayer({ id: "addr_b-marker", type: "circle", source: "addr_b",
|
| 98 |
+
paint: { "circle-radius": 9, "circle-color": "#9333ea", "circle-stroke-color": "#fff", "circle-stroke-width": 2.5 } });
|
| 99 |
+
});
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
function resetSide(side) {
|
| 103 |
+
const ul = document.getElementById("steps" + side.toUpperCase());
|
| 104 |
+
ul.innerHTML = "";
|
| 105 |
+
for (const sid of STEPS_ORDER) {
|
| 106 |
+
const [lbl, hint] = STEP_LABELS[sid] || [sid, ""];
|
| 107 |
+
const li = document.createElement("li");
|
| 108 |
+
li.id = `step-${side}-${sid}`;
|
| 109 |
+
li.className = "pending";
|
| 110 |
+
li.innerHTML = `
|
| 111 |
+
<span class="icon">○</span>
|
| 112 |
+
<div>
|
| 113 |
+
<div class="label">${lbl}</div>
|
| 114 |
+
<div class="meta">${hint}</div>
|
| 115 |
+
</div>
|
| 116 |
+
<span class="meta time"></span>`;
|
| 117 |
+
ul.appendChild(li);
|
| 118 |
+
}
|
| 119 |
+
document.getElementById("step-" + side + "-" + STEPS_ORDER[0]).classList.replace("pending", "running");
|
| 120 |
+
|
| 121 |
+
document.getElementById("report" + side.toUpperCase()).classList.add("hidden");
|
| 122 |
+
document.getElementById("paragraph" + side.toUpperCase()).innerHTML = "";
|
| 123 |
+
document.getElementById("glance" + side.toUpperCase()).innerHTML = "";
|
| 124 |
+
document.getElementById("sources" + side.toUpperCase()).innerHTML = "";
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
function markStep(side, stepId, ev) {
|
| 128 |
+
const li = document.getElementById(`step-${side}-${stepId}`);
|
| 129 |
+
if (!li) return;
|
| 130 |
+
li.className = ev.ok ? "ok" : "err";
|
| 131 |
+
li.querySelector(".icon").textContent = ev.ok ? "✓" : "✗";
|
| 132 |
+
if (ev.elapsed_s != null) {
|
| 133 |
+
li.querySelector(".time").textContent = ev.elapsed_s.toFixed(2) + "s";
|
| 134 |
+
}
|
| 135 |
+
if (ev.result) {
|
| 136 |
+
let div = li.querySelector(".result");
|
| 137 |
+
if (!div) {
|
| 138 |
+
div = document.createElement("div"); div.className = "result";
|
| 139 |
+
li.appendChild(div);
|
| 140 |
+
}
|
| 141 |
+
div.textContent = formatResult(ev.result);
|
| 142 |
+
} else if (ev.err) {
|
| 143 |
+
let div = li.querySelector(".result");
|
| 144 |
+
if (!div) {
|
| 145 |
+
div = document.createElement("div"); div.className = "result";
|
| 146 |
+
li.appendChild(div);
|
| 147 |
+
}
|
| 148 |
+
div.textContent = "error: " + ev.err;
|
| 149 |
+
}
|
| 150 |
+
const idx = STEPS_ORDER.indexOf(stepId);
|
| 151 |
+
if (idx >= 0 && idx + 1 < STEPS_ORDER.length) {
|
| 152 |
+
const next = document.getElementById(`step-${side}-${STEPS_ORDER[idx + 1]}`);
|
| 153 |
+
if (next && next.classList.contains("pending")) next.classList.replace("pending", "running");
|
| 154 |
+
}
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
function formatResult(r) {
|
| 158 |
+
if (typeof r !== "object") return String(r);
|
| 159 |
+
return Object.entries(r)
|
| 160 |
+
.map(([k, v]) => `${k}: ${typeof v === "object" ? JSON.stringify(v) : v}`)
|
| 161 |
+
.join(" · ");
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
function renderParagraph(side, text) {
|
| 165 |
+
const html = (text || "").replace(/\[([a-z0-9_]+)\]/gi, (_, d) =>
|
| 166 |
+
`<span class="cite" title="document id: ${d}">[${d}]</span>`);
|
| 167 |
+
document.getElementById("paragraph" + side.toUpperCase()).innerHTML = html;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
function renderGlance(side, ev) {
|
| 171 |
+
const ul = document.getElementById("glance" + side.toUpperCase());
|
| 172 |
+
if (!ul) return;
|
| 173 |
+
const rows = [];
|
| 174 |
+
if (ev.sandy) {
|
| 175 |
+
rows.push({c: "hit", mark: "■", html: "Inside <strong>Sandy 2012</strong> inundation extent"});
|
| 176 |
+
} else {
|
| 177 |
+
rows.push({c: "miss", mark: "□", html: "Outside Sandy 2012 inundation extent"});
|
| 178 |
+
}
|
| 179 |
+
const dep = ev.dep || {};
|
| 180 |
+
const depHits = Object.entries(dep).filter(([_, v]) => (v.depth_class || 0) > 0);
|
| 181 |
+
if (depHits.length) {
|
| 182 |
+
for (const [scen, v] of depHits) {
|
| 183 |
+
const lbl = scen.replace("dep_", "").replace(/_/g, " ");
|
| 184 |
+
rows.push({c: "hit", mark: "■", html: `Inside DEP ${lbl} — <strong>${v.depth_label}</strong>`});
|
| 185 |
+
}
|
| 186 |
+
} else {
|
| 187 |
+
rows.push({c: "miss", mark: "□", html: "Outside all DEP stormwater scenarios"});
|
| 188 |
+
}
|
| 189 |
+
const fn = ev.floodnet;
|
| 190 |
+
if (fn && fn.n_sensors) {
|
| 191 |
+
if (fn.n_flood_events_3y > 0) {
|
| 192 |
+
const peak = fn.peak_event;
|
| 193 |
+
const peakStr = peak && peak.max_depth_mm
|
| 194 |
+
? `, peak <span class="gnum">${peak.max_depth_mm} mm</span>` : '';
|
| 195 |
+
rows.push({c: "hit", mark: "■",
|
| 196 |
+
html: `<span class="gnum">${fn.n_flood_events_3y}</span> FloodNet events (3 yr)${peakStr}`});
|
| 197 |
+
} else {
|
| 198 |
+
rows.push({c: "miss", mark: "□",
|
| 199 |
+
html: `<span class="gnum">${fn.n_sensors}</span> FloodNet sensor(s), no events`});
|
| 200 |
+
}
|
| 201 |
+
}
|
| 202 |
+
const ida = ev.ida_hwm;
|
| 203 |
+
if (ida && ida.n_within_radius > 0) {
|
| 204 |
+
const ht = ida.max_height_above_gnd_ft != null
|
| 205 |
+
? `up to <span class="gnum">${ida.max_height_above_gnd_ft} ft</span> above ground` : '';
|
| 206 |
+
rows.push({c: "hit", mark: "■",
|
| 207 |
+
html: `<span class="gnum">${ida.n_within_radius}</span> Ida 2021 HWMs ≤${ida.radius_m} m${ht ? ', ' + ht : ''}`});
|
| 208 |
+
}
|
| 209 |
+
const mt = ev.microtopo;
|
| 210 |
+
if (mt) {
|
| 211 |
+
rows.push({c: "note", mark: "◆",
|
| 212 |
+
html: `Elevation <span class="gnum">${mt.point_elev_m} m</span>, lower than <span class="gnum">${mt.rel_elev_pct_200m}%</span> of nearby (200 m)`});
|
| 213 |
+
}
|
| 214 |
+
const c311 = ev.nyc311;
|
| 215 |
+
if (c311 && c311.n > 0) {
|
| 216 |
+
rows.push({c: "note", mark: "◆",
|
| 217 |
+
html: `<span class="gnum">${c311.n}</span> 311 flood complaints ≤${c311.radius_m} m, ${c311.years} yr`});
|
| 218 |
+
}
|
| 219 |
+
ul.innerHTML = rows
|
| 220 |
+
.map(r => `<li class="${r.c}"><span class="gmark">${r.mark}</span><span class="gtext">${r.html}</span></li>`).join("");
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
function renderSources(side, ev, paraText) {
|
| 224 |
+
const fired = new Set([...(paraText || "").matchAll(/\[([a-z0-9_]+)\]/g)].map(m => m[1]));
|
| 225 |
+
const order = [
|
| 226 |
+
"sandy", "dep_extreme_2080", "dep_moderate_2050", "dep_moderate_current",
|
| 227 |
+
"floodnet", "ida_hwm", "microtopo", "nyc311",
|
| 228 |
+
"rag_dep_2013", "rag_nycha", "rag_coned", "rag_mta", "rag_comptroller",
|
| 229 |
+
];
|
| 230 |
+
const present = new Set();
|
| 231 |
+
if (ev.sandy) present.add("sandy");
|
| 232 |
+
for (const [k, v] of Object.entries(ev.dep || {})) {
|
| 233 |
+
if ((v.depth_class || 0) > 0) present.add(k);
|
| 234 |
+
}
|
| 235 |
+
if (ev.floodnet && ev.floodnet.n_sensors > 0) present.add("floodnet");
|
| 236 |
+
if (ev.ida_hwm && ev.ida_hwm.n_within_radius > 0) present.add("ida_hwm");
|
| 237 |
+
if (ev.microtopo) present.add("microtopo");
|
| 238 |
+
if (ev.nyc311 && ev.nyc311.n > 0) present.add("nyc311");
|
| 239 |
+
if (ev.rag) for (const h of ev.rag) present.add(h.doc_id);
|
| 240 |
+
|
| 241 |
+
const ol = document.getElementById("sources" + side.toUpperCase());
|
| 242 |
+
ol.innerHTML = order.filter(d => present.has(d)).map(d => {
|
| 243 |
+
const label = SOURCE_LABELS[d] || d;
|
| 244 |
+
const dim = fired.has(d) ? "" : ' style="opacity:0.5"';
|
| 245 |
+
return `<li${dim}><span class="src-tag">${d}</span><span class="src-cite">${label}</span></li>`;
|
| 246 |
+
}).join("");
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
async function updateMapForSide(side, geo) {
|
| 250 |
+
ensureMap();
|
| 251 |
+
if (!map.loaded()) await new Promise(res => map.once("load", res));
|
| 252 |
+
const sideKey = side.toLowerCase();
|
| 253 |
+
map.getSource("addr_" + sideKey).setData({
|
| 254 |
+
type: "FeatureCollection",
|
| 255 |
+
features: [{ type: "Feature", geometry: { type: "Point", coordinates: [geo.lon, geo.lat] }, properties: {} }],
|
| 256 |
+
});
|
| 257 |
+
const url = (p) => `${p}?lat=${geo.lat}&lon=${geo.lon}&r=1500`;
|
| 258 |
+
const [sandy, dep, fn] = await Promise.all([
|
| 259 |
+
fetch(url("/api/layers/sandy")).then(r => r.json()).catch(() => null),
|
| 260 |
+
fetch(url("/api/layers/dep_extreme_2080")).then(r => r.json()).catch(() => null),
|
| 261 |
+
fetch(`/api/floodnet_near?lat=${geo.lat}&lon=${geo.lon}&r=1000`).then(r => r.json()).catch(() => null),
|
| 262 |
+
]);
|
| 263 |
+
if (sandy) map.getSource("sandy_" + sideKey).setData(sandy);
|
| 264 |
+
if (dep) map.getSource("dep_" + sideKey).setData(dep);
|
| 265 |
+
if (fn) map.getSource("fn_" + sideKey).setData(fn);
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
function fitBoth(ga, gb) {
|
| 269 |
+
if (!ga || !gb || !map.loaded()) return;
|
| 270 |
+
const bounds = new maplibregl.LngLatBounds()
|
| 271 |
+
.extend([ga.lon, ga.lat]).extend([gb.lon, gb.lat]);
|
| 272 |
+
map.fitBounds(bounds, { padding: 80, duration: 800, maxZoom: 13 });
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
let geoA = null, geoB = null;
|
| 276 |
+
|
| 277 |
+
document.querySelectorAll(".chip[data-a]").forEach((btn) => {
|
| 278 |
+
btn.addEventListener("click", (e) => {
|
| 279 |
+
e.preventDefault();
|
| 280 |
+
document.getElementById("qa").value = btn.getAttribute("data-a");
|
| 281 |
+
document.getElementById("qb").value = btn.getAttribute("data-b");
|
| 282 |
+
document.getElementById("cform").requestSubmit();
|
| 283 |
+
});
|
| 284 |
+
});
|
| 285 |
+
|
| 286 |
+
document.getElementById("cform").addEventListener("submit", (e) => {
|
| 287 |
+
e.preventDefault();
|
| 288 |
+
const a = document.getElementById("qa").value.trim();
|
| 289 |
+
const b = document.getElementById("qb").value.trim();
|
| 290 |
+
if (!a || !b) return;
|
| 291 |
+
document.getElementById("aTitle").textContent = a;
|
| 292 |
+
document.getElementById("bTitle").textContent = b;
|
| 293 |
+
resetSide("a"); resetSide("b");
|
| 294 |
+
ensureMap();
|
| 295 |
+
geoA = geoB = null;
|
| 296 |
+
document.getElementById("cgo").disabled = true;
|
| 297 |
+
if (evtSrc) evtSrc.close();
|
| 298 |
+
evtSrc = new EventSource(`/api/compare?a=${encodeURIComponent(a)}&b=${encodeURIComponent(b)}`);
|
| 299 |
+
|
| 300 |
+
evtSrc.addEventListener("step", (msg) => {
|
| 301 |
+
const ev = JSON.parse(msg.data);
|
| 302 |
+
markStep(ev.side, ev.step, ev);
|
| 303 |
+
});
|
| 304 |
+
evtSrc.addEventListener("final", (msg) => {
|
| 305 |
+
const ev = JSON.parse(msg.data);
|
| 306 |
+
const side = ev.side;
|
| 307 |
+
document.getElementById("report" + side.toUpperCase()).classList.remove("hidden");
|
| 308 |
+
if (ev.geocode) {
|
| 309 |
+
if (side === "a") geoA = ev.geocode; else geoB = ev.geocode;
|
| 310 |
+
updateMapForSide(side, ev.geocode).then(() => fitBoth(geoA, geoB));
|
| 311 |
+
}
|
| 312 |
+
if (ev.paragraph) renderParagraph(side, ev.paragraph);
|
| 313 |
+
renderGlance(side, ev);
|
| 314 |
+
renderSources(side, ev, ev.paragraph || "");
|
| 315 |
+
});
|
| 316 |
+
evtSrc.addEventListener("done", () => {
|
| 317 |
+
document.getElementById("cgo").disabled = false;
|
| 318 |
+
evtSrc.close();
|
| 319 |
+
});
|
| 320 |
+
evtSrc.addEventListener("error", () => {
|
| 321 |
+
document.getElementById("cgo").disabled = false;
|
| 322 |
+
});
|
| 323 |
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
| 6 |
+
<title>Riprap — NYC public schools flood exposure register</title>
|
| 7 |
+
<link rel="stylesheet" href="/static/style.css">
|
| 8 |
+
<link rel="stylesheet" href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css">
|
| 9 |
+
<script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
|
| 10 |
+
</head>
|
| 11 |
+
<body class="register-mode">
|
| 12 |
+
|
| 13 |
+
<header class="topbar">
|
| 14 |
+
<div class="topbar-inner">
|
| 15 |
+
<div class="brand">
|
| 16 |
+
<span class="brand-name">Riprap</span>
|
| 17 |
+
<span class="brand-sep">·</span>
|
| 18 |
+
<span class="brand-tag">NYC public schools — flood exposure register</span>
|
| 19 |
+
</div>
|
| 20 |
+
<div class="topbar-right">
|
| 21 |
+
<a href="/" class="modelink">address</a>
|
| 22 |
+
<a href="/compare" class="modelink">compare</a>
|
| 23 |
+
<select id="classPicker" class="topbar-select">
|
| 24 |
+
<option value="schools">DOE schools</option>
|
| 25 |
+
<option value="nycha">NYCHA developments</option>
|
| 26 |
+
<option value="mta_entrances">MTA subway entrances</option>
|
| 27 |
+
</select>
|
| 28 |
+
<span class="local-pill" title="Pre-computed by the same FSM, bulk mode."><span class="dot"></span>bulk · pre-computed</span>
|
| 29 |
+
</div>
|
| 30 |
+
</div>
|
| 31 |
+
</header>
|
| 32 |
+
|
| 33 |
+
<div class="register-summary">
|
| 34 |
+
<div class="reg-stat"><span class="reg-stat-num" id="totalCount">—</span><span class="reg-stat-lbl">schools assessed</span></div>
|
| 35 |
+
<div class="reg-stat tier-1"><span class="reg-stat-num" id="tier1Count">—</span><span class="reg-stat-lbl">Tier 1</span></div>
|
| 36 |
+
<div class="reg-stat tier-2"><span class="reg-stat-num" id="tier2Count">—</span><span class="reg-stat-lbl">Tier 2</span></div>
|
| 37 |
+
<div class="reg-stat tier-3"><span class="reg-stat-num" id="tier3Count">—</span><span class="reg-stat-lbl">Tier 3</span></div>
|
| 38 |
+
<div class="reg-stat-spacer"></div>
|
| 39 |
+
<div class="reg-controls">
|
| 40 |
+
<input type="text" id="filter" placeholder="filter by name, address, or borough…" />
|
| 41 |
+
<select id="boroughFilter">
|
| 42 |
+
<option value="">All boroughs</option>
|
| 43 |
+
<option>Manhattan</option><option>Bronx</option>
|
| 44 |
+
<option>Brooklyn</option><option>Queens</option><option>Staten Island</option>
|
| 45 |
+
</select>
|
| 46 |
+
<button id="exportCsv" class="btn-secondary" type="button">CSV</button>
|
| 47 |
+
<button id="exportGeojson" class="btn-secondary" type="button">GeoJSON</button>
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
|
| 51 |
+
<div class="register-workbench">
|
| 52 |
+
<section class="reg-table-wrap panel">
|
| 53 |
+
<h2>Ranked by exposure score<span class="hint">click any row for details</span></h2>
|
| 54 |
+
<div class="reg-tablescroll">
|
| 55 |
+
<table id="regTable">
|
| 56 |
+
<thead>
|
| 57 |
+
<tr>
|
| 58 |
+
<th>Tier</th>
|
| 59 |
+
<th>Score</th>
|
| 60 |
+
<th>School</th>
|
| 61 |
+
<th>Borough</th>
|
| 62 |
+
<th class="num">Sandy</th>
|
| 63 |
+
<th class="num">DEP 2080</th>
|
| 64 |
+
<th class="num">311</th>
|
| 65 |
+
<th class="num">FloodNet</th>
|
| 66 |
+
<th class="num">Ida</th>
|
| 67 |
+
</tr>
|
| 68 |
+
</thead>
|
| 69 |
+
<tbody id="regBody"></tbody>
|
| 70 |
+
</table>
|
| 71 |
+
</div>
|
| 72 |
+
</section>
|
| 73 |
+
|
| 74 |
+
<aside class="reg-detail panel" id="regDetail">
|
| 75 |
+
<h2>Detail<span class="hint">select a school</span></h2>
|
| 76 |
+
<div class="reg-detail-empty" id="detailEmpty">
|
| 77 |
+
Select a row to load the cited per-asset report.
|
| 78 |
+
</div>
|
| 79 |
+
<div class="reg-detail-body hidden" id="detailBody">
|
| 80 |
+
<div id="detailHeader" class="reg-detail-header"></div>
|
| 81 |
+
<div id="detailMap"></div>
|
| 82 |
+
<div class="report-section"><h3>At a glance</h3><ul id="detailGlance" class="glance-list"></ul></div>
|
| 83 |
+
<div class="report-section"><h3>Summary</h3><p id="detailParagraph"></p>
|
| 84 |
+
<div id="detailNoPara" class="hint" style="margin-top: 8px;">
|
| 85 |
+
Tier-3 paragraphs are computed on demand. <button id="livePara" type="button" class="btn-secondary">Generate cited paragraph (~10 s)</button>
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
<div class="report-section"><h3>Sources fired</h3><ol id="detailSources" class="sources-list"></ol></div>
|
| 89 |
+
</div>
|
| 90 |
+
</aside>
|
| 91 |
+
</div>
|
| 92 |
+
|
| 93 |
+
<footer>
|
| 94 |
+
<div class="foot-inner">
|
| 95 |
+
<div class="foot-col">
|
| 96 |
+
<h3>Bulk mode</h3>
|
| 97 |
+
<p>
|
| 98 |
+
The same 8-specialist FSM that answers a single address is run
|
| 99 |
+
in batch over every NYC public school (1,992 from the DOE
|
| 100 |
+
location dataset). Each row is scored on a transparent rubric —
|
| 101 |
+
Sandy 2012 inundation, DEP stormwater scenarios, FloodNet sensor
|
| 102 |
+
history, USGS Ida 2021 high-water marks, LiDAR micro-topography,
|
| 103 |
+
NYC 311 flood complaints. Tier-1 and Tier-2 schools have their
|
| 104 |
+
full Granite-reconciled cited paragraph pre-computed; Tier-3
|
| 105 |
+
paragraphs are generated on click.
|
| 106 |
+
</p>
|
| 107 |
+
</div>
|
| 108 |
+
<div class="foot-col">
|
| 109 |
+
<h3>Source</h3>
|
| 110 |
+
<p>
|
| 111 |
+
NYC DOE 2019-2020 School Point Locations
|
| 112 |
+
(<a href="https://data.cityofnewyork.us/Education/2019-2020-School-Point-Locations/a3nt-yts4" target="_blank">a3nt-yts4</a>).
|
| 113 |
+
Same data substrate as the address mode.
|
| 114 |
+
</p>
|
| 115 |
+
</div>
|
| 116 |
+
</div>
|
| 117 |
+
</footer>
|
| 118 |
+
|
| 119 |
+
<script src="/static/register.js"></script>
|
| 120 |
+
</body>
|
| 121 |
+
</html>
|
|
@@ -0,0 +1,315 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Riprap — Bulk mode (schools register).
|
| 2 |
+
|
| 3 |
+
const SOURCE_LABELS = {
|
| 4 |
+
geocode: "NYC DCP Geosearch",
|
| 5 |
+
sandy: "NYC OpenData 5xsi-dfpx — Sandy 2012 inundation",
|
| 6 |
+
dep_extreme_2080: "NYC DEP Stormwater — Extreme 3.66 in/hr + 2080 SLR",
|
| 7 |
+
dep_moderate_2050: "NYC DEP Stormwater — Moderate 2.13 in/hr + 2050 SLR",
|
| 8 |
+
dep_moderate_current: "NYC DEP Stormwater — Moderate 2.13 in/hr current",
|
| 9 |
+
floodnet: "FloodNet NYC — live ultrasonic sensor network",
|
| 10 |
+
nyc311: "NYC 311 (Socrata erm2-nwe9) — flood descriptors",
|
| 11 |
+
microtopo: "USGS 3DEP 30 m DEM via py3dep",
|
| 12 |
+
ida_hwm: "USGS STN — Hurricane Ida 2021 HWMs (Event 312, NY)",
|
| 13 |
+
prithvi_water: "Prithvi-EO 2.0 (300M, NASA/IBM) Sen1Floods11 — satellite water segmentation",
|
| 14 |
+
rag_dep_2013: "NYC DEP Wastewater Resiliency Plan (2013)",
|
| 15 |
+
rag_nycha: "NYCHA — Flood Resilience: Lessons Learned",
|
| 16 |
+
rag_coned: "Con Edison Climate Change Resilience Plan (Case 22-E-0222)",
|
| 17 |
+
rag_mta: "MTA Climate Resilience Roadmap (Oct 2025)",
|
| 18 |
+
rag_comptroller: "NYC Comptroller — \"Is NYC Ready for Rain?\" (2024)",
|
| 19 |
+
};
|
| 20 |
+
|
| 21 |
+
const $ = (s) => document.querySelector(s);
|
| 22 |
+
|
| 23 |
+
let allRows = [];
|
| 24 |
+
let filteredRows = [];
|
| 25 |
+
let selected = null;
|
| 26 |
+
let detailMap = null;
|
| 27 |
+
|
| 28 |
+
function tierBadge(t) {
|
| 29 |
+
const cls = "tier-badge tier-" + t;
|
| 30 |
+
return `<span class="${cls}">${t}</span>`;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
function yn(b) { return b ? '<span class="yn yes">●</span>' : '<span class="yn no">○</span>'; }
|
| 34 |
+
|
| 35 |
+
function renderTable() {
|
| 36 |
+
const tbody = $("#regBody");
|
| 37 |
+
tbody.innerHTML = filteredRows.map((r, i) => {
|
| 38 |
+
const snap = r.snap || {};
|
| 39 |
+
const sandy = snap.sandy ? yn(true) : yn(false);
|
| 40 |
+
const dep80 = snap.dep && snap.dep.dep_extreme_2080 && snap.dep.dep_extreme_2080.depth_class > 0 ? yn(true) : yn(false);
|
| 41 |
+
const c311 = (snap.nyc311 && snap.nyc311.n) || 0;
|
| 42 |
+
const fnEv = (snap.floodnet && snap.floodnet.n_flood_events_3y) || 0;
|
| 43 |
+
const idaN = (snap.ida_hwm && snap.ida_hwm.n_within_radius) || 0;
|
| 44 |
+
return `<tr data-idx="${i}">
|
| 45 |
+
<td>${tierBadge(r.tier)}</td>
|
| 46 |
+
<td class="num">${r.score}</td>
|
| 47 |
+
<td>
|
| 48 |
+
<div class="rname">${r.name}</div>
|
| 49 |
+
<div class="raddr">${r.address || ""}</div>
|
| 50 |
+
</td>
|
| 51 |
+
<td>${r.borough || ""}</td>
|
| 52 |
+
<td class="num">${sandy}</td>
|
| 53 |
+
<td class="num">${dep80}</td>
|
| 54 |
+
<td class="num">${c311 || ""}</td>
|
| 55 |
+
<td class="num">${fnEv || ""}</td>
|
| 56 |
+
<td class="num">${idaN || ""}</td>
|
| 57 |
+
</tr>`;
|
| 58 |
+
}).join("");
|
| 59 |
+
|
| 60 |
+
tbody.querySelectorAll("tr").forEach((tr) => {
|
| 61 |
+
tr.addEventListener("click", () => selectRow(parseInt(tr.dataset.idx, 10)));
|
| 62 |
+
});
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
function applyFilters() {
|
| 66 |
+
const q = ($("#filter").value || "").toLowerCase();
|
| 67 |
+
const boro = $("#boroughFilter").value || "";
|
| 68 |
+
filteredRows = allRows.filter((r) => {
|
| 69 |
+
if (boro && r.borough !== boro) return false;
|
| 70 |
+
if (!q) return true;
|
| 71 |
+
return (
|
| 72 |
+
(r.name || "").toLowerCase().includes(q) ||
|
| 73 |
+
(r.address || "").toLowerCase().includes(q) ||
|
| 74 |
+
(r.borough || "").toLowerCase().includes(q) ||
|
| 75 |
+
(r.bbl || "").toString().includes(q)
|
| 76 |
+
);
|
| 77 |
+
});
|
| 78 |
+
renderTable();
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
function selectRow(idx) {
|
| 82 |
+
selected = filteredRows[idx];
|
| 83 |
+
if (!selected) return;
|
| 84 |
+
$("#detailEmpty").classList.add("hidden");
|
| 85 |
+
$("#detailBody").classList.remove("hidden");
|
| 86 |
+
$("#detailHeader").innerHTML = `
|
| 87 |
+
<div class="rname">${selected.name}</div>
|
| 88 |
+
<div class="raddr">${selected.address}, ${selected.borough}</div>
|
| 89 |
+
<div class="rmeta">BBL: ${selected.bbl || "—"} · Tier ${selected.tier} · Score ${selected.score}</div>`;
|
| 90 |
+
renderDetail(selected);
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
function renderDetail(row) {
|
| 94 |
+
const snap = row.snap || {};
|
| 95 |
+
// glance
|
| 96 |
+
const rows = [];
|
| 97 |
+
if (snap.sandy) rows.push({c:"hit", mark:"■", html:"Inside <strong>Sandy 2012</strong> inundation extent"});
|
| 98 |
+
else rows.push({c:"miss",mark:"□", html:"Outside Sandy 2012 inundation extent"});
|
| 99 |
+
const dep = snap.dep || {};
|
| 100 |
+
const depHits = Object.entries(dep).filter(([_,v]) => (v.depth_class || 0) > 0);
|
| 101 |
+
if (depHits.length) {
|
| 102 |
+
for (const [scen, v] of depHits) {
|
| 103 |
+
const lbl = scen.replace("dep_","").replace(/_/g, " ");
|
| 104 |
+
rows.push({c:"hit", mark:"■", html:`Inside DEP ${lbl} — <strong>${v.depth_label}</strong>`});
|
| 105 |
+
}
|
| 106 |
+
} else {
|
| 107 |
+
rows.push({c:"miss", mark:"□", html:"Outside all DEP stormwater scenarios"});
|
| 108 |
+
}
|
| 109 |
+
const fn = snap.floodnet;
|
| 110 |
+
if (fn && fn.n_sensors) {
|
| 111 |
+
if (fn.n_flood_events_3y > 0) {
|
| 112 |
+
const peak = fn.peak_event;
|
| 113 |
+
const peakStr = peak && peak.max_depth_mm ? `, peak <span class="gnum">${peak.max_depth_mm} mm</span>` : "";
|
| 114 |
+
rows.push({c:"hit", mark:"■", html:`<span class="gnum">${fn.n_flood_events_3y}</span> FloodNet events (3 yr)${peakStr}`});
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
const ida = snap.ida_hwm;
|
| 118 |
+
if (ida && ida.n_within_radius > 0) {
|
| 119 |
+
const ht = ida.max_height_above_gnd_ft != null ? `up to <span class="gnum">${ida.max_height_above_gnd_ft} ft</span> above ground` : "";
|
| 120 |
+
rows.push({c:"hit", mark:"■", html:`<span class="gnum">${ida.n_within_radius}</span> Ida HWMs ≤${ida.radius_m} m${ht ? ", " + ht : ""}`});
|
| 121 |
+
}
|
| 122 |
+
const mt = snap.microtopo;
|
| 123 |
+
if (mt) {
|
| 124 |
+
rows.push({c:"note", mark:"◆", html:`Elevation <span class="gnum">${mt.point_elev_m} m</span>; lower than <span class="gnum">${mt.rel_elev_pct_200m}%</span> of nearby (200 m)`});
|
| 125 |
+
}
|
| 126 |
+
const c311 = snap.nyc311;
|
| 127 |
+
if (c311 && c311.n > 0) {
|
| 128 |
+
rows.push({c:"note", mark:"◆", html:`<span class="gnum">${c311.n}</span> 311 flood complaints ≤${c311.radius_m} m, ${c311.years} yr`});
|
| 129 |
+
}
|
| 130 |
+
$("#detailGlance").innerHTML = rows.map(r =>
|
| 131 |
+
`<li class="${r.c}"><span class="gmark">${r.mark}</span><span class="gtext">${r.html}</span></li>`).join("");
|
| 132 |
+
|
| 133 |
+
// paragraph
|
| 134 |
+
const para = snap.paragraph;
|
| 135 |
+
const noPara = $("#detailNoPara");
|
| 136 |
+
if (para) {
|
| 137 |
+
$("#detailParagraph").innerHTML = (para || "").replace(/\[([a-z0-9_]+)\]/gi,
|
| 138 |
+
(_, d) => `<span class="cite" title="document id: ${d}">[${d}]</span>`);
|
| 139 |
+
noPara.classList.add("hidden");
|
| 140 |
+
} else {
|
| 141 |
+
$("#detailParagraph").innerHTML = "";
|
| 142 |
+
noPara.classList.remove("hidden");
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
// sources
|
| 146 |
+
const fired = new Set([...(para || "").matchAll(/\[([a-z0-9_]+)\]/g)].map(m => m[1]));
|
| 147 |
+
const order = [
|
| 148 |
+
"sandy", "dep_extreme_2080", "dep_moderate_2050", "dep_moderate_current",
|
| 149 |
+
"floodnet", "ida_hwm", "microtopo", "nyc311",
|
| 150 |
+
"rag_dep_2013", "rag_nycha", "rag_coned", "rag_mta", "rag_comptroller",
|
| 151 |
+
];
|
| 152 |
+
const present = new Set();
|
| 153 |
+
if (snap.sandy) present.add("sandy");
|
| 154 |
+
for (const [k, v] of Object.entries(snap.dep || {})) {
|
| 155 |
+
if ((v.depth_class || 0) > 0) present.add(k);
|
| 156 |
+
}
|
| 157 |
+
if (snap.floodnet && snap.floodnet.n_sensors > 0) present.add("floodnet");
|
| 158 |
+
if (snap.ida_hwm && snap.ida_hwm.n_within_radius > 0) present.add("ida_hwm");
|
| 159 |
+
if (snap.microtopo) present.add("microtopo");
|
| 160 |
+
if (snap.nyc311 && snap.nyc311.n > 0) present.add("nyc311");
|
| 161 |
+
if (snap.rag) for (const h of snap.rag) present.add(h.doc_id);
|
| 162 |
+
|
| 163 |
+
$("#detailSources").innerHTML = order.filter(d => present.has(d)).map(d => {
|
| 164 |
+
const dim = fired.has(d) ? "" : ' style="opacity:0.5"';
|
| 165 |
+
return `<li${dim}><span class="src-tag">${d}</span><span class="src-cite">${SOURCE_LABELS[d] || d}</span></li>`;
|
| 166 |
+
}).join("");
|
| 167 |
+
|
| 168 |
+
showDetailMap(row);
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
function showDetailMap(row) {
|
| 172 |
+
const div = $("#detailMap");
|
| 173 |
+
if (!detailMap) {
|
| 174 |
+
detailMap = new maplibregl.Map({
|
| 175 |
+
container: "detailMap",
|
| 176 |
+
style: {
|
| 177 |
+
version: 8,
|
| 178 |
+
sources: {
|
| 179 |
+
carto: { type: "raster",
|
| 180 |
+
tiles: ["https://a.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png"],
|
| 181 |
+
tileSize: 256, attribution: "© OSM · CARTO" },
|
| 182 |
+
},
|
| 183 |
+
layers: [{ id: "bg", type: "raster", source: "carto" }],
|
| 184 |
+
},
|
| 185 |
+
center: [row.lon, row.lat], zoom: 14, attributionControl: { compact: true },
|
| 186 |
+
});
|
| 187 |
+
detailMap.on("load", () => {
|
| 188 |
+
detailMap.addSource("addr", { type: "geojson", data: { type:"FeatureCollection", features:[] }});
|
| 189 |
+
detailMap.addLayer({ id:"addr-marker", type:"circle", source:"addr",
|
| 190 |
+
paint: { "circle-radius": 9, "circle-color": "#1642DF", "circle-stroke-color":"#fff", "circle-stroke-width": 2.5 }});
|
| 191 |
+
});
|
| 192 |
+
}
|
| 193 |
+
const setMarker = () => {
|
| 194 |
+
detailMap.getSource("addr").setData({ type:"FeatureCollection", features:[
|
| 195 |
+
{ type:"Feature", geometry:{ type:"Point", coordinates:[row.lon, row.lat] }, properties:{} }
|
| 196 |
+
]});
|
| 197 |
+
detailMap.flyTo({ center: [row.lon, row.lat], zoom: 14 });
|
| 198 |
+
};
|
| 199 |
+
if (detailMap.loaded()) setMarker(); else detailMap.once("load", setMarker);
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
async function generateLiveParagraph() {
|
| 203 |
+
if (!selected) return;
|
| 204 |
+
const btn = $("#livePara"); btn.disabled = true; btn.textContent = "Generating…";
|
| 205 |
+
try {
|
| 206 |
+
const u = `/api/stream?q=${encodeURIComponent(selected.address + ", " + selected.borough)}`;
|
| 207 |
+
// collect SSE final event
|
| 208 |
+
const text = await new Promise((resolve, reject) => {
|
| 209 |
+
const es = new EventSource(u);
|
| 210 |
+
es.addEventListener("final", (m) => { resolve(JSON.parse(m.data)); es.close(); });
|
| 211 |
+
es.addEventListener("error", () => { reject(new Error("stream error")); es.close(); });
|
| 212 |
+
setTimeout(() => { reject(new Error("timeout")); es.close(); }, 90000);
|
| 213 |
+
});
|
| 214 |
+
if (text.paragraph) {
|
| 215 |
+
selected.snap.paragraph = text.paragraph;
|
| 216 |
+
selected.snap.rag = text.rag || selected.snap.rag;
|
| 217 |
+
renderDetail(selected);
|
| 218 |
+
}
|
| 219 |
+
} catch (e) {
|
| 220 |
+
btn.textContent = "Failed: " + e.message;
|
| 221 |
+
btn.disabled = false;
|
| 222 |
+
}
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
function exportCsv() {
|
| 226 |
+
const cols = ["tier","score","name","address","borough","bbl","bin",
|
| 227 |
+
"lat","lon","sandy","dep_extreme_2080","floodnet_events_3y",
|
| 228 |
+
"ida_hwms_800m","nyc311_5y"];
|
| 229 |
+
const lines = [cols.join(",")];
|
| 230 |
+
for (const r of filteredRows) {
|
| 231 |
+
const s = r.snap || {};
|
| 232 |
+
const row = [
|
| 233 |
+
r.tier, r.score, JSON.stringify(r.name), JSON.stringify(r.address || ""),
|
| 234 |
+
r.borough || "", r.bbl || "", r.bin || "", r.lat, r.lon,
|
| 235 |
+
s.sandy ? 1 : 0,
|
| 236 |
+
(s.dep && s.dep.dep_extreme_2080 && s.dep.dep_extreme_2080.depth_class) || 0,
|
| 237 |
+
(s.floodnet && s.floodnet.n_flood_events_3y) || 0,
|
| 238 |
+
(s.ida_hwm && s.ida_hwm.n_within_radius) || 0,
|
| 239 |
+
(s.nyc311 && s.nyc311.n) || 0,
|
| 240 |
+
];
|
| 241 |
+
lines.push(row.join(","));
|
| 242 |
+
}
|
| 243 |
+
const blob = new Blob([lines.join("\n")], { type: "text/csv" });
|
| 244 |
+
download(blob, "riprap_schools_register.csv");
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
function exportGeojson() {
|
| 248 |
+
const features = filteredRows.map((r) => ({
|
| 249 |
+
type: "Feature",
|
| 250 |
+
geometry: { type: "Point", coordinates: [r.lon, r.lat] },
|
| 251 |
+
properties: {
|
| 252 |
+
tier: r.tier, score: r.score, name: r.name, address: r.address,
|
| 253 |
+
borough: r.borough, bbl: r.bbl, bin: r.bin,
|
| 254 |
+
sandy: !!(r.snap && r.snap.sandy),
|
| 255 |
+
dep_extreme_2080: (r.snap && r.snap.dep && r.snap.dep.dep_extreme_2080 && r.snap.dep.dep_extreme_2080.depth_class) || 0,
|
| 256 |
+
},
|
| 257 |
+
}));
|
| 258 |
+
const blob = new Blob([JSON.stringify({ type: "FeatureCollection", features })],
|
| 259 |
+
{ type: "application/geo+json" });
|
| 260 |
+
download(blob, "riprap_schools_register.geojson");
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
function download(blob, filename) {
|
| 264 |
+
const u = URL.createObjectURL(blob);
|
| 265 |
+
const a = document.createElement("a");
|
| 266 |
+
a.href = u; a.download = filename;
|
| 267 |
+
document.body.appendChild(a); a.click();
|
| 268 |
+
setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(u); }, 200);
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
// Asset class is parsed from URL path: /register/<asset_class>
|
| 272 |
+
const ASSET_CLASS = (location.pathname.match(/\/register\/([^\/?]+)/) || [])[1] || "schools";
|
| 273 |
+
|
| 274 |
+
const ASSET_TITLES = {
|
| 275 |
+
schools: "NYC public schools — flood exposure register",
|
| 276 |
+
nycha: "NYCHA developments — flood exposure register",
|
| 277 |
+
mta_entrances: "MTA subway entrances — flood exposure register",
|
| 278 |
+
};
|
| 279 |
+
document.title = `Riprap — ${ASSET_TITLES[ASSET_CLASS] || ASSET_CLASS}`;
|
| 280 |
+
const tagSpan = document.querySelector(".brand-tag");
|
| 281 |
+
if (tagSpan) tagSpan.textContent = ASSET_TITLES[ASSET_CLASS] || ASSET_CLASS;
|
| 282 |
+
|
| 283 |
+
const classPicker = document.getElementById("classPicker");
|
| 284 |
+
if (classPicker) {
|
| 285 |
+
classPicker.value = ASSET_CLASS;
|
| 286 |
+
classPicker.addEventListener("change", () => {
|
| 287 |
+
location.href = `/register/${classPicker.value}`;
|
| 288 |
+
});
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
(async function init() {
|
| 292 |
+
const r = await fetch(`/api/register/${ASSET_CLASS}`);
|
| 293 |
+
if (!r.ok) {
|
| 294 |
+
const script = `python scripts/build_${ASSET_CLASS}_register.py`;
|
| 295 |
+
$("#regBody").innerHTML = `<tr><td colspan="9" style="padding: 20px; color: var(--text-muted);">Register not built. Run <code>${script}</code>.</td></tr>`;
|
| 296 |
+
return;
|
| 297 |
+
}
|
| 298 |
+
const data = await r.json();
|
| 299 |
+
allRows = data.rows || [];
|
| 300 |
+
filteredRows = allRows.slice();
|
| 301 |
+
|
| 302 |
+
// tier counts
|
| 303 |
+
$("#totalCount").textContent = allRows.length;
|
| 304 |
+
$("#tier1Count").textContent = allRows.filter(r => r.tier === 1).length;
|
| 305 |
+
$("#tier2Count").textContent = allRows.filter(r => r.tier === 2).length;
|
| 306 |
+
$("#tier3Count").textContent = allRows.filter(r => r.tier === 3).length;
|
| 307 |
+
|
| 308 |
+
renderTable();
|
| 309 |
+
|
| 310 |
+
$("#filter").addEventListener("input", applyFilters);
|
| 311 |
+
$("#boroughFilter").addEventListener("change", applyFilters);
|
| 312 |
+
$("#exportCsv").addEventListener("click", exportCsv);
|
| 313 |
+
$("#exportGeojson").addEventListener("click", exportGeojson);
|
| 314 |
+
$("#livePara").addEventListener("click", generateLiveParagraph);
|
| 315 |
+
})();
|