LoginSignup
21
10

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

Last updated at Posted at 2022-05-24

スクリーンショット 2022-05-24 13.06.53.png

2024-06-30追記

本記事の内容は上記リポジトリにて公開されているライブラリに統合されています。簡単に利用できます。

TL;DR

  • MapLibre GL JSのv2.2.0-pre.2で3D-Terrainが実装された
  • MapLibre GL JSのaddProtocol()を使う事で読み込んだタイルデータを加工できる
  • addProtocol()で地理院標高タイルをTerrainRGBへ変換することで、標高タイルを使って3D-Terrainを表示できた

MapLibre GL JSと3D-Terrain

3D-Terrainの実装はかなり長い時間がかけられており、早い段階からデモを触ったりしてワクワクしていたものでした。
それが最近マージされ、2.2.0-pre.2でプレリリースとして公開されました。

必要なデータ

DEMタイルが必要で、TerrainRGB(もしくはTerrarium)でエンコードされた画像である必要があります。
一方で地理院標高タイルはこのいずれでもない特殊なエンコーディングです。

標高エンコーディングの考え方については下記を参照
https://qiita.com/Kanahiro/items/e22594b738655a189c1d#rgb%E5%80%A4%E3%81%AE%E6%A8%99%E9%AB%98%E6%8F%9B%E7%AE%97

MapLibre(Mapbox) GL JSと標高タイル

地理院タイルは標高エンコーディングが独特であるため、MapLibreで素直に利用する事ができません。
それでもなお標高タイルをMapLibre(Mapbox) GL JSで使おうとした先行事例がありました。

先行事例

https://qiita.com/tattii88/items/3d88907c7116d1aeb028
Mapbox GL JS自体をフォークして、標高タイルのエンコーディングを実装しています。

https://qiita.com/frogcat/items/d12bed4e930b83eb3544
ServiceWorkerfetch()を監視して標高タイル画像をTerrainRGBに変換しています。

https://qiita.com/T-ubu/items/c35023e1df2362bd8e7f
v2.2.0-pre.2で標高タイルをそのまま使って3D-Terrainを表示しています。

しかし最近MapLibreにはaddProtocol()という関数が実装されました。これを用いれば、もっとラクに標高タイルをTerrainRGBに変換する事ができます。

addProtocol()

この関数は名前からは使い道がわかりにくいのですが、要はカスタムしたsourceを定義する事ができます。
ここでいうプロトコルとは、sample://で言うsampleです。タイルURLでこのような特定のプロトコルを接頭辞とすることで、タイルへのリクエストなどをaddProtocol()で定義したとおりの振る舞いとする事ができます。

標高タイルをTerrainRGBに変換する

MapLibreのMapインスタンスを初期化します。なお標高タイル(gsidem)のtilesの書き方に注意してください(gsidem://がプロトコル)。

import maplibregl, { Map } from 'maplibre-gl';

new Map({
    container: 'map',
    maxPitch: 85,
    style: {
        version: 8,
        sources: {
            gsi: {
                type: 'raster',
                tiles: [
                    'https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/{z}/{x}/{y}.jpg',
                ],
                attribution: '地理院タイル',
            },
            gsidem: {
                type: 'raster-dem',
                tiles: [
                    'gsidem://https://cyberjapandata.gsi.go.jp/xyz/dem_png/{z}/{x}/{y}.png',
                ],
                tileSize: 256,
                maxzoom: 14,
            },
        },
        layers: [
            {
                id: 'gsi',
                type: 'raster',
                source: 'gsi',
            },
        ],
        terrain: {
            source: 'gsidem',
            exaggeration: 1.2,
        },
    },
});

addProtocol()で振る舞いを定義します。

const gsidem2terrainrgb = (r, g, b): number[] => {
    let height = r * 655.36 + g * 2.56 + b * 0.01;
    if (r === 128 && g === 0 && b === 0) {
        height = 0;
    } else if (r >= 128) {
        height -= 167772.16;
    }
    height += 100000;
    height *= 10;
    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;
    return [tR, tG, tB];
};

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;

        const context = canvas.getContext('2d');
        context.drawImage(image, 0, 0);
        const imageData = context.getImageData(
            0,
            0,
            canvas.width,
            canvas.height,
        );
        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];
        }
        context.putImageData(imageData, 0, 0);
        canvas.toBlob((blob) =>
            blob.arrayBuffer().then((arr) => callback(null, arr, null, null)), // ここで返すデータは、画像のArrayBuffer()でなければならない
        );
    };
    image.src = params.url.replace('gsidem://', '');
    return { cancel: () => {} };
});

すると冒頭のスクリーンショットのように3D-Terrainを表示する事ができます。

スクリーンショット 2022-05-24 13.29.01.png

所感

  • 画像の変換が入るからと言ってパフォーマンスが明らかに悪くなる、とかはありませんでした。
    • ImageDataをPNGに変換する際、上記例ではcanvasの機能を使っているが、fast-pngを使った方が速いかもしれない。
    • 標高タイルからTerrainRGBへの変換は、先行事例にあった関数の方がぱっと見で速そうに見える。
  • addProtocol()によりタイルデータを改変出来るようになったので使い道は無限大。ちなみにこの関数はMapboxでは実装されていない。
    • type: 'custom'のレイヤーでタイルデータが読めるようになったらうれしいなぁ…。
21
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
10