圖磚概要
ExpTech 以 XYZ 圖磚(slippy map tiles)提供雷達回波、衛星雲圖、風場、QPESUMS 降水預報等氣象圖層,以及底圖、地形、街道建築圖磚。 氣象圖層都依時間更新:先向列表端點取得目前有哪些時間,再把時間填進圖磚網址。圖磚可以直接加進 MapLibre GL JS、Leaflet、OpenLayers 等地圖函式庫。
| 圖層 | 時間列表 | 圖磚網址 | 縮放 | 資料來源 |
|---|---|---|---|---|
| 雷達回波 | /api/v2/tiles/radar/list |
/api/v2/tiles/radar/{time}/{z}/{x}/{y}.webp |
3–12 | 中央氣象署 |
| 衛星雲圖 | /api/v2/tiles/satellite/{channel}/list |
/api/v2/tiles/satellite/{channel}/{style}/{time}/{z}/{x}/{y}.webp |
0–11 | Himawari-9(JMA,經 NOAA Open Data) |
| 風場 | /api/v2/tiles/wind/{model}/list |
/api/v2/tiles/wind/{model}/{cycle}/{time}/{z}/{x}/{y}.webp |
0–11 | NOAA GFS、ECMWF 開放資料 |
| QPESUMS | /api/v2/tiles/qpesums/list |
見該頁 | 見該頁 | 中央氣象署 QPESUMS |
| 底圖 | 不需要(TileJSON) | 見該頁 | 見 TileJSON | 見該頁 |
- 雷達回波圖磚註明來源・可商用
建議標示資料來源:中央氣象署、ExpTech Studio(exptech.dev)
- 衛星雲圖圖磚註明來源・可商用
建議標示資料來源:日本氣象廳 Himawari-9(經 NOAA Open Data)、ExpTech Studio(exptech.dev)
- 風場(GFS)註明來源・可商用
建議標示資料來源:NOAA GFS、ExpTech Studio(exptech.dev)
- 風場(ECMWF)註明來源・可商用
建議標示資料來源:ECMWF 開放資料(CC BY 4.0)、ExpTech Studio(exptech.dev)
- QPESUMS 圖磚註明來源・可商用
建議標示資料來源:中央氣象署 QPESUMS、ExpTech Studio(exptech.dev)
- 行政區底圖、地形註明來源・可商用
建議標示資料來源:ExpTech Studio(exptech.dev)
- 街道建築註明來源・可商用
建議標示資料來源:© OpenStreetMap contributors、© OpenMapTiles、ExpTech Studio(exptech.dev)
- 中央氣象署:另須遵守中央氣象署的資料使用規範(例如政府資料開放授權條款);氣象與地震資訊以中央氣象署發布的內容為準。
- 日本氣象廳 Himawari-9(經 NOAA Open Data):另須遵守日本氣象廳與 NOAA Open Data 對 Himawari 資料的使用條件。
- NOAA、NOAA GFS:美國政府資料,原則上沒有使用限制;不可暗示 NOAA 推薦或背書你的產品。
- ECMWF 開放資料:ECMWF 開放資料以 CC BY 4.0 授權:須標示 ECMWF 與授權名稱,並說明資料經過處理。
- OpenStreetMap:OpenStreetMap 資料以 ODbL 授權:地圖上須顯示「© OpenStreetMap contributors」並連到 https://www.openstreetmap.org/copyright 。
- OpenMapTiles:使用 OpenMapTiles 圖層架構的圖磚須標示「© OpenMapTiles」。
標示位置、範例與商業使用申請見資料授權與來源標示。
目前不需要 API Token。所有列表與圖磚都回 Access-Control-Allow-Origin: *,可以直接在瀏覽器跨網域載入。
氣象圖層的列表與圖磚放在不同主機:
- 時間列表:https://api.core-tnn1.exptech.dev
- 圖磚與二進位檔:https://static.core-tnn1.exptech.dev
- 底圖、地形、街道建築:https://lb.exptech.dev
- 向列表端點取得可用時間。
- 把列表還原成 Unix 秒(雷達、衛星的列表是 delta 編碼,見下方)。
- 只用列表裡的時間組圖磚網址。列表以外的時間會得到 404 或 500,不會自動找最接近的一筆。
- 把網址模板加進地圖。
{z}、{x}、{y}留給地圖函式庫替換。
delta 編碼的時間列表
Section titled “delta 編碼的時間列表”雷達與衛星的列表是 [最舊時間, 差值, 差值, …],單位都是 Unix 秒,由舊到新。第一個數是最舊一筆的時間,之後每個數是與前一筆的差。沒有資料時是空陣列 []。
[1794641400, 600, 600, 600]還原後是 1794641400、1794642000、1794642600、1794643200(臺灣時間 2026-11-14 15:30–16:00)。
// [最舊時間, Δ, Δ, …] → [時間, 時間, …](Unix 秒,由舊到新)function decodeTimes(list) { const out = []; for (let i = 0; i < list.length; i++) { out.push(i === 0 ? list[0] : out[i - 1] + list[i]); } return out;}風場列表不同:每筆是 { "t": 有效時間, "c": 模式初始時間 } 物件,見 風場。
- 座標:XYZ(Web Mercator,EPSG:3857),
y由北往南遞增,與 OpenStreetMap 相同,不是 TMS。 - 尺寸與格式:256 × 256 像素 WebP,含透明通道。沒有資料的區域是透明的。
- 縮放範圍:超出範圍的
z仍回 200,內容是 1 × 1 透明 GIF(Content-Type: image/gif)。請在地圖函式庫設定minzoom/maxzoom,超過上限時放大最高層級的圖磚,不要再請求。 - 副檔名:網址結尾寫
.png也會回 WebP。請一律用.webp。
同一個圖磚網址的內容固定(時間、模式初始時間都在網址裡),可以放心在用戶端長期快取。
| 項目 | 回應標頭 | 說明 |
|---|---|---|
| 時間列表(雷達、衛星、風場、QPESUMS) | 無 Cache-Control |
新資料最多延遲約 1 秒出現在列表;需要最新時間時直接重新請求 |
| 雷達圖磚 | 無 Cache-Control,有 ETag |
帶 If-None-Match 可得 304 |
| 衛星圖磚 | Cache-Control: public, max-age=300,有 ETag |
404 的結果會保留約 10 秒 |
| 風場圖磚、WND1 二進位 | Cache-Control: public, max-age=1800,有 ETag |
404 的結果會保留約 10 秒 |
| QPESUMS 圖磚 | 未確認 | 404 的結果會保留約 5 秒 |
列表回應在用戶端支援時會以 gzip 壓縮(Content-Encoding: gzip)。瀏覽器的 fetch 會自動解壓縮;用 curl 請加 --compressed。
資料來源標示
Section titled “資料來源標示”在地圖上使用時,把各圖層的建議標示(見本頁的資料授權)設定在圖層 source 的 attribution,地圖函式庫會把它顯示在地圖角落;疊多個圖層時,每個圖層各自設定即可。下方範例的雷達圖層就是這樣設定。標示的位置與其他範例見資料授權與來源標示。
兩個可以直接執行的雷達回波動畫頁面,分別使用 MapLibre GL JS 與 Leaflet。
MapLibre GL JS
Section titled “MapLibre GL JS”取得最近 12 個雷達時間,預先載入後輪流顯示。底圖用 OpenStreetMap 的公開圖磚只是為了讓範例能直接執行;正式服務請改用自己的底圖或 ExpTech 底圖。
<!doctype html><html lang="zh-Hant-TW"><head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>雷達回波動畫(MapLibre GL JS)</title> <style> html, body, #map { height: 100%; margin: 0; } #time { position: absolute; top: 10px; left: 10px; padding: 4px 8px; background: #fff; font: 14px sans-serif; } </style></head><body> <div id="map"></div> <div id="time"></div> <script> const API = "https://api.core-tnn1.exptech.dev"; const STATIC = "https://static.core-tnn1.exptech.dev"; const FRAMES = 12; // 動畫使用最近幾個時間 const INTERVAL = 500; // 每個畫面停留的毫秒數 const OPACITY = 0.8;
function decodeTimes(list) { const out = []; for (let i = 0; i < list.length; i++) { out.push(i === 0 ? list[0] : out[i - 1] + list[i]); } return out; }
function formatTime(sec) { return new Date(sec * 1000).toLocaleString("zh-TW", { timeZone: "Asia/Taipei", hour12: false }); }
const map = new maplibregl.Map({ container: "map", center: [121, 23.7], zoom: 6, style: { version: 8, sources: { osm: { type: "raster", tiles: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"], tileSize: 256, maxzoom: 19, attribution: "© OpenStreetMap contributors", }, }, layers: [{ id: "osm", type: "raster", source: "osm" }], }, });
async function addRadar() { const res = await fetch(`${API}/api/v2/tiles/radar/list`); if (!res.ok) throw new Error(`radar list: HTTP ${res.status}`); const times = decodeTimes(await res.json()).slice(-FRAMES); if (times.length === 0) return;
// 每個時間一組 source + layer。全部先加進地圖(透明度 0)讓圖磚預先載入,再輪流顯示。 times.forEach((t, i) => { map.addSource(`radar-${t}`, { type: "raster", tiles: [`${STATIC}/api/v2/tiles/radar/${t}/{z}/{x}/{y}.webp`], tileSize: 256, minzoom: 3, maxzoom: 12, attribution: "資料來源:中央氣象署、ExpTech Studio(exptech.dev)", }); map.addLayer({ id: `radar-${t}`, type: "raster", source: `radar-${t}`, paint: { "raster-opacity": i === times.length - 1 ? OPACITY : 0, "raster-fade-duration": 0, }, }); });
const label = document.getElementById("time"); let current = times.length - 1; label.textContent = formatTime(times[current]); setInterval(() => { map.setPaintProperty(`radar-${times[current]}`, "raster-opacity", 0); current = (current + 1) % times.length; map.setPaintProperty(`radar-${times[current]}`, "raster-opacity", OPACITY); label.textContent = formatTime(times[current]); }, INTERVAL); }
map.on("load", () => { addRadar().catch((err) => console.error(err)); }); </script></body></html>只要最新一張時,取 times[times.length - 1] 加一組 source + layer 即可。
Leaflet
Section titled “Leaflet”<!doctype html><html lang="zh-Hant-TW"><head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>雷達回波動畫(Leaflet)</title> <style> html, body, #map { height: 100%; margin: 0; } .radar-time { padding: 4px 8px; background: #fff; font: 14px sans-serif; } </style></head><body> <div id="map"></div> <script> const API = "https://api.core-tnn1.exptech.dev"; const STATIC = "https://static.core-tnn1.exptech.dev"; const FRAMES = 12; const INTERVAL = 500; const OPACITY = 0.8;
function decodeTimes(list) { const out = []; for (let i = 0; i < list.length; i++) { out.push(i === 0 ? list[0] : out[i - 1] + list[i]); } return out; }
function formatTime(sec) { return new Date(sec * 1000).toLocaleString("zh-TW", { timeZone: "Asia/Taipei", hour12: false }); }
const map = L.map("map").setView([23.7, 121], 7); L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19, attribution: "© OpenStreetMap contributors", }).addTo(map);
const label = L.control({ position: "topright" }); label.onAdd = () => L.DomUtil.create("div", "radar-time"); label.addTo(map);
async function addRadar() { const res = await fetch(`${API}/api/v2/tiles/radar/list`); if (!res.ok) throw new Error(`radar list: HTTP ${res.status}`); const times = decodeTimes(await res.json()).slice(-FRAMES); if (times.length === 0) return;
// 每個時間一個圖層,先全部加入(透明度 0)讓圖磚預先載入。 const layers = times.map((t, i) => L.tileLayer(`${STATIC}/api/v2/tiles/radar/${t}/{z}/{x}/{y}.webp`, { minZoom: 3, maxNativeZoom: 12, // 超過 12 時放大第 12 層的圖磚 maxZoom: 19, opacity: i === times.length - 1 ? OPACITY : 0, attribution: "資料來源:中央氣象署、ExpTech Studio(exptech.dev)", }).addTo(map), );
let current = times.length - 1; label.getContainer().textContent = formatTime(times[current]); setInterval(() => { layers[current].setOpacity(0); current = (current + 1) % layers.length; layers[current].setOpacity(OPACITY); label.getContainer().textContent = formatTime(times[current]); }, INTERVAL); }
addRadar().catch((err) => console.error(err)); </script></body></html>- 只請求列表裡的時間。 不存在的時間:雷達圖磚回 500(內容只有一個換行),衛星與風場圖磚回 404 JSON
{"error":"Data not found for specified timestamp"}。 - 列表會隨時間增加、刪除。 長時間開著的頁面請定期重新取得列表(例如每 5 分鐘),並移除已經不在列表中的時間。
- 時間戳都是 Unix 秒。 網址裡的
{time}、{cycle}不要用毫秒。 - 雷達列表在圖磚主機(https://static.core-tnn1.exptech.dev)也能取得,但那份會快取約 1 小時,不適合拿來找最新時間;請用列表主機。
- 範例使用的 OpenStreetMap 公開圖磚有使用規範,正式服務請換成其他底圖。
