先日、天気予報に用いられる地域区分である予報区に関するGISデータが、気象庁からオープンデータとして公開されました。→予報区等GISデータの一覧
以前は、別のソースから予報区の地理情報を作る必要がありましたが、今回シェープファイルで公開された事で気軽にアプリケーションを作れるようになりました
ということで、PostGISの勉強を兼ねて、地図上をクリックしたらそこの予報区(一次細分区域)をポップアップするという小さなアプリを作ってみました。
はじめに
構成
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」というファイル)
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を入れている方は、そちらから接続して確認するとわかりやすいかと思います。
サーバーサイド
サーバーサイドは小さなSinatraのWebアプリなので一気に実装します。
gemfileはこちら
bundle install --path vendor/bundle
をお忘れなく
gem 'sinatra'
gem 'sinatra-contrib'
gem 'pg'
gem 'activerecord'
gem 'dotenv'
dotenvを使って.envに環境変数を出しています。
myadapter=postgresql
myusername={ユーザー名}
mypassword={設定してれば}
mydatabase=weather_forecast_map
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を置いているだけです。
<!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の同期処理を使っていて、手抜き感があるのはご愛嬌。
$(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はこれだけ
#mapid { height: 600px; }
index.rbはviews
ディレクトリへ、index.jsとmaster.cssはpublic
ディレクトリへそれぞれ格納します。
ここまで来たら bundle exec ruby app.rb
で起動し、ブラウザでhttp://localhost:4567/
へアクセス!!
最後に
冒頭のgifのように動いたでしょうか?
動かない場合はDBにちゃんとデータが入っているか、ポスグレとSinatraがちゃんと繋ぎ込めているかあたりを確認すると良さそうです。
冒頭で説明した通り気象庁が予報区のシェープファイルをオープンデータ化してくれたことで、GIS初心者でも簡単に利用することができました。ありがとうございます
気象庁では、予報区ごとの天気予報のXMLフィードも公開しているので、それと合わせて、簡単な天気予報アプリが作れそうです。
記事内に誤り等ありましたら、コメントや編集リクエストをよろしくお願いします!!
参考にした記事
activerecordの概要を知る
Rails で PostGIS を使う方法(Docker, Heroku) -Qiita
点を包含する面を探すSQL文
PostgreSQL で緯度経度から住所を検索する -present
shp2pgsqlについて
PostGIS データの格納と表示 -Let's Postgres
座標系とSRIDの概要を知る
空間参照系の概要 -Qiita