14
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ヴァル研究所Advent Calendar 2018

Day 23

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

Last updated at Posted at 2019-02-04

先日、天気予報に用いられる地域区分である予報区に関する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

14
17
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?