タイル画像のアーカイブを zip から独自ファイル形式に変更して、国土地理院の標準地図および 航空地図(オルト地図)で動作確認できた。
zipよりもはるかに高性能であり、タイルの更新、追加にも対応している。
ファイル構造はzipに倣っており、最初がタイルデータ部、末尾にタイル管理部、最後が全体管理部である。 タイルデータ部およびタイル管理部には更新タイルや追加タイルのための予備エリアを含む。 その詳細は後述する。
対象はタイル画像だけではなく、OSM(OpenStreetMap)地図のレンダリングに使うOSMバイナリレコードも 同じファイル形式とする(地図アプリでの追加/更新はないため、予備エリアはない)。
zip から 独自方式の切り替えは期間をかけて、少しずつ行うが、ここで、全体を考えた仕様としたい。
各エントリには次のローカルヘッダ(24バイト)を置く。
エントリデータの最終更新時刻 8バイト (ミリ秒単位の UNIXタイム) CRC32 チェックサム 8バイト 参照回数 4バイト エントリデータの長さ 4バイト(単位:バイト)
それほどの時刻精度はいらないので4バイトでもいいが、プログラムが分かりやすい。
CRC32は4バイトで表せるが、Javaには unsigned int がないため、long の方がプログラムが シンプルですむ。
ヘッダの 24バイトが全体に占める比率は微々たるもののため、以上のように決めた。
国土地理院地図の場合、センターに更新時刻を問い合わせ、保存データの時間より 新しければ、ダウンロードするが、ある地域の一部が更新されただけで、実際には更新されて いないことが多い。これをチェックサムで判定する。変更がないときは、 最終更新時刻だけを書き換える。
更新があった場合、古いエントリはそのまま残し、新しいデータを予備エリアに追加する。 予備エリアがたりなくなったとき、古いタイルを削除し、改めて予備エリアを確保する。 新規追加タイル分だけ、ファイルサイズは大きくなる。
この再構築時に、参照回数が極めて小さいものは除外してもよい。 一般にサイズが小さいタイルはダウンロードやレンダリングに時間がかからないため 必要が生じたときダウンロード/レンダリングすればよい。
次回、修正するときにでも、ローカルヘッダには id(zoom, x, y)を追加しておきたい。 この情報があれば、例えば、階層ディレクトリにエントリを展開するとき、 エントリ管理部に頼らず、エントリ本体部だけをシリアルにスキャンするだけで、展開ができる。
現状ではエントリ管理部に更新された古い情報もあるため、ローカルヘッダを使えば、 最新データだけではなく、過去のデータも得ることができる。
エントリ管理部はエントリ毎に次の 16バイトとする。
id(zoom, x, y) 8バイト(zoom 2バイト、x 3バイト、y 3バイト) offset 8バイト
予備エントリ分を含む。予備の場合 id は zoom=30, x=0, y=0 とする。 offset は何でもいいが、-1 としておく。
予備エントリの zoom は使われている zoom の最大値(現在は 21)よりおおきければ よい。
ファイル上では更新により、同じ id が存在するが、メモリ上では、 前からあるエントリの offset を更新することにより、同じ id が存在することはない。 また、エントリ追加では id 順に並び替えるため、ファイル上の並びとは異なる。
OSMバイナリレコードの場合は offset は4バイト範囲であるが、 タイル画像の場合は数10GB まで想定しているため、offset は8バイトとした。
メモリ上では id の昇順に並び替え、バイナリ検索を使う。
zoom は1バイトで十分なため、id 7バイト、offset 5バイトとすれば、 16バイトを12バイトに縮小でき、メモリ使用量が削減できる。
しかし、バイナリ検索プログラムを自作しなければならない。 標準APIですませるため、シンプルにしておく。
offsetReserved 8バイト(予備エリア先頭を指す) inUseEntries 4バイト(有効エントリ数) nEntries 4バイト(全エントリ数)
予備エリアおよび予備エントリ数は定期的にチェックして、 少なくなったら、警告する。
再構築処理の実行は数か月~数年に一度でよいので、自動起動とはしない。 バックアップをとってマニュアルで行う。
タイル更新の際、アーカイブファイルとしては、旧エントリ本体および旧エントリ管理データは そのまま残し、予備エリアおよび予備エントリ管理に追加登録するのは簡単である。
更新による追加が起きたアーカイブファイルを読み込もうとしたとき、 同じ id をもつエントリが二つ以上存在することに注意がいる。
そのまま読み込んでソートしてバイナリ検索を行うと、最新エントリの位置(index)が返される とは限らない。
旧エントリは無視して最新エントリだけをエントリ配列に読み込む必要がある。 エントリ配列の後部は予備のため、どのくらい重複があるか分からなくても配列は作成できる。 つまり、配列のサイズはファイル上のサイズと同じでよい。
エントリ配列に登録する際、id をキー、配列の index を値として HashMap に登録しておく。 登録前にこの HashMap を使って、既に、この id が登録済みかを調べる。 もし、登録済みであれば、後にある方が新しいため、エントリ配列に登録されているエントリの offset を新しいものに変更する。 HashMap に登録がなければ、エントリ配列に当該エントリ HashMap に id、配列index を登録する。
全てのエントリを登録すれば後は予備エントリとする。これでソートすれば、バイナリ検索が使える。
現在の総エントリ数は 20万程度である。 現在は全アーカイブファイルのエントリ管理部と全体管理部を メモリに読み込み、常駐させることができる。
200万エントリでも必要メモリは 32MB であるから問題ない。 1000万タイルとしも 180MB である。 しかし、一つのアーカイブファイルのサイズが大きくなりすぎる。
100万~200万タイルになった場合、 zoom 別のアーカイブファイルとする。
タイル画像については、Tile.Source の順序数をindexとした配列で 管理するのが分かりやすいであろう。
レンダリング用はこれとは別の管理方法をとる。