1
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 を使って自動車ルート候補一覧を取得する

1
Last updated at Posted at 2026-03-04

はじめに

本記事では、ZENRIN Maps API Web API「自動車ルート候補一覧取得2.0(route_mbn/drive_list)」 を使用し、出発地・目的地間の複数ルート候補を取得し、距離・所要時間・料金を比較表示する方法を解説します。
本サンプルは、自動車ルート候補一覧取得2.0 API を用いて、 複数ルートを比較・検討する一連の流れ を確認できる実装例です。

本記事では、個々の機能説明ではなく、「複数ルート候補を取得し、一覧と地図で比較する流れ」を中心に解説します。

この記事でできること

  • ZENRIN Maps API を利用した地図の表示
  • 出発地・目的地の緯度経度入力
  • 地図クリックによる出発地・目的地の緯度経度自動入力
  • 自動車ルート候補一覧取得2.0(drive_list)による複数ルート候補検索
  • 複数ルートのポリライン同時表示
  • START / GOAL のユーザーウィジェット表示
  • 距離・所要時間・料金の一覧表示
  • ルート選択時の地図上ルート強調表示
  • ポリゴン指定による迂回エリア設定
  • クリアボタンによる検索結果と地図表示のリセット

APIキーの取得手順

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

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

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

ZENRIN Maps APIの始め方

公式リファレンス

ファイル構成

├─ zma_route_list.html
├─ css/
 └─ zma_route_list.css
└─ js/
└─ zma_route_list.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 - 自動車ルート候補一覧取得2.0 サンプル</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_route_list.css">
</head>
<body>
    <!-- 地図を全画面表示 -->
    <div id="ZMap"></div>

    <!-- 検索画面(地図上にオーバーレイ) -->
    <div class="form-panel">
        <div class="panel-header">
            <h2>🚗 自動車ルート候補一覧取得2.0</h2>
        </div>
        <div class="panel-body">
            <div class="info-box">
                <h3>📝 利用用途</h3>
                <p>
                    このサンプルは、出発地と目的地を指定して複数のルート候補を取得し、
                    距離・時間・料金を比較して最適なルートを選択できる実用例です。
                </p>
            </div>

            <div class="form-group">
                <label>出発地(経度, 緯度)</label>
                <div class="input-with-action">
                    <input type="text" id="from" placeholder="例: 139.767125,35.681236" value="139.767125,35.681236">
                    <button type="button" class="btn-secondary" id="btnSetFrom" onclick="setClickMode('from')">地図クリックで設定</button>
                </div>
                <small style="color: #666; font-size: 12px;">例: 東京駅</small>
            </div>

            <div class="form-group">
                <label>目的地(経度, 緯度)</label>
                <div class="input-with-action">
                    <input type="text" id="to" placeholder="例: 139.691706,35.689487" value="139.691706,35.689487">
                    <button type="button" class="btn-secondary" id="btnSetTo" onclick="setClickMode('to')">地図クリックで設定</button>
                </div>
                <small style="color: #666; font-size: 12px;">例: 新宿駅</small>
            </div>

            <div class="form-group">
                <label>料金計算用車種(toll_type)</label>
                <select id="tollType" class="select">
                    <option value="light">軽自動車</option>
                    <option value="normal" selected>普通車</option>
                    <option value="middle">中型車</option>
                    <option value="large">大型車</option>
                    <option value="big">特大車</option>
                </select>
                <small style="color: #666; font-size: 12px;">料金計算に使用する車種を選択</small>
            </div>

            <div class="form-group">
                <label>ルート探索用車種(regulation_type)</label>
                <select id="regulationType" class="select">
                    <optgroup label="大型車">
                        <option value="121100">四輪(大型貨物)</option>
                        <option value="121200">四輪(大型乗用)</option>
                        <option value="121210">四輪(大型バス)</option>
                        <option value="121220">四輪(路線バス)</option>
                    </optgroup>
                    <optgroup label="普通車">
                        <option value="122100">四輪(普通貨物)</option>
                        <option value="122200" selected>四輪(普通乗用)</option>
                        <option value="122210">四輪(普通バス)</option>
                        <option value="122220">四輪(普通タクシー)</option>
                        <option value="122230">四輪(普通軽自動車)</option>
                    </optgroup>
                    <optgroup label="中型車">
                        <option value="123100">四輪(中型貨物)</option>
                        <option value="123200">四輪(中型乗用)</option>
                        <option value="123210">四輪(中型バス)</option>
                        <option value="123220">四輪(マイクロバス)</option>
                    </optgroup>
                    <optgroup label="小型車">
                        <option value="124100">四輪(小型貨物)</option>
                        <option value="124200">四輪(小型乗用)</option>
                        <option value="124210">四輪(小型バス)</option>
                    </optgroup>
                    <optgroup label="特定中型車">
                        <option value="125100">四輪(特定中型貨物)</option>
                        <option value="125200">四輪(特定中型乗用)</option>
                        <option value="125210">四輪(特定中型バス)</option>
                        <option value="125220">四輪(特定中型マイクロバス)</option>
                    </optgroup>
                    <optgroup label="準中型車">
                        <option value="126100">四輪(準中型貨物)</option>
                        <option value="126200">四輪(準中型乗用)</option>
                        <option value="126210">四輪(準中型バス)</option>
                        <option value="126220">四輪(準中型マイクロバス)</option>
                    </optgroup>
                    <optgroup label="特殊車両">
                        <option value="301000">特殊車両(大型)</option>
                        <option value="302000">特殊車両(小型)</option>
                        <option value="302010">特殊車両(農耕車)</option>
                    </optgroup>
                </select>
                <small style="color: #666; font-size: 12px;">ルート探索時の規制情報を考慮する車種を選択</small>
            </div>

            <div class="form-group">
                <label>迂回エリア設定</label>
                <div style="display: flex; gap: 8px; margin-bottom: 8px;">
                    <button type="button" class="btn-secondary" id="btnStartPolygon" onclick="startPolygonDrawing()">
                        📐 ポリゴン描画開始
                    </button>
                    <button type="button" class="btn-secondary" id="btnFinishPolygon" onclick="finishPolygonDrawing()" disabled>
                        ✓ 確定
                    </button>
                    <button type="button" class="btn-secondary" id="btnClearPolygon" onclick="clearPolygon()" disabled>
                        🗑️ 削除
                    </button>
                </div>
                <div id="polygonInfo" style="display: none; padding: 8px; background: #f0f4ff; border-radius: 4px; font-size: 12px; margin-top: 8px;">
                    <strong>迂回エリア:</strong> <span id="polygonBounds"></span>
                </div>
                <small style="color: #666; font-size: 12px;">地図上をクリックしてポリゴンを作成(右クリックで確定)</small>
            </div>

            <div style="display: flex; gap: 8px;">
                <button class="btn" id="searchBtn" onclick="searchRouteCandidates()" style="flex: 1;">
                    🔍 ルート候補を検索
                </button>
                <button class="btn-secondary" id="clearBtn" onclick="clearSearch()" style="flex: 1;">
                    🗑️ クリア
                </button>
            </div>

            <div id="errorMessage" style="display: none;"></div>
        </div>
    </div>

    <!-- 検索結果画面(地図上にオーバーレイ) -->
    <div class="results-panel">
        <div class="panel-header">
            <h2>ルート候補一覧</h2>
        </div>
        <div class="panel-body">
            <div id="results"></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_route_list.js"></script>
</body>
</html>
CSS(クリックで展開)
zma_route_list.css
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    margin: 0;
    padding: 0;
    overflow: hidden;
}

/* 地図を全画面表示 */
#ZMap {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100vh;
    z-index: 1;
}

/* 検索画面パネル(地図上にオーバーレイ) */
.form-panel {
    position: absolute;
    top: 20px;
    left: 20px;
    width: 380px;
    max-height: calc(100vh - 40px);
    background: white;
    border-radius: 12px;
    box-shadow: 0 4px 20px rgba(0,0,0,0.3);
    z-index: 1000;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

/* 検索結果パネル(地図上にオーバーレイ) */
.results-panel {
    position: absolute;
    top: 20px;
    right: 20px;
    width: 420px;
    max-height: calc(100vh - 40px);
    background: white;
    border-radius: 12px;
    box-shadow: 0 4px 20px rgba(0,0,0,0.3);
    z-index: 1000;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.panel-header {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    padding: 16px 20px;
}

.panel-header h2 {
    font-size: 18px;
    margin: 0;
}

.panel-body {
    padding: 20px;
    overflow-y: auto;
    flex: 1;
}


.form-group {
    margin-bottom: 15px;
}

.form-group label {
    display: block;
    margin-bottom: 5px;
    font-weight: 600;
    color: #333;
    font-size: 14px;
}

.form-group input,
.form-group select {
    width: 100%;
    padding: 10px;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 14px;
    background: white;
}

.form-group input:focus,
.form-group select:focus {
    outline: none;
    border-color: #667eea;
}

.form-group select {
    cursor: pointer;
}

.btn {
    background: #667eea;
    color: white;
    border: none;
    padding: 12px 24px;
    border-radius: 4px;
    cursor: pointer;
    font-size: 16px;
    font-weight: 600;
    width: 100%;
    transition: background 0.3s;
}

.btn:hover {
    background: #5568d3;
}

.btn:disabled {
    background: #ccc;
    cursor: not-allowed;
}

.btn-secondary {
    background: #6c757d;
    color: white;
    border: none;
    padding: 8px 16px;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
    font-weight: 600;
    transition: background 0.3s;
    flex: 1;
}

.btn-secondary:hover:not(:disabled) {
    background: #5a6268;
}

.btn-secondary:disabled {
    background: #ccc;
    cursor: not-allowed;
}

.btn-secondary.active {
    background: #44505a;
}

.input-with-action {
    display: flex;
    gap: 8px;
    align-items: center;
}

.input-with-action input {
    flex: 1;
}


.route-card {
    background: white;
    border: 2px solid #e0e0e0;
    border-radius: 8px;
    padding: 15px;
    margin-bottom: 15px;
    transition: all 0.3s;
    cursor: pointer;
}

.route-card:hover {
    border-color: #667eea;
    box-shadow: 0 2px 8px rgba(102, 126, 234, 0.2);
}

.route-card.selected {
    border-color: #667eea;
    background: #f0f4ff;
}

.route-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 10px;
}

.route-number {
    background: #667eea;
    color: white;
    padding: 4px 12px;
    border-radius: 20px;
    font-size: 12px;
    font-weight: 600;
}

.route-stats {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 10px;
    margin-top: 10px;
}

.stat-item {
    text-align: center;
}

.stat-label {
    font-size: 11px;
    color: #666;
    margin-bottom: 4px;
}

.stat-value {
    font-size: 18px;
    font-weight: 600;
    color: #333;
}

.stat-value.distance {
    color: #10b981;
}

.stat-value.time {
    color: #3b82f6;
}

.stat-value.toll {
    color: #f59e0b;
}

.loading {
    text-align: center;
    padding: 40px;
    color: #666;
}

.spinner {
    border: 3px solid #f3f3f3;
    border-top: 3px solid #667eea;
    border-radius: 50%;
    width: 40px;
    height: 40px;
    animation: spin 1s linear infinite;
    margin: 0 auto 20px;
}

@keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}

.error {
    background: #fee;
    color: #c33;
    padding: 15px;
    border-radius: 4px;
    margin-bottom: 15px;
    border: 1px solid #fcc;
}

.info-box {
    background: #e3f2fd;
    border-left: 4px solid #2196f3;
    padding: 15px;
    margin-bottom: 20px;
    border-radius: 4px;
}

.info-box h3 {
    font-size: 16px;
    margin-bottom: 8px;
    color: #1976d2;
}

.info-box p {
    font-size: 14px;
    color: #555;
    line-height: 1.6;
}

@media (max-width: 1200px) {
    .form-panel {
        width: calc(100% - 40px);
        max-width: 380px;
    }
    
    .results-panel {
        width: calc(100% - 40px);
        max-width: 420px;
        top: auto;
        bottom: 20px;
        right: 20px;
        max-height: calc(50vh - 20px);
    }
}
zma_route_list.js
// マップ変数
let map;
let mapInitialized = false; // 地図の初期化完了フラグ
let routePolylines = []; // 複数のルートポリラインを保持
let fromMarker = null;
let toMarker = null;
let currentRouteItems = []; // 現在表示中のルート候補データ

// ポリゴン描画関連
let polygonDrawingMode = false; // ポリゴン描画モード
let polygonPoints = []; // ポリゴンの頂点
let polygonWidget = null; // 描画中のポリゴン
let polygonMarkers = []; // ポリゴンの頂点マーカー
let avoidAreaPolygon = null; // 確定した迂回エリアポリゴン
let clickSetMode = null; // 'from' | 'to' を地図クリックで設定

// API設定
const API_ENDPOINT = 'https://test-web.zmaps-api.com/route/route_mbn/drive_list';
const API_KEY = 'YOUR_API_KEY'; // 実際の運用では環境変数などから取得
const API_AUTH = 'referer';

// ルートの色定義(各ルートを色分け)
const ROUTE_COLORS = [
    '#0066ff', // 青(推奨ルート)
    '#10b981', // 緑
    '#f59e0b', // オレンジ
    '#ef4444', // 赤
    '#8b5cf6', // 紫
    '#ec4899', // ピンク
    '#06b6d4', // シアン
    '#84cc16'  // ライム
];

// 地図の初期化
ZMALoader.setOnLoad(function (mapOptions, error) {
    if (error) {
        console.error('Map Loader Error:', error);
        return;
    }

    // 地図を作成
    const mapElement = document.getElementById('ZMap');
    if (!mapElement) {
        console.error('ZMap element is not found.');
        return;
    }

    // 東京駅を中心に設定(数値として明示的に設定)
    const lat = 35.681236;
    const lng = 139.767125;

    // 座標の検証
    if (isNaN(lat) || isNaN(lng) || lat < -90 || lat > 90 || lng < -180 || lng > 180) {
        console.error('Invalid coordinates for map center:', {lat, lng});
        return;
    }

    // ZDCが利用可能か確認
    if (typeof ZDC === 'undefined' || !ZDC.LatLng) {
        console.error('ZDC is not available');
        return;
    }

    try {
        // 地図オプションを設定
        mapOptions.center = new ZDC.LatLng(lat, lng);
        mapOptions.zoom = 14;
        mapOptions.mouseWheelReverseZoom = true;

        map = new ZDC.Map(
            mapElement,
            mapOptions,
            function () {
                // 地図の初期化が完了
                try {
                    map.addControl(new ZDC.ZoomButton('bottom-right', new ZDC.Point(-20, -35)));
                    map.addControl(new ZDC.ScaleBar('bottom-left'));
                    mapInitialized = true;
                    console.log('地図の初期化が完了しました');
                    
                    // 地図クリックイベント(ポリゴン描画用)
                    setupPolygonDrawing();
                } catch (error) {
                    console.error('地図コントロールの追加に失敗:', error);
                    mapInitialized = true; // コントロールのエラーでも地図は利用可能
                    setupPolygonDrawing();
                }
            },
            function () {
                console.error('地図の初期化に失敗しました');
                mapInitialized = false;
            }
        );
    } catch (error) {
        console.error('地図の作成に失敗:', error);
        mapInitialized = false;
    }
});

/**
 * ルート候補を検索する
 */
async function searchRouteCandidates() {
    // UI要素の取得
    const fromInput = document.getElementById('from');
    const toInput = document.getElementById('to');
    const tollTypeSelect = document.getElementById('tollType');
    const regulationTypeSelect = document.getElementById('regulationType');
    const searchBtn = document.getElementById('searchBtn');
    const resultsDiv = document.getElementById('results');
    const errorDiv = document.getElementById('errorMessage');

    // エラーメッセージをクリア
    errorDiv.style.display = 'none';
    errorDiv.textContent = '';

    // 入力値の取得とバリデーション
    const from = fromInput.value.trim();
    const to = toInput.value.trim();

    if (!from || !to) {
        showError('出発地と目的地を入力してください');
        return;
    }

    // 座標形式のバリデーション(経度,緯度)
    const fromCoords = from.split(',');
    const toCoords = to.split(',');

    if (fromCoords.length !== 2 || toCoords.length !== 2) {
        showError('座標は「経度,緯度」の形式で入力してください(例: 139.767125,35.681236)');
        return;
    }

    const fromLng = parseFloat(fromCoords[0].trim());
    const fromLat = parseFloat(fromCoords[1].trim());
    const toLng = parseFloat(toCoords[0].trim());
    const toLat = parseFloat(toCoords[1].trim());

    if (isNaN(fromLng) || isNaN(fromLat) || isNaN(toLng) || isNaN(toLat)) {
        showError('座標は数値で入力してください');
        return;
    }

    // 座標範囲のバリデーション
    if (fromLat < -90 || fromLat > 90 || toLat < -90 || toLat > 90 ||
        fromLng < -180 || fromLng > 180 || toLng < -180 || toLng > 180) {
        showError('座標が範囲外です(緯度: -90〜90, 経度: -180〜180)');
        return;
    }

    // ローディング表示
    searchBtn.disabled = true;
    resultsDiv.innerHTML = '<div class="loading"><div class="spinner"></div>ルート候補を検索中...</div>';
    
    // 地図をクリア
    clearMap();

    try {
        // 車種の取得
        const tollType = tollTypeSelect ? tollTypeSelect.value : 'normal';
        const regulationType = regulationTypeSelect ? regulationTypeSelect.value : '122200';
        
        // 迂回エリアの取得
        const avoidArea = getAvoidAreaBounds();
        
        // APIリクエストパラメータの構築
        const params = new URLSearchParams({
            from: `${fromLng},${fromLat}`,
            to: `${toLng},${toLat}`,
            llunit: 'dec',              // 10進度形式
            datum: 'JGD',               // 世界測地系
            toll_type: tollType,         // 料金計算用車種(light, normal, middle, large, big)
            regulation_type: regulationType  // ルート探索用車種(121100-302010)
        });
        
        // 迂回エリアが設定されている場合は追加
        if (avoidArea) {
            params.append('avoid_area', avoidArea);
        }

        // APIリクエストの実行(GETメソッドを使用)
        const response = await fetch(`${API_ENDPOINT}?${params.toString()}`, {
            method: 'GET',
            headers: {
                'x-api-key': API_KEY,
                'Authorization': API_AUTH
            }
        });

        if (!response.ok) {
            // エラーレスポンスの詳細を取得
            const errorText = await response.text();
            console.error('エラーレスポンス本文:', errorText);
            try {
                const errorJson = JSON.parse(errorText);
                console.error('エラーレスポンス(JSON):', errorJson);
            } catch (e) {
                // JSONパースに失敗した場合はそのまま表示
            }
            throw new Error(`HTTPエラー: ${response.status} ${response.statusText}`);
        }

        // JSONレスポンスの取得
        const data = await response.json();
        

        // エラーレスポンスのチェック
        // レスポンス構造: {status: 'OK', result: {...}} または {status: {result: true/false, code: '...', message: '...'}}
        if (data.status === 'OK') {
            // statusが'OK'の場合は成功
            console.log('APIリクエスト成功: status = OK');
        } else if (data.status && typeof data.status === 'object' && !data.status.result) {
            // statusがオブジェクトでresultがfalseの場合はエラー
            const errorCode = data.status.code || 'UNKNOWN';
            const errorMessage = data.status.message || 'ルート検索に失敗しました';
            console.error('=== エラー詳細 ===');
            console.error('エラーコード:', errorCode);
            console.error('エラーメッセージ:', errorMessage);
            console.error('レスポンス全体:', JSON.stringify(data, null, 2));
            throw new Error(`エラーコード: ${errorCode} - ${errorMessage}`);
        } else {
            // その他の場合はエラーとして扱う
            console.error('=== 予期しないレスポンス構造 ===');
            console.error('レスポンス全体:', JSON.stringify(data, null, 2));
            throw new Error('予期しないレスポンス構造です');
        }

        // ルート候補の取得
        const items = data.result?.item || [];
        
        if (items.length === 0) {
            console.error('ルート候補が見つかりませんでした');
            resultsDiv.innerHTML = '<div class="error">ルート候補が見つかりませんでした</div>';
            return;
        }

        // ルート候補データを保存
        currentRouteItems = items;
        
        // ルート候補の表示
        displayRouteCandidates(items);
        
        // 地図上にルートを描画
        displayRoutesOnMap(items, fromLng, fromLat, toLng, toLat);

    } catch (error) {
        console.error('ルート検索エラー:', error);
        showError(`エラーが発生しました: ${error.message}`);
        resultsDiv.innerHTML = '';
    } finally {
        searchBtn.disabled = false;
    }
}

/**
 * マーカーとルートをクリア
 */
function clearMap() {
    if (!map || !mapInitialized) return;
    
    try {
        // ポリラインを削除
        routePolylines.forEach(polyline => {
            if (polyline) {
                try {
                    map.removeWidget(polyline);
                } catch (error) {
                    console.warn('clearMap: Error removing polyline', error);
                }
            }
        });
        routePolylines = [];
        
        // マーカーを削除
        if (fromMarker) {
            try {
                map.removeWidget(fromMarker);
            } catch (error) {
                console.warn('clearMap: Error removing fromMarker', error);
            }
            fromMarker = null;
        }
        if (toMarker) {
            try {
                map.removeWidget(toMarker);
            } catch (error) {
                console.warn('clearMap: Error removing toMarker', error);
            }
            toMarker = null;
        }
    } catch (error) {
        console.error('clearMap: Error clearing map', error);
    }
}

/**
 * ユーザーウィジェットでマーカーを追加(スタート/ゴール用)
 * @param {number} lng - 経度
 * @param {number} lat - 緯度
 * @param {boolean} isFrom - 出発地かどうか
 */
function addMarker(lng, lat, isFrom) {
    if (!map || !mapInitialized) {
        console.warn('addMarker: Map is not initialized');
        return null;
    }
    
    // 数値に変換
    const longitude = typeof lng === 'string' ? parseFloat(lng) : lng;
    const latitude = typeof lat === 'string' ? parseFloat(lat) : lat;
    
    // 数値チェック
    if (typeof longitude !== 'number' || typeof latitude !== 'number' || 
        isNaN(longitude) || isNaN(latitude)) {
        return null;
    }
    
    // 有効な範囲チェック
    if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180) {
        return null;
    }
    
    try {
        const position = new ZDC.LatLng(latitude, longitude);
        
        // スタート/ゴール用のHTMLを作成
        const bgColor = isFrom ? '#10b981' : '#ef4444'; // スタート: 緑、ゴール: 赤
        const icon = isFrom ? 'fa-play' : 'fa-flag-checkered';
        const label = isFrom ? 'START' : 'GOAL';
        
        const htmlSource = `
            <div style="
                background-color: ${bgColor};
                color: white;
                padding: 10px 16px;
                border-radius: 25px;
                font-size: 13px;
                font-weight: bold;
                box-shadow: 0 3px 10px rgba(0,0,0,0.4);
                white-space: nowrap;
                display: inline-flex;
                align-items: center;
                gap: 8px;
                border: 3px solid white;
                text-shadow: 0 1px 2px rgba(0,0,0,0.2);
            ">
                <i class="fas ${icon}" style="font-size: 16px;"></i>
                <span>${label}</span>
            </div>
        `;
        
        // ユーザーウィジェットを作成
        const widget = new ZDC.UserWidget(position, {
            htmlSource: htmlSource,
            offset: new ZDC.Point(-60, -20)
        });
        
        map.addWidget(widget);
        return widget;
    } catch (error) {
        console.error('マーカーの追加に失敗:', error);
        return null;
    }
}

/**
 * ルートを地図上に表示(ポリライン)
 * @param {Array} items - ルート候補の配列
 * @param {number} fromLng - 出発地経度
 * @param {number} fromLat - 出発地緯度
 * @param {number} toLng - 目的地経度
 * @param {number} toLat - 目的地緯度
 */
function displayRoutesOnMap(items, fromLng, fromLat, toLng, toLat) {
    // 地図の初期化を確認
    if (!mapInitialized || !map) {
        console.warn('displayRoutesOnMap: 地図が初期化されていません。少し待ってから再試行してください。');
        // 地図の初期化を待つ(最大5秒)
        let retryCount = 0;
        const maxRetries = 50; // 50回 × 100ms = 5秒
        const checkInterval = setInterval(() => {
            retryCount++;
            if (mapInitialized && map) {
                clearInterval(checkInterval);
                displayRoutesOnMap(items, fromLng, fromLat, toLng, toLat);
            } else if (retryCount >= maxRetries) {
                clearInterval(checkInterval);
                console.error('displayRoutesOnMap: 地図の初期化がタイムアウトしました');
            }
        }, 100);
        return;
    }

    if (!items || items.length === 0) {
        console.error('displayRoutesOnMap: items is missing or empty');
        return;
    }

    clearMap();

    // 出発地・目的地にマーカーを表示
    fromMarker = addMarker(fromLng, fromLat, true);
    toMarker = addMarker(toLng, toLat, false);

    // 全ルートの座標を収集(地図の表示範囲を調整するため)
    const allCoordinates = [];

    // 各ルートをポリラインで描画
    items.forEach((item, index) => {
        const route = item.route;
        
        if (!route) {
            console.warn(`displayRoutesOnMap: route[${index}] is missing`);
            return;
        }

        // 座標を取得
        const coordinates = [];
        
        // レスポンス構造の確認: route.link[] または route.line.coordinates
        if (route.link && Array.isArray(route.link)) {
            route.link.forEach((link, linkIndex) => {
                if (link.line && link.line.coordinates && Array.isArray(link.line.coordinates)) {
                    link.line.coordinates.forEach((coord) => {
                        // GeoJSON形式は[経度, 緯度]なので、ZDC.LatLngは(緯度, 経度)に変換
                        if (Array.isArray(coord) && coord.length >= 2) {
                            const lng = parseFloat(coord[0]); // 経度
                            const lat = parseFloat(coord[1]); // 緯度
                            
                            // NaNチェック
                            if (!isNaN(lng) && !isNaN(lat) && 
                                lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) {
                                try {
                                    const latLng = new ZDC.LatLng(lat, lng);
                                    coordinates.push(latLng);
                                    allCoordinates.push(latLng);
                                } catch (error) {
                                    console.error(`displayRoutesOnMap: Error creating LatLng for route[${index}], link[${linkIndex}]:`, error, {lng, lat});
                                }
                            } else {
                                console.warn(`displayRoutesOnMap: Invalid coordinates for route[${index}], link[${linkIndex}]:`, {lng, lat});
                            }
                        }
                    });
                }
            });
        } else if (route.line && route.line.coordinates && Array.isArray(route.line.coordinates)) {
            route.line.coordinates.forEach((coord) => {
                // GeoJSON形式は[経度, 緯度]なので、ZDC.LatLngは(緯度, 経度)に変換
                if (Array.isArray(coord) && coord.length >= 2) {
                    const lng = parseFloat(coord[0]); // 経度
                    const lat = parseFloat(coord[1]); // 緯度
                    
                    // NaNチェック
                    if (!isNaN(lng) && !isNaN(lat) && 
                        lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) {
                        try {
                            const latLng = new ZDC.LatLng(lat, lng);
                            coordinates.push(latLng);
                            allCoordinates.push(latLng);
                        } catch (error) {
                            console.error(`displayRoutesOnMap: Error creating LatLng for route[${index}]:`, error, {lng, lat});
                        }
                    } else {
                        console.warn(`displayRoutesOnMap: Invalid coordinates for route[${index}]:`, {lng, lat});
                    }
                }
            });
        } else {
            return;
        }

        if (coordinates.length === 0) {
            console.warn(`displayRoutesOnMap: route[${index}] has no valid coordinates`);
            return;
        }

        try {
            // ルートの色を決定(インデックスに応じて色分け)
            const color = ROUTE_COLORS[index % ROUTE_COLORS.length];
            
            // ポリラインを作成
            const polyline = new ZDC.Polyline(coordinates, {
                color: color,
                width: 6, // 太さ
                pattern: 'solid', // 実線
                opacity: 0.8 // 不透明度
            });

            // 地図が初期化されていることを確認してから追加
            if (map && mapInitialized) {
                map.addWidget(polyline);
                routePolylines.push(polyline);
            } else {
                console.warn(`displayRoutesOnMap: 地図が初期化されていないため、route[${index}]をスキップしました`);
            }
        } catch (error) {
            console.error(`displayRoutesOnMap: Error creating polyline for route[${index}]`, error);
        }
    });

    // 地図の表示範囲を調整(ルート全体が表示されるように)
    if (allCoordinates.length > 0 && map && mapInitialized) {
        try {
            const bounds = new ZDC.LatLngBounds();
            let validCoordCount = 0;
            
            allCoordinates.forEach(coord => {
                if (coord) {
                    try {
                        // ZDC.LatLngオブジェクトの場合はそのまま使用
                        bounds.extend(coord);
                        validCoordCount++;
                    } catch (error) {
                        console.warn('displayRoutesOnMap: Error extending bounds with coord', error, coord);
                    }
                }
            });
            
            // 有効な座標が2つ以上ある場合のみfitBoundsを実行
            if (validCoordCount >= 2) {
                map.fitBounds(bounds);
            } else {
                console.warn('displayRoutesOnMap: 有効な座標が不足しています。validCoordCount:', validCoordCount);
            }
        } catch (error) {
            console.error('displayRoutesOnMap: Error fitting bounds', error);
        }
    }
}

/**
 * 選択されたルートを強調表示
 * @param {number} selectedIndex - 選択されたルートのインデックス
 */
function highlightRoute(selectedIndex) {
    if (!map || !mapInitialized || !routePolylines || routePolylines.length === 0) {
        return;
    }

    routePolylines.forEach((polyline, index) => {
        if (!polyline) return;
        
        // 選択されたルートは太く、他は細く
        if (index === selectedIndex) {
            polyline.setOptions({
                width: 10,
                opacity: 1.0
            });
        } else {
            polyline.setOptions({
                width: 4,
                opacity: 0.5
            });
        }
    });
}

/**
 * ルート探索種別の説明を取得する
 * @param {number} searchType - 探索種別コード(1-5)
 * @returns {string} 探索種別の説明
 */
function getSearchTypeLabel(searchType) {
    const searchTypes = {
        1: '推奨(所要時間が短いルート)',
        2: '一般優先(有料道路を極力使用しない)',
        3: '道幅優先(道幅が広い道路を使用)',
        4: '距離優先(距離が短いルート)',
        5: '別ルート(推奨ルートの次に所要時間が短い)'
    };
    return searchTypes[searchType] || `探索種別: ${searchType}`;
}

/**
 * ルート候補を表示する
 * @param {Array} items - ルート候補の配列
 */
function displayRouteCandidates(items) {
    const resultsDiv = document.getElementById('results');
    
    // ルート候補を距離でソート(最短距離順)
    const sortedItems = [...items].sort((a, b) => {
        const distanceA = a.route?.distance || 0;
        const distanceB = b.route?.distance || 0;
        return distanceA - distanceB;
    });

    let html = '';

    sortedItems.forEach((item, index) => {
        const route = item.route;
        const header = item.header;
        if (!route) return;

        // 距離(kmに変換)
        const distance = route.distance ? (route.distance / 1000).toFixed(2) : '-';
        
        // 時間(分を時間と分に変換)
        let timeText = '-';
        if (route.time) {
            const totalMinutes = Math.round(route.time);
            const hours = Math.floor(totalMinutes / 60);
            const minutes = totalMinutes % 60;
            if (hours > 0) {
                timeText = `${hours}時間${minutes}分`;
            } else {
                timeText = `${minutes}分`;
            }
        }

        // 料金
        const toll = route.toll !== undefined ? route.toll.toLocaleString() : '0';
        
        // ルートID(詳細情報取得時に使用)
        const routeId = route.route_id || '';
        
        // ルート探索種別
        const searchType = header?.search_type || null;
        const searchTypeLabel = searchType ? getSearchTypeLabel(searchType) : '';

        html += `
            <div class="route-card" onclick="selectRoute(${index}, '${routeId}')">
                <div class="route-header">
                    <span class="route-number">ルート ${index + 1}</span>
                    ${index === 0 ? '<span style="color: #10b981; font-size: 12px; font-weight: 600;">⭐ 最短距離</span>' : ''}
                </div>
                ${searchTypeLabel ? `<div style="margin-bottom: 10px; padding: 6px 10px; background: #f0f4ff; border-radius: 4px; font-size: 12px; color: #667eea; font-weight: 600;">${searchTypeLabel}</div>` : ''}
                <div class="route-stats">
                    <div class="stat-item">
                        <div class="stat-label">距離</div>
                        <div class="stat-value distance">${distance} km</div>
                    </div>
                    <div class="stat-item">
                        <div class="stat-label">所要時間</div>
                        <div class="stat-value time">${timeText}</div>
                    </div>
                    <div class="stat-item">
                        <div class="stat-label">料金</div>
                        <div class="stat-value toll">¥${toll}</div>
                    </div>
                </div>
                ${routeId ? `<div style="margin-top: 10px; font-size: 11px; color: #666; word-break: break-all;">ルートID: ${routeId}</div>` : ''}
            </div>
        `;
    });

    resultsDiv.innerHTML = html;
}

/**
 * ルートを選択する
 * @param {number} index - ルートのインデックス
 * @param {string} routeId - ルートID
 */
function selectRoute(index, routeId) {
    // 選択状態の視覚的フィードバック
    const cards = document.querySelectorAll('.route-card');
    cards.forEach((card, i) => {
        if (i === index) {
            card.classList.add('selected');
        } else {
            card.classList.remove('selected');
        }
    });

    // 地図上でルートを強調表示
    highlightRoute(index);

    console.log(`選択されたルート: ${index + 1}, ルートID: ${routeId}`);
}

/**
 * エラーメッセージを表示する
 * @param {string} message - エラーメッセージ
 */
function showError(message) {
    const errorDiv = document.getElementById('errorMessage');
    errorDiv.textContent = message;
    errorDiv.style.display = 'block';
    errorDiv.className = 'error';
}

/**
 * ポリゴン描画のセットアップ
 */
function setupPolygonDrawing() {
    if (!map || !mapInitialized) return;
    
    // 地図のクリックイベント
    map.addEventListener('click', function(event) {
        if (polygonDrawingMode && event.latlng) {
            // 緯度経度を取得
            let lat, lng;
            if (typeof event.latlng.lat === 'function') {
                lat = event.latlng.lat();
                lng = event.latlng.lng();
            } else {
                lat = event.latlng.lat;
                lng = event.latlng.lng;
            }
            
            // ポリゴンの頂点を追加
            addPolygonPoint(lat, lng);
        } else if (clickSetMode && event.latlng) {
            // 出発・目的地の設定モード
            let lat, lng;
            if (typeof event.latlng.lat === 'function') {
                lat = event.latlng.lat();
                lng = event.latlng.lng();
            } else {
                lat = event.latlng.lat;
                lng = event.latlng.lng;
            }
            
            // 入力欄へ反映
            if (clickSetMode === 'from') {
                document.getElementById('from').value = `${lng.toFixed(6)},${lat.toFixed(6)}`;
                // 既存マーカーを更新
                if (fromMarker && map) {
                    map.removeWidget(fromMarker);
                }
                fromMarker = addMarker(lng, lat, true);
            } else if (clickSetMode === 'to') {
                document.getElementById('to').value = `${lng.toFixed(6)},${lat.toFixed(6)}`;
                if (toMarker && map) {
                    map.removeWidget(toMarker);
                }
                toMarker = addMarker(lng, lat, false);
            }
            
            // モード解除
            clickSetMode = null;
            updateClickModeUI();
        }
    });
    
    // 右クリックでポリゴンを確定
    map.addEventListener('rightclick', function(event) {
        if (polygonDrawingMode && polygonPoints.length >= 3) {
            finishPolygonDrawing();
        }
    });
}

/**
 * ポリゴン描画を開始
 */
function startPolygonDrawing() {
    if (!map || !mapInitialized) {
        alert('地図が初期化されていません');
        return;
    }

    // クリック設定モードを解除
    clickSetMode = null;
    updateClickModeUI();
    
    // 既存のポリゴンをクリア
    clearPolygon();
    
    polygonDrawingMode = true;
    polygonPoints = [];
    
    // ボタンの状態を更新
    document.getElementById('btnStartPolygon').disabled = true;
    document.getElementById('btnFinishPolygon').disabled = false;
    document.getElementById('btnClearPolygon').disabled = false;
    
    // 情報表示をクリア
    document.getElementById('polygonInfo').style.display = 'none';
    
    alert('地図上をクリックしてポリゴンの頂点を追加してください。\n右クリックまたは「確定」ボタンで完成します。');
}

/**
 * ポリゴンの頂点を追加
 * @param {number} lat - 緯度
 * @param {number} lng - 経度
 */
function addPolygonPoint(lat, lng) {
    if (!map || !mapInitialized) return;
    
    const point = new ZDC.LatLng(lat, lng);
    polygonPoints.push(point);
    
    // 頂点マーカーを追加
    const marker = new ZDC.UserWidget(point, {
        htmlSource: `
            <div style="
                background-color: #ef4444;
                color: white;
                width: 20px;
                height: 20px;
                border-radius: 50%;
                border: 2px solid white;
                box-shadow: 0 2px 4px rgba(0,0,0,0.3);
            "></div>
        `,
        offset: new ZDC.Point(-10, -10)
    });
    map.addWidget(marker);
    polygonMarkers.push(marker);
    
    // ポリゴンを更新(3点以上で描画)
    if (polygonPoints.length >= 3) {
        updatePolygon();
    }
}

/**
 * ポリゴンを更新(描画)
 */
function updatePolygon() {
    if (!map || polygonPoints.length < 3) return;
    
    // 既存のポリゴンを削除
    if (polygonWidget) {
        map.removeWidget(polygonWidget);
    }
    
    // ポリゴンを作成(最初の点を最後にも追加して閉じる)
    const closedPoints = [...polygonPoints, polygonPoints[0]];
    
    polygonWidget = new ZDC.Polygon(closedPoints, {
        fill: '#ef4444',
        fillPattern: 'solid',
        stroke: '#dc2626',
        strokeWidth: 2,
        opacity: 0.3
    });
    
    map.addWidget(polygonWidget);
}

/**
 * ポリゴン描画を確定
 */
function finishPolygonDrawing() {
    if (polygonPoints.length < 3) {
        alert('ポリゴンは最低3点必要です');
        return;
    }
    
    polygonDrawingMode = false;
    
    // 確定したポリゴンを保存
    if (polygonWidget) {
        // 既存の迂回エリアポリゴンを削除
        if (avoidAreaPolygon) {
            map.removeWidget(avoidAreaPolygon);
        }
        
        // 確定したポリゴンを別の色で表示
        const closedPoints = [...polygonPoints, polygonPoints[0]];
        avoidAreaPolygon = new ZDC.Polygon(closedPoints, {
            fill: '#f59e0b',
            fillPattern: 'solid',
            stroke: '#d97706',
            strokeWidth: 3,
            opacity: 0.4
        });
        map.addWidget(avoidAreaPolygon);
        
        // 描画中のポリゴンを削除
        if (polygonWidget) {
            map.removeWidget(polygonWidget);
            polygonWidget = null;
        }
    }
    
    // バウンディングボックスを計算して表示
    const bounds = calculateBounds(polygonPoints);
    if (bounds) {
        const boundsText = `南西: ${bounds.southWest.lng.toFixed(6)}, ${bounds.southWest.lat.toFixed(6)} | 北東: ${bounds.northEast.lng.toFixed(6)}, ${bounds.northEast.lat.toFixed(6)}`;
        document.getElementById('polygonBounds').textContent = boundsText;
        document.getElementById('polygonInfo').style.display = 'block';
    }
    
    // ボタンの状態を更新
    document.getElementById('btnStartPolygon').disabled = false;
    document.getElementById('btnFinishPolygon').disabled = true;
    document.getElementById('btnClearPolygon').disabled = false;
}

/**
 * ポリゴンをクリア
 */
function clearPolygon() {
    polygonDrawingMode = false;
    polygonPoints = [];
    
    // 描画中のポリゴンを削除
    if (polygonWidget) {
        map.removeWidget(polygonWidget);
        polygonWidget = null;
    }
    
    // 確定したポリゴンを削除
    if (avoidAreaPolygon) {
        map.removeWidget(avoidAreaPolygon);
        avoidAreaPolygon = null;
    }
    
    // 頂点マーカーを削除
    polygonMarkers.forEach(marker => {
        if (marker && map) {
            map.removeWidget(marker);
        }
    });
    polygonMarkers = [];
    
    // ボタンの状態を更新
    document.getElementById('btnStartPolygon').disabled = false;
    document.getElementById('btnFinishPolygon').disabled = true;
    document.getElementById('btnClearPolygon').disabled = true;
    
    // 情報表示をクリア
    document.getElementById('polygonInfo').style.display = 'none';
}

/**
 * ポリゴンのバウンディングボックスを計算
 * @param {Array} points - ポリゴンの頂点配列
 * @returns {Object|null} バウンディングボックス {southWest: {lat, lng}, northEast: {lat, lng}}
 */
function calculateBounds(points) {
    if (!points || points.length < 3) return null;
    
    let minLat = Infinity, maxLat = -Infinity;
    let minLng = Infinity, maxLng = -Infinity;
    
    points.forEach(point => {
        const lat = typeof point.lat === 'function' ? point.lat() : point.lat;
        const lng = typeof point.lng === 'function' ? point.lng() : point.lng;
        
        minLat = Math.min(minLat, lat);
        maxLat = Math.max(maxLat, lat);
        minLng = Math.min(minLng, lng);
        maxLng = Math.max(maxLng, lng);
    });
    
    return {
        southWest: { lat: minLat, lng: minLng },
        northEast: { lat: maxLat, lng: maxLng }
    };
}

/**
 * 迂回エリアのバウンディングボックスを取得
 * @returns {string|null} 迂回エリアパラメータ(南西経度,南西緯度,北東経度,北東緯度)
 */
function getAvoidAreaBounds() {
    if (!avoidAreaPolygon || polygonPoints.length < 3) {
        return null;
    }
    
    const bounds = calculateBounds(polygonPoints);
    if (!bounds) return null;
    
    // リファレンス形式: 南西経度,南西緯度,北東経度,北東緯度
    return `${bounds.southWest.lng},${bounds.southWest.lat},${bounds.northEast.lng},${bounds.northEast.lat}`;
}

/**
 * 出発/目的地のクリック設定モードを更新
 * @param {'from'|'to'|null} mode
 */
function setClickMode(mode) {
    if (!mapInitialized) {
        alert('地図が初期化されていません');
        return;
    }
    // ポリゴン描画中は切り替え不可
    if (polygonDrawingMode) {
        alert('ポリゴン描画中は座標設定を行えません。描画を完了または削除してください。');
        return;
    }
    clickSetMode = mode;
    updateClickModeUI();
}

// ボタンの状態を視覚的に反映
function updateClickModeUI() {
    const fromBtn = document.getElementById('btnSetFrom');
    const toBtn = document.getElementById('btnSetTo');
    if (fromBtn && toBtn) {
        fromBtn.classList.toggle('active', clickSetMode === 'from');
        toBtn.classList.toggle('active', clickSetMode === 'to');
    }
}

/**
 * 検索結果をクリア
 */
function clearSearch() {
    // 検索結果をクリア
    const resultsDiv = document.getElementById('results');
    if (resultsDiv) {
        resultsDiv.innerHTML = '';
    }
    
    // エラーメッセージをクリア
    const errorDiv = document.getElementById('errorMessage');
    if (errorDiv) {
        errorDiv.style.display = 'none';
        errorDiv.textContent = '';
    }
    
    // 地図上のルートとマーカーをクリア
    clearMap();
    
    // ルート候補データをクリア
    currentRouteItems = [];
    
    // クリック設定モードを解除
    clickSetMode = null;
    updateClickModeUI();
}

/**
 * Enterキーで検索を実行
 */
document.addEventListener('DOMContentLoaded', () => {
    const fromInput = document.getElementById('from');
    const toInput = document.getElementById('to');

    [fromInput, toInput].forEach(input => {
        input.addEventListener('keydown', (e) => {
            if (e.key === 'Enter') {
                e.preventDefault();
                searchRouteCandidates();
            }
        });
    });
});

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

検索結果です。
routelist_result.png

ルートを選択時
routelist_selected.png

ステップ毎の解説

Step 1:地図の初期化

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'));
    mapInitialized = true;
});
  • ZENRIN Maps API JavaScriptを利用した地図生成処理
  • 中心座標・ズームレベルの設定
  • ズームボタン・スケールバーの追加

※ 地図初期化完了前にルート描画を行わないよう、フラグ管理が重要

Step 2:出発地・目的地入力値の取得とチェック

    // 入力値の取得とバリデーション
    const from = fromInput.value.trim();
    const to = toInput.value.trim();

    if (!from || !to) {
        showError('出発地と目的地を入力してください');
        return;
    }

    // 座標形式のバリデーション(経度,緯度)
    const fromCoords = from.split(',');
    const toCoords = to.split(',');

    if (fromCoords.length !== 2 || toCoords.length !== 2) {
        showError('座標は「経度,緯度」の形式で入力してください(例: 139.767125,35.681236)');
        return;
    }

    const fromLng = parseFloat(fromCoords[0].trim());
    const fromLat = parseFloat(fromCoords[1].trim());
    const toLng = parseFloat(toCoords[0].trim());
    const toLat = parseFloat(toCoords[1].trim());

    if (isNaN(fromLng) || isNaN(fromLat) || isNaN(toLng) || isNaN(toLat)) {
        showError('座標は数値で入力してください');
        return;
    }

    // 座標範囲のバリデーション
    if (fromLat < -90 || fromLat > 90 || toLat < -90 || toLat > 90 ||
        fromLng < -180 || fromLng > 180 || toLng < -180 || toLng > 180) {
        showError('座標が範囲外です(緯度: -90〜90, 経度: -180〜180)');
        return;
    }
  • 出発地・目的地の経度緯度文字列取得
  • 「経度,緯度」形式のチェック
  • 数値変換と範囲チェック

※ 入力形式エラー時は API を呼び出さないことが重要

Step 3:API リクエストパラメータの組み立て

    // APIリクエストパラメータの構築
        const params = new URLSearchParams({
            from: `${fromLng},${fromLat}`,
            to: `${toLng},${toLat}`,
            llunit: 'dec',              // 10進度形式
            datum: 'JGD',               // 世界測地系
            toll_type: tollType,         // 料金計算用車種(light, normal, middle, large, big)
            regulation_type: regulationType  // ルート探索用車種(121100-302010)
        });
        
        // 迂回エリアが設定されている場合は追加
        if (avoidArea) {
            params.append('avoid_area', avoidArea);
        }
  • 出発地・目的地座標の設定
  • 車種条件(toll_type / regulation_type)の指定
  • 測地系・座標単位の明示
  • 迂回エリアの設定

Step 4:ルート候補一覧の取得

// APIリクエストの実行(GETメソッドを使用)
const response = await fetch(`${API_ENDPOINT}?${params.toString()}`, {
    method: 'GET',
    headers: {
        'x-api-key': API_KEY,
        'Authorization': API_AUTH
    }
});
  • HTTP リクエスト送信処理
  • drive_list API の呼び出し
  • レスポンスステータス判定

Step 5:ルート候補一覧の表示

const sortedItems = [...items].sort((a, b) => {
const distanceA = a.route?.distance || 0;
const distanceB = b.route?.distance || 0;
return distanceA - distanceB;
});

const distance = route.distance ? (route.distance / 1000).toFixed(2) : '-';

if (route.time) {
const totalMinutes = Math.round(route.time);
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
}
  • 取得したルート候補の距離順ソート処理
  • 距離(m → km)・所要時間(分 → 時間/分)の表示用変換
  • 料金情報・探索種別ラベルを含めた一覧生成処理

Step 6:地図上へのルート描画

// GeoJSON([lng, lat])形式のルート情報を ZDC.LatLng へ変換
if (route.link && Array.isArray(route.link)) {
    route.link.forEach(link => {
        link.line.coordinates.forEach(coord => {
            const lng = coord[0];
            const lat = coord[1];

            coordinates.push(new ZDC.LatLng(lat, lng));
        });
    });
}


// ルートポリラインの生成
const polyline = new ZDC.Polyline(coordinates, {
    color: color,
    width: 6,
    opacity: 0.8
});
  • GeoJSON 形式の座標配列を ZDC.LatLng へ変換
  • ルートごとのポリライン生成と色分け表示
  • 全ルートを含む表示範囲への自動フィット

※ GeoJSON は [経度, 緯度]、ZDC.LatLng は (緯度, 経度) の順である点に注意

Step 7:ルート選択時の強調表示

routePolylines.forEach((polyline, index) => {
    if (index === selectedIndex) {
        polyline.setOptions({ width: 10, opacity: 1.0 });
    } else {
        polyline.setOptions({ width: 4, opacity: 0.5 });
    }
});
  • 一覧クリックに連動したルート選択処理
  • 選択ルートの線幅・透明度変更
  • 非選択ルートの視認性調整

Step 8:地図クリックによる座標設定

map.addEventListener('click', function (event) {
    // ポリゴン描画モード中は座標設定を行わない
    if (clickSetMode === 'polygon') {
        return;
    }

    const lat = event.latlng.lat;
    const lng = event.latlng.lng;

    // 出発地/目的地の入力欄へ反映
    if (clickSetMode === 'from') {
        document.getElementById('from').value = `${lng.toFixed(6)},${lat.toFixed(6)}`;
        setStartMarker(lat, lng);
    } else if (clickSetMode === 'to') {
        document.getElementById('to').value = `${lng.toFixed(6)},${lat.toFixed(6)}`;
        setGoalMarker(lat, lng);
    }
});
  • 地図クリックイベントの取得
  • クリックモード(出発地/目的地)の判定
  • 経度緯度の小数点桁数調整と入力欄への反映
  • START / GOAL マーカーの更新

Step 9:ポリゴン指定による迂回エリア設定

function getAvoidAreaBounds() {
  return `${bounds.southWest.lng},${bounds.southWest.lat},${bounds.northEast.lng},${bounds.northEast.lat}`;
}
  • 地図上で描画したポリゴン頂点の保持
  • バウンディングボックス算出処理
  • avoid_area パラメータ用文字列生成

** ※ API へ送信されるのはポリゴンではなく矩形(BBox)である点に注意**

Step 10:クリア処理

clearMap();
currentRouteItems = [];
clickSetMode = null;
  • 検索結果一覧のクリア
  • 地図上ポリライン・マーカー削除
  • 入力・クリックモード状態の初期化

※ 前回検索結果が残らないよう、必ず状態変数も初期化することが重要

おわりに

自動車ルート候補一覧取得2.0(drive_list)API を利用することで、 単一ルートを取得するだけでは分からない 「複数ルートを並べて比較する」 という使い方が可能になります。

本記事のサンプルでは、

  • ルート候補の一覧表示
  • 地図上での複数ルート描画
  • 選択ルートの強調表示
  • 車種条件や迂回エリアを考慮した検索

といった、実際の業務システムでよく求められる処理を一通り実装しています。
まずはサンプルを動かしながら、 「どの処理がどの機能につながっているのか」を確認してみてください。

そのうえで、

  • 配送・巡回ルートの事前検討
  • 規制条件を考慮したナビゲーション
  • 災害・工事を想定した迂回ルート確認

など、ご自身の用途に合わせて条件指定や UI を調整していくことで、 より実践的なルート検索機能へ発展させることができます。

本記事が、自動車ルート候補一覧取得 API を使い始める際の参考になれば幸いです。

1
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
1
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?