地図タイルの概要
ExpTech は XYZ タイル(slippy map tiles)で、レーダーエコー、衛星画像、風向・風速、QPESUMS の降水予報などの気象レイヤーと、ベースマップ、地形、道路・建物のタイルを提供します。 気象レイヤーは時刻ごとに更新されます。まずリストのエンドポイントで、今ある時刻を取得し、その時刻をタイル URL に入れます。タイルは MapLibre GL JS、Leaflet、OpenLayers などの地図ライブラリに、そのまま追加できます。
| 図層 | 時刻リスト | タイル URL | ズーム | データ出典 |
|---|---|---|---|---|
| レーダーエコー | /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 | ひまわり9号(気象庁、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 を参照 | 各ページを参照 |
データライセンス
Section titled “データライセンス”- レーダータイル出典明記・商用可
推奨表記データ出典:中央気象署、ExpTech Studio(exptech.dev)
- 衛星画像タイル出典明記・商用可
推奨表記データ出典:気象庁 ひまわり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)
- 中央気象署:中央気象署のデータ利用規約(政府データオープンライセンスなど)も適用されます。気象・地震情報は中央気象署の発表を正とします。
- 気象庁 ひまわり9号(NOAA Open Data 経由):ひまわりデータに対する気象庁および NOAA Open Data の利用条件も適用されます。
- 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 符号化です。下記を参照)。
- タイル URL には、リストにある時刻だけを使います。リストにない時刻は 404 または 500 になり、最も近い時刻へは自動では寄りません。
- URL テンプレートを地図に追加します。
{z}、{x}、{y}は地図ライブラリが置き換えます。
delta 符号化の時刻リスト
Section titled “delta 符号化の時刻リスト”レーダーと衛星のリストは [最も古い時刻, 差分, 差分, …] です。単位はすべて Unix 秒で、古い順です。最初の数が最も古い 1 件の時刻で、それ以降の各数は直前との差です。データがないときは空配列 [] です。
[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": モデルの初期時刻 } というオブジェクトです。風向・風速を参照してください。
タイルの仕様
Section titled “タイルの仕様”- 座標:XYZ(Web Mercator、EPSG:3857)です。
yは北から南へ増えます。OpenStreetMap と同じで、TMS ではありません。 - サイズと形式:256 × 256 ピクセルの WebP で、透過チャンネルを含みます。データがない領域は透明です。
- ズーム範囲:範囲外の
zも 200 を返します。内容は 1 × 1 の透明 GIF(Content-Type: image/gif)です。地図ライブラリでminzoom/maxzoomを設定し、上限を超えたときは最上位ズームのタイルを拡大してください。追加のリクエストは出さないでください。 - 拡張子:URL の末尾を
.pngにしても WebP が返ります。必ず.webpを使ってください。
同じタイル URL の内容は固定です(時刻もモデルの初期時刻も URL に含まれます)。クライアントで長期にキャッシュしてかまいません。
| 項目 | レスポンスヘッダ | 説明 |
|---|---|---|
| 時刻リスト(レーダー、衛星、風向・風速、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 に設定します。地図ライブラリが地図の隅に表示します。図層を重ねるときは、図層ごとに設定すれば足ります。下の例のレーダーレイヤーは、そのように設定しています。表記の位置と他の例はデータライセンスと出典表示を参照してください。
そのまま実行できるレーダーエコーのアニメーションページが 2 つあります。それぞれ 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 を 1 組作ります。先にすべて地図へ追加し(不透明度 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>最新の 1 枚だけでよいときは、times[times.length - 1] を取り、source と layer を 1 組追加すれば足ります。
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;
// 時刻ごとにレイヤーを 1 つ作り、先にすべて追加して(不透明度 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(内容は改行 1 つだけ)を返します。衛星と風向・風速のタイルは 404 の JSON
{"error":"Data not found for specified timestamp"}を返します。 - リストは時間とともに増え、削除されます。 長く開いたままのページは、定期的にリストを取り直してください(例:5 分ごと)。リストから外れた時刻は取り除いてください。
- タイムスタンプはすべて Unix 秒です。 URL の
{time}と{cycle}にミリ秒は使わないでください。 - レーダーのリストはタイルホスト(https://static.core-tnn1.exptech.dev)でも取得できます。ただしそちらは約 1 時間キャッシュされるため、最新の時刻を探す用途には向きません。リスト用のホストを使ってください。
- 例で使っている OpenStreetMap の公開タイルには利用ルールがあります。本番のサービスでは、別のベースマップに替えてください。
