これまでは、簡単化も図ったが、どちらかと言えば、パフォーマンスに拘り、プログラムは複雑になってきた。 Map4 では原点に立ち戻り、シンプル化を目指した。使用開始はさらに数か月先と考えている。
シンプル化された面は多々あるが、逆に複雑さを増した面もある。
パフォーマンス上、毎回レンダリングするのではなく、レンダリングしたタイルの利用に方針を切り替える。 これにより、レンダリングに多少時間がかかってもよい。
可変長バイトコードやブロックバッファのページ管理も試した。 極端に難しいわけではないが、なるべくシンプルにという簡単からは採用を見送りたい。
大きな変更点は以下の通り。
アプリの要になるのは、OSM地図のレンダリングに使うOSMバイナリレコードのフォーマットである。
現在は Encoder、Parser、Devider、ByteCoder の4ステップで最終的なOSMバイナリレコードを作成している。 特に、可変長バイトコード化は当初は考えていなかったため、手順が増えた。 いずれ、DeviderとByteCoderは一体化したい。
バイナリレコードフォーマットは簡略化したが、管理データを含めたため、その分、複雑になってしまった。
OSMデータ提供サイトからは圧縮形式(pbf)のファイルをダウンロードする。日本地図領域のファイルサイズは 2024年3月26日時点で 1.87GBであるが、xmlテキストファイルに解凍すると、42GBになる。
アプリ開発段階ではファイル形式を修正するたびに、ファイルの作り直しとなる。
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
japan.osm(42GB) の変換結果は node0.obf(106MB)、way0.obf(1GB)、way1.obf(1GB)、way2.obf(995MB)、relation0.obf(38.7MB)、全体で3.11GB となった。
OSM の xmlデータには Nodeデータが無数にあるが、殆どが Way を構成する頂点(ノード)の座標値であり、 Wayレコードに吸収された。単独で意味を持つごく少数のノードのみが node0.obf に格納されている。
このフォーマットは Map4の開発当初より変更していない。 いわば、テキスト形式をバイナリ形式に変換しただけであり、これでは地図のレンダリングはできない。
デジタル地図を構成するデータは 点と線と面 である。OSMでは Node は点(point)、Way は線(line)か面(polygon)を表す。 Relation はそれらからなる総合体である。例えば、バス路線relationは、バス停(point) とバス路線(line) からなる。 都道府県や市区町村境界 relation は、polygon または multipolygon である。 multipolygon は一般的には複数の outer polygon とそれぞれの outer polygon には 0 個以上の inner polygon を含む。 inner polygon は例えば、横浜市境界relationにとっては 横浜市(outer polygon)にある鎌倉市の飛び地(inner polygon)のようなもの。鎌倉市境界relation にとっては、 outer polygon は鎌倉市本体の outer polygon 以外に、この飛び地も outer polygon の一つである。
広域森林も木のない所は inner polygon であるが、この inner polygon は別のレコードでは 公園polygon であったり、 住宅地 polygon であったりすることがある。
relation は自分だけのメンバーを持つこともあれば、 他の独立した point、line、polygon、relation をメンバーとすることもある。
Map4ではレンダリングに一切使わない relation も多い。また、レンダリングに使わないタグも多くある。 これらのタグは OSMバイナリレコードには含めていない。
現在のフォーマットを下に示す。最終形ではないので、いずれ、もう少し分かりやすくすべきであろう。 見かけの項目を減らすことに拘り過ぎている。{num_nodes}* が分かりにくい。また、type と length を分離した方がいい。
head, num_nodes, {lon,lat}*, {key,val}* ... point/line/polygon head, {num_nodes}*, {lon,lat}*, {key,val}* ... multipolygon head: 第0,1bit(0x03) 0: point、 1: line、 2: polygon、 3: multipolygon 下位3バイト レコード長 num_nodes: multipolygonの場合、最終要素(最終inner polygonノード数)の最上位ビットは 1 とする。
メッシュ(zoom相当)に分割するのみで、バイナリレコードフォーマットは上記と同じ。
高ズームに限り、二つに分ける。中ズーム 8、高ズーム 8、12 も試した。 合計サイズは少し小さくなるが、最大ファイルが大きくなる。
合計サイズが多少増えても最大サイズを小さくしたい。
低ズーム zoom 3 max 10.3MB 7_3.dat 合計 21MB 中ズーム zoom 8 13.4MB 224_101.dat 263MB 高ズーム zoom 8 22.9MB 224_101.dat 218MB zoom 12 3.4MB 7273_3225.dat 2.1GB
高ズームは zoom 12 では重複が4以上(一つのレコードが4タイル以上にまたがる)の場合 zoom 8 に置いた。
このとき、zoom 8 の最大が大きく、zoom 12 は小さいので、 6タイル以上またがるとき、zoom 8 に置く場合について調べた。
高ズーム zoom 8 18.9MB 224_101.dat 202MB zoom 12 3.55MB 7273_3225.dat 2.3GB
高ズームでは、zoom12で重複が4以上のとき、zoom 9 に置いてみた。
高ズーム zoom 9 13.2MB 448_203.dat 322MB zoom 12 3.4MB 7273_3225.dat 2.1GB
高ズームでは、zoom12で重複が16以上のとき、zoom 9 に置いてみた。
高ズーム zoom 9 11.2MB 448_203.dat 205MB zoom 12 3.55MB 7273_3225.dat 2.68GB
低ズーム zoom 3 max 10.3MB 7_3.dat 合計 21MB 中ズーム zoom 9 5.6MB 448_203.dat 265MB 高ズーム zoom 9 11.2MB 448_203.dat 218MB zoom 12 3.55MB 7273_3225.dat 2.68GB
最終的には境界ボックスが加わるので、この段階では、これで進める。 境界ボックスの比重が高いのは 高ズームの zoom 12 である。 高ズームの zoom 9は巨大なレコードだけなので、境界ボックスを加えてもサイズは少し大きくなるだけである。
高ズームでは、zoom 9のファイルの入れ替え頻度は小さいので、 zoom 12に比べて大きいことはさほど問題ではない。
ページ分割によるメモリ共用は面倒であるが、サイズ 15、3MBをそれぞれ複数用意して それらを共用する程度であれば、プログラム的には楽であろう。
最大値は後で調べるが、10~20%程度多めのバッファを用意しておく。
まずは 15MB x 10 + 3MB x 30=240MB 程度とする。 適正数は実測で決める。 15MB、5MB、2MB の3区分とする案もある。大都市では大きなファイルが多数あり、 地方では小さいファイルが中心になる。1MB も多数用意しておけば、小さいファイルは吐き出されることが 少なく、地方が優遇される。大都市のタイル更新では小さなメモリは使われない。
可変長バイトコードを使えば、ファイルサイズは小さくなるが、 空間検索のために、メモリ上は管理データが必要となる。 このため、差分コード化に戻す。
ブロック分割されたファイル毎に実行する。
レコードフォーマットは以下の通りとする。
head(1), length, [num_nodes], {lon,lat}*, {key,val}* ... point/line/polygon head(1), length, num_polys, {num_nodes}*, {lon,lat}*, {key,val}* ... multipolygon head: 第0,1bit(0x03) 0: point、 1: line、 2: polygon、 3: multipolygon
head(レコード type) は1バイト固定、 length、 num_polys、num_nds は 符号なし可変長バイトコード である。
length には、head と length 自体の長さは含まれない。
先頭の lon、lat は4バイト固定整数ペアで、次のノードから 可変長バイトコード(符号あり)とする。
ただし、multipolygonの場合、各polygon毎に先頭は4バイト固定整数ペアとする。
タグ部 {key,val}* は入力ファイルのものと同じ。
レコード毎に空間検索に使う境界ボックスなどの管理データを持つ。
メモリ上では int配列とする。先頭は flag;type;length、flag は width、height が各2バイト、合わせて4バイトの時 0、それぞれ4バイトの時 1 とする。type 0(Pointレコード)のときは width;height はなし(共に値は1なので省略)。
head;length(4), minlon(4), minlat(4), width;height(0/4/8) head: 第0,1bit(0x03) 0: point、 1: line、 2: polygon、 3: multipolygon 第4bit(0x10) 0: width, height は2バイト、 1: width, height は4バイト 第5bit(0x20) 0: nameタグなし、 1: nameタグあり
nameタグの有無により、空間検索でのマージンを変える。
バイナリレコードはバイト配列としてそのままメモリに読込む。 ファイル先頭部の管理でーたは int配列に格納する。
class Block { byte[] vals; // OSMバイナリレコード(ファイル全体をbyte配列として読込んだもの) int[] idxs; // 境界ボックスなど // その他 }
Windows版地図では、最初の数年間は、事前にタイルを生成して、それを Windowsタブレットにコピーして地図を表示していた。 Android版ではその方法はとらず、リアルタイムで表示時点でタイル画像をレンダリングしている。 画面全体のタイル画像を1秒前後で生成できれば、さほど不都合はない。
しかし、地図の精度を上げて見栄えを追求すると、それだけレンダリングに時間がかかる。 そこで、Map4では、一度レンダリングで作成したタイル画像はストレージに保存することにした。二度目からは保存したタイル画像を読み込む。 初めての場合のみ、作成し、保存する。
現在、中・低ズームでこの方法を採用している。
高ズームではレンダリングが状況によって変わることがある。例えば、バス路線図の場合、選択範囲のバス路線図を表示したり、 選択した特定の路線だけを強調表示したりしている。
これに対しては、ベースとなるタイル地図画像だけをファイルにセーブしておき、 選択範囲のバス路線のレンダリングはベース画像を読み込んで、そこにバス路線だけを上書きすればよい。
将来的には、地図に同時に全ての地物のアイコンや名称を表示するのではなく、選択した特定の地物だけを表示できるようにしたい。 このとき、問題になるのは、アイコンや名称(文字)の重なりである。
店のアイコンや名前などを除いたものをベース地図とする。地名、道路名、交差点名、信号機などはベース地図に含まれる。 ベース地図に含まれるアイコンや文字列との重なりを回避する場合、画像データからある場所に文字やアイコンが描画されているか どうかを判断するのは簡単ではない。レンダリングでは、描画領域を矩形としてリストに登録している。 登録済みの矩形と重なる場合には、描画を止めている。
保存するのはタイル画像だけではなく、画像とセットで、ベース地図の矩形リストを保存しておく。 ベース地図にアイコンや文字列を上書きするときは、この矩形リストを読み込み、重なり回避を続ける。