OSM(OpenStreetMap)を使い始めて10年を迎えた。 最初の数年(または4、5年)はOSMデータをPostgreSQL/PostGISに取り込んで、Mapnikでレンダリングした。
その後、データベースもMapnikも一切使わない独自の方法を採っている。
レンダリング用のOSMバイナリレコードファイルを作成して、独自のプログラムでレンダリングしている。
Mapnikの場合、レンダリングなどで別のフリーソフトを使用しているが、 自作地図アプリは Windows用では C#基本機能、スマホでは Android Javaの基本機能のみですべてを実現している。
かっては、フリーソフトをあれこれ使ったことがあるが、言語の基本機能に比べると、何かと苦労する。 余計な苦労を避けるため、標準機能に限定している。
日常的に使用するのは日本地図であるが、ダウンロードやOSMバイナリレコード作成にそれなりの時間がかかるため、 確認のために、関東、中部、関西といった地域別データを対象とすることもある。
OSMデータは xml 形式であるが、pbfという圧縮形式が普及している。xmlファイルは大きくは nodeセクション、wayセクション、relationセクションに分かれる。
大雑把には、node は point データ、way は line や polygon データ、relation は複合体で、node、way、relation がそのメンバーである。
座標値自体はすべて nodeセクションにある。このため、line(折れ線)、polygonを座標値列データを求めるのにも、 時間や大きなメモリを必要とする。
ワンパスで nodeセクション、wayセクション、relationセクションの順で OSMバイナリレコードを作成することはできない。 前処理で node、way、relation 別の中間ファイルを生成する。
中間ファイルは最終的なレンダリング時間に関係しないため、ファイルサイズはさほど問題ではない。
OSMタグ {key, val} はコード化している。 key (amenity、leisure, shop など)がコード表にない 場合、無視している。あらかじめ決めているkeyが一つもない場合は、レコード自体を捨てている。
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カードの容量は問題ないが、パソコンからスマホ/タブレットへのファイル転送時間が増大する。
しかし、プログラムの簡単化を一番重視すると、総サイズの増大を許容すべきかも知れない。
圧縮ファイル *.osm.pbf を osmosis で解凍して、そのテキストデータをパイプ接続で EncoderEx で 受けて、テキスト(xml)データをバイナリレコードに変換する。
osmosis --rbf c:/gisdata/pbf/japan-latest-internal.osm.pbf --wx file=- | java -Xmx6g -classpath /GIS/class EncoderEx japan
国境や国名だけであれば、Overpass API で必要なデータだけを入手する方が楽である。 しかし、大きな湖や砂漠などをレンダリングしたいとなると、その方法は使えない。
面積の大きいポリゴンデータだけを抽出することはできないと思われる。 処理時間はかかるが、asia.osm、europe.osm などから抽出したい。
osm xmlではway(ラインまたはポリゴン)の座標データは node id列である。これを極座標値列に変換する。
現在のプログラムでは変換テーブルは node_id、lon、lat 配列で、node_id順である。
japan.osmでは問題ないが、asia.osm や europe.osm に対応できるかどうかは未確認である。 検索はバイナリサーチのため、メモリ容量上は対応できても、処理時間がかかりすぎるかもしれない。
以前は、node_id をブロックに分けて、そのうちの何ブロックかだけを lon、lat 配列に展開していた。 欠番のところは無効データが入っており、検索は単なる添え字アクセスなので、バイナリサーチに比べれば、 桁違いに速い。ブロックの交代がいるため、プログラム的には現在のプログラムより複雑である。
以前のパソコンの主メモリは 8GB、現在は 16MB である。より大きなメモリを使えるようになったので、 プログラムを簡単にしたい。
世界地図は中低ズームだけでよい。この場合、建物や幹線道路未満のデータはいらない。 node_id を lon、lat に変換するのは、これらのデータを除いてから、 とすれば node_id、lon、lat配列のサイズは抑えられるので、バイナリサーチで対応できるであろう。
その場合も現在のプログラムよりは少し複雑になる。pointレコードになるものはその node_id をどこかに 記録しておき wayセクションに移る。ここで、レンダリング対象となる可能性がある way の node_id 列を 表に登録する。この段階では node_id順とは限らない。pointセクションで登録したものと重ならないようにする。
その都度、並び替えを行えば、重複を避けるのは簡単であるが、頻繁に並び替えを行うと時間がかかる可能性が ある。
北海道、本州、四国、九州などは除外する。ファイルサイズの縮小は僅かである(37.0 -> 36.7MB)。
将来的には、巨大なレコードはブロック分割は行わず、レコード毎のファイルとするかもしれない。
空間インデックスを使う代わりに、ブロック分割する。
{lon,lat}* は先頭は現状の絶対値、次からは前のノードとの差分値を2バイトペアで表す。 2バイトで表せないときは中間に必要なだけノードを挿入して、差分が2バイトで表せるようにする。
例えば、ポリゴンは面積の大きい順にレンダリングする。湖の中に島があり、その島の中に池があった場合、 湖、島、池の順にレンダリングするので、うまくいく。 この大きさは現在はポリゴンの面積ではなく境界ボックスbbox(ポリゴンの外接矩形)の面積としている。
面積の方が望ましいとか別の用途で面積が欲しくなれば、下のレコード形式に way_area を加える。 way_area は {key, val} に含めることもできる。
length:4, head:4, osm_id:8, uid:4, time:4, lon:4, lat:4, {key:2,val:2*}* -- point
length:4, ~同上~ time:4, bbox:16, num_nds:4, {lon4/2,lat4/2}*, {key:2,val:2*}* -- line/polygon
length:4, ~同上~ time:4, bbox:16, num_polys:2, {num_nds:4}*, {lon:4/2,lat:4/2}*, {key:2,val:2*}* -- multipolygon
bbox: minlon, minlat, maxlon, maxlat
num_ndsはごく一部のレコードで、2バイト幅を超えるものがあるので、4バイト幅とする。
{lon,lat}* は先頭が 4バイトペアで次からは前との差分を2バイトペアで表す。必要に応じて、ノード間に 1つ以上のノードを挿入して、差分が2バイトに収まるようにする。
relationについてはかなりの処理がいるが、 node と way については、単純にブロック分割するだけである。 しかし、polygon についてはいずれ中心位置の算出が必要になる。
まずは node と way を zoom 12 と zoom 8 で 分割してみる。
zoom 12 は 3.33GB、zoom 8 は 32MB となった。このときは zoom 12 に置く場合、重複が3以上となる場合に、zoom 8に置いた。
zoom 12 に置く場合、重複が起きるとき、zoom 8 に置くと zoom 12 は 2.92GB、zoom 8 は 252MB となった。zoom 8 では重複がありうる。zoom 8 での最大は 27.8MB となった。
head は上位4ビットを type、下位28ビットを rec_id とする。osm_id はレコードの識別には使えない。 マルチポリゴンの場合、同じ outer polygon が二つ以上の場合、outer polygon 毎のレコードとなるため、osm_id は同じとなる。
上記の node、way は relation のメンバーを含んでいる。ただし、relation では同じ way、node を繰り返し使用する。 例えば、バス路線relation では、同じバス停、同じ道を複数の路線が使う。 また、行政境界線は少なくとも、二つの行政が共通で使う。3回以上共用されることも珍しくない。
このため、単純に relation で lineレコードや polygon/multipolygon レコードを組み立てると、 総レコードサイズは大幅に増加する。また、polygon/multipolygon は way単独のものより、 はるかに大きくなることが多く、zoom 8のブロックをまたぐものが多くなり、この重複により、 総ファイルサイズが一層大きくなる。
バス路線や航路などはレンダリングでは、繋いで長いものにする必要はないが、 広域森林や大きな湖などは必ず大きな polygon/multipolygon にする必要がある。
バス路線は独立したレコードにするのではなく、道路レコードにバス路線IDを含めておくだけでもよい。 一般に point/lineレコードの場合は複数のrelationが共用する場合、この方をとれる。
行政境界の場合も、境界線と線の内側に行政名を描くだけの場合、同じように共用できる。 しかし、地図を区別に異なる色で塗りつぶす場合には、polygon/multipolygonにしなければならない。
まずは、広域森林、湖(relationによる水域)など、レンダリングでは polygon/multipolygon化が必須のものについて、 relation処理でpolygon/multipolygonレコードを作り、総ファイルサイズがどれくらい増加するかを調べたい。
まず、ハイズームだけを作成した。zoom 12 が 2.92GB、zoom 8 が 351MB となった。 この後、行政境界、バス路線などで増加するが、思ったよりは少なくて済みそうである。