0
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?

気象庁サイトを利用したビューアの作成 20 防災速報・解説情報(地図)

0
Last updated at Posted at 2026-07-25

目次・全体的な注意点(第1回の記事)

今回は、文章の気象情報の発表状況を地図表示するページを作成していきます

気象防災速報・気象解説情報そのものの説明については、前回の記事をご覧ください

apiのURL

他の情報と同様、ブラウザの開発者ツールでネットワークタブを開いた状態で気象庁ホームページにアクセスし、Fetch/XHRでフィルタするとデータを格納したファイルが見つかります

また、地図表示の際に用いるデータとして、

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次細分区域ごとのポリゴンについて、情報が発表されているかを判定して、発表されているものについて着色します(判定部は後述)

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']);
// 詳細なポリゴンデータについても同様の処理を行う(記事末尾のサンプルコード参照)

情報の発表状況判定用の関数です。与えられた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>
0
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
0
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?