Sinatra
GIS
オープンデータ
PostGIS
気象庁

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

先日、天気予報に用いられる地域区分である予報区に関するGISデータが、気象庁からオープンデータとして公開されました。→予報区等GISデータの一覧

以前は、別のソースから予報区の地理情報を作る必要がありましたが、今回シェープファイルで公開された事で気軽にアプリケーションを作れるようになりました:yum:

ということで、PostGISの勉強を兼ねて、地図上をクリックしたらそこの予報区(一次細分区域)をポップアップするという小さなアプリを作ってみました。

ezgif-1-f5a7bab1bcc6.gif


はじめに

構成

DB: PostgreSQL + PostGIS

サーバーサイド: Sinatra

フロント: leaflet.js + jQuery

環境


  • macOS mojave 10.14

  • psql (PostgreSQL) 11.1


  • PostGIS 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エラー問題

$ 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データの一覧の「一次細分区域等」のリンクを踏んでシェープファイルの入ったZipを落としてきます。

(2019/02/04現在は「20190125_AreaForecastLocalM_1saibun_GIS.zip」というファイル)

解凍するとシェープを構成する3つのファイルが入っています。

スクリーンショット 2019-02-05 2.26.03.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ができました。

\dtSELECT name from districts; あたりで確認してみましょう。QGISを入れている方は、そちらから接続して確認するとわかりやすいかと思います。

スクリーンショット 2019-02-05 3.03.47.png


サーバーサイド

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

gemfileはこちら

bundle install --path vendor/bundleをお忘れなく


gemfile

gem 'sinatra'

gem 'sinatra-contrib'
gem 'pg'
gem 'activerecord'
gem 'dotenv'

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


.env

myadapter=postgresql

myusername={ユーザー名}
mypassword={設定してれば}
mydatabase=weather_forecast_map

app.rbはこちら


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を置いているだけです。


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の同期処理を使っていて、手抜き感があるのはご愛嬌。


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はこれだけ


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

点を包含する面を探すSQL文

PostgreSQL で緯度経度から住所を検索する -present

shp2pgsqlについて

PostGIS データの格納と表示 -Let's Postgres

座標系とSRIDの概要を知る

空間参照系の概要 -Qiita