風向・風速
全球の地上 10 m の風の予報です。出典は NOAA GFS と ECMWF オープンデータ(0.25° の緯度経度格子)です。これは数値予報であり、観測ではありません。 形は 2 つあります。色を付けた風速タイル(WebP)と、u/v 成分を含むバイナリ(WND1)です。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 時間まで |
各有効時刻には、最新の 1 回のモデル計算(cycle)の結果だけを残します。有効時刻を過ぎたデータは約 48 時間保持します。
共通のルール(座標、キャッシュ、出典表示、完全な地図の例)は 地図タイルの概要 を参照してください。
データライセンス
Section titled “データライセンス”- 風(GFS)出典明記・商用可
推奨表記データ出典: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 は不要です。
利用可能な時刻の取得
Section titled “利用可能な時刻の取得”modelgfs または ecmwf。
curl --compressed "https://api.core-tnn1.exptech.dev/api/v2/tiles/wind/<model>/list"const res = await fetch("https://api.core-tnn1.exptech.dev/api/v2/tiles/wind/<model>/list");if (!res.ok) throw new Error(`HTTP ${res.status}`);const data = await res.json();console.log(data);import requests
res = requests.get( "https://api.core-tnn1.exptech.dev/api/v2/tiles/wind/<model>/list", timeout=10,)res.raise_for_status()print(res.json())成功(データがないときは [ ])。クライアントが対応しているときは gzip で圧縮
[ { "t": 1794603600, "c": 1794592800 }, { "t": 1794614400, "c": 1794614400 }, { "t": 1794625200, "c": 1794614400 }]model が gfs でも ecmwf でもないtext/html
サーバーエラー(プレーンテキストのエラーメッセージ)text/plain
試してみる
レスポンスのフィールド
Section titled “レスポンスのフィールド”有効時刻の古い順です。リストには、過ぎた時刻と将来の予報時刻の両方が入っています。
| フィールド | 型 | 説明 |
|---|---|---|
[].t | integer | 有効時刻(この風が表す時刻)です。Unix 秒。 |
[].c | integer | この予報を出したモデルの初期時刻(cycle)です。Unix 秒。タイルと WND1 の URL を組むときに必須です。 |
風速タイルの取得
Section titled “風速タイルの取得”https://static.core-tnn1.exptech.dev/api/v2/tiles/wind/{model}/{cycle}/{time}/{z}/{x}/{y}.webp modelgfs または ecmwf。
cycleリストのその件の c(Unix 秒)です。現在保持している初期時刻と違うときは 404 です。
timeリストのその件の t(Unix 秒)です。
zズームレベル。0–11。範囲外は 1 × 1 の透明 GIF を返します。
xXYZ タイルの列番号です。
yXYZ タイルの行番号です(北から南)。
curl -o output.webp "https://static.core-tnn1.exptech.dev/api/v2/tiles/wind/<model>/<cycle>/<time>/<z>/<x>/<y>.webp"const res = await fetch("https://static.core-tnn1.exptech.dev/api/v2/tiles/wind/<model>/<cycle>/<time>/<z>/<x>/<y>.webp");if (!res.ok) throw new Error(`HTTP ${res.status}`);const blob = await res.blob();import requests
res = requests.get( "https://static.core-tnn1.exptech.dev/api/v2/tiles/wind/<model>/<cycle>/<time>/<z>/<x>/<y>.webp", timeout=10,)res.raise_for_status()with open("output.webp", "wb") as f: f.write(res.content)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"}サーバーエラーレスポンスボディなし
試してみる
風速 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 |
元の風データ(WND1)
Section titled “元の風データ(WND1)”風のパーティクルアニメーションを自分で描くとき、または地点の風向・風速を取るときは、このエンドポイントで格子全体の u/v 成分をダウンロードします。
利用可能な時刻の取得(v1)
Section titled “利用可能な時刻の取得(v1)”modelgfs または ecmwf。
curl --compressed "https://api.core-tnn1.exptech.dev/api/v1/wind/<model>/list"const res = await fetch("https://api.core-tnn1.exptech.dev/api/v1/wind/<model>/list");if (!res.ok) throw new Error(`HTTP ${res.status}`);const data = await res.json();console.log(data);import requests
res = requests.get( "https://api.core-tnn1.exptech.dev/api/v1/wind/<model>/list", timeout=10,)res.raise_for_status()print(res.json())成功(データがないときは [ ])。クライアントが対応しているときは gzip で圧縮
[ { "t": 1794614400, "c": 1794614400 }, { "t": 1794625200, "c": 1794614400 }]model が gfs でも ecmwf でもないtext/html
サーバーエラー(プレーンテキストのエラーメッセージ)text/plain
試してみる
レスポンス形式は /api/v2/tiles/wind/{model}/list と完全に同じです(t、c)。どちらでも使えます。
風データのダウンロード
Section titled “風データのダウンロード”modelgfs または ecmwf。
cycleリストのその件の c(Unix 秒)です。現在保持している初期時刻と違うときは 404 です。
timeリストのその件の t(Unix 秒)です。
curl -o output.bin --compressed "https://static.core-tnn1.exptech.dev/api/v1/wind/<model>/<cycle>/<time>.bin"const res = await fetch("https://static.core-tnn1.exptech.dev/api/v1/wind/<model>/<cycle>/<time>.bin");if (!res.ok) throw new Error(`HTTP ${res.status}`);const blob = await res.blob();import requests
res = requests.get( "https://static.core-tnn1.exptech.dev/api/v1/wind/<model>/<cycle>/<time>.bin", timeout=10,)res.raise_for_status()with open("output.bin", "wb") as f: f.write(res.content)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
試してみる
WND1 形式
Section titled “WND1 形式”すべて little-endian です。固定ヘッダは 67 bytes で、その後にモデル名、続いて u と v の 2 つの面が続きます。
| オフセット(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 です。 - 格子の大きさ、起点、間隔は、必ずヘッダから読んでください。固定値にしないでください。
MapLibre GL JS に風速レイヤーを追加
Section titled “MapLibre GL JS に風速レイヤーを追加”リストのうち、有効時刻が現在に最も近い 1 件を取り、その c と t でタイル URL を組み、モデルに合わせて出典を設定します。
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 をデコードして地点の風を調べる
Section titled “WND1 をデコードして地点の風を調べる”上の 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 の URL は 404 になります(約 10 秒以内に再試行しても 404 のままです)。404 を受けたらリストを取り直し、新しい
cで URL を組んでください。 - リストにある
tとcだけを使ってください。どちらも Unix 秒です。 modelがgfsでもecmwfでもないとき、またはパスが数値でないときは 404 です。- 同じ URL の内容は固定なので、クライアントで長期にキャッシュできます。
- GFS と ECMWF は時間分解能と予報の長さが違うため、そのまま置き換えられません。比べるときは、同じ有効時刻を使ってください。
- 地図には、使ったモデルに合わせて、データライセンスの推奨表記で出典を表示してください。
