国連と欧州委員会主催のハッカソンOSS4SDGに、津波3Dマップでエントリーしてみた

## OSS4SDG ハッカソンとは

SDGs をテーマに、国連と欧州連合（EU）の欧州委員が主催するオンラインハッカソンです。
今回のハッカソンのテーマは、「SDG#11 - Sustainable Cities & Communities」。

参加者は、10月3日~10月31日の期間で、7つのお題のうちの一つを選び、作品を制作し提出します。

```
1. Develop a City´s SDG scoring graph (都市のSDGsスコアリング・グラフの作成)
2. Development of apps based on OSM data（OSMのデータを使ったアプリ開発）
3. Tasking Manager（OSMのタスキングマネージャーの認証問題解決）
4. Disaster risk manager（災害リスク管理）
5. Translation challenge（翻訳）
6. OpenStreetMap website improvement challenge（OSMのウェブサイトの改善）
7. Open challenge（自由なチャンレジ）
```

_公式サイト_
https://ideas.unite.un.org/sdg11/Page/Overview

今回、ありがたい事にファイナリストに選んでいただき、オンラインの受賞セレモニーでスピーチする時間を頂きました。

自分のスピーチは、1:03:57 辺りからです。
https://youtu.be/JjHiG0DJTjM?t=3837

せっかくなので、スピーチでは話せなかった制作裏話というか、詳細についてこの記事で書いてみようと思います。

## 何を作ったか

津波3Dマップを作成しました。
自分が住んでいる、和歌山県串本町では、将来南海トラフ地震が予想されています。

政府が公開している予想データを元に、津波の浸水範囲・浸水深を、時系列で見ることができる3Dマップを開発しました。

![199149866-a43ad2fd-a184-4eef-ba09-023a507fcd88.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142797/20e3da16-a0ac-3a71-7a5f-a59422c8a52c.png)

**デモ**
https://naogify.github.io/nankai-trough-map/

## どう作ったか


### データ編

内閣府の Webサイトに「南海トラフの巨大地震モデル検討会」の資料が載っていました。
複数の震源地やケースを元に、南海トラフ地震の予測を行われていたので、こちらもとても興味深い資料です。
https://www.bousai.go.jp/jishin/nankai/model/


上記の検討会で話し合われた内容を元に作成したデータが以下にありました。
https://www.geospatial.jp/ckan/organization/naikakufu-01

今回は自分の住んでいる串本の、紀伊半島沖が震源地で、津波が堤防を乗り越えた時に堤防を破壊するというシミュレーションデータを使用しました。
```
・震源地が、「ケース②：「紀伊半島沖」に「大すべり域＋超大すべり域」」
・津波が堤防を乗り越えた時に破壊
```

_使用したデータ_
https://www.geospatial.jp/ckan/dataset/1211/resource/d2313be4-49ba-415b-b1cf-5a3ed82fe50c  
上記 URL でダウンロードした zip ファイルの中にある **浸水メッシュ_ケース02_堤防破堤_06系.csv** を使用。

各ケースや条件の詳細についてはこちらの資料をご参考ください
[南海トラフの巨大地震モデル検討会（第二次報告）津波断層モデル編](https://www.bousai.go.jp/jishin/nankai/model/pdf/20120829_2nd_report01.pdf)

### データ加工編
CSV の中身は以下の形式でした。

```
"経度","緯度","浸水深公表値（ｍ）","到達時間_01cm（秒）","到達時間_30cm（秒）","到達時間_01m（秒）","到達時間_03m（秒）","到達時間_05m（秒）","到達時間_10m（秒）","到達時間_20m（秒）","到達時間_30m（秒）","到達時間_40m（秒）","到達時間_最高水位（秒）","参考値：浸水深（m）","参考値：地殻変動後の標高（m）","参考値：隆起量（m）"
135.646143,34.833645,0,38481,-9999,-9999,-9999,-9999,-9999,-9999,-9999,-9999,43199,0.04,0.59,-0.14
135.646690,34.833647,1,37047,-9999,-9999,-9999,-9999,-9999,-9999,-9999,-9999,43175,0.06,0.58,-0.14
```

- 経度
- 緯度
- 浸水深公表値（ｍ）
- 到達時間_01cm（秒）
- 到達時間_30cm（秒）
- 到達時間_01m（秒）
- 到達時間_03m（秒）
- 到達時間_05m（秒）
- 到達時間_10m（秒）
- 到達時間_20m（秒）
- 到達時間_30m（秒）
- 到達時間_40m（秒）
- 到達時間_最高水位（秒）
- 参考値：浸水深（m）
- 参考値：地殻変動後の標高（m）
- 参考値：隆起量（m）


#### 点から、四角いポリゴンを作成

まず、ダウンロードした CSV を GeoJSON に変換して表示してみると以下の様になりました。
![point.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142797/7c0ab94e-a940-8a78-5919-d462f769ff6c.png)

これでは、津波の浸水深を Mapbox GL JS の `fill-extrusion` で表示することはできないので、点から 1辺が 10mのポリゴンを作成します。

![grid.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142797/3c0cbde4-f4f8-7727-0917-b44aa795bfbb.png)

この時メートルから、緯度経度への変換が必要なのですが、地球は丸いので緯度ごとに 1m の緯度が変わってきます。

これは、[meterstodegrees](https://github.com/eduardogspereira/meters-to-degrees)という npm モジュールを使って解決しました。基準となる緯度と指定した距離をメートルで渡すと対応する緯度もしくは経度を返してくれます。

更に JavaScript では小数点計算をした時に誤差が出てしまうので、[js-big-decimal](https://github.com/royNiladri/js-big-decimal)というモジュールを使って、小数点の計算をしています。

以下が、点から1辺が10mのポリゴンを生成するスクリプトです。

```
const meterstodegrees = require("meterstodegrees");
const bigDecimal = require('js-big-decimal');

function point2mesh(lat, lon, meters) {

    const baseLat = parseInt(lat);

    const latDegreesOf10m = meterstodegrees.latDegrees(baseLat, meters)
    const lonDegreesOf10m = meterstodegrees.lonDegrees(baseLat, meters)

    let lon1 = bigDecimal.add(`${lon}`, `${lonDegreesOf10m}`);
    let lat1 = bigDecimal.subtract(`${lat}`, `${latDegreesOf10m}`);

    // cast all values to float
    lat = parseFloat(lat);
    lon = parseFloat(lon);
    lat1 = parseFloat(lat1);
    lon1 = parseFloat(lon1);

    var coordinates = [
        [
            [lon, lat],
            [lon1, lat],
            [lon1, lat1],
            [lon, lat1],
            [lon, lat],
        ]
    ];

    return coordinates;
}
```

上の関数を、CSV から GeoJSON に変換する以下のスクリプト内で使用し、1辺が10mの四角いポリゴンを含む、GeoJSON を生成しました。
https://github.com/naogify/nankai-trough-map/blob/main/bin/csv2geojson.js

（ちなみに、全てのデータを使うと、スクリプトの処理時間が長すぎて計算すると数日かかる計算になったので、今回は事前に串本付近のデータだけ切り出しています）

そして作成した GeoJSON を、tippecannoe で `mbtiles` に変換します。

```
tippecanoe -zg -o nankai-trough-tsunami-v1.mbtiles -l g-simplestyle-v1 --drop-densest-as-needed ./data.geojson
```

### データ可視化・スタイル編

すこし苦労したのが、元データが その地点で 1cm になるのに何秒かかるかというデータの持ち方だったことです。時系列ごとの浸水深であればシンプルなのですが、少し工夫をして以下の様に、スライダーで選択された時間と、前後の浸水深の項目を比較して、`serFilter` で表示・非表示をしています。

まずは、各、浸水深の高さと色をレイヤーとしてセットします。

#### 浸水深のレイヤーをセット
```
map.addLayer({
  id: 'tsunami-1cm',
  type: 'fill-extrusion',
  source: 'nankai-trough',
  'source-layer': 'g-simplestyle-v1',
  paint: {
    'fill-extrusion-height': 0.01,
    'fill-extrusion-color': '#FFF323'
    'fill-extrusion-opacity': 0.8,
  }
}, 'poi');
```

#### 現在時刻によって、適切な浸水深のレイヤーを表示・非表示にするフィルターを用意

現在時刻が、ある浸水深の到達時間より大きいかつ、次に深い浸水深の到達時間より小さい（`もし次の到達時間の値が -9999の場合は、最高到達時間より小さい`）場合は、そのレイヤーを表示する様に、フィルターをセットします。

具体的な、上の条件をセットする関数は以下になります。
```
const compareNow = (arriveTime, nextArriveTime, currentTime) => {
  return [
    'all',
    [
      'all',
      ['!=', ['to-number', ['get', arriveTime]], -9999], // もし到達時間_30cm_s が -9999 なら
      ['<=', ['to-number', ['get', arriveTime]], currentTime], // 現在の時間が「到達時間_01cm_s」よりも大きい
    ],
    ['case',
      ['==', ['to-number', ['get', nextArriveTime]], -9999], // もし到達時間_30cm_s が -9999 なら
      ['>', ['to-number', ['get', '到達時間_最高水位_s']], currentTime], // 到達時間_最高水位_s と現在の時間を比較して小さいか
      ['>', ['to-number', ['get', nextArriveTime]], currentTime], // 到達時間_30cm_s と現在の時間を比較して小さければ
    ]
  ]
}
```

上の関数を以下の様に使います。
```
const filterBy = (currentTime) => {

  map.setFilter('tsunami-1cm', compareNow('到達時間_01cm_s', '到達時間_30cm_s', currentTime));
  map.setFilter('tsunami-30cm', compareNow('到達時間_30cm_s', '到達時間_01m_s', currentTime));
  map.setFilter('tsunami-1m', compareNow('到達時間_01m_s', '到達時間_03m_s', currentTime));
  map.setFilter('tsunami-3m', compareNow('到達時間_03m_s', '到達時間_05m_s', currentTime));
  map.setFilter('tsunami-5m', compareNow('到達時間_05m_s', '到達時間_10m_s', currentTime));
  map.setFilter('tsunami-10m', compareNow('到達時間_10m_s', '到達時間_20m_s', currentTime));
  map.setFilter('tsunami-20m', compareNow('到達時間_20m_s', '到達時間_30m_s', currentTime));
  map.setFilter('tsunami-30m', compareNow('到達時間_30m_s', '到達時間_40m_s', currentTime));
  map.setFilter('tsunami-40m', compareNow('到達時間_40m_s', '到達時間_最高水位_s', currentTime));
  map.setFilter('tsunami-highest', [
    'all',
    ['<=', ['to-number', ['get', '到達時間_最高水位_s']], currentTime], // 現在の時間が 到達時間_最高水位_s より大きいかつ、他の到達時間より大きい or 他の到達時間が -9999
    ['any', ['<=', ['to-number', ['get', '到達時間_01cm_s']], currentTime], ['==', ['to-number', ['get', '到達時間_01cm_s']], -9999]],
    ['any', ['<=', ['to-number', ['get', '到達時間_30cm_s']], currentTime], ['==', ['to-number', ['get', '到達時間_30cm_s']], -9999]],
    ['any', ['<=', ['to-number', ['get', '到達時間_01m_s']], currentTime], ['==', ['to-number', ['get', '到達時間_01m_s']], -9999]],
    ['any', ['<=', ['to-number', ['get', '到達時間_03m_s']], currentTime], ['==', ['to-number', ['get', '到達時間_03m_s']], -9999]],
    ['any', ['<=', ['to-number', ['get', '到達時間_05m_s']], currentTime], ['==', ['to-number', ['get', '到達時間_05m_s']], -9999]],
    ['any', ['<=', ['to-number', ['get', '到達時間_10m_s']], currentTime], ['==', ['to-number', ['get', '到達時間_10m_s']], -9999]],
    ['any', ['<=', ['to-number', ['get', '到達時間_20m_s']], currentTime], ['==', ['to-number', ['get', '到達時間_20m_s']], -9999]],
    ['any', ['<=', ['to-number', ['get', '到達時間_30m_s']], currentTime], ['==', ['to-number', ['get', '到達時間_30m_s']], -9999]],
    ['any', ['<=', ['to-number', ['get', '到達時間_40m_s']], currentTime], ['==', ['to-number', ['get', '到達時間_40m_s']], -9999]],
  ]);
}
```

これにより、以下の様に現在時刻により、それぞれのレイヤーを表示します。

![demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142797/1b07b88d-2353-dbb1-6333-4272907e8099.gif)



## まとめ

説明しきれない箇所もあったと思いますが、コードは以下のリポジトリにあるのでご参考ください。データ加工に使ったスクリプトは `bin` ディレクトリ内に入れています。

https://github.com/naogify/nankai-trough-map


OSS4SDGの主催の皆様このような機会をありがとうございました！特にハッカソンについて教えていただいた@T-ubuさん、ありがとうございます。

もしまた今後も開催される様でしたら、ぜひ皆さんも参加してみてください。


## アトリビューション

- [「内閣府 南海トラフの巨大地震モデル検討会」（津波断層モデル(11)津波浸水深データ（ケース1）ケース02_堤防破堤）](https://www.geospatial.jp/ckan/dataset/1211)
- [OpenStreetMap](https://www.openstreetmap.org/copyright)
- [GSI Japan](https://www.gsi.go.jp/)
- [Geolonia,Inc.](https://geolonia.com/)