Golangによるosm.pbf読み込み2
前回の記事では、osm.pbf(OpenStreetMapのデータ)の件数をカウントするプログラムを動作させました。
今回はこのプログラムを改修して、GeoJSONファイルを出力するプログラムを作成したいと思います。
GeoJSONとは
GeoJSONは、JSONの仕様を拡張して図形(ジオメトリ)や属性付きの図形(フィーチャー)を収容できるテキストファイルの仕様です。以下、GISオープン教材からの引用による説明です。
GeoJSONは、JavaScript Object Notation (JSON) を基とした、GISデータを記述するためのフォーマットです(地理空間データ交換フォーマット)。この形式では、Point, LineString, Polygon, MultiPoint, MultiLineString,MultiPolygon,GeometryCollectionをサポートしています。軽量言語であり、Web GISでの利用例が多く見られます。
「GIS実習オープン教材」GeoJSONの章より引用
https://gis-oer.github.io/gitbook/book/materials/web_gis/GeoJSON/GeoJSON.html
GeoJSONの例を以下に示します。
2〜4行目が線要素、5〜7行目が点要素(注記)を表しています。
{"type":"FeatureCollection","features": [ {"type":"Feature", "geometry":{"type":"LineString","coordinates":[[135.4979582,34.6868559],[135.4979642,34.6866337],[135.4979665,34.686527],[135.4979904,34.6854483],[135.4979914,34.6852815],[135.4980065,34.6841434],[135.4980094,34.6836657],[135.4980227,34.682807],[135.4980223,34.6827652],[135.4980176,34.6822954],[135.497997,34.6816507],[135.4979905,34.6815066]]}, "properties":{"Layer":"1104","Elno":"06OC884-1104-0001"}}, {"type":"Feature", "geometry":{"type":"Point","coordinates":[135.4517403,34.6628267]}, "properties":{"Layer":"8162","Elno":"06OC971-8162-0167","Text":"夕凪公園","Vnflag":"0","Angle":"0"}},
GeoJSONファイルは、QGISなどのディスクトップGISにてシェープファイルなどと同様に取り扱えます。また、ベクトルタイル(mbtiles)への変換もツールを使って容易にできます。
仕様の説明は、以下がわかりやすいと思います。
https://ja.wikipedia.org/wiki/GeoJSON
GeoJSON出力プログラム
では、早速プログラムを確認しましょう。
今回は、日本全国のosm.pbf(japan-latest.osm.pbf)のNode(点)要素の学校(amenityタグ=school)を抽出し、GeoJSONファイルに変換するプログラムを作成します。
ソースコードは以下の通りです。
package main import ( "fmt" "io" "log" "os" "runtime" "strings" "time" "github.com/qedus/osmpbf" ) func main() { // 開始時刻の表示 fmt.Println("Start:", time.Now()) // ターゲットのosm.pbfをオープン //f, err := os.Open("/Users/takamotokeiji/data/osm.pbf/shikoku-latest.osm.pbf") f, err := os.Open("/Users/takamotokeiji/data/osm.pbf/japan-latest.osm.pbf") if err != nil { log.Fatal(err) } defer f.Close() // ファイルを書き込み用にオープン (mode=0666) file, err := os.Create("./output.json") if err != nil { log.Fatal(err) } defer file.Close() // FeatureCollectionレコード(ヘッダー的なもの)を出力 file.WriteString("{\"type\":\"FeatureCollection\",\"features\":[\n") d := osmpbf.NewDecoder(f) // use more memory from the start, it is faster d.SetBufferSize(osmpbf.MaxBlobSize) // start decoding with several goroutines, it is faster err = d.Start(runtime.GOMAXPROCS(-1)) if err != nil { log.Fatal(err) } var nc, wc, rc uint64 var endl bool for { if v, err := d.Decode(); err == io.EOF { break } else if err != nil { log.Fatal(err) } else { switch v := v.(type) { case *osmpbf.Node: // Node(点要素)の場合の処理 if v.Tags["amenity"] == "school" { // 最後のレコード出力時にはカンマを出力しない if endl { file.WriteString(",\n") } else { endl = true } // 要素情報の出力 file.WriteString("{\"type\":\"Feature\",\"geometry\":{\"type\":\"Point\",") file.WriteString(fmt.Sprintf("\"coordinates\":[%.7f,%.7f]}", v.Lon, v.Lat)) // 属性文字のエスケープ関連文字の訂正 if strings.Contains(v.Tags["name"], "\\") { file.WriteString(fmt.Sprintf(",\"properties\":{\"name\":\"%s\"}}", strings.Replace(v.Tags["name"], "\\", "", -1))) } else if strings.Contains(v.Tags["name"], "\n") { file.WriteString(fmt.Sprintf(",\"properties\":{\"name\":\"%s\"}}", strings.Replace(v.Tags["name"], "\n", "", -1))) } else if strings.Contains(v.Tags["name"], "\"") { file.WriteString(fmt.Sprintf(",\"properties\":{\"name\":\"%s\"}}", strings.Replace(v.Tags["name"], "\"", " ", -1))) } else { file.WriteString(fmt.Sprintf(",\"properties\":{\"name\":\"%s\"}}", v.Tags["name"])) } } // Process Node v. nc++ case *osmpbf.Way: // Process Way v. wc++ case *osmpbf.Relation: // Process Relation v. rc++ default: log.Fatalf("unknown type %T\n", v) } } } // FeatureCollection終端を出力 file.WriteString("]}\n") // 要素数の表示 fmt.Printf("Nodes: %d, Ways: %d, Relations: %d\n", nc, wc, rc) // 終了時刻の表示 fmt.Println("End:", time.Now()) }
以降、ポイントを解説します。
ファイルの作成
Goでのファイルの作成は、以下のように行います。
// ファイルを書き込み用にオープン (mode=0666) file, err := os.Create("./output.json") if err != nil { log.Fatal(err) } defer file.Close()
関数Call(os.Open)時に、ファイルディスクリプタf
とエラーerr
が同時に取得できます。
:=
は、変数宣言と代入を同時に行う演算子です。- 事前にvarで宣言している場合は=で代入します。
log.Fatal(err)
は、コンソールにエラーを出力してプログラムを終了(os.Exit(1)を呼び出し)します。defer file.Close()のdefer
は、プログラム終了時に実行する処理を指定します。
osm.pbfのオープンとパース
osm.pbfのパース(変換とか翻訳とかいった意味です)にはライブラリqedus/osmpbf
を使います。
パースにはDecoderを生成し、デコーダのイテレータ的な関数(Decode)を使って1件づつ読み取ります。
f, err := os.Open("/Users/takamotokeiji/data/osm.pbf/japan-latest.osm.pbf") d := osmpbf.NewDecoder(f) // start decoding with several goroutines, it is faster err = d.Start(runtime.GOMAXPROCS(-1)) if err != nil { log.Fatal(err) } var nc, wc, rc uint64 var endl bool for { if v, err := d.Decode(); err == io.EOF { break } else if err != nil { log.Fatal(err) } else { switch v := v.(type) {
- os.Openにてosm.pbfファイルをオープン
- osmpbfにてデコーダを作成しStart関数を呼び出し(呼び出し時点でパースは並列実行されています)
- デコーダのDecode関数(イテレータ)により、変数vにosm.pbfの要素を取得
GeoJSONファイルの出力
今回は、GeoJSONファイル出力をベタにWriteStringにて実装しています。
GeoJSONは、FeatureCollection配下のFeature(要素)にGeometry(座標)やproperty(属性)を保有しますので、以下のようにファイルに出力します。
// FeatureCollectionレコード(ヘッダー的なもの)を出力 file.WriteString("{\"type\":\"FeatureCollection\",\"features\":[\n") // 要素情報の出力 file.WriteString("{\"type\":\"Feature\",\"geometry\":{\"type\":\"Point\",") file.WriteString(fmt.Sprintf("\"coordinates\":[%.7f,%.7f]}", v.Lon, v.Lat)) file.WriteString(fmt.Sprintf(",\"properties\":{\"name\":\"%s\"}}", v.Tags["name"]))
以下のようにDecode関数で取得した要素(構造体)v
からGeoJSONに必要な情報を取得しファイルに出力します。
今回は、以下のデータを出力しています。
- coordinates(座標)として緯度経度(Lat、Lon)
- properties(属性)として名称(Tag[“name”])
- 変数vのメンバーTagは、複数のKey-Valueで構成されるmap(辞書型)です。
名称の一部の文字列を置き換え
ここは経験則になるのですが、Tag[“name”]には、"
、\n
、\
という3種類の文字が含まれていました。
このデータをそのままGeoJSONのpropertyとして出力すると、JSON形式のフォーマット違反となってしまいますので、以下のように文字の置き換えを行なっています。
// 属性文字のエスケープ関連文字の訂正 if strings.Contains(v.Tags["name"], "\\") { file.WriteString(fmt.Sprintf(",\"properties\":{\"name\":\"%s\"}}", strings.Replace(v.Tags["name"], "\\", "", -1))) } else if strings.Contains(v.Tags["name"], "\n") { file.WriteString(fmt.Sprintf(",\"properties\":{\"name\":\"%s\"}}", strings.Replace(v.Tags["name"], "\n", "", -1))) } else if strings.Contains(v.Tags["name"], "\"") { file.WriteString(fmt.Sprintf(",\"properties\":{\"name\":\"%s\"}}", strings.Replace(v.Tags["name"], "\"", " ", -1))) } else { file.WriteString(fmt.Sprintf(",\"properties\":{\"name\":\"%s\"}}", v.Tags["name"])) }
実行結果
私のPC(Mac mini M1 メモリ16GB)にて、japan-latest.osm.pbfを処理したところ、以下のように約1分で処理が終了しました。
API server listening at: 127.0.0.1:30905
Start: 2021-07-25 15:24:30.797359 +0900 JST m=+0.000734293
Nodes: 204570114, Ways: 26982674, Relations: 121552
End: 2021-07-25 15:25:35.130877 +0900 JST m=+64.335658085
Process exiting with code: 0
出力されたJSONファイルの要素数は20,400件でした。以下に例示します。
{"type":"FeatureCollection","features":[ {"type":"Feature","geometry":{"type":"Point","coordinates":[139.9374733,35.9000282]},"properties":{"name":"東大柏"}}, {"type":"Feature","geometry":{"type":"Point","coordinates":[140.8462716,37.2401729]},"properties":{"name":"トワダ"}}, {"type":"Feature","geometry":{"type":"Point","coordinates":[140.1671416,35.5502057]},"properties":{"name":"予備校ARROWS"}},
まとめ
今回は、Golangを使ってosm.pbfからGeoJSONファイルを抽出するプログラムを作成しました。
ソースコードは、以下のGitHubリポジトリのchap2
に収容しています。
https://github.com/takamotobiz/learngo
また、今回作成したプログラムを使って全世界の学校データ(planet.osm.pbf)をベクトルタイル配信していますので、興味ある方は以下のURLから確認してみてください。
https://labo.takamoto.biz/planet-school/
背景地図にはGeolonia社が開発しているOceanusを使用しています。
地図を拡大すると学校のシンボルの名称が表示されます。
ただし、対象は点データ(Node)のみですので、面化されたデータ(Way、Relation)は処理していませんので、ご注意ください。
コメント