OSMデータは自由にカスタマイズできる[1]。OSMを使い始めて約8年になる。 当初は、フリーソフト osm2pgsql[2] を使って、日本地図領域のデータ japan-latest.osm.pbf を PostgreSQL/PostGIS データベースにインポートして、Mapnik[3] を使ってレンダリングしていた。
しかし、現在は、osm2pgsql や Mapnik は使わず、 japan-latest.osm.pbf を解凍した japan-latest.osm(xml形式のテキストファイル)を使ってレンダリングしている。
パソコンでここに述べるバイナリ形式のレコードファイルを作成して、 スマホではそのレコードを読み込んで直接、地図をレンダリング(描画)している。
osm_id は long、way_area は float、他は int である。
num_nodes は multipolygon の場合、先頭は outer polygon のノード数、 以降は inner polygon のノード数である。 line と polygon の場合は1要素で、ノード数となる。
head の上位3バイトは x0 以降のサイズ(単位: byte)とする。
head, x0, y0, x1, y1, rec_id, osm_id, [way_area,] [xc, yc,] {key, val,}+ {num_nodes,}* {x, y,}*
head:
第0,1bit 0: point、1: line、2: polygon、3: multipolygon
第4bit(0x10) xc, yc, 1:有り 0:無し
head にはレコードの長さも含んでいる。 空間検索では x0, y0, x1, y1 の値だけ読んで、次のレコードに進むときに、レコードの長さを使う。
line/polygonレコードの場合、num_nodes はなくてもレコードの長さから算出できるが、 multipolygonの場合、polygon毎のノード数が必要なため、これと形式を合わせた。
OSMのxml形式のパ-スは複雑で、処理時間もかかるため、低中高ズーム用ファイルの作成は標準ズーム用ファイルから作成する。
パース処理はこれまでと同じく極座標でよい。class OSM でレコードを管理しているが、 このコントラクタで極座標をXY平面座標に変換する。
x、 y座標値を合わせて一つの long 値とすることもできる。 x, y の範囲は 0 ~ 230 であるから、結果の long 値は非負 (0 ~ 262)となる。
{key, val}+ には例外的に OSMタグ以外の情報を置くことも許す。 地名描画の競合調整をパソコンの OSMParser で行い、その結果は 特殊な key、val とする。 この情報はごく一部のレコードだけに置かれるため、レコード自体に特別なフィールドを設けず、 このようにするのが効率的である。
OSMParserはプログラム規模も大きく、メンテナンスはその分、注意がいる。 また、プログラムの実行時間も大きい。このため、仕様の変更は少なくしたい。
これに対して、OSMDividerは小さいプログラムであり、実行時間も OSMParser の数十分の一に過ぎない。 このため、仕様の変更や実行のやり直しは簡単である。 出力レコード・フォーマットは基本的には OSMParser と同じであるが、細部は異なる。
なるべく差分の効果を大きくするため、基準の座標値 (x0, y0) を境界ボックスの中心座標とする。 (w, h) は中心から境界線までの長さ、即ち、境界ボックスの幅、高さの半分とする。
head, x0, y0, w, h, (minzoom, wid, zorder), osm_id, [way_area,] [xc, yc,] {key, val,}+ {num_nodes,}* {x, y,}* head: 第0,1bit(0x03) 0: point、1: line、2: polygon、3: multipolygon 第4bit(0x10) xc, yc, 1:有り 0:無し 第6bit(0x40) 0: GIS, 1: Map 追加2023.1.7 第7bit(0x80) 1: 差分座標、0: 絶対座標
(x0, y0) は、境界ボックスの中心座標で、常に絶対座標である。 (w, h) は境界ボックスの幅と高さのそれぞれ半分とする。
境界ボックスはマージンを加えて操作するため、通常の座標値よりも要求される精度は低い。
(w, h) および (xc, yc)、(x, y) の (x0, y0) からの差分座標が全て2バイトで表現できる場合にかぎり、 差分レコードとする。
多分、実際上は (w, h) が2バイトであれば、全ての差分座標が2バイトで表現できるであろう。 japanM.dat でそのことを確認したが、念のため、 全てのノードの差分座標が2バイトで表されることを確認しておく。
OSMDivider では writeShortメソッドのサポートは楽であるが、スマホではパフォーマンス上、 レコードを int配列に置くため、差分コードの場合、int 型変数を short型二つに分解する必要がある。 下位2バイトを short型として取り出す時注意がいる。
pointレコードの場合 (w, h) は省略可能であるが、レコード全体に占める比重は小さいため、 処理の単純化のために、(w, h) データを持たせる。値は (0, 0)である。差分レコード扱いとする。
minzoom 1byte、wid 1byte、zorder 2byte は合わせて4バイトである。
GISでは陸地は land-polygons-split-3857 を使っている。 Mapでは water-polygons-split-3857 を使うことを試している。 レコード長の大きいデータがあるため、データ長の単位を4バイトにする(バイナリデータは int 配列である)。 GISでは、レコード長はバイト単位である。当面、Mapでは GISのデータもそのまま使えるようにするため、 GIS/Mapフラグで、レコード長が byte単位か int単位かを区別して、両者に対応できるようにする。
地図アプリGISの場合、そのままでは Map用のデータは使えない。 これだけであれば、修正は軽微であるが、他にも違いが出てくるかも知れない。
メモリ使用量には余裕があるため、ファイルサイズの縮小は喫緊の課題ではない。 ディスクキャッシュの働きにより、ファイル読み込み時間が全体の地図表示時間に占める比率は極めて小さい。
復号時間がかかる高度な圧縮方法は論外であるが、境界ボックスの左上端を原点とする相対(差分)座標は サポートする。X座標とY座標(の差分値)をペアで4バイトで表現する。 これにより、全体的には3割程度のファイルサイズ縮小となる。
name、ref、operator などの OSMタグの値は任意の文字列である。 文字列には4バイト整数のコードを割り当て、 OSMバイナリレコードフォーマット上の val の値としている。
具体的には出現する文字列を改行区切り(テキストエディタで確認できる)の一つの大きな文字列(辞書) [char配列] としている。 各文字列の先頭位置(オフセット)をその文字列のコードとしている。
OSM地図データの基準は japan.osm であるが、この更新には全体で1時間ほどかかるため、 簡単なテストでは kanto.osm, kyushu.osm や、狭い範囲の最新データ hot.osm を使うこともある。 この場合、辞書は同じ方がスマホプログラムは楽である。 このため、OSMParser は、起動時に現在の辞書を CharBuffer cbWordsUTF16 に 読み込み、そのサイズを iniUTF16 に設定しておく。このとき、mapWords にも登録しておく。
パースでは、mapWords を使って、文字列をコードに変える。 mapWords になかった場合には、mapWordsおよび cbWordsUTF16 への追加登録を行う。
OSMデータの更新は数か月~半年に1回のペースなので、ほぼ間違いなく追加登録が起きる。 japan.osm、kanto.osm の順に更新した場合、kanto.osm では追加登録は起きない。 kanto.osm、japan.osm の順に更新した場合は、通常は kanto.osm で追加があり、 japan.osm でも追加がおきる。
iniUTF16 は追加があったかどうかを判別するために使う。
文字列タグの削除により、その時点では存在しない文字列が辞書に残ることがある。 量的には少ないので、問題はないが、気になる場合は、一旦、辞書ファイルを削除してから OSMParser を実行して、japanN.dat を作成すれば、現在使用中の文字列だけを含む辞書が作成される。 japanN.dat から、低中高ズーム用ファイル japanL.dat、japanM.dat、japanH.dat を作成する過程では辞書の更新は起きない。
可変長コードを使う場合、辞書は、よく現れる文字列順とした方が僅かであるが、 平均レコードサイズを小さくできる。OSMParser で出現頻度を調べて、出現頻度順の辞書を出力しておき、 その辞書を使ってパースし直せばよい。
しかし、全体としては文字列型タグを含むレコードの比率は小さいため、 そこで数バイトの短縮があっても、全レコードの平均サイズの縮小は微々たるものとなる。
スマホ側では辞書ファイルを int[] caWordsUTF16 に読み込むだけ。HashMap mapWords は要らない。
これまでは、辞書ファイルをそのまま char配列 caWordsUTF16 に読み込んでいた。 単語の終わりは1文字ずつチェックして '\n' が現れるまでとしている。
static void readWords(String file) { File f = new File(file); int file_length = (int) f.length(); caWordsUTF16 = new char[file_length/2]; try (DataInputStream dis = new DataInputStream( new BufferedInputStream(new FileInputStream(file)))) { int ix = 0; while (ix < caWordsUTF16.length) { char c = dis.readChar(); caWordsUTF16[ix++] = c; } } catch (EOFException e) { ; // ファイル読み込み完了 } catch (Exception e) { e.printStackTrace(); } } static String getString(int code) { try { if (code < caWordsUTF16.length) { int end = code; while (end < caWordsUTF16.length && caWordsUTF16[end] != '\n' && end - code < 1024) end++; return new String(caWordsUTF16, code, end - code); } } catch (Exception e) { e.printStackTrace(); } return null; }
単語の切り出しを少し簡単にするため、末尾に '\n' を置く代わりに、先頭に単語の文字数を置くことにする。
static void readWords(String file) { File f = new File(file); int file_length = (int) f.length(); caWordsUTF16 = new char[file_length/2]; try (DataInputStream dis = new DataInputStream( new BufferedInputStream(new FileInputStream(file)))) { for (int ix = 0, code = 0; ix < caWordsUTF16.length; ) { char c = dis.readChar(); if (c == '\n') { caWordsUTF16[code] = (char)(ix - code); code = ++ix; // '\n' の次(次の文字列の先頭=長さが入る位置) } else { caWordsUTF16[++ix] = c; } // 元の '\n' は無くなり、単語の文字は一つずつ後ろにずれ、先頭に長さが入る } } catch (EOFException e) { ; // ファイル読み込み完了 } catch (Exception e) { e.printStackTrace(); } } static String getString(int code) { if (code < caWordsUTF16.length-1) { return new String(caWordsUTF16, code+1, caWordsUTF16[code]); } return null; }