トップ地図アプリMap3 > 任意地点の標高が得られるようにする

任意地点の標高が得られるようにする

はじめに

現地点の標高は GPSで得られるが、日本地図上の任意地点の標高が得られるようにしたい。 標高データは国土地理院サイトから一括ダウンロードすることもできるが、 個人利用では全地点の標高データを得たいわけではない。標高データはタイル地図と 同様にタイル単位でpng画像ファイル形式で提供されているものをその都度ダウンロードする。

入手した標高タイルファイルはローカルディスクに保存しておく。

標高を求めるプログラム

国土地理院が提供する標高データにはいくつかあり、精度の高いものは得られる範囲が限られている。 記事[4]に従い、精度の高いものから順に(「DEM5A」、「DEM5B」、「DEM5C」、「DEM10B」)入手することを試みる。

DEM5A~5C は zoom 15、DEM10B は zoom 14 のタイル座標名で提供される。

ダウンロードしたファイルはディレクトリ dem/5a、dem/5b、dem/5c、dem/10b 下に保存する。 ここで na は Not Available の略で、自作地図では日本地図領域としているが、 DEM10B にも標高ファイルが存在しない場所がある。 その場合、繰り返し調べる無駄を省くために、ファイル名だけのnullファイルを格納しておく。

zoom 14 と zoom 15 に分かれるため、注意がいる。

最初に dem/10b に nullファイルがあるかどうか調べて、そこに nullファイルがあれば、 標高データは一切存在しないことになるか、どうかである。 zoom 14のファイルは zoom 15の4ファイルに相当する。 zoom 15 のあるファイルがなければ、zoom 14 の DEM10B を探しに行くが、 そこになければ nullファイルが格納される。 しかし、zoom 15の4つのうちの別のファイルが zoom 15 に存在するかも知れない。 となると、zoom 14 の nullファイルのチェックではこれが分からない。

それゆえ、nullファイルは zoom 14 の DEM5NA(dem/5na)ディレクトリにおく。 DEM10B で探すが、見つからなかった場合は zoom 15でのファイル名で nullファイルを置く。

最初に、DEM5NAディレクトリを調べて、ここにあれば、それで終了する。ここになかった場合、 「DEM5A」、「DEM5B」、「DEM5C」、「DEM10B」の順に調べ、見つかったところで終了する。

タイル画像をダウンロードしてみる

DEM5Aのタイルは https://cyberjapandata.gsi.go.jp/xyz/dem5a_png/{z}/{x}/{y}.png でダウンロードできる。

東京駅を含むタイルの場合 https://cyberjapandata.gsi.go.jp/xyz/dem5a_png/15/29105/12903.png 48.8KB(50,065バイト) となる。この場合、DEM5B、DEM5Cの URL は https://cyberjapandata.gsi.go.jp/xyz/dem5b_png/15/29105/12903.png および https://cyberjapandata.gsi.go.jp/xyz/dem5c_png/15/29105/12903.png となるが、 いずれも存在しなかった。

DEM10Bは https://cyberjapandata.gsi.go.jp/xyz/dem_png/14/14552/6451.png 50.7KB(51,960 バイト) となる。 これをそのまま画像ファイルとして表示すると下図のようになる。

 
       DEM5A            DEM10B

この場合DEM10Bの画像を縦横二分した右下部分を拡大したものが DEM5A となる。

DEM5Aでは所々に白っぽいものが見えるが、データ不正か欠落であろう。

一つのタイルには256x256地点の標高データが含まれている。タイルとしては存在していても 部分的に欠落しているとなれば、保存するタイル画像は存在すれば DEM5A、DEM5B、DEM5C のいずれか と DEM10B となる。部分的に欠落している場合、周辺の平均値の方が精度が高いかも知れない。 また、データが欠落していても問題がない用途も多いと思われる。

最初のプログラム

まずは、地図の中心点の標高を zoom 14標高タイルとzoom 15標高タイルから求めて併記してみる。 一度ダウンロードしたタイルはストレージに保存する。また、zoom 14, 15 それぞれ最大4タイルをメモリにキャッシュする。

当面、標高タイル中の全ピクセル(256x256)の値が要るわけではないので、getPixelメソッドで特定ピクセルの値を取り出す。

getPixelsメソッドで、256x256要素の4バイト配列として取り出せる。多くの画素の値が要る場合には 画素単位で値を読み出すより、標高データの取り出しが迅速に行える。

画素の値から以下に従って標高値を取り出す[2]。

画素値(RGB値)から標高値h(m)の計算式は下記のとおりです。

uは標高分解能(0.01m)を表します。また、無効値は(R, G, B)=(128, 0, 0)です。

記事[4] では次のようにして標高 h を求めている。

    if (r != 128 || g != 0 || b != 0) {
      var d = r * this.pow2_16 + g * this.pow2_8 + b;
      h = (d < this.pow2_23) ? d : d - this.pow2_24;
      if (h == -this.pow2_23) h = 0;
      else h *= 0.01;
   }

非同期処理

ダウンロードはメインスレッドでは実行できない。タイル地図画像ファイルのダウンロードはOSMレンダリングと同様に、 スタンバイした複数のスレッドで実行している。この方法を採ることもできるが、ここでは、動的にスレッドを生成する方法を使ってみる。 立て続けに標高データのリクエストが生まれたとき、同じ標高タイルのダウンロードが同時に複数起きるため、これを避ける対策がいるかも知れない。

別スレッドの実行自体は次のように簡単であった。setElevation処理はスレッド立ち上げだけで終わるため、 別スレッドでの処理が終わらない内に次の実行により、新たなスレッドが立ち上げられることがある。 スレッドの重なり抑制はまだいれていない。

    void setElevation(int zoom, double x, double y) {
        // Singleの別スレッドを立ち上げる
        Executors.newSingleThreadExecutor().execute(() -> {
            Elevation ev = Elevation.getElevation(zoom, x, y);
            // 別スレッド内での処理を管理し実行する
            HandlerCompat.createAsync(getMainLooper()).post(() ->
                // Mainスレッドに渡す
                ele_view.setText(ev.src5.name() + ": " + ev.ele5 + "m dem_10b: " + ev.ele10 + "m")
            );
        });
    }

標高が表示されるようになったが、数値が間違っている。

RGB のところが RBG に間違えていた。

   int pixel = bmp15.getPixel(X15%256, Y15%256);
   //int v = Color.red(pixel)*(1<<16) + Color.blue(pixel)*(1<<8) + Color.green(pixel);
   int v = Color.red(pixel)*(1<<16) + Color.green(pixel)*(1<<8) + Color.blue(pixel);
   ele.ele5 = v == (1<<23) ? Float.MIN_VALUE :  // 無効
              v  < (1<<23) ? v * 0.01f : (v - (1<<24)) * 0.01f;

リファレンス

[1] GIS:日本のDEMデータの入手方法
[2] 標高タイルの詳細仕様
[3] 標高タイルの作成方法と地理院地図で表示される標高値について
[4] 標高を求めるプログラム
[5] Bitmap で Pixel 操作してみた
[6] [Android] HttpURLConnection GET で画像をダウンロードする