ドローンを安全に飛行させたり、登山用の地図アプリを開発したり、あるいは土地の3Dモデルを作成したりする際に、「指定した緯度・経度の地面の高さ(標高)」を知りたい場面は非常に多くあります。
幸いなことに、日本では国土地理院が非常に高精度な標高データを無償で公開してくれています。この記事では、国土地理院の「標高タイル」という仕組みを利用して、特定の緯度・経度における標高をプログラムで取得する方法を、サンプルコードと共に詳しく解説します。
標高タイルとは?
国土地理院は、日本全国の標高データを、まるでWeb地図のようにタイル状に分割されたPNG画像として提供しています。これを「標高タイル」と呼びます。
一見するとただの画像のようですが、その各ピクセルの 色(RGB値) には、その地点の標高情報が特殊な計算式に基づいてエンコードされています。特定の色を読み解くことで、そのピクセルの標高をメートル単位で正確に知ることができます。
今回は数種類ある標高タイルの中でも特に高精度な 「DEM5A(5mメッシュ)」 を利用します。
標高取得の3ステップ
プログラムで標高を取得する流れは、大きく分けて以下の3ステップです。
-
緯度・経度を「タイル座標」に変換する
知りたい地点の緯度・経度が、地球全体を覆うタイル群の「何番目のタイルの、どのピクセル」に当たるのかを計算します。これはウェブメルカトル図法に基づいた計算式で行います。 -
対応する標高タイル画像を取得する
ステップ1で計算したタイル座標を基に、国土地理院のサーバー上の正しいURLを組み立て、PNG画像をダウンロードします。 -
画像の色情報から標高をデコードする
ダウンロードしたPNG画像を解析し、目的のピクセルのRGB値を取得します。その値を国土地理院が定めた計算式に当てはめ、標高をメートル単位に変換します。
実際にやってみよう (Node.js)
準備
まず、プロジェクトのフォルダでnpmを初期化し、必要なライブラリをインストールします。
- axios: HTTPリクエストを行い標高タイルをダウンロード
- pngjs: PNG画像のバイナリデータを解析し、ピクセル情報にアクセス
# プロジェクトフォルダを作成して移動
mkdir elevation-checker
cd elevation-checker
# npmプロジェクトを初期化
npm init -y
# 必要なライブラリをインストール
npm install axios pngjs
プログラム全体像
次に、get_elevation.js というファイル名でプログラムを作成します。以下は大津市役所の緯度経度を例に、その地点の標高を取得する処理です。
※詳細なコードは
get_elevation.jsを参照してください。
使い方
ターミナルで以下を実行します。
node get_elevation.js
成功すると、以下のように標高が出力されます。
[INFO] 緯度: 35.0045, 経度: 135.8685 の標高を取得します...
[INFO] 対応タイルURL: https://cyberjapandata.gsi.go.jp/xyz/dem5a_png/15/29111/13594.png
[SUCCESS] 標高の取得に成功しました: 85.85 m
まとめと応用
このように、いくつかのライブラリを組み合わせることで、緯度経度からピンポイントで標高を取得するプログラムを簡単に作成できます。
今回のプログラムを応用すれば、
ドローンの飛行ルート上の対地高度を事前に計算する
GPSログから登山ルートの標高グラフを作成する
特定の範囲の標高データをメッシュ状に取得し、3D地形モデルを生成する
など、様々なアプリケーションを開発することが可能です。ぜひ、このプログラムをベースに、あなたのアイデアを実現してみてください。
get_elevation.js
/**
* 国土地理院の標高タイルを利用して、指定した緯度経度の標高を取得するプログラム
*
* 参照: 国土地理院タイル仕様
* https://maps.gsi.go.jp/development/siyou.html
*/
// --- 必要なモジュールのインポート ---
const axios = require('axios');
const { PNG } = require('pngjs');
// --- メインとなる関数 ---
/**
* 指定された緯度経度から国土地理院の標高タイルを解析し、標高を算出します。
* @param {number} lat 緯度 (例: 35.0045)
* @param {number} lon 経度 (例: 135.8685)
* @returns {Promise<number>} 標高(m)
*/
async function getElevation(lat, lon) {
console.log(`[INFO] 緯度: ${lat}, 経度: ${lon} の標高を取得します...`);
// ズームレベル (DEM5Aは15で固定)
const z = 15;
// --- ステップ1: 緯度経度をタイル座標とピクセル座標に変換 ---
const R = 128 / Math.PI;
const worldCoordX = R * (lon * Math.PI / 180 + Math.PI);
const worldCoordY = -R / 2 * Math.log((1 + Math.sin(lat * Math.PI / 180)) / (1 - Math.sin(lat * Math.PI / 180))) + 128;
const pixelCoordX = worldCoordX * Math.pow(2, z);
const pixelCoordY = worldCoordY * Math.pow(2, z);
const tileX = Math.floor(pixelCoordX / 256);
const tileY = Math.floor(pixelCoordY / 256);
const pixelX = Math.floor(pixelCoordX - tileX * 256);
const pixelY = Math.floor(pixelCoordY - tileY * 256);
// --- ステップ2: 対応する標高タイル画像のURLを構築して取得 ---
// データセット: DEM5A (5mメッシュ)
const url = `https://cyberjapandata.gsi.go.jp/xyz/dem5a_png/${z}/${tileX}/${tileY}.png`;
console.log(`[INFO] 対応タイルURL: ${url}`);
try {
const response = await axios.get(url, {
responseType: 'arraybuffer', // PNGをバイナリデータとして取得
timeout: 10000 // 10秒でタイムアウト
});
// --- ステップ3: 画像の色情報から標高をデコード ---
const png = PNG.sync.read(response.data);
// 指定ピクセルのインデックスを計算
const idx = (png.width * pixelY + pixelX) << 2;
const r = png.data[idx];
const g = png.data[idx + 1];
const b = png.data[idx + 2];
// RGB値から標高を計算する国土地理院の公式
const pow2_23 = 8388608; // 2^23
const pow2_24 = 16777216; // 2^24
let elevation = 0;
// r=128, g=0, b=0 は「データなし」を示す
if (r === 128 && g === 0 && b === 0) {
throw new Error('指定された座標の標高データがありません(海など)。');
}
const d = r * Math.pow(2, 16) + g * Math.pow(2, 8) + b;
elevation = (d < pow2_23) ? d : d - pow2_24;
if (elevation === -pow2_23) {
throw new Error('指定された座標の標高データがありません。');
}
// 標高値は0.01m単位なので、100で割ってメートルに変換
elevation *= 0.01;
return elevation;
} catch (error) {
if (error.response && error.response.status === 404) {
throw new Error('標高タイルの取得に失敗しました (404 Not Found)。範囲外の可能性があります。');
}
// その他のエラー(タイムアウト、デコード失敗など)
throw new Error(`標高の取得または解析中にエラーが発生しました: ${error.message}`);
}
}
// --- プログラムの実行部分 ---
(async () => {
// 標高を調べたい地点の緯度経度(例: 滋賀県大津市役所)
const TARGET_LOCATION = {
lat: 35.0045,
lon: 135.8685
};
try {
const elevation = await getElevation(TARGET_LOCATION.lat, TARGET_LOCATION.lon);
// 結果を小数点以下2桁で表示
console.log(`\n[SUCCESS] 標高の取得に成功しました: ${elevation.toFixed(2)} m`);
} catch (error) {
console.error(`\n[ERROR] 処理に失敗しました: ${error.message}`);
}
})();