跳到內容
主控台

風場

全球地面 10 公尺風場預報,來源是 NOAA GFS 與 ECMWF 開放資料(0.25° 經緯網格)。這是數值模式預報,不是觀測。 提供兩種形式:已上色的風速圖磚(WebP),以及含 u/v 分量的二進位風場(WND1),可在用戶端自行畫粒子動畫。

模式 model 時間步長 預報長度
gfs 0–120 小時逐時,之後每 3 小時 最長 384 小時
ecmwf 00、12 UTC 初始場:0–144 小時每 3 小時,之後每 6 小時;06、18 UTC 初始場:每 3 小時 00、12 UTC 到 240 小時;06、18 UTC 到 90 小時

每個有效時間只保留最新一次模式運算(cycle)的結果;有效時間已過去的資料保留約 48 小時。

共通規則(座標、快取、資料來源標示、完整地圖範例)見 圖磚概要。

  • 建議標示資料來源:NOAA GFS、ExpTech Studio(exptech.dev)

  • 風場(ECMWF)註明來源・可商用

    建議標示資料來源:ECMWF 開放資料(CC BY 4.0)、ExpTech Studio(exptech.dev)

上游條款
  • NOAA GFS:美國政府資料,原則上沒有使用限制;不可暗示 NOAA 推薦或背書你的產品。
  • ECMWF 開放資料:ECMWF 開放資料以 CC BY 4.0 授權:須標示 ECMWF 與授權名稱,並說明資料經過處理。

標示位置、範例與商業使用申請見資料授權與來源標示。

目前不需要 API Token。

GEThttps://api.core-tnn1.exptech.dev/api/v2/tiles/wind/{model}/list
伺服器TNN-1
路徑參數
  • modelstring必填

    gfs 或 ecmwf。

請求範例
curl --compressed "https://api.core-tnn1.exptech.dev/api/v2/tiles/wind/<model>/list"
回應

成功(沒有資料時為 [ ]);用戶端支援時以 gzip 壓縮

[
{
"t": 1794603600,
"c": 1794592800
},
{
"t": 1794614400,
"c": 1794614400
},
{
"t": 1794625200,
"c": 1794614400
}
]

model 不是 gfs 或 ecmwftext/html

伺服器錯誤(純文字錯誤訊息)text/plain

試試看
路徑參數

Token 只會從你的瀏覽器直接送到 API。

依有效時間由舊到新排列。列表同時包含已過去的時間與未來的預報時間。

欄位類型說明
[].tinteger有效時間(這筆風場代表的時刻),Unix 秒。
[].cinteger產生這筆預報的模式初始時間(cycle),Unix 秒。組圖磚與 WND1 網址時必填。
GEThttps://static.core-tnn1.exptech.dev/api/v2/tiles/wind/{model}/{cycle}/{time}/{z}/{x}/{y}.webp
伺服器TNN-1
路徑參數
  • modelstring必填

    gfs 或 ecmwf。

  • cycleinteger必填

    列表中該筆的 c(Unix 秒)。與目前保存的初始時間不符時回 404。

  • timeinteger必填

    列表中該筆的 t(Unix 秒)。

  • zinteger必填

    縮放層級,0–11。超出範圍回 1 × 1 透明 GIF。

  • xinteger必填

    XYZ 圖磚欄號。

  • yinteger必填

    XYZ 圖磚列號(由北往南)。

請求範例
curl -o output.webp "https://static.core-tnn1.exptech.dev/api/v2/tiles/wind/<model>/<cycle>/<time>/<z>/<x>/<y>.webp"
回應

image/webp,256 × 256,全球範圍,半透明(風越強越不透明),有損壓縮;帶 Cache-Control: public, max-age=1800。z 不在 0–11 時改回 1 × 1 透明 GIF(image/gif)binary image/webp

If-None-Match 與 ETag 相符,沒有內容沒有回應本文

cycle 或 time 為 0

bad cycle

沒有該時間,或 cycle 已被較新的模式運算取代(application/json;從內容無法分辨是哪一種)

{
"error": "Data not found for specified timestamp"
}

伺服器錯誤沒有回應本文

試試看
路徑參數

Token 只會從你的瀏覽器直接送到 API。

風速 0–32 m/s,超過 32 m/s 以 32 的顏色表示。顏色在節點之間線性內插:

風速 (m/s) 顏色
0 #282C6E
2 #3056A8
4 #348CBA
6 #40B296
8 #68C45E
11 #B0D048
14 #E2BE4A
17 #E28C42
21 #D6543C
25 #BA3478
28 #9E3CB4
32 #EED6FF

需要自己畫風場粒子動畫或取點位風向風速時,用這組端點下載整個網格的 u/v 分量。

GEThttps://api.core-tnn1.exptech.dev/api/v1/wind/{model}/list
伺服器TNN-1
路徑參數
  • modelstring必填

    gfs 或 ecmwf。

請求範例
curl --compressed "https://api.core-tnn1.exptech.dev/api/v1/wind/<model>/list"
回應

成功(沒有資料時為 [ ]);用戶端支援時以 gzip 壓縮

[
{
"t": 1794614400,
"c": 1794614400
},
{
"t": 1794625200,
"c": 1794614400
}
]

model 不是 gfs 或 ecmwftext/html

伺服器錯誤(純文字錯誤訊息)text/plain

試試看
路徑參數

Token 只會從你的瀏覽器直接送到 API。

回應格式與 /api/v2/tiles/wind/{model}/list 完全相同(t、c),兩者可以互換。

GEThttps://static.core-tnn1.exptech.dev/api/v1/wind/{model}/{cycle}/{time}.bin
伺服器TNN-1
路徑參數
  • modelstring必填

    gfs 或 ecmwf。

  • cycleinteger必填

    列表中該筆的 c(Unix 秒)。與目前保存的初始時間不符時回 404。

  • timeinteger必填

    列表中該筆的 t(Unix 秒)。

請求範例
curl -o output.bin --compressed "https://static.core-tnn1.exptech.dev/api/v1/wind/<model>/<cycle>/<time>.bin"
回應

application/octet-stream,WND1 格式。帶 Cache-Control: public, max-age=1800;用戶端支援時以 gzip 傳輸(Content-Encoding: gzip),瀏覽器的 fetch 會自動解壓縮binary WND1

If-None-Match 與 ETag 相符,沒有內容沒有回應本文

沒有該時間,或 cycle 已被較新的模式運算取代

{
"error": "Data not found for specified timestamp"
}

伺服器錯誤(純文字錯誤訊息)text/plain

試試看
路徑參數

Token 只會從你的瀏覽器直接送到 API。

全部 little-endian。固定標頭 67 bytes,接著是模式名稱,再接 u、v 兩個平面。

位移(bytes) 型別 欄位 說明
0 4 bytes ASCII magic 固定 WND1
4 uint16 version 目前為 1
6 uint16 width 網格欄數 W
8 uint16 height 網格列數 H
10 float64 lat0 第 0 列的緯度(度)
18 float64 lon0 第 0 欄的經度(度)
26 float64 dlat 每列的緯度步進(度,帶正負號;負值表示由北往南)
34 float64 dlon 每欄的經度步進(度,帶正負號)
42 float32 uMin u 分量量化下限(m/s)
46 float32 uMax u 分量量化上限(m/s)
50 float32 vMin v 分量量化下限(m/s)
54 float32 vMax v 分量量化上限(m/s)
58 uint64 time 有效時間,Unix 秒
66 uint8 nameLen 模式名稱長度 N
67 N bytes ASCII model gfs 或 ecmwf
67 + N uint8 × W × H u 東西向分量(向東為正),逐列排列
67 + N + W × H uint8 × W × H v 南北向分量(向北為正),逐列排列
  • 還原數值:u = uMin + byte × (uMax − uMin) / 255,v 同理。單位 m/s。上下限是這一份風場自己的最小、最大值,每個檔案都不同。
  • 網格位置:第 j 列、第 i 欄的位移是 j × W + i,位置是緯度 lat0 + j × dlat、經度 lon0 + i × dlon。經度會跨越 ±180°,請自行換算。
  • 缺值:原始資料的缺值會被量化成 0(也就是下限),無法與真正的最小值區分。整份風場都缺值時,上下限都是 0。
  • 網格大小、起點與步進請一律從標頭讀,不要寫死。

取列表中有效時間最接近現在的一筆,用它的 c 與 t 組圖磚網址,並依模式設定來源標示。

const API = "https://api.core-tnn1.exptech.dev";
const STATIC = "https://static.core-tnn1.exptech.dev";
async function addWind(map, model = "gfs") {
const frames = await (await fetch(`${API}/api/v2/tiles/wind/${model}/list`)).json();
if (frames.length === 0) return null;
const now = Date.now() / 1000;
let best = frames[0];
for (const f of frames) {
if (Math.abs(f.t - now) < Math.abs(best.t - now)) best = f;
}
map.addSource("wind", {
type: "raster",
tiles: [`${STATIC}/api/v2/tiles/wind/${model}/${best.c}/${best.t}/{z}/{x}/{y}.webp`],
tileSize: 256,
minzoom: 0,
maxzoom: 11,
attribution: model === "gfs"
? "資料來源:NOAA GFS、ExpTech Studio(exptech.dev)"
: "資料來源:ECMWF 開放資料(CC BY 4.0)、ExpTech Studio(exptech.dev)",
});
map.addLayer({ id: "wind", type: "raster", source: "wind" });
return best;
}

依上方 WND1 格式解碼整個網格,再取最近格點的風向與風速。

const STATIC = "https://static.core-tnn1.exptech.dev";
function decodeWnd1(buffer) {
const dv = new DataView(buffer);
const magic = String.fromCharCode(dv.getUint8(0), dv.getUint8(1), dv.getUint8(2), dv.getUint8(3));
if (magic !== "WND1") throw new Error("not a WND1 file");
const version = dv.getUint16(4, true);
if (version !== 1) throw new Error(`unsupported WND1 version ${version}`);
const width = dv.getUint16(6, true);
const height = dv.getUint16(8, true);
const lat0 = dv.getFloat64(10, true);
const lon0 = dv.getFloat64(18, true);
const dlat = dv.getFloat64(26, true);
const dlon = dv.getFloat64(34, true);
const uMin = dv.getFloat32(42, true);
const uMax = dv.getFloat32(46, true);
const vMin = dv.getFloat32(50, true);
const vMax = dv.getFloat32(54, true);
const time = Number(dv.getBigUint64(58, true));
const nameLen = dv.getUint8(66);
const model = new TextDecoder().decode(new Uint8Array(buffer, 67, nameLen));
const n = width * height;
const start = 67 + nameLen;
const uBytes = new Uint8Array(buffer, start, n);
const vBytes = new Uint8Array(buffer, start + n, n);
const u = new Float32Array(n);
const v = new Float32Array(n);
const uScale = (uMax - uMin) / 255;
const vScale = (vMax - vMin) / 255;
for (let k = 0; k < n; k++) {
u[k] = uMin + uBytes[k] * uScale;
v[k] = vMin + vBytes[k] * vScale;
}
return { model, time, width, height, lat0, lon0, dlat, dlon, u, v };
}
// 取最近格點的風(u 向東、v 向北,m/s)
function windAt(field, lat, lon) {
const j = Math.round((lat - field.lat0) / field.dlat);
const wrap = Math.abs(360 / field.dlon);
let i = Math.round((lon - field.lon0) / field.dlon);
i = ((i % wrap) + wrap) % wrap;
if (j < 0 || j >= field.height || i >= field.width) return null;
const k = j * field.width + i;
const u = field.u[k];
const v = field.v[k];
const speed = Math.hypot(u, v);
const from = (Math.atan2(-u, -v) * 180 / Math.PI + 360) % 360; // 風的來向,0° = 北風
return { u, v, speed, from };
}
const res = await fetch(`${STATIC}/api/v1/wind/gfs/1794614400/1794625200.bin`);
const field = decodeWnd1(await res.arrayBuffer());
console.log(windAt(field, 25.03, 121.56));
  • cycle 會變。 新的模式運算出來後,同一個有效時間會改由新的 cycle 提供,舊 cycle 的圖磚與 WND1 網址回 404(約 10 秒內重試仍是 404)。收到 404 時請重新取得列表,用新的 c 組網址。
  • 請只使用列表裡的 t 與 c,兩者都是 Unix 秒。
  • model 不是 gfs 或 ecmwf,或路徑不是數字時回 404。
  • 同一個網址的內容固定,可以在用戶端長期快取。
  • GFS 與 ECMWF 的時間解析度與預報長度不同,不能直接互相替換;比較兩者時請用相同的有效時間。
  • 圖上請依使用的模式,照資料授權的建議標示註明來源。