はじめに
MapLibre GL JSでダムに沈んだ村を可視化する という素敵な記事があります。
この記事では 地理院標高タイル を使った 別解 をやってみます。
できあがり
デモ: https://frogcat.github.io/underwater-village/
ソース: https://github.com/frogcat/underwater-village
解説
方針
こちらは 地理院標高タイル:dem5a_png を直接表示してみたものです。どうやら水部が赤く塗り分けられているようですね?
地理院標高タイル によるとこの赤い部分は無効値である #800000
で塗られているということになります。
無効値(標高タイル(テキスト形式)の「e」に該当する箇所)は(R, G, B)=(128, 0, 0)です。
入力されたタイル画像について、対応する標高PNGタイルが #800000
の部分だけを維持し、他は transparent black にして返すことができれば求める画像が得られそうです。
実装
短いのでそのまま貼っておきます。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>underwater-village</title>
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0" />
<link href="https://unpkg.com/maplibre-gl@5.4.0/dist/maplibre-gl.css" rel="stylesheet" />
<script src="https://unpkg.com/maplibre-gl@5.4.0/dist/maplibre-gl.js"></script>
<script>
function createLoadFn() {
const load = (url) =>
new Promise((resolve) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => {
const canvas = document.createElement("canvas");
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const context = canvas.getContext("2d");
context.drawImage(img, 0, 0);
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
resolve({ canvas, context, imageData });
};
img.src = url;
});
const toBlob = (canvas) =>
new Promise((resolve) => {
canvas.toBlob(resolve);
}).then((blob) => blob.arrayBuffer());
return async function (params) {
const [y, x, z] = params.url.split(/[\./]/).reverse().slice(1);
const url = [
params.url.replace("mask://", "https://"),
`https://cyberjapandata.gsi.go.jp/xyz/dem5a_png/${z}/${x}/${y}.png`,
];
const [src, dem] = await Promise.all(url.map(load));
for (let i = 0; i < dem.imageData.data.length; i += 4) {
if (
dem.imageData.data[i + 0] !== 0x80 ||
dem.imageData.data[i + 1] !== 0x00 ||
dem.imageData.data[i + 2] !== 0x00
)
src.imageData.data.fill(0, i, i + 4);
}
src.context.putImageData(src.imageData, 0, 0);
return {
data: await toBlob(src.canvas),
};
};
}
</script>
</head>
<body>
<div id="map" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0"></div>
<script>
maplibregl.addProtocol("mask", createLoadFn());
new maplibregl.Map({
container: "map",
center: [136.485452, 35.69576],
zoom: 13,
hash: true,
style: "style.json",
});
</script>
</body>
</html>
addProtocol についてはこちらの記事を参照してください。
本記事のコアはこのあたりです。
const [src, dem] = await Promise.all(url.map(load));
for (let i = 0; i < dem.imageData.data.length; i += 4) {
if (
dem.imageData.data[i + 0] !== 0x80 ||
dem.imageData.data[i + 1] !== 0x00 ||
dem.imageData.data[i + 2] !== 0x00
)
src.imageData.data.fill(0, i, i + 4);
}
src.context.putImageData(src.imageData, 0, 0);
- 元画像と pngdem を取得して ImageData として参照できるようにしておく
- pngdem の RGBA をイテレートして、
#800000
でない場合には元画像の対応箇所を透明黒で塗る - 終わったら canvas に imageData を描き戻し、PNG Blob を返す
おわりに
ラスタ合成によってタイル画像から選択的なクリッピングを行う事例を紹介しました。マスクとして使える適当なタイルがある場合には手軽な方式だと思います。
実際には標高タイル/ズームレベルによっては水部に無効値ではなくて標高値(水面の標高値?)が入っていることがあったり、5m メッシュ標高は計測対象外エリアが無効値であったりするので注意が必要です。また画像側が z=16~18 をサポートしていても DEM が z=15 まで、みたいな場合の処理は課題です。
標高タイルにこだわらず、地理院の標準地図をマスクとして 水色の部分だけをスルー みたいな雑な使い方も面白い効果を生むかもしれません。
おまけ
特段エリアを限定していないので、神奈川県の 宮ヶ瀬村 の様子も見ることができました。他にも見つかるかもしれませんね。
https://frogcat.github.io/underwater-village/#13.43/35.52305/139.23113