トップ地図アプリGIS > 地図アプリ GIS の基本方針

地図アプリ GIS の基本方針

地図アプリ: 最初の一歩

基本方針

Windows PC/Tablet版をAndroidスマホ/Tabletに移行してまもなく3年、実時間レンダリングをベースとしたため、 レスポンスを追求してきた。

レンダリング結果をファイルに保存し、次回からこれを表示する方式に変える。 変更があった場合には、バックグラウンドでタイル画像を更新して、再描画する。

特に、低中ズームのレンダリング時間は数倍~10倍に増えてもいいので、プログラムの簡略化に重点を置く。

座標値データ

レコード上ノードの座標値は極座標を4バイト整数化して扱う。 座標値列データは lon、lat 順の int配列とすることもあるが、relation処理で複数の way をつなぎ合わせて 一つの polygon とする場合などでは lonを上位4バイト、latを下位4バイトとして一つの long型整数とした方が プログラムがシンプルになる。

従って、int lon, lat; とするときと long lonlat; とすることがある。

座標値データの精度

OSMデータの精度は小数点以下7桁の精度である。高ズームでは 10,000,000 をかけて整数化する。

中低ズームではこれほどの精度はいらない。 中ズームでは、1,000,000、低ズームでは 100,000 をかけて整数化する。

ラインやポリゴン座標値列データは先頭は座標値そのものの4バイトペアとする。 次からは前との差分を2バイトペアで表す(以下、差分コードと呼ぶ)。

差分が2バイトで表せないときは、中間に最低限必要な数だけノードを追加挿入して、 差分が2バイトで表せるように補正しておく。

座標値列データ全体を4バイトペアで表すよりも、 このような差分形式とする方が平均的なレコードサイズを3、4割ほど縮小できる。

差分をその大きさにより、1, 2, 3, ... バイトで表す可変長バイトコードの方が平均レコードサイズの縮小効果は大きいが 復号に時間がかかる。

バイナリレコード(ブロック)ファイルはメモリにキャッシュされ、通常、 ファイルからの読み込みよりも、そこ(キャッシュ)からの取り出しの方が多い。 このため、可変長バイトコードよりも差分コードの方が有利となる。

中低ズームの精度は小数点以下7桁ではなく、6桁、5桁とした方が、差分コード、可変長バイトコード ともバイナリレコードファイルが小さくなる。差分コードの場合、ノードの挿入が減少または殆どなくなる。

LRUキャッシュ

地図アプリでは様々なキャッシュを備えている。まず、タイル画像データキャッシュがある。 現状では、画面には最大で12タイルが表示される。現在、30タイル分をキャッシュしている。

レンダリング用としてバイナリレコード(ブロック)キャッシュ(現在、10ブロック)がある。

上記の両キャッシュはこれまではそれぞれ独自プログラムで実装していた。

地図アプリ GIS では、LinkedHashMap を使ったシンプルな実装(LRUCache)を共通的に使用する。

今回新たに、等高線表示をサポートする。ここでも、同じLRUキャッシュを使う。

キャッシュはメモリを消費する。メモリが問題にならなくても、サイズは大きいほどいいとは限らない。 サイズが大きすぎると、古いデータが長期間保存される。 廃棄するとき、ガーベージコレクションの負担が大きくなる可能性がある。

地図アプリMap4では、OSMの各種配列データの再利用を試みた。サイズがあまり大きくなるのは好ましくないので、 一旦、解放するときがある。中途半端なやり方は逆効果になる恐れがある。

現アプリGISでは、OSMの各種配列データは一切再利用していない。全て、動的に確保し、レンダリングが終われば、 自動的に解放される。使用期間は短いために、ヒープ領域の浅い位置にある。

レンダリングでも、動的確保を極力さけてきた。効果はあるとしてもプログラムを分かりにくくする。 動的確保しても、すぐに解放するならば、ガーベージコレクションの負担は小さい。

タイルキャッシュのデータ更新

国土地理院地図の場合、ダウンロードしたファイルを表示するだけのため、プログラムは簡単である。

Androidでは、メインスレッド(UIスレッド)では、ストレージからのファイル読み込みやダウンロードができない。

次のようにすれば、ファイル読み込みやダウンロードを別スレッドで実行できる。

  Tile tile = Tile.get(src, zoom, x, y); //
  if (tile.bmp != null && tile.status == Tile.Status.ready) {
      // cache にあれば OSM と GSI は共通処理
      int tx = (tile.x + MAX) % MAX;
      drawBitmap(canvas, tile.bmp, PX * (tx - bx) - offX, PX * (tile.y - by) - offY);
  } else if (fGSI) {
      if (tile.bmp == null && tile.status != Tile.Status.busy) {
          tile.status = Tile.Status.busy;
          newCachedThreadPool(5).execute(() -> {
              tile.bmp = loadBitmap(tile.name + ".webp");
              if (tile.bmp == null) download(tile);
              tile.status = tile.bmp != null ? Tile.Status.ready : Tile.Status.free;
              // Mainスレッドで実行する
              HandlerCompat.createAsync(getMainLooper()).post(this::invalidate);
          });
      }
  } else {  // OSM
      synchronized (Updater.objLock) {
          Updater.objLock.notify();
      }
  }

上のプログラムでは引数を 5 としているが、同時実行スレッド数が5以下であるかどうかは未確認である。

OSM地図の場合、ストレージにファイルがあったとしても、読み込んだビットマップ画像に 等高線やバス路線などを動的に追加描画することがあるため、上記の並列処理方法は使っていない。

固定数の更新専用スレッドが待機しており、notify()メソッドでそこにシグナルを送るだけである。

リファレンス

[1]
[2]