トップ地図アプリMap3 > OSMレンダリングの基礎

OSMレンダリングの基礎

はじめに

OSM(OpenStreetMap)地図の場合、国土地理院地図とは異なり、デジタルデータを使ってレンダリングを行う。 自由度は極めて高く、自分好みの地図を実現できるが、プログラム規模は何十倍もの大きさとなる。

元となるデータは xml形式の巨大なファイルであるが、予め、パソコンで加工して、 レンダリングに都合のいいバイナリ形式のファイルとする。一つのレコードは point、line、polygon を表す。 polygon には単純な一つの閉ループからなる多角形データだけではなく、ポリゴン(outer polygon)の中に、 複数の穴(inner polygon)があるマルチポリゴンも含まれる。

xmlファイルからバイナリレコードを作るのも簡単な仕事ではないが、これにつては別途詳細に述べる。

OSMレンダリングでは、現在(MapX)、ベースとなるクラスとして Block、OSM、TileToRender、SuperRenderer があるが、できれば TileToRender と SuperRenderer はなくしたい。

BlockはOSMバイナリファイルを管理するものであり、OSMは個々のバイナリレコードを管理するものである。

自作OSM地図は日本地図が対象であるが、陸地については世界全体が対象となる。 OSMデータベースにとっては、海岸線データであるが、レンダリング上は polygonデータとして扱う。 特に、大陸は巨大であり、日本地図に限っても、本州、北海道、九州などは巨大なポリゴンであり、 中高ズームではそのままでは描画できない。 このため、小さな無数のポリゴンに分割したバイナリレコードにより、陸地を描画する。

陸地は単なるポリゴンであるため、陸地だけのレンダリングであれば比較的簡単である。 この陸地のレンダリングにより、OSMレンダリングの基礎を築きたい。

タイル地図のレンダリング

国土地理院地図、OSM地図は共に、Google Mapに準拠しており、パソコン時代に生まれたものであり、 タイルのサイズは 256x256画素である。近年のスマホはパソコンよりもはるかに高解像度であり、 縦横3倍の 768x768画素に拡大すると文字が丁度よい大きさとなる。 アンチエイリアシングにより、文字の見栄えは悪くはないが、小さな文字はややシャープさに欠ける。

国土地理院地図のタイル画像は 256x256画素であるため、これを縦横3倍の 768x768画素に拡大するしか方法はないが、 OSM地図の場合には、レンダリングでは 256x256画素のタイル画像(ビットマップ)を作成して、表示するときに拡大する方法と 最初からレンダリングで 768x768画素のタイル画像を作成する方法がある。

道路、境界線や森林、公園、建物など図形は 256x256画素の方がレンダリング時間が短縮され、拡大しても見栄えはさほど損なわれない。 しかし、特に小さな文字の見栄えはやや悪くなる。

最初に図形だけを256x256画素タイルにレンダリングして、それを 768x768画素に拡大してから文字を描画するならば、都合がいいが、 実際の地図のレンダリングでは、図形と文字の描画を前半と後半にまとめることはできない。

まず、陸地、広域森林など時間のかかる強大なポリゴンのレンダリングを 256x256画素で行い、 それを768x768画素に拡大してから残りの全てのレンダリングを行う。 少し複雑になるが、レンダリング時間の短縮と文字の見栄えを良くするために、この方法を採用したい。

現在は 64タイルをキャッシュしている。タイルサイズが256x256画素(256x256x4=256KB)であれば、合計16MBであるが、 タイルサイズを縦横3倍にすると、144MB になる。Androidアプリとしては小さな負担ではないが、 現在は十分に余裕のある値である。

国土地理院地図を表示している場合は、キャッシュするタイルのサイズを 256x256画素にできるが、 切り替えには負担がかかるため、キャッシュ上のタイル画像は 768x768画素とする。 国土地理院地図タイルの場合は768x768画素に拡大したビットマップをキャッシュする。

タブレットの場合、パソコンよりも画面サイズは小さいが、256x256画素のままで、拡大処理は一切行わない。

タブレットとスマホでは少し処理が異なるが、スマホが中心のプログラムとして、分かりやすいものとしたい。

MapXではどうしていたか

スマホの場合、ダウンロードで得られる 256x256タイル画像を 768x768画像に拡大表示しているが、 どんな手順であったか、調べてみる。

class Tile では this.bmp = Bitmap.createBitmap(Map.PX, Map.PX, Bitmap.Config.ARGB_8888); としているので、768 x 768画素のビットマップデータとなる。

ダウンロードした bmp をタイルの bmp に移すのは次のようにしている。 これでは拡大は起きず、768x768画素中の4分の1(左上隅)にコピーされる。

    void putBitmapToTile(Bitmap bmp, Tile tile) {
        int[] buffer = tileToRender.buffer;    //new int[256 * 256];    // 中継バッファ
        bmp.getPixels(buffer, 0, 256, 0, 0, 256, 256);       // bmp の pixels を buffer に読み出す
        tile.bmp.setPixels(buffer, 0, 256, 0, 0, 256, 256);  // buffer の値を tile.bmp の pixels に書き込む
    }

onDraw では下の drawBitmap を使っているので、ここで拡大が起きるはず。

    void drawBitmap(Canvas canvas, Bitmap bmp, int xoff, int yoff) {
        int xo256 = xoff * 256 / PX;
        int yo256 = yoff * 256 / PX;
        r256.set(xoff < 0 ? -xo256 : 0, yoff < 0 ? -yo256 : 0,
                xoff + PX < W ? 256 : (int)((W-xoff) * 256.0 / PX),
                yoff + PX < H ? 256 : (int)((H-yoff) * 256.0 / PX));
        rPX.set(Math.max(xoff, 0), Math.max(yoff, 0),
                Math.min(xoff + PX, W), Math.min(yoff + PX, H));
        canvas.drawBitmap(bmp, r256, rPX, paintCanvas);
    }

ダウンロードだけを考えれば、Tile の bmp に無駄があるが、描画のエラーはない。

OSMレンダリングの場合どうなるのか? 768x768画素タイルにレンダリングが行われているので、 onDrawでは拡大が要らないはずだが、同じdrawBitmap が使われる。なぜ、これで正しく描画されるのであろうか?

r256、rPX は必ずしも 256x256、768x768 になるとは限らず、拡大が起きるときと起きないときがあるのかも知れない。

このときのプログラムには描画上のエラーはないとしても、無駄があるかも知れない。今はこれ以上の追求をやめる。

履歴およびメモ

2023.11.29 タブレットでは陸地が正しく描画されるが、スマホでは描画がおかしい

MapXでは陸地の描画が正常であり、map3 も同じデータを使っている。

スマホ     #1 /storage/2EBA-4DDF/Map/lands_low1/1/0.dat 25ms 209KB
タブレット #1 /storage/emulated/0/Map/lands_low1/1/0.dat 16ms 209KB

スマホでは例えば下のような画面になる。大きなポリゴンでエラーが起きるようである。 何故、スマホとタブレットで異なるのだろう。MapXではこのようなエラーは起きない。

タブレットのタイルは 256x256画素のまま、一方、スマホは 768x768 画素に拡大している。

レンダリング方法を MapX とは変えた。そこに、拡大の考慮が漏れているのではなかろうか。

拡大は onDraw時に行っている。したがって、以下のように修正した。

    public Tile() {
        // bmp = Bitmap.createBitmap(Map.PX, Map.PX, Bitmap.Config.ARGB_8888);
        bmp = Bitmap.createBitmap(256, 256, Bitmap.Config.ARGB_8888);
        canvas = new Canvas(bmp);
    }

    void set(int zoom, int x, int y) {
        reset();
        this.zoom = zoom;
        this.x = x;
        this.y = y;
        //fact = ((float) Map.PX) / (1 << (30 - zoom));  // 画素単位
        //xpx = (float)x * Map.PX;       // 画素単位
        //ypx = (float)y * Map.PX;
        fact = 256.0f / (1 << (30 - zoom));  // 画素単位
        xpx = x * 256.0f;       // 画素単位
        ypx = y * 256.0f;
    }

これで、スマホでも正しく描画された。

MapX はレンダリング時に拡大しており、onDrawでは拡大していなかった。 この方が、精度は高くなるが、必要メモリは大きくなり、処理時間もかかるだろう。 map3 では、ダウンロード画像と合わせて、レンダリングでは 256x256画素として、 表示で 768x768 とする。

しかし、実際上はパフォーマンスと文字の見栄えを両立させるため、 陸地や広域森林などのレンダリングは 256x256画素、その他は 768x768画素としたい。

2023.11.29 陸地ポリゴンレンダリングに着手

TileToRenderer と SuperRenderer をやめたため、かなりのプログラム修正となった。

空間検索で陸地ポリゴンレコードの抽出は確認できたが、描画は起きない。

読み込んでいるのが bx=0, by=0 のようだ。このブロックアドレスが間違いのようだ。 Rendererの zoom, x, y に値がセットされていない。

zoom, x, y をセットしたら、空間検索結果は1レコードとなった。しかし、描画されない。

polygon頂点座標が全て (0, 0) であった。

fact, xpx, ypx のセットが漏れていた。これを修正すると、陸地ポリゴンが描画されるようになった。

思ったよりトラブルは少なかった。

リファレンス

[1] 世界は1枚の画像から : グーグルマップのしくみを探る(1)