feat(p1): city flood stages and zones from CMU Water Center's 2024 flood map
CI / Format & lint (push) Successful in 49s
CI / Test suite (push) Successful in 59s
Docs / Validate documentation (push) Successful in 27s
Security / Dependency vulnerabilities (push) Successful in 1m5s
Security / Static analysis (push) Successful in 17s
Security / License report (push) Successful in 45s
Rust / fmt, clippy, test, build (push) Successful in 3m42s

CMU Water Center's Chiang Mai flood hazard map (CMFlood) replaces the older
municipal map. It is built on the October 2024 flood and has the Ping leave
its banks above P.1 4.20 m, with five stages: 4.30, 4.50, 4.70, 5.00 and
5.30 m (the 2024 record). The old map's seven stages (3.70-4.60 m) said the
city floods from 3.70 m; CMU calls that map out of date.

- P1_FLOOD_STAGES: the five CMU stages, discharge on RID's 2024 rating.
  /forecast's P.1 `stages` now has five entries (same format). Thresholds
  are unchanged: 4.20 m danger is the bank top, 3.70 m warning half a metre
  below (as on CMU's gauge page). flood_config.json and the Rust parity
  fixtures regenerated (only the stages changed).
- flood-zones-p1.geojson: CMU's KML (L1..L5) as five disjoint zones, zone k
  floods first at stage k; scripts/build_flood_zones.py rebuilds it.
- Dashboard: CMU's colours and credit (map attribution); the stage grid follows
  the number of stages; zone lookup for markers honours holes. The city
  verdict uses statusOf's classes: flooding (over the banks) from 4.20 m,
  high water from 3.70 m. Zones show by themselves when the river is over its
  banks or stage 1 is >= 50 % likely. Alert, replay and verdict texts no
  longer call 3.70 m city flooding (EN + TH).
- Docs: DATA_SOURCES 5.1 (CMFlood: the map, flood marks, poles, city gauges),
  FLOOD_FORECASTING, DASHBOARD_REDESIGN, NOTIFICATIONS, README credits.
This commit is contained in:
grabowski committed 2026-10-26 14:19:52 +07:00
1 parent 180d95c6bd
commit 701bb87d34
13 files changed
+301 -101

No files matched your search

+3 -2
View File
@@ -194,8 +194,9 @@ models/ trained bundles (gitignored) and evaluation results
Royal Irrigation Department (RID) gauge telemetry and reservoir reports;
Hydro-Informatics Institute (HII) / ThaiWater open API; Open-Meteo; OpenStreetMap
contributors for the river geometry; Chiang Mai Municipality for the inundation map
the P.1 stages are keyed to. All instruments are theirs; we aggregate, store, fill
contributors for the river geometry; the CMU Water Center (Chiang Mai University)
for the flood hazard map of the 2024 flood that the P.1 stages and the city's flood
zones come from. All instruments are theirs; we aggregate, store, fill
gaps and forecast.
## License
+3 -3
View File
@@ -52,7 +52,7 @@ One screen, no page scroll on a desktop:
| Top bar | brand · **Chiang Mai verdict** (P.1 level + 24 h forecast, always visible, click selects P.1) · gauges reporting · highest 24 h flood chance · data age · EN/ไทย · light/dark · Get alerts |
| Left | two lists: **RID gauges** (status bar, code, name, margin to the warning level, 72 h sparkline, level and 6 h change; sort by river order, closest to the warning level, rising) and **Basin stations** (ThaiWater/HII: % of bank height, river and agency; sort fullest first or by name); filter; legend |
| Centre | map on a self-hosted vector basemap (labels in the page language): Ping mainstem and tributaries, gauges coloured by status with code + level labels, layers Basin stations (diamonds), Rain 24 h and Flood zones, the team's markers from zoom 12, one legend |
| Right | selected RID gauge: level, 6 h change, discharge, warning/danger level with margins, capacity; level scale with now and 24 h peak; history (3 d / 7 d / 30 d / 90 d / 1 y / all or dates: level, discharge, the 24 h peaks as issued, warning/danger bands, hover values; up to 7 d it ends in the current forecast); forecast table (peak, chance of warning/danger at 6/12/24 h, model, issue time, disclaimer); for P.1 the seven inundation stages, the zones toggle and "How accurate is the forecast?"; data links. A basin station shows level (m MSL), % of bank height with ThaiWater's class, distance to and height of the bank top, gauge reading, agency (latest reading only: no history is kept for them) |
| Right | selected RID gauge: level, 6 h change, discharge, warning/danger level with margins, capacity; level scale with now and 24 h peak; history (3 d / 7 d / 30 d / 90 d / 1 y / all or dates: level, discharge, the 24 h peaks as issued, warning/danger bands, hover values; up to 7 d it ends in the current forecast); forecast table (peak, chance of warning/danger at 6/12/24 h, model, issue time, disclaimer); for P.1 the five city flood stages, the zones toggle and "How accurate is the forecast?"; data links. A basin station shows level (m MSL), % of bank height with ThaiWater's class, distance to and height of the bank top, gauge reading, agency (latest reading only: no history is kept for them) |
| Status bar | data credits, "2.7M readings since 2018" (opens the data statistics), API, source code |
**Narrower screens:**
@@ -104,11 +104,11 @@ One screen, no page scroll on a desktop:
| Danger | level ≥ `threshold_danger` |
| No data | no reading for 24 h |
- **City verdict:** the old dashboard's rule — flooding from P.1 ≥ 3.70 m; "likely" at danger ≥ 50 % or warning ≥ 70 %; watch at warning ≥ 20 % or level ≥ 3.20 m.
- **City verdict:** the classes of `statusOf` for P.1 — flooding (the Ping over its banks) from 4.20 m, high water from 3.70 m; "flooding likely" at danger ≥ 50 %, "high water likely" at warning ≥ 70 %; watch at warning ≥ 20 % or level ≥ 3.20 m. (Until 2026-10-25 it said flooding from 3.70 m, after the older municipal map.)
- **Data age:** "feed stale" after 3 h, except during RID's overnight pause (21:00 to 09:00 with an evening reading). The pause shows as "overnight pause", the same rule as the notifier's `FEED_NIGHT_UNTIL_HOUR`.
- **Discharge:** comes with RID's report. When the newest hour is a level-only reading, the panel shows the last RID discharge with its time ("at 14:00").
- **Rain layer:** off by default unless any station has heavy rain (> 35 mm / 24 h); an explicit choice is remembered.
- **Flood zones:** shown automatically while P.1 is at or above a stage, unless hidden by the user.
- **Flood zones:** `src/static/flood-zones-p1.geojson`, CMU Water Center's flood hazard map of the October 2024 flood (built by `scripts/build_flood_zones.py` from CMU's KML): five disjoint zones, zone k floods first when P.1 reaches stage k (4.30, 4.50, 4.70, 5.00, 5.30 m), in CMU's colours from deep red to yellow, credited in the map attribution. Shown automatically while P.1 is over its banks (danger level) or stage 1 is at least 50 % likely within 24 h, unless hidden by the user. A point's zone (team markers) honours the holes: dry islands inside a zone are outside it.
- **Basin stations:** ThaiWater's classes for the level as a share of the bank height (critically low ≤ 10 %, low ≤ 30 %, normal ≤ 70 %, high ≤ 100 %, over the bank). On the map and in the list over the bank is danger, high is watch, the rest normal; no data after 24 h. Gauges RID already lists are shown once, as RID gauges. The layer is off until the list is opened or the button pressed.
- **History:** above 2,500 readings (30 d and longer for most gauges) the chart shows the daily mean with the daily low–high band, so a flood peak is not averaged away; gaps longer than 3.5 h (2.5 days when daily) break the line rather than bridging RID's nightly pause. Short ranges reload with every refresh, long ones at most hourly. "All" asks for `limit=100000`: `/measurements/history` returns the oldest rows first and stops at its 50,000 default, which made the old dashboard's "All time" for P.1 end in October 2024.
- **Issued forecasts:** the history's violet line is the model's 24 h peak for every hour (`/api/forecast/history`, `source = model` only: a heuristic row is the level itself and drew a dip into the line). The nightly gaps were RID's overnight batch; the collector now forecasts those hours when they arrive (docs/FLOOD_FORECASTING.md, Serving).
+30
View File
@@ -235,6 +235,34 @@ Oct 2024 flood). Mae Kuang Udom Thara is the second upstream reservoir.
| `https://hydro-1.net` | RID Upper-Northern Hydrology Center — hourly/daily tables, hydrology yearbooks (rating curves) for P-series stations; scrape/download |
| `https://water.rid.go.th/flood/flood/daily.pdf` | RID daily flood bulletin (PDF only) |
### 5.1 CMU Water Center — CMFlood (Chiang Mai city) (verified 2026-10-25)
`https://watercenter.cmu.ac.th/chiangmai/cmflood/warning/*`, the Chiang Mai urban
flood warning site of Chiang Mai University's water center (with RID, ONWR, DDPM
and the province). A Next.js app; its data paths are in the page chunks
(`/_next/static/chunks/app/...`). No licence is stated; we credit CMU.
- **Flood hazard map, 2024 flood** — `/data/KML5Layer/L1.kml` … `L5.kml`: the
flooded extent at P.1 = 4.30 / 4.50 / 4.70 / 5.00 / 5.30 m (2.5 / 3.0 / 22 / 57 /
156 km², cumulative). CMU: the Ping overflows its banks above P.1 4.20 m, and the
older municipal map (2011 flood) no longer fits the city's defences. **In use:**
`P1_FLOOD_STAGES` and `src/static/flood-zones-p1.geojson`
(`uv run scripts/build_flood_zones.py`).
- **Flood marks 2024** — `/data/New_floodMark_2025_fixed_like_pole.geojson` (4.9 MB):
4,869 points in Chiang Mai and Lamphun with the October 2024 depth above the road
in cm (P.1 = 5.30 m), the marker's place and type.
- **Flood poles** — `/data/PoleCNX2025_v2.geojson`: 219 poles with ground level and
maximum flood level (m MSL) and the depth.
- **City-reach gauges** (station page): P.67, P.103, P.1, P.104 (RID, `ST.16`
every 15 min), FloodBoy FBP.2/FBP.3 (CMU climate data centre) and DDPM CMI01–03
(camera gauges, ~5 min, public CCTV snapshots), with gauge zero, bank levels
and chainage. CMU reads them from its own Supabase project with a public browser
key (tables `disaster_level`, `flagship_hydro`, `water_levels`, `hydro_levels`;
timestamps are Bangkok time labelled `+00:00`); history starts 2026-08 (FBP
2025-09). Not ingested: too short to train on, and polling CMU's database is
using their infrastructure — prefer the original sources or ask CMU.
- **P.1 forecast** (`floodforecast`): login only.
---
## 6. Integration status & recommended order
@@ -250,6 +278,8 @@ Oct 2024 flood). Mae Kuang Udom Thara is the second upstream reservoir.
| Satellite QPE (GSMaP/IMERG) | ❌ | Backfill training rainfall (GEE) |
| Open-Meteo rain | ✅ Hourly by ping-collector → `openmeteo_rain` (5 catchment points, past 5 days + 48 h forecast); the 2021+ archive trains the hgb-v3 rain features | — |
| HII API key (dams, forecasts) | ❌ | Contact HII for sanctioned access |
| CMU CMFlood flood map (2024) | ✅ Static (2026-10-25): P.1 stages and the city's flood zones | Rebuild with `scripts/build_flood_zones.py` if CMU revises the map |
| CMU CMFlood marks, poles, city gauges | ❌ | See 5.1 |
**Collector configuration** (ping-collector): HII, the dams and Open-Meteo are
fetched once per clock hour, also while RID is being retried; one-shot via
+15 -9
View File
@@ -465,15 +465,19 @@ validated.
**Thresholds are per-station as of 2026-08-10.** `THRESHOLDS` now carries
calibrated (warning, danger) pairs for all 16 stations, derived from the DB's
`discharge_percent` (RID % of channel capacity): warning = median level at
75–85% capacity, danger = median level at 95–105%. P.1 instead uses the official
Chiang Mai inundation map (`P1_FLOOD_STAGES`): warning 3.70 m (city flooding
begins, stage 1) and danger 4.20 m (stage 5). The prior single default of
75–85% capacity, danger = median level at 95–105%. P.1 is keyed to its banks:
danger 4.20 m, where the Ping overflows them (CMU Water Center's flood map of the
2024 flood; the lower bank is 304.65 m MSL = 4.15 m on the gauge), and warning
3.70 m, half a metre below. (Until 2026-10-25 the docs called 3.70 m "city flooding
begins", after the older municipal map; the values did not change.) The prior single default of
(3.0, 4.5) m made P.103 badly over-alert (its bank-full level is ~6.75 m) and
P.67 under-alert (overflow at ~2.9 m, 1.6 m below the old danger line).
**A retrain is required after any threshold change** — classifier labels depend
on them; until then, model rows report the thresholds baked into their bundle.
P.1 additionally reports `stages`: exceedance probability for each of the seven
official inundation stages (3.70–4.60 m), computed from the regression head and
P.1 additionally reports `stages`: exceedance probability for each of the five
city flood stages (`P1_FLOOD_STAGES`, 4.30–5.30 m, from CMU Water Center's flood
hazard map of the October 2024 flood; the seven stages of the older municipal map,
3.70–4.60 m, were replaced on 2026-10-25), computed from the regression head and
its calibration sigma, so they need no retrain and no per-stage classifiers.
### 2026-08-13: Mae Ngat dam features — a documented negative result
@@ -661,14 +665,16 @@ and `scripts/install.sh` does that on every install.
The dashboard (`src/static/dashboard.html`) loads `/forecast` with the gauges; if
it fails or is empty, the page works without forecasts. Where the model shows:
- **Top bar.** Chiang Mai's verdict from P.1: flooding at ≥ 3.70 m; "likely" at a
24 h danger chance ≥ 50 % or warning chance ≥ 70 %; "watch" at a warning chance
≥ 20 % or a level ≥ 3.20 m. Next to it, the highest 24 h warning chance of any gauge.
- **Top bar.** Chiang Mai's verdict from P.1, in the classes of the gauge status:
flooding (over the banks) at ≥ 4.20 m; high water at ≥ 3.70 m; "flooding likely" at
a 24 h danger chance ≥ 50 %, "high water likely" at a warning chance ≥ 70 %;
"watch" at a warning chance ≥ 20 % or a level ≥ 3.20 m. Next to it, the highest
24 h warning chance of any gauge.
- **Gauge list and map.** A gauge is "watch" from a 24 h warning chance of 25 %.
- **Station panel.** Per horizon (6/12/24 h) the predicted peak and the warning and
danger chances, the model version and when it was trained, or "Simple estimate" when
the row came from the heuristic fallback. For P.1 the chance of reaching each of the
seven inundation stages. The history chart overlays the 24 h peaks as they were
five city flood stages. The history chart overlays the 24 h peaks as they were
issued (`/api/forecast/history`), and "How accurate is the forecast?" loads
`/api/forecast/skill`.
- **Replay.** "Replay the October 2024 flood" plays `src/static/flood-2024-all.json`:
+2 -1
View File
@@ -26,7 +26,8 @@ seventy-two. In a quiet season a subscriber hears nothing.
Station slugs are the code lowercased without the dot: `p1`, `p103`, `p67`.
Thresholds are the ones in `src/ml/features.py` (`THRESHOLDS`): P.1 3.70 /
4.20 m, P.103 5.95 / 6.75 m, and so on.
4.20 m (half a metre below the banks / over the banks; the city floods in stages
from 4.30 m), P.103 5.95 / 6.75 m, and so on.
The outlook topic is opt-in for a reason: it is model output, and the message
says so. Observed-crossing topics only ever report a gauge reading.
+18 -28
View File
@@ -73,38 +73,28 @@
"P.1": [
{
"stage": 1,
"level": 3.7,
"discharge_cms": 405.0
},
{
"stage": 2,
"level": 3.9,
"discharge_cms": 438.0
},
{
"stage": 3,
"level": 4.0,
"discharge_cms": 458.0
},
{
"stage": 4,
"level": 4.1,
"discharge_cms": 478.0
},
{
"stage": 5,
"level": 4.2,
"discharge_cms": 493.0
},
{
"stage": 6,
"level": 4.3,
"discharge_cms": 508.0
},
{
"stage": 7,
"level": 4.6,
"discharge_cms": 558.0
"stage": 2,
"level": 4.5,
"discharge_cms": 538.0
},
{
"stage": 3,
"level": 4.7,
"discharge_cms": 564.0
},
{
"stage": 4,
"level": 5.0,
"discharge_cms": 604.0
},
{
"stage": 5,
"level": 5.3,
"discharge_cms": 656.0
}
]
},
Binary file not shown.
Binary file not shown.
Binary file not shown.
+151
View File
@@ -0,0 +1,151 @@
# /// script
# requires-python = ">=3.11"
# dependencies = ["shapely>=2.0"]
# ///
"""Build src/static/flood-zones-p1.geojson from CMU Water Center's flood map.
CMU's Chiang Mai flood hazard map (watercenter.cmu.ac.th, CMFlood, built from
the October 2024 flood) publishes the flooded area at five P.1 levels as KML:
/data/KML5Layer/L1.kml (P.1 = 4.30 m) to L5.kml (5.30 m, the 2024 record).
Each file is the whole extent at that level. The dashboard wants one ring per
stage instead: zone k is what floods first when P.1 reaches stage k's level
(the extent at level k minus everything flooded at a lower level).
uv run scripts/build_flood_zones.py # fetch the KML from CMU
uv run scripts/build_flood_zones.py --kml-dir D # or read L1..L5.kml from D
The levels must match P1_FLOOD_STAGES in src/ml/features.py.
"""
import argparse
import json
import re
import urllib.request
from pathlib import Path
import shapely
from shapely import make_valid
from shapely.geometry import MultiPolygon, Polygon, mapping
from shapely.ops import unary_union
REPO = Path(__file__).resolve().parents[1]
OUT = REPO / "src" / "static" / "flood-zones-p1.geojson"
BASE = "https://watercenter.cmu.ac.th/data/KML5Layer"
LEVELS = [4.30, 4.50, 4.70, 5.00, 5.30] # L1..L5, P.1 gauge level in m
# ~3 m at Chiang Mai: invisible at the zoom levels the map allows, and it
# keeps the file small. Slivers below MIN_AREA (about 300 m²) come from the
# layers' borders not lining up exactly and are dropped.
SIMPLIFY_DEG = 3e-5
MIN_AREA_DEG2 = 2.5e-8
GRID = 1e-5 # coordinates snapped to 5 decimals (~1 m) with valid geometry
SOURCE = (
"CMU Water Center (ศูนย์วิชาการสนับสนุนการบริหารจัดการน้ำ มหาวิทยาลัยเชียงใหม่), "
"Chiang Mai flood hazard map from the October 2024 flood, "
"https://watercenter.cmu.ac.th/chiangmai/cmflood/warning/floodmap"
)
def fetch(i: int, kml_dir: Path | None) -> str:
if kml_dir:
return (kml_dir / f"L{i}.kml").read_text(encoding="utf-8")
req = urllib.request.Request(
f"{BASE}/L{i}.kml", headers={"User-Agent": "Mozilla/5.0"}
)
with urllib.request.urlopen(req, timeout=60) as r:
return r.read().decode("utf-8")
def ring(text: str) -> list:
pts = []
for tok in text.split():
x, y, *_ = tok.split(",")
pts.append((float(x), float(y)))
return pts
def kml_extent(text: str):
polys = []
for body in re.findall(r"<Polygon>(.*?)</Polygon>", text, re.S):
outer = re.search(
r"<outerBoundaryIs>.*?<coordinates>(.*?)</coordinates>", body, re.S
)
inner = re.findall(
r"<innerBoundaryIs>.*?<coordinates>(.*?)</coordinates>", body, re.S
)
polys.append(
make_valid(Polygon(ring(outer.group(1)), [ring(s) for s in inner]))
)
return unary_union(polys)
def polygons(geom) -> list:
if isinstance(geom, Polygon):
return [geom]
return [
g for g in getattr(geom, "geoms", []) if isinstance(g, (Polygon, MultiPolygon))
]
def clean(geom):
parts = []
for g in polygons(geom):
for p in polygons(g):
if p.area >= MIN_AREA_DEG2:
parts.append(p)
return unary_union(parts)
def rounded(coords):
# the geometry is already on the grid; this only drops float noise
if isinstance(coords, (list, tuple)) and coords and isinstance(coords[0], float):
return [round(coords[0], 5), round(coords[1], 5)]
return [rounded(c) for c in coords]
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("--kml-dir", type=Path, help="read L1..L5.kml from here")
args = ap.parse_args()
flooded = None
features = []
for i, level in enumerate(LEVELS, 1):
extent = kml_extent(fetch(i, args.kml_dir))
extent = shapely.set_precision(
make_valid(extent.simplify(SIMPLIFY_DEG, preserve_topology=True)), GRID
)
if flooded is None:
new, flooded = extent, extent
else:
new = shapely.difference(extent, flooded, grid_size=GRID)
flooded = shapely.union(flooded, extent, grid_size=GRID)
zone = shapely.set_precision(clean(new), GRID)
assert zone.is_valid, f"zone {i} is not a valid geometry"
geo = mapping(zone)
features.append(
{
"type": "Feature",
"properties": {"zone": i, "level": level},
"geometry": {
"type": geo["type"],
"coordinates": rounded(geo["coordinates"]),
},
}
)
print(f"zone {i} (P.1 {level:.2f} m): {len(polygons(zone))} polygons")
doc = {
"type": "FeatureCollection",
"source": SOURCE,
"note": "Zone k floods first when P.1 reaches its level; zones are disjoint.",
# highest zone first, so the zones that flood first are drawn on top
"features": features[::-1],
}
OUT.write_text(
json.dumps(doc, ensure_ascii=False, separators=(",", ":")) + "\n",
encoding="utf-8",
)
print(f"wrote {OUT.relative_to(REPO)}: {OUT.stat().st_size / 1024:.0f} KB")
if __name__ == "__main__":
main()
+17 -14
View File
@@ -22,9 +22,11 @@ logger = logging.getLogger(__name__)
# "*" is the default applied to any station without an explicit override.
# Calibrated 2026-08-10 from the DB's discharge_percent (RID % of channel
# capacity): warning = median level at 75-85% capacity, danger = median level
# at 95-105%. P.1 instead uses the official Chiang Mai inundation map keyed to
# the P.1 gauge: city flooding begins at 3.70 m (stage 1) and reaches most
# districts by 4.20 m (stage 5) — see P1_FLOOD_STAGES.
# at 95-105%. P.1 is keyed to its banks instead: CMU Water Center's flood map
# (2024 flood) has the Ping leave its banks above 4.20 m (the lower, right
# bank is 304.65 m MSL = 4.15 m on the gauge), so danger = 4.20 m and warning
# = 3.70 m, half a metre below the bank like CMU's own gauge page. The city
# floods in stages from 4.30 m, see P1_FLOOD_STAGES.
THRESHOLDS: Dict[str, Tuple[float, float]] = {
"*": (3.0, 4.5),
"P.1": (3.70, 4.20),
@@ -58,18 +60,19 @@ THRESHOLDS: Dict[str, Tuple[float, float]] = {
"P.92": (2.95, 3.60),
}
# Official Chiang Mai flood-onset stages at the P.1 gauge (Nawarat Bridge),
# from the municipal inundation map (พื้นที่ท่วมตัวเมืองเชียงใหม่, events of
# 2548/2554/2565 BE): gauge level in m, RID discharge in m³/s. Each stage
# floods progressively more city zones.
# Chiang Mai flood stages at the P.1 gauge (Nawarat Bridge), from CMU Water
# Center's flood hazard map built on the October 2024 flood (CMFlood,
# watercenter.cmu.ac.th; zones in src/static/flood-zones-p1.geojson, built by
# scripts/build_flood_zones.py): gauge level in m, RID discharge in m³/s on
# RID's 2024 rating. Each stage floods more of the city; stage 5 is the 2024
# record. (The older municipal map, 3.70-4.60 m, is out of date: the city's
# flood defences have changed since.)
P1_FLOOD_STAGES: List[Dict[str, float]] = [
{"stage": 1, "level": 3.70, "discharge_cms": 405},
{"stage": 2, "level": 3.90, "discharge_cms": 438},
{"stage": 3, "level": 4.00, "discharge_cms": 458},
{"stage": 4, "level": 4.10, "discharge_cms": 478},
{"stage": 5, "level": 4.20, "discharge_cms": 493},
{"stage": 6, "level": 4.30, "discharge_cms": 508},
{"stage": 7, "level": 4.60, "discharge_cms": 558},
{"stage": 1, "level": 4.30, "discharge_cms": 508},
{"stage": 2, "level": 4.50, "discharge_cms": 538},
{"stage": 3, "level": 4.70, "discharge_cms": 564},
{"stage": 4, "level": 5.00, "discharge_cms": 604},
{"stage": 5, "level": 5.30, "discharge_cms": 656},
]
FLOOD_STAGES: Dict[str, List[Dict[str, float]]] = {"P.1": P1_FLOOD_STAGES}
+61 -43
View File
@@ -270,7 +270,7 @@
.pbar { display: inline-block; width: 46px; height: 6px; border-radius: 3px; background: var(--panel-2); vertical-align: middle; margin-right: 7px; overflow: hidden; }
.pbar i { display: block; height: 100%; border-radius: 3px; }
.note { color: var(--muted); font-size: 11.5px; margin-top: 9px; line-height: 1.5; }
.stages { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
.stages { display: grid; grid-template-columns: repeat(var(--n, 5), 1fr); gap: 4px; }
.stage { border: 1px solid var(--line); border-radius: 6px; padding: 5px 2px; text-align: center; }
.stage b { display: block; font-size: 12.5px; font-weight: 600; }
.stage span { display: block; font-size: 10.5px; color: var(--muted); }
@@ -428,7 +428,7 @@
<div id="legend-zones" hidden>
<span class="label" data-i18n="layer.zones">Flood zones</span>
<span class="key"><i style="background:#99201c"></i><span data-i18n="legend.zones.first">Zone 1 floods first</span></span>
<span class="key"><i style="background:#f5c542"></i><span data-i18n="legend.zones.last">Zone 7 last</span></span>
<span class="key"><i style="background:#f5d547"></i><span data-i18n="legend.zones.last">Zone 5 last</span></span>
<span class="key"><i style="background:#2f86c8"></i><span data-i18n="legend.zones.flooded">River at or above the zone's level</span></span>
</div>
</div>
@@ -502,12 +502,14 @@
'verdict.loading': 'Loading…',
'verdict.tip': 'Chiang Mai city, P.1 Nawarat Bridge. Experimental model, not an official warning.',
'verdict.ok': 'Chiang Mai: no flooding expected in the next 24 h',
'verdict.watch': (pct) => `Chiang Mai: watch · ${pct}% chance of flooding within 24 h`,
'verdict.watch': (pct) => `Chiang Mai: watch · ${pct}% chance of the warning level within 24 h`,
'verdict.watch.level': 'Chiang Mai: watch · river elevated',
'verdict.likely': 'Chiang Mai: flooding likely within 24 h',
'verdict.flooding': 'Chiang Mai: flooding · stage 1 reached',
'verdict.high.likely': 'Chiang Mai: high water likely within 24 h',
'verdict.high': 'Chiang Mai: high water · close to the top of the banks',
'verdict.flooding': 'Chiang Mai: flooding · the Ping is over its banks',
'verdict.none': 'Chiang Mai: no current reading',
'verdict.sub': (lvl, warn) => `P.1 Nawarat Bridge ${lvl} m · flooding starts at ${warn} m`,
'verdict.sub': (lvl, bank) => `P.1 Nawarat Bridge ${lvl} m · overflows its banks above ${bank} m`,
'kpi.reporting': 'Gauges reporting',
'kpi.risk': 'Highest flood chance · 24 h',
'kpi.rising': 'Rising > 5 cm in 6 h',
@@ -544,7 +546,7 @@
'legend.rain.veryheavy': 'Very heavy 90–150',
'legend.rain.extreme': 'Extreme > 150',
'legend.zones.first': 'Zone 1 floods first',
'legend.zones.last': 'Zone 7 last',
'legend.zones.last': 'Zone 5 last',
'legend.zones.flooded': 'River at or above the zone’s level',
'layer.group': 'Map layers',
'layer.rain': 'Rain 24 h',
@@ -585,7 +587,7 @@
'd.disclaimer': 'Experimental model, not an official warning. Official alerts:',
'd.stages.title': 'Chiang Mai inundation stages',
'd.stages.sub': 'chance within 24 h',
'd.stages.explain': 'City flooding begins at stage 1 (3.70 m); each further stage floods more districts.',
'd.stages.explain': (bank, first, n, last) => `The Ping overflows its banks above ${bank} m. From stage 1 (${first} m) to stage ${n} (${last} m, the 2024 record) each stage floods more of the city. Zones: CMU Water Center, from the 2024 flood.`,
'd.stages.show': 'Show zones on the map',
'd.stages.hide': 'Hide zones',
'stage.chip': (n) => `S${n}`,
@@ -628,9 +630,9 @@
'alerts.t.danger': 'Any gauge: danger level',
'alerts.t.danger.d': 'Only the serious crossings, basin-wide. Highest priority: rings through Do Not Disturb on most phones.',
'alerts.t.p1.warning': 'Chiang Mai city (P.1) warning',
'alerts.t.p1.warning.d': 'Nawarat Bridge crosses 3.70 m (stage 1: low-lying riverside areas), and the all-clear.',
'alerts.t.p1.warning.d': 'Nawarat Bridge crosses 3.70 m (half a metre below the top of the banks), and the all-clear.',
'alerts.t.p1.danger': 'Chiang Mai city (P.1) danger',
'alerts.t.p1.danger.d': 'Nawarat Bridge crosses 4.20 m (stage 5: inner city districts).',
'alerts.t.p1.danger.d': 'Nawarat Bridge crosses 4.20 m (the river overflows its banks; the city floods from 4.30 m).',
'alerts.t.p103.warning': 'Ring Road 3 (P.103) warning',
'alerts.t.p103.warning.d': 'Downstream city gauge crosses 5.95 m.',
'alerts.t.outlook': 'Early warning (model forecast)',
@@ -694,7 +696,7 @@
'skill.explain': 'Every hour the deployed model’s 24 h peak forecast for P.1 is stored; once those 24 hours have passed it is compared with what the river did. Skill is how much better the model was than assuming the level stays where it is (0 = no better, 1 = perfect). A retrained model appears as a new row, so improvement, or its absence, is visible here rather than claimed.',
'skill.better': (v, prev, d) => `The current model ${v} is more accurate than ${prev}: its peak error is ${d} cm lower on the hours it has served.`,
'skill.worse': (v, prev, d) => `The current model ${v} has a higher peak error than ${prev} so far (+${d} cm).`,
'skill.caveat.quiet': 'Every verified hour so far was below 2 m, so this measures quiet-river accuracy only. The model is built and judged for flood onset (lead time before 3.70 m), which no quiet week can test: see the backtests in the documentation.',
'skill.caveat.quiet': 'Every verified hour so far was below 2 m, so this measures quiet-river accuracy only. The model is built and judged for high water (lead time before the 3.70 m warning level), which no quiet week can test: see the backtests in the documentation.',
'skill.caveat.regime': 'The versions served different weeks; the ≥ 2 m column compares them on the hours that matter.',
'skill.single': (v) => `Only ${v} has enough verified hours yet; the next retrain adds a row to compare.`,
'skill.young': (v, n, min) => `${v} has ${n} verified hours; a comparison needs ${min}.`,
@@ -732,10 +734,11 @@
'replay.back': 'Back to live data',
'replay.scrub': 'Position in the replay',
'replay.p1': (lvl) => `P.1 ${lvl} m`,
'replay.flooding': 'Flooding: the river is above stage 1 (3.70 m)',
'replay.alert': (peak) => `Model alert: flooding within 24 h likely${peak}`,
'replay.flooding': 'Flooding: the river is over its banks (above 4.20 m)',
'replay.high': 'High water: above the warning level (3.70 m); the banks overflow at 4.20 m',
'replay.alert': (peak) => `Model alert: the warning level is likely within 24 h${peak}`,
'replay.alert.peak': (lvl) => `, predicted peak ${lvl} m`,
'replay.elevated': (pct) => `Model: elevated flood risk (${pct}% within 24 h)`,
'replay.elevated': (pct) => `Model: elevated risk (${pct}% chance of the warning level within 24 h)`,
'replay.noflood': 'Model: no flood expected',
'replay.note': 'Gauge readings as recorded in 2024. The model line is what a model trained only on data before 1 September 2024 said at each hour.',
'd.reading.replay': (when) => `Reading ${when}, 2024 replay`,
@@ -758,12 +761,14 @@
'verdict.loading': 'กำลังโหลด…',
'verdict.tip': 'เมืองเชียงใหม่ สถานี P.1 สะพานนวรัฐ แบบจำลองทดลอง ไม่ใช่การเตือนภัยอย่างเป็นทางการ',
'verdict.ok': 'เชียงใหม่: ไม่คาดว่าจะเกิดน้ำท่วมใน 24 ชม. ข้างหน้า',
'verdict.watch': (pct) => `เชียงใหม่: เฝ้าติดตาม · โอกาสน้ำท่วม ${pct}% ภายใน 24 ชม.`,
'verdict.watch': (pct) => `เชียงใหม่: เฝ้าติดตาม · โอกาสถึงระดับเตือนภัย ${pct}% ภายใน 24 ชม.`,
'verdict.watch.level': 'เชียงใหม่: เฝ้าติดตาม · ระดับน้ำสูง',
'verdict.likely': 'เชียงใหม่: มีแนวโน้มน้ำท่วมภายใน 24 ชม.',
'verdict.flooding': 'เชียงใหม่: น้ำท่วม · ถึงระดับ 1 แล้ว',
'verdict.high.likely': 'เชียงใหม่: มีแนวโน้มน้ำขึ้นสูงภายใน 24 ชม.',
'verdict.high': 'เชียงใหม่: น้ำขึ้นสูง · ใกล้ล้นตลิ่ง',
'verdict.flooding': 'เชียงใหม่: น้ำท่วม · แม่น้ำปิงล้นตลิ่ง',
'verdict.none': 'เชียงใหม่: ไม่มีข้อมูลล่าสุด',
'verdict.sub': (lvl, warn) => `P.1 สะพานนวรัฐ ${lvl} ม. · เริ่มท่วมที่ ${warn} ม.`,
'verdict.sub': (lvl, bank) => `P.1 สะพานนวรัฐ ${lvl} ม. · น้ำล้นตลิ่งเมื่อเกิน ${bank} ม.`,
'kpi.reporting': 'สถานีที่รายงาน',
'kpi.risk': 'โอกาสน้ำท่วมสูงสุด · 24 ชม.',
'kpi.rising': 'น้ำขึ้น > 5 ซม. ใน 6 ชม.',
@@ -800,7 +805,7 @@
'legend.rain.veryheavy': 'หนักมาก 90–150',
'legend.rain.extreme': 'รุนแรง > 150',
'legend.zones.first': 'พื้นที่ระดับ 1 ท่วมก่อน',
'legend.zones.last': 'ระดับ 7 ท่วมหลังสุด',
'legend.zones.last': 'ระดับ 5 ท่วมหลังสุด',
'legend.zones.flooded': 'ระดับน้ำถึงหรือเกินระดับของพื้นที่',
'layer.group': 'ชั้นข้อมูลแผนที่',
'layer.rain': 'ฝน 24 ชม.',
@@ -841,7 +846,7 @@
'd.disclaimer': 'แบบจำลองทดลอง ไม่ใช่การเตือนภัยอย่างเป็นทางการ ประกาศทางการ:',
'd.stages.title': 'ระดับน้ำท่วมเมืองเชียงใหม่',
'd.stages.sub': 'โอกาสภายใน 24 ชม.',
'd.stages.explain': 'น้ำเริ่มท่วมเมืองที่ระดับ 1 (3.70 ม.) และแต่ละระดับถัดไปจะท่วมพื้นที่เพิ่มขึ้น',
'd.stages.explain': (bank, first, n, last) => `แม่น้ำปิงล้นตลิ่งเมื่อระดับน้ำเกิน ${bank} ม. ตั้งแต่ระดับ 1 (${first} ม.) ถึงระดับ ${n} (${last} ม. สูงสุดเมื่อปี 2567) แต่ละระดับท่วมพื้นที่ในเมืองเพิ่มขึ้น พื้นที่น้ำท่วม: ศูนย์วิชาการสนับสนุนการบริหารจัดการน้ำ มช. จากน้ำท่วมปี 2567`,
'd.stages.show': 'แสดงพื้นที่บนแผนที่',
'd.stages.hide': 'ซ่อนพื้นที่',
'stage.chip': (n) => `ระดับ ${n}`,
@@ -884,9 +889,9 @@
'alerts.t.danger': 'สถานีใดก็ได้: ระดับอันตราย',
'alerts.t.danger.d': 'เฉพาะการข้ามระดับที่ร้ายแรง ทั้งลุ่มน้ำ ความสำคัญสูงสุด: ดังผ่านโหมดห้ามรบกวนในโทรศัพท์ส่วนใหญ่',
'alerts.t.p1.warning': 'เมืองเชียงใหม่ (P.1) ระดับเตือนภัย',
'alerts.t.p1.warning.d': 'สะพานนวรัฐข้าม 3.70 ม. (ระดับ 1: พื้นที่ริมน้ำที่ต่ำ) และแจ้งเมื่อกลับสู่ปกติ',
'alerts.t.p1.warning.d': 'สะพานนวรัฐข้าม 3.70 ม. (ต่ำกว่าขอบตลิ่งราวครึ่งเมตร) และแจ้งเมื่อกลับสู่ปกติ',
'alerts.t.p1.danger': 'เมืองเชียงใหม่ (P.1) ระดับอันตราย',
'alerts.t.p1.danger.d': 'สะพานนวรัฐข้าม 4.20 ม. (ระดับ 5: ย่านใจกลางเมือง)',
'alerts.t.p1.danger.d': 'สะพานนวรัฐข้าม 4.20 ม. (น้ำล้นตลิ่ง ในเมืองเริ่มท่วมที่ 4.30 ม.)',
'alerts.t.p103.warning': 'ถนนวงแหวน 3 (P.103) ระดับเตือนภัย',
'alerts.t.p103.warning.d': 'สถานีท้ายเมืองข้าม 5.95 ม.',
'alerts.t.outlook': 'เตือนล่วงหน้า (แบบจำลองพยากรณ์)',
@@ -950,7 +955,7 @@
'skill.explain': 'ทุกชั่วโมงระบบบันทึกค่าพยากรณ์ระดับน้ำสูงสุดใน 24 ชม. ของ P.1 ไว้ เมื่อครบ 24 ชม. จึงนำมาเทียบกับระดับน้ำจริง "ทักษะ" คือแบบจำลองดีกว่าการสมมติว่าระดับน้ำคงที่มากเพียงใด (0 = ไม่ดีกว่า, 1 = สมบูรณ์แบบ) แบบจำลองที่ฝึกใหม่จะปรากฏเป็นแถวใหม่ จึงเห็นได้ว่าดีขึ้นจริงหรือไม่',
'skill.better': (v, prev, d) => `แบบจำลองปัจจุบัน ${v} แม่นยำกว่า ${prev}: ค่าคลาดเคลื่อนต่ำกว่า ${d} ซม. ในช่วงที่ให้บริการ`,
'skill.worse': (v, prev, d) => `แบบจำลองปัจจุบัน ${v} มีค่าคลาดเคลื่อนสูงกว่า ${prev} (+${d} ซม.)`,
'skill.caveat.quiet': 'ชั่วโมงที่ตรวจสอบทั้งหมดอยู่ต่ำกว่า 2 ม. จึงวัดได้เพียงความแม่นยำช่วงน้ำปกติ แบบจำลองถูกสร้างและประเมินสำหรับช่วงน้ำเริ่มท่วม (เวลาเตือนล่วงหน้าก่อน 3.70 ม.) ซึ่งสัปดาห์ปกติทดสอบไม่ได้ ดูผลทดสอบย้อนหลังในเอกสาร',
'skill.caveat.quiet': 'ชั่วโมงที่ตรวจสอบทั้งหมดอยู่ต่ำกว่า 2 ม. จึงวัดได้เพียงความแม่นยำช่วงน้ำปกติ แบบจำลองถูกสร้างและประเมินสำหรับช่วงน้ำขึ้นสูง (เวลาเตือนล่วงหน้าก่อนถึงระดับเตือนภัย 3.70 ม.) ซึ่งสัปดาห์ปกติทดสอบไม่ได้ ดูผลทดสอบย้อนหลังในเอกสาร',
'skill.caveat.regime': 'แต่ละเวอร์ชันให้บริการคนละช่วงเวลา คอลัมน์ ≥ 2 ม. เปรียบเทียบเฉพาะชั่วโมงที่สำคัญ',
'skill.single': (v) => `มีเพียง ${v} ที่มีข้อมูลตรวจสอบเพียงพอ การฝึกครั้งถัดไปจะเพิ่มแถวให้เปรียบเทียบ`,
'skill.young': (v, n, min) => `${v} มีข้อมูลตรวจสอบ ${n} ชั่วโมง ต้องการอย่างน้อย ${min} เพื่อเปรียบเทียบ`,
@@ -988,10 +993,11 @@
'replay.back': 'กลับสู่ข้อมูลสด',
'replay.scrub': 'ตำแหน่งในการเล่นย้อนหลัง',
'replay.p1': (lvl) => `P.1 ${lvl} ม.`,
'replay.flooding': 'น้ำท่วม: ระดับน้ำเกินระดับ 1 (3.70 ม.)',
'replay.alert': (peak) => `แบบจำลองเตือน: มีแนวโน้มน้ำท่วมภายใน 24 ชม.${peak}`,
'replay.flooding': 'น้ำท่วม: แม่น้ำล้นตลิ่ง (เกิน 4.20 ม.)',
'replay.high': 'น้ำขึ้นสูง: เกินระดับเตือนภัย (3.70 ม.) น้ำจะล้นตลิ่งที่ 4.20 ม.',
'replay.alert': (peak) => `แบบจำลองเตือน: มีแนวโน้มถึงระดับเตือนภัยภายใน 24 ชม.${peak}`,
'replay.alert.peak': (lvl) => ` คาดระดับสูงสุด ${lvl} ม.`,
'replay.elevated': (pct) => `แบบจำลอง: ความเสี่ยงน้ำท่วมสูงขึ้น (${pct}% ภายใน 24 ชม.)`,
'replay.elevated': (pct) => `แบบจำลอง: ความเสี่ยงสูงขึ้น (โอกาสถึงระดับเตือนภัย ${pct}% ภายใน 24 ชม.)`,
'replay.noflood': 'แบบจำลอง: ไม่คาดว่าจะเกิดน้ำท่วม',
'replay.note': 'ค่าระดับน้ำตามที่บันทึกในปี 2567 เส้นแบบจำลองคือสิ่งที่แบบจำลองซึ่งฝึกด้วยข้อมูลก่อนวันที่ 1 กันยายน 2567 เท่านั้นคาดการณ์ไว้ในแต่ละชั่วโมง',
'd.reading.replay': (when) => `ข้อมูลเมื่อ ${when} (ย้อนเหตุการณ์ 2567)`,
@@ -1114,16 +1120,20 @@
return b.bank > 100 ? 'danger' : b.bank > 70 ? 'watch' : 'normal';
}
// City verdict from P.1's level and its 24 h forecast. City flooding
// starts at stage 1 (P.1 3.70 m), the official inundation stages.
// City verdict from P.1's level and its 24 h forecast, with the same
// classes as statusOf. The Ping overflows its banks above the danger
// level (4.20 m) and the city floods in stages from 4.30 m (CMU Water
// Center's 2024 flood map); the warning level is half a metre below.
function cityVerdict() {
const p1 = state.byCode.get('P.1');
if (!p1 || p1.level == null) return { cls: 'nodata', text: t('verdict.none'), sub: '' };
const warn = p1.warn ?? 3.7;
const warn = p1.warn ?? 3.7, bank = p1.danger ?? 4.2;
const f = p1.fc[24];
const sub = t('verdict.sub', m2(p1.level), m2(warn));
if (p1.level >= warn) return { cls: 'danger', text: t('verdict.flooding'), sub };
if (f && (f.p_danger >= 0.5 || f.p_warning >= 0.7)) return { cls: 'warning', text: t('verdict.likely'), sub };
const sub = t('verdict.sub', m2(p1.level), m2(bank));
if (p1.level >= bank) return { cls: 'danger', text: t('verdict.flooding'), sub };
if (p1.level >= warn) return { cls: 'warning', text: t(f && f.p_danger >= 0.5 ? 'verdict.likely' : 'verdict.high'), sub };
if (f && f.p_danger >= 0.5) return { cls: 'warning', text: t('verdict.likely'), sub };
if (f && f.p_warning >= 0.7) return { cls: 'warning', text: t('verdict.high.likely'), sub };
if ((f && f.p_warning >= 0.2) || p1.level >= warn - 0.5) {
return { cls: 'watch', text: f ? t('verdict.watch', Math.round(f.p_warning * 100)) : t('verdict.watch.level'), sub };
}
@@ -1281,7 +1291,8 @@
function replayMessage() {
const p1 = state.byCode.get('P.1'), f = p1 && p1.fc[24];
if (p1 && p1.level != null && p1.level >= (p1.warn ?? 3.7)) return ['flood', t('replay.flooding')];
if (p1 && p1.level != null && p1.level >= (p1.danger ?? 4.2)) return ['flood', t('replay.flooding')];
if (p1 && p1.level != null && p1.level >= (p1.warn ?? 3.7)) return ['alert', t('replay.high')];
if (f && f.p_warning >= 0.5) return ['alert', t('replay.alert', f.predicted_max_level != null ? t('replay.alert.peak', m2(f.predicted_max_level)) : '')];
if (f && f.p_warning >= 0.2) return ['elevated', t('replay.elevated', Math.round(f.p_warning * 100))];
return ['quiet', f ? t('replay.noflood') : ''];
@@ -1691,13 +1702,16 @@
}
return inside;
}
// inside the outer ring and in none of its holes (the zones are rings
// around each other and have islands that stay dry)
const pointInPolygon = (lat, lon, rings) => rings.length > 0 && pointInRing(lat, lon, rings[0]) && !rings.slice(1).some((h) => pointInRing(lat, lon, h));
function zoneOfPoint(lat, lon) {
if (!state.zones) return null;
let best = null;
state.zones.features.forEach((f) => {
const g = f.geometry;
const polygons = g.type === 'Polygon' ? [g.coordinates] : g.type === 'MultiPolygon' ? g.coordinates : [];
if (polygons.some((rings) => rings.length && pointInRing(lat, lon, rings[0])) && (!best || f.properties.zone < best.properties.zone)) best = f;
if (polygons.some((rings) => pointInPolygon(lat, lon, rings)) && (!best || f.properties.zone < best.properties.zone)) best = f;
});
return best;
}
@@ -1712,10 +1726,13 @@
return `<div class="popup"><div class="label">${escapeHtml(t('poi.label'))}</div><h3>${escapeHtml(poi.name)}</h3>${poi.note ? `<div style="color:var(--muted)">${escapeHtml(poi.note)}</div>` : ''}${body}</div>`;
}
// Chiang Mai inundation zones, digitised from the municipal flood map.
// Zone 1 floods first (deep red), zone 7 last; a zone the river has
// reached is drawn as water.
const ZONE_COLORS = { 1: '#99201c', 2: '#c0392b', 3: '#d35400', 4: '#e67e22', 5: '#f39c12', 6: '#f5c542', 7: '#f9e3a1' };
// Chiang Mai flood zones from CMU Water Center's flood hazard map (the
// October 2024 flood; scripts/build_flood_zones.py). Zone k is the area
// that floods first when P.1 reaches stage k: zone 1 (4.30 m, deep red)
// to zone 5 (5.30 m, the 2024 record, yellow), CMU's colours. A zone the
// river has reached is drawn as water.
const ZONE_COLORS = { 1: '#99201c', 2: '#c62828', 3: '#e2603a', 4: '#f2a35c', 5: '#f5d547' };
const ZONE_SOURCE = '<a href="https://watercenter.cmu.ac.th/chiangmai/cmflood/warning/floodmap" target="_blank" rel="noopener">CMU Water Center</a>';
const p1Level = () => { const p1 = state.byCode.get('P.1'); return p1 ? p1.level : null; };
const zoneStage = (zone) => (state.p1Stages || []).find((s) => s.stage === zone) || null;
function zoneStyle(feature) {
@@ -1723,7 +1740,7 @@
const now = p1Level();
if (now != null && now >= level) return { pane: 'zones', color: '#1b5e88', weight: 2, dashArray: null, fillColor: '#2f86c8', fillOpacity: 0.62 };
const st = zoneStage(zone);
return { pane: 'zones', color: '#8a5a2b', weight: 1.1, dashArray: '4 4', fillColor: ZONE_COLORS[zone] || '#f9e3a1', fillOpacity: 0.34 + 0.3 * Math.min(1, st ? st.p_exceed : 0) };
return { pane: 'zones', color: '#8a5a2b', weight: 1.1, dashArray: '4 4', fillColor: ZONE_COLORS[zone] || '#f5d547', fillOpacity: 0.34 + 0.3 * Math.min(1, st ? st.p_exceed : 0) };
}
function zonePopup(feature) {
const zone = feature.properties.zone, level = Number(feature.properties.level);
@@ -1740,16 +1757,17 @@
if (on) {
try {
if (!state.zones) state.zones = await json(fetch('/static/flood-zones-p1.geojson'));
state.zoneLayer = L.geoJSON(state.zones, { style: zoneStyle, pane: 'zones' }).bindPopup((layer) => zonePopup(layer.feature)).addTo(state.map);
state.zoneLayer = L.geoJSON(state.zones, { style: zoneStyle, pane: 'zones', attribution: ZONE_SOURCE }).bindPopup((layer) => zonePopup(layer.feature)).addTo(state.map);
if (fly) state.map.flyToBounds(state.zoneLayer.getBounds(), { maxZoom: 13, duration: 0.7 });
} catch (e) { $('layer-zones').setAttribute('aria-pressed', 'false'); $('legend-zones').hidden = true; }
}
renderDetail();
}
function autoZones() {
// Flooding in progress: show the zones unless the user hid them
const now = p1Level();
const flooded = now != null && (state.p1Stages || []).some((s) => now >= s.level);
// The river over its banks, or stage 1 likely within 24 h: show the
// zones unless the user hid them
const p1 = state.byCode.get('P.1'), now = p1Level();
const flooded = now != null && (now >= ((p1 && p1.danger) ?? 4.2) || (state.p1Stages || []).some((s) => now >= s.level || s.p_exceed >= 0.5));
if (flooded && !state.zoneLayer && !state.zonesUserHidden) setZones(true, false);
else if (state.zoneLayer) state.zoneLayer.setStyle(zoneStyle);
}
@@ -2212,13 +2230,13 @@
}
const stages = s.code === 'P.1' && state.p1Stages ? `
<div class="sect"><h3 class="label"><span>${escapeHtml(t('d.stages.title'))}</span><span>${escapeHtml(t('d.stages.sub'))}</span></h3>
<div class="stages">${state.p1Stages.map((g) => {
<div class="stages" style="--n:${state.p1Stages.length}">${state.p1Stages.map((g) => {
const reached = s.level != null && s.level >= g.level;
const color = reached ? css('--danger-ink') : g.p_exceed >= 0.5 ? css('--warning-ink') : g.p_exceed >= 0.2 ? css('--watch-ink') : css('--text');
return `<div class="stage" title="${escapeHtml(t('stage.tip', g.stage, g.level.toFixed(2), Math.round(g.p_exceed * 100)))}">
<span>${escapeHtml(t('stage.chip', g.stage))}</span><b class="num" style="color:${color}">${pct(g.p_exceed)}</b><span class="num">${g.level.toFixed(2)}</span></div>`;
}).join('')}</div>
<div class="sect-actions"><span class="note" style="margin:0">${escapeHtml(t('d.stages.explain'))}</span>
<div class="sect-actions"><span class="note" style="margin:0">${escapeHtml(t('d.stages.explain', m2(s.danger ?? 4.2), m2(state.p1Stages[0].level), state.p1Stages.length, m2(state.p1Stages[state.p1Stages.length - 1].level)))}</span>
<button type="button" class="btn" id="zones-from-panel" style="white-space:nowrap">${escapeHtml(t(state.zoneLayer ? 'd.stages.hide' : 'd.stages.show'))}</button></div>
</div>` : '';
const fcSection = `
+1 -1
View File
@@ -1 +1 @@
{"type": "FeatureCollection", "source": "Chiang Mai inundation zones traced by the project owner from the municipal flood map (stages keyed to the P.1 gauge)", "features": [{"type": "Feature", "properties": {"zone": 7, "level": 4.6}, "geometry": {"coordinates": [[[98.99880625810528, 18.784477298167957], [98.99461039320016, 18.784274422928206], [98.99414790361936, 18.781294057606146], [98.99338756491807, 18.77996508784213], [98.99256873862737, 18.77968821782146], [98.98692468597324, 18.779355973196743], [98.98619359107005, 18.778220799120035], [98.98405879395233, 18.777556303429193], [98.98297677349444, 18.776697992618082], [98.98233340997882, 18.77592274038571], [98.98180702164893, 18.77503673346787], [98.98104668294985, 18.774123033955036], [98.9778298653743, 18.769637528153936], [98.97973071834002, 18.767394731388947], [98.98049105704126, 18.766065652036048], [98.98049105704126, 18.765096525071655], [98.98242115331647, 18.762964427873897], [98.98338619858873, 18.76207835285959], [98.98440973145495, 18.761358413482498], [98.98546250811472, 18.760998442642233], [98.98616435919331, 18.760583090595347], [98.98695394168521, 18.76014004736058], [98.98724637964602, 18.759309338262582], [98.98715864825817, 18.757758669801362], [98.98724638064289, 18.755211123039246], [98.98721713684694, 18.753632726146023], [98.98745108721613, 18.75172201541686], [98.99393973813358, 18.748777676714795], [98.99866675103556, 18.747498759225508], [99.0030078853332, 18.746585240801167], [99.00614314912758, 18.74667659260706], [99.00918194313522, 18.74672226862053], [99.01386072121386, 18.74736173151757], [99.0182500903365, 18.74873200099759], [99.02210887637972, 18.749965234019825], [99.02673942254995, 18.751792228986318], [99.03040517274661, 18.75393895756561], [99.03098399049287, 18.757273130646723], [99.02919930154764, 18.76453500551959], [99.0288616577684, 18.766590196376086], [99.02866869279399, 18.770289471074904], [99.02804164057648, 18.774262682143842], [99.02770399693168, 18.776637430154793], [99.02669103598669, 18.780975818695225], [99.025822809126, 18.783761466324705], [99.02345930267506, 18.784355122980827], [99.02350753750017, 18.784948777544088], [99.02418282505863, 18.78526843682765], [99.02519575690206, 18.785725092291386], [99.02577457480851, 18.786410072570334], [99.0255334006813, 18.787277710257186], [99.02403811313752, 18.78942394572998], [99.02283224249948, 18.791204849547142], [99.02220518976799, 18.792163789955964], [99.0211922587178, 18.793442369230405], [99.02042050194001, 18.794720939297036], [99.01955227507938, 18.795953835929012], [99.01897345717293, 18.79682142444355], [99.01831436082307, 18.79972730702255], [99.01777863778688, 18.801147303888698], [99.01702862553361, 18.802871569690467], [99.0166536199107, 18.803581557567696], [99.01504645094093, 18.805356511465817], [99.01413572177813, 18.806167912570658], [99.01242140805891, 18.807182158449038], [99.01092040802666, 18.805891108789595], [99.00797426275983, 18.805985005956316], [99.00526904617476, 18.809089450292632], [99.00167351849208, 18.80961415378073], [99.00199808697452, 18.80540186486514], [99.00143684883221, 18.80078451832455], [98.99833449246728, 18.79662657839105], [98.99910609832347, 18.789740446568647], [98.99880625810528, 18.784477298167957]]], "type": "Polygon"}}, {"type": "Feature", "properties": {"zone": 6, "level": 4.3}, "geometry": {"coordinates": [[[98.99651377950943, 18.761493728814628], [98.99694656013548, 18.7613525083853], [98.99782672066647, 18.762019222629647], [98.99944621598775, 18.762919283242184], [99.00015034441276, 18.763319308389143], [99.00096009210193, 18.764119355836726], [99.00074885357509, 18.764919399489102], [99.00043199578278, 18.765919448717298], [98.99979828020088, 18.767219503847343], [98.99937580314582, 18.7679528638581], [98.99884770914343, 18.770686263907393], [98.9986012640993, 18.772386286222428], [98.9992701861035, 18.773619624795415], [99.00007993379262, 18.774552956095135], [99.00088968148879, 18.775919610459027], [99.00201628690667, 18.778286229521896], [99.00289644735096, 18.779419528368976], [99.00353016293286, 18.780719479383535], [99.0035301629087, 18.78101946665386], [99.00328371795962, 18.781586107819905], [99.00317809869364, 18.782252742043923], [99.00296686019078, 18.782719384448313], [99.00307247949615, 18.783152694110754], [99.0039174336062, 18.783552671243896], [99.0040582592913, 18.783919316114662], [99.00409346571223, 18.78501924594029], [99.00412867251362, 18.785719197922077], [99.00398784682858, 18.786752454497957], [99.00370619591746, 18.78721908506462], [99.00328371923854, 18.78805235017147], [99.00300206786852, 18.78905226213726], [99.00226273302252, 18.7902854787191], [99.00138257249154, 18.79071876888726], [99.00022076059048, 18.790018838061286], [98.99944621932224, 18.789318904325512], [98.99948142574317, 18.78675245574125], [98.99919977437315, 18.78525256508587], [98.99898853584631, 18.78388598641746], [98.99944015986506, 18.782701561531724], [98.99939250851816, 18.780942116278794Line truncated
{"type":"FeatureCollection","source":"CMU Water Center (ศูนย์วิชาการสนับสนุนการบริหารจัดการน้ำ มหาวิทยาลัยเชียงใหม่), Chiang Mai flood hazard map from the October 2024 flood, https://watercenter.cmu.ac.th/chiangmai/cmflood/warning/floodmap","note":"Zone k floods first when P.1 reaches its level; zones are disjoint.","features":[{"type":"Feature","properties":{"zone":5,"level":5.3},"geometry":{"type":"MultiPolygon","coordinates":[[[[98.95482,18.60229],[98.95482,18.60216],[98.95917,18.60088],[98.95459,18.60222],[98.95444,18.60154],[98.95346,18.60186],[98.95307,18.60177],[98.95178,18.60203],[98.94869,18.60221],[98.94905,18.60067],[98.94797,18.59868],[98.94385,18.5997],[98.94379,18.60101],[98.94408,18.60146],[98.94416,18.60196],[98.9446,18.60308],[98.94494,18.6034],[98.94503,18.60371],[98.94482,18.60484],[98.94354,18.60675],[98.94345,18.6078],[98.94461,18.60768],[98.94583,18.60748],[98.94662,18.60717],[98.94763,18.60687],[98.9484,18.60672],[98.94965,18.60677],[98.95044,18.60703],[98.95134,18.60699],[98.95201,18.60603],[98.95214,18.60414],[98.95287,18.60355],[98.95445,18.60278],[98.95475,18.60247],[98.95482,18.60229]]],[[[98.95961,18.59584],[98.95869,18.59578],[98.95763,18.59582],[98.95742,18.5955],[98.95711,18.59523],[98.9564,18.59495],[98.95614,18.59468],[98.95595,18.59438],[98.95586,18.59414],[98.95592,18.59392],[98.95592,18.59283],[98.9559,18.59242],[98.95579,18.59223],[98.95525,18.59174],[98.95481,18.59153],[98.95452,18.59129],[98.95342,18.58989],[98.95314,18.58965],[98.95287,18.58959],[98.95244,18.58932],[98.95038,18.5891],[98.94994,18.58913],[98.94938,18.58908],[98.94895,18.58896],[98.94816,18.5885],[98.94783,18.5885],[98.94749,18.58838],[98.94578,18.58828],[98.946,18.58864],[98.94507,18.58949],[98.94459,18.58978],[98.94296,18.58992],[98.94301,18.59051],[98.94297,18.59073],[98.94134,18.59123],[98.93999,18.59202],[98.93981,18.59204],[98.93855,18.59273],[98.93846,18.59284],[98.93875,18.59325],[98.93875,18.59346],[98.93782,18.59365],[98.93685,18.59446],[98.93651,18.59442],[98.93339,18.59661],[98.93269,18.59765],[98.93216,18.59832],[98.93213,18.59871],[98.93218,18.59913],[98.93195,18.59969],[98.93192,18.60016],[98.93202,18.60057],[98.93167,18.60063],[98.93166,18.60081],[98.9321,18.6013],[98.93213,18.60173],[98.93232,18.60248],[98.93222,18.60275],[98.93225,18.60291],[98.93255,18.60323],[98.93245,18.60353],[98.93247,18.6038],[98.93239,18.60396],[98.93212,18.6042],[98.93205,18.6045],[98.9319,18.60473],[98.93152,18.60505],[98.93145,18.60517],[98.93153,18.60567],[98.93142,18.60582],[98.931,18.60586],[98.93092,18.60601],[98.93086,18.60645],[98.93078,18.60657],[98.93054,18.60672],[98.93039,18.60692],[98.92976,18.60739],[98.92941,18.608],[98.92933,18.60834],[98.92939,18.60871],[98.92929,18.60897],[98.92925,18.60928],[98.92942,18.60957],[98.92945,18.60975],[98.92936,18.60995],[98.92906,18.61005],[98.92901,18.61025],[98.92926,18.61056],[98.92931,18.61069],[98.92902,18.611],[98.92862,18.61115],[98.9284,18.61128],[98.92808,18.61165],[98.92786,18.61201],[98.9276,18.61212],[98.92745,18.61234],[98.92723,18.61233],[98.92705,18.61242],[98.92693,18.61264],[98.92695,18.61305],[98.92658,18.61358],[98.92642,18.61409],[98.92629,18.61423],[98.92601,18.61432],[98.9256,18.61466],[98.92568,18.61499],[98.92563,18.61509],[98.92534,18.61532],[98.92535,18.61543],[98.9256,18.6157],[98.92535,18.61601],[98.92529,18.61636],[98.92502,18.61661],[98.92501,18.61697],[98.92489,18.61732],[98.9247,18.61757],[98.9247,18.61764],[98.92482,18.61764],[98.92497,18.61775],[98.9249,18.61809],[98.92509,18.61832],[98.92471,18.61858],[98.92475,18.61868],[98.92469,18.61875],[98.92492,18.61907],[98.92463,18.6192],[98.92463,18.6194],[98.92507,18.62013],[98.92517,18.62024],[98.92562,18.62043],[98.92566,18.62049],[98.92559,18.62065],[98.92581,18.62094],[98.92568,18.62113],[98.92566,18.62124],[98.9257,18.62135],[98.92584,18.62136],[98.92588,18.62141],[98.92596,18.62194],[98.92602,18.62203],[98.92584,18.62231],[98.92591,18.62242],[98.92591,18.62257],[98.93644,18.59744],[98.93965,18.59502],[98.94937,18.58922],[98.95029,18.58928],[98.95245,18.58954],[98.95313,18.59072],[98.95542,18.5928],[98.95579,18.59472],[98.95758,18.59591],[98.95997,18.59598],[98.96048,18.59634],[98.96162,18.59702],[98.96387,18.5978],[98.96508,18.59839],[98.96552,18.59906],[98.96601,18.59926],[98.96603,18.59925],[98.9653,18.59823],[98.96475,18.59772],[98.9642,18.59737],[98.96194,18.59652],[98.96116,18.59618],[98.96014,18.59556],[98.95961,18.59584]]],[[[98.96682,18.60731],[98.96707,18.60715],[98.96783,18.60633],[98.96682,18.60731]]],[[[98.96642,18.61255],[98.9664,18.61234],[98.96607,18.61181],[98.96568,18.61141],[98.9661,18.6119],[98.96639,18.6124],[98.96642,18.61255]]],[[[98.96632,18.61356],[98.96643,18.61329],[98.96584,18.61408],[98.96632,18.61356]]],[[[98.96374,18.61492],[98.9635,18.61506],[98.96365,18.61502],[98.96369,18.615],[98.96378,18.61499],[98.96426,18.61487],[98.96374,18.61Line truncated