เขียนเว็บเตือนภัยน้ำท่วมด้วย Open API: Open-Meteo, ThaiWater, RainViewer และ Google Flood Hub
เว็บเตือนภัยน้ำท่วมที่ดีไม่ได้ต้องมีเซนเซอร์เป็นของตัวเอง งานหลักคือ เลือกแหล่งข้อมูลให้ถูก ตั้งเกณฑ์ให้มีเหตุผล และนำเสนอให้คนอ่านเข้าใจในไม่กี่วินาที บทความนี้จะพาไล่ดู API สาธารณะที่ใช้ได้จริงในประเทศไทย ทีละแหล่ง พร้อมโค้ดที่รันได้ทันทีด้วย Node.js 18 ขึ้นไป (มี fetch ในตัว ไม่ต้องติดตั้งแพ็กเกจเพิ่ม)
ค่าจากโมเดลและสถานีอัตโนมัติอาจคลาดเคลื่อนหรือขาดหาย เว็บเตือนภัยที่ทำเองควรลิงก์ไปยังประกาศของหน่วยงานรัฐ (กรมอุตุนิยมวิทยา, สสน., ปภ., กรมชลประทาน) เสมอ และไม่ควรใช้แทนคำสั่งอพยพหรือประกาศทางการ
1. เว็บเตือนภัยต้องใช้ข้อมูลอะไรบ้าง
ข้อมูลที่ต้องใช้แบ่งได้เป็น 3 ชั้น ตามลำดับเวลาที่น้ำมาถึง
| ชั้นข้อมูล | ตอบคำถาม | แหล่งในบทความนี้ |
|---|---|---|
| ฝน (พยากรณ์ + ตอนนี้) | "จะมีน้ำเพิ่มเข้ามาในระบบไหม" | Open-Meteo Forecast, RainViewer |
| ปริมาณน้ำในแม่น้ำ | "แม่น้ำจะรับไหวไหมในอีกหลายวันข้างหน้า" | Open-Meteo Flood API (GloFAS), Google Flood Forecasting API |
| ระดับน้ำจากสถานีจริง | "ตอนนี้น้ำอยู่สูงแค่ไหนเทียบกับตลิ่ง" | ThaiWater / สสน. |
หลักสำคัญมีสองข้อ
- ข้อมูลตัวเลขให้ผ่าน backend ของเราเสมอ เพราะต้อง cache (ไม่ยิง API ต้นทางทุกครั้งที่มีคนเปิดหน้า) ต้องคำนวณเกณฑ์ และบางแหล่งส่ง JSON มาใหญ่หลาย MB
- ภาพแผนที่ (tiles) ให้เบราว์เซอร์โหลดตรง เพราะเป็นไฟล์ภาพที่ CDN ของผู้ให้บริการ cache ไว้แล้ว
ชั้นข้อมูลฝนมีอีกทางเลือกหนึ่งคือ WeatherNext 3 ของ Google DeepMind ซึ่งให้พยากรณ์รายชั่วโมงพร้อมช่วงความไม่แน่นอน (64 ฉากทัศน์) แต่ต้องขอสิทธิ์และมีข้อจำกัดเรื่องฝนหนักที่ต้องรู้ก่อนใช้ → WeatherNext 3: ข้อจำกัดเมื่อใช้กับประเทศไทย
2. ฝนพยากรณ์: Open-Meteo Forecast API
Open-Meteo รวมโมเดลพยากรณ์อากาศของหน่วยงานอุตุนิยมวิทยาหลายประเทศ (เช่น ECMWF, NOAA, DWD) ไว้หลัง API เดียว ไม่ต้องสมัครและไม่ต้องใช้ API key ใช้ฟรีสำหรับงานที่ไม่ใช่เชิงพาณิชย์โดยขอให้ใช้อย่างเป็นธรรม (ถ้าเกิน 10,000 ครั้งต่อวันให้ติดต่อผู้ให้บริการ) ข้อมูลเผยแพร่ภายใต้ CC BY 4.0 จึงต้องแสดงเครดิตพร้อมลิงก์ไปยัง Open-Meteo ทุกที่ที่แสดงข้อมูล
ปริมาณฝนอย่างเดียวยังบอกไม่ได้ว่าน่ากังวลแค่ไหน จึงต้องแปลงเป็นระดับตามเกณฑ์ปริมาณฝนของกรมอุตุนิยมวิทยา ซึ่งพิจารณาฝนใน 24 ชั่วโมง ตั้งแต่ 07:00 น. ของวันหนึ่งถึง 07:00 น. ของวันรุ่งขึ้น
| ปริมาณฝน 24 ชม. (07:00–07:00 น.) | ระดับ |
|---|---|
| 0.1–10.0 มม. | ฝนเล็กน้อย |
| 10.1–35.0 มม. | ฝนปานกลาง |
| 35.1–90.0 มม. | ฝนหนัก |
| 90.1 มม. ขึ้นไป | ฝนหนักมาก |
// rain.mjs — ฝนพยากรณ์รายวันตามรอบวันของกรมอุตุนิยมวิทยา (07:00–07:00 น.) แล้วจัดระดับตามเกณฑ์ อต.
const RAIN_LEVELS = [
{ min: 90.1, label: "ฝนหนักมาก", level: 3 },
{ min: 35.1, label: "ฝนหนัก", level: 2 },
{ min: 10.1, label: "ฝนปานกลาง", level: 1 },
{ min: 0.1, label: "ฝนเล็กน้อย", level: 0 },
];
function classifyRain(mm) {
const { label, level } = RAIN_LEVELS.find((r) => mm >= r.min) ?? { label: "ไม่มีฝน", level: 0 };
return { label, level };
}
// ฝนรายชั่วโมงของ Open-Meteo คือฝนสะสมของ "ชั่วโมงก่อนหน้า" เวลาที่ระบุ
// ค่าที่ติดป้าย 08:00 คือฝน 07:00–08:00 → ถอย 8 ชั่วโมงแล้วอ่านวันที่ = วันฝนที่เริ่ม 07:00 น.
function rainDayOf(localTime) {
const t = new Date(localTime + "Z"); // อ่านตัวเลขตรง ๆ ไม่ขึ้นกับ timezone ของเครื่อง
t.setUTCHours(t.getUTCHours() - 8);
return t.toISOString().slice(0, 10);
}
export async function rainForecast(lat, lon, days = 3) {
const url = new URL("https://api.open-meteo.com/v1/forecast");
url.search = new URLSearchParams({
latitude: lat,
longitude: lon,
hourly: "precipitation,precipitation_probability",
timezone: "Asia/Bangkok", // เวลาใน response เป็นเวลาไทย
forecast_days: days + 1, // เผื่อชั่วโมงถึง 07:00 น. ของวันสุดท้าย
});
const res = await fetch(url);
if (!res.ok) throw new Error(`Open-Meteo ${res.status}`);
const { hourly } = await res.json();
const byDay = new Map();
hourly.time.forEach((time, i) => {
const day = rainDayOf(time);
const d = byDay.get(day) ?? { date: day, mm: 0, chance: 0, hours: 0 };
d.mm += hourly.precipitation[i] ?? 0;
d.chance = Math.max(d.chance, hourly.precipitation_probability[i] ?? 0);
d.hours += 1;
byDay.set(day, d);
});
return [...byDay.values()]
.filter((d) => d.hours === 24) // ตัดวันที่ข้อมูลไม่ครบ 24 ชั่วโมง
.slice(0, days)
.map(({ date, mm, chance }) => ({ date, mm: Math.round(mm * 10) / 10, chance, ...classifyRain(mm) }));
}พารามิเตอร์ที่ควรรู้
timezone=Asia/Bangkokทำให้เวลาใน response เป็นเวลาไทย ถ้าไม่ใส่จะได้เวลา GMThourly=precipitationคือฝนสะสมของชั่วโมงก่อนหน้า เวลาที่ระบุ ค่าที่ติดป้าย 08:00 จึงเป็นฝนช่วง 07:00–08:00 โค้ดจึงถอย 8 ชั่วโมงก่อนอ่านวันที่ เพื่อให้ได้วันฝนที่เริ่ม 07:00 น.forecast_daysใส่ได้สูงสุด 16 วัน แต่ความแม่นยำมักลดลงมากเมื่อพยากรณ์ไกลออกไป
กับดักที่ 1: รอบวันฝนไม่ใช่เที่ยงคืนถึงเที่ยงคืน
Open-Meteo มีตัวแปร daily=precipitation_sum ให้ใช้สะดวก แต่ตัดรอบที่เที่ยงคืน ส่วนเกณฑ์ของกรมอุตุนิยมวิทยานับ 07:00–07:00 น. ตัวอย่างจริงที่เชียงใหม่ตอนเขียนบทความ
| วิธีตัดรอบ | ฝนวันที่ 4 ต.ค. 2026 | ระดับ |
|---|---|---|
เที่ยงคืนถึงเที่ยงคืน (daily=precipitation_sum) | 3.0 มม. | ฝนเล็กน้อย |
07:00–07:00 น. ตามกรมอุตุฯ (รวมจาก hourly) | 16.4 มม. | ฝนปานกลาง |
ข้อมูลพยากรณ์ชุดเดียวกัน แต่ได้ระดับต่างกัน เพราะฝนส่วนใหญ่ตกหลังเที่ยงคืนถึงเช้าวันที่ 5 ถ้าจะใช้คำเรียกระดับฝนของกรมอุตุฯ ต้องใช้รอบวันของกรมอุตุฯ ด้วย
หลักการเดียวกันนี้ใช้กับข้อมูลทุกแหล่ง สำหรับโมเดลที่ออกผลเป็นเวลา UTC อย่าง WeatherNext รอบวัน 07:00–07:00 น. ตรงกับวัน UTC พอดี จึงจัดกลุ่มได้ง่ายกว่า → รอบวันฝน 07:00–07:00 น. ตรงกับวัน UTC
3. ปริมาณน้ำในแม่น้ำ: Open-Meteo Flood API
Flood API ให้ค่า river discharge (ปริมาณน้ำไหลผ่าน หน่วย m³/s) จาก Global Flood Awareness System (GloFAS) ของโครงการ Copernicus (Alfieri et al., 2013; Harrigan et al., 2020) ค่าเริ่มต้นเป็น GloFAS v4 ความละเอียด 0.05° (ประมาณ 5 กม.) รายวัน ข้อมูลย้อนหลังตั้งแต่ปี 1984 และพยากรณ์ล่วงหน้าได้สูงสุด 210 วัน (forecast_days 0–210, ค่าเริ่มต้น 92 วัน)
https://flood-api.open-meteo.com/v1/flood?latitude=15.72&longitude=100.14&daily=river_discharge&forecast_days=14กับดักที่ 2: พิกัดต้องตกบนตัวแม่น้ำ
API จะเลือกจุดกริดที่ใกล้พิกัดที่ส่งไป ถ้าจุดนั้นเป็นลำน้ำสาขาเล็ก ๆ ค่าที่ได้จะต่างกันมาก ตัวอย่างจริงบริเวณนครสวรรค์ที่ทดลองตอนเขียนบทความ
| พิกัดที่ส่งไป | จุดกริดที่ได้ | ปริมาณน้ำ (m³/s) |
|---|---|---|
| 15.70, 100.11 | 15.675, 100.125 | 1.82 |
| 15.72, 100.14 | 15.725, 100.125 | 2,248.05 |
ห่างกันแค่ประมาณ 5 กม. แต่ค่าต่างกันกว่าพันเท่า เพราะจุดหลังตกบนแม่น้ำเจ้าพระยา ส่วนจุดแรกไม่ใช่ เอกสารของ Open-Meteo เองก็เตือนว่าที่ความละเอียด 5 กม. อาจเลือกแม่น้ำผิด และแนะนำให้ลองขยับพิกัดทีละประมาณ 0.1° (พารามิเตอร์ cell_selection มีค่า land เป็นค่าเริ่มต้น และมี sea, nearest) ให้ดู latitude/longitude ใน response ทุกครั้ง และเทียบกับลำดับขนาดที่ควรเป็น ก่อนบันทึกพิกัดจุดเฝ้าระวังลงระบบ
กับดักที่ 3: ตัวเลขดิบไม่มีความหมายถ้าไม่มีเกณฑ์
2,248 m³/s มากหรือน้อย? คำตอบขึ้นกับแม่น้ำสายนั้น วิธีที่ใช้ง่ายและอธิบายได้คือ เทียบกับสถิติย้อนหลังของจุดเดียวกัน เช่น ถ้าเกิน percentile ที่ 90 ให้ "เฝ้าระวัง" ถ้าเกิน percentile ที่ 98 ให้ "วิกฤต" ตัวเลข P90/P98 นี้เป็นเกณฑ์สำหรับสอนแนวคิด ไม่ใช่เกณฑ์ทางการ (ระบบเตือนภัยของ GloFAS เองใช้เกณฑ์จากคาบการเกิดซ้ำ 2, 5 และ 20 ปี) และควรปรับให้เข้ากับแม่น้ำแต่ละสาย
P90 ในหัวข้อนี้คือ percentile ของ สถิติย้อนหลัง 15 ปี ของจุดเดียวกัน (ตอบว่า "สูงผิดปกติไหม") ส่วน _p90 ของ WeatherNext 3 คือ percentile ของ 64 ฉากทัศน์ของพยากรณ์รอบเดียว (ตอบว่า "แย่ได้แค่ไหน") ทั้งสองแบบใช้ร่วมกันได้ เช่น เตือนเมื่อ _p90 ของพยากรณ์สูงกว่า P90 ของสถิติ → อ่านพยากรณ์แบบ ensemble ให้เป็น
// river.mjs — เทียบปริมาณน้ำพยากรณ์กับสถิติย้อนหลังของจุดเดียวกัน
const FLOOD_API = "https://flood-api.open-meteo.com/v1/flood";
async function getDischarge(params) {
const url = new URL(FLOOD_API);
url.search = new URLSearchParams({ daily: "river_discharge", ...params });
const res = await fetch(url);
if (!res.ok) throw new Error(`Flood API ${res.status}`);
return res.json();
}
function percentile(sorted, p) {
return sorted[Math.floor(p * (sorted.length - 1))];
}
// คำนวณเกณฑ์ครั้งเดียวแล้วเก็บไว้ (ไม่ต้องดึงข้อมูล 15 ปีทุกครั้ง)
export async function buildThresholds(lat, lon) {
const { daily } = await getDischarge({
latitude: lat, longitude: lon,
start_date: "2011-01-01", end_date: "2025-12-31",
});
const values = daily.river_discharge.filter((v) => v != null).sort((a, b) => a - b);
return { p50: percentile(values, 0.5), p90: percentile(values, 0.9), p98: percentile(values, 0.98) };
}
export async function riverStatus(lat, lon, t) {
const data = await getDischarge({ latitude: lat, longitude: lon, forecast_days: 14 });
const { time, river_discharge: q } = data.daily;
const peak = Math.max(...q);
const level = peak >= t.p98 ? "วิกฤต" : peak >= t.p90 ? "เฝ้าระวัง" : "ปกติ";
return {
cell: [data.latitude, data.longitude], // จุดกริดที่ API เลือกให้จริง
today: q[0],
peak,
peakDate: time[q.indexOf(peak)],
level,
};
}ทดลองเรียกทุกฟังก์ชันพร้อมกัน
import { rainForecast } from "./rain.mjs";
import { buildThresholds, riverStatus } from "./river.mjs";
import { waterLevels } from "./waterlevel.mjs";
console.table(await rainForecast(18.79, 98.98)); // เชียงใหม่
const [lat, lon] = [15.72, 100.14]; // แม่น้ำเจ้าพระยา นครสวรรค์
const t = await buildThresholds(lat, lon);
console.log(await riverStatus(lat, lon, t));
console.table((await waterLevels("พระนครศรีอยุธยา")).slice(0, 5));$ node try.mjs
{ cell: [ 15.725006, 100.125 ], today: 2248.05, peak: 2248.05, peakDate: '2026-10-04', level: 'เฝ้าระวัง' }GloFAS เหมาะกับการดูแนวโน้มและการเตือนล่วงหน้าระดับลุ่มน้ำ แต่ตัวเลขอาจต่างจากสถานีวัดน้ำของกรมชลประทานในจุดเดียวกัน จึงควรใช้คู่กับข้อมูลสถานีจริงในหัวข้อ 5 และเรียกเกณฑ์ว่า "สูงกว่าปกติของพื้นที่" ไม่ใช่ "น้ำจะท่วม"
สถิติในตัวอย่างคำนวณจากชุดข้อมูลแบบ seamless ของ Open-Meteo ซึ่งต่อข้อมูลย้อนหลัง (consolidated) เข้ากับข้อมูลพยากรณ์ในช่วงหลัง ถ้าจะใช้ในงานวิจัยหรือตั้งเกณฑ์จริง ควรเลือกช่วงที่เป็นข้อมูล consolidated ล้วน (ตาม Open-Meteo คือปี 1984 ถึงกลางปี 2022) หรือใช้ GloFAS-ERA5 reanalysis โดยตรง
ถ้าใช้ Open-Meteo ในงานเชิงพาณิชย์หรือปริมาณมาก ต้องสมัครแผนแบบเสียเงิน แล้วใช้โดเมน customer-… (เช่น customer-flood-api.open-meteo.com) คู่กับพารามิเตอร์ apikey
4. มุมของ Google: Flood Hub และ Flood Forecasting API
นอกจาก GloFAS แล้ว Google Research ก็มีระบบพยากรณ์น้ำท่วมในแม่น้ำด้วย AI ที่ครอบคลุมกว่า 150 ประเทศ รวมถึงประเทศไทย ข้อมูลเปิดให้ดูฟรีบน Flood Hub และให้นักพัฒนาดึงไปใช้ผ่าน API
4.1 ทำงานอย่างไร
ระบบของ Google มีสองแบบจำลองต่อกัน คือ แบบจำลองอุทกวิทยา (hydrologic model) ที่พยากรณ์ปริมาณน้ำในแม่น้ำ และ แบบจำลองพื้นที่น้ำท่วม (inundation model) ที่คาดว่าพื้นที่ใดจะถูกน้ำท่วมและลึกเท่าใด ข้อมูลนำเข้าเป็นผลพยากรณ์อากาศระดับโลก ข้อมูลสถานีวัดน้ำ และภาพดาวเทียมที่เปิดเผยต่อสาธารณะ ตามหน้าโครงการ พยากรณ์น้ำท่วมในแม่น้ำได้ล่วงหน้าสูงสุด 7 วัน และมีพยากรณ์น้ำท่วมฉับพลันในเมืองล่วงหน้าสูงสุด 24 ชั่วโมงที่ความละเอียด 20 × 20 กม.
งานวิจัยที่ตีพิมพ์ใน Nature (Nearing et al., 2024) รายงานว่าแบบจำลอง AI นี้พยากรณ์เหตุการณ์น้ำท่วมรุนแรงใน ลุ่มน้ำที่ไม่มีสถานีวัดน้ำ (ungauged) ล่วงหน้า 5 วัน ได้ความน่าเชื่อถือใกล้เคียงหรือดีกว่าการพยากรณ์ของ GloFAS ณ วันเกิดเหตุ (lead time 0 วัน) ซึ่งสำคัญกับประเทศที่สถานีวัดน้ำยังไม่ทั่วถึง
4.2 Flood Forecasting API
Flood Forecasting API ไม่มีค่าใช้จ่าย ข้อมูลอยู่ภายใต้ CC BY 4.0 แต่ต้องลงชื่อใน waitlist ก่อน เมื่อได้รับอนุมัติแล้ว ให้ส่ง Google Cloud Project ID กลับไป สร้าง API key และเปิดใช้ API ในโปรเจกต์ นอกจากนี้ยังมีชุดข้อมูลย้อนหลังให้ใช้ เช่น Google Runoff Reanalysis & Reforecast (GRRR) ปริมาณน้ำท่าปี 1980–2023 และ Inundation History
| ข้อมูล | ความหมาย |
|---|---|
gauges | จุดพยากรณ์ (gauge) พร้อมพิกัด ชื่อแม่น้ำ หน่วยงานเจ้าของข้อมูล (source) และ qualityVerified |
floodStatus | สถานะล่าสุดของแต่ละ gauge: severity (EXTREME, SEVERE, ABOVE_NORMAL, NO_FLOODING, UNKNOWN), แนวโน้ม forecastTrend (RISE, FALL, NO_CHANGE) และชุดแผนที่พื้นที่น้ำท่วม |
// google-flood.mjs — สถานะน้ำท่วมล่าสุดในไทยจาก Google Flood Forecasting API
// ต้องลงชื่อใน waitlist ให้ได้รับอนุมัติ แล้วตั้ง GOOGLE_FLOOD_API_KEY ใน environment
const BASE = "https://floodforecasting.googleapis.com/v1";
const KEY = process.env.GOOGLE_FLOOD_API_KEY;
async function post(path, body) {
const res = await fetch(`${BASE}/${path}?key=${KEY}`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`${path} ${res.status}: ${await res.text()}`);
return res.json();
}
// 1) สถานะล่าสุดของทุก gauge ในไทย (ค่าเริ่มต้นคืนเฉพาะ gauge ที่โมเดลผ่านการตรวจคุณภาพ)
const { floodStatuses = [] } = await post("floodStatus:searchLatestFloodStatusByArea", { regionCode: "TH" });
// 2) ชื่อสถานีและแม่น้ำ (อย่า cache ผลนี้นานเกิน 1 วัน เพราะ gauge ถูกเพิ่ม/ลบได้)
const { gauges = [] } = await post("gauges:searchGaugesByArea", { regionCode: "TH" });
const info = new Map(gauges.map((g) => [g.gaugeId, g]));
const ORDER = { EXTREME: 0, SEVERE: 1, ABOVE_NORMAL: 2, NO_FLOODING: 3, UNKNOWN: 4 };
const rows = floodStatuses
.filter((s) => s.severity !== "NO_FLOODING")
.sort((a, b) => ORDER[a.severity] - ORDER[b.severity])
.map((s) => ({
severity: s.severity, // EXTREME / SEVERE / ABOVE_NORMAL / UNKNOWN
trend: s.forecastTrend, // RISE / FALL / NO_CHANGE
river: info.get(s.gaugeId)?.river ?? "-",
site: info.get(s.gaugeId)?.siteName ?? s.gaugeId,
issued: s.issuedTime,
source: s.source, // หน่วยงานเจ้าของข้อมูล gauge
}));
console.table(rows);- ค่าเริ่มต้นของ API คืนเฉพาะ gauge ที่โมเดล ผ่านการตรวจคุณภาพ ถ้าเปิด
includeNonQualityVerifiedเอกสารเตือนให้ใช้ด้วยความระมัดระวัง - รายการ gauge เปลี่ยนได้ เอกสารแนะนำ ไม่ให้ cache นานเกิน 1 วัน
- ระดับ
severityเป็นเกณฑ์ของระบบ Google ไม่ใช่ระดับเตือนภัยของกรมชลประทานหรือ สสน. ควรแสดงคู่กับระดับน้ำจากสถานีจริงในหัวข้อ 5 และอ้างอิงประกาศทางการ
Google มีอีกชิ้นหนึ่งที่ต่อกับระบบนี้ได้ คือฝนพยากรณ์จาก WeatherNext 3 ของ Google DeepMind ซึ่งเป็นโมเดลเบื้องหลัง Google Maps Platform Weather API ใช้แทนหรือคู่กับ Open-Meteo ในหัวข้อ 2 ได้ ดูวิธีเรียกที่ Weather API: พยากรณ์รายชั่วโมงของจุดหนึ่ง แต่ก่อนนำฝนจากโมเดลนี้มาใช้ตัดสินเตือนภัย ควรอ่าน ยังไม่ได้ประเมินฝนหนัก ก่อน
5. ระดับน้ำจากสถานีจริง: ThaiWater / สสน.
คลังข้อมูลน้ำแห่งชาติ (สสน.) รวมข้อมูลสถานีโทรมาตรจากหลายหน่วยงาน เช่น กรมชลประทาน กรมทรัพยากรน้ำ และกรมอุตุนิยมวิทยา ช่องทางเข้าถึงมีสองแบบ ซึ่งสถานะต่างกันมาก
5.1 ช่องทางทางการ: มาตรฐานแลกเปลี่ยนข้อมูล (standard.thaiwater.net)
standard.thaiwater.net เป็น ข้อกำหนด (specification) ที่หน่วยงานด้านน้ำใช้แลกเปลี่ยนข้อมูลกัน ไม่ใช่ endpoint เดียวที่เปิดให้ทุกคนเรียก ในเอกสารใช้ URL ตัวอย่างอย่าง https://api.my.go.th/twsapi/v1.0/ ส่วน URL จริงขึ้นกับหน่วยงานเจ้าของข้อมูล
| Resource | Path | ข้อมูล |
|---|---|---|
| A001 | /Rainfall | ปริมาณฝน |
| A002 | /Runoff | น้ำท่า (การไหลในลำน้ำ) |
| A003–A005 | /LargesizedWaterResources ฯลฯ | แหล่งน้ำขนาดใหญ่ / กลาง / เล็ก |
| A006 | /WaterQuality | คุณภาพน้ำ |
| B001 | /StationInfo | ข้อมูลสถานี |
| B002–B004 | /LargesizedWaterResourcesInfo ฯลฯ | ข้อมูลแหล่งน้ำ |
พารามิเตอร์ใช้ชุดเดียวกันทุก resource เช่น latest, startDatetime/endDatetime, provinceCode, amphoeCode, basinCode, stationCode ผลลัพธ์เป็น JSON แบบ time series (metadata + timeSeriesObservation[]) ส่วนการยืนยันตัวตนรองรับ API Key, คู่กุญแจ หรือ OAuth 2.0 / OpenID Connect
ติดต่อขอสิทธิ์จากหน่วยงานเจ้าของข้อมูลตามมาตรฐานนี้ จะได้ข้อตกลงการใช้งานที่ชัดเจนและ endpoint ที่ไม่เปลี่ยนโดยไม่แจ้งล่วงหน้า
5.2 สำหรับการเรียนรู้: JSON ที่หน้าเว็บ thaiwater.net ใช้
หน้าเว็บ thaiwater.net ดึงข้อมูลจาก api-v3.thaiwater.net โดยไม่ต้องใช้ key ดูได้จากแท็บ Network ใน DevTools เช่น
Endpoint (ต่อจาก /api/v1/thaiwater30) | ข้อมูล |
|---|---|
/public/thailand | ภาพรวมทั้งประเทศ: ระดับน้ำ ฝน เขื่อน พายุ (~6 MB) |
/public/rain_24h | ฝนสะสม 24 ชม. รายสถานี |
/public/thaiwater/weather | พยากรณ์อากาศ |
endpoint เหล่านี้เป็นการสื่อสารภายในระหว่างหน้าเว็บกับ backend ของ thaiwater.net ไม่มีเอกสาร ไม่มีข้อตกลงการใช้งาน และอาจเปลี่ยนหรือปิดได้ทุกเมื่อ ใช้เพื่อศึกษารูปแบบข้อมูลได้ แต่ถ้าจะใช้จริงควรขอสิทธิ์ตามข้อ 5.1 และไม่ว่ากรณีใด ต้อง cache ฝั่ง server และไม่เรียกถี่เกินจำเป็น
ข้อมูลระดับน้ำแต่ละสถานีมีทั้ง ระดับน้ำ (ม.รทก.), ระดับตลิ่งต่ำสุด (min_bank) และ ระดับท้องน้ำ (ground_level) ค่า storage_percent คือระดับน้ำคิดเป็นร้อยละของความจุลำน้ำถึงตลิ่ง
storage_percent = (ระดับน้ำ − ระดับท้องน้ำ) ÷ (ระดับตลิ่ง − ระดับท้องน้ำ) × 100เช่น สถานี C.67 สะพานหัวเวียง: (5.51 − (−6.58)) ÷ (2.75 − (−6.58)) × 100 ≈ 129.6% หมายความว่าน้ำล้นตลิ่งแล้ว 2.76 ม. สูตรนี้ตรงกับค่าที่ API ส่งมาในสถานีส่วนใหญ่ (761 จาก 788 สถานีที่มีข้อมูลครบตอนทดสอบ) จึงควรใช้ค่า storage_percent ที่ API ให้มาโดยตรง และ situation_level 1–5 แปลงเป็นข้อความตามเกณฑ์ ≤10, >10, >30, >70, >100 เปอร์เซ็นต์
// waterlevel.mjs — ระดับน้ำจากสถานีโทรมาตร (JSON ที่ thaiwater.net ใช้แสดงผล)
// ⚠️ endpoint นี้ไม่มีเอกสารทางการ อาจเปลี่ยนได้ทุกเมื่อ — ใช้เพื่อการเรียนรู้
const URL_TW = "https://api-v3.thaiwater.net/api/v1/thaiwater30/public/thailand";
const SITUATION = { 1: "น้ำน้อยวิกฤติ", 2: "น้ำน้อย", 3: "น้ำปกติ", 4: "น้ำมาก", 5: "ล้นตลิ่ง" };
const STALE_HOURS = 6; // ข้อมูลเก่ากว่านี้ = สถานีอาจขัดข้อง อย่าใช้ตัดสินเตือนภัย
export async function waterLevels(provinceName) {
const res = await fetch(URL_TW);
if (!res.ok) throw new Error(`ThaiWater ${res.status}`);
const json = await res.json(); // ~6 MB — ควรเรียกจาก backend แล้ว cache เท่านั้น
return json.waterlevel.data.data
.filter((r) => r.geocode.province_name.th === provinceName && r.waterlevel_msl != null)
.map((r) => ({
station: r.station.tele_station_name.th,
code: r.station.tele_station_oldcode,
lat: r.station.tele_station_lat,
lon: r.station.tele_station_long,
time: r.waterlevel_datetime,
level_msl: Number(r.waterlevel_msl),
bank_msl: r.station.min_bank,
percent_of_bank: Number(r.storage_percent),
situation: SITUATION[r.situation_level] ?? "-",
// เวลาของ ThaiWater เป็นเวลาไทย ไม่มี timezone ต่อท้าย
stale: Date.now() - Date.parse(r.waterlevel_datetime.replace(" ", "T") + "+07:00") > STALE_HOURS * 3600e3,
}))
.sort((a, b) => b.percent_of_bank - a.percent_of_bank);
}ข้อมูลสถานีชุดนี้ยังเป็น "ค่าวัดจริง" ที่ใช้ตรวจเทียบความแม่นของโมเดลพยากรณ์ในพื้นที่ของเราได้ด้วย → ตรวจเทียบกับฝนที่วัดได้จริง
กับดักที่ 4: ข้อมูลเก่าที่ดูเหมือนข้อมูลใหม่
ตอนทดสอบเจอสถานีหนึ่งในอยุธยาที่ค่าล่าสุดเป็นของ 2 วันก่อน API ยังส่งค่าออกมาตามปกติ ไม่ได้บอกว่าขาดหาย ถ้าไม่เช็กเวลา หน้าเว็บจะแสดงค่าเก่าเหมือนเป็นค่าปัจจุบัน ฟิลด์ stale ในโค้ดด้านบนมีไว้แก้ปัญหานี้ ให้แสดงสถานีเหล่านั้นเป็นสีเทา และไม่นำไปคำนวณการเตือน
6. รวมทุกแหล่งเป็น API เดียว พร้อม cache
backend ทำหน้าที่สามอย่าง คือ ดึงตามรอบ (แต่ละแหล่งตั้งอายุ cache ต่างกันตามความถี่ที่ข้อมูลเปลี่ยน), ส่งค่าเก่าพร้อมบอกว่าเก่าเมื่อแหล่งต้นทางล่ม และส่ง JSON ก้อนเดียวให้หน้าเว็บ
// alerts.mjs — รวมทุกแหล่งเป็น endpoint เดียว พร้อม cache ในหน่วยความจำ
import http from "node:http";
import { readFile } from "node:fs/promises";
import { rainForecast } from "./rain.mjs";
import { buildThresholds, riverStatus } from "./river.mjs";
import { waterLevels } from "./waterlevel.mjs";
const cache = new Map();
async function cached(key, ttlMs, fn) {
const hit = cache.get(key);
if (hit && Date.now() - hit.at < ttlMs) return hit.value;
try {
const value = await fn();
cache.set(key, { at: Date.now(), value });
return value;
} catch (err) {
if (hit) return { ...hit.value, stale: true }; // แหล่งล่ม → ส่งค่าเก่าพร้อมบอกว่าเก่า
throw err;
}
}
const MIN = 60_000;
const SITE = { name: "นครสวรรค์", lat: 15.72, lon: 100.14, province: "นครสวรรค์" };
async function buildAlerts() {
const thresholds = await cached("thresholds", 24 * 60 * MIN, () => buildThresholds(SITE.lat, SITE.lon));
const [rain, river, stations] = await Promise.all([
cached("rain", 30 * MIN, () => rainForecast(SITE.lat, SITE.lon)),
cached("river", 60 * MIN, () => riverStatus(SITE.lat, SITE.lon, thresholds)),
cached("water", 10 * MIN, () => waterLevels(SITE.province)),
]);
const overBank = stations.filter((s) => !s.stale && s.percent_of_bank > 100);
return {
site: SITE.name,
generatedAt: new Date().toISOString(),
rain,
river: { ...river, thresholds },
overBank,
stations,
sources: ["Open-Meteo (CC BY 4.0)", "คลังข้อมูลน้ำแห่งชาติ (สสน.)"],
};
}
http
.createServer(async (req, res) => {
if (req.url === "/") {
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
return res.end(await readFile(new URL("./index.html", import.meta.url)));
}
if (req.url !== "/api/alerts") return res.writeHead(404).end();
try {
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
res.end(JSON.stringify(await buildAlerts(), null, 2));
} catch (err) {
res.writeHead(502).end(JSON.stringify({ error: String(err) }));
}
})
.listen(3000, () => console.log("http://localhost:3000/api/alerts"));| ข้อมูล | อายุ cache | เหตุผล |
|---|---|---|
| เกณฑ์จากสถิติ | 24 ชม. | คำนวณจากข้อมูล 15 ปี เปลี่ยนช้ามาก |
| ปริมาณน้ำ GloFAS | 60 นาที | โมเดลอัปเดตวันละครั้ง |
| ฝนพยากรณ์ | 30 นาที | โมเดลอัปเดตทุกไม่กี่ชั่วโมง |
| ระดับน้ำสถานี | 10 นาที | สถานีรายงานทุก 10–60 นาที |
ตอนทดสอบ ครั้งแรกใช้เวลาประมาณ 20 วินาที (ส่วนใหญ่หมดไปกับการดึงสถิติ 15 ปี) ส่วนครั้งถัดไปที่อ่านจาก cache ใช้ไม่ถึง 50 มิลลิวินาที ในระบบจริงควรคำนวณเกณฑ์ล่วงหน้าแล้วเก็บลงฐานข้อมูล และย้าย cache ไปไว้ใน Redis หรือใช้ revalidate ของ framework เช่น Next.js
7. แผนที่: OpenStreetMap + เรดาร์ฝน RainViewer ด้วย Leaflet
RainViewer ให้ภาพเรดาร์ฝนเป็น tile วางซ้อนบนแผนที่ได้ ขั้นตอนคือเรียก weather-maps.json เพื่อหาเฟรมล่าสุด แล้วประกอบ URL ของ tile จาก host + path ของเฟรมนั้น
<!doctype html>
<html lang="th">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>เตือนภัยน้ำ</title>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
<style>
body { margin: 0; font-family: system-ui, "Noto Sans Thai", sans-serif; background: #fbfaf7; color: #1f1d1a; }
header { padding: 12px 16px; display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }
.card { border: 1px solid #d6d1c4; border-radius: 6px; padding: 8px 12px; background: #fff; }
.watch { border-color: #c2410c; color: #c2410c; }
#map { height: calc(100vh - 80px); }
</style>
</head>
<body>
<header>
<strong id="site">กำลังโหลด…</strong>
<span class="card" id="river"></span>
<span class="card" id="rain"></span>
</header>
<div id="map"></div>
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
<script type="module">
const COLORS = { "ล้นตลิ่ง": "#b91c1c", "น้ำมาก": "#2563eb", "น้ำปกติ": "#15803d", "น้ำน้อย": "#ca8a04", "น้ำน้อยวิกฤติ": "#7c2d12" };
const map = L.map("map").setView([15.2, 100.3], 8);
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19,
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(map);
// 1) เรดาร์ฝนล่าสุดจาก RainViewer (โหลดตรงจากเบราว์เซอร์)
const rv = await (await fetch("https://api.rainviewer.com/public/weather-maps.json")).json();
const frame = rv.radar.past.at(-1);
L.tileLayer(`${rv.host}${frame.path}/256/{z}/{x}/{y}/2/1_1.png`, {
opacity: 0.6,
maxNativeZoom: 7, // ระดับซูมสูงสุดที่ฟรีให้ — ซูมเกินนี้ Leaflet จะขยายภาพเอง
maxZoom: 19,
attribution: '<a href="https://www.rainviewer.com/">RainViewer</a>',
}).addTo(map);
// 2) ข้อมูลตัวเลขจาก backend ของเรา
const a = await (await fetch("/api/alerts")).json();
document.querySelector("#site").textContent = `จุดเฝ้าระวัง: ${a.site}`;
const river = document.querySelector("#river");
river.textContent = `แม่น้ำ ${a.river.today.toLocaleString()} m³/s · ${a.river.level}`;
if (a.river.level !== "ปกติ") river.classList.add("watch");
const rainMax = a.rain.reduce((m, d) => (d.level > m.level ? d : m));
document.querySelector("#rain").textContent = `ฝน 3 วัน: สูงสุด ${rainMax.label} (${rainMax.date})`;
for (const s of a.stations) {
L.circleMarker([s.lat, s.lon], {
radius: 7,
color: s.stale ? "#9ca3af" : COLORS[s.situation] ?? "#6b7280",
fillOpacity: 0.8,
})
.bindPopup(`<b>${s.station}</b> (${s.code})<br>${s.level_msl} ม.รทก. · ${s.percent_of_bank}% ของตลิ่ง<br>${s.situation} · ${s.time}`)
.addTo(map);
}
</script>
</body>
</html>รันทั้งระบบด้วย node alerts.mjs แล้วเปิด http://localhost:3000
- OpenStreetMap (Tile Usage Policy): ต้องแสดงเครดิต © OpenStreetMap contributors ให้เห็นชัด, ต้องส่ง Referer/User-Agent ที่ระบุตัวแอปได้, ห้ามดาวน์โหลด tile จำนวนมากหรือโหลดล่วงหน้าเพื่อใช้ออฟไลน์, ต้องเคารพ HTTP cache และไม่มีการรับประกันความพร้อมใช้งาน (no SLA) ถ้าเว็บมีผู้ใช้มาก ควรใช้ผู้ให้บริการ tile เชิงพาณิชย์หรือตั้ง tile server เอง
- RainViewer (สรุปการเปลี่ยนแปลง API): ตั้งแต่ 1 ม.ค. 2026 API ฟรีให้ใช้ เพื่อการส่วนตัวหรือการศึกษา เหลือเฉพาะเรดาร์ย้อนหลัง 2 ชั่วโมงทุก 10 นาที, ไม่มี nowcast, ซูมได้สูงสุดระดับ 7 (จึงตั้ง
maxNativeZoom: 7), เหลือชุดสี Universal Blue ชุดเดียว และจำกัด 100 ครั้งต่อนาทีต่อ IP ต้องแสดงเครดิตพร้อมลิงก์ไปยัง rainviewer.com
8. เปรียบเทียบแหล่งข้อมูล
| Open-Meteo Forecast | Open-Meteo Flood | Google Flood Forecasting | ThaiWater (ทางการ) | ThaiWater (หน้าเว็บ) | RainViewer | |
|---|---|---|---|---|---|---|
| ข้อมูล | ฝนพยากรณ์ | ปริมาณน้ำ (จำลอง GloFAS) | สถานะน้ำท่วมราย gauge + แผนที่น้ำท่วม (AI) | ฝน น้ำท่า แหล่งน้ำ | ระดับน้ำ ฝน เขื่อน | เรดาร์ฝน (ภาพ) |
| ต้องใช้ key | ไม่ (งานไม่เชิงพาณิชย์) | ไม่ (งานไม่เชิงพาณิชย์) | ต้องผ่าน waitlist + API key (ฟรี) | ต้องขอสิทธิ์ | ไม่ แต่ไม่มีข้อตกลงการใช้งาน | ไม่ (ส่วนตัว/การศึกษา) |
| ความเสถียรของ API | มีเอกสาร มีเวอร์ชัน | มีเอกสาร มีเวอร์ชัน | มีเอกสาร (v1) | ตามมาตรฐาน | เปลี่ยนได้ทุกเมื่อ | มีเอกสาร แต่ลดบริการลงแล้ว |
| ความละเอียด | กริดของโมเดล | กริด ~5 กม. รายวัน | ราย gauge, ล่วงหน้าสูงสุด 7 วัน | รายสถานี | รายสถานี | ภาพเรดาร์ ซูมสูงสุด 7 |
| เครดิต | CC BY 4.0 | CC BY 4.0 | CC BY 4.0 | ตามข้อตกลง | ระบุแหล่ง สสน. | ต้องแสดงเครดิต |
ตารางนี้เน้นแหล่งที่เรียกได้ทันที ถ้าต้องการเทียบกับแหล่งที่ต้องขอสิทธิ์อย่าง WeatherNext 3 และ TMD Weather Forecast API ของกรมอุตุฯ ดู ใช้คู่กับแหล่งข้อมูลอื่นอย่างไร
9. เช็กลิสต์ก่อนเปิดเว็บเตือนภัยจริง
- พิกัดจุดเฝ้าระวังทุกจุดได้ตรวจแล้วว่าตกบนลำน้ำหลัก (กับดักที่ 2)
- เกณฑ์เตือนอธิบายที่มาได้ และหน้าเว็บบอกว่าคิดจากอะไร (กับดักที่ 3)
- สถานีที่ข้อมูลเก่าถูกตัดออกจากการตัดสิน (กับดักที่ 4)
- ตรวจเทียบพยากรณ์กับค่าวัดจริงในพื้นที่อย่างน้อยหนึ่งฤดูฝน (วิธีตรวจเทียบ)
- มี cache ฝั่ง server และมี fallback เมื่อแหล่งต้นทางล่ม
- แสดงเวลาที่ดึงข้อมูลล่าสุดบนหน้าเว็บ
- ฝนรายวันใช้รอบ 07:00–07:00 น. ก่อนเทียบเกณฑ์กรมอุตุฯ (กับดักที่ 1)
- แสดงเครดิตแหล่งข้อมูลครบ (Open-Meteo, Google, สสน., OpenStreetMap, RainViewer)
- มีลิงก์ไปประกาศทางการของกรมอุตุนิยมวิทยา สสน. และ ปภ.
- แจ้งเตือนเฉพาะ ตอนระดับเปลี่ยน ไม่ใช่ทุกครั้งที่ดึงข้อมูล (ช่องทางเช่น LINE Messaging API หรืออีเมล ส่วน LINE Notify ปิดบริการไปแล้วเมื่อ 31 มี.ค. 2025)
โน้ตที่เชื่อมกัน
บทความนี้กับ WeatherNext 3 กับประเทศไทย เป็นโน้ตคู่กัน: บทความนี้เน้น ระบบ (ดึงข้อมูลหลายแหล่ง ตั้งเกณฑ์ แสดงผล) ส่วนบทความนั้นเน้น แหล่งข้อมูลฝนหนึ่งแหล่งแบบเจาะลึก จุดที่สองบทความต่อกัน
| แนวคิด | ในบทความนี้ | ในบทความ WeatherNext 3 |
|---|---|---|
| รอบวันฝน 07:00–07:00 น. | กับดักที่ 1 (Open-Meteo, เวลาไทย) | 8.2 (WeatherNext, เวลา UTC) |
| เกณฑ์ปริมาณฝนของกรมอุตุฯ | หัวข้อ 2 | 8.1 (รวมเกณฑ์การกระจายของฝน) |
| percentile | กับดักที่ 3 (สถิติย้อนหลัง) | หัวข้อ 5 (ensemble) |
| ความละเอียดกริดกับค่าจุดเดียว | กับดักที่ 2 (GloFAS 5 กม.) | 4.3 (ฝนเฉลี่ยกริด 10 กม.) |
| ข้อมูลสถานีจริง | หัวข้อ 5 (ThaiWater) | 8.4 (ใช้ตรวจเทียบโมเดล) |
| AI ของ Google | หัวข้อ 4 (Flood Hub: แม่น้ำ) | หัวข้อ 1 (WeatherNext 3: ฝน) |
แหล่งอ้างอิง
งานวิชาการ
- Alfieri, L., Burek, P., Dutra, E., Krzeminski, B., Muraro, D., Thielen, J., & Pappenberger, F. (2013). GloFAS – global ensemble streamflow forecasting and flood early warning. Hydrology and Earth System Sciences, 17(3), 1161–1175. https://doi.org/10.5194/hess-17-1161-2013
- Harrigan, S., Zsoter, E., Alfieri, L., Prudhomme, C., Salamon, P., Wetterhall, F., Barnard, C., Cloke, H., & Pappenberger, F. (2020). GloFAS-ERA5 operational global river discharge reanalysis 1979–present. Earth System Science Data, 12(3), 2043–2060. https://doi.org/10.5194/essd-12-2043-2020
- Nearing, G., Cohen, D., Dube, V., Gauch, M., Gilon, O., Harrigan, S., และคณะ (2024). Global prediction of extreme floods in ungauged watersheds. Nature, 627(8004), 559–563. https://doi.org/10.1038/s41586-024-07145-1
- Zippenfenig, P. (2024). Open-Meteo.com Weather API (Version 1.4.0) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.7970649
เอกสารและแหล่งข้อมูล
- กรมอุตุนิยมวิทยา: เกณฑ์อากาศ
- กรมอุตุนิยมวิทยา: ฤดูกาลของประเทศไทย (เกณฑ์ฝน 24 ชม. 07:00–07:00 น.)
- Open-Meteo Docs: Weather Forecast API
- Open-Meteo Flood API
- Open-Meteo: Terms & Data License (GitHub)
- ECMWF CEMS: GloFAS Reporting Points
- Google Flood Forecasting (Google Research)
- Google Flood Forecasting API
- Flood Hub: Which locations are covered by the forecasts?
- มาตรฐานแลกเปลี่ยนข้อมูลน้ำ (standard.thaiwater.net)
- คลังข้อมูลน้ำแห่งชาติ (thaiwater.net)
- RainViewer API และ API Transition Summary
- OpenStreetMap Tile Usage Policy
- LINE Developers: LINE Notify will be discontinued (31 มี.ค. 2025)
- Leaflet