0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【初心者向け】ZENRIN Maps APIで標高・水深情報を取得する方法

Last updated at Posted at 2025-12-25

はじめに

地図の上をクリックすると、その場所の 標高(山・地形)や 水深(海・川の深さ)を取得できたら便利だと思いませんか?

例えば、

  • 登山ルートを検討する
  • 避難所や安全ルートの標高をチェックする
  • 災害・浸水シミュレーション素材を作る
  • 海岸や川周辺の地形調査をする
  • サイトやアプリに「クリックで標高表示」機能を追加する

といった用途で役立ちます。

本記事では、ZENRIN Maps API の 標高・水深情報検索 API を使って、
地図クリックだけで標高を取得し、マーカー番号(1〜30)や統計(最大・最小・平均)も見られる
シンプルかつ学習しやすいサンプルを作っていきます。

掲載しているコードはすべてそのままコピーして動かせる構成になっており、
HTML と JavaScript を読み進めるだけで、標高取得の基本的な仕組みが理解できるようになっています。

この記事でできること

  • 地図を表示する
  • 地図クリックで指定地点の標高(m)/水深(m)を取得する
  • 取得結果をマーカー(番号付き)で表示し、一覧・統計(最大/最小/平均)を表示する

APIキーの取得手順

ZENRIN Maps API を利用するには、事前に APIキーの取得が必要です。

現在、ZENRIN Maps API は 2か月間の無料トライアルが用意されており、期間中は主要な機能を実際にお試しいただけます。開発や評価の初期段階でも安心してご利用いただけます。

APIキーの取得方法については、以下の記事で詳しく解説されています。
初めての方は、まずこちらをご覧いただき、APIキーの発行と設定を行ってください。

ZENRIN Maps APIの始め方

公式リファレンス

ファイル構成

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(クリックで展開)
zma_elevation.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; }
}
zma_elevation.js
// マップ変数
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();
});

コードを実行した結果は、以下になります。
elevation_default.png

elevation_2.png

ステップ解説

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);
    }
}
  • positionlng,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 を使った地図アプリ開発の最初の一歩として、
ぜひ自分の用途に合わせて拡張しながら楽しんでみてください。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?