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
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:
1 parent
180d95c6bd
commit
701bb87d34
13 files changed
+301
-101
No files matched your search
+17
-14
@@ -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
@@ -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 @@
|
||||
{"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
|
||||
Reference in new issue
Block a user