トップ地図アプリMap > OSMバイナリレコードファイル形式

OSMバイナリレコードファイル形式

前ページ

はじめに

これまでファイル形式の細部は何度か変更してきた。 昨年(2025年)には、閉ループタイプの natural=cliff のレンダリングの不備に対応するため、 reversedフラグを追加した。今後も、このようなケースが起きるかも知れないが、 ファイル形式の修正は極力避け、やむ得ず変更するときは、なるべく互換性を保ちたい。

Encoder で中間ファイルを作り、Parser で最終的な OSMバイナリレコードを作成する。

これまでは長年、メッシュ(Block)分割を行ってきたが、今回、分割は行わず、R-Tree管理とした。 平均レコード長は100バイトにも持たない大きさのため、葉ノードにレコード自体を含めている。

R-Treeに先立ち、レコードは Mortonコード順に並び替えておく。この並び替えなしに R-Treeを 作成してもうまく行かない。

地図アプリにとって重要なのは、最終的な OSMバイナリレコード形式である。

元になるOSM(xml形式)データは大きくは node、way、relation セクションで構成される。 中間ファイルは、三つで node.obf、way.obf、relation.obf の順に作成される。

OSMバイナリレコードは図形レコードであり point、line、polygon、multipolygon レコードである。

OSMの node、way、relationオブジェクトから直接、図形(地図)を描くことは難しいため、 図形描画に適したOSMバイナリレコードを作り出す。

標準OSM地図では通常はこのOSMバイナリレコードを osm2pgsql というOSM専用のフリーソフトを使って、 PostgreSQL/PostGIS データベースに取り込む。 そして、Mapnikという地図ソフトを使って、タイル地図画像データを作成する。

OSMに触れて11年になる。当初の数年は上記の方法でOSMタイル地図画像データを作っていた。 osm2pgsql や Mapnik は地図専用ソフトであり、使いこなすのは容易ではない。 osm2pgsql は Windows では使えない時期もあり、 Windows上で Linux を動かす環境を構築したこともある。 osm2pgsql や Mapnik のバージョンアップにも対応しなければならない。 これらの煩雑さから、osm2pgsql、PostgreSQL/PostGIS、Mapnik から離れて、 これら全てを自作プログラムで対応することにした。

完全に Mapnik互換の地図を作るのは難しいが、個人使用ではMapnikに近い地図を作るのは それほど難しくはない。OSMバイナリレコードはパソコンで作成するが、 Mapnikに相当する処理はスマホでも可能である。

Encoder

Encoderの出力フォーマット:   文字コードは UTF-16。
length:4, osm_id:8, uid:4, time:4, lon, lat, {key, val}*                         ----- Node   
length:4, osm_id:8, uid:4, time:4, bbox:16, num_nds, {lon, lat}*, {key, val}*    ----- Way  
length:4, osm_id:8, uid:4, time:4, num_members, {type, ref, role}*, {key, val}*  ----- Relation
bbox: minlon, minlat, maxlon, maxlat

lengthは自分を含むレコード長(単位:バイト)。

uid、userはペアでコード化する。現在(2024年10月)、japan.osm で、4万弱のため、2バイトでもよいが、 4バイトを割り当てる。

データ更新時刻 time は分単位として、4バイトを割り当てる。精度はもっと下げてもよいので、2バイト化も可能である。 しかし、レコードサイズの縮小に拘らない方がよい。

role は enum で定義し、レコード上は2バイトで表す。inner、outer などいくつかについては、 レンダリングに関係するが、レンダリングでは無視するものが殆どである。

【ソースデータ例】
  <node id='31253515' timestamp='2022-01-09T09:59:40Z' uid='7707735' user='zyxzyx'
 visible='true' version='5' changeset='115935292' lat='35.6536052' lon='139.7601994' />

japan のファイルサイズは node 136MB、way 3.99GB、relation 37.0MB、計4.13GBとなった。

当面、レンダリング対象外のrelationを除くと、relation は 28MBとなった。

maxId=2D922B13B, nodes=FF19CB7, ways=23BD110, rels=28F11(167697), 実行時間: 32.77分

relation member のノードは 153,588 nodes, 11.9 MB、ウェイは 677,412 ways, 217.8 MB であった。 ウェイレコードは平均で 300B を超える。

全体の5%程度であるから、ブロックファイルとの重複は問題でない。 同じようにzoom 8か9で分割すれば、入れ替え頻度はzoom 12分割のブロックファイルより小さくなるので、 osm_idによるメンバー取り出し平均時間は十分に小さいであろう。

レコードに組み立てた場合、行政境界などでは同じウエイが2回以上使われる。また、 境界ボックスは大きくなるため、zoom 12だけの分割はできず、zoom 7、8といった分割も必要となる。  

現在は、バス路線や行政境界名の描画はブロックファイルの総サイズを抑えるために、工夫をしている。 このような工夫をやめ、プログラムをシンプルにすると、ブロックファイルの総サイズは更に何割か大きくなる。

つまり、relationレコードを事前に組み立て、色んな工夫をやめると、総サイズが大きくなりすぎる。 SDカードの容量は問題ないが、パソコンからスマホ/タブレットへのファイル転送時間が増大する。

しかし、プログラムの簡単化を一番重視すると、総サイズの増大を許容すべきかも知れない。

レンダリング対象外の巨大なポリゴン/マルチポリゴンを除外する

北海道、本州、四国、九州などは除外する。ファイルサイズの縮小は僅かである(37.0 -> 36.7MB)。

Parser

空間インデックスを使う代わりに、ブロック分割する。

{lon,lat}* は先頭は現状の絶対値、次からは前のノードとの差分値を2バイトペアで表す。 2バイトで表せないときは中間に必要なだけノードを挿入して、差分が2バイトで表せるようにする。

Encoderの出力では座標値は極座標であるが、Parser の出力は世界XY平面座標とする。 zoom 0 では世界地図が1枚のタイルで表され、そのX,Y座標値は共に 0.0 から 1.0 とする。 整数表示とするため、230 をかける。つまり、座標値は 0 ~ 230 とする。

中低ズームではこれほどの精度はいらないため、中ズームでは 0 ~ 225、低ズームでは 0 ~ 220 とする。

タイルのレンダリングでは、タイル座標に変換する必要があるが、世界XY平面座標ではこの変換が 簡単な掛け算と引き算だけ済む利点がある。世界XY平面座標を使う理由はそこにある。

length:4, head:4, osm_id:8, uid:4, time:4, bbox:16, num_nds:4, {x:4/2,y:4/2}*, {key:2,val:2*}* -- point/line/polygon  
length:4, ~同上~ time:4, bbox:16, num_polys:2, {num_nds:4}*, {x:4/2,y:4/2}*, {key:2,val:2*}* -- multipolygon
bbox: xmin, ymin, xmax, ymax

num_polys(ポリゴン数)は2バイトとしているが、いずれ、4バイトにした方が分かりやすい。

{lon,lat}* は先頭が 4バイトペアで次からは前との差分を2バイトペアで表す。必要に応じて、ノード間に 1つ以上のノードを挿入して、差分が2バイトに収まるようにする。

OSMバイナリレコードの length の次の head は先頭の 4ビットが type で、最下位ビットを fReversedフラグとしている。

out.writeInt((type<<28) + (fReversed ? 1 : 0));

ポリゴン内部の塗りつぶしでは、時計回りとあるいは反時計回りかソフトによって決まりがある。 natural=cliff(崖)のようにラインによっては山か窪みかによって方向が変わる。 一つのレコードにポリゴンの塗りつぶしとなるタグと方向性があるラインタグが含まれる場合、 両者の方向が相反するケースがある。このため、reversedフラグを導入した。

relation

上記の node、way は relation のメンバーを含んでいる。ただし、relation では同じ way、node を繰り返し使用する。 例えば、バス路線relation では、同じバス停、同じ道を複数の路線が使う。 また、行政境界線は少なくとも、二つの行政が共通で使う。3回以上共用されることも珍しくない。

このため、単純に relation で lineレコードや polygon/multipolygon レコードを組み立てると、 総レコードサイズは大幅に増加する。また、polygon/multipolygon は way単独のものより、 はるかに大きくなることが多く、zoom 8のブロックをまたぐものが多くなり、この重複により、 総ファイルサイズが一層大きくなる。

バス路線や航路などはレンダリングでは、繋いで長いものにする必要はないが、 広域森林や湖などは必ず polygon/multipolygon にする必要がある。 

バス路線は独立したレコードにするのではなく、道路レコードにバス路線IDを含めておくだけでもよい。 一般に point/lineレコードの場合は複数のrelationが共用する場合、この方をとれる。

レコード管理(class Record)

以前は差分コードの効果は大きかった。その後、レンダリングには要らない osm_id、uid、time をレコードに付け加えたため、相対的に、差分コードの効果は薄らいだ。 それでも、差分コードをやめると、バイナリレコードファイルは3割前後増加するであろう。

メモリ上でも、差分コードの方がレコード管理に使われるメモリが2割ほど少なくて済むであろう。 しかし、メモリに置くのはあるタイルのレンダリングに使われるレコードのみであり、それほどメモリが逼迫している わけではない。よって、差分を元に戻す。

num_nds:4, {x:4/2,y:4/2}* と {num_nds:4}*, {x:4/2,y:4/2}* を同じように、int 配列に格納する。

num_nds を Record のメンバー変数とすることもできる。 multipolygon に対しては、最初の outer polygon のノード数を num_nds におけば、統一が図れる。

残りの inner polygon の長さは、座標値列データの前に置く。ただ、outer polygon だけが特別扱いの点がひっかかる。

point レコードの場合、座標値は bbox で分かる。num_nds は 0 として、int配列は使わない。

リファレンス

[1] OpenStreetMap Data Extracts