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 を使った自動車ルート検索(drive_ptp)表示

0
Last updated at Posted at 2026-02-23

はじめに

ZENRIN Maps API には、さまざまな用途に応じて利用できる検索機能があります。
本記事では、その中でも「自動車ルート検索」を扱います。
出発地と目的地を指定すると、距離・時間・料金を含むルート情報が取得でき、実際の地図と合わせて表示することができます。

地図アプリの試作や社内ツールの作成など、まずは簡単にルート検索を試してみたい方に向けて、シンプルなサンプルコードを紹介します。
入力フォームや検索結果の見た目も含めて、動作のイメージがつかみやすいように構成しています。

難しい設定はなく、緯度経度と探索タイプを指定するだけで利用できますので、これからルート検索を取り入れたい方の最初のステップとして参考になれば幸いです。

この記事でできること

  • 地図の表示
  • 出発地・目的地の緯度経度入力
  • 地図クリックで自動的に緯度経度を入力
  • 自動車ルート検索(drive_ptp)
  • ルートのポリライン表示
  • START/GOAL のユーザーウィジェット表示
  • 距離 / 時間 / 現金料金 / ETC料金の表示
  • クリアボタンによるリセット

APIキーの取得手順

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

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

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

ZENRIN Maps APIの始め方

公式リファレンス

ファイル構成

route_sample/
├─ index.html           画面全体のレイアウト
├─ css/
   └─ zma_route_search.css      UIスタイル
└─ js/
    └─ zma_route_search.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_search.css">
</head>

<body>
    <div id="ZMap"></div>

    <div class="route-panel">
        <div class="panel-header">
            <h2><i class="fas fa-route"></i> 自動車ルート検索2.0</h2>
            <p>出発地と目的地を指定してルートを検索します</p>
        </div>

        <div class="panel-body">
            <!-- 出発地セクション -->
            <div class="form-section-title">
                <i class="fas fa-map-marker-alt"></i> 出発地
            </div>

            <div class="form-grid">
                <label class="form-label">経度</label>
                <input type="text" class="input" id="fromLng" placeholder="例: 139.767125">
                
                <label class="form-label">緯度</label>
                <input type="text" class="input" id="fromLat" placeholder="例: 35.681236">
            </div>
            <div class="input-hint">
                <i class="fas fa-info-circle"></i> 例: 東京駅 (経度: 139.767125, 緯度: 35.681236)
            </div>

            <!-- 地図クリック設定セクション -->
            <div class="form-section-title">
                <i class="fas fa-mouse-pointer"></i> 地図クリック設定
            </div>

            <div class="click-mode-selector">
                <label class="radio-label">
                    <input type="radio" name="clickMode" value="from" checked>
                    <span>出発地に設定</span>
                </label>
                <label class="radio-label">
                    <input type="radio" name="clickMode" value="to">
                    <span>目的地に設定</span>
                </label>
            </div>
            <div class="input-hint">
                <i class="fas fa-info-circle"></i> 地図上をクリックすると、選択した方のテキストボックスに緯度経度が自動入力されます
            </div>

            <!-- 目的地セクション -->
            <div class="form-section-title">
                <i class="fas fa-flag-checkered"></i> 目的地
            </div>

            <div class="form-grid">
                <label class="form-label">経度</label>
                <input type="text" class="input" id="toLng" placeholder="例: 139.691706">
                
                <label class="form-label">緯度</label>
                <input type="text" class="input" id="toLat" placeholder="例: 35.689487">
            </div>
            <div class="input-hint">
                <i class="fas fa-info-circle"></i> 例: 新宿駅 (経度: 139.691706, 緯度: 35.689487)
            </div>

            <!-- 探索タイプセクション -->
            <div class="form-section-title">
                <i class="fas fa-cog"></i> 探索タイプ
            </div>

            <div class="form-grid">
                <label class="form-label">探索タイプ</label>
                <select id="searchType" class="select">
                    <option value="1" selected>推奨(所要時間が短いルート)</option>
                    <option value="2">一般優先(有料道路を極力使用しない)</option>
                    <option value="3">道幅優先(道幅が広い道路を使用)</option>
                    <option value="4">距離優先(距離が短いルート)</option>
                    <option value="5">別ルート(推奨ルートの次に所要時間が短い)</option>
                </select>
            </div>

            <!-- アクションボタン -->
            <div class="actions">
                <button id="btnSearch" class="btn primary"><i class="fas fa-search"></i> ルート検索</button>
                <button id="btnClear" class="btn"><i class="fas fa-eraser"></i> クリア</button>
            </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 class="route-info" id="routeInfo">
                <div class="route-info-title">
                    <i class="fas fa-info-circle"></i> ルート情報
                </div>
                <div class="route-info-grid">
                    <div class="route-info-item">
                        <div class="route-info-label">距離</div>
                        <div class="route-info-value distance" id="routeDistance">-</div>
                    </div>
                    <div class="route-info-item">
                        <div class="route-info-label">所要時間</div>
                        <div class="route-info-value time" id="routeTime">-</div>
                    </div>
                    <div class="route-info-item">
                        <div class="route-info-label">現金料金</div>
                        <div class="route-info-value toll" id="routeToll">-</div>
                    </div>
                    <div class="route-info-item">
                        <div class="route-info-label">ETC料金</div>
                        <div class="route-info-value toll" id="routeTollEtc">-</div>
                    </div>
                </div>
            </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_search.js"></script>
</body>

</html>
CSS(クリックで展開)
zma_route_search.css
/* 基本スタイル */
* { box-sizing: border-box; }
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; height: 100vh; overflow: hidden; margin: 0; }
#ZMap { position: absolute; width: 100%; height: 100%; top: 0; left: 0; }

/* パネルスタイル */
.route-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; }
.panel-header p { font-size: 12px; opacity: .9; margin: 0; }
.panel-body { padding: 16px 20px 20px; overflow-y: auto; }

/* フォームスタイル */
.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; font-weight: 600; }
.input, .select { width: 100%; padding: 10px 12px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px; transition: all .2s; }
.input:focus, .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; transition: all .2s; }
.btn.primary { background: #667eea; color: #fff; }
.btn:hover { filter: brightness(0.97); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }

/* メッセージスタイル */
.error-message { display: none; background: #fee; color: #c33; padding: 10px 12px; border-radius: 8px; margin-bottom: 10px; font-size: 12px; }
.error-message.show { display: block; }

/* ローディングスタイル */
.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); } }

/* ルート情報スタイル */
.route-info { display: none; background: #f8f9fa; padding: 16px; border-radius: 8px; margin-top: 16px; }
.route-info.show { display: block; }
.route-info-title { font-size: 14px; font-weight: 600; margin-bottom: 12px; color: #333; border-bottom: 2px solid #e0e0e0; padding-bottom: 8px; }
.route-info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.route-info-item { background: #fff; padding: 10px; border-radius: 6px; border: 1px solid #e0e0e0; }
.route-info-label { font-size: 11px; color: #666; margin-bottom: 4px; }
.route-info-value { font-size: 16px; font-weight: 600; color: #333; }
.route-info-value.distance { color: #667eea; }
.route-info-value.time { color: #10b981; }
.route-info-value.toll { color: #f59e0b; }

/* セクションタイトル */
.form-section-title { font-size: 14px; font-weight: 600; color: #333; margin: 16px 0 12px; padding-bottom: 8px; border-bottom: 2px solid #e0e0e0; }
.form-section-title:first-child { margin-top: 0; }

/* 入力フィールドの説明 */
.input-hint { font-size: 11px; color: #666; margin-top: 4px; line-height: 1.4; }

/* 地図クリック設定 */
.click-mode-selector { display: flex; gap: 16px; margin-bottom: 8px; }
.radio-label { display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 13px; color: #555; }
.radio-label input[type="radio"] { cursor: pointer; }
.radio-label span { user-select: none; }

/* レスポンシブデザイン */
@media (max-width: 768px) { 
    .route-panel { width: calc(100% - 40px); } 
    .form-grid { grid-template-columns: 1fr; } 
    .route-info-grid { grid-template-columns: 1fr; }
}
zma_route_search.js
// マップ変数
let map;
let routePolyline = null;
let fromMarker = null;
let toMarker = null;

// DOM要素
const $ = (id) => document.getElementById(id);

// API設定
const API_ENDPOINT = 'https://test-web.zmaps-api.com/route/route_mbn/drive_ptp';
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.681236;
    const lng = 139.767125;

    // 地図オプションを設定
    mapOptions.center = new ZDC.LatLng(lat, lng);
    mapOptions.zoom = 14;
    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', function(event) {
                // イベント発生時の緯度経度を取得
                // event.latlng は ZDC.LatLng オブジェクト
                if (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;
                    }
                    
                    // 選択されているモードを取得
                    const clickMode = document.querySelector('input[name="clickMode"]:checked').value;
                    
                    // 選択された方のテキストボックスに入力
                    if (clickMode === 'from') {
                        $('fromLng').value = lng.toFixed(6);
                        $('fromLat').value = lat.toFixed(6);
                    } else {
                        $('toLng').value = lng.toFixed(6);
                        $('toLat').value = lat.toFixed(6);
                    }
                }
            });
        },
        function () {
            setError('地図の初期化に失敗しました');
        }
    );
});

// マーカーとルートをクリア
function clearRoute() {
    if (routePolyline && map) {
        map.removeWidget(routePolyline);
        routePolyline = null;
    }
    if (fromMarker && map) {
        map.removeWidget(fromMarker);
        fromMarker = null;
    }
    if (toMarker && map) {
        map.removeWidget(toMarker);
        toMarker = null;
    }
}

// ユーザーウィジェットでマーカーを追加(スタート/ゴール用)
function addMarker(lng, lat, title, isFrom) {
    if (!map) {
        console.warn('Map is not initialized');
        return;
    }
    
    // 数値に変換
    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)) {
        setError('座標情報が無効です');
        return null;
    }
    
    // 有効な範囲チェック
    if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180) {
        setError('座標が範囲外です');
        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>
        `;
        
        // ユーザーウィジェットを作成(マーカーの中心に表示されるようにオフセットを調整)
        // ウィジェットの幅は約120px、高さは約40pxを想定
        const widget = new ZDC.UserWidget(position, {
            htmlSource: htmlSource,
            offset: new ZDC.Point(-60, -20) // 中央に配置するためのオフセット(幅の半分、高さの半分)
        });
        
        map.addWidget(widget);
        return widget;
    } catch (error) {
        console.error('マーカーの追加に失敗:', error);
        setError('マーカーの表示に失敗しました');
        return null;
    }
}

// ルートを地図上に表示(ポリライン)
function displayRoute(routeData) {
    if (!map || !routeData) {
        console.error('displayRoute: map or routeData is missing');
        return;
    }

    // レスポンス構造を確認: result.item[0].route.link
    const route = routeData.route;
    if (!route || !route.link || !Array.isArray(route.link) || route.link.length === 0) {
        console.error('displayRoute: route.link is missing or empty', routeData);
        return;
    }

    clearRoute();

    // 全リンクの座標を結合
    const allCoordinates = [];
    
    route.link.forEach((link, linkIndex) => {
        if (link.line && link.line.coordinates && Array.isArray(link.line.coordinates)) {
            link.line.coordinates.forEach((coord, coordIndex) => {
                // GeoJSON形式は[経度, 緯度]なので、ZDC.LatLngは(緯度, 経度)に変換
                if (Array.isArray(coord) && coord.length >= 2) {
                    const lng = coord[0]; // 経度
                    const lat = coord[1]; // 緯度
                    allCoordinates.push(new ZDC.LatLng(lat, lng));
                }
            });
        } else {
            console.warn(`displayRoute: link[${linkIndex}] has no line.coordinates`);
        }
    });

    console.log(`displayRoute: Total coordinates: ${allCoordinates.length}`);

    if (allCoordinates.length === 0) {
        console.error('displayRoute: No coordinates found');
        return;
    }

    try {
        // ポリラインを作成(ルートを線で表示)
        // より目立つ色と太さに設定
        routePolyline = new ZDC.Polyline(allCoordinates, {
            color: '#0066ff', // 鮮やかな青色
            width: 10, // 太さを10pxに
            pattern: 'solid', // 実線
            opacity: 1.0 // 不透明度を最大に
        });

        map.addWidget(routePolyline);
        console.log('displayRoute: Polyline added successfully');

        // 地図の表示範囲を調整(ルート全体が表示されるように)
        const bounds = new ZDC.LatLngBounds();
        allCoordinates.forEach(coord => {
            bounds.extend(coord);
        });
        map.fitBounds(bounds);
    } catch (error) {
        console.error('displayRoute: Error creating polyline', error);
    }
}

// ローディング表示
function setLoading(show) {
    $('loading').classList.toggle('show', show);
    $('btnSearch').disabled = show;
}

// エラーメッセージ表示
function setError(message) {
    const box = $('errorMessage');
    $('errorMessageText').textContent = message || '';
    box.classList.toggle('show', Boolean(message));
}

// ルート情報を表示
function displayRouteInfo(routeData) {
    const routeInfo = $('routeInfo');
    if (!routeData || !routeData.route) {
        routeInfo.classList.remove('show');
        return;
    }

    const route = routeData.route;
    
    // 距離(kmに変換)
    const distanceKm = route.distance ? (route.distance / 1000).toFixed(1) : '-';
    $('routeDistance').textContent = distanceKm !== '-' ? `${distanceKm} km` : '-';
    
    // 時間(分を時間と分に変換、小数点を丸める)
    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}分`;
        }
    }
    $('routeTime').textContent = timeText;
    
    // 料金
    const toll = route.toll !== undefined ? route.toll.toLocaleString() : '-';
    $('routeToll').textContent = toll !== '-' ? ${toll}` : '-';
    
    // ETC料金
    const tollEtc = route.toll_etc !== undefined ? route.toll_etc.toLocaleString() : '-';
    $('routeTollEtc').textContent = tollEtc !== '-' ? ${tollEtc}` : '-';

    routeInfo.classList.add('show');
}

// ルート検索実行
async function searchRoute() {
    setError('');
    clearRoute();
    $('routeInfo').classList.remove('show');

    // 入力値の取得
    const fromLng = $('fromLng').value.trim();
    const fromLat = $('fromLat').value.trim();
    const toLng = $('toLng').value.trim();
    const toLat = $('toLat').value.trim();
    const searchType = $('searchType').value;

    // バリデーション
    if (!fromLng || !fromLat || !toLng || !toLat) {
        setError('出発地と目的地の緯度・経度を入力してください');
        return;
    }

    const fromLngNum = parseFloat(fromLng);
    const fromLatNum = parseFloat(fromLat);
    const toLngNum = parseFloat(toLng);
    const toLatNum = parseFloat(toLat);

    if (isNaN(fromLngNum) || isNaN(fromLatNum) || isNaN(toLngNum) || isNaN(toLatNum)) {
        setError('緯度・経度は数値で入力してください');
        return;
    }

    setLoading(true);
    try {
        // APIパラメータを構築
        const params = new URLSearchParams({
            search_type: searchType,
            from: `${fromLngNum},${fromLatNum}`,
            to: `${toLngNum},${toLatNum}`,
            llunit: 'dec',
            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 items = Array.isArray(json.result?.item) ? json.result.item : [];
        if (items.length === 0 || !items[0].status || !items[0].status) {
            throw new Error('ルートが見つかりませんでした');
        }

        const routeData = items[0];
        
        // デバッグ: レスポンス構造を確認
        console.log('Route data structure:', routeData);
        console.log('Route data.route:', routeData.route);
        console.log('Route data.route.link:', routeData.route?.link);
        
        // ルートを地図上に表示
        displayRoute(routeData);
        
        // 出発地・目的地にマーカーを表示
        fromMarker = addMarker(fromLngNum, fromLatNum, '出発地', true);
        toMarker = addMarker(toLngNum, toLatNum, '目的地', false);
        
        // ルート情報を表示
        displayRouteInfo(routeData);

    } catch (e) {
        setError(e.message);
        console.error('Route search error:', e);
    } finally {
        setLoading(false);
    }
}

// フォームをクリア
function clearForm() {
    $('fromLng').value = '';
    $('fromLat').value = '';
    $('toLng').value = '';
    $('toLat').value = '';
    $('searchType').value = '1';
    clearRoute();
    $('routeInfo').classList.remove('show');
    setError('');
}

// イベント設定
function wireEvents() {
    $('btnSearch').addEventListener('click', searchRoute);
    $('btnClear').addEventListener('click', clearForm);
    
    // Enterキーで検索
    ['fromLng', 'fromLat', 'toLng', 'toLat'].forEach(id => {
        $(id).addEventListener('keydown', (e) => {
            if (e.key === 'Enter') {
                e.preventDefault();
                searchRoute();
            }
        });
    });
}

// ページ読み込み時にイベントを設定
window.addEventListener('DOMContentLoaded', () => {
    wireEvents();
});

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

検索結果です。
route_result.png

ステップ毎の解説

Step 1:地図の初期化

ZMALoader.setOnLoad(function (mapOptions, error) {
    const lat = 35.681236;
    const lng = 139.767125;

    mapOptions.center = new ZDC.LatLng(lat, lng);
    mapOptions.zoom = 14;

    map = new ZDC.Map(
        document.getElementById('ZMap'),
        mapOptions,
        function () {
            map.addControl(new ZDC.ZoomButton('bottom-right', new ZDC.Point(-20, -35)));
            map.addControl(new ZDC.ScaleBar('bottom-left'));
        }
    );
});
  • ローダ読み込み後に地図を生成する処理
  • 中心座標(東京駅)とズームレベルを指定する初期化処理
  • ズームボタン・スケールバーを UI に追加する操作

ポイント

  • ZDC.Map() のコールバックは 地図描画完了後に呼ばれる
  • HTML の id="ZMap" がないと地図が表示されないため注意

Step 2:地図クリックで緯度経度を取得

map.addEventListener('click', function(event) {
    const lat = event.latlng.lat();
    const lng = event.latlng.lng();

    const clickMode = document.querySelector('input[name="clickMode"]:checked').value;
    if (clickMode === 'from') {
        $('fromLng').value = lng.toFixed(6);
        $('fromLat').value = lat.toFixed(6);
    } else {
        $('toLng').value = lng.toFixed(6);
        $('toLat').value = lat.toFixed(6);
    }
});
  • クリック位置の緯度経度を取得し、選択中の入力欄へ自動反映する処理
  • latlng.lat() / latlng.lng() による値取得
  • 出発地と目的地をラジオボタンで切り替える操作

Step 3:入力値のバリデーション

if (!fromLng || !fromLat || !toLng || !toLat) {
    setError('出発地と目的地の緯度・経度を入力してください');
    return;
}
if (isNaN(parseFloat(fromLng)) || isNaN(parseFloat(fromLat))) {
    setError('緯度・経度は数値で入力してください');
    return;
}
  • 未入力チェック
  • 数値チェック
  • 検索前に必要情報を揃えておくための検証処理

ポイント

  • 数値チェックは必ず parseFloat() を使用(文字列だと API でエラー)
  • バリデーションは API コール前に必ず行う

Step 4:ルート検索 API パラメータの生成

const params = new URLSearchParams({
    search_type: searchType,
    from: `${fromLngNum},${fromLatNum}`,
    to: `${toLngNum},${toLatNum}`,
    llunit: 'dec',
    datum: 'JGD'
});
  • drive_ptp に必要なパラメータを組み立てる処理
  • fromto を「経度,緯度」で渡す点に注意
  • 測地系 JGD と緯度経度単位 dec の指定

ポイント

  • from/to の順序は “経度 → 緯度”(逆にすると正常検索できない)

Step 5:ルート検索 API の実行

const res = await fetch(`${API_ENDPOINT}?${params}`, {
    headers: {
        'x-api-key': API_KEY,
        'Authorization': API_AUTH
    }
});
const json = await res.json();
const routeData = json.result.item[0];
  • fetch() によるルート検索 API の呼び出し
  • result.item[0] からルート情報を取得する処理
  • ステータスチェックによる異常系のハンドリング

Step 6:ルート座標の整形とポリライン描画

route.link.forEach(link => {
    link.line.coordinates.forEach(coord => {
        const lng = coord[0];
        const lat = coord[1];
        allCoordinates.push(new ZDC.LatLng(lat, lng));
    });
});

routePolyline = new ZDC.Polyline(allCoordinates, {
    color: '#0066ff',
    width: 10,
    pattern: 'solid'
});
map.addWidget(routePolyline);
  • GeoJSON形式 [経度, 緯度]ZDC.LatLng(緯度, 経度) に変換する処理
  • すべての link の座標を統合し一本のポリラインを生成する操作
  • 青色の太い線でルートを描画するスタイリング処理

ポイント

  • ZENRIN API の座標は 配列順が「経度 → 緯度」

Step 7:START / GOAL のユーザーウィジェット表示

const widget = new ZDC.UserWidget(position, {
    htmlSource: htmlSource,
    offset: new ZDC.Point(-60, -20)
});
map.addWidget(widget);
  • START/GOAL を強調表示するための独自ウィジェット生成処理
  • HTML/CSS を直接組み込んだラベルバッジの描画操作
  • 座標上の位置を調整するための offset 指定

Step 8:距離・時間・料金の表示

$('routeDistance').textContent = `${(route.distance / 1000).toFixed(1)} km`;
$('routeToll').textContent     = ${route.toll.toLocaleString()}`;
$('routeTollEtc').textContent  = ${route.toll_etc.toLocaleString()}`;
  • 距離(m → km へ変換)
  • 料金(現金 / ETC)
  • HTML 要素への反映処理

ポイント

  • 料金は toLocaleString()で桁区切りにすると見やすくなる

Step 9:リセット(クリア)処理

$('fromLng').value = '';
$('fromLat').value = '';
$('toLng').value = '';
$('toLat').value = '';
clearRoute();
  • 入力欄の初期化
  • ポリライン削除
  • START/GOAL のユーザーウィジェット削除
  • エラーと情報表示のリセット処理

おわりに

今回紹介したサンプルは、自動車ルート検索に必要な最小限の機能をまとめたものです。
出発地・目的地の指定からルートの描画、料金の取得といった基本的な動きを確認できますので、まずはシンプルに動作を理解したいときに役立てていただければ幸いです。

実際の開発では、今回の仕組みをもとに、現在位置の取得やマーカーアイコンの変更、条件に応じたルート比較など、さらに便利な機能を追加していくこともできます。
用途に応じてAPIパラメータを調整してみてください。

ZENRIN Maps API はWebアプリや業務システムに組み込みやすい設計となっていますので、ぜひ今回のサンプルを参考に、ルート検索を活用したさまざまな場面でご利用いただければ幸いです。

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?