OSMのデータ形式(xml)は比較的にシンプルである。 しかし、日本地図領域全体を表すOSMデータは XML形式(プレーンテキスト)では2024年4月末では 42.3GB である。 到底、全体をメモリに読込める大きさではない。
また、直接、地図を描ける仕様のでーたではない。
一挙に、地図を描けるようなデータ形式にするのは、簡単ではない。このため、いくつかの手順を踏んで、 最終的に地図のレンダリングに使うバイナリレコードファイルを作成する。
OSMファイルは大きくは3つのセクションに分かれる。 Nodeセクション、Wayセクション、Relationセクションである。
Nodeは単独でひとつの地図上の Point情報となるケースもあるが、殆どのNode は Way を構成する点の座標を表す。
Wayは Node ID列であり、座標値そのものは Nodeを見ないと分からない。 Nodeの数は膨大なため、Node ID を座標値に置き換える作業は簡単ではない。
そのため、まず、最初に、Node ID を座標値に置き替え、Wayオブジェクトを座標値列データ(バイナリ)とする。
極座標に 107 をかけて、整数化すると、丁度4バイト(int型)で表せる。
highway=motorway、amenity=post_office といったOSMタグはキー(highway、amenity)、 値(motorway、post_office)ともコード化して2バイト整数(short型)で表す。
OSMタグには admin_level=7、layer=-1 といった整数値を値とするものもある。この場合、 キーが short、値が int(4バイト)となる。ele=1023.6(標高)のように、値が float型になるものもある。 name(名称) や ref(道路番号、略号など) など値が文字列となるものもある。
これらOSMタグのコード化、バイナリ化もここで行う。
この段階のレコード形式は次のようになる。
Encoder.java は約300行であり、さほど複雑ではない。 しかし、巨大なメモリを使用するため、次のステップの Parser との一体化は避けたい。
Encoderの出力フォーマット length, osm_id, lon, lat, {key, val}* ----- Node length, osm_id, num_nds, {lon, lat}*, {key, val}* ----- Way length, osm_id, num_members, {type, ref, role}*, {key, val}* ----- Relation
length、lon、lat は4バイト、osm_id は8バイトである。key は2バイト、 val は2、4、2*n バイト である。文字列は UTF-16コードである。
2024年4月下旬の japan-latest.osm に対する結果は文字コードを UTF-8 とした場合、 node 106MB、way 2.99GB、relation 40.5MB となった。
UTF-16とした場合は node 110MB、way 3.02GB、relation 49.7MB となった。 name:en を含んでいることから、英字の比重が高いようだ。
UTF-8では、文字列の長さが奇数になるケースがある。 余分に1バイト(0x00)を加えて偶数に調整することもできるが、UTF-16ではそのまま偶数になる。
OSMバイナリレコードはメモリ上では short配列とした方が好都合である。
この段階では relation ファイルは小さいが、次の段階で生成されるバイナリレコードファイルの サイズは大きくなる。
森林、湖、都道府県境界など巨大な多角形は Relation で定義される。 Way をつなぎ合わせて、閉ループを構成する。例えば、東京都には島嶼部があるため、 複数の outer polygon となる。一つの outer polygon にはいくつかの inner polygon を 含むことがある。市区町村境界で言えば、他市の飛び地が inner polygon となる。 他市にとっては outer polygon の一つとなる。
広域森林には一般に平地、湖、住宅地などがあり、これらが inner polygon となる。
Parserの大きな仕事は Relation処理である。 一つの relation に複数の outer polygon があった場合には、outer polygon 毎の レコードとする。 つまり、以下の multipolygon は outer polygon は一つで、inner polygon が一つ以上となる。 ひとつの relation から複数のレコードが生まれる。
バス路線relation の場合、複数のバス停、複数の道路がメンバーとなる。 道路メンバーをつなぎ合わせたものが、バス路線となる。
道路とは別にバス路線レコードを作成することもできるが、 現在はそうせず、道路レコードにバス路線relationのID を付加している。 バス路線relationのタグである。name(バス路線名)やref(バス路線番号)は独立したファイルとしている。
バス路線レコードが道路レコードに比べて、小さければ、 このような方法は採らず、独立したレコードにする方が分かりやすい。必ずしも、繋ぎ合わせる必要はなく、 osm_idが同じ複数のレコードでよいので、処理は簡単である。 独立レコードとしたとき、ファイルサイズがどの程度増えるか調べた方がよい。
このバス路線relationのIDは {key,val}* に含めている。 つまり、Encoder の出力では {key,val}* は OSMタグデータだけであるが、 Parserの出力には OSMタグ以外のデータも含まれる。
多角形領域の中心にはアイコンや名称を描画するが、この中心は幾何学的な重心ではなく、 算出は楽ではない。この仕事も Parser で行う。この中心座標も タグ部に含める。 小さな無名の建物がレコードの半数を超えるが、このレコードには中心座標はいらない。
polygon/multipolygon レコードには並び替えのために way_area(面積)が必要となる。 空間検索結果については極座標をタイルXY平面座標に変換するが、 way_area算出時間はこれと同程度と推測される。実測して、時間がかかるようならば、 レコードに含ませる。
way_area はレコードの並び替えで使用する。 レンダリング上は相対的な値でよいので、空間検索結果のレコードについて、 極座標をタイル座標に変換した後、polygon/multipolygon の way_area(面積)を求めてもよい。 座標変換よりも簡単な計算で求まるため、レンダリング時間の増加は小さい。
タグ部の長さ(バイト単位)が奇数のときは 0x00 を追加して、 レコード全体の長さが2の倍数になるように調整する。
文字コードを UTF-16 とすれば、調整なしに文字列エリアの長さが2の倍数になる。
num_nds は極めて稀に2バイトで表せないことがある。現在は、レンダリングしていないレコードのため、 除外することもできるが、将来のことを考えて、2バイトと4バイトをサポートする。 Parserの場合、レコードの出力箇所が複数あるため、この段階では4バイトに統一しておく方がトラブルが少ない。 最終的に Devider で殆どの num_nds を2バイトで表し、例外的に、4バイトも許す。
num_nds を前にまとめて、{lon,lat}* を一まとめにできるが、 階層的な方がプログラムは楽である。
head, {lon,lat}, tags_length, {key,val}* ... point head, num_nds, {lon,lat}*, tags_length, {key,val}* ... line head, num_nds, {lon,lat}*, tags_length, {key,val}* ... polygon head, num_polys, {num_nds, {lon,lat}*}*, tags_length, {key,val}* ... multipolygon head: 第0,1bit(0x03) 0: point、 1: line、 2: polygon、 3: multipolygon
japan-high 2.91GB、japan-mid 317MB、japan-low 27.6MBとなった。
kanto-high 588MB、kanto-mid 53.3MB、kanto-low 5.24MBとなった。
最後に、矩形領域に分割する。way_area は Parser 自体で使用するため、Parser の出力に加えたが、 Devider で追加してもよい。
osm_id は特定のレコードに対してのみ、Parser で タグ部に追加する。
特定の polygon/multipolygon について中心座標を Devider でタグ部に追加する。