🌏
MapLibre GL JSと国土地理院 標高タイルで3D地形を表示する
はじめに
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形式と地理院標高タイルの考え方の違いについては、下記の記事が詳しいです。
デモサイト
- MapLibre GL JSと国土地理院 標高タイルで3D地形を表示するデモサイトです(後述の方法2)。
RGB値の標高換算式(=エンコーディング)
- Terrain-RGB形式と標高タイルのRGB値の標高換算式(=エンコーディング)は下記のとおりになります。
(引用元:『現場のプロがわかりやすく教える 位置情報エンジニア養成講座』)
Terrain-RGB形式
- RGBピクセル値を3桁の256進数として捉え、横軸が縦軸を標高とした傾き0.1の単調増加関数になります。
Terrain-RGB形式の標高換算式
標高=-10000+((R値×256×256+G値×256+B値)×0.1)
- 国土地理院 標高タイルをTerrain-RGB形式に変換したものです。
富士山付近
標高タイル
- 標高タイルの詳細仕様です。
- 国土地理院 標高タイルも、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地形として表示する。
- 方法2:事前にTerrain-RGB形式に変換した、国土地理院 標高タイルを3D地形として表示する。
方法1:国土地理院 標高タイルを変換モジュールを用いてTerrain-RGB形式に変換して、3D地形として表示する方法
- 標高タイルをTerrain-RGB形式に変換するモジュールの使い方は下記のとおりです。
- 完全なソースコードは下記のGitHubから入手してください。
- 標高タイルを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 += 10000;
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 });
});
- デモサイトです。
方法2:事前にTerrain-RGB形式に変換した、国土地理院 標高タイルを3D地形として表示する方法
- Terrain-RGB形式に変換した、国土地理院 標高タイル(DEM10B PNG形式)を下記のGitHubで公開していますので、これを使用しました。
- 完全なソースコードは下記のGitHubから入手してください。
- 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 });
});
- デモサイトです。
参考文献
Discussion