跳到內容
主控台

圖磚概要

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)

    使用 cloudtop、cloudmask、sst、tpw 等 NOAA 產品圖層時,標示文字再加上 NOAA。

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

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

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

  • 建議標示資料來源:中央氣象署 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
  1. 向列表端點取得可用時間。
  2. 把列表還原成 Unix 秒(雷達、衛星的列表是 delta 編碼,見下方)。
  3. 只用列表裡的時間組圖磚網址。列表以外的時間會得到 404 或 500,不會自動找最接近的一筆。
  4. 把網址模板加進地圖。{z}、{x}、{y} 留給地圖函式庫替換。

雷達與衛星的列表是 [最舊時間, 差值, 差值, …],單位都是 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。

在地圖上使用時,把各圖層的建議標示(見本頁的資料授權)設定在圖層 source 的 attribution,地圖函式庫會把它顯示在地圖角落;疊多個圖層時,每個圖層各自設定即可。下方範例的雷達圖層就是這樣設定。標示的位置與其他範例見資料授權與來源標示。

兩個可以直接執行的雷達回波動畫頁面,分別使用 MapLibre GL JS 與 Leaflet。

取得最近 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>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/maplibre-gl.css" />
<script src="https://unpkg.com/[email protected]/dist/maplibre-gl.js"></script>
<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 即可。

<!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>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
<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: "&copy; 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 公開圖磚有使用規範,正式服務請換成其他底圖。