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)は処理していませんので、ご注意ください。
コメント