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?

気象庁サイトを利用したビューアの作成 11 津波警報等(表)

0
Posted at

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

今回は、津波警報・津波注意報等の表示ページを作成してみます

津波に関する情報は発表頻度が低いものも多く、レアケースでどのような表現となるか確認できていない部分もありますので、ご了承ください。確実な情報提供を希望される方は、気象庁防災情報XMLのPULL型提供ページ(無償)や、気象業務支援センター(有償)から入手することが可能です

津波警報とは

海中や海に近い陸地で地震が発生すると、海水が持ち上げられて、津波が発生することがあります

津波は普通の波とは異なりすぐに引くことはなく、数分から数十分にわたって洪水のように押し寄せ続け、人や物を押し流します。土砂災害や火山噴火の火砕流と並んで立ち退き避難が必要な重大災害のため、気象庁や各国の気象機関1は地震の揺れを観測することで地震の震源と規模をいち早く推定し、津波が押し寄せる前に警報を発表するよう努めています

津波は気象現象や普通の波とは異なる特性があるため、情報を表現するに当たっては誤解を生じないよう注意が必要です

津波注意報・津波警報・大津波警報で、警戒が必要な範囲が大きく異なります
気象の注意報・警報はどちらも似たような範囲で警戒が必要ですが、津波注意報では海中でしか被害が起きないのに対し、津波警報・大津波警報では陸地にも被害が及びます。この特性を考慮して警戒を呼びかけないと、いわゆるオオカミ少年に陥る可能性があるため注意が必要です
第1波より第2波第3波が大きくなる場合が多くあります
2011年の東北地方太平洋沖地震(東日本大震災)では、地震から2時間以上経って最大の津波が観測されました。津波の観測値を伝える際は、後からこれより高い波がくるおそれがあるという危機感が損なわれないよう注意が必要です(例えば「最大波」「すでに津波到達」などの表現は、これで津波が全て終わったかのような誤解を与えないよう慎重に使う必要があります)
沖合より陸地で高くなります
津波は水深が浅くなるほど高くなるため、沖合の波浪計などで観測された津波の値を伝える場合には、これより大きい津波が襲って来ることを意識して伝える必要があります。(電文内ではこのまま陸上に到達した場合の推定値があるため、活用を検討してください)
超巨大地震では、高さではなく「巨大」「高い」などの表現になります
津波の警報電文内の予想高さは、地震の規模を数分で正しく決定できないような超巨大地震の場合には数値ではなく「巨大」「高い」などの定性表現となりますので、対応できるようシステム開発には注意が必要です

この他にも注意点がありますので、津波警報・注意報について(気象庁)津波について(気象庁)の内容をご確認ください

apiのURL

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

最近発生した現象のリストは、 https://www.jma.go.jp/bosai/tsunami/data/list.json に格納されています

個別の津波の情報は、 https://www.jma.go.jp/bosai/tsunami/data/{jsonName}.json に格納されています。{jsonName}の部分は個々の情報によって異なり、list.json内に情報ごとのファイル名が格納されています

最近情報が発表されていない場合には、ファイルが空となることもあります。 https://mm394587.net/jmaViewer/sampleJson/index.html に過去の発表時のファイルを保存していますので、よろしければご参考にしてください

apiの構造

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

地震・津波・火山関連の電文の説明は「地震火山関連_解説資料.pdf」としてまとめられており、この資料の内容が参考になります

津波リスト(list.json)

list.json
[
  {
    "ctt": "20260420235230",
    "eid": "20260420165303",
    "rdt": "2026-04-20T23:52:00+09:00",
    "ttl": "津波観測に関する情報",
    "ift": "発表",
    "ser": "31",
    "at": "2026-04-20T16:53:00+09:00",
    "anm": "三陸沖",
    "acd": "288",
    "cod": "+39.8+143.2-20000/",
    "mag": "7.7",
    "kind": [],
    "json": "20260420235230_20260420165303_VTSE51_31.json"
  },
  {
    "ctt": "20260420234546",
    "eid": "20260420165303",
    "rdt": "2026-04-20T23:45:00+09:00",
    "ttl": "津波予報",
    "ift": "発表",
    "ser": 0,
    "at": "2026-04-20T16:53:00+09:00",
    "anm": "三陸沖",
    "acd": "288",
    "cod": "+39.8+143.2-20000/",
    "mag": "7.7",
    "kind": [
      {
        "code": "100",
        "kind": "津波予報(若干の海面変動)"
      },...
    ],
    "json": "20260420234546_20260420165303_VTSE41_0.json"
  },......
]

このapiは略称を多用しており少々読み取りづらいですが、解説資料の内容から以下のように推測できます

要素名 説明
{i}.ctt 発信時刻(ConTrol/daTetime)
{i}.eid EventID
{i}.rdt 発表時刻(ReportDateTime)
{i}.ttl 標題(head/Title)
{i}.ift 情報形態(InfoType)
{i}.ser 情報番号(Serial)
{i}.at 地震検知時刻(ArrivalTime)
{i}.anm 震央地名(Area/Name)
{i}.acd 震央地名コード(Area/Code)
{i}.cod 震源座標(Coordinate)
{i}.mag マグニチュード(Magnitude)
{i}.kind 各地の津波警報等
{i}.kind.{j}.code 津波予報区コード
{i}.kind.{j}.kind 警報コード
{i}.json 個別情報のjsonファイル名

地震情報の記事で説明したように、eventIDは必ずしも一連の現象で一致するとは限らないため注意が必要です。また、codは深さが省略されたり、空となることもあるため注意が必要です

津波予報区コードと予報区名の対応表は、気象庁防災情報XMLの個別コード表内、地震火山関連コード表に掲載されています。また、個別の津波情報内にはコードと地域名が併記されているため、実用上はそちらを用いて表示すれば問題はないかと思います

警報コードと警報種別の対応も、同じコード表内に掲載されています

コード 説明 備考
00 津波なし
50 警報解除 大津波警報または津波警報の解除
51 津波警報
52 大津波警報
53 大津波警報:発表 大津波警報の新規発表または切替
60 津波注意報解除
62 津波注意報
71 津波予報(若干の海面変動)
72 津波予報(若干の海面変動) 津波注意報解除、津波予報への切替
73 津波予報(若干の海面変動) 大津波警報または津波警報の解除、津波予報への切替

多分こういうことかと思います

→津波なし →津波注意報 →津波警報 →大津波警報
津波なし→ 00 62 51 53
津波注意報→ 60 62 51 53
津波警報→ 50 62 51 53
大津波警報→ 50 62 51 52

個別の津波情報({jsonName}.json)

例1 津波警報

{
  "Control": {
    "Title": "津波警報・注意報・予報a"
  },
  "Head": {
    "Title": "津波警報・津波注意報・津波予報",
    "ReportDateTime": "2026-04-20T17:08:00+09:00",
    "Headline": {
      "Text": "津波警報を切り替えました。\nただちに避難してください。"
    }
  },
  "Body": {
    "Tsunami": {
      "Forecast": {
        "Item": [
          {
            "Area": {
              "Name": "北海道太平洋沿岸中部",
              "Code": "101"
            },
            "Category": {
              "Kind": {
                "Name": "津波警報",
                "Code": "51"
              },
              "LastKind": {
                "Name": "津波警報",
                "Code": "51"
              }
            },
            "FirstHeight": {
              "ArrivalTime": "2026-04-20T17:30:00+09:00"
            },
            "MaxHeight": {
              "TsunamiHeight": "3"
            }
          },
          {
            "Area": {
              "Name": "青森県太平洋沿岸",
              "Code": "201"
            },
            "Category": {
              "Kind": {
                "Name": "津波警報",
                "Code": "51"
              },
              "LastKind": {
                "Name": "津波注意報",
                "Code": "62"
              }
            },
            "FirstHeight": {
              "ArrivalTime": "2026-04-20T17:20:00+09:00"
            },
            "MaxHeight": {
              "TsunamiHeight": "3",
              "Revise": "更新"
            }
          },......
        ]
      }
    },
    "Earthquake": [
      {
        "OriginTime": "2026-04-20T16:52:00+09:00",
        "ArrivalTime": "2026-04-20T16:53:00+09:00",
        "Hypocenter": {
          "Area": {
            "Name": "三陸沖",
            "Code": "288",
            "Coordinate": "+39.8+143.2-10000/"
          }
        },
        "Magnitude": "7.5"
      }
    ],
    "Comments": {
      "WarningComment": {
        "Text": "ただちに避難してください。\n \n<津波警報>\n津波による被害が発生します......",
        "Code": "0149 0122 0123 0124 0131 0132"
      },
      "FreeFormComment": "[予想される津波の高さの解説]\n予想される津波が高いほど、より甚大な被害が生じます。\n10m超  巨大な津波が襲い壊滅的な被害が生じる......"
    }
  }
}

気象庁防災情報XML解説資料内の、地震火山関連の資料におおむね準拠した構造となっています

要素名 説明
Control.Title 情報種別
Head.Title 標題
Head.ReportDateTime 発表時刻
Head.Headline.Text 見出し文
Body.Tsunami.Forecast 津波警報など
Body.Earthquake 原因地震
Body.Comments.WarningComment.Text 固定付加文
Body.Comments.FreeFormComment 自由付加文

Body.Tsunami.Forecast内には津波予報区が列挙されており、それぞれについてKind(発表中の警報種別)、LastKind(変更前の警報種別)が格納されています

Body.Tsunami.Forecast.Item.{i}.FirstHeight(第1波到達予想)は以下のように、津波到達前は到達予想時刻が格納されますが、第1波到達後は「第1波の到達を確認」「津波到達中と推測」等のCondition要素に変わります。また、追加や変更があった場合にはRevise要素が追加されます

津波到達前
"FirstHeight": {
  "ArrivalTime": "2026-04-20T17:30:00+09:00",
  "Revise": "追加"
}
津波到達前(津波到達まで猶予がない場合)
"FirstHeight": {
  "ArrivalTime": "2026-04-20T17:30:00+09:00",
  "Condition": "ただちに津波来襲と予測"
}
津波到達後
"FirstHeight": {
  "Condition": "第1波の到達を確認",
  "Revise": "更新"
}
津波到達後だが観測なし
"FirstHeight": {
  "Condition": "津波到達中と推測",
  "Revise": "更新"
}

Body.Tsunami.Forecast.Item.{i}.MaxHeight(最大波)は以下のように、津波の予想高さが格納されます

予想高さ1m
"MaxHeight": {
  "TsunamiHeight": "1",
  "Revise": "更新"
}
予想高さ0.2m未満
"MaxHeight": {
  "TsunamiHeight": "<0.2"
}

この部分は気象庁防災情報XMLとは若干仕様が異なるようで、予想高さ20センチ未満の場合は「0.2」ではなく「<0.2」と格納される模様です。予想高さ10m以上の場合の表現は不明ですが、気象庁ホームページのソースコードを読む限り「>」「≧」にマッチするコードはあるものの「≦」にマッチするコードはないようなので、おそらく「≧10」と格納されるのではないかと考えています。同様に定性表現の場合もMaxHeightに「巨大」「高い」と直接格納されるのではないかと思います。XMLの仕様からは、大津波警報の予想高さが上方修正された場合にはCondition要素で「重要」が報じられると思われます

予想高さ10m以上(推測)
"MaxHeight": {
  "TsunamiHeight": "≧10",
  "Condition": "重要",
  "Revise": "更新"
}
定性表現(推測)
"MaxHeight": {
  "TsunamiHeight": "巨大"
}

Body.Earthquake(原因地震)は地震情報とおおむね同様ですが、原因地震を複数持つ可能性があるため配列表現となっています

例2 各地の満潮時刻・津波到達予想時刻に関する情報 / 津波観測に関する情報

{
  "Control": {
    "Title": "津波情報a"
  },
  "Head": {
    "Title": "各地の満潮時刻・津波到達予想時刻に関する情報",
    "ReportDateTime": "2026-04-20T20:15:00+09:00",
    "Headline": {
      "Text": "各地の満潮時刻と津波到達予想時刻をお知らせします。"
    }
  },
  "Body": {
    "Tsunami": {
      "Observation": {
        "Item": [
          {
            "Area": {
              "Name": "北海道太平洋沿岸東部",
              "Code": "100"
            },
            "Station": [
              {
                "Name": "浜中町霧多布港",
                "Code": "10021",
                "FirstHeight": {
                  "Condition": "第1波識別不能"
                },
                "MaxHeight": {
                  "DateTime": "2026-04-20T18:59:00+09:00",
                  "TsunamiHeight": "0.2"
                },
                "latlon": {
                  "lat": 43.08,
                  "lon": 145.12
                }
              }
            ]
          },
          {
            "Area": {
              "Name": "北海道太平洋沿岸中部",
              "Code": "101"
            },
            "Station": [
              {
                "Name": "浦河",
                "Code": "10101",
                "FirstHeight": {
                  "Condition": "第1波識別不能"
                },
                "MaxHeight": {
                  "DateTime": "2026-04-20T18:31:00+09:00",
                  "TsunamiHeight": "0.4"
                },
                "latlon": {
                  "lat": 42.17,
                  "lon": 142.77
                }
              },......
            ]
          }
        ]
      }
      "Forecast": {
        "CodeDefine": {
          "Type": "潮位観測点"
        },
        "Item": [
          {
            "Area": {
              "Name": "北海道太平洋沿岸東部",
              "Code": "100"
            },
            "Category": {
              "Kind": {
                "Name": "津波注意報",
                "Code": "62"
              },
              "LastKind": {
                "Name": "津波注意報",
                "Code": "62"
              }
            },
            "FirstHeight": {
              "Condition": "第1波の到達を確認"
            },
            "MaxHeight": {
              "TsunamiHeight": "1"
            },
            "Station": [
              {
                "Name": "釧路",
                "Code": "10001",
                "HighTideDateTime": "2026-04-20T18:07:00+09:00",
                "FirstHeight": {
                  "Condition": "津波到達中と推測"
                },
                "latlon": {
                  "lat": 42.98,
                  "lon": 144.37
                }
              },
              {
                "Name": "根室市花咲",
                "Code": "10002",
                "HighTideDateTime": "2026-04-20T18:11:00+09:00",
                "FirstHeight": {
                  "Condition": "津波到達中と推測"
                },
                "latlon": {
                  "lat": 43.28,
                  "lon": 145.57
                }
              },......
            ]
          }
        ]
      }
    },
    "Earthquake": [
      {
        "OriginTime": "2026-04-20T16:52:00+09:00",
        "ArrivalTime": "2026-04-20T16:53:00+09:00",
        "Hypocenter": {
          "Area": {
            "Name": "三陸沖",
            "Code": "288",
            "Coordinate": "+39.8+143.2-20000/"
          }
        },
        "Magnitude": "7.7"
      }
    ],
    "Comments": {
      "WarningComment": {
        "Text": "津波と満潮が重なると、津波はより高くなりますので十分な注意が必要です。",
        "Code": "0110"
      }
    }
  }
}

津波警報電文にあった情報に加え、各観測点ごとの津波観測情報・津波到達予想が格納されています

観測情報の方の第1波の情報(Body.Tsunami.Observation.Item.{i}.Station.{j}.FirstHeight)は、正常に観測できた場合には第1波の時刻と押し引きが格納されます。第1波がはっきりしない場合にはCondition要素で「第1波識別不能」を報じます

正常に観測できた場合の例
"FirstHeight": {
  "ArrivalTime": "2026-04-20T18:16:00+09:00",
  "Initial": "押し",
  "Revise": "更新"
}
第1波がはっきりしない場合
"FirstHeight": {
  "Condition": "第1波識別不能",
  "Revise": "追加"
}

観測情報の方の最大波の情報(Body.Tsunami.Observation.Item.{i}.Station.{j}.MaxHeight)は、観測時刻と高さが格納されます

正常に観測できた場合の例
"MaxHeight": {
  "DateTime": "2026-04-20T19:34:00+09:00",
  "TsunamiHeight": "0.3", 
  "Revise": "更新"
}

XML電文と異なり、水位が上昇中の場合は、高さに「+」を付けて報じるようです

水位が上昇中の場合の例
"MaxHeight": {
  "DateTime": "2026-04-20T19:34:00+09:00",
  "TsunamiHeight": "0.2+", 
  "Revise": "更新"
}

最大値が観測可能範囲を超えた場合の表現については不明ですが、気象庁ホームページのソースコードからは高さに「>」または「≧」を付けて表現するのではないかと思われます

最大波の高さが観測可能範囲を超えた場合の例(推定)
"MaxHeight": {
  "DateTime": "2026-04-20T19:15:00+09:00",
  "Condition": "重要",
  "TsunamiHeight": ">5.5", 
  "Revise": "更新"
}

解説資料によれば、観測された津波が数センチの微弱な津波の場合は、Condition要素で「微弱」を報じるとみられます

微弱な津波の例(推定)
"MaxHeight": {
  "DateTime": "2026-04-20T19:31:00+09:00",
  "Condition": "微弱", 
  "Revise": "追加"
}

現時点で観測された高さが予想より大幅に小さい場合には、後の最大波への警戒を損ねない目的で、Condition要素で「観測中」を報じます

観測された高さが予想より大幅に小さい場合
"MaxHeight": {
  "Condition": "観測中"
}

津波到達予想は、津波予報区に加え観測点別の到達予想が追加されます。形式は津波予報区別の場合とおおむね同じですが、満潮時刻(HighTideDateTime)と観測点の緯度経度が追加されます

"Station": [
  {
    "Name": "いわき市小名浜",
    "Code": "25002",
    "HighTideDateTime": "2026-04-20T18:48:00+09:00",
    "FirstHeight": {
      "ArrivalTime": "2026-04-20T18:00:00+09:00"
    },
    "latlon": {
      "lat": 36.93,
      "lon": 140.9
    }
  },......
]

例3 沖合の津波観測に関する情報

{
  "Control": {
    "Title": "沖合の津波観測に関する情報"
  },
  "Head": {
    "Title": "沖合の津波観測に関する情報",
    "ReportDateTime": "2026-04-20T17:40:00+09:00",
    "Headline": {
      "Text": null
    }
  },
  "Body": {
    "Tsunami": {
      "Observation": {
        "CodeDefine": {
          "Type": "潮位観測点"
        },
        "Item": [
          {
            "Area": {
              "Name": null,
              "Code": null
            },
            "Station": [
              {
                "Name": "浦河沖50kmA",
                "Code": "10172",
                "Sensor": "水圧計",
                "FirstHeight": {
                  "ArrivalTime": "2026-04-20T17:15:00+09:00",
                  "Initial": "押し",
                  "Revise": "追加"
                },
                "MaxHeight": {
                  "Condition": "観測中",
                  "Revise": "追加"
                },
                "latlon": {
                  "lat": 41.56,
                  "lon": 142.82
                }
              },
              {
                "Name": "青森東方沖50kmB",
                "Code": "20173",
                "Sensor": "水圧計",
                "FirstHeight": {
                  "ArrivalTime": "2026-04-20T17:06:00+09:00",
                  "Initial": "押し"
                },
                "MaxHeight": {
                  "Condition": "観測中"
                },
                "latlon": {
                  "lat": 40.59,
                  "lon": 142.28
                }
              },......
            ]
          }
        ]
      },
      "Estimation": {
        "CodeDefine": {
          "Type": "沿岸地域"
        },
        "Item": [
          {
            "Area": {
              "Name": "岩手県",
              "Code": "210"
            },
            "FirstHeight": {
              "ArrivalTime": "2026-04-20T17:11:00+09:00",
              "Condition": "早いところでは既に津波到達と推定"
            },
            "MaxHeight": {
              "Condition": "推定中"
            }
          },
          {
            "Area": {
              "Name": "宮城県",
              "Code": "220"
            },
            "FirstHeight": {
              "ArrivalTime": "2026-04-20T17:29:00+09:00",
              "Condition": "早いところでは既に津波到達と推定",
              "Revise": "更新"
            },
            "MaxHeight": {
              "DateTime": "2026-04-20T17:35:00+09:00",
              "TsunamiHeight": "1",
              "Revise": "更新"
            }
          }
        ]
      }
    },
    "Earthquake": [
      {
        "OriginTime": "2026-04-20T16:52:00+09:00",
        "ArrivalTime": "2026-04-20T16:53:00+09:00",
        "Hypocenter": {
          "Area": {
            "Name": "三陸沖",
            "Code": "288",
            "Coordinate": "+39.8+143.2-10000/"
          }
        },
        "Magnitude": "7.5"
      }
    ],
    "Comments": {
      "WarningComment": {
        "Text": "沖合での観測値であり、沿岸では津波はさらに高くなります。",
        "Code": "0115"
      }
    }
  }
}

Body.Tsunami.Observation.Item.{i}(観測値部分)の構造は例2とほぼ同じですが、観測点名に対応する地域名(Area.Name)は格納されません。

また、沖合の観測値から推定される沿岸部の津波の高さがEstimation要素として格納されます。例1・例2のForecast要素と似たような構造となっていますが、推定される津波の高さが予想より小さい場合には、Observation要素の「観測中」と同様の考え方で「推定中」という表現がConditionに格納されます

表示用のコード

ここまでの内容を表に起こしていきます

津波情報一覧

大津波警報/津波警報/津波注意報/津波予報それぞれの背景色を定義しておきます。情報ごとの色は気象庁ホームページの配色指針を参考にしました。(「ev5」は「earthquakeVolcanoLevel5」の略。噴火警報の表示ページを将来作成する際に共通利用することを念頭に置いて作成しています)

<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;}
  .ev5{ background:#c800ff; color:white;}
  .ev4{ background:#ff2800; color:white;}
  .ev2{ background:#faf500; color:black;}
  .ev0{ background:#80ffff; color:black;}
</style>

情報のタイトルを読み取る際には、含まれている文言に応じて色分けを変更します

let infoTitle = report['ttl'], className = "";
if( infoTitle.includes("大津波警報")){
  className = "ev5";
}else if( infoTitle.includes("津波警報")){
  className = "ev4";
}else if( infoTitle.includes("津波注意報")){
  className = "ev2";
}else if( infoTitle.includes("津波予報")){
  className = "ev0";
}
out += "<td class='" + className + "'>" + infoTitle + "</td>";

個別の津波情報の表形式の表示ページと、地図形式の表示ページ(今後作成)へのリンクを作成します

if( report['json']==undefined){
  out += "<td></td><td></td>";
}else{
  out += "<td><a href='./tsunami.html?json=" + report['json'] + "'>表</a></td>";
  out += "<td><a href='./tsunamiMap.html?json=" + report['json'] + "'>地図</a></td>";
}

個別の津波情報

URLパラメータから取得する個別の津波情報のURLを取得し、fetchで入手します

const params = new URLSearchParams(window.location.search);

get();

function get(){
  fetch("https://www.jma.go.jp/bosai/tsunami/data/" + params.get("json"))
  .then((response) => response.json())
  .then((report) => display(report));
}

観測値や予測値を読み取る際に、Revise(更新や追加)要素がある場合には重要度の高い情報として強調表示するようにします。また、FirstHeight.Condition要素(「ただちに津波来襲と予測」や「第1波の到達を確認」)がある場合には、到達予想時刻に優先して表示するようにします

if( station['FirstHeight']['Condition']==undefined){
  stationArrivalTime = Temporal.ZonedDateTime.from( station['FirstHeight']['ArrivalTime']+"[Asia/Tokyo]");
  stationArrivalTime = dateFormat( stationArrivalTime, 'd日h:n');
  stationInitial = station['FirstHeight']['Initial'];
}else{
  stationArrivalTime = station['FirstHeight']['Condition'];
}
if( station['FirstHeight']['Revise']!=undefined){
  stationInitial += " <strong>" + station['FirstHeight']['Revise'] + "</strong>";
}

最大の高さは、観測値と予測値共通の書式設定関数を定義して整形します

stationMaxHeight = formatMaxHeight(station['MaxHeight']);
formatMaxHeight()
function formatMaxHeight( maxHeight){
  let height = "";
  if( maxHeight!=undefined && maxHeight['TsunamiHeight']!=undefined){
    height = maxHeight['TsunamiHeight'];
  }
  try{
    if( height.includes("<")){ // 「<」が含まれる場合は削除して「m未満」を追加
      height = height.replace("<","") + "m未満";
    }else if( height.includes(">") || height.includes("")){ // 「>」「≧」が含まれる場合は削除して「m以上」を追加
      height = height.replace(">","").replace("","") + "m以上";
    }else if( height!="" && !height.includes("巨大") && !height.includes("高い")){ // それ以外の場合で、定性表現を含まない場合は「m」を追加
      height = height + "m";
    }
    if( height.includes("+")){ // 「+」が含まれる場合は「(上昇中)」を追加
      height = height.replace("+","") + "(上昇中)";
    }
    if( maxHeight['Condition']!=undefined){ // Conditionが"観測中","推定中","微弱","欠測"の場合はそのまま追加。それ以外の要素(「重要」等)が含まれる場合は強調表示で追加
      if( ["観測中","推定中","微弱","欠測"].includes(maxHeight['Condition'])){
        height += " " + maxHeight['Condition'];
      }else{
        height += " <strong>" + maxHeight['Condition'] + "</strong>";
      }
    }
    if( maxHeight['Revise']!=undefined){ // Revise要素(追加/更新)が含まれる場合は強調表示で追加
      height += " <strong>" + maxHeight['Revise'] + "</strong>";
    }
    return height;
  }catch(e){
    console.log( e);
    return "";
  }
}

予想部については、津波予報区を警報の種別ごとにまとめて表示しています。警報の種別は警報名を1文字目に含み、「解除」を含まない場合にその種別と判定し、色分けして表示しています

const warnLevels = { '大津波警報':'ev5','津波警報':'ev4','津波注意報':'ev2','津波予報(若干の海面変動)':'ev0'}
for( let targetWarnKind in warnLevels){
  let isFirst = true;
  for( let item of report['Body']['Tsunami']['Forecast']['Item']){
    let areaName=item['Area']['Name'], warnKind="", areaFirstTime="", areaMaxHeight="";
    warnKind = item['Category']['Kind']['Name'];
    if( warnKind.indexOf(targetWarnKind)==0 && !warnKind.includes("解除")){
      if( isFirst){
        isFirst = false;
        out += "<tr><th class='" + warnLevels[warnKind] + "'>" + targetWarnKind + "</th><td class='" + warnLevels[warnKind] + "'>第1波</td><td class='" + warnLevels[warnKind] + "'>満潮</td></tr>";
      }
    }else{
      continue;
    }
    ......

原因地震部分は地震情報とほぼ同様ですが、原因地震が複数ある場合にも対応可能な構造にします

if( report['Body']['Earthquake']!=undefined){
  out += "<tr><th>原因地震</th><td>";
  for( let i in report['Body']['Earthquake']){
    out += "<table>";
    if( report['Body']['Earthquake'][i]['OriginTime']!=undefined){
      let originTime = Temporal.ZonedDateTime.from( report['Body']['Earthquake'][i]['OriginTime']+"[Asia/Tokyo]");
      out += "<tr><th>発生時刻</th><td>" + dateFormat(originTime,'d日h時n分') + "ごろ</td></tr>";
    }
    if( report['Body']['Earthquake'][i]['Hypocenter']!=undefined){
      if( report['Body']['Earthquake'][i]['Hypocenter']['Area']!=undefined){
        out += "<tr><th>震源</th><td>" + report['Body']['Earthquake'][i]['Hypocenter']['Area']['Name'] + "</td></tr>";
        let coordinate = report['Body']['Earthquake'][i]['Hypocenter']['Area']['Coordinate'];
        if( coordinate!=undefined){
          coordinate = coordinate.split(/(?=[-+])/); 
          if( coordinate.length>=3){
            let depth = coordinate[2].replace("/","") / 1000 * (-1);
            if( depth==0){
              out += "<tr><th>深さ</th><td>" + "ごく浅い</td></tr>";
            }else{
              out += "<tr><th>深さ</th><td>" + depth + "キロ</td></tr>";
            }
          }
        }
      }
    }
    if( report['Body']['Earthquake'][i]['Magnitude']!=undefined){
      out += "<tr><th>マグニチュード</th><td>" + report['Body']['Earthquake'][i]['Magnitude'] + "</td></tr>";
    }
    out += "</table>";
  }
  out += "</td></tr>";
}

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

サンプルページ
サンプルページ(Date APIを利用した版)

津波リスト

<!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;}
    .ev5{ background:#c800ff; color:white;}
    .ev4{ background:#ff2800; color:white;}
    .ev2{ background:#faf500; color:black;}
    .ev0{ background:#80ffff; color:black;}
  </style>
</head>
<body>
  <h1>津波情報(一覧)</h1>
  <div id="out"></div>
  <script>
    "use strict";

    get();

    function get(){
      fetch("https://www.jma.go.jp/bosai/tsunami/data/list.json")
      .then((response) => response.json())
      .then((reports) => display(reports));
    }

    function display( reports){
      let out = "";

      out += "<table><tr><th>発表時刻</th><th>情報の種類</th><th>震源</th><th>M</th><th colspan='2'>詳細</th></tr>";
      for( let report of reports){
        out += "<tr>";
        try{
          let reportDatetime = Temporal.ZonedDateTime.from( report['at']+"[Asia/Tokyo]");
          out += "<td>" + dateFormat(reportDatetime,'m/d h:n') + "</td>";
        }catch(e){
          out += "<td></td>";
        }
        let infoTitle = report['ttl'], className = "";
        if( infoTitle.includes("大津波警報")){
          className = "ev5";
        }else if( infoTitle.includes("津波警報")){
          className = "ev4";
        }else if( infoTitle.includes("津波注意報")){
          className = "ev2";
        }else if( infoTitle.includes("津波予報")){
          className = "ev0";
        }
        out += "<td class='" + className + "'>" + infoTitle + "</td>";
        let areaName = report['anm'];
        out += "<td>" + areaName + "</td>";
        if( report['mag']==undefined){
          out += "<td></td>";
        }else{
          out += "<td>" + report['mag'] + "</td>";
        }
        if( report['json']==undefined){
          out += "<td></td><td></td>";
        }else{
          out += "<td><a href='./tsunami.html?json=" + report['json'] + "'>表</a></td>";
          out += "<td><a href='./tsunamiMap.html?json=" + report['json'] + "'>地図</a></td>";
        }
        out += "</tr>";
      }
      out += "</table>";
      document.getElementById("out").innerHTML = out;
    }

    function dateFormat( t, f='y-m-dTh:n:s', is24=false){ // for Temporal
      const j=["","","","","","","",""];const e=["","Mon","Tue","Wed","Thu","Fri","Sat","Sun"];
      if(is24&&t.hour==0){t.substract({days:1});f=f.replace(/h/g,"24").replace(/H/g,24);}
      const y=t.year,m=t.month,d=t.day,h=t.hour,n=t.minute,s=t.second,w=t.dayOfWeek;
      f=f.replace(/y/g,("000"+y).slice(-4)).replace(/Y/g,y).replace(/m/g,("0"+m).slice(-2)).replace(/M/g,m).replace(/d/g,("0"+d).slice(-2)).replace(/D/g,d);
      f=f.replace(/w/g,j[w]).replace(/W/g,e[w]);
      f=f.replace(/h/g,("0"+h).slice(-2)).replace(/H/g,h).replace(/n/g,("0"+n).slice(-2)).replace(/N/g,n).replace(/s/g,("0"+s).slice(-2)).replace(/S/g,s);
      return f;
    }
  </script>
</body>
</html>

個別の津波情報

<!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;}
    th,td{ padding:2px 8px; border-style:solid; border-width:1px 0; border-color:#d8d8db;}
    th{ background-color:#f1f1f4; white-space:nowrap; text-align:left;}
    .ev5{ background:#c800ff; color:white;}
    .ev4{ background:#ff2800; color:white;}
    .ev2{ background:#faf500; color:black;}
    .ev0{ background:#80ffff; color:black;}
    strong{ background:#faf500;}
  </style>
</head>
<body>
  <h1>津波情報</h1>
  <div id="out"></div>
  <script>
    "use strict";
    const params = new URLSearchParams(window.location.search);


    get();

    function get(){
      fetch("https://www.jma.go.jp/bosai/tsunami/data/" + params.get("json"))
      .then((response) => response.json())
      .then((report) => display(report));
    }

    function display( report){
      let out = "";

      let reportDatetime = Temporal.ZonedDateTime.from( report['Head']['ReportDateTime']+"[Asia/Tokyo]");
      out += "<table>";
      out += "<tr><th>情報名</th><td>" + report['Head']['Title'] + "</td></tr>";
      out += "<tr><th>発表時刻</th><td>" + dateFormat(reportDatetime,'y/m/d h:n') + " " + report['Head']['InfoType'] + "</td></tr>";
      if( report['Head']['Headline']!=undefined && report['Head']['Headline']['Text']!=null){
        out += "<tr><th></th><td>" + report['Head']['Headline']['Text'] + "</td></tr>";
      }
      if( report['Body']['Tsunami']!=undefined){
        if( report['Body']['Tsunami']['Observation']!=undefined){
          out += "<tr><th>津波観測</th><td>";
          out += "<table>";
          out += "<tr><th></th><th>第1波</th><th>最大波</th></tr>";
          for( let item of report['Body']['Tsunami']['Observation']['Item']){
            if( item['Station']!=undefined){
              for( let station of item['Station']){
                let stationName=station['Name'], stationArrivalTime="", stationInitial="", stationMaxTime="", stationMaxHeight="", stationMaxHeightCondition="";
                try{
                  if( station['FirstHeight']['Condition']==undefined){
                    stationArrivalTime = Temporal.ZonedDateTime.from( station['FirstHeight']['ArrivalTime']+"[Asia/Tokyo]");
                    stationArrivalTime = dateFormat( stationArrivalTime, 'd日h:n');
                    stationInitial = station['FirstHeight']['Initial'];
                  }else{
                    stationArrivalTime = station['FirstHeight']['Condition'];
                  }
                  if( station['FirstHeight']['Revise']!=undefined){
                    stationInitial += " <strong>" + station['FirstHeight']['Revise'] + "</strong>";
                  }
                }catch(e){}
                try{
                  stationMaxTime = Temporal.ZonedDateTime.from( station['MaxHeight']['DateTime']+"[Asia/Tokyo]");
                  stationMaxTime = dateFormat( stationMaxTime, 'd日h:n');
                }catch(e){}
                stationMaxHeight = formatMaxHeight(station['MaxHeight']);
                out += "<tr>";
                out += "<th>" + stationName + "</th>";
                out += "<td>" + stationArrivalTime + " " + stationInitial + "</td>";
                out += "<td>" + stationMaxTime + " " + stationMaxHeight + "</td>";
                out += "</tr>";
              }
            }
          }
          out += "</table>";
          out += "</td></tr>";
        }
        if( report['Body']['Tsunami']['Estimation']!=undefined){
          out += "<tr><th>沖合観測から推定</th><td>";
          out += "<table>";
          out += "<tr><th></th><th>第1波</th><th>最大波(推定)</th></tr>";
          for( let item of report['Body']['Tsunami']['Estimation']['Item']){
            let areaName=item['Area']['Name'], arrivalTime="", maxTime="", maxHeight="";
            try{
              arrivalTime = Temporal.ZonedDateTime.from( item['FirstHeight']['ArrivalTime']+"[Asia/Tokyo]");
              arrivalTime = dateFormat( arrivalTime, 'd日h:n');
              if( item['FirstHeight']['Condition']!=undefined){
                arrivalTime = item['FirstHeight']['Condition'];
              }
            }catch(e){}
            try{
              maxTime = Temporal.ZonedDateTime.from( item['MaxHeight']['DateTime']+"[Asia/Tokyo]");
              maxTime = dateFormat( maxTime, 'd日h:n');
            }catch(e){}
            maxHeight = formatMaxHeight( item['MaxHeight']);
            out += "<tr>";
            out += "<th>" + areaName + "</th>";
            out += "<td>" + arrivalTime + "</td>";
            out += "<td>" + maxTime + " " + maxHeight + "</td>";
            out += "</tr>";
          }
          out += "</table>";
          out += "</td></tr>";
        }
        if( report['Body']['Tsunami']['Forecast']!=undefined){
          out += "<tr><th>津波到達予想</th><td>";
          out += "<table>";
          const warnLevels = { '大津波警報':'ev5','津波警報':'ev4','津波注意報':'ev2','津波予報(若干の海面変動)':'ev0'}
          for( let targetWarnKind in warnLevels){
            let isFirst = true;
            for( let item of report['Body']['Tsunami']['Forecast']['Item']){
              let areaName=item['Area']['Name'], warnKind="", areaFirstTime="", areaMaxHeight="";
              warnKind = item['Category']['Kind']['Name'];
              if( warnKind.indexOf(targetWarnKind)==0 && !warnKind.includes("解除")){
                if( isFirst){
                  isFirst = false;
                  out += "<tr><th class='" + warnLevels[warnKind] + "'>" + targetWarnKind + "</th><td class='" + warnLevels[warnKind] + "'>第1波</td><td class='" + warnLevels[warnKind] + "'>満潮</td></tr>";
                }
              }else{
                continue;
              }
              try{
                if( item['FirstHeight']['Condition']==undefined){
                  areaFirstTime = Temporal.ZonedDateTime.from( item['FirstHeight']['ArrivalTime']+"[Asia/Tokyo]");
                  areaFirstTime = dateFormat( areaFirstTime, 'd日h:n');
                }else{
                  areaFirstTime = item['FirstHeight']['Condition'];
                }
                if( item['FirstHeight']['Revise']!=undefined){
                  areaFirstTime += " <strong>" + item['FirstHeight']['Revise'] + "</strong>";
                }
              }catch(e){}
              areaMaxHeight = formatMaxHeight( item['MaxHeight']);
              out += "<tr>";
              out += "<th>" + areaName + "</th>";
              out += "<th>" + areaFirstTime + "</th>";
              out += "<th>最大 " + areaMaxHeight + "</th>";
              out += "</tr>";
              if( item['Station'] != undefined){
                for( let station of item['Station']){
                  let stationName = station['Name'], stationFirstTime = "", stationHighTideTime = "";
                  if( station['FirstHeight']['Condition']==undefined){
                    stationFirstTime = Temporal.ZonedDateTime.from( station['FirstHeight']['ArrivalTime']+"[Asia/Tokyo]");
                    stationFirstTime = dateFormat( stationFirstTime, 'd日h:n');
                  }else{
                    stationFirstTime = station['FirstHeight']['Condition'];
                  }
                  if( station['FirstHeight']['Revise']!=undefined){
                    stationFirstTime += " <strong>" + station['FirstHeight']['Revise'] + "</strong>";
                  }
                  try{
                    stationHighTideTime = Temporal.ZonedDateTime.from( station['HighTideDateTime']+"[Asia/Tokyo]");
                    stationHighTideTime = dateFormat( stationHighTideTime, '満潮 d日h:n');
                  }catch(e){}
                  out += "<tr>";
                  out += "<td> " + stationName + "</td>";
                  out += "<td>" + stationFirstTime + "</td>";
                  out += "<td>" + stationHighTideTime + "</td>";
                  out += "</tr>";
                }
              }
            }
          }
          out += "</table>";
          out += "</td></tr>";
        }
      }
      if( report['Body']['Earthquake']!=undefined){
        out += "<tr><th>原因地震</th><td>";
        for( let i in report['Body']['Earthquake']){
          out += "<table>";
          if( report['Body']['Earthquake'][i]['OriginTime']!=undefined){
            let originTime = Temporal.ZonedDateTime.from( report['Body']['Earthquake'][i]['OriginTime']+"[Asia/Tokyo]");
            out += "<tr><th>発生時刻</th><td>" + dateFormat(originTime,'d日h時n分') + "ごろ</td></tr>";
          }
          if( report['Body']['Earthquake'][i]['Hypocenter']!=undefined){
            if( report['Body']['Earthquake'][i]['Hypocenter']['Area']!=undefined){
              out += "<tr><th>震源</th><td>" + report['Body']['Earthquake'][i]['Hypocenter']['Area']['Name'] + "</td></tr>";
              let coordinate = report['Body']['Earthquake'][i]['Hypocenter']['Area']['Coordinate'];
              if( coordinate!=undefined){
                coordinate = coordinate.split(/(?=[-+])/); 
                if( coordinate.length>=3){
                  let depth = coordinate[2].replace("/","") / 1000 * (-1);
                  if( depth==0){
                    out += "<tr><th>深さ</th><td>" + "ごく浅い</td></tr>";
                  }else{
                    out += "<tr><th>深さ</th><td>" + depth + "キロ</td></tr>";
                  }
                }
              }
            }
          }
          if( report['Body']['Earthquake'][i]['Magnitude']!=undefined){
            out += "<tr><th>マグニチュード</th><td>" + report['Body']['Earthquake'][i]['Magnitude'] + "</td></tr>";
          }
          out += "</table>";
        }
        out += "</td></tr>";
      }
      if( report['Body']['Comments']!=undefined){
        out += "<tr><th></th><td>";
        for( let commentType in report['Body']['Comments']){
          if( report['Body']['Comments'][commentType]['Text']!=undefined){
            out += report['Body']['Comments'][commentType]['Text'].replaceAll("\n","<br>") + "<br>";
          }
        }
        out += "</td></tr>";
        if( report['Body']['Comments']['FreeFormComment']!=undefined){
          out += "<tr><th></th><td>" + report['Body']['Comments']['FreeFormComment'].replaceAll("\n","<br>") + "</td></tr>";
        }
      }
      out += "</table>";
      document.getElementById("out").innerHTML = out;
    }

    function formatMaxHeight( maxHeight){
      let height = "";
      if( maxHeight!=undefined && maxHeight['TsunamiHeight']!=undefined){
        height = maxHeight['TsunamiHeight'];
      }
      try{
        if( height.includes("<")){
          height = height.replace("<","") + "m未満";
        }else if( height.includes(">") || height.includes("")){
          height = height.replace(">","").replace("","") + "m以上";
        }else if( height!="" && !height.includes("巨大") && !height.includes("高い")){
          height = height + "m";
        }
        if( height.includes("+")){
          height = height.replace("+","") + "(上昇中)";
        }
        if( maxHeight['Condition']!=undefined){
          if( ["観測中","推定中","微弱","欠測"].includes(maxHeight['Condition'])){
            height += " " + maxHeight['Condition'];
          }else{
            height += " <strong>" + maxHeight['Condition'] + "</strong>";
          }
        }
        if( maxHeight['Revise']!=undefined){
          height += " <strong>" + maxHeight['Revise'] + "</strong>";
        }
        return height;
      }catch(e){
        console.log( e);
        return "";
      }
    }

    function dateFormat( t, f='y-m-dTh:n:s', is24=false){ // for Temporal
      const j=["","","","","","","",""];const e=["","Mon","Tue","Wed","Thu","Fri","Sat","Sun"];
      if(is24&&t.hour==0){t.substract({days:1});f=f.replace(/h/g,"24").replace(/H/g,24);}
      const y=t.year,m=t.month,d=t.day,h=t.hour,n=t.minute,s=t.second,w=t.dayOfWeek;
      f=f.replace(/y/g,("000"+y).slice(-4)).replace(/Y/g,y).replace(/m/g,("0"+m).slice(-2)).replace(/M/g,m).replace(/d/g,("0"+d).slice(-2)).replace(/D/g,d);
      f=f.replace(/w/g,j[w]).replace(/W/g,e[w]);
      f=f.replace(/h/g,("0"+h).slice(-2)).replace(/H/g,h).replace(/n/g,("0"+n).slice(-2)).replace(/N/g,n).replace(/s/g,("0"+s).slice(-2)).replace(/S/g,s);
      return f;
    }
  </script>
</body>
</html>
  1. 気象機関と書きましたが、どちらかというと天気予報をする機関とは別組織が担当していることが多いです

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?