🌏

MapLibre GL JSと国土地理院 標高タイルで3D地形を表示する

2023/10/23に公開

はじめに

MapLibre GL JSでは地形データがあれば、3D地形を表示することができます。地形データはDEM(数値標高モデル)とも呼ばれ、MapLibre GL JSではDEMを地形データとして利用しますが、Terrain-RGB形式というエンコーディングのDEMが利用されます。また、国土地理院から標高タイル(DEM)が配信されていますが、今回は、国土地理院 標高タイルのうち、DEM10B PNG形式の標高タイルを使用して、MapLibre GL JSで3D地形を表示します。なお、国土地理院 標高タイルは、10mの地上解像度で配信されており、詳細な地形表現が可能ですが、Terrain-RGB形式ではないエンコーディングを利用しているため、Terrain-RGB形式に変換する必要があります。Terrain-RGB形式と地理院標高タイルの考え方の違いについては、下記の記事が詳しいです。

https://qiita.com/Kanahiro/items/e22594b738655a189c1d

デモサイト

  • MapLibre GL JSと国土地理院 標高タイルで3D地形を表示するデモサイトです(後述の方法2)。

altテキスト
富士山付近

RGB値の標高換算式(=エンコーディング)

Terrain-RGB形式

  • RGBピクセル値を3桁の256進数として捉え、横軸が縦軸を標高とした傾き0.1の単調増加関数になります。
Terrain-RGB形式の標高換算式
標高=-10000+((R値×256×256+G値×256+B値)×0.1)
  • 国土地理院 標高タイルをTerrain-RGB形式に変換したものです。

    富士山付近

標高タイル

  • 標高タイルの詳細仕様です。

https://maps.gsi.go.jp/development/demtile.html

  • 国土地理院 標高タイルも、Terrain-RGB形式とベースの考え方は全く同じで、計算式が異なり下記のとおりになります。
標高タイルの標高換算式
標高=(R値×256×256+G値×256+B値)×0.01
# ただし[R,G,B] = [128,0,0]を無効値とする
# ただし[128,0,1]以上の場合2^24を引き去る
  • 国土地理院 標高タイルです。

    富士山付近

MapLibre GL JSと国土地理院 標高タイルで3D地形を表示する方法

MapLibre GL JSと国土地理院 標高タイルで3D地形を表示するには、下記の2通りの方法があるようです(観測範囲内)。基本的には、どちらの方法でも3D地形を表示することができます。方法1は、すでにホスティングされている、国土地理院 標高タイルを利用できるメリットがあり、一方で、変換モジュールによるTerrain-RGB形式への変換が必要というデメリットもあります。方法2は、事前に国土地理院 標高タイルをTerrain-RGB形式に変換しているので、方法1のような変換モジュールによるTerrain-RGB形式への変換が不要というメリットがあり、一方で、すでにホスティングされている、国土地理院 標高タイルが利用できないというデメリットもあります。

  • 方法1:国土地理院 標高タイルを変換モジュールを用いてTerrain-RGB形式に変換して、3D地形として表示する。

https://qiita.com/Kanahiro/items/1e9c1a4ad6be76b27f0f

  • 方法2:事前にTerrain-RGB形式に変換した、国土地理院 標高タイルを3D地形として表示する。

https://qiita.com/hfu/items/915d7fb961d05670cab2

方法1:国土地理院 標高タイルを変換モジュールを用いてTerrain-RGB形式に変換して、3D地形として表示する方法

  • 標高タイルをTerrain-RGB形式に変換するモジュールの使い方は下記のとおりです。
  • 完全なソースコードは下記のGitHubから入手してください。

https://github.com/shi-works/gsi-terrain-dem-on-maplibre-gl-js-demo

  • 標高タイルをTerrain-RGB形式に変換するモジュールです。
maplibre-gl-gsi-terrain-fast-png.js
// maplibre-gl-gsi-terrain
// 【参考】https://qiita.com/Kanahiro/items/1e9c1a4ad6be76b27f0f

// 'fast-png'パッケージから'encode'関数をインポート。これは画像データをPNG形式にエンコードするために使用。
import { encode as fastPngEncode } from 'https://cdn.jsdelivr.net/npm/fast-png@6.1.0/+esm';

// RGB値を元に地形の高さを計算し、その高さに対応する新たなRGB値を返す関数
const gsidem2terrainrgb = (r, g, b) => {
    // まず、RGB値を元に地形の高さを計算
    let height = r * 655.36 + g * 2.56 + b * 0.01;

    // 特定のRGB値(128, 0, 0)は高さ0として扱う
    if (r === 128 && g === 0 && b === 0) {
        height = 0;
    } else if (r >= 128) {
        // Rが128以上の場合は、地形の高さから一定値を引く
        height -= 167772.16;
    }

    // 地形の高さに基準値を加算し、さらにスケーリング
    height += 100000;
    height *= 10;

    // 新たなRGB値を計算
    const tB = (height / 256 - Math.floor(height / 256)) * 256;
    const tG =
        (Math.floor(height / 256) / 256 -
            Math.floor(Math.floor(height / 256) / 256)) *
        256;
    const tR =
        (Math.floor(Math.floor(height / 256) / 256) / 256 -
            Math.floor(Math.floor(Math.floor(height / 256) / 256) / 256)) *
        256;

    // 新たなRGB値を返す
    return [tR, tG, tB];
};

// 地形データを扱うためのプロトコルをmaplibreglに追加
maplibregl.addProtocol('gsidem', (params, callback) => {
    // 新しい画像を作成
    const image = new Image();
    image.crossOrigin = '';

    image.onload = () => {
        // キャンバスを作成し、画像のサイズに合わせる
        const canvas = document.createElement('canvas');
        canvas.width = image.width;
        canvas.height = image.height;

        // 2Dコンテキストを取得し、画像を描画
        const context = canvas.getContext('2d');
        context.drawImage(image, 0, 0);

        // 画像のピクセルデータを取得
        const imageData = context.getImageData(
            0,
            0,
            canvas.width,
            canvas.height,
        );

        // すべてのピクセルについて、RGB値を変換
        for (let i = 0; i < imageData.data.length / 4; i++) {
            const tRGB = gsidem2terrainrgb(
                imageData.data[i * 4],
                imageData.data[i * 4 + 1],
                imageData.data[i * 4 + 2],
            );
            imageData.data[i * 4] = tRGB[0];
            imageData.data[i * 4 + 1] = tRGB[1];
            imageData.data[i * 4 + 2] = tRGB[2];
        }

        // fast-pngのencode関数を使用して画像データをPNG形式にエンコード
        const pngData = fastPngEncode({
            width: canvas.width,
            height: canvas.height,
            data: imageData.data,
        });

        // PNGデータをArrayBufferとしてcallback関数に渡す
        callback(null, pngData.buffer, null, null);

        /*
        // 変換後の画像データをキャンバスに戻す
        context.putImageData(imageData, 0, 0);

        // キャンバスからblobを作成し、そのblobをArrayBufferとしてcallback関数に渡す
        canvas.toBlob((blob) =>
            blob.arrayBuffer().then((arr) => callback(null, arr, null, null)),
        );
        */
    };

    // 画像のURLを取得し、gsidemプロトコル部分を除去してからimage.srcに設定
    image.src = params.url.replace('gsidem://', '');

    // キャンセル処理を返す(今回は特に何もしない)
    return { cancel: () => { } };
});
  • 標高タイルをTerrain-RGB形式に変換するモジュール(maplibre-gl-gsi-terrain-fast-png.js)を読み込みます。
html
<head>
    <title>国土地理院 標高タイル(DEM10B)</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src='https://unpkg.com/maplibre-gl@3.1.0/dist/maplibre-gl.js'></script>
    <link href='https://unpkg.com/maplibre-gl@3.1.0/dist/maplibre-gl.css' rel='stylesheet' />
    <script type="module" src="maplibre-gl-gsi-terrain-fast-png.js"></script>
  • map.addSourceのtilesを下記のとおりにします。
  • gsidem://https://cyberjapandata.gsi.go.jp/xyz/dem_png/{z}/{x}/{y}.pngとすることで、標高タイルがTerrain-RGB形式に変換されます。
  • map.setTerrain({ 'source': 'gsidem', 'exaggeration': 1 });とすることで、3D地形が表示されます。
  • exaggerationは標高を強調する倍率になります。
javascript
        map.on('load', () => {
            // 標高タイルソース
            map.addSource("gsidem", {
                type: 'raster-dem',
                tiles: [
                    'gsidem://https://cyberjapandata.gsi.go.jp/xyz/dem_png/{z}/{x}/{y}.png',
                ],
                attribution: '<a href="https://maps.gsi.go.jp/development/ichiran.html#dem" target="_blank">地理院タイル(標高タイル)</a>',
                tileSize: 256
            });

            // 標高タイルセット
            map.setTerrain({ 'source': 'gsidem', 'exaggeration': 1 });

        });
  • デモサイトです。

https://shi-works.github.io/gsi-terrain-dem-on-maplibre-gl-js-demo/#12.46/35.34967/138.76069/-36/73

方法2:事前にTerrain-RGB形式に変換した、国土地理院 標高タイルを3D地形として表示する方法

  • Terrain-RGB形式に変換した、国土地理院 標高タイル(DEM10B PNG形式)を下記のGitHubで公開していますので、これを使用しました。

https://github.com/shi-works/gsi-dem-10b-terrain-rgb

  • 完全なソースコードは下記のGitHubから入手してください。

https://github.com/shi-works/gsi-terrain-dem-terrain-rgb-on-maplibre-gl-js-demo

  • map.addSourceのtilesを下記のとおりにします。
  • map.setTerrain({ 'source': 'gsidem-terrain-rgb', 'exaggeration': 1 });とすることで、3D地形が表示されます。
  • exaggerationは標高を強調する倍率になります。
javascript
        map.on('load', () => {
            // 標高タイルソース
            map.addSource("gsidem-terrain-rgb", {
                type: 'raster-dem',
                tiles: [
                    'https://xs489works.xsrv.jp/raster-tiles/gsi/gsi-dem-terrain-rgb/{z}/{x}/{y}.png',
                ],
                attribution: '<a href="https://maps.gsi.go.jp/development/ichiran.html#dem" target="_blank">地理院タイル(標高タイル)</a>',
                tileSize: 256
            });

            // 標高タイルセット
            map.setTerrain({ 'source': 'gsidem-terrain-rgb', 'exaggeration': 1 });
        });
  • デモサイトです。

https://shi-works.github.io/gsi-terrain-dem-terrain-rgb-on-maplibre-gl-js-demo/#12.46/35.34967/138.76069/-36/73

参考文献

https://maplibre.org/maplibre-gl-js/docs/examples/3d-terrain/

https://qiita.com/Kanahiro/items/e22594b738655a189c1d

https://qiita.com/Kanahiro/items/1e9c1a4ad6be76b27f0f

Discussion