気象庁のオープンデータとPostGIS+Sinatraで、地図から予報区を判定する。

先日、天気予報に用いられる地域区分である予報区に関するGISデータが、気象庁からオープンデータとして公開されました。→[予報区等GISデータの一覧](https://www.data.jma.go.jp/developer/gis.html)
以前は、別のソースから予報区の地理情報を作る必要がありましたが、今回シェープファイルで公開された事で気軽にアプリケーションを作れるようになりました:yum:

ということで、PostGISの勉強を兼ねて、地図上をクリックしたらそこの予報区(一次細分区域)をポップアップするという小さなアプリを作ってみました。
![ezgif-1-f5a7bab1bcc6.gif](https://qiita-image-store.s3.amazonaws.com/0/159912/8677df27-37b4-dcc6-8c06-ea37641bae5e.gif)

# はじめに
**構成**
DB: PostgreSQL + PostGIS
サーバーサイド: Sinatra
フロント: leaflet.js + jQuery

**環境**

* macOS mojave 10.14
* psql (PostgreSQL) 11.1
* [PostGIS](https://postgis.net/) 2.5.1
* ruby 2.5.1
* Sinatra 2.0.5

# 作っていく
## DBのセットアップ
#### PostgreSQLのセットアップ。
ここでデータベースまで作ってしまいます。

```
$ brew install postgresql

# バージョン確認
$ psql --version

#起動
$ pg_ctl -D /usr/local/var/postgres start

#停止
$ pg_ctl stop

# DBを作って確認。名前はお好きに
$ createdb weather_forecast_map
$ psql -l
```

`echo 'export PGDATA="/usr/local/var/postgres"' >> ~/.bash_profile` でデフォルトのデータの位置を指定してやると、起動時に`-D`オプションがいらなくなります。 `source .bash_profile`をお忘れなく

#### 続いてPostGISもセットアップ

`brew install`時に、Linkが切れているというwarningが出ましたが、↓の記事を参考にディレクトリ作って再度installしたら入りました。[Homebrewで入れたPythonでのlinkエラー問題](https://qiita.com/Jung0/items/d4012814e6fb1b694208)

```
$ brew install postgis

# DBに入ってEXTENSIONを作成
$ psql weather_forecast_map
weather_forecast_map=# CREATE EXTENSION postgis;

# バージョン確認
weather_forecast_map=# SELECT PostGIS_Version();

# 抜ける
weather_forecast_map=# \q
```

### DBにデータを入れ込む

[予報区等GISデータの一覧](https://www.data.jma.go.jp/developer/gis.html)の「一次細分区域等」のリンクを踏んでシェープファイルの入ったZipを落としてきます。
(2019/02/04現在は「20190125_AreaForecastLocalM_1saibun_GIS.zip」というファイル)

解凍するとシェープを構成する3つのファイルが入っています。
<img width="384" alt="スクリーンショット 2019-02-05 2.26.03.png" src="https://qiita-image-store.s3.amazonaws.com/0/159912/8ca115a0-6a8b-3976-c2e7-90cd9498c207.png">

PostGISと一緒に入ってくる`shp2pgsql`というコマンドを使って、このシェープファイルを、ジオメトリー型としてテーブルにインサートしていくSQLファイルを発行します。

```
shp2pgsql -W utf-8 -D -I -s 6668 さっきのZipを解凍したディレクトリ/一次細分区域等.shp > 任意の作業ディレクトリ/insert.sql
```

`-W`→読むファイルの文字コードを指定しています。
`-D`→インサートするデータをダンプ型にします(インサートが早くなる。らしい)。
`-I`→空間インデックスを貼るSQLを最後に発行します。
`-s`→シェープファイルの座標系を指定します。先ほどのページ曰く
>データは世界測地系（JGD2011）で作成しています。

とのことなので、SRID6668を指定します。

データベースを起動し、生成されたSQLファイルを読み込み、テーブルを作ります。

```
$ psql weather_forecast_map
weather_forecast_map=# \i insert.sql

# 出来上がったテーブル名が扱いずらいので適当に改名
weather_forecast_map=# ALTER TABLE 一次細分区域等 RENAME TO districts;
```

これで予報区の面データが入ったDBができました。
`\dt`や`SELECT name from districts;` あたりで確認してみましょう。QGISを入れている方は、そちらから接続して確認するとわかりやすいかと思います。

<img width="1420" alt="スクリーンショット 2019-02-05 3.03.47.png" src="https://qiita-image-store.s3.amazonaws.com/0/159912/e2d70927-51b5-3fdd-69da-79c6765c9841.png">


## サーバーサイド
サーバーサイドは小さなSinatraのWebアプリなので一気に実装します。

gemfileはこちら
`bundle install --path vendor/bundle`をお忘れなく

```ruby:gemfile
gem 'sinatra'
gem 'sinatra-contrib'
gem 'pg'
gem 'activerecord'
gem 'dotenv'
```

dotenvを使って.envに環境変数を出しています。

```plaintext:.env
myadapter=postgresql
myusername={ユーザー名}
mypassword={設定してれば}
mydatabase=weather_forecast_map
```

app.rbはこちら

```ruby:app.rb
require 'sinatra'
require 'sinatra/reloader'
require 'active_record'
require 'dotenv/load'
require 'json'

class District < ActiveRecord::Base
    establish_connection(
        adapter:  ENV['myadapter'],
        host:     "",
        username: ENV['myusername'],
        password: "",
        database: ENV['mydatabase']
    )
end

get '/' do
  erb :index
end

get '/district' do
  lat = params['lat']
  lng = params['lng']
  record = District.where("ST_Within(ST_GeomFromText('POINT(#{lng} #{lat})', 6668),geom)").first

  if record
    response = {
      exist: true,
      district: record['name']
    }
  else
    response = {
      exist: false,
      district: ""
    }
  end
  return response.to_json
end
```

`/`にリクエストされると後ほど作るindex.erbを返します。
また`/district`にリクエストされると、そのリクエストの`lat`パラメータ、`lng`パラメータに応じてDBを検索し、結果を小さなJSONにして返します。

#### SQL文について
`/district`にリクエストがあるとそのパラメータに応じ、active_recordを通じて先ほど作ったDBに対し `SELECT * FROM weather_forecast_map WHERE ST_Within(ST_GeomFromText('POINT(#{lng} #{lat})', 6668),geom)`というSQL文を発行します。

`ST_Within`はPostGISが持つ関数で、与えられた点を包含する面を検索します。引数の順番がlng(経度)→lat(緯度)であることに注意してください。(私は何回もハマりました...)
例えばこんな感じ

```
weather_forecast_map=# SELECT name FROM districts WHERE ST_Within(ST_GeomFromText('POINT(138.780615 35.486829)', 6668),geom);
      name      
----------------
 東部・富士五湖
(1 row)
```

## フロント
こちらも変わったことはしていないのでサクッと作ります。
HTMLでは必要なCSSとJSを呼び、leaflet.jsが地図を描画するdivを置いているだけです。

```html:index.erb
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.4.0/dist/leaflet.css"/>
    <link rel="stylesheet" href="master.css">
    <title>weather_forecast_map</title>
</head>
<body>
   <div id="mapid"></div>
   <script src="https://unpkg.com/leaflet@1.4.0/dist/leaflet.js"></script>
   <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
   <script src="index.js"></script>
</body>
</html>
```

続いてJSです。leflet.jsを使い、地理院地図を背景タイルとして呼んでいます。
内容はコメントを参照してください。jQueryの同期処理を使っていて、手抜き感があるのはご愛嬌。

```javascript:index.js
$(function(){
//leaflet.jsが地図を描画する設定
  var mymap = L.map('mapid').setView([35.689480, 139.691834], 7);

  L.tileLayer('https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png', {
    attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
    maxZoom: 18,
  }).addTo(mymap);

  var marker;

//以下、地図がクリックされたらサーバーサイドをコールし、該当予報区または「該当なし」をポップアップするイベントを追加
  var addMarker = function(lat, lng, message){
    marker = L.marker([lat, lng]);
    marker.setLatLng([lat, lng]).bindPopup('<p>' + message + '</p>').addTo(mymap).openPopup();
  }

  function getForecastDistrict(lat, lng){
    var result = $.ajax({
      type: 'GET',
      url: 'http://localhost:4567/district',
      dataType: 'json',
      async: false,
      data: {
        lat: lat,
        lng: lng
      }
    }).responseText;
    return JSON.parse(result);
  }

  //クリックイベント
  mymap.on('click', function(e) {
    // すでにマーカーがあったら消す
    if (marker !== undefined) {
      mymap.removeLayer(marker)
    }

    //クリック位置の経緯度取得
    lat = e.latlng.lat;
    lng = e.latlng.lng;
    response = getForecastDistrict(lat, lng);

    if (response.exist) {
      addMarker(lat, lng, response.district);
    } else {
      addMarker(lat, lng, "該当無し");
    }
  });
});
```

CSSはこれだけ

```css:master.css
#mapid { height: 600px; }
```

index.rbは`views`ディレクトリへ、index.jsとmaster.cssは`public`ディレクトリへそれぞれ格納します。

ここまで来たら `bundle exec ruby app.rb`で起動し、ブラウザで`http://localhost:4567/`へアクセス!!

# 最後に
冒頭のgifのように動いたでしょうか?
動かない場合はDBにちゃんとデータが入っているか、ポスグレとSinatraがちゃんと繋ぎ込めているかあたりを確認すると良さそうです。

冒頭で説明した通り気象庁が予報区のシェープファイルをオープンデータ化してくれたことで、GIS初心者でも簡単に利用することができました。ありがとうございます:clap:
気象庁では、予報区ごとの天気予報のXMLフィードも公開しているので、それと合わせて、簡単な天気予報アプリが作れそうです。

記事内に誤り等ありましたら、コメントや編集リクエストをよろしくお願いします!!


# 参考にした記事
activerecordの概要を知る
[Rails で PostGIS を使う方法（Docker, Heroku） -Qiita](https://qiita.com/mochizukikotaro/items/1385604bc7e63fd97770)

点を包含する面を探すSQL文
[PostgreSQL で緯度経度から住所を検索する -present](https://tnakamura.hatenablog.com/entry/2017/10/04/postgis)

shp2pgsqlについて
[PostGIS データの格納と表示 -Let's Postgres](https://lets.postgresql.jp/documents/tutorial/PostGIS/3)

座標系とSRIDの概要を知る
[空間参照系の概要 -Qiita](https://qiita.com/boiledorange73/items/b98d3d1ef3abf7299aba)