はじめに
地図の上をクリックすると、その場所の 標高(山・地形)や 水深(海・川の深さ)を取得できたら便利だと思いませんか?
例えば、
- 登山ルートを検討する
- 避難所や安全ルートの標高をチェックする
- 災害・浸水シミュレーション素材を作る
- 海岸や川周辺の地形調査をする
- サイトやアプリに「クリックで標高表示」機能を追加する
といった用途で役立ちます。
本記事では、ZENRIN Maps API の 標高・水深情報検索 API を使って、
地図クリックだけで標高を取得し、マーカー番号(1〜30)や統計(最大・最小・平均)も見られる
シンプルかつ学習しやすいサンプルを作っていきます。
掲載しているコードはすべてそのままコピーして動かせる構成になっており、
HTML と JavaScript を読み進めるだけで、標高取得の基本的な仕組みが理解できるようになっています。
この記事でできること
- 地図を表示する
- 地図クリックで指定地点の標高(m)/水深(m)を取得する
- 取得結果をマーカー(番号付き)で表示し、一覧・統計(最大/最小/平均)を表示する
APIキーの取得手順
ZENRIN Maps API を利用するには、事前に APIキーの取得が必要です。
現在、ZENRIN Maps API は 2か月間の無料トライアルが用意されており、期間中は主要な機能を実際にお試しいただけます。開発や評価の初期段階でも安心してご利用いただけます。
APIキーの取得方法については、以下の記事で詳しく解説されています。
初めての方は、まずこちらをご覧いただき、APIキーの発行と設定を行ってください。
公式リファレンス
- 地図描画 メインクラス(Map)
- 拡大縮小ボタンのクラス(ZoomButton)
- コンパスのクラス(Compass)
- スケールバーのクラス(ScaleBar)
- マーカーのクラス(Marker)
- 標高・水深情報検索(elevation)
ファイル構成
project/
├── index.html
├── css/
│ └── zma_elevation.css
└── js/
└── zma_elevation.js
サンプルコード
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ZENRIN Maps API - 標高情報取得サンプル</title>
<!-- Font Awesome 6.4.2 (SIL Open Font License) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
<link rel="stylesheet" href="css/zma_elevation.css">
</head>
<body>
<div id="ZMap"></div>
<div class="control-panel">
<div class="panel-header">
<h2><i class="fas fa-mountain"></i> 標高情報取得</h2>
<p>地図上をクリックして標高を取得できます</p>
</div>
<div class="panel-body">
<!-- 操作方法セクション -->
<div class="form-section-title">
<i class="fas fa-info-circle"></i> 操作方法
</div>
<div class="info-box">
<p><i class="fas fa-mouse-pointer"></i> 地図上をクリックすると、その地点の標高を取得します</p>
<p><i class="fas fa-list"></i> 取得した標高情報は一覧に表示されます</p>
<p><i class="fas fa-chart-line"></i> 複数地点の標高を比較できます</p>
<p><i class="fas fa-info-circle"></i> マーカーのナンバリングは30地点まで対応しています</p>
</div>
<!-- 現在の標高表示セクション -->
<div class="form-section-title">
<i class="fas fa-map-marker-alt"></i> 現在の標高
</div>
<div class="elevation-display">
<div class="elevation-value" id="currentElevation">--</div>
<div class="elevation-unit">m</div>
</div>
<div class="coordinate-info" id="coordinateInfo">
<span id="coordinateText">座標: --</span>
</div>
<!-- 標高リストセクション -->
<div class="form-section-title">
<i class="fas fa-list-ul"></i> 標高リスト
<span class="badge" id="pointCount">0</span>
</div>
<div class="elevation-list" id="elevationList">
<div class="empty-message">地図上をクリックして標高を取得してください</div>
</div>
<!-- 統計情報セクション -->
<div class="form-section-title" id="statsTitle" style="display: none;">
<i class="fas fa-chart-bar"></i> 統計情報
</div>
<div class="stats-container" id="statsContainer" style="display: none;">
<div class="stat-item">
<span class="stat-label">最高標高</span>
<span class="stat-value" id="maxElevation">--</span>
<span class="stat-unit">m</span>
</div>
<div class="stat-item">
<span class="stat-label">最低標高</span>
<span class="stat-value" id="minElevation">--</span>
<span class="stat-unit">m</span>
</div>
<div class="stat-item">
<span class="stat-label">標高差</span>
<span class="stat-value" id="elevationDiff">--</span>
<span class="stat-unit">m</span>
</div>
<div class="stat-item">
<span class="stat-label">平均標高</span>
<span class="stat-value" id="avgElevation">--</span>
<span class="stat-unit">m</span>
</div>
</div>
<!-- 操作ボタン -->
<div class="actions">
<button id="btnClear" class="btn"><i class="fas fa-eraser"></i> クリア</button>
<button id="btnCenter" class="btn"><i class="fas fa-crosshairs"></i> 中心に移動</button>
</div>
<!-- メッシュタイプ選択 -->
<div class="form-section-title">
<i class="fas fa-cog"></i> 設定
</div>
<div class="form-grid">
<label class="form-label">メッシュタイプ</label>
<select id="meshType" class="select">
<option value="N05m,N10m,D500m" selected>5m/10m/500m(推奨)</option>
<option value="N05m">5mメッシュ</option>
<option value="N10m">10mメッシュ</option>
<option value="D500m">500mメッシュ</option>
</select>
</div>
<!-- エラーメッセージ -->
<div class="error-message" id="errorMessage">
<i class="fas fa-exclamation-triangle"></i>
<span id="errorMessageText"></span>
</div>
<!-- ローディング -->
<div class="loading" id="loading">
<div class="spinner"></div>
<p>標高を取得中...</p>
</div>
</div>
</div>
<script src="https://test-js.zmaps-api.com/zma_loader.js?key=YOUR_API_KEY&auth=referer"></script>
<script src="js/zma_elevation.js"></script>
</body>
</html>
CSS(クリックで展開)
/* 基本スタイル */
* { box-sizing: border-box; }
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; height: 100vh; overflow: hidden; margin: 0; padding: 0; }
#ZMap { position: absolute; width: 100%; height: 100%; top: 0; left: 0; }
/* パネルスタイル */
.control-panel { position: absolute; top: 20px; left: 20px; width: 420px; max-height: calc(100vh - 40px); background: #fff; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.15); z-index: 1000; overflow: hidden; display: flex; flex-direction: column; }
.panel-header { padding: 16px 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; }
.panel-header h2 { font-size: 18px; margin: 0 0 4px; display: flex; align-items: center; gap: 8px; }
.panel-header p { font-size: 12px; opacity: .9; margin: 0; }
.panel-body { padding: 16px 20px 20px; overflow-y: auto; flex: 1; }
/* セクションタイトル */
.form-section-title { font-size: 14px; font-weight: 600; color: #333; margin: 16px 0 12px; padding-bottom: 8px; border-bottom: 2px solid #e0e0e0; display: flex; align-items: center; gap: 8px; }
.form-section-title:first-child { margin-top: 0; }
.form-section-title .badge { margin-left: auto; background: #667eea; color: #fff; padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: normal; }
/* 情報ボックス */
.info-box { background: #f8f9fa; padding: 12px; border-radius: 8px; margin-bottom: 12px; }
.info-box p { font-size: 12px; color: #555; margin: 6px 0; display: flex; align-items: center; gap: 8px; }
.info-box p:first-child { margin-top: 0; }
.info-box p:last-child { margin-bottom: 0; }
/* 標高表示 */
.elevation-display { display: flex; align-items: baseline; gap: 8px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; border-radius: 8px; margin-bottom: 8px; }
.elevation-value { font-size: 48px; font-weight: 700; color: #fff; line-height: 1; }
.elevation-unit { font-size: 24px; color: #fff; opacity: 0.9; }
.coordinate-info { font-size: 11px; color: #666; text-align: center; margin-bottom: 12px; }
/* 標高リスト */
.elevation-list { max-height: 300px; overflow-y: auto; margin-bottom: 12px; }
.empty-message { text-align: center; color: #999; font-size: 12px; padding: 20px; }
.elevation-item { background: #f8f9fa; padding: 12px; border-radius: 8px; margin-bottom: 8px; border: 2px solid transparent; cursor: pointer; transition: all .2s; }
.elevation-item:hover { background: #eef2ff; border-color: #667eea; }
.elevation-item-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
.elevation-item-number { font-size: 11px; color: #999; }
.elevation-item-elevation { font-size: 18px; font-weight: 600; color: #667eea; }
.elevation-item-details { font-size: 11px; color: #666; }
.elevation-item-coord { margin-bottom: 4px; }
.elevation-item-meta { display: flex; gap: 8px; }
.elevation-item-mesh { background: #fff; border: 1px solid #e3e7ff; border-radius: 4px; padding: 2px 6px; }
.elevation-item-time { color: #999; }
/* 統計情報 */
.stats-container { background: #f8f9fa; padding: 12px; border-radius: 8px; margin-bottom: 12px; }
.stat-item { display: flex; align-items: center; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #e0e0e0; }
.stat-item:last-child { border-bottom: none; }
.stat-label { font-size: 12px; color: #555; }
.stat-value { font-size: 18px; font-weight: 600; color: #667eea; }
.stat-unit { font-size: 12px; color: #999; margin-left: 4px; }
/* フォームスタイル */
.form-grid { display: grid; grid-template-columns: 120px 1fr; gap: 10px 12px; align-items: center; margin-bottom: 12px; }
.form-label { font-size: 12px; color: #555; }
.select { width: 100%; padding: 10px 12px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px; transition: all .2s; background: #fff; }
.select:focus { outline: none; border-color: #667eea; box-shadow: 0 0 0 3px rgba(102,126,234,.1); }
/* ボタンスタイル */
.actions { display: flex; gap: 8px; margin: 12px 0; }
.btn { background: #e9ecef; border: none; border-radius: 8px; padding: 10px 14px; cursor: pointer; font-size: 14px; display: flex; align-items: center; gap: 6px; transition: all .2s; }
.btn:hover { filter: brightness(0.97); background: #dee2e6; }
.btn i { font-size: 12px; }
/* エラーメッセージ */
.error-message { display: none; background: #fee; color: #c33; padding: 10px 12px; border-radius: 8px; margin-bottom: 10px; font-size: 12px; }
.error-message.show { display: flex; align-items: center; gap: 8px; }
/* ローディング */
.loading { display: none; text-align: center; padding: 16px; }
.loading.show { display: block; }
.spinner { border: 3px solid #f3f3f3; border-top: 3px solid #667eea; border-radius: 50%; width: 34px; height: 34px; animation: spin 1s linear infinite; margin: 0 auto 8px; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.loading p { font-size: 12px; color: #666; margin: 0; }
/* スクロールバーのスタイル */
.elevation-list::-webkit-scrollbar { width: 6px; }
.elevation-list::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 3px; }
.elevation-list::-webkit-scrollbar-thumb { background: #667eea; border-radius: 3px; }
.elevation-list::-webkit-scrollbar-thumb:hover { background: #5568d3; }
.panel-body::-webkit-scrollbar { width: 6px; }
.panel-body::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 3px; }
.panel-body::-webkit-scrollbar-thumb { background: #667eea; border-radius: 3px; }
.panel-body::-webkit-scrollbar-thumb:hover { background: #5568d3; }
/* レスポンシブデザイン */
@media (max-width: 768px) {
.control-panel { width: calc(100% - 40px); }
.form-grid { grid-template-columns: 1fr; }
.elevation-value { font-size: 36px; }
.elevation-unit { font-size: 20px; }
}
// マップ変数
let map;
let markers = [];
let elevationPoints = [];
// 最大標高点数(マーカーのナンバリングが30まで)
const MAX_ELEVATION_POINTS = 30;
const $ = (id) => document.getElementById(id);
const API_ENDPOINT = 'https://test-web.zmaps-api.com/search/elevation';
const API_KEY = 'YOUR_API_KEY'; // 実際の運用では環境変数などから取得
const API_AUTH = 'referer';
ZMALoader.setOnLoad(function (mapOptions, error) {
if (error) {
console.error('Map Loader Error:', error);
setError('地図の読み込みに失敗しました');
return;
}
// 富士山を中心に設定(標高のサンプルとして適切)
const lat = 35.360556;
const lng = 138.727778;
mapOptions.center = new ZDC.LatLng(lat, lng);
mapOptions.zoom = 12;
mapOptions.mouseWheelReverseZoom = true;
const mapElement = document.getElementById('ZMap');
if (!mapElement) {
console.error('ZMap element is not found.');
setError('地図表示エリアが見つかりません');
return;
}
map = new ZDC.Map(
mapElement,
mapOptions,
function () {
map.addControl(new ZDC.ZoomButton('bottom-right', new ZDC.Point(-20, -35)));
map.addControl(new ZDC.ScaleBar('bottom-left'));
map.addEventListener('click', onMapClick);
},
function () {
setError('地図の初期化に失敗しました');
}
);
});
function onMapClick(e) {
if (!e || !e.latlng) {
return;
}
const lat = e.latlng.lat;
const lng = e.latlng.lng;
getElevation(lat, lng);
}
async function getElevation(lat, lng) {
setError('');
// 30地点の制限チェック
if (elevationPoints.length >= MAX_ELEVATION_POINTS) {
setError(`最大${MAX_ELEVATION_POINTS}地点までしか追加できません。クリアボタンで削除してから追加してください。`);
return;
}
setLoading(true);
try {
const meshType = $('meshType').value;
const position = `${lng},${lat}`;
const params = new URLSearchParams({
position: position,
mesh_type: meshType,
datum: 'JGD'
});
const res = await fetch(`${API_ENDPOINT}?${params.toString()}`, {
headers: {
'x-api-key': YOUR_API_KEY,
'Authorization': API_AUTH
}
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const json = await res.json();
if (json.status !== 'OK') {
throw new Error(json.message || '標高情報の取得に失敗しました');
}
const result = json.result;
if (!result || !result.item || result.item.length === 0) {
throw new Error('標高情報が見つかりませんでした');
}
// 最初の結果を使用(複数のメッシュタイプが指定されている場合)
const item = result.item[0];
const elevation = parseFloat(item.elevation);
if (isNaN(elevation)) {
throw new Error('標高データが無効です');
}
addElevationPoint(lat, lng, elevation, item);
} catch (e) {
console.error('標高取得エラー:', e);
setError(e.message || '標高情報の取得に失敗しました');
} finally {
setLoading(false);
}
}
function addElevationPoint(lat, lng, elevation, item) {
// 新しい地点の番号を決定(1から30まで)
const pointNumber = elevationPoints.length + 1;
const point = {
id: Date.now(),
lat: lat,
lng: lng,
elevation: elevation,
meshType: item.mesh_type || 'N/A',
timestamp: new Date(),
number: pointNumber
};
elevationPoints.push(point);
// マーカーを追加(ナンバリング付き)
addMarker(lat, lng, elevation, pointNumber);
updateCurrentElevation(elevation, lat, lng);
updateElevationList();
updateStatistics();
}
function addMarker(lat, lng, elevation, number) {
if (!map) {
console.warn('Map is not initialized');
return;
}
try {
const position = new ZDC.LatLng(lat, lng);
// ナンバリング用のContent Style IDを取得(1-30)
const numberStyleMap = {
1: ZDC.MARKER_NUMBER_ID_1_L, 2: ZDC.MARKER_NUMBER_ID_2_L, 3: ZDC.MARKER_NUMBER_ID_3_L,
4: ZDC.MARKER_NUMBER_ID_4_L, 5: ZDC.MARKER_NUMBER_ID_5_L, 6: ZDC.MARKER_NUMBER_ID_6_L,
7: ZDC.MARKER_NUMBER_ID_7_L, 8: ZDC.MARKER_NUMBER_ID_8_L, 9: ZDC.MARKER_NUMBER_ID_9_L,
10: ZDC.MARKER_NUMBER_ID_10_L, 11: ZDC.MARKER_NUMBER_ID_11_L, 12: ZDC.MARKER_NUMBER_ID_12_L,
13: ZDC.MARKER_NUMBER_ID_13_L, 14: ZDC.MARKER_NUMBER_ID_14_L, 15: ZDC.MARKER_NUMBER_ID_15_L,
16: ZDC.MARKER_NUMBER_ID_16_L, 17: ZDC.MARKER_NUMBER_ID_17_L, 18: ZDC.MARKER_NUMBER_ID_18_L,
19: ZDC.MARKER_NUMBER_ID_19_L, 20: ZDC.MARKER_NUMBER_ID_20_L, 21: ZDC.MARKER_NUMBER_ID_21_L,
22: ZDC.MARKER_NUMBER_ID_22_L, 23: ZDC.MARKER_NUMBER_ID_23_L, 24: ZDC.MARKER_NUMBER_ID_24_L,
25: ZDC.MARKER_NUMBER_ID_25_L, 26: ZDC.MARKER_NUMBER_ID_26_L, 27: ZDC.MARKER_NUMBER_ID_27_L,
28: ZDC.MARKER_NUMBER_ID_28_L, 29: ZDC.MARKER_NUMBER_ID_29_L, 30: ZDC.MARKER_NUMBER_ID_30_L
};
// マーカーオプションを設定
const markerOptions = {
styleId: ZDC.MARKER_COLOR_ID_BLUE_L // マーカーのスタイル(L=Large)
};
// ナンバリングを設定(1-30の範囲内の場合のみ)
if (number >= 1 && number <= 30 && numberStyleMap[number]) {
markerOptions.contentStyleId = numberStyleMap[number];
}
const marker = new ZDC.Marker(position, markerOptions);
map.addWidget(marker);
markers.push(marker);
marker.addEventListener('click', () => {
updateCurrentElevation(elevation, lat, lng);
});
} catch (error) {
console.error('マーカーの追加に失敗:', error);
}
}
function updateCurrentElevation(elevation, lat, lng) {
if (elevation !== null && lat !== null && lng !== null) {
$('currentElevation').textContent = elevation.toFixed(1);
$('coordinateText').textContent = `座標: ${lat.toFixed(6)}, ${lng.toFixed(6)}`;
} else {
$('currentElevation').textContent = '--';
$('coordinateText').textContent = '座標: --';
}
}
function updateElevationList() {
const list = $('elevationList');
const pointCount = $('pointCount');
pointCount.textContent = elevationPoints.length;
if (elevationPoints.length === 0) {
list.innerHTML = '<div class="empty-message">地図上をクリックして標高を取得してください</div>';
return;
}
const sortedPoints = [...elevationPoints].reverse();
list.innerHTML = sortedPoints.map((point, index) => {
const timeStr = point.timestamp.toLocaleTimeString('ja-JP');
const pointNumber = point.number || (sortedPoints.length - index);
return `
<div class="elevation-item" data-id="${point.id}">
<div class="elevation-item-header">
<span class="elevation-item-number">#${pointNumber}</span>
<span class="elevation-item-elevation">${point.elevation.toFixed(1)}m</span>
</div>
<div class="elevation-item-details">
<div class="elevation-item-coord">${point.lat.toFixed(6)}, ${point.lng.toFixed(6)}</div>
<div class="elevation-item-meta">
<span class="elevation-item-mesh">${point.meshType}</span>
<span class="elevation-item-time">${timeStr}</span>
</div>
</div>
</div>
`;
}).join('');
list.querySelectorAll('.elevation-item').forEach(item => {
item.addEventListener('click', () => {
const id = parseInt(item.dataset.id);
const point = elevationPoints.find(p => p.id === id);
if (point) {
const position = new ZDC.LatLng(point.lat, point.lng);
map.setCenter(position);
map.setZoom(16);
updateCurrentElevation(point.elevation, point.lat, point.lng);
}
});
});
}
function updateStatistics() {
if (elevationPoints.length === 0) {
$('statsTitle').style.display = 'none';
$('statsContainer').style.display = 'none';
return;
}
const elevations = elevationPoints.map(p => p.elevation);
const max = Math.max(...elevations);
const min = Math.min(...elevations);
const diff = max - min;
const avg = elevations.reduce((a, b) => a + b, 0) / elevations.length;
$('maxElevation').textContent = max.toFixed(1);
$('minElevation').textContent = min.toFixed(1);
$('elevationDiff').textContent = diff.toFixed(1);
$('avgElevation').textContent = avg.toFixed(1);
$('statsTitle').style.display = 'block';
$('statsContainer').style.display = 'block';
}
function clearMarkers() {
if (!map) return;
markers.forEach(marker => {
try {
map.removeWidget(marker);
} catch (e) {
console.warn('マーカーの削除に失敗:', e);
}
});
markers = [];
}
function clearForm() {
elevationPoints = [];
clearMarkers();
updateCurrentElevation(null, null, null);
updateElevationList();
updateStatistics();
setError('');
}
function centerMap() {
if (!map) return;
if (elevationPoints.length > 0) {
const firstPoint = elevationPoints[0];
const position = new ZDC.LatLng(firstPoint.lat, firstPoint.lng);
map.setCenter(position);
map.setZoom(14);
} else {
// 富士山に戻る
const position = new ZDC.LatLng(35.360556, 138.727778);
map.setCenter(position);
map.setZoom(12);
}
}
function setLoading(show) {
$('loading').classList.toggle('show', show);
}
function setError(message) {
const box = $('errorMessage');
$('errorMessageText').textContent = message || '';
box.classList.toggle('show', Boolean(message));
}
function wireEvents() {
$('btnClear').addEventListener('click', clearForm);
$('btnCenter').addEventListener('click', centerMap);
}
window.addEventListener('DOMContentLoaded', () => {
wireEvents();
});
ステップ解説
Step 1:地図描画領域とコントロールパネルを準備する
<div id="ZMap"></div>
<div class="control-panel">
<div class="panel-header">
<h2><i class="fas fa-mountain"></i> 標高情報取得</h2>
<p>地図上をクリックして標高を取得できます</p>
</div>
...
<select id="meshType" class="select">
<option value="N05m,N10m,D500m" selected>5m/10m/500m(推奨)</option>
<option value="N05m">5mメッシュ</option>
...
</select>
...
<div class="elevation-list" id="elevationList">...</div>
</div>
-
#ZMapの高さ確保による地図表示の安定化。 - 機能別に整理されたコントロールパネル構成による操作性の向上。
-
meshTypeと API パラメータmesh_typeの直接対応による運用上の明確性。 - メッシュ優先順序による標高取得結果の違いへの注意。
Step 2:初期セットアップ
// マップ変数
let map;
let markers = [];
let elevationPoints = [];
// 最大標高点数(マーカーのナンバリングが30まで)
const MAX_ELEVATION_POINTS = 30;
const $ = (id) => document.getElementById(id);
const API_ENDPOINT = 'https://test-web.zmaps-api.com/search/elevation';
const API_KEY = 'YOUR_API_KEY'; // 実運用では環境変数などから取得
const API_AUTH = 'referer';
-
markers・elevationPointsを用いた状態管理の徹底。 -
MAX_ELEVATION_POINTSによるリソース制御の明確化。 - APIキーをそのままコードに書くと第三者に悪用される危険性の存在。
- 本番環境では APIキーを外部に見えない場所で管理する仕組みの必要。
Step 3:ZENRIN Maps API ローダのコールバックで地図初期化
ZMALoader.setOnLoad(function (mapOptions, error) {
if (error) {
console.error('Map Loader Error:', error);
setError('地図の読み込みに失敗しました');
return;
}
// 富士山を中心に設定(標高のサンプルとして適切)
const lat = 35.360556;
const lng = 138.727778;
mapOptions.center = new ZDC.LatLng(lat, lng);
mapOptions.zoom = 12;
mapOptions.mouseWheelReverseZoom = true;
const mapElement = document.getElementById('ZMap');
if (!mapElement) {
console.error('ZMap element is not found.');
setError('地図表示エリアが見つかりません');
return;
}
map = new ZDC.Map(
mapElement,
mapOptions,
function () {
map.addControl(new ZDC.ZoomButton('bottom-right', new ZDC.Point(-20, -35)));
map.addControl(new ZDC.ScaleBar('bottom-left'));
map.addEventListener('click', onMapClick);
},
function () {
setError('地図の初期化に失敗しました');
}
);
});
- 富士山を初期中心とした標高サンプルとしての適性。
- マップ初期化成功時の UI コントロール追加の順序性。
- クリックイベント登録によるユーザー操作の導線確保。
-
ZMALoaderに依存したmapOptions受け渡し仕様への注意。
Step 4:地図クリックで緯度経度を取得する
function onMapClick(e) {
if (!e || !e.latlng) {
return;
}
const lat = e.latlng.lat;
const lng = e.latlng.lng;
getElevation(lat, lng);
}
-
e.latlngの存在チェックによるイベント安全性確保。 - 引数の
(lat, lng)と API のlng,lat形式の混同防止。 - UI と API で順序が異なることによる設計上の落とし穴。
Step 5:標高取得の実処理:getElevation(lat, lng)
async function getElevation(lat, lng) {
setError('');
// 30地点の制限チェック
if (elevationPoints.length >= MAX_ELEVATION_POINTS) {
setError(`最大${MAX_ELEVATION_POINTS}地点までしか追加できません。クリアボタンで削除してから追加してください。`);
return;
}
setLoading(true);
try {
const meshType = $('meshType').value;
const position = `${lng},${lat}`;
const params = new URLSearchParams({
position: position,
mesh_type: meshType,
datum: 'JGD'
});
const res = await fetch(`${API_ENDPOINT}?${params.toString()}`, {
headers: {
'x-api-key': API_KEY,
'Authorization': API_AUTH
}
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const json = await res.json();
if (json.status !== 'OK') {
throw new Error(json.message || '標高情報の取得に失敗しました');
}
const result = json.result;
if (!result || !result.item || result.item.length === 0) {
throw new Error('標高情報が見つかりませんでした');
}
// 最初の結果を使用(複数のメッシュタイプが指定されている場合)
const item = result.item[0];
const elevation = parseFloat(item.elevation);
if (isNaN(elevation)) {
throw new Error('標高データが無効です');
}
addElevationPoint(lat, lng, elevation, item);
} catch (e) {
console.error('標高取得エラー:', e);
setError(e.message || '標高情報の取得に失敗しました');
} finally {
setLoading(false);
}
}
-
positionをlng,latとして送信する API 仕様の厳守。 - 複数メッシュ指定時の優先順序による取得結果の差異への注意。
- 標高値の欠損・水深値(負値)を含むレスポンスの特性理解。
-
setErrorを用いたユーザー向けエラー提示の徹底。 - 通信遅延を考慮したローディング表示の必要。
Step 6:取得結果を内部配列へ保存・マーカー追加
function addElevationPoint(lat, lng, elevation, item) {
// 新しい地点の番号を決定(1から30まで)
const pointNumber = elevationPoints.length + 1;
const point = {
id: Date.now(),
lat: lat,
lng: lng,
elevation: elevation,
meshType: item.mesh_type || 'N/A',
timestamp: new Date(),
number: pointNumber
};
elevationPoints.push(point);
// マーカーを追加(ナンバリング付き)
addMarker(lat, lng, elevation, pointNumber);
updateCurrentElevation(elevation, lat, lng);
updateElevationList();
updateStatistics();
}
-
Date.now()によるユニークID生成の簡潔性。 -
meshTypeの保持による一覧表示での識別性向上。 - マーカー追加→UI更新の一貫した更新フローの維持。
-
elevationPointsを唯一の真実源とするデータ整合性の確保。
Step 7:マーカー生成とナンバリング
function addMarker(lat, lng, elevation, number) {
if (!map) {
console.warn('Map is not initialized');
return;
}
try {
const position = new ZDC.LatLng(lat, lng);
const numberStyleMap = {
1: ZDC.MARKER_NUMBER_ID_1_L, 2: ZDC.MARKER_NUMBER_ID_2_L, /* ... */ 30: ZDC.MARKER_NUMBER_ID_30_L
};
const markerOptions = {
styleId: ZDC.MARKER_COLOR_ID_BLUE_L
};
if (number >= 1 && number <= 30 && numberStyleMap[number]) {
markerOptions.contentStyleId = numberStyleMap[number];
}
const marker = new ZDC.Marker(position, markerOptions);
map.addWidget(marker);
markers.push(marker);
marker.addEventListener('click', () => {
updateCurrentElevation(elevation, lat, lng);
});
} catch (error) {
console.error('マーカーの追加に失敗:', error);
}
}
-
MARKER_NUMBER_ID_*による1〜30の番号付与の安定性。 - ナンバリング上限を増やす際のマーカーアセット追加の必要。
- マーカークリックによる双方向状態更新の実現。
- マーカー配列による削除管理の容易性。
Step 8:一覧表示と項目クリックの連携
function updateElevationList() {
const list = $('elevationList');
const pointCount = $('pointCount');
pointCount.textContent = elevationPoints.length;
if (elevationPoints.length === 0) {
list.innerHTML = '<div class="empty-message">地図上をクリックして標高を取得してください</div>';
return;
}
const sortedPoints = [...elevationPoints].reverse();
list.innerHTML = sortedPoints.map((point, index) => {
const timeStr = point.timestamp.toLocaleTimeString('ja-JP');
const pointNumber = point.number || (sortedPoints.length - index);
return `
<div class="elevation-item" data-id="${point.id}">
<div class="elevation-item-header">
<span class="elevation-item-number">#${pointNumber}</span>
<span class="elevation-item-elevation">${point.elevation.toFixed(1)}m</span>
</div>
<div class="elevation-item-details">
<div class="elevation-item-coord">${point.lat.toFixed(6)}, ${point.lng.toFixed(6)}</div>
<div class="elevation-item-meta">
<span class="elevation-item-mesh">${point.meshType}</span>
<span class="elevation-item-time">${timeStr}</span>
</div>
</div>
</div>
`;
}).join('');
list.querySelectorAll('.elevation-item').forEach(item => {
item.addEventListener('click', () => {
const id = parseInt(item.dataset.id);
const point = elevationPoints.find(p => p.id === id);
if (point) {
const position = new ZDC.LatLng(point.lat, point.lng);
map.setCenter(position);
map.setZoom(16);
updateCurrentElevation(point.elevation, point.lat, point.lng);
}
});
});
}
-
reverse()による新着順表示の可読性向上。 -
data-idに基づく地図センタリングとの連携性。 -
toLocaleTimeStringによるローカライズ表示の柔軟性。 - 大量リストでのレンダリング最適化(仮想リスト化)の必要性。
Step 9:統計計算
function updateStatistics() {
if (elevationPoints.length === 0) {
$('statsTitle').style.display = 'none';
$('statsContainer').style.display = 'none';
return;
}
const elevations = elevationPoints.map(p => p.elevation);
const max = Math.max(...elevations);
const min = Math.min(...elevations);
const diff = max - min;
const avg = elevations.reduce((a, b) => a + b, 0) / elevations.length;
$('maxElevation').textContent = max.toFixed(1);
$('minElevation').textContent = min.toFixed(1);
$('elevationDiff').textContent = diff.toFixed(1);
$('avgElevation').textContent = avg.toFixed(1);
$('statsTitle').style.display = 'block';
$('statsContainer').style.display = 'block';
}
- 入力データソースを elevationPoints に統一した整合性維持。
-
toFixed(1)による表示フォーマット統一の明確化。 - 水深(負値)を含む場合の統計値解釈の注意。
- 要素無し時の UI 非表示による視認性確保。
Step 10:クリア/センタリング等の補助機能
function clearMarkers() {
if (!map) return;
markers.forEach(marker => {
try {
map.removeWidget(marker);
} catch (e) {
console.warn('マーカーの削除に失敗:', e);
}
});
markers = [];
}
function clearForm() {
elevationPoints = [];
clearMarkers();
updateCurrentElevation(null, null, null);
updateElevationList();
updateStatistics();
setError('');
}
function centerMap() {
if (!map) return;
if (elevationPoints.length > 0) {
const firstPoint = elevationPoints[0];
const position = new ZDC.LatLng(firstPoint.lat, firstPoint.lng);
map.setCenter(position);
map.setZoom(14);
} else {
// 富士山に戻る
const position = new ZDC.LatLng(35.360556, 138.727778);
map.setCenter(position);
map.setZoom(12);
}
}
-
clearFormによる UI・内部状態の完全初期化。 - 富士山を初期センターとした初期状態再現性。
- 状況に応じたズームレベル調整の柔軟性。
- クリア/センター操作の操作性向上。
Step 11:ローディング・エラーハンドリング
function setLoading(show) {
$('loading').classList.toggle('show', show);
}
function setError(message) {
const box = $('errorMessage');
$('errorMessageText').textContent = message || '';
box.classList.toggle('show', Boolean(message));
}
- ローディング表示による処理中の視覚的フィードバック。
-
setErrorによる例外時のユーザー通知の即時性。 - ネットワーク障害・APIエラーの可視化による保守容易性。
おわりに
今回のサンプルでは、地図をクリックして標高を取得し、
マーカー・一覧・統計まで一通りそろった「標高チェックツール」を作成しました。
クリックするだけで高度が分かる仕組みは、実際のサービスや業務でも応用しやすく、
- 不動産サイトの「この物件の標高」表示
- 避難所情報システムの標高チェック
- 観光サイトの山・展望スポット紹介
- ドローン飛行ルート作成の補助
- 防災やハザードマップの補足情報
など、さまざまな場面で役立てることができます。
また、今回のコードはシンプルにまとめてあるため、
色を変えたマーカー表示や CSV 形式での書き出し、
複数地点の標高差から「断面図」を描く機能など、
次のステップにも発展させやすくなっています。
ZENRIN Maps API を使った地図アプリ開発の最初の一歩として、
ぜひ自分の用途に合わせて拡張しながら楽しんでみてください。

