PostGISでODTPの駅情報を描画する

先週始めたアルバイト？インターン？で、ＧＩＳソフトを用いたので、復習を兼ねてODPTデータに含まれる駅を地図上に描画します。
PostGIS など、学びたての為、誤りを含んでいる可能性があります。間違いが含まれている場合は、お手数ですがコメント又は修正して頂けると幸いです。

## 環境

今回用いる環境は、次の通りです。

- Windows 10 Home（クライアント）
- CentOS 7（サーバー）

また、利用するソフトは次の通りです。

- pgrouting （ドッカーコンテナイメージで利用。サーバー側で使用。）
- pgAdmin4 （CentOS側に入れてもいいとは思うのですが、Winedowsに入れました。）

予め、これらのは利用できるようになっているとします。

## PostgreSQL へのデータ登録

路線情報をデータベースにインポートしていきます。
今回、データインポートは、Goを用いて行いました。
``apikey``には、[ODTP](https://tokyochallenge.odpt.org/)から取得したAPIキーを利用してください。
また、``dbinfo``には、PostgreSQLのデータベース情報を入力します。

ODPT の APIから JSON を取得し、 PostgreSQL に登録するプログラムです。すでにテーブルが存在する場合、削除して作り直しますので、ご注意ください。

※ 正しく動作しない場合は、お手数ですがコメントください。

```go
package main

import (
	"time"
	"strings"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"bytes"
	"database/sql"
	_ "github.com/lib/pq"
)

// go get github.com/lib/pq

const apikey = "[ODPT API Key]"
const dbinfo = "host=[hostname] port=[dbport] user=[username] password=[password] dbname=[dbname] sslmode=disable"

type Operator struct {
	Id       string    `json:"@id"`
	Type     string `json:"@type"`
	Date     string `json:"dc:date"`
	Context	 string `json:"@context"`
	Title	 string `json:"dc:title"`
	SameAs	 string `json:"owl:sameAs"`
}
type Railway struct {
	Id       string `json:"@id"`
	Type     string `json:"@type"`
	Date     string `json:"dc:date"`
	Context	 string `json:"@context"`
	Title	 string `json:"dc:title"`
	SameAs	 string `json:"owl:sameAs"`
	LineCode string `json:"odpt:lineCode"`
	Operator string `json:"odpt:operator"`
	StationOder []StationOderElement `json:"odpt:stationOrder"`
}
type StationOderElement struct{
	Index	int `json:"odpt:index"`
	Station string `json:"odpt:station"`
}
type Station struct{
	Id       string     `json:"@id"`
	Type     string 	`json:"@type"`
	Date     string 	`json:"dc:date"`
	Lat		 float64	`json:"geo:lat"`
	Context  string		`json:"@context"`
	Title	 string 	`json:"dc:title"`
	Lon		 float64	`json:"geo:long"`
	SameAs	 string 	`json:"owl:sameAs"`
	Railway  string		`json:"odpt:railway"`
	Operator string		`json:"odpt:operator"`
	StationCode string	`json:"odpt:stationCode"`
}

func get_operator(){
	// Connect Postgre SQL
	db, err := sql.Open("postgres", dbinfo)
	defer db.Close()
	if err != nil {
		fmt.Println(err)
	}

	// Create table
	db.Query("DROP TABLE odpt_operator;")
	db.Query("CREATE TABLE odpt_operator (Id text,Type text,Date text,Context text,Title text,SameAs text)")
	// Create table
	db.Query("DROP TABLE odpt_station;")
	db.Query("CREATE TABLE odpt_station (Id text,Type text,Date text,Lat real,Context text,Title text,Lon real,SameAs text,Railway text,Operator text,StationCode text)")

	// Access web
	url := "https://api-tokyochallenge.odpt.org/api/v4/odpt:Operator?acl:consumerKey="+apikey;
	res, err := http.Get(url)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	body, err := ioutil.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	buf := bytes.NewBuffer(body)
	text := buf.String()

	// JSON Decoding
	var ttttt []Operator
	if err := json.Unmarshal(([]byte)(text), &ttttt); err != nil {
		log.Fatal(err)
	}
	for _, p := range ttttt {
		// insert
		db.Exec("insert into odpt_operator (Id,Type,Date,Context,Title,SameAs) values ($1,$2,$3,$4,$5,$6)",p.Id, p.Type,p.Date,p.Context,p.Title,p.SameAs)
		fmt.Printf("%s->%s\n", p.SameAs,p.Title)
		time.Sleep(time.Second*1)
		get_station(p.SameAs)
	}
	
	// SELECT
	rows, err := db.Query("SELECT SameAs,Title FROM odpt_operator")

	if err != nil {
			fmt.Println(err)
	}

	for rows.Next() {
		var e Operator_
		rows.Scan(&e.SameAs, &e.Title)
		//			es = append(es, e)
		e.SameAs=strings.TrimSpace(e.SameAs)
		e.Title=strings.TrimSpace(e.Title)
		fmt.Printf("%s_%s\n", e.SameAs,e.Title)
	}
}

func get_station(operator string){
	// Connect Postgre SQL
	db, err := sql.Open("postgres", dbinfo)
	defer db.Close()
	if err != nil {
		fmt.Println(err)
	}

	// Access web
	url := "https://api-tokyochallenge.odpt.org/api/v4/odpt:Station?odpt:operator="+operator+"&acl:consumerKey="+apikey;
	res, err := http.Get(url)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	body, err := ioutil.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	buf := bytes.NewBuffer(body)
	text := buf.String()

	fmt.Printf(text)
	// JSON Decoding
	var ttttt []Station
	if err := json.Unmarshal(([]byte)(text), &ttttt); err != nil {
		log.Fatal(err)
	}
	for _, p := range ttttt {
		// insert
		db.Exec("insert into odpt_station (Id,Type,Date,Lat,Context,Title,Lon,SameAs,Railway,Operator,StationCode) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)",p.Id, p.Type,p.Date,p.Lat,p.Context,p.Title,p.Lon,p.SameAs,p.Railway,p.Operator,p.StationCode)
	}
}

func get_railway(){
	// Connect Postgre SQL
	db, err := sql.Open("postgres", dbinfo)
	defer db.Close()
	if err != nil {
		fmt.Println(err)
	}

	// Create table
	db.Exec("DROP TABLE odpt_railway;")
	db.Exec("CREATE TABLE odpt_railway (Id text,Type text,Date text,Context text,Title text,SameAs text,LineCode text,Operator text)")
	db.Exec("DROP TABLE odpt_railway_edge;")
	db.Exec("CREATE TABLE odpt_railway_edge (StationFromIndex integer,StationFrom text,StationTo text,Railway text,isoutbound boolean)")

	// Access web
	url := "http://api-tokyochallenge.odpt.org/api/v4/odpt:Railway?acl:consumerKey="+apikey;
	res, err := http.Get(url)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	body, err := ioutil.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	buf := bytes.NewBuffer(body)
	text := buf.String()

	// JSON Decoding
	var ttttt []Railway
	if err := json.Unmarshal(([]byte)(text), &ttttt); err != nil {
		log.Fatal(err)
	}
	for _, p := range ttttt {
		// insert
        db.Exec("insert into odpt_railway (Id,Type,Date,Context,Title,SameAs,LineCode,Operator) values ($1,$2,$3,$4,$5,$6,$7,$8)",p.Id, p.Type,p.Date,p.Context,p.Title,p.SameAs,p.LineCode,p.Operator)

		for ind := range p.StationOder{
			if ind==0{
				continue
			}
			// insert
			station1 := p.StationOder[ind-1].Station
			station2 := p.StationOder[ind].Station
			station1index := p.StationOder[ind-1].Index
			station2index := p.StationOder[ind].Index

			_, err1 := db.Exec("insert into odpt_railway_edge (StationFromIndex,StationFrom,StationTo,Railway,isoutbound) values ($1,$2,$3,$4,$5)", station1index,station1,station2,p.SameAs,true)
			if err1 != nil {
				log.Fatal(err1)
			}
			_, err2 := db.Exec("insert into odpt_railway_edge (StationFromIndex,StationFrom,StationTo,Railway,isoutbound) values ($1,$2,$3,$4,$5)", station2index,station2,station1,p.SameAs,false)
			if err2 != nil {
				log.Fatal(err2)
			}
		}
	}
	
/*	// SELECT
	rows, err := db.Query("SELECT SameAs,Title FROM odpt_railway")

	if err != nil {
			fmt.Println(err)
	}

	for rows.Next() {
		var e Operator_
		rows.Scan(&e.SameAs, &e.Title)
		//			es = append(es, e)
		e.SameAs=strings.TrimSpace(e.SameAs)
		e.Title=strings.TrimSpace(e.Title)
		fmt.Printf("%s_%s\n", e.SameAs,e.Title)
	}*/
}

func main() {
	get_operator()
	get_railway()
}

type Operator_ struct{
	SameAs string
	Title string
}
```

実行すると、下の画像のように、データベースに登録されます。

![](https://gyazo.com/3f88293672e32d0f80bf737432fcb0ea.png)

このプログラムでは、次のようなテーブルを作成します。

|テーブル名|表すもの|
|---|---|
|odpt_operator|運行会社情報|
|odpt_station|駅情報|
|odpt_railway|路線情報|
|odpt_railway_edge|駅接続情報|

## Post GIS のインストール

続けて、Post GIS のインストールを行っていきます。
データベースで次のSQLを実行し、PostGISをインストールします。

```sql
CREATE EXTENSION postgis;
```
## Geo情報の作成

PostGIS では、geometryという地理空間情報１つで記憶されています。なので、緯度や経度といった情報をgeometry型に変換します。geometry型には、次のような種類があります。

|名称|形式|表しているもの|
|---|---|---|
|POINT|(0,0)|単一の点|
|LINESTRING|(0 0,1 1,1 2)|単一の折れ線|
|POLYGON|((0 0,4 0,4 4,0 4,0 0),(1 1, 2 1, 2 2, 1 2,1 1))|単一の多角形|
|MULTIPOINT|((0 0),(1,2))|複数点|
|MULTILINESTRING|((0 0,1 1,1 2),(2 3,3 2,5 4))|複数線|
|MULTIPOLYGON|(((0 0,4 0,4 4,0 4,0 0),(1 1,2 1,2 2,1 2,1 1)), ((-1 -1,-1 -2,-2 -2,-2 -1,-1 -1)))|複数多角形|
|GEOMETRYCOLLECTION|(POINT(2 3),LINESTRING(2 3,3 4))|ジオメトリ コレクション|

※[www.finds.jp](https://www.finds.jp/docs/pgisman/2.3.0/using_postgis_dbmanagement.html#RefObject)より。[Qiita](https://qiita.com/boiledorange73/items/6d1200b69b5d5c88bcd0)でもまとめてくださっている方がいます。

また、これらのデータを扱うことのできる関数が複数用意されています。

様々な関数一覧は、[PostGISリファレンス](https://www.finds.jp/docs/pgisman/1.5.1/reference.html)をご覧ください。

## Geometry の付与

駅情報への Geometry を付与します。Geometry の付与は、ST_SetSRID関数を利用することで、下記の SQL 一行で行えます。

```sql
SELECT SameAs, ST_SetSRID(ST_Point(lon, lat), 4326) FROM odpt_station;
```

SELECT された結果を開いてみましょう。

![](https://gyazo.com/162c6538aababa0f6a43377c0b5b6f6d.png)

さらに、列名の右側に表示されている目のようなアイコンをクリックすると、下記のように、地図上にプロットされます。

![](https://gyazo.com/e372eb247009fa8f04750732dad2d103.png)

首都圏を拡大してみましょう。

![](https://gyazo.com/72033a584b99e22c0a9aaee26c6f27a0.jpg)

駅が正しく描画されていることが分かります。

なお、今回は省略しますが、``ST_MakeLine``などの関数を用いることで、路線の線を描画することもできます。

## GTFS もプロットしてみる

因みに、GTFSデータをインポートすることで、GTFSをマップ上に描画することもできます。データベースへのインポート手順は[GTFSファイルをマージしてPostgreSQL+PostGISに投入する方法](https://qiita.com/niyalist/items/22b743d973e4a6aeef86)などをご覧ください。GTFSの場合は、下のようなSQLで、 Geometry を付与したテーブルを作成することができます。

```sql
AS SELECT stop_id, ST_SetSRID(ST_Point(stop_lon, stop_lat), 4326) FROM stops;
```

オーストラリア・ブリスベンの TransLink の GTFS に含まれる stop をプロットすると、次のようにして描画されます。

#### 縮小図

![](https://gyazo.com/b0f6c3d09d733668bd08a70b547b2849.jpg)

#### 拡大図

![](https://gyazo.com/12f553c031a4bc95dde94e1d99415901.jpg)

データベースからWebクライアント上でそのまま描画できて便利ですね。