気象庁の台風位置表から GeoJSON を作成する

## はじめに

気象庁から過去の気象データとして[台風位置表](https://www.data.jma.go.jp/yoho/typhoon/position_table/index.html)が提供されています。データ形式は PDF と CSV が提供されていますが、内容が異なるほか、速報値は PDF でしか提供されていないようです。

この PDF から データを取り出して、GeoJSON のラインデータやポイントデータを作成した作業を記録として残します。なお、一部手動作業を含みます。

:::note warn
台風位置表の解釈については、誤っている部分があるかもしれません。ご利用に当たっては自己責任でお願いします。
:::
:::note warn
気象庁のデータを利用するときに、CSV 等で提供されているのに見落としていることがあります。今回もその可能性は否定できませんので、ご了承ください。
:::

## 台風位置表の中身

台風位置表の PDF ですが、以下の情報が含まれた表になっています。

* 日本時（月、日、時）
* 中心位置（経度、緯度）
* 中心気圧
* 最大風速
* 暴風域半径
* 強風域半径
* 大きさ・強さ 等

![2022年台風第14号位置表（速報）.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/9b4170dd-b93b-1c36-5b11-72bc4fd7f109.png)
（[2022年台風第14号の位置表](https://www.data.jma.go.jp/yoho/data/typhoon/T2214.pdf) ※投稿時点では速報値）

また、月や日、東経・西経、北緯・南緯の別の情報は、一度記載されるとその後は省略されます。

まさに「見る」ためのもので、データ解析には適した形式ではありませんが、PDF をコピー＆ペーストでテキストファイルに落とすと、ある程度情報が取り出せるように思えます。

## コピペの結果を観察する

今回は、**PDF を全選択（Ctrl＋A）して、テキストファイルにコピー＆ペーストしたものを出発点**に、日本時～暴風域半径までのデータを取り出してみます。

コピペしたテキストファイルを見てみます。

```

2022年台風第14号 NANMADOL (2214)
中心 最大
気圧 風速
hPa m/s
台風発生
9 14 03 22.4 N 140.1 E 996 18 --- SE: 390 NW: 280 － －
06 22.5 140.5 996 18 --- SE: 390 NW: 280 － －
09 22.5 140.6 996 18 --- SE: 390 NW: 280 － －
（中略）
15 00 23.2 140.2 990 23 --- SE: 390 NW: 280 － －
03 23.1 139.7 985 25 --- SE: 390 NW: 330 － －
06 23.1 139.5 985 25 --- SE: 390 NW: 330 － －
09 23.3 139.2 980 30 95 SE: 390 NW: 330 － －
（中略）
18 00 27.9 131.7 910 55 E: 185 W: 150 E: 750 W: 650 大型 猛烈な
01 28.2 131.6 910 55 E: 185 W: 150 E: 750 W: 650 大型 猛烈な
02 28.4 131.5 910 55 E: 185 W: 150 E: 750 W: 650 大型 猛烈な
（以下、省略）
```

以下のような特徴があります。

* 表の各行は改行区切りで分離できる。
* 表の罫線の情報は取り出せない。
* 各情報の間にはスペースが入る。
	* ただし、表では空白であっても、テキストにすると「空白である」という情報は取り出せない。そのため、たとえば、一番最初の情報が、月なのか、日なのか、時なのかはわからない。
* 日付は月、日が省略されうる。時刻は2桁で0埋めされる。
* 経緯度は小数点以下一桁で固定されていそう（小数点部分が0でも `xxx.0` と記載される）。
* 経度、緯度それぞれの後に東経・西経（E/W）、北緯・南緯（N/S）の情報が入ることがある。
* 暴風域及び強風域は、単独で数値が入っている場合と、方角別に記載されている場合がある。


## 正規表現で情報を抜き出す

上記の特徴を踏まえて、正規表現で各情報を取り出すことを試みます。まず、最大風速まで取り出し、暴風域半径については残りの部分から後処理で取り出すこととします。このときの正規表現は以下の通りです。

```JavaScript
const m = line.match(/^(\S*)\s?(\S*)\s?(\d\d)\s(\d+\.\d+)\s?(N?S?)\s(\d+\.\d+)\s?(E?W?)\s(\d+)\s([-0-9]+)\s(.*)/);
```

これで、`m[1]`～`m[9]` で最大風速までの各情報にアクセスできます。`m[10]` は、暴風域半径を含む残りの部分です。 

ただし、月が省略されている場合、日の情報が最初に取得されてしまうので、以下のように、`m[1]` と `m[2]` を両方チェックしながら取得するようにしています。（このあたりは、正規表現のエンジンによるのかもしれません。）

```JavaScript
if(m[1] && m[2]) tmp.month = +m[1];  // 月
if(m[1] && !m[2]) tmp.date = +m[1];  // 日
if(m[2]) tmp.date = +m[2];  // 日
```

また、この情報は保持するようにしており、省略されていた場合、直前に取得できた月、日のデータを用いることにしています。


また、後回しにした暴風域についても正規表現で取り出しますが、単独で数値が入っている場合と方角別に記載されている場合の2パターンでマッチングを試みて、マッチした方の情報を使います。
```JavaScript
const other = m[10];
const m1 = other.match(/^([0-9]+)\s/);
const m2 = other.match(/^(E|S|NE|NW|W|N|SW|SE):\s(\d+)\s(E|S|NE|NW|W|N|SW|SE):\s(\d+)\s/);
```

以下、各行にマッチングをかけるロジック部分のまとめです。

```JavaScript
const main = (d) => {
  // 入力の d は、テキスト全文

  // 省略されうる情報の保持用
  let tmp = {
    "month": null,
    "date": null,
    "ns": "N",
    "ew": "E"
  };
  
  // 改行区切りの各行に対して正規表現で情報取得
  d.split("\n").forEach( line => {
    if(!line) return;
    
    const m = line.match(/^(\S*)\s?(\S*)\s?(\d\d)\s(\d+\.\d+)\s?(N?S?)\s(\d+\.\d+)\s?(E?W?)\s(\d+)\s([-0-9]+)\s(.*)/);
    if(!m) return;
    
    if(m[1] && m[2]) tmp.month = +m[1];
    if(m[1] && !m[2]) tmp.date = +m[1];
    if(m[2]) tmp.date = +m[2];
    if(m[5]) tmp.ns = m[5];
    if(m[7]) tmp.ew = m[7];
    
    const lat = +m[4] * ( tmp.ns == "S" ? -1 : 1 );
    const lng = +m[6] * ( tmp.ew == "W" ? -1 : 1 );
    
    // 暴風域
    const boufu = {
      "direction": [],
      "range": []
    };
    
    const m1 = other.match(/^([0-9]+)\s/);
    const m2 = other.match(/^(E|S|NE):\s(\d+)\s(W|N|SW):\s(\d+)\s/);
    
    if(m1){
      boufu.direction.push("-");
      boufu.range.push(+m1[1]);
    }
    
    if(m2){
      boufu.direction.push(m2[1]);
      boufu.range.push(+m2[2]);
      boufu.direction.push(m2[3]);
      boufu.range.push(+m2[4]);
    }
    
    if(boufu.range.length < 1){
      boufu.direction.push("-");
      boufu.range.push(+0);
    }
    
    // "日時（〇月〇日〇時）,経度,緯度,中心気圧,最大風速,暴風域（方向別の場合は大きい方）" を出力
    console.log(`${tmp.month}月${tmp.date}日${m[3]}時,${lng},${lat},${m[8]},${m[9]},${Math.max(...boufu.range)||"NA"}`);
  });
}
```

## GeoJSON へ変換する

上記正規表現で、PDF をもとにしたテキストデータから情報を抜き出せましたので、GeoJSON へ変換して各種 GIS ソフトウェアで使いやすいようにしておきます。

本筋ではないのでコードは省略しますが、今回は、[地理院地図](https://maps.gsi.go.jp/)で表示するための[スタイル設定](https://github.com/gsi-cyberjapan/geojson-with-style-spec)付きの GeoJSON とすることとし、ラインデータとポイントデータ（3種類）を出力できるようにしました。

以下、サンプル画像になります（出典：台風の経路については気象庁の「[2022年台風第14号の位置表](https://www.data.jma.go.jp/yoho/data/typhoon/T2214.pdf) （速報値）」を加工、背景は[地理院タイル](https://maps.gsi.go.jp/development/ichiran.html#pale)）。

![LINEpng.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/28ec5326-6d19-6758-ecdb-a4a2911ce7fa.png)
ラインデータ
![ICON.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/fc9cd088-bd16-48f6-1fe6-5575088f8491.png)
ポイントデータ（`Icon` タイプ）
![DIVICON.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/383e30bf-c23f-f21b-f2c7-1219b3a0702c.png)
ポイントデータ（`DivIcon` タイプ） ※日時を表示
![CIRCLE.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/913080ec-dd69-ac03-0dcb-b617530dc1a4.png)
ポイントデータ（`Circle` タイプ） ※半径は暴風域半径（複数の場合は最大値）


![SAMPLE.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/2a75ccf6-7136-17b6-10fb-80b4e26b22c4.png)
重ねて表示（台風上陸付近をクローズアップ）

デモサイトは以下の通りです。台風位置表 PDF から全コピペしたテキストファイルを選択すると、上記4種類の GeoJSON に加えて CSV へ変換できます。

https://mghs15.github.io/stolytelling-R1-T19-arage/pdf2route.html

:::note warn
ポイントデータ（`Circle` タイプ）では、 暴風域半径をもとに円の大きさを変更しています。ただし、この表現が適切かどうかはわかりません。ご利用の際はご注意ください。
:::

レポジトリはこちら。

https://github.com/mghs15/stolytelling-R1-T19-arage

## 終わりに

規則的なデータであれば、正規表現で頑張って情報を取得できます。最初から CSV 等の生データで欲しいというはありますが、人生に余裕があれば、正規表現という呪文で宝探しをするのも楽しいです。