今回は、文章の気象情報の発表状況を地図表示するページを作成していきます
気象防災速報・気象解説情報そのものの説明については、前回の記事をご覧ください
apiのURL
他の情報と同様、ブラウザの開発者ツールでネットワークタブを開いた状態で気象庁ホームページにアクセスし、Fetch/XHRでフィルタするとデータを格納したファイルが見つかります
- 全国に発表されている気象情報の一覧は https://www.jma.go.jp/bosai/information/data/r8/information.json
- 個別の情報は
https://www.jma.go.jp/bosai/information/data/r8/denbun/{jsonName}.json
に格納されています。{jsonName}の部分は information.json に格納されています
また、地図表示の際に用いるデータとして、
- 地域名や地域の包含関係が https://www.jma.go.jp/bosai/common/const/area.json
- 1次細分区域(○○県北部、南部 等)のポリゴンデータが https://www.jma.go.jp/bosai/common/const/geojson/class10s.json
- 1次細分区域のより細かいポリゴンデータが
https://www.jma.go.jp/bosai/common/const/geojson/class10s_{i}.json
に格納されています。{i}は地域ごとに0~9で、その境界は - https://www.jma.go.jp/bosai/common/const/relm.json に格納されています
apiの構造
気象庁ホームページの内部apiの構造は、気象庁防災情報XMLの構造をモデルとしているものが多いです。このため、気象庁防災情報XMLの解説資料が参考になる場合が多いです
気象情報の電文の説明は
- 気象防災速報(竜巻注意、竜巻目撃)は「竜巻注意情報_解説資料.pdf」
- 気象解説情報と、竜巻関連以外の気象防災速報は「気象解説情報形式_解説資料.pdf」
- 天候情報は「天候情報_解説資料.pdf」
としてまとめられており、これらの資料の内容が参考になります
気象情報の一覧(information.json)
[
{
"controlTitle": "府県気象解説情報", // 情報名称。「府県気象防災速報」「全般気象解説情報」等
"editorialOffice": "室蘭地方気象台", // 発信官署
"publishingOffice": "室蘭地方気象台", // 編集官署
"headTitle": "胆振・日高地方気象解説情報(大雨)", // 標題
"reportDatetime": "2026-07-18T16:10:00+09:00", // 発表時刻
"eventId": "FJPSE260004",
"infoType": "発表", // 情報形態。「発表」「訂正」「取消」のいずれか
"serial": "3", // 号数
"areaType": "offices", // 地域種別。「japan」「centers」「offices」のいずれか
"areaCode": "015000", // 地域コード
"areaCodes": [ "015000"], // 地域コード(複数の場合がある)
"jsonName": "20260718071047_0_VPFJ51_015000", // 個別の気象情報のファイル名
"validMap": "2026-07-19T09:10:00+00:00", // 気象庁サイトの地図上の表示期間
"dataType": "xml",
"infoTag": [
{ "name": "情報タグ", "condition": "大雨"} // 情報タグ。「台風 大雨 雷」等複数になる場合もある
],
"areaTag": [
{ "name": "胆振地方", "code": "015010"},
{ "name": "日高地方", "code": "015020"}
]
},
{
"controlTitle": "府県気象解説情報",
"editorialOffice": "旭川地方気象台",
"publishingOffice": "旭川地方気象台",
"headTitle": "上川・留萌地方気象解説情報(大雨)",
"reportDatetime": "2026-07-18T16:15:00+09:00",
"validDatetime": null,
"eventId": "FJPSC260005",
"infoType": "発表",
"serial": "2",
"areaType": "offices",
"areaCode": "012000",
"areaCodes": [ "012000"],
"jsonName": "20260718071538_0_VPFJ51_012000",
"validMap": "2026-07-19T09:15:00+00:00",
"dataType": "xml",
"infoTag": [
{ "name": "情報タグ", "condition": "大雨"}
],
"areaTag": [
{ "name": "上川地方", "code": "012010"},
{ "name": "留萌地方", "code": "012020"}
]
},......
]
editorialOfficeは電文処理の条件分岐に用いる電文作成官署、publishingOfficeは業務的に電文の作成に責任を持っている官署を表します。内部の処理はeditorialOfficeによって行い、表示する発表官署はpublishingOfficeとしてください
validMapとして、気象庁ホームページの地図上の情報表示期間が設定されています。これを利用することで、気象防災速報(竜巻注意)など賞味期限の短い情報は短く、その他の情報は長く表示するようなことができます
その他の説明は、前回の記事をご覧ください
表示用のコード
ここまでの内容を使って、表示用のコードを書いていきます
アメダスのデータを地図表示した記事の時などと同様に、Leaflet.jsを用いて地図表示していきます
headタグ内でLeafletのスタイルシートとスクリプトを読み込みます(Leafletのチュートリアルからそのまま拝借)
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""></script>
ページのレイアウトを定義します。html、bodyを画面幅いっぱいに表示する設定とし、flexでメニュー部分(div#menu)と地図部分(div#map)に分割します
<style>
*{ font-family:sans-serif;}
html, body{ width:100%; height:100%; margin:0; overflow:hidden;}
body{ display:flex; flex-direction:column;}
div#menu{ flex:0.01;}
div#map{ flex:0.99; background:#3b4580;}
</style>
ポリゴンデータ(広域・詳細)と情報の格納用のオブジェクトを作成します
let globals = {"class10DetailedPolygons":{}}, layers = { 'class10s':L.layerGroup(), 'class10sDetailed':{}}, infos = {};
for( let i=0; i<=9; i++){
layers['class10sDetailed'][i] = L.layerGroup();
}
地図を定義し、情報発表状況の塗りつぶし・境界線用のレイヤーを定義します
let map = L.map('map').setView([35, 135], 5);
// <- 表側 pane2:境界線 pane1:情報発表状況(塗り) 裏側->
for( let i=0; i<3; i++){
map.createPane("pane"+i).style.zIndex = i*10;
}
地域情報・広域のポリゴンデータと、詳細のポリゴンデータの境界データを取得します
function getGlobals(){
let promises = [];
let urls = {
"areas":"https://www.jma.go.jp/bosai/common/const/area.json", // 地域名・包含関係のデータ
"areaLimits":"https://www.jma.go.jp/bosai/common/const/relm.json", // 詳細なポリゴンデータの境界緯経度のデータ
"class10Polygons":"https://www.jma.go.jp/bosai/common/const/geojson/class10s.json", // 1次細分区域ごとの広域のポリゴンデータ
}
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.allSettled(promises).then((values) => {
initialize();
});
}
地図レイヤーを定義します。情報発表状況の地図の上に重ねる、行政界のみ(背景透過)のレイヤーを使います
function initialize(){
L.control.layers({
"気象庁地図":L.tileLayer('https://www.jma.go.jp/tile/jma/base/{z}/{x}/{y}.png', { minZoom:4, maxZoom:12, maxNativeZoom:10, attribution: '<a href="https://www.jma.go.jp/jma/kishou/info/coment.html">気象庁</a>', pane:"pane2"}).addTo(map),
}).addTo(map);
get();
}
情報リストを取得します
function get(){
let promises = [], datas = {};
let urls = {
"infos":"https://www.jma.go.jp/bosai/information/data/r8/information.json"
}
for( let elemName in urls){
promises.push(
new Promise((resolve, reject) => {
fetch(urls[elemName])
.then((response) => response.json())
.then((response) => {
datas[elemName] = response;
resolve(elemName);
});
})
);
}
Promise.allSettled(promises).then((values) => {
jointInfos( datas);
});
}
各情報を infos[地域種別][地域コード] のオブジェクトに格納して整理します
function jointInfos( datas){
infos = {};
for( let info of datas['infos']){
let areaType = info['areaType'], areaCodes = info['areaCodes'];
if( infos[areaType]==undefined){
infos[areaType] = [];
}
for( let areaCode of areaCodes){
if( infos[areaType][areaCode]==undefined){
infos[areaType][areaCode] = [];
}
infos[areaType][areaCode].push( info);
}
}
console.log( infos);
removeLayers();
}
地図を描画します。1次細分区域ごとのポリゴンについて、情報が発表されているかを判定して、発表されているものについて着色します(判定部は後述)
L.geoJSON( globals['class10Polygons'], {
style: function(feature) {
let infoColor = "#c8c8cb", weight = 1, class10Code = feature['properties']['code'];
infoColor = judgeColor( class10Code);
if( feature['properties']['islandBold']){
weight = 8;
}
return { fillColor:infoColor, fillOpacity:1, weight:weight, color:infoColor, pane:"pane1"};
},
onEachFeature: function( feature, layer){
layer.on( 'click', function(e){
featureJump( feature['properties']['code']); // クリックされた場合に表に飛ぶためのイベント
});
}
}).addTo(layers['class10s']);
// 詳細なポリゴンデータについても同様の処理を行う(記事末尾のサンプルコード参照)
情報の発表状況判定用の関数です。与えられた1次細分区域コードから府県コードを判定し、validMap(地図表示期間)を過ぎていないこと、情報種別でallまたは該当の情報が選ばれている場合に黄色#f2e700を返し、そうでない場合に灰色#c8c8cbを返します
function judgeColor( class10Code){
const targetKind = document.getElementById('targetKind').value;
let infoColor = "#c8c8cb";
if( globals['areas']['class10s'][class10Code]!=undefined){
let officeCode = globals['areas']['class10s'][class10Code]['parent'];
if( infos['offices'][officeCode]!=undefined){
for( let report of infos['offices'][officeCode]){
let now = new Date(), validMap = new Date( report['validMap']);
if( validMap.getTime() < now.getTime()){
continue;
}
if( report['controlTitle']==undefined){
if( targetKind=="info" || targetKind == "all"){
infoColor = "#f2e700";
}
}else if( report['controlTitle'].includes("解説情報") && (targetKind=="info" || targetKind == "all")){
infoColor = "#f2e700";
// 気象防災速報(竜巻注意/竜巻目撃)は「竜巻注意情報」のcontrolTitleで配信されるため、竜巻も条件に付加
}else if( (report['controlTitle'].includes("防災速報") || report['controlTitle'].includes("竜巻")) && (targetKind=="headline" || targetKind == "all")){
infoColor = "#f2e700";
}else if( report['controlTitle'].includes("熱中症警戒アラート") && (targetKind=="heat" || targetKind == "all")){
infoColor = "#f2e700";
}else if( report['controlTitle'].includes("天候情報") && (targetKind=="climate" || targetKind == "all")){
infoColor = "#f2e700";
}
}
}
}
return infoColor;
}
地図がクリックされた場合にはfeatureJump関数を呼び(drawInfos()関数内参照)、情報の表表示ページ(前回作成)に飛びます
function featureJump( class10Code){
if( globals['areas']['class10s'][class10Code]!=undefined){
let officeCode = globals['areas']['class10s'][class10Code]['parent'];
let centerCode = globals['areas']['offices'][officeCode]['parent'];
let officeCodes = globals['areas']['centers'][centerCode]['children'].sort((a,b)=>{return a-b});
window.location.href = "./infoList.html?area=010000," + centerCode + "," + officeCodes.join("|");
}
}
地図が移動された際には、現在の緯度経度に応じて詳細なポリゴンデータの取得が必要か判定し、必要なら取得します
map.on('moveend', function () {
getClass10sDetailed();
});
function getClass10sDetailed(){
let promises = [];
for( let i=0; i<=9; i++){
promises.push(
new Promise((resolve, reject)=>{
let ne = L.latLng(globals['areaLimits'][i]['ne']), sw = L.latLng(globals['areaLimits'][i]['sw']);
let polygonBounds = L.latLngBounds(ne,sw);
if( 8<=map.getZoom() && polygonBounds.intersects(map.getBounds())){
if( globals['class10DetailedPolygons'][i]==undefined){
fetch("https://www.jma.go.jp/bosai/common/const/geojson/class10s_" + i + ".json")
.then((response) => response.json())
.then((response) => {
globals['class10DetailedPolygons'][i] = response;
resolve(i);
})
}else{
resolve(i);
}
}else{
resolve(i);
};
})
);
}
Promise.allSettled(promises).then((values)=>{
drawInfos();
});
}
地図が拡大・縮小された際には、縮小時は広域の地図を、拡大時は詳細の地図を表示します
function controlLayers(){
if( 8<=map.getZoom()){
if( map.hasLayer(layers['class10s'])){
map.removeLayer( layers['class10s']);
}
for( let i in globals['class10DetailedPolygons']){
if( !map.hasLayer(layers['class10sDetailed'][i])){
map.addLayer( layers['class10sDetailed'][i]);
}
}
}else{
if( !map.hasLayer(layers['class10s'])){
map.addLayer( layers['class10s']);
}
for( let i in globals['class10DetailedPolygons']){
if( map.hasLayer(layers['class10sDetailed'][i])){
map.removeLayer( layers['class10sDetailed'][i]);
}
}
}
}
表示ページ全体のソースコード
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>防災速報・解説情報(地図)</title>
<!-- https://leafletjs.com/ -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""></script>
<style>
*{ font-family:sans-serif;}
html, body{ width:100%; height:100%; margin:0; overflow:hidden;}
body{ display:flex; flex-direction:column;}
div#menu{ flex:0.01;}
div#map{ flex:0.99; background:#3b4580;}
</style>
</head>
<body>
<div id="menu">
<select id="targetKind">
<option value="all">全て</option>
<option value="headline">気象防災速報</option>
<option value="info">解説情報</option>
<option value="heat">熱中症警戒アラート</option>
<option value="climate">天候情報</option>
</select>
</div>
<div id="map"></div>
<script>
"use strict";
let globals = {"class10DetailedPolygons":{}}, layers = { 'class10s':L.layerGroup(), 'class10sDetailed':{}}, infos = {};
for( let i=0; i<=9; i++){
layers['class10sDetailed'][i] = L.layerGroup();
}
let map = L.map('map').setView([35, 135], 5);
// <- 表側 pane2:境界線 pane1:情報発表状況(塗り) 裏側->
for( let i=0; i<3; i++){
map.createPane("pane"+i).style.zIndex = i*10;
}
getGlobals();
function getGlobals(){
let promises = [];
let urls = {
"areas":"https://www.jma.go.jp/bosai/common/const/area.json",
"areaLimits":"https://www.jma.go.jp/bosai/common/const/relm.json",
"class10Polygons":"https://www.jma.go.jp/bosai/common/const/geojson/class10s.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.allSettled(promises).then((values) => {
initialize();
});
}
function initialize(){
L.control.layers({
"気象庁地図":L.tileLayer('https://www.jma.go.jp/tile/jma/base/{z}/{x}/{y}.png', { minZoom:4, maxZoom:12, maxNativeZoom:10, attribution: '<a href="https://www.jma.go.jp/jma/kishou/info/coment.html">気象庁</a>', pane:"pane2"}).addTo(map),
}).addTo(map);
get();
}
function get(){
let promises = [], datas = {};
let urls = {
"infos":"https://www.jma.go.jp/bosai/information/data/r8/information.json"
}
for( let elemName in urls){
promises.push(
new Promise((resolve, reject) => {
fetch(urls[elemName])
.then((response) => response.json())
.then((response) => {
datas[elemName] = response;
resolve(elemName);
});
})
);
}
Promise.allSettled(promises).then((values) => {
jointInfos( datas);
});
}
function jointInfos( datas){
infos = {};
for( let info of datas['infos']){
let areaType = info['areaType'], areaCodes = info['areaCodes'];
if( infos[areaType]==undefined){
infos[areaType] = [];
}
for( let areaCode of areaCodes){
if( infos[areaType][areaCode]==undefined){
infos[areaType][areaCode] = [];
}
infos[areaType][areaCode].push( info);
}
}
console.log( infos);
removeLayers();
}
document.getElementById("targetKind").addEventListener("change", function(e){
removeLayers();
});
function removeLayers(){
if( map.hasLayer(layers['class10s'])){
map.removeLayer(layers['class10s']);
}
for( let i=0; i<=9; i++){
if( map.hasLayer(layers['class10sDetailed'][i])){
map.removeLayer(layers['class10sDetailed'][i]);
}
}
layers['class10s'] = L.layerGroup();
for( let i=0; i<=9; i++){
layers['class10sDetailed'][i] = L.layerGroup();
}
getClass10sDetailed();
}
map.on('moveend', function () {
getClass10sDetailed();
});
function getClass10sDetailed(){
let promises = [];
for( let i=0; i<=9; i++){
promises.push(
new Promise((resolve, reject)=>{
let ne = L.latLng(globals['areaLimits'][i]['ne']), sw = L.latLng(globals['areaLimits'][i]['sw']);
let polygonBounds = L.latLngBounds(ne,sw);
if( 8<=map.getZoom() && polygonBounds.intersects(map.getBounds())){
if( globals['class10DetailedPolygons'][i]==undefined){
fetch("https://www.jma.go.jp/bosai/common/const/geojson/class10s_" + i + ".json")
.then((response) => response.json())
.then((response) => {
globals['class10DetailedPolygons'][i] = response;
resolve(i);
})
}else{
resolve(i);
}
}else{
resolve(i);
};
})
);
}
Promise.allSettled(promises).then((values)=>{
drawInfos();
});
}
function drawInfos(){
L.geoJSON( globals['class10Polygons'], {
style: function(feature) {
let infoColor = "#c8c8cb", weight = 1, class10Code = feature['properties']['code'];
infoColor = judgeColor( class10Code);
if( feature['properties']['islandBold']){
weight = 8;
}
return { fillColor:infoColor, fillOpacity:1, weight:weight, color:infoColor, pane:"pane1"};
},
onEachFeature: function( feature, layer){
layer.on( 'click', function(e){
featureJump( feature['properties']['code']);
});
}
}).addTo(layers['class10s']);
for( let i in globals['class10DetailedPolygons']){
L.geoJSON( globals['class10DetailedPolygons'][i], {
style: function(feature) {
let infoColor = "#c8c8cb", weight = 1, class10Code = feature['properties']['code'];
infoColor = judgeColor( class10Code);
if( feature['properties']['islandBold']){
weight = 8;
}
return { fillColor:infoColor, fillOpacity:1, weight:weight, color:infoColor, pane:"pane1"};
},
onEachFeature: function( feature, layer){
layer.on( 'click', function(e){
featureJump( feature['properties']['code']);
});
}
}).addTo(layers['class10sDetailed'][i]);
}
controlLayers();
}
function featureJump( class10Code){
if( globals['areas']['class10s'][class10Code]!=undefined){
let officeCode = globals['areas']['class10s'][class10Code]['parent'];
let centerCode = globals['areas']['offices'][officeCode]['parent'];
let officeCodes = globals['areas']['centers'][centerCode]['children'].sort((a,b)=>{return a-b});
window.location.href = "./infoList.html?area=010000," + centerCode + "," + officeCodes.join("|");
}
}
function judgeColor( class10Code){
const targetKind = document.getElementById('targetKind').value;
let infoColor = "#c8c8cb";
if( globals['areas']['class10s'][class10Code]!=undefined){
let officeCode = globals['areas']['class10s'][class10Code]['parent'];
if( infos['offices'][officeCode]!=undefined){
for( let report of infos['offices'][officeCode]){
let now = new Date(), validMap = new Date( report['validMap']);
if( validMap.getTime() < now.getTime()){
continue;
}
if( report['controlTitle']==undefined){
if( targetKind=="info" || targetKind == "all"){
infoColor = "#f2e700";
}
}else if( report['controlTitle'].includes("解説情報") && (targetKind=="info" || targetKind == "all")){
infoColor = "#f2e700";
}else if( (report['controlTitle'].includes("防災速報") || report['controlTitle'].includes("竜巻")) && (targetKind=="headline" || targetKind == "all")){
infoColor = "#f2e700";
}else if( report['controlTitle'].includes("熱中症警戒アラート") && (targetKind=="heat" || targetKind == "all")){
infoColor = "#f2e700";
}else if( report['controlTitle'].includes("天候情報") && (targetKind=="climate" || targetKind == "all")){
infoColor = "#f2e700";
}
}
}
}
return infoColor;
}
function controlLayers(){
if( 8<=map.getZoom()){
if( map.hasLayer(layers['class10s'])){
map.removeLayer( layers['class10s']);
}
for( let i in globals['class10DetailedPolygons']){
if( !map.hasLayer(layers['class10sDetailed'][i])){
map.addLayer( layers['class10sDetailed'][i]);
}
}
}else{
if( !map.hasLayer(layers['class10s'])){
map.addLayer( layers['class10s']);
}
for( let i in globals['class10DetailedPolygons']){
if( map.hasLayer(layers['class10sDetailed'][i])){
map.removeLayer( layers['class10sDetailed'][i]);
}
}
}
}
</script>
</body>
</html>