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?

気象庁サイトを利用したビューアの作成 17 台風情報(表)

0
Posted at

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

今回は、台風情報を表形式で表示するページを作成していきます

台風とは

赤道に近い熱帯の海上で発生する低気圧のことを「熱帯低気圧」と呼び、そのうち北西太平洋にあって、最大風速がおよそ17m/s以上のものを「台風」と呼びます

台風の周りには積乱雲と呼ばれる分厚い雲があり、熱帯から持ち込んだ大量の水蒸気を持ち込んだまま日本周辺に北上して、広い範囲に大雨をもたらすことがあります。また、台風の中心付近では強い風が吹いており、台風が接近すると外出は非常に危険になります

台風は夏から秋にかけて日本によく接近し、大雨や暴風をもたらします。大きな被害をもたらすことがあること、また、中心付近ほど強い風や雨をもたらすことから、被害を軽減するために精密な進路の予想が発表されます

海外でも似たような現象があり、北東太平洋や北大西洋では「ハリケーン」、インド洋や南太平洋では「サイクロン」と呼ばれます(南米周辺では類似の事象が少なく定まった名前はない)

apiのURL

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

  • 現在情報を発表している台風の一覧は https://www.jma.go.jp/bosai/typhoon/data/targetTc.json
  • それぞれの台風の実況・予報は https://www.jma.go.jp/bosai/typhoon/data/{tcCode}/specifications.json
    に格納されています。{tcCode}の部分は targetTc.json に格納されています

apiの構造

気象庁ホームページの内部apiの構造は、気象庁防災情報XMLの構造をモデルとしているものが多いです。このため、気象庁防災情報XMLの解説資料が参考になる場合が多いです

台風情報の電文の説明は「台風解析・予報情報電文(新形式)_解説資料.pdf」としてまとめられており、この資料の内容が参考になります

現在情報を発表している台風の一覧(targetTc.json)

targetTc.json
[
  {
    "tropicalCyclone": "TC2611", // TC番号(熱帯低気圧番号)
    "typhoonNumber": "2609", // 台風番号
    "category": "TY", // 台風の階級
    "issue": "2026-07-05T13:05:00+09:00" // 発表時刻
  },...
]

TC番号は、熱帯低気圧の発生順に振られる番号で、日本の気象庁(北西太平洋の台風の監視を担当)は「西暦の下2桁+台風の号数」という形で命名されます

台風番号は、熱帯低気圧が発達して台風となった場合に振られる番号で、毎年1号から順に振られます。熱帯低気圧の場合は「a」「b」等の番号が割り振られます。こちらは通番というわけではないため、たとえばa、bと発生した後にaが消滅したような場合には再びaが割り振られることがあります

熱帯低気圧の中には台風に発達せず消滅するものもあるほか、後から発生した熱帯低気圧が先に台風に発達する場合もあるため、TC番号と台風番号は必ずしも一致しません。年の後ろになるほど、TC番号のほうが大きくなっていく場合が多いと思います

台風の階級は 「TY(Typhoon)」「STS(Severe Tropical Storm)」「TS(Tropical Storm)」等が入ります

specifications.json
[
  {
    "part": "title", // 共通部
    "typhoonNumber": "2609",
    "name": { "jp": "バービー"},......
  },
  {
    "part": { "jp": "実況"}, // 実況部
    "position": {
      "deg": [ 13.1, 148.7],
      "dm": [ [ 13, 5], [ 148, 40]]
    },
    "location": "マリアナ諸島",......
  }, // この後に1時間後の推定が入る場合がある
  {
    "part": { "jp": "予報 24時間後"}, // 予報部(24時間後)
    "position": {
      "deg": [ 14.6, 144.3],
      "dm": [ [ 14, 35], [ 144, 20]]
    },
    "location": "マリアナ諸島",......
  },...
]

個別の台風実況・予報を格納する specifications.json は、予報時刻ごとの情報を格納した配列となっています。{specifications.json}.0 に各時刻共通の情報が、{specifications.json}.1 に実況の台風情報が格納され、台風が日本に接近している場合には{specifications.json}.2 に1時間後の台風情報が格納されます。その後に12時間後、24時間後などの台風の位置や強さの予想が格納されます

{
  "part": "title",
  "issue": { // 発表時刻
    "JST": "2026-07-05T13:05:00+09:00", // 日本標準時
    "UTC": "2026-07-05T04:05:00Z" // 協定世界時(日本標準時 - 9時間)
  },
  "typhoonNumber": "2609", // 台風番号
  "name": { "jp": "バービー"}, // 台風名
  "category": { "jp": "台風", "en": "TY"}
}

共通部はpartが「title」となり、発表時刻や台風番号、台風名が格納されます

実況部・1時間後の推定部
{
  "part": { "jp": "実況"}, // 1時間後の推定の場合は「推定 1時間後」となる
  "maximumWind": {
    "sustained": { // 最大風速
      "m/s": "55", "kt": "105", "note": "中心付近"
    },
    "gust": { // 最大瞬間風速
      "m/s": "75", "kt": "150"
    }
  },
  "galeWarning": [ // 強風域
    {
      "area": "",
      "range": { "km": 500, "nm": 270} // キロメートル単位、海里単位
    },
    {
      "area": "",
      "range": { "km": 390, "nm": 210}
    }
  ],
  "stormWarning": [ // 暴風域
    {
      "area": { "jp": "全域"},
      "range": { "km": 140, "nm": 75}
    }
  ],
  "advancedHours": 0, // 予報時間(後述の validtime から何時間後の実況/推定か)
  "category": { "jp": "台風", "en": "TY"}, // 台風の階級
  "scale": "-", // 台風の大きさ。「-」「大型」「超大型」となる場合がある
  "intensity": "猛烈な", // 台風の強さ。「-」「強い」「非常に強い」「猛烈な」となる場合がある
  "position": {
    "deg": [ 13.1, 148.7], // 緯度経度(小数表記)
    "dm": [ [ 13, 5], [ 148, 40]], // 緯度経度(度分表記)
    "accuracy": "正確" // 位置の正確さ。「正確」「ほぼ正確」「不確実」となる場合がある。出現しない場合がある
  },
  "location": "マリアナ諸島", // 位置
  "course": "西北西", // 進行方向
  "speed": { "km/h": "10", "kt": "6"}, // 速度(km/h、ノット)。速度が遅い場合には"km/h"と"kt"は出現しない場合がある
  "pressure": "920", // 中心気圧
  "validtime": { // 予報対象時刻
    "JST": "2026-07-05T12:00:00+09:00", // 日本標準時
    "UTC": "2026-07-05T03:00:00Z" // 協定世界時(日本標準時 - 9時間)
  }
}

最大風速(maximumWind.sustained)には "note": "中心付近" が付く場合と付かない場合があります。一般に台風が北上して温帯低気圧の性質を帯びてくると、中心から遠く離れた場所で強い風が吹く可能性が高まることから「中心付近の最大風速」とは呼ばずに単に「最大風速」と呼ぶようになるため、その識別用に付加されます

強風域(galeWarning)や暴風域(stormWarning)は、地形の影響がない場合に強風(15m/s以上)や暴風(25m/s以上)が吹くおそれのある地域を表します。南側では何km、北側では何kmのように方向を分けて表現される場合と、全方向に何kmのように方向を分けずに表現される場合があります

position.degは例えば「13.1, 148.7」なら北緯13.1度、東経148.7度を表します。マイナスの場合は南緯/西経を表します。position.dmは例えば「[13,5],[148,40]」なら北緯13度5分、東経148度40分を表します

速度(speed)は5kt(9km/h)以下の場合 speed.note = "ゆっくり"(進行方向が明らかな場合) または speed.note = "ほとんど停滞"(進行方向が定まらない場合)となり、その場合は speed."km/h" と speed.kt は出現しません。「ほとんど停滞」の場合には 進行方向(course) は「不定」となります

予報部
{
  "part": { "jp": "予報 24時間後"},
  "maximumWind": {
    "sustained": { "m/s": "55", "kt": "110", "note": "中心付近"},
    "gust": { "m/s": "80", "kt": "155"}
  },
  "stormWarning": [ // 暴風警戒域
    {
      "area": { "jp": "全域"},
      "range": { "km": 250, "nm": 135}
    }
  ],
  "advancedHours": 24, // 予報時間(実況部の validtime から何時間後の予報か)
  "category": { "jp": "台風", "en": "TY"},
  "intensity": "猛烈な",
  "position": {
    "deg": [ 14.6, 144.3],
    "dm": [ [ 14, 35], [ 144, 20]]
  },
  "probabilityCircleRadius": { "km": 65, "nm": 35}, // 予報円の大きさ
  "location": "マリアナ諸島",
  "course": "西北西",
  "speed": { "km/h": "20", "kt": "11"},
  "pressure": "905",
  "validtime": {
    "JST": "2026-07-06T12:00:00+09:00",
    "UTC": "2026-07-06T03:00:00Z"
  }
}

probabilityCircleRadiusは予報円の大きさを表します。予報円とは、台風の中心が7割の確率で入る範囲のことです。円が大きいほど、台風の予想の位置が定まらない、すなわち台風の進路が予想しづらいことを表します。台風の強さとは関係ありません

stormWarningは予報部では暴風警戒域(風速25m/s以上の暴風の吹くおそれがある地域)を表します

表示用のコード

ここまでの内容を使って、表示用のコードを書いていきます

台風のリストを取得します

function getTargetTcs(){
  fetch("https://www.jma.go.jp/bosai/typhoon/data/targetTc.json")
  .then((response) => response.json())
  .then((targetTcs) => {
    makeTargetTcSelect( targetTcs);
  });
}

台風の選択欄を作成します。typhoonNumberが2桁以上の場合(2610 等)は「台風10号」のように、2桁以下の場合(a 等)は「熱帯低気圧a」のように表記します

function makeTargetTcSelect(targetTcs){
  let targetTcSelect = "";
  for( let targetTc of targetTcs){
    targetTcSelect += "<option value='" + targetTc['tropicalCyclone'] + "'>"
    if( targetTc['typhoonNumber'].length>2){
      targetTcSelect += "台風" + targetTc['typhoonNumber'].slice(-2)*1 + "";
    }else{
      targetTcSelect += "熱帯低気圧" + targetTc['typhoonNumber'];
    }
    targetTcSelect += "</option>";
  }
  document.getElementById("targetTc").innerHTML = targetTcSelect;
  getSpec();
}

個別の台風情報を取得します

function getSpec(){
  let tcNumber = document.getElementById("targetTc").value;
  if( tcNumber==""){
    document.getElementById("out").innerHTML = "発表中の台風情報はありません";
  }else{
    fetch("https://www.jma.go.jp/bosai/typhoon/data/" + tcNumber + "/specifications.json")
    .then((response) => response.json())
    .then((spec) => display(spec));
  }
}

specifications.jsonを解析し、表示していきます。partが「title」の場合は共通部と判断し、台風の号数または熱帯低気圧の名前を表示します

if( report['part']=='title'){
  if( report['name']!=undefined){
    out += "<tr><th colspan = '2'>台風" + report['typhoonNumber'].slice(-2)*1 + "号(" + report['name']['jp'] + "/" + report['name']['en'] + ")</th></tr>";
  }else if( report!=undefined){
    out += "<tr><th colspan = '2'>熱帯低気圧" + report['typhoonNumber'] + "</th></tr>";
  }
}

続いて実況部・推定部・予測部を解析していきます。scaleは予測部では出現しないため、存在判定を行います

let validTime = new Date( report['validtime']['JST']);
let infoType = report['part']['jp'];
out += "<tr><th colspan='2'>" + dateFormat(validTime,'D日h時') + "" + infoType.split(" ")[0] + "</th></tr>";
out += "<tr><td>種別</td><td>" + report['category']['jp'] + "(" + report['category']['en'] + ")</td></tr>";
if( report['scale']!=undefined){
  out += "<tr><td>大きさ</td><td>" + report['scale'] + "</td></tr>";
}
out += "<tr><td>強さ</td><td>" + report['intensity'] + "</td></tr>";
out += "<tr><td>" + report['location'] + "</td>";
out += "<td>";

緯度経度は正負を判別して東西南北(EWSN)の表記を変更します

let latDm = report['position']['dm'][0], lonDm = report['position']['dm'][1];
out += Math.abs(latDm[0]) + "° " + latDm[1] + "'";
if( report['position']['deg'][0]>0){
  out += "N";
}else{
  out += "S";
}
out += "(" + Math.abs(report['position']['deg'][0]) + "°)";
out += "<br>";
out += Math.abs(lonDm[0]) + "°" + lonDm[1] + "'";
if( report['position']['deg'][1]>0){
  out += "E";
}else{
  out += "W";
}
out += "(" + Math.abs(report['position']['deg'][1]) + "°)";
out += "</td></tr>";

速度は"km/h"や"kt"が格納される場合と、"note"(ゆっくり/ほとんど停滞)が格納される場合があるため、どちらにも対応できるようにします

out += "<tr><td>" + report['course'] + "</td>";
out += "<td>";
if( report['speed']['km/h'] != undefined){
  out += report['speed']['km/h'] + "km/h(" + report['speed']['kt'] + "kt)";
}
if( report['speed']['note'] != undefined){
  out += report['speed']['note']['jp'];
}
out += "</td></tr>";
out += "<tr><td>中心気圧</td><td>" + report['pressure'] + "hPa</td></tr>";

最大風速は"note"に「中心付近」が入る場合と入らない場合で場合分け、stormWarningは実況では暴風域、予想では暴風警戒域となるため場合分けを行います

if( report['maximumWind']!=undefined && report['maximumWind']['sustained']['m/s']!=0){ // 台風消滅時は最大風速が0m/sとなるため表記しない
  let maxWind = report['maximumWind']['sustained'], gust = report['maximumWind']['gust'];
  out += "<tr><td>";
  if( maxWind['note']!=undefined){
    out += maxWind['note'] + "";
  }
  out += "最大風速</td>";
  out += "<td>" + maxWind['m/s'] + "m/s(" + maxWind['kt'] + "kt)</td></tr>";
  out += "<tr><td>最大瞬間風速</td>";
  out += "<td>" + gust['m/s'] + "m/s(" + gust['kt'] + "kt)</td></tr>";
}
if( report['stormWarning']!=undefined){ // 暴風域がない場合は要素が出現しない
  if( infoType=="実況"){
    out += "<tr><td>暴風域</td><td>";
  }else{
    out += "<tr><td>暴風警戒域</td><td>";
  }
  for( let stormWarn of report['stormWarning']){
    if( stormWarn['area']['jp'] != undefined){
      out += stormWarn['area']['jp'] + " ";
    }else{
      out += stormWarn['area'] + " ";
    }
    out += stormWarn['range']['km'] + "km(" + stormWarn['range']['nm'] + "海里)<br>";
  }
  out += "</td></tr>";
}
if( report['galeWarning']!=undefined){ // 強風域は予想などでは出現しない
  //......
}
if( report['probabilityCircleRadius']!=undefined){ // 予報円の半径は実況では出現しない
  //......
}

表示ページ全体のソースコード

サンプルページ

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>台風情報(表)</title>
  <style>
    *{ font-family:sans-serif;}
    table, tr, td, th{ border-collapse:collapse; white-space:nowrap; text-align:center;}
    th,td{ padding:2px 8px; border-style:solid; border-width:1px 0; border-color:#d8d8db;}
    th{ background-color:#f1f1f4;}
  </style>
</head>
<body>
  <h1>台風情報</h1>
  <div id="menu">
    <select id="targetTc"></select>
  </div>
  <div id="out"></div>
  <script>
    "use strict";

    getTargetTcs();

    function getTargetTcs(){
      fetch("https://www.jma.go.jp/bosai/typhoon/data/targetTc.json")
      .then((response) => response.json())
      .then((targetTcs) => {
        makeTargetTcSelect( targetTcs);
      });
    }
    
    function makeTargetTcSelect(targetTcs){
      let targetTcSelect = "";
      for( let targetTc of targetTcs){
        targetTcSelect += "<option value='" + targetTc['tropicalCyclone'] + "'>"
        if( targetTc['typhoonNumber'].length>2){
          targetTcSelect += "台風" + targetTc['typhoonNumber'].slice(-2)*1 + "";
        }else{
          targetTcSelect += "熱帯低気圧" + targetTc['typhoonNumber'];
        }
        targetTcSelect += "</option>";
      }
      document.getElementById("targetTc").innerHTML = targetTcSelect;
      getSpec();
    }

    document.getElementById("targetTc").addEventListener("change", function(e){
      getSpec();
    });

    function getSpec(){
      let tcNumber = document.getElementById("targetTc").value;
      if( tcNumber==""){
        document.getElementById("out").innerHTML = "発表中の台風情報はありません";
      }else{
        fetch("https://www.jma.go.jp/bosai/typhoon/data/" + tcNumber + "/specifications.json")
        .then((response) => response.json())
        .then((spec) => display(spec));
      }
    }

    function display( spec){
      let out = "";
      out += "<table>";
      let reportDatetime = new Date( spec[0]['issue']['JST']);
      for( let report of spec){
        if( report['part']=='title'){
          if( report['name']!=undefined){
            out += "<tr><th colspan = '2'>台風" + report['typhoonNumber'].slice(-2)*1 + "号(" + report['name']['jp'] + "/" + report['name']['en'] + ")</th></tr>";
          }else if( report!=undefined){
            out += "<tr><th colspan = '2'>熱帯低気圧" + report['typhoonNumber'] + "</th></tr>";
          }
        }else{
          let validTime = new Date( report['validtime']['JST']);
          let infoType = report['part']['jp'];
          out += "<tr><th colspan='2'>" + dateFormat(validTime,'D日h時') + "" + infoType.split(" ")[0] + "</th></tr>";
          out += "<tr><td>種別</td><td>" + report['category']['jp'] + "(" + report['category']['en'] + ")</td></tr>";
          if( report['scale']!=undefined){
            out += "<tr><td>大きさ</td><td>" + report['scale'] + "</td></tr>";
          }
          out += "<tr><td>強さ</td><td>" + report['intensity'] + "</td></tr>";
          out += "<tr><td>" + report['location'] + "</td>";
          out += "<td>";
          let latDm = report['position']['dm'][0], lonDm = report['position']['dm'][1];
          out += Math.abs(latDm[0]) + "° " + latDm[1] + "'";
          if( report['position']['deg'][0]>0){
            out += "N";
          }else{
            out += "S";
          }
          out += "(" + Math.abs(report['position']['deg'][0]) + "°)";
          out += "<br>";
          out += Math.abs(lonDm[0]) + "°" + lonDm[1] + "'";
          if( report['position']['deg'][1]>0){
            out += "E";
          }else{
            out += "W";
          }
          out += "(" + Math.abs(report['position']['deg'][1]) + "°)";
          out += "</td></tr>";
          out += "<tr><td>" + report['course'] + "</td>";
          out += "<td>";
          if( report['speed']['km/h'] != undefined){
            out += report['speed']['km/h'] + "km/h(" + report['speed']['kt'] + "kt)";
          }
          if( report['speed']['note'] != undefined){
            out += report['speed']['note']['jp'];
          }
          out += "</td></tr>";
          out += "<tr><td>中心気圧</td><td>" + report['pressure'] + "hPa</td></tr>";
          if( report['maximumWind']!=undefined && report['maximumWind']['sustained']['m/s']!=0){
            let maxWind = report['maximumWind']['sustained'], gust = report['maximumWind']['gust'];
            out += "<tr><td>";
            if( maxWind['note']!=undefined){
              out += maxWind['note'] + "";
            }
            out += "最大風速</td>";
            out += "<td>" + maxWind['m/s'] + "m/s(" + maxWind['kt'] + "kt)</td></tr>";
            out += "<tr><td>最大瞬間風速</td>";
            out += "<td>" + gust['m/s'] + "m/s(" + gust['kt'] + "kt)</td></tr>";
          }
          if( report['stormWarning']!=undefined){
            if( infoType=="実況"){
              out += "<tr><td>暴風域</td><td>";
            }else{
              out += "<tr><td>暴風警戒域</td><td>";
            }
            for( let stormWarn of report['stormWarning']){
              if( stormWarn['area']['jp'] != undefined){
                out += stormWarn['area']['jp'] + " ";
              }else{
                out += stormWarn['area'] + " ";
              }
              out += stormWarn['range']['km'] + "km(" + stormWarn['range']['nm'] + "海里)<br>";
            }
            out += "</td></tr>";
          }
          if( report['galeWarning']!=undefined){
            out += "<tr><td>強風域</td><td>";
            for( let galeWarn of report['galeWarning']){
              if( galeWarn['area']['jp'] != undefined){
                out += galeWarn['area']['jp'] + " ";
              }else{
                out += galeWarn['area'] + " ";
              }
              out += galeWarn['range']['km'] + "km(" + galeWarn['range']['nm'] + "海里)<br>";
            }
            out += "</td></tr>";
          }
          if( report['probabilityCircleRadius']!=undefined){
            out += "<tr><td>予報円の半径</td><td>";
            out += report['probabilityCircleRadius']['km'] + "km(" + report['probabilityCircleRadius']['nm'] + "海里)";
            out += "</td></tr>";
          }
        }
      }
      out += "</table>";
      document.getElementById("out").innerHTML = out;
    }

    function dateFormat( d, f='y-m-dTh:n:s', is24=false){ // for DateAPI
      const j=["","","","","","",""];const e=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
      if(is24&&d.getHours()==0){d.setDate(d.getDate()-1);f=f.replace(/h/g,"24").replace(/H/g,24);}
      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>
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?