コンテンツにスキップ
コンソール

地図タイルの概要

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 を参照 各ページを参照
  • レーダータイル出典明記・商用可

    推奨表記データ出典:中央気象署、ExpTech Studio(exptech.dev)

  • 衛星画像タイル出典明記・商用可

    推奨表記データ出典:気象庁 ひまわり9号(NOAA Open Data 経由)、ExpTech Studio(exptech.dev)

    cloudtop、cloudmask、sst、tpw など NOAA プロダクトのレイヤーを使う場合は、表記に NOAA を加えてください。

  • 推奨表記データ出典:NOAA GFS、ExpTech Studio(exptech.dev)

  • 推奨表記データ出典: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
  1. リストのエンドポイントで、使える時刻を取得します。
  2. リストを Unix 秒に復元します(レーダーと衛星のリストは delta 符号化です。下記を参照)。
  3. タイル URL には、リストにある時刻だけを使います。リストにない時刻は 404 または 500 になり、最も近い時刻へは自動では寄りません。
  4. URL テンプレートを地図に追加します。{z}、{x}、{y} は地図ライブラリが置き換えます。

レーダーと衛星のリストは [最も古い時刻, 差分, 差分, …] です。単位はすべて 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": モデルの初期時刻 } というオブジェクトです。風向・風速を参照してください。

  • 座標: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 を使います。

直近 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 を 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 組追加すれば足ります。

<!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;
// 時刻ごとにレイヤーを 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 の公開タイルには利用ルールがあります。本番のサービスでは、別のベースマップに替えてください。