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に必要な情報を取得しファイルに出力します。

VSCodeにてブレークポイント停止中に変数vを参照した状態

今回は、以下のデータを出力しています。

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

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

17 − four =