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

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

前ページ
OSMバイナリレコードファイルの作成
可変長バイトコードを試す

はじめに

OSMバイナリレコード形式およびファイルの更新手順をここにまとめておく。

レコード形式が全体に与える影響は極めて大きい。 特に、最終段階でのフォーマットが重要であり、これまでに、二転三転している。

フォーマットはシンプルでファイルサイズが小さくなることが望ましい。 しかし、両者は相容れないところがある。多少、ファイルサイズが大きくなっても、形式上のシンプルさを重視している。 その方がプログラムが分かりやすい。

Geofabrik Download Serverから japan-latest.osm.pbf ファイルをダウンロードする

Geofabrik Download Server[1] から japan-latest.osm.pbf をダウンロードする。 ダウンロードから最終的な OSMバイナリレコードファイルを作成して、 スマホやタブレットに転送するにはそれなりの時間がかかるため、 新しいデータをいち早く確認したい場合には kanto-latest.osm.pbf や shikoku-latest.osm.pbf をダウンロードすることもある。

これらのデータは昨晩のデータである。狭い範囲で、最新のデータを得る方法として OverpassAPI[2] や JOSM による方法がある。

JOSM は編集ソフトであるが、狭い範囲であるが、ダウンロードしたファイルを外部ファイルに出力できる。

osm.pbfファイルを解凍する

osm.pbfファイルは圧縮形式のファイルである。これを xml形式のテキストファイルに解凍する。 解凍すると japan-latest.osm ファイルは約40GBの大きさになる。

パイプライン処理で必要なディスク容量を減らす方法もあるが、 ここではテキストファイルを作成する方法を採る。

解凍には、osm.pbfファイルの本家である osmosis か、別のフリーソフト osmconvert を使う。

解凍は osmconvert が速い(約10分)。ただし、osmosisに比べて使用実績が少ない。

osmconvert d:/downloads/japan-latest.osm.pbf > d:/osm/japan.osm
osmconvert d:/downloads/kanto-latest.osm.pbf > d:/osm/kanto.osm
osmconvert d:/downloads/shikoku-latest.osm.pbf > d:/osm/shikoku.osm

osmconvert でエラーが起きる場合、osmosis を使う。解凍に時間がかかる(約1時間)。

osmosis --rbf d:/downloads/japan-latest.osm.pbf --wx d:/osm/japan.osm

japan-latest.osm.pbf(1.87GB) を japan.osm(42GB) に解凍した。

xml形式をバイナリ形式に変換する

xml形式は分かりやすく、柔軟性が高いが、解釈に時間がかかる。 そこで単純なバイナリ形式に変換する。

OSMデータはNode、Way、Relationの三つのオブジェクトで構成される。

Nodeセクション、Wayセクションは巨大なため、パソコンでは日本地図領域の全データを同時にメモリに載せることはできない。 そのため、1パスで、xmlファイルからOSMバイナリレコードファイルを作り出すことはできない。

このため、第一フェーズとしては、xmlファイルをバイナリ化して、 Nodeセクション、Wayセクション、Relationセクションに分けて三つのファイルに出力する。

今回は次のようなフォーマットとしている。xml ファイルでは、Wayセクションのノード列は ノードID(osm_id)列であるが、 それを座標値に置き換えたものを出力する。 文字コードは UTF-8 とする。

length, osm_id, lon, lat, {key, val}*                         ----- Node   
length, osm_id, num_nodes, {lon, lat}*, {key, val}*           ----- Way  
length, osm_id, num_members, {type, ref, role}*, {key, val}*  ----- Relation       
java -Dfile.encoding=UTF-8 -Xmx5g -classpath ./class OSMUtil -encode japan

japan.osm(42GB) の変換結果は node0.obf(106MB)、way0.obf(1GB)、way1.obf(1GB)、way2.obf(995MB)、relation0.obf(38.7MB)、全体で3.11GB となった。

OSMバイナリレコードファイルを生成する

大半のNodeは Wayを構成する頂点の座標のみを表す。 ごく一部が単独で例えばバス停や交差点の信号機などの Pointレコードとなる。

一部の Way は Relation のメンバーとなるが、単独で、Line(道路など)や Polygon(建物、公園など)が多い。

Relationは都道府県境界(Polygon/Multipolygon)、広域森林(Polygon/Multipolygon)、バス路線などを表す。 バス路線はバス停(Point)や道路(Line)をメンバーとしてもつ。 メンバーは、通常は、それぞれ、単独でバス停レコード、道路レコードにもなっている。 Map3では、バス路線道路には中央に細い青線を引いている。バス路線レコードを新たに追加するのではなく、 バス停レコード、道路レコードにバス路線id列を含めている。

Relationの扱いが一番難しい。

陸地ポリゴンも本州、北海道など巨大であるが、高ズームでは、これを多数のポリゴンに分割したもので陸地の描画を行う。

Node、Way、Relation の場合はファイルサイズはさほど気にする必要はないが、 OSMバイナリレコードは直接、レンダリングに使うものであるからなるべくコンパクトにする必要がある。 また、無数のレコードから所望のレコードを素早く抽出する必要がある。

Map4 では これまでのバージョンとは大幅に異なるシンプルなフォーマットとする。

head(2),               [num_nodes(4)],  {lon,lat}*, tags_length(2), {key,val}*  ... point/line/polygon 
head(2), num_polys(2), {num_nodes(4), {lon,lat}*}*, tags_length(2), {key,val}*  ... multipolygon
head:  第0,1bit(0x03) 0: point、   1: line、    2: polygon、 3: multipolygon
num_nodes: multipolygonでは、最終要素(最終inner polygonノード数)の最上位ビットは 1 とする。

バイナリレコードファイルは高、中、低ズームの三つとなる。

java -Dfile.encoding=UTF-8 -Xmx5g -classpath ./class OSMUtil -parse japan low
java -Dfile.encoding=UTF-8 -Xmx5g -classpath ./class OSMUtil -parse japan mid
java -Dfile.encoding=UTF-8 -Xmx5g -classpath ./class OSMUtil -parse japan high

結果は japan-low.dat(23.7MB)、japan-mid.dat(301MB)、japan-high.dat(2.85GB) となった。

OSMバイナリレコードファイルを分割する

メッシュ(zoom相当)に分割する。バイナリレコードフォーマットは下記に示す。

フォーマットはParserとは少し変更した。

num_polys、num_nds、tags_length は2バイト、num_nds に限り、4バイトのケースもある。 現在は1レコードのみ。

{lon,lat}* は先頭のみ絶対値(4バイトペア)、次のノードからは前のノードとの差分で2バイトペア である。

head(4),                       {lon,lat},  tags_length(2), {key,val}*  ... point
head(4),             num_nds,  {lon,lat}*, tags_length(2), {key,val}*  ... line/polygon
head(4), num_polys, {num_nds, {lon,lat}*}*, tags_length(2), {key,val}*  ... multipolygon
head:
  第0,1bit(0x03) 0: point、 1: line、 2: polygon、 3: multipolygon
  下位3バイト レコード長(headを含む)

元々の座標値の差分は2バイトで表せないことがある。この場合、下のプログラムで線分の中間に ノードを挿入して、必ず2バイト(厳密には15ビット)で表せるようにしている。

高ズームでは 極座標に 107 をかけて整数化している。これがOSMデータの精度である。 中、低ズームではこれほど高い精度はいらないため、 106、105 に精度を下げている。

前のノードとの差分が2バイトで表せないときには、ノードを内挿しているため、 精度を下げる方がこの内挿が減り、ファイルサイズが縮小する。

    int[] getDifference(ByteBuffer bb, Rectangle rect, DataInputStream dis, int num) throws Exception {
        int[] lonlats = new int[num*2];       // 中心座標計算用               
        int ix = 0;
        int lon0 = dis.readInt();
        int lat0 = dis.readInt();
        lonlats[ix++] = lon0;
        lonlats[ix++] = lat0;
        int minLon = lon0;
        int minLat = lat0;
        int maxLon = lon0;
        int maxLat = lat0;

        bb.putInt(lon0);    // lon 無変換
        bb.putInt(lat0);    // lat 無変換
        for (int i = 1; i < num; i++) {
            int lon1 = dis.readInt();
            int lat1 = dis.readInt();
            lonlats[ix++] = lon1;
            lonlats[ix++] = lat1;
            if (lon1 < minLon) minLon = lon1;
            if (lat1 < minLat) minLat = lat1;
            if (lon1 > maxLon) maxLon = lon1;
            if (lat1 > maxLat) maxLat = lat1;
            int dlon = lon1 - lon0;
            int dlat = lat1 - lat0;
            if (Math.abs(dlon) < (1<<15) && Math.abs(dlat) < (1<<15)) {
                bb.putShort((short)dlon);
                bb.putShort((short)dlat);
            } else {
                double v = (double)Math.max(Math.abs(dlon), Math.abs(dlat)) / (1<<15);
                int n = (int)(v+0.5) + 1;
                double ddlon = (double)dlon / n;
                double ddlat = (double)dlat / n;
                for (int k = 0; k < n; k++) {
                    bb.putShort((short)ddlon);
                    bb.putShort((short)ddlat);
                }
            }
            lon0 = lon1;
            lat0 = lat1;
        }
        if (rect != null) {
            rect.setRect(minLon, minLat, maxLon-minLon, maxLat-minLat);
        }
        return lonlats;
    }

これまで、中ズームは zoom 7、高ズームは zoom 12、zoom 7 で分割していたが、 中ズームは zoom 9、高ズームは zoom 13、zoom 10 分割に変更した。 ファイルサイズの合計は大きくなるが、最大サイズを小さくする方が重要なため、変更した。

java -Dfile.encoding=UTF-8 -Xmx5g -classpath ./class OSMUtil -divide  3  3 japan-low
java -Dfile.encoding=UTF-8 -Xmx5g -classpath ./class OSMUtil -divide  9  9 japan-mid
java -Dfile.encoding=UTF-8 -Xmx5g -classpath ./class OSMUtil -divide 13 10 japan-high

結果は japan-low3(21.2MB)、japan-mid9(265MB)、japan-high10(345MB)、japan-high13(2.68GB) となった。

境界ボックスを追加する

ブロック分割されたファイル毎に実行する。

レコードフォーマットは以下の通りとする。現在は、境界ボックス bbox を追加しているだけである。

このファイルがAndroid機に short配列データとして、読込まれる。空間検索は head と bbox で行われる。 空間検索で抽出されたレコードについてのみ、レコードのパースおよびレンダリングが行われる。

境界ボックスとは line/polygon/multipolygon の外接矩形である。本家Java の Rectangle { x, y, width, height } に沿って { minlon, minlat, maxlon-minlon, maxlat-minlat } を それぞれ4バイトの整数で表している。

高ズームでは width(maxlon-minlon)、height(maxlat-minlat)を2バイトで表せないものが多くある。 中、低ズームは精度を下げているが、やはり、2バイトで表せないものが多いであろう。

head にフラグを置き、4バイトと2バイトをサポートする案もある。 何割かのレコードのサイズが4バイト縮小するが、空間検索では2バイトか4バイトにより処理が変わり、 処理時間が増す。これをさけ、bbox は 4バイトx4 としている。

bbox は空間検索用である。例えば、境界ボックスがそのファイルの矩形より大きければ、常に抽出される。 maxlon、maxlat が分割ファイル座標のmaxlon、maxlat を超える場合、分割ファイル座標の maxlon、maxlat に合わせていいはずである。

境界ボックスは外接矩形よりも大きければよいので、width、heightが2バイトで表せるように、精度を落としても、 抽出されるレコードはさほど増えないはずである。

要するに、width、height は元より、4値とも2バイトで表現できる可能性がある。 したがって、現在の16バイトが 12バイトあるいは 8バイトまで縮小できる可能性がある。

レコード当たり、4バイトあるいは8バイトの縮小はそれほど大きいわけではないので、じっくり考えてからのことでよい。

head,                             {lon,lat},  tags_length, {key,val}*  ... point
head, bbox,             num_nds,  {lon,lat}*, tags_length, {key,val}*  ... line/polygon
head, bbox, num_polys, {num_nds, {lon,lat}*}*, tags_length, {key,val}*  ... multipolygon
head:
  第0,1bit(0x03) 0: point、 1: line、 2: polygon、 3: multipolygon
  下位3バイト レコード長(headを含む)
java -Dfile.encoding=UTF-8 -Xmx5g -classpath ./class OSMUtil -bbox japan low 3
java -Dfile.encoding=UTF-8 -Xmx5g -classpath ./class OSMUtil -bbox japan mid 9
java -Dfile.encoding=UTF-8 -Xmx5g -classpath ./class OSMUtil -bbox japan high 10
java -Dfile.encoding=UTF-8 -Xmx5g -classpath ./class OSMUtil -bbox japan high 13

結果は japan-low3(max 14MB, 計27.7MB)、japan-mid9(max 13MB, 計318MB)、 japan-high10(max 14MB 909_403.dat 東京, 計340MB)、japan-high13(max 6MB, 計3.17GB) となった。

共用バッファは 16MBx8、4MBx16、1MBx32 とすると、合計 224MB となる。

以下のように、最大ファイルサイズは 15MB弱である。

c:\map>java -Dfile.encoding=UTF-8 -Xmx5g -classpath ./class OSMUtil -bbox japan low 3
BBox max 10360KB c:\map\data1\japan-low3\7\3.dat
実行時間: 0.01分

c:\map>java -Dfile.encoding=UTF-8 -Xmx5g -classpath ./class OSMUtil -bbox japan mid 9
BBox max 12980KB c:\map\data1\japan-mid9\454\201.dat
実行時間: 0.12分

c:\map>java -Dfile.encoding=UTF-8 -Xmx5g -classpath ./class OSMUtil -bbox japan high 10
BBox max 14578KB c:\map\data1\japan-high10\909\403.dat
実行時間: 0.23分

c:\map>java -Dfile.encoding=UTF-8 -Xmx5g -classpath ./class OSMUtil -bbox japan high 13
BBox max 5791KB c:\map\data1\japan-high13\7179\3255.dat
実行時間: 3.04分

高ズーム分割を 12、10 としてみる。最大ファイルサイズが大きくなるので、13、10分割の方を採る。

c:\map>java -Dfile.encoding=UTF-8 -Xmx5g -classpath ./class OSMUtil -bbox japan high 10
BBox max 19202KB c:\map\data1\japan-high10\909\403.dat
実行時間: 0.26分

c:\map>java -Dfile.encoding=UTF-8 -Xmx5g -classpath ./class OSMUtil -bbox japan high 12
BBox max 16729KB c:\map\data1\japan-high12\3592\1622.dat
実行時間: 2.04分

トータルのファイルサイズを問題にせず、最大ファイルサイズのみを小さくするには高ズームは zoom 13 だけで 分割する方がよい。合計ファイルサイズは 10.2GB、最大は 11MB である。 ファイルサイズは zoom 13、10分割の約3倍の大きさになるが、パフォーマンスは良い。 zoom 13、10分割では東京のレンダリングでは 5791KB+14578KB のファイルを使うところが zoom 13単独分割では 11157KB で済む。空間検索時間が短くなる。 抽出されるレコードはおなじであるから、レンダリング時間は変わらない。ただし、zoom 10分割ファイルは広範囲をカバーするため、 スクロールして、広い範囲を表示するときには、有利に働く。

6GB程度のファイルサイズ増加はストレージ容量としては問題でないが、 パソコンからAndroid機へのファイル転送時間が約3倍になるのは歓迎できない。

ということから、高ズームは zoom 13、10の2分割とする。数値は今後再び変更するかも知れない。

c:\map>java -Dfile.encoding=UTF-8 -Xmx5g -classpath ./class OSMUtil -bbox japan high 13
BBox max 11157KB c:\map\data1\japan-high13\7274\3225.dat
実行時間: 14.70分

Androidスマホ/タブレットにファイル転送する

c:/map/data4/bus_route.tsv(バスrelationデータ) を Map4/Common/guides/ にコピーする。

フリーワード検索用 c:/map/data4/osmdata_japan.dat を Map4/Common/ にコピーする。

最終的なOSMバイナリレコードファイルは Map4/dat/ にコピーする。

リファレンス

[1] OpenStreetMap Data Extracts
[2] JA:Overpass API
[3] JOSM