トップ地図アプリMap3 > 国土地理院地図を表示する

国土地理院地図を表示する

はじめに

最初に国土地理院地図の表示を行い、そこにGPS軌跡を描きたい。

国土地理院地図の場合、タイル画像をダウンロードして表示するだけのため、 レンダリングを要するOSM(OpenStreetMap)地図の表示に比べればはるかに簡単である。

ただし、OSM地図表示も考慮したプログラムの枠組みとするため、国土地理院地図だけを考えたプログラムよりは複雑となる。

プログラム構造

並列処理

現有するAndroidスマホおよびAndroidタブレットは共に8コアであり、ダウンロードやOSMレンダリングは並列処理で行う。 現在はスマホ、タブレットとも、スレッド数は 6 にしている。

国土地理院地図表示については、以前、新しいスタイルの並列処理(並列ダウンロード)も試みたことがある。 プログラムはスリムになるが、並列レンダリングのことを考え、現在は、古典的な並列処理方法を採用している。

OSMレンダリングが中心のため、RenderThread としているが、ダウンロードも担っている。

Thread.sleep(100)を使うのはスマートではないが、電池使用量上のロスはさほどない。 スクロールやズーム変更時には大量のリクエストが生まれるため、sleepしないため、レスポンス上のロスはない。

静止からスクロールが起きた場合、リクエストが生まれるが、全スレッドが sleep 状態であるため、待ち状態となる。 6スレッドが待ち状態に入るタイミングはずれてくるため、平均的には数十msの待ちで、リクエストの処理が始まる。 この待ち時間より、ファイル読み込み/ダウンロード/レンダリング の時間の方が大きいので、100ms で問題はない。 もっと小さい値に変えてもいいが、表示が速くなるわけではない。

スレッド数は1とか2よりは、6とか7の方がいい。5~8では大差がない。 スレッド数が大きいほど、メモリ使用量が増えるため、現在は6にしている。

一つのタイルの準備が終わるごとに、画面全体を再描画[map.invalidate()]するのは、効率は良くない。 しかし、全タイルの準備が終わってから、再描画するのも良くない。 synchronized (map.objLock) { map.invalidate(); } は、その中間で、 再描画回数を少し抑止する効果がある。

再描画については改善の余地はあるが、プログラムは複雑にしたくない。

public class RenderThread extends Thread {

    static int NumThreads;
    static Thread[] threads;

    static boolean run;
    int thread_number;
    Renderer renderer;
    Map map;

    public RenderThread(Map map, int num) {
        this.map = map;
        this.thread_number = num;
        this.renderer = new Renderer(map, num);
        System.out.printf("RenderThead #%d ready.\n", num);
    }

    public void run() {
        run = true;
        while (run) {
            Tile tile = Tile.getRequest(map.src, map.zoom);
            if (tile != null) {
                tile.status = Tile.Status.busy;
                renderer.render(tile);
                tile.status = Tile.Status.ready;
                synchronized (map.objLock) {
                    map.invalidate();
                }
            } else {
                try {
                    Thread.sleep(100);          // 100ms
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

タイル画像のキャッシング

地図は 256x256画素のビットマップ(タイル)を張り合わせた形のものとなる。 中心は Bitmap bmp であるが、属性と合わせてクラス Tile で管理する。

当初はタイルのリクエストはキューで管理していた。多重リクエストを避けるために、別途 HashSet も用いていた。

現在は、Tile配列で管理している。タイルの検索は配列の順次探索としている。 現在の配列サイズは 64 であり、この程度の数であればハッシュを使うまでもない。

タイルは Source、zoom、x、y で特定できるが、これらを一つの long key としている。 従って、探索は long変数の比較となる。 更なる高速化が必要であれば、long値のハッシュ表を使うことになる。 

タイルは最初に初期化処理で生成されるため、配列の要素が null ということはないが、念のため、 チェックしている。

タイルの状態(Status)は、最初は free(空き)である。タイルの表示では、該当タイルがなかった場合、 この時点で alloc()メソッドでタイルを割り当て、waiting(読み込み/ダウンロード/レンダリング待ち)状態となる。

alloc() は空きを探すが、空きがあるのは最初だけで、すぐになくなる。 現在のプログラムは常に空きがないか探しているので、非効率である。

通常は、古いタイルを廃棄して、その場所を割り当てている。FIFOに近い方式である。 前段で空きを探す無駄がある。前段と後段を一体化する、あるいは、LRU方式に変えるなど改良の余地がある。 パフォーマンス上は全タイルをサーチしても問題はないので、LRU方式がよいかも知れない。 空きがあれば、その時点でスキャンをやめればよい。

RenderThread は waiting 状態のものをサーチする。

地図のソースや zoom が変わった場合、以前のリクエストは廃棄してよい。この破棄は getRequestメソッドで行っている。

public class Tile {
    enum Status { free, waiting, busy, ready }
    enum Source { japan, kanto, hot, gsi, ort, osm }

    final static String OSM = "https://a.tile.openstreetmap.org/";
    final static String GSI = "https://cyberjapandata.gsi.go.jp/xyz/std/";
    final static String ORT = "https://cyberjapandata.gsi.go.jp/xyz/ort/";

    final static Tile[] cache = new Tile[64];
    static int ixEntry = 0;

    int ixCache;            // cacheのインデックス
    Status status;
    Bitmap bmp;             // 256x256画素
    Source src;             // 地図のソース
    int zoom, x, y;         // タイルアドレス
    String url, dir, path;
    long key;

    public Tile() {
        this.bmp = Bitmap.createBitmap(Map.PX, Map.PX, Bitmap.Config.ARGB_8888);
    }

    static void initialize() {
        for (int i = 0; i < cache.length; i++) {
            cache[i] = new Tile();
            cache[i].ixCache = i;
            cache[i].status = Status.free;
        }
    }

    void set(Source src, int zoom, int x, int y, long key) {
        this.src = src;
        this.zoom = zoom;
        this.x = x;
        this.y = y;
        this.key = key;     // ユニーク
        this.dir = Map.DIR + "tiles/" + src.name() + "/" + zoom + "/" + x;
        this.path = this.dir + "/" + y + ".jpg";      // download先
        if (src == Source.japan || src == Source.kanto || src == Source.hot) {
            this.url = null;
        } else {
            String zxy = zoom + "/" + x + "/" + y;
            this.url = src == Source.osm ? OSM + zxy + ".png" :
                       src == Source.gsi ? GSI + zxy + ".png" :
                       src == Source.ort ? ORT + zxy + ".jpg" : null;
        }
    }

    // サイクリックに使用
    static Tile alloc(Source src, int zoom, int x, int y, long key) {
        synchronized (cache) {
            for (Tile tile : cache) {
                if (tile != null && (tile.bmp == null || tile.status == Status.free)) {
                    tile.set(src, zoom, x, y, key);
                    tile.status = Status.waiting;
                    return tile;
                }
            }
            while (true) {
                Tile tile = cache[ixEntry];
                ixEntry = (ixEntry + 1) & 0x3f;     // キャッシュサイズ=64
                if (tile == null) continue;
                tile.set(src, zoom, x, y, key);
                if (tile.status != Status.busy && tile.status != Status.waiting) {
                    tile.status = Status.waiting;
                    return tile;
                }
            }
        }
    }

    static Tile get(long key) {
        synchronized(cache) {
            for (Tile tile : cache) {
                if (tile != null && tile.bmp != null && tile.key == key) {
                    return tile;
                }
            }
            return null;
        }
    }

    static Tile getRequest(Source src, int zoom) {
        synchronized(cache) {
            for (Tile tile : cache) {
                if (tile != null && tile.status == Status.waiting) {
                    if (tile.src == src && tile.zoom == zoom) {
                        return tile;
                    }
                    tile.status = Status.free;
                }
            }
            return null;
        }
    }
}
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (!initialized) {
            return;     // まだ初期化が終わっていない
        }

        int MAX = (int)Math.pow(2,zoom);

        int offX = (CX - (W >> 1)) % PX;
        int offY = (CY - (H >> 1)) % PX;
        int bx = BX(0);
        int ex = EX(0);
        int by = BY(0);
        int ey = EY(0);

        for (int xx = bx; xx < ex; xx++) {
            int x = (xx + MAX) % MAX;
            for (int y = by; y < ey; y++) {
                long key = key(src, zoom, x, y);
                Tile tile = Tile.get(key);
                if (tile == null || tile.bmp == null || tile.status == Tile.Status.free) {   // キャッシュにない
                    Tile.alloc(src, zoom, x, y, key);      // waitingになる
                } else if (tile.status == Tile.Status.ready) {
                    int tx = (tile.x + MAX) % MAX;
                    drawBitmap(canvas, tile.bmp, PX * (tx - bx) - offX, PX * (tile.y - by) - offY);
                } else if (tile.status != Tile.Status.busy &&
                        tile.status != Tile.Status.waiting) {
                    System.out.printf("onDraw error: status=%s, path=%s\n",
                            tile.status.name(), tile.path);

                }
            }
        }
    }

プログラムの簡単化

FIFO方式のままであるが、allocメソッドを簡単化した。 立て続けに zoom を変更して、 全てのタイルが busy か waiting であれば、割り当ては起きない。

    static void alloc(Source src, int zoom, int x, int y, long key) {
        synchronized (cache) {
            for (int n = 0; n < cache.length; n++) {
                Tile tile = cache[ixEntry];
                if (tile.status != Status.busy && tile.status != Status.waiting) {
                    tile.set(src, zoom, x, y, key);
                    tile.status = Status.waiting;
                    break;
                }
                ixEntry = (ixEntry + 1) & 0x3f;     // キャッシュサイズ=64
            }
        }
    }

次のようにすれば立て続けの zoom 変更にも対応できる。zoom の異なる待ちはキャンセルされる。 現在 busy のものはキャンセルされないが、その数は高々スレッド数に過ぎない。

    static void alloc(Source src, int zoom, int x, int y, long key) {
        synchronized (cache) {
            for (int n = 0; n < cache.length; n++) {
                Tile tile = cache[ixEntry];
                if (tile.status != Status.busy &&
                        (tile.status != Status.waiting || tile.zoom != zoom)) {
                    tile.set(src, zoom, x, y, key);
                    tile.status = Status.waiting;
                    break;
                }
                ixEntry = (ixEntry + 1) & 0x3f;     // キャッシュサイズ=64
            }
        }
    }

履歴およびメモ

2023.11.27 間違ったタイルが表示されることがある【保留】

今回見つかったのは 15-29083-12917 2023/11/1 であった。以前のMapXプログラムにバグがあったのかも知れない。 新しいデータで間違いがあった場合、原因を調べることにする。

13-7272-3228 2023/11/26 も間違っていた。昨日ダウンロードしたものであり、明らかに、現在のプログラムにバグが残っている。

ロック(排他制御)に問題があり、間違った場所に書き込まれたのかも知れない。

onDrawで alloc され、path と url が決まる。ここでの誤りは考えにくい。この時点では、割り当てられたタイルには、 直前の ビットマップデータが残っていることが多い。新たな url に対するダウンロードに失敗した場合、 この古いデータが書き込まれるのかも知れない。しかし、ダウンロードしたデータを書き込んでいるので、ここにエラーがあるわけではなさそう。

ダウンロードには時間がかかり、パラレルに実行するため、url と path に食い違いが起きるのであろうか?

これまでに見つかったエラーは5、6タイルである。 MapXが生んだエラーかも知れないので、MapXと map3 ではタイル画像の保存先を変更した。map3 でもエラーが見つかったとき、調べなおす。

リファレンス

[1]