今回は、気象庁ホームページから雷の観測状況を取得し、表形式で表示するページを作成していきます
気象庁の雷観測
気象庁では雷が発生した際に起こる強力な電磁波を検知することで、落雷の位置を特定する「雷監視システム(LIDEN)」を運用しています。このシステムでは、雷によって発生した電磁波を全国30か所の観測点で検知し、その検知時間差を精密に特定することで落雷の位置を判定します
観測結果は、気象庁ホームページの「雨雲の動き」のページで見ることができます。このページでは、雷の活動度の分布を4段階で表した「雷活動度」も見ることができます
雷に関する防災情報としては「雷注意報」がありますが、夏の午後などは地域によってはほぼ毎日雷注意報が発表されているような場合もあります。そのような場合に、雷活動度や発雷地点を確認することで、自分のいる地域の危険度をより細かく、リアルタイムに確認することができます
apiのURL
他の情報と同様、ブラウザの開発者ツールでネットワークタブを開いた状態で気象庁ホームページにアクセスし、Fetch/XHRでフィルタするとデータを格納したファイルが見つかります。ドメイン制限は(現在のところは)かかっていないため、自由に取得できます
- 最新の観測時刻は https://www.jma.go.jp/bosai/jmatile/data/nowc/targetTimes_N3.json
- 各時刻の雷の観測状況は
https://www.jma.go.jp/bosai/jmatile/data/nowc/{basetime}/none/{validtime}/surf/liden/data.geojson?id=liden
に格納されています
これだけでも地図描画や緯度経度の表示はできるのですが、緯度経度では位置が分かりづらいため、
- 国内の各地域とその包含関係 https://www.jma.go.jp/bosai/common/const/area.json
- 各市町村・府県の中心位置 https://www.jma.go.jp/bosai/common/const/xy.json
- 各市町村の東西南北の緯度経度 https://www.jma.go.jp/bosai/common/const/class20relm.json
の各ファイルも活用して「○○市付近」のような形で雷の位置を表示するコードを書いていきます
apiの構造
最新の観測時刻(targetTimes_N3.json)
[
{
"basetime": "20260731090000",
"validtime": "20260731091000",
"elements": [
"thns", "thns_nd", "trns", "trns_nd", "slmcs_fcst", "slmcs_unify", "slmcs_unifyfcst"
]
},
{
"basetime": "20260731090500",
"validtime": "20260731090500",
"elements": [
"liden"
]
},
{
"basetime": "20260731090000",
"validtime": "20260731090000",
"elements": [
"thns", "thns_nd", "trns", "trns_nd", "amds_rain10m", "liden", "slmcs", "slmcs_fcst", "slmcs_unify", "slmcs_unifyfcst"
]
},
{
"basetime": "20260731085500",
"validtime": "20260731085500",
"elements": [
"liden"
]
},......
]
basetimeとvalidtimeごとに、値のある要素(elements)が列挙されています。elementsに「liden」が含まれる時間に観測データがあります。雷観測のデータのような観測系のデータでは、basetimeとvalidtimeは同じになります
予報系のデータでは「○○時のデータをもとに予測した△△時のデータ」のように、時間を2つ持つことになります。「○○時」がbasetime、「△△時」がvalidtimeに相当します
この手順を飛ばして https://www.jma.go.jp/bosai/jmatile/data/nowc/{basetime}/none/{validtime}/surf/liden/data.geojson?id=liden の {basetime} と {validtime} を推測してデータを取得しないでください。まだ気象庁側のサーバーにデータができていなかった場合に、データなしのキャッシュがサーバー上に残ってしまい、気象庁ホームページの正規利用者が最新の観測状況を見るのが遅れてしまう場合があります
各時刻の雷の観測状況(data.geojson?id=liden)
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
139.672,
35.754
]
},
"properties": {
"id": "nowc_20260731090500_20260731090500_liden_0",
"obstimeJST": "2026/07/31 18:04:32.760",
"type": 0
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
139.747,
35.734
]
},
"properties": {
"id": "nowc_20260731090500_20260731090500_liden_1",
"obstimeJST": "2026/07/31 18:04:32.820",
"type": 1
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
145.577,
33.657
]
},
"properties": {
"id": "nowc_20260731090500_20260731090500_liden_2",
"obstimeJST": "2026/07/31 18:00:14.560",
"type": 4
}
},......
]
}
雷の観測結果がgeojson形式で格納されています。propertiesの内容は、obstimeJSTが観測時刻、typeが雷の種別(雲間雷か対地雷か)です
typeの数字については、配信資料に関する技術資料によると、雲間雷では0または1、対地雷では4となるようです
表示用のコード
ここまでの内容を使って、表示用のコードを書いていきます。単に緯度経度を表示するだけだと味気ないので、気象庁ホームページ内の各種位置情報ファイルを利用して「○○市付近」などと表示していきます
各自治体名(area.json)、各自治体の中心の位置(xy.json)、各自治体の東西南北の位置(class20relm.json)を取得します
function getGlobals(){
let datas = {};
let promises = [];
let urls = {
"areas":"https://www.jma.go.jp/bosai/common/const/area.json",
"areaCenters":"https://www.jma.go.jp/bosai/common/const/xy.json",
"class20Limits":"https://www.jma.go.jp/bosai/common/const/class20relm.json"
};
for( let elemName in urls){
promises.push(
new Promise((resolve, reject) => {
fetch(urls[elemName])
.then((response) => response.json())
.then((response) => {
globals[elemName] = response;
resolve(elemName);
});
})
);
}
Promise.all(promises).then((values) => {
getLatestTime();
});
}
LIDENの最新の観測時刻を取得します。観測要素(elements)に「liden」を含むものがあった場合、最新の観測時刻とみなして値をセットします
function getLatestTime(){
fetch("https://www.jma.go.jp/bosai/jmatile/data/nowc/targetTimes_N3.json")
.then((response) => response.json())
.then((targetTimes) => {
for( let targetTime of targetTimes){
if( targetTime['elements'].includes("liden")){
let validtimeUtc = new Date( targetTime['validtime'].substring(0,4), targetTime['validtime'].substring(4,6)-1, targetTime['validtime'].substring(6,8), targetTime['validtime'].substring(8,10), targetTime['validtime'].substring(10,12));
console.log( validtimeUtc);
let validtimeJst = new Date( validtimeUtc.getTime() + 9 * 60 * 60 * 1000);
document.getElementById("targetTime").value = dateFormat( validtimeJst, 'y-m-dTh:n');
get();
break;
}
}
});
}
雷観測データを取得します
function get(){
let targetTime = new Date( document.getElementById("targetTime").value);
let targetTimeUtc = new Date( targetTime.getTime() - 9 * 60 * 60 * 1000);
fetch("https://www.jma.go.jp/bosai/jmatile/data/nowc/" + dateFormat(targetTimeUtc,'ymdhns') + "/none/" + dateFormat(targetTimeUtc,'ymdhns') + "/surf/liden/data.geojson?id=liden")
.then((response) => response.json())
.then((lidens) => {
analyse( lidens);
});
}
観測データを「○○市付近」などに変換します。雷観測データの各緯度経度(lat,lon)と自治体の中心の位置(xy.json。このコード内での変数名はglobals['areaCenters'])との距離を計算します。この際、全ての自治体との距離を計算すると計算量が膨大になるため、自治体の東西南北の緯度経度(class20relm.json。このコード内での変数名はglobals['class20Limits'])と比較して重なっている市町村についてのみ判定を行い、最も近い市町村を判定して「○○市付近」と判定します。どの市町村とも重なっていない場合は、都道府県の中心の位置と比較して、最も近いものを基準に「○○県の△△ 約□□km」と判定します
function analyse( lidens){
// 以下、display( lidens); の直前までのコードは発雷位置を自治体からの距離に変換して表示するためのもので、単に発雷の緯度経度を表示するだけの場合は不要
for( let feature of lidens['features']){
let lat = feature['geometry']['coordinates'][1], lon = feature['geometry']['coordinates'][0];
let nearClass20s = [];
for( let class20Code in globals['class20Limits']){ // 各市町村の東西南北位置と重なりがあるか判定する
let nn = globals['class20Limits'][class20Code]['ne'][0], ee = globals['class20Limits'][class20Code]['ne'][1];
let ss = globals['class20Limits'][class20Code]['sw'][0], ww = globals['class20Limits'][class20Code]['sw'][1];
if( ss<=lat && lat<=nn && ww<lon && lon<=ee){
nearClass20s.push( class20Code);
}
}
if( 0<nearClass20s.length){ // 重なりのある市町村が1つまたは複数あった場合
nearClass20s.sort((a,b)=>{ // 距離でソート
let latA = globals['areaCenters']['class20s'][a][0], lonA = globals['areaCenters']['class20s'][a][1];
let distanceA = distance( lat, lon, latA, lonA);
let latB = globals['areaCenters']['class20s'][b][0], lonB = globals['areaCenters']['class20s'][b][1];
let distanceB = distance( lat, lon, latB, lonB);
return distanceA - distanceB;
})
let nearestClass20 = nearClass20s[0], nearestClass20Name = globals['areas']['class20s'][nearestClass20]['name'];
feature['properties']['position'] = nearestClass20Name + "付近"; // 最も近い市町村名 + 付近 と表示する
}else{ // 重なりのある市町村がない場合
let offices = Object.keys( globals['areas']['offices']);
offices.sort((a,b)=>{ // 各都道府県の中心位置との距離でソート
let latA = globals['areaCenters']['offices'][a][0], lonA = globals['areaCenters']['offices'][a][1];
let distanceA = distance( lat, lon, latA, lonA);
let latB = globals['areaCenters']['offices'][b][0], lonB = globals['areaCenters']['offices'][b][1];
let distanceB = distance( lat, lon, latB, lonB);
return distanceA - distanceB;
})
let nearestOffice = offices[0], officeName = globals['areas']['offices'][nearestOffice]['name'];
let latA = globals['areaCenters']['offices'][nearestOffice][0], lonA = globals['areaCenters']['offices'][nearestOffice][1];
let dist = distance( lat, lon, latA, lonA), dir = degToDir(angle( lat, lon, latA, lonA)); // 都道府県の中心位置からの距離と方角を求める
feature['properties']['position'] = officeName + "の" + dir + " 約" + Math.round(dist/1000) + "km"; // (最も近い都道府県名)の(方角) 約(距離)km付近 と表示する
}
}
display( lidens);
}
距離・方位を求めるのに使用した関数は以下の通りです。 https://zenn.dev/bellbind/scraps/bb2cfb915430f5 を参考に作成しました。おそらく地球を真球とみなして判定した距離・方位のため、厳密な距離・方位とは差が出ることと思いますので、目安として活用してください(気象庁ホームページで遊んでいながらあまり理系の素養がなく、浅い説明しかできなくてすみません)
// 緯度経度から距離を求める関数。返り値はメートル単位。https://zenn.dev/bellbind/scraps/bb2cfb915430f5 を参考に作成
function distance( lat1, lon1, lat2, lon2){
const rad = Math.PI / 180, r = 6371000;
const p1 = lat1 * rad, p2 = lat2 * rad, q1 = lon1 * rad, q2 = lon2 * rad;
return r * Math.acos(Math.cos(p1) * Math.cos(p2) * Math.cos(q1 - q2) + Math.sin(p1) * Math.sin(p2));
}
// 緯度経度から方位を求める関数。返り値は-180°~180°。lat1,lon1からlat2,lon2がどちらの方角にあるかを、北を0°、東を90°として示す。https://zenn.dev/bellbind/scraps/bb2cfb915430f5 を参考に作成
function angle( lat1, lon1, lat2, lon2){
const rad = Math.PI / 180, r = 6371000;
const p1 = lat1 * rad, p2 = lat2 * rad, q1 = lon1 * rad, q2 = lon2 * rad;
const dq = q2 - q1, cosP2 = Math.cos(p2);
const y = cosP2 * Math.sin(dq);
const z = Math.cos(p1) * Math.sin(p2) - Math.sin(p1) * cosP2 * Math.cos(dq);
return Math.atan2(y, z) * 180 / Math.PI;
}
// 方位(度)を8方位に変換する関数
function degToDir( deg){
const dirs = [[22.5, "東"], [67.5, "北東"], [112.5, "北"], [157.5, "北西"], [202.5, "西"], [247.5, "南西"], [292.5, "南"], [337.5, "南東"], [360.1, "東"]];
for( let dir of dirs){
if( (deg+360) % 360 < dir[0]){
return dir[1];
}
}
}
観測結果を表形式に起こしていきます
function display( lidens){
let out = "";
out += "<table>";
out += "<tr><th>緯度</th><th>経度</th><th>大まかな位置</th><th>観測時刻</th><th>種別</th></tr>";
for( let feature of lidens['features']){
out += "<tr>";
out += "<td>" + feature['geometry']['coordinates'][1] + "</td>";
out += "<td>" + feature['geometry']['coordinates'][0] + "</td>";
out += "<td>" + feature['properties']['position'] + "</td>";
out += "<td>" + feature['properties']['obstimeJST'] + "</td>";
let type = feature['properties']['type'];
if( type==0 || type==1){
type = "雲間";
}else if( type==4){
type = "対地";
}
out += "<td>" + type + "</td>";
out += "</tr>";
}
out += "</table>";
document.getElementById("out").innerHTML = out;
}
表示ページ全体のソースコード
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>雷観測</title>
<style>
*{ font-family:sans-serif;}
body{ font-size:16px; width:1024px; max-width:calc(100% - 80px); margin:auto;}
table, tr, td, th{ border-collapse:collapse; text-align:center; border-style:solid; border-width:1px 0; border-color:#d8d8db;}
th,td{ padding:2px 8px;}
th{ background-color:#f1f1f4;}
</style>
</head>
<body>
<div id="menu">
<button id="get">取得</button>
<input type="datetime-local" id="targetTime">
</div>
<div id="out"></div>
<script>
"use strict";
let globals = {};
getGlobals();
// 自治体の位置情報を取得するコード。単に発雷の緯度経度を表示するだけならコメントアウトして直接 getLatestTime(); を呼んでも良い
function getGlobals(){
let datas = {};
let promises = [];
let urls = {
"areas":"https://www.jma.go.jp/bosai/common/const/area.json",
"areaCenters":"https://www.jma.go.jp/bosai/common/const/xy.json",
"class20Limits":"https://www.jma.go.jp/bosai/common/const/class20relm.json"
};
for( let elemName in urls){
promises.push(
new Promise((resolve, reject) => {
fetch(urls[elemName])
.then((response) => response.json())
.then((response) => {
globals[elemName] = response;
resolve(elemName);
});
})
);
}
Promise.all(promises).then((values) => {
getLatestTime();
});
}
function getLatestTime(){
fetch("https://www.jma.go.jp/bosai/jmatile/data/nowc/targetTimes_N3.json")
.then((response) => response.json())
.then((targetTimes) => {
for( let targetTime of targetTimes){
if( targetTime['elements'].includes("liden")){
let validtimeUtc = new Date( targetTime['validtime'].substring(0,4), targetTime['validtime'].substring(4,6)-1, targetTime['validtime'].substring(6,8), targetTime['validtime'].substring(8,10), targetTime['validtime'].substring(10,12));
console.log( validtimeUtc);
let validtimeJst = new Date( validtimeUtc.getTime() + 9 * 60 * 60 * 1000);
document.getElementById("targetTime").value = dateFormat( validtimeJst, 'y-m-dTh:n');
get();
break;
}
}
});
}
document.getElementById("get").addEventListener("click", function(e){
get();
});
function get(){
let targetTime = new Date( document.getElementById("targetTime").value);
let targetTimeUtc = new Date( targetTime.getTime() - 9 * 60 * 60 * 1000);
fetch("https://www.jma.go.jp/bosai/jmatile/data/nowc/" + dateFormat(targetTimeUtc,'ymdhns') + "/none/" + dateFormat(targetTimeUtc,'ymdhns') + "/surf/liden/data.geojson?id=liden")
.then((response) => response.json())
.then((lidens) => {
analyse( lidens);
});
}
function analyse( lidens){
// 以下、display( lidens); の直前までのコードは発雷位置を自治体からの距離に変換して表示するためのもので、単に発雷の緯度経度を表示するだけの場合は不要
for( let feature of lidens['features']){
let lat = feature['geometry']['coordinates'][1], lon = feature['geometry']['coordinates'][0];
let nearClass20s = [];
for( let class20Code in globals['class20Limits']){
let nn = globals['class20Limits'][class20Code]['ne'][0], ee = globals['class20Limits'][class20Code]['ne'][1];
let ss = globals['class20Limits'][class20Code]['sw'][0], ww = globals['class20Limits'][class20Code]['sw'][1];
if( ss<=lat && lat<=nn && ww<lon && lon<=ee){
nearClass20s.push( class20Code);
}
}
if( 0<nearClass20s.length){
nearClass20s.sort((a,b)=>{
let latA = globals['areaCenters']['class20s'][a][0], lonA = globals['areaCenters']['class20s'][a][1];
let distanceA = distance( lat, lon, latA, lonA);
let latB = globals['areaCenters']['class20s'][b][0], lonB = globals['areaCenters']['class20s'][b][1];
let distanceB = distance( lat, lon, latB, lonB);
return distanceA - distanceB;
})
let nearestClass20 = nearClass20s[0], nearestClass20Name = globals['areas']['class20s'][nearestClass20]['name'];
feature['properties']['position'] = nearestClass20Name + "付近";
}else{
let offices = Object.keys( globals['areas']['offices']);
offices.sort((a,b)=>{
let latA = globals['areaCenters']['offices'][a][0], lonA = globals['areaCenters']['offices'][a][1];
let distanceA = distance( lat, lon, latA, lonA);
let latB = globals['areaCenters']['offices'][b][0], lonB = globals['areaCenters']['offices'][b][1];
let distanceB = distance( lat, lon, latB, lonB);
return distanceA - distanceB;
})
let nearestOffice = offices[0], officeName = globals['areas']['offices'][nearestOffice]['name'];
let latA = globals['areaCenters']['offices'][nearestOffice][0], lonA = globals['areaCenters']['offices'][nearestOffice][1];
let dist = distance( lat, lon, latA, lonA), dir = degToDir(angle( lat, lon, latA, lonA));
feature['properties']['position'] = officeName + "の" + dir + " 約" + Math.round(dist/1000) + "km";
}
}
display( lidens);
}
function display( lidens){
let out = "";
out += "<table>";
out += "<tr><th>緯度</th><th>経度</th><th>大まかな位置</th><th>観測時刻</th><th>種別</th></tr>";
for( let feature of lidens['features']){
out += "<tr>";
out += "<td>" + feature['geometry']['coordinates'][1] + "</td>";
out += "<td>" + feature['geometry']['coordinates'][0] + "</td>";
out += "<td>" + feature['properties']['position'] + "</td>";
out += "<td>" + feature['properties']['obstimeJST'] + "</td>";
let type = feature['properties']['type'];
if( type==0 || type==1){
type = "雲間";
}else if( type==4){
type = "対地";
}
out += "<td>" + type + "</td>";
out += "</tr>";
}
out += "</table>";
document.getElementById("out").innerHTML = out;
}
// 緯度経度から距離を求める関数。単に発雷の緯度経度を表示するだけなら不要。返り値はメートル単位。https://zenn.dev/bellbind/scraps/bb2cfb915430f5 を参考に作成
function distance( lat1, lon1, lat2, lon2){
const rad = Math.PI / 180, r = 6371000;
const p1 = lat1 * rad, p2 = lat2 * rad, q1 = lon1 * rad, q2 = lon2 * rad;
return r * Math.acos(Math.cos(p1) * Math.cos(p2) * Math.cos(q1 - q2) + Math.sin(p1) * Math.sin(p2));
}
// 緯度経度から方位を求める関数。単に発雷の緯度経度を表示するだけなら不要。返り値は-180°~180°。lat1,lon1からlat2,lon2がどちらの方角にあるかを、北を0°、東を90°として示す。https://zenn.dev/bellbind/scraps/bb2cfb915430f5 を参考に作成
function angle( lat1, lon1, lat2, lon2){
const rad = Math.PI / 180, r = 6371000;
const p1 = lat1 * rad, p2 = lat2 * rad, q1 = lon1 * rad, q2 = lon2 * rad;
const dq = q2 - q1, cosP2 = Math.cos(p2);
const y = cosP2 * Math.sin(dq);
const z = Math.cos(p1) * Math.sin(p2) - Math.sin(p1) * cosP2 * Math.cos(dq);
return Math.atan2(y, z) * 180 / Math.PI;
}
// 方位(度)を8方位に変換する関数。単に発雷の緯度経度を表示するだけなら不要
function degToDir( deg){
const dirs = [[22.5, "東"], [67.5, "北東"], [112.5, "北"], [157.5, "北西"], [202.5, "西"], [247.5, "南西"], [292.5, "南"], [337.5, "南東"], [360.1, "東"]];
for( let dir of dirs){
if( (deg+360) % 360 < dir[0]){
return dir[1];
}
}
}
function dateFormat( d, f='y-m-dTh:n:s'){ // for DateAPI
const j=["日","月","火","水","木","金","土"];const e=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
f=f.replace(/y/g,("000"+d.getFullYear()).slice(-4)).replace(/Y/g,d.getFullYear()).replace(/m/g,("0"+(d.getMonth()+1)).slice(-2)).replace(/M/g,d.getMonth()+1).replace(/d/g,("0"+d.getDate()).slice(-2)).replace(/D/g,d.getDate());
f=f.replace(/w/g,j[d.getDay()]).replace(/W/g,e[d.getDay()]);
f=f.replace(/h/g,("0"+d.getHours()).slice(-2)).replace(/H/g,d.getHours()).replace(/n/g,("0"+d.getMinutes()).slice(-2)).replace(/N/g,d.getMinutes()).replace(/s/g,("0"+d.getSeconds()).slice(-2)).replace(/S/g,d.getSeconds());
return f;
}
</script>
</body>
</html>