MapLibre GL JSでのパレットPNGタイルの凡例情報のポップアップ表示について

:::note info
この記事は [MapLibre Advent Calendar 2024](https://qiita.com/advent-calendar/2024/maplibre) 16日目の記事です。
:::
# はじめに
この記事では、MapLibre GL JSを使ってデータPNG（パレットPNG）タイルを表示し、クリックした地点の凡例情報をポップアップで表示する仕組みを紹介します。対象とする地図データは、RGB（赤・緑・青）の値で土地利用を表現したラスタータイルです。この仕組みを活用すると、独自のラスタータイルに凡例情報を付けて、インタラクティブな地図アプリを作成することが可能になります。具体には、下記の記事で作成した国土数値情報の土地利用細分メッシュデータ（100mメッシュ）のパレットPNGタイルをMapLibre GL JSで表示し、クリック時に取得したRGB値をもとに凡例情報を表示します。

https://qiita.com/shi-works/items/87c6b3507318747404b1

パレットPNGタイルのRGB値の取得には、産業技術総合研究所の「2. 位置を指定して値を取得」で公開されている「パレットPNGタイル」のサンプル関数を参考にさせていただきました。詳細は以下のリンクから確認できます。このサンプルに少し手を加え、今回のMapLibre GL JSによる凡例情報の表示を行いました。

https://gsj-seamless.jp/labs/datapng/gridpngtile.html

# 凡例情報のポップアップ表示までの流れ
 - 凡例情報のポップアップ表示までの流れは以下のとおりになります。
1. 凡例情報の作成
1. クリック地点の緯度・経度を取得
1. タイル座標を計算し、タイル画像を参照
1. タイル内のクリック地点に対応するピクセルのRGB値を取得
1. RGB値を凡例情報と照合し、対応する凡例情報を特定
1. ポップアップで凡例情報を表示

## 凡例情報の作成
 - [パレットPNGタイル作成時に設定したRGB値と土地利用種別](https://qiita.com/shi-works/items/87c6b3507318747404b1#%E6%89%8B%E6%B3%952%E5%85%83%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E3%83%87%E3%82%A3%E3%82%BE%E3%83%AB%E3%83%96%E7%B5%B1%E5%90%88%E3%81%97%E3%83%87%E3%83%BC%E3%82%BFpng%E3%81%AB%E5%A4%89%E6%8F%9B%E3%81%97%E3%81%A6%E3%83%A9%E3%82%B9%E3%82%BF%E3%83%BC%E3%82%BF%E3%82%A4%E3%83%AB%E5%8C%96%E3%81%99%E3%82%8B%E6%89%8B%E6%B3%95)をもとに、凡例情報を作成します。
```javascript
        // 凡例情報セット
        const legend_tochi = [
            { r: 255, g: 255, b: 0, title: '田' },
            { r: 255, g: 204, b: 153, title: 'その他の農用地' },
            { r: 0, g: 170, b: 0, title: '森林' },
            { r: 255, g: 153, b: 0, title: '荒地' },
            { r: 255, g: 0, b: 0, title: '建物用地' },
            { r: 140, g: 140, b: 140, title: '道路' },
            { r: 180, g: 180, b: 180, title: '鉄道' },
            { r: 200, g: 70, b: 15, title: 'その他の用地' },
            { r: 0, g: 0, b: 255, title: '河川地及び湖沼' },
            { r: 255, g: 255, b: 153, title: '海浜' },
            { r: 0, g: 204, b: 255, title: '海水域' },
            { r: 0, g: 255, b: 0, title: 'ゴルフ場' },
        ];
```

## クリック地点の座標を取得
 - MapLibre GL JSのclickイベントを使って、クリックした場所の緯度・経度（lat,lng）を取得します。
```javascript
map.on('click', (e) => {
    （中略）
    const lat = e.lngLat.lat; // 緯度
    const lng = e.lngLat.lng; // 経度
    （中略）
});
```

## タイル座標を計算し、タイル画像を参照
 - クリック地点が属するタイルを特定するために、緯度・経度とズームレベルを元にタイル座標（x, y）を計算します。
```javascript
        /// ****************
        // latLngToTile 緯度経度をタイル座標に変換する関数
        //  lat: 経度
        //  lng: 緯度
        //  z: ズームレベル
        //  戻り値: タイル座標オブジェクト（x, yフィールドを持ちます)
        //    ※通常，地図ライブラリ内に同様の関数が用意されています．
        /// ****************
        function latLngToTile(lat, lng, z) {
            const
                w = Math.pow(2, (z === undefined) ? 0 : z) / 2,		// 世界全体のピクセル幅
                yrad = Math.log(Math.tan(Math.PI * (90 + lat) / 360));

            return { x: (lng / 180 + 1) * w, y: (1 - yrad / Math.PI) * w };
        };
```



# タイル内のクリック地点に対応するピクセルのRGB値を取得
 - クリック地点がタイル内のどのピクセルに相当するかを計算し、そのピクセルのRGB値を取得します。
 - 取得したRGB値を凡例情報と照合し、対応する凡例情報を特定します。
```javascript
        /// ****************
        // getLegendItem 凡例情報，タイルURL，座標，ズームレベルを指定して凡例項目を取得する関数
        //  legend: 凡例情報オブジェクト．r,g,b,titleを持つ凡例項目オブジェクトの配列
        //	url: タイル画像のURLテンプレート．
        //		ズームレベル，X, Y座標をそれぞれ{z},{x},{y}として埋め込む
        //  lat: 経度オブジェクト
        //  lng: 緯度オブジェクト
        //  z:　ズームレベル
        //  invalid: 追加無効値を相当する数値で指定．デフォルトは指定なし
        //  戻り値: 成功時に凡例項目オブジェクトを受け取るプロミス．該当するものがない場合はnullを受け取ります
        /// ****************
        function getLegendItem(legend, url, lat, lng, z, invalid = undefined) {
            return new Promise(function (resolve, reject) {
                const
                    p = latLngToTile(lat, lng, z),
                    x = Math.floor(p.x),			// タイルX座標
                    y = Math.floor(p.y),			// タイルY座標
                    i = (p.x - x) * 256,			// タイル内i座標
                    j = (p.y - y) * 256,			// タイル内j座標
                    img = new Image();

                img.crossOrigin = 'anonymous';	// クロスオリジン対応
                img.onload = function () {
                    const
                        canvas = document.createElement('canvas'),
                        context = canvas.getContext('2d');
                    let
                        v,
                        d;

                    canvas.width = 1;
                    canvas.height = 1;
                    context.drawImage(img, i, j, 1, 1, 0, 0, 1, 1);
                    d = context.getImageData(0, 0, 1, 1).data;
                    if (d[3] !== 255) {
                        v = null;
                    } else {
                        v = legend.find(o => o.r == d[0] && o.g == d[1] && o.b == d[2])
                    }
                    resolve(v);
                }
                img.onerror = function () {
                    resolve(null);
                }
                img.src = url.replace('{z}', z).replace('{y}', y).replace('{x}', x);
            });
        };
```

# ポップアップで凡例情報を表示
 - 最後に、照合した凡例情報をポップアップとして表示します。
```javascript
        // クリック時にポップアップ表示
        map.on('click', function (e) {
            // 表示されているレイヤーのIDを格納する配列
            var rasterLayerIds = [];
            const mapLayers = map.getStyle().layers;

            // 全てのレイヤーを走査して、'type'が'raster'のものをフィルタリング
            mapLayers.forEach(layer => {
                // レイヤーのtypeプロパティを取得
                const type = layer.type;
                if (type === 'raster') {
                    rasterLayerIds.push(layer.id);
                }
            });

            console.log('表示されているレイヤー: ' + rasterLayerIds);

            // 現在表示されているラスターレイヤをもとにポップアップ表示
            RasterTileUrl = 'https://shiworks.xsrv.jp/raster-tiles/mlit-ksj/L03-b-21_dissolved/{z}/{x}/{y}.png';
            legend = legend_tochi;

            // クリック地点の座標を取得
            const lng = e.lngLat.lng;
            const lat = e.lngLat.lat;

            getLegendItem(legend, RasterTileUrl, lat, lng, Math.trunc(map.getZoom())).then(function (v) {
                let s = '';
                let res = (v ? v.title : '取得できません');
                if (rasterLayerIds == 'ksj-L03-b-21') {
                    s = '<p>' + '土地利用種別' + '<br>' + '<b>' + res + '</b>' + '<br>' + '<a href=\https://www.google.com/maps?q=' + lat + "," + lng + "&hl=ja' target='_blank'>🌎Google Maps</a>" + '</p>';
                }
                new maplibregl.Popup()
                    .setLngLat(e.lngLat)
                    .setHTML(s)
                    .addTo(map);
            });
        });
```

# デモサイトとソースコード
- デモサイトはこちらになります。
https://shiwaku.github.io/mlit-ksj-tochimesh-datapng/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205045/2e9c9450-88bc-ab36-740e-2a97abd41e17.png)

- ソースコードはこちらになります。
https://github.com/shiwaku/mlit-ksj-tochimesh-datapng

引き続き検証を行います。