Golangによるosm.pbf読み込み3

前回の記事では、qedus/osmpbfを使って、osm.pbfの点データ(Node)のみをGeoJSON形式で出力するプログラムを作成しました。

今回は、違うプロダクト(osmpbfparser-go)を使って、点だけでなく線や面(Way、Relation)もGeoJSONファイル出力するプログラムを作成したいと思います。

osmpbfparser-goとは

osmpbfparser-goとは、Goにてosm.pbfを取り扱うためのパーサーで、james_lin氏(台湾の方のようです)がGitHub上で公開しています。
ライセンス形態はMITライセンス(著作権表示とライセンス条項の記載があれば改変や派生物の頒布も自由なライセンス)です。

API仕様は、以下に示されています。

https://pkg.go.dev/github.com/jneo8/osmpbfparser-go

osmpbfparser-goの特徴

osmpbfparser-goの特徴は、Way->NodeおよびRelation->Wayの関係をパーサーが吸収(関係を保持)してくれることです。
これは、osmpbfparser-goの利用者がWay->NodeおよびRelation->Wayの関係を意識することなくosm.pbfを利用できるということであり、他のパーサーにはない大きなメリットとなっています。

一方で、Way->NodeおよびRelation->Wayの関係構築にハードウェアリソースを多く使用し、処理時間もかかることも事実です。

具体的には、Node、Way、Relationの関係をlevelDB(Google製のインメモリデータベース)上に構築しているようで、このために処理対象のosm.pbfと同程度のメモリを使用し処理に時間もかかります。
私のマシン(Mac mini M1 メモリ16GB)で日本全国データ(japan-latest.osm.pbf、約1.65GB)を処理したところ、3時間ほどかかりました。

GeoJSON出力プログラム

今回は、リポジトリのcmd/exampleの下にあるmain.goを少し改修して、日本全国のosm.pbf(japan-latest.osm.pbf)のNode(点)要素の学校(amenityタグ=school)を抽出し、GeoJSONファイルに変換するプログラムを作成します。

改修後のソースコードは以下の通りです。

package main

import (
    "fmt"
    "log"
    "os"
    "time"

    "github.com/jneo8/osmpbfparser-go"
)

func main() {
    parser := osmpbfparser.New(
        osmpbfparser.Args{
            PBFFile:     "/Users/takamotokeiji/data/osm.pbf/japan-latest.osm.pbf",
            LevelDBPath: "/tmp/osmpbfparser",
            BatchSize:   10000,
        },
    )
    var nc, wc, rc uint64
    // ファイルを書き込み用にオープン (mode=0666)
    file, err := os.Create("./output.json")
    if err != nil {
        panic(err)
    }
    defer file.Close()

    str := "Start:"
    str += time.Now().Format("2006-01-02 15:04:05")
    str += "\n"
    fmt.Println(str)

    file.WriteString("{\"type\":\"FeatureCollection\",\"features\":[\n")

    var fi bool = true
    for emt := range parser.Iterator() {

        tags := emt.GetTags()
        if nv, fl := tags["amenity"]; fl {
            if nv == "school" {

                if fi {
                    fi = false
                } else {
                    file.Write([]byte(",\n"))
                }

                rawJSON, err := emt.ToGeoJSON()
                if err != nil {
                    log.Fatal(err)
                }
                file.Write([]byte(rawJSON))

                switch emt.Type {
                case 0: //Node
                    nc++
                case 1: //Way
                    wc++
                case 2: //Relation
                    rc++
                }
            }
        }
    }
    file.WriteString("]}\n")

    fmt.Printf("Nodes: %d, Ways: %d, Relations: %d\n", nc, wc, rc)

    str1 := "End:"
    str1 += time.Now().Format("2006-01-02 15:04:05")
    str1 += "\n"
    fmt.Println(str1)

    if err := parser.Err(); err != nil {
        log.Fatal(err)
    }
}

以降にポイントを解説します。

パーサーの生成とデータ取得

osm.pbfのパーサーは、イテレータ(データを連続取得できるオブジェクト)として提供されています。
パーサーは、osmpbfparser.New関数を使って取得します。
また、引数にはosm.pbfファイルとlevelDBが使用するパスを設定します。

    parser := osmpbfparser.New(
        osmpbfparser.Args{
            PBFFile:     "/Users/takamotokeiji/data/osm.pbf/japan-latest.osm.pbf",
            LevelDBPath: "/tmp/osmpbfparser",
            BatchSize:   10000,
        },
    )

取得したパーサー(変数parser)はイテレータですので、以下のようにforループで変数emtに連続して取得できます。

    for emt := range parser.Iterator() {

タグ取得

取得した要素emt(Elementの意味だと思います)には、osm.pbfに設定されているNodeやWay、Relationの持つ座標やプロパティ(属性)などが設定されています。

タグを取得する場合は、以下のように要素emtの持つGetTags関数を呼び出し、タグの辞書tagsを取得します。
取得した辞書からキー値"amenity"を指定した設定値(Value)を取得し必要なタグ(="school")か判定します。

        tags := emt.GetTags()
        if nv, fl := tags["amenity"]; fl {
            if nv == "school" {

要素のGeoJSON化

取得した要素のGeoJSON化は、以下のように変換用の関数ToGeoJSONが用意されています。

                rawJSON, err := emt.ToGeoJSON()

しかしこの関数、変換結果をFeatureCollectionとして返却します。
変換結果をファイルにそのまま書き出しても、1要素であれば問題ないと思いますが、2要素以上ですとGeoJSONとしてはフォーマット不正になります。

この対策として、FeatureCollectionの付与はmai.go(33行目)で行い、ソースelement_geojson.goのToGeoJSON関数にてFeatureCollectionを出力しないように強調表示している3行をコメントアウトします。

package osmpbfparser

import (
    "reflect"
    "strconv"

    geojson "github.com/paulmach/go.geojson"
)

// ToGeoJSON convery element to JSON bytes.
func (e *Element) ToGeoJSON() ([]byte, error) {
    switch e.Type {
    }
    // fc := geojson.NewFeatureCollection()
    f := e.ToGeoJSONFeature()
    // fc.AddFeature(f)
    // rawJSON, err := fc.MarshalJSON()
    rawJSON, err := f.MarshalJSON()
    return rawJSON, err
}

同時に、main.goにてFeatureCollectionを付与する改修も行なっています。

実行結果

私のPC(Mac mini M1 メモリ16GB)にて、japan-latest.osm.pbfを処理したところ、以下のように約2時間で処理が終了しました。

出力されたJSONファイルの要素数は以下のような結果でした。

  • Node: 20,386
  • Way : 25,311
  • Relation: 301

抽出結果のGeoJSONをtippecanoeにてmbtilesに変換してtileserver-glにて配信した結果が以下となります。

記号(Node)に加えて線(Way)と面(Way、Relation)が表示されている

配信URLは以下ですので、興味ある方は確認してみてください。

https://labo.takamoto.biz/japan-school/

背景地図にはGeolonia社が開発しているOceanusを使用しています。

まとめ

今回は、Golangを使ってosm.pbfからGeoJSONファイルを抽出するプログラムを作成しました。
ソースコードは、以下のGitHubリポジトリにて公開しています。

https://github.com/takamotobiz/osmpbfparser-go

今回使用したosmpbfparser-goはとてもよいのですが、如何せんコンピュータリソースを多く使いますし、処理時間も長いです。

目標は、Goの学習を兼ねてplanet.osm.pbfから任意のデータを高速に抽出(とりあえずGeoJSON形式で)することです。
osmpbfparser-goは、日本全国くらいが実質的な限界で、それ以上の範囲では数日必要だったり、(メモリやスワップ量によりますが)OutOfMemoryで処理が止まったりします。

次回は、もう少し別なアプローチをしようと思います。

コメントを残す

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

2 × 1 =