1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

日本周辺の地震をリアルタイム地図化する — 気象庁 API が CORS で叩けない問題を USGS GeoJSON で回避した話

1
Posted at

「日本で最近どこで地震があったか、地図で一目で見たい」を ブラウザだけ で実現する。気象庁の防災情報 XML はブラウザ直叩きできない (CORS なし)、地震情報サイトは UI が遅い、というニッチを USGS の FDSN event service (GeoJSON、CORS あり、認証不要) で埋める。マグニチュードで円サイズ、深さで色、5 分おき自動更新。約 300 行。

earthquake-jp の画面: 日本列島を中心にした暗色マップに、過去 7 日 M2.5+ の地震 15 件がエピセンターの円として描画されている。表面付近 (0-30km) は赤、中浅は黄〜緑、深い地震は青で表示。下部に深さと大きさの凡例、件数/最大/平均/直近時刻のスタッツ

🌐 デモ: https://sen.ltd/portfolio/earthquake-jp/
📦 GitHub: https://github.com/sen-ltd/earthquake-jp

なぜ気象庁ではなく USGS なのか

国内発生地震を最も網羅的に持っているのは 気象庁 (JMA) — これは事実。ただし、ブラウザから直接叩こうとすると詰まる:

  • CORS が無効 → ブラウザの fetch / XHR から取れない (Same-origin policy で Access-Control-Allow-Origin ヘッダ無し)
  • 配信形式が XML で JSON 化されていない (P2P 地震情報など独立プロジェクトが二次配信しているが、それぞれ別 API)
  • bulk download にはアカウント防災科研 J-SHIS 経由が必要

つまりサーバを 1 段挟まないと使えない、というのが本記事の前提課題。

これに対して USGS の FDSN event service (earthquake.usgs.gov/fdsnws/event/1/) は:

  • CORS が有効 (Access-Control-Allow-Origin: *)
  • auth 不要
  • GeoJSON 直返し
  • 5-10 分の遅延 で更新される

なので fetch() 1 行で完結する。代償は「USGS の M2.5 以上だけ」になることで、M2 以下の微小地震は気象庁の独壇場のままだが、「日本周辺の最近の地震を視覚的に把握する」 という目的には十分。

URL 1 本にすべて詰める

USGS の FDSN クエリは GET パラメータで時間窓と地理窓を指定できる。日本周辺の bbox は (lat 24-46, lon 122-148) を採用 (列島 + 南西諸島 + 太平洋プレート境界の手前まで):

export const JP_BBOX = {
  minLat: 24, maxLat: 46, minLon: 122, maxLon: 148,
};

export function buildFeedUrl(windowHours, minMag, nowMs = Date.now()) {
  const end = new Date(nowMs).toISOString();
  const start = new Date(nowMs - windowHours * 3600_000).toISOString();
  const params = new URLSearchParams({
    format: "geojson",
    starttime: start, endtime: end,
    minlatitude: String(JP_BBOX.minLat),
    maxlatitude: String(JP_BBOX.maxLat),
    minlongitude: String(JP_BBOX.minLon),
    maxlongitude: String(JP_BBOX.maxLon),
    minmagnitude: String(minMag),
    orderby: "time",
  });
  return `https://earthquake.usgs.gov/fdsnws/event/1/query?${params.toString()}`;
}

nowMs がパラメータ化されているので、Date.now() を 2_000_000_000_000 (架空の時刻) に固定したテストが書ける。

マグニチュードを「線形の円半径」にマップする理由

リヒタースケールは エネルギーの底-10 対数 で、M7 は M6 の 32 倍のエネルギー、M5 は M3 の 1000 倍。ナイーブに「面積 ∝ エネルギー」「半径 ∝ エネルギー」をやると、M9 の円が画面を埋め尽くす一方で M3 が見えなくなる。

実用的なのは 半径を線形 in magnitude にすること:

export function magnitudeToRadius(mag) {
  if (mag == null || mag < 0) return 4;
  const r = 3 + (mag - 1) * 4.4;
  if (r < 3) return 3;
  if (r > 38) return 38;
  return r;
}

これで:

観測 半径
M1 3 px
M3 11.8 px
M5 20.6 px
M7 29.4 px
M9 38 px (clamp)

「M が 2 上がると、面積はおおよそ 2-3 倍に増える」見え方で、視覚的には自然な「ひと回り大きい」スケールになる。 数学的にはエネルギー比とは違うが、地図上で読みやすい ことの方が大事。

クランプは「M9 級が 1 件入ったときに視野全体を埋めない」ため。実際の 2011 東北 (M9.1) を入れても、隣接県の M5 が同じ画面で見える、を保ちたい。

深さで色を変える理由

地震の被害は マグニチュード × 深さ で決まる。M5 でも深さ 10 km なら直下型で建物が揺れる、M6 でも深さ 500 km なら広域でゆるく揺れるだけ。なので地図上で 深さを色で示す と「赤 (浅) は気をつけたほうがいい、青 (深) は揺れの広がりは大きいが被害は少なめ」というメンタルマップが直感的に立つ。

6 段の anchor を線形補間:

const stops = [
  [0,   [0xef, 0x44, 0x44]], // red    — surface
  [30,  [0xf9, 0x73, 0x16]], // orange
  [70,  [0xea, 0xb3, 0x08]], // yellow
  [150, [0x10, 0xb9, 0x81]], // teal-green
  [300, [0x06, 0x6c, 0xb5]], // blue
  [700, [0x1e, 0x40, 0xaf]], // deep blue — slab
];

色相を一周させない (黄を超えたら緑→青に進む、赤に戻らない) ので、色相環の循環を見ているのか上下を見ているのか で迷うことがない。これは地理可視化での経験則。

Leaflet 採用と CARTO ダークタイル

地図ライブラリは Leaflet (1.9.4) を CDN ロード。maplibre-gl は ベクタータイル前提でいい感じだが、無料の vector tile プロバイダ (MapTiler, Stadia) は API キーが要る。今回は キー不要・raster 完結 で済む構成にしたいので Leaflet に倒した。

タイルは CartoDB Voyager Dark (OSM ベースの暗色テーマ、attribution 守れば free):

const TILE_URL = "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png";
const TILE_ATTRIB =
  '&copy; <a href="https://www.openstreetmap.org/copyright">OSM</a> &copy; <a href="https://carto.com/attributions">CARTO</a>';

L.tileLayer(TILE_URL, { attribution: TILE_ATTRIB, maxZoom: 9, minZoom: 3 }).addTo(map);

これで「ページ全体は暗いダークテーマ、エピセンターの円だけ色がついて目立つ」見せ方になる。maxZoom: 9 は「日本列島全体が見える + 都道府県名が読める」を確保しつつ、深い zoom-in は USGS 詳細ページに譲るための制限。

5 分 polling の理由

USGS は WebSocket / SSE による push を提供していない。なので クライアント側で polling。間隔は 5 分:

  • USGS の公開遅延が 5-10 分なので、5 分間隔ならほぼ全更新を拾える
  • 5 分 polling で M2.5+ × 7 日窓だと、レスポンスは 10-30 件程度。10 KB に満たない GET。USGS のレートリミットに当たることはない
  • ブラウザのタブがバックグラウンドに行くと setInterval が throttle されるが、復帰時に即 fetch する設計にしてあるので、戻ったときに最新化される
function scheduleAutoRefresh() {
  if (state.refreshTimer) clearInterval(state.refreshTimer);
  state.refreshTimer = setInterval(fetchAndRender, REFRESH_INTERVAL_MS);
  state.countdownTimer = setInterval(updateCountdown, 1000);
  updateCountdown();
}

ステータス行に「次回更新まで X:YZ」のカウントダウンを出している。ユーザに「今表示しているデータがどの時点のものか」を伝えるのは可視化ツールの基本。

純粋ロジックの単独テスト

quakes.js は DOM / Leaflet / fetch を一切持たない。script.js の側にあるのは「マップ作る・fetch する・popup 出す」だけで、計算はすべて quakes.js 経由。

これで node --test が直接消費できる:

$ npm test
✔ buildFeedUrl carries window + bbox + minmagnitude params
✔ buildFeedUrl windows backwards from nowMs
✔ magnitudeToRadius grows linearly in magnitude
✔ magnitudeToRadius clamps the upper end so M9 doesn't blow out
✔ depthToColor returns warm hex for surface events
✔ depthToColor returns cool hex for slab-depth events
✔ depthToColor interpolates between anchor stops
✔ featureToRow flattens USGS GeoJSON to {lat, lon, depth, mag, ...}
✔ filterToJp drops points outside the bbox
✔ filterByWindow keeps only rows newer than (now - hours*3600s)
✔ statsFromRows tracks count, max magnitude, average, most-recent time
✔ formatRelativeJa uses 分前 / 時間前 / M/D HH:mm thresholds
… (17 tests total)
ℹ tests 17  ℹ pass 17  ℹ fail 0

USGS API はテストで叩かない。nowMs を注入できる関数設計にしてあるので、合成 Feature と固定時刻だけで全パスが回る。

触る

https://sen.ltd/portfolio/earthquake-jp/ で 24 時間〜30 日 / M2.5〜M5 を切り替えながら、最近の日本周辺地震をマップで見られる。各エピセンターをクリックすると、マグニチュード・深さ・場所・JST 時刻・USGS 詳細ページへのリンクが popup される。

ソース: https://github.com/sen-ltd/earthquake-jp — MIT、合計 ~300 行 (JS) + 17 ユニットテスト、ビルド不要、npm 依存ゼロ (Leaflet は CDN ロード)。


🛠 本記事は SEN 合同会社 が公開している小さな開発者ツール群の 1 つ。他は portfolio 一覧 から。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?