雨の日の屋根がある歩行者ルート検索を Google Maps API と ZENRIN Maps API で比較してみた

# 要約

本記事では、雨の日でも濡れずに安全に移動できる歩行者ルートの実現を目指し、Google Maps API（Directions API）と ZENRIN Maps API（route_mbn/walk）の実装例と特徴を徹底比較します。Google Maps API は、基本的な経路（距離・所要時間）を取得できるグローバル向けのサービスですが、雨天時の「屋根があるかどうか」といった細かな区間情報は提供していません。一方、ZENRIN Maps API は、日本国内向けに高精度な地図データを活用し、各区間の屋根の有無、構造情報、さらには歩数や消費カロリーなどの詳細データを返すため、雨の日の安全な歩行ルート検索に非常に有用です。

---

# 1. はじめに

雨の日の移動では、濡れるリスクを回避するために、屋根のあるルートを選択することが重要です。特に歩行者にとって、雨天時に屋根が連続しているルートは安全かつ快適な移動手段となります。そこで本記事では、以下の２つの API を用いて実際の実装例を比較し、それぞれのメリットや制約について解説します。

- **Google Maps API（Directions API）**  
  世界中で利用されている信頼性の高いルート検索サービス。
- **ZENRIN Maps API（route_mbn/walk）**  
  日本国内向けに特化し、雨天時の安全な歩行を支援するための詳細なルート情報を提供。

---

# 2. 各 API の概要

### 2.1 Google Maps API（Directions API）

- **基本機能**:  
  出発地点と目的地を指定し、歩行者をはじめ、車、自転車、公共交通機関など複数の移動モードに対応したルート検索が可能です。
- **取得できる情報**:  
  全体の所要時間、距離、各経路（legs）の詳細な情報を返します。
- **制約点**:  
  ルート内の各区間における「屋根の有無」や、構造情報などは提供されないため、雨天対策としては追加のカスタム処理が必要です。

#### Google MAPS API キーの取得方法

[参考サイト](https://qiita.com/Haruka-Ogawa/items/997401a2edcd20e61037)

### 2.2 ZENRIN Maps API（route_mbn/walk）

- **基本機能**:  
  日本国内向けに、歩行者ルート検索を高精度な地図データを用いて実施。
- **取得できる情報**:  
  ルート全体の所要時間・距離に加え、各リンクごとに `structure_type` や `street_type` などの詳細な構造情報を返します。
- **追加機能**:  
  屋根がある区間とない区間を、色分け（例：屋根ありは緑、屋根なしは赤）で視覚的に区別できるほか、歩数や消費カロリーなどの追加メトリクスも取得可能です。
- **メリット**:  
  日本国内の道路規制や一方通行、標高データなどを反映した、雨の日の安全なルート検索に最適な API です。

#### Zenrin MAPS API キーの取得方法

ZENRIN Maps API を使用するためには、検証用 ID と PW を取得しなければなりません。
必要事項を入力して送信し、お試し ID は下記からで簡単に発行できました。（2 か月無料でお試しできます）

[ZENRIN Maps API 無料お試し ID お申込みフォーム](https://www.zenrin-datacom.net/solution/zenrin-maps-api/trial?fm_cp=6757baf3203e3a00bb118509&fm_mu=6763950faffc4404b17d8578&utm_campaign=6757baf3203e3a00bb118509&utm_medium=else&utm_source=Qiita15)
  ![trialForm.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3981491/95e46565-c04a-4be1-967c-dc17b63ad9f8.png)

#### 検証用 ID と PW の確認

フォーム送信から 3 営業日以内にメールにて検証用 ID と PW が発行されます。
[参考サイト](https://qiita.com/goldie9/items/1983bf8c4ad796290cbf)でコンソール内の設定、API キーや認証方式を設定してください。

# 3. 実装例の詳細

以下に、各 API を利用した実装例の概要と主な処理内容を示します。

### 3.1 ZENRIN Maps API の実装例
![zRoute.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3981491/4e8c2e29-3994-4180-a38b-4c9243375bf8.png)
#### [参照サイト](https://orion8-dev.github.io/walking_route_js/zmap_index.html)

### ソースコード

<details>
<summary>HTML</summary>

```HTML
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>route_mbn/walk</title>
    <link rel="stylesheet" href="style.css">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
    <script src="	https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js
    "></script>
   <script src="https://{DOMAIN}/zma_loader.js?key=[APIキー]&auth=[認証方式]"></script>
</head>

<body>
    <h1>歩行者ルート検索2.0</h1>
    <div class="container-fluid">
        <div id="info">
            <div id="ZMap">
                <button type="button" class="btn btn-warning" id="liveToastBtn">ヒントを表示</button>
                <div class="toast-container">
                    <div id="liveToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
                        <div class="toast-header">
                            <strong class="me-auto">Tips</strong>
                            💡
                            <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
                        </div>
                        <div class="toast-body">
                            <p>
                                💡 Ctrl キーを押しながらマウスを上下にドラッグすると、地図を傾けることができます。<br><br>
                                💡 Ctrlキーを押しながらマウスを左右にドラッグすると地図を回転できます。
                            </p>
                        </div>
                    </div>
                </div>
            </div>

            <ul class="list-group">
                <li class="list-group-item d-flex justify-content-between align-items-right">
                    所要時間
                    <span class="badge text-bg-primary rounded-pill" id="time"></span>
                </li>
                <li class="list-group-item d-flex justify-content-between align-items-right">
                    距離
                    <span class="badge text-bg-primary rounded-pill" id="dist"></span>
                </li>
                <li class="list-group-item d-flex justify-content-between align-items-right">
                    ステップ
                    <span class="badge text-bg-primary rounded-pill" id="steps"></span>
                </li>
                <li class="list-group-item d-flex justify-content-between align-items-right">
                    消費カロリー
                    <span class="badge text-bg-primary rounded-pill" id="caloriBurn"></span>
                </li>
            </ul>
        </div>
    </div>

    <script src="script.js"></script>

</body>

</html>

```

</details>

ZENRIN のサンプルでは、Bootstrap を用いたシンプルなレイアウトで、地図表示エリアとルート情報（所要時間、距離、歩数、消費カロリー）を表示しています。  
また、ユーザー向けのヒントをトースト表示することで、地図の操作方法（傾ける・回転するなど）を案内しています。

<details>

<summary>Javascript</summary>

```Javascript
// オブジェクト
let map, origin, destination;
// 中心点の緯度経度（東京駅）
const lat = 35.669055759072684, lng = 139.75864681491296;

const toastTrigger = document.getElementById('liveToastBtn')
const toastLive = document.getElementById('liveToast')

if (toastTrigger) {
    const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toastLive)
    toastTrigger.addEventListener('click', () => {
        toastBootstrap.show()
    })
}

// マーカーを表示する関数
function showMarker(origin, destination) {
    const markerArr = [origin, destination];
    const pinStyles = [ZDC.MARKER_COLOR_ID_RED_L, ZDC.MARKER_COLOR_ID_GREEN_L]; // 出発地点と目的地のマーカースタイル

    markerArr.forEach((location, index) => {
        // マーカーを作成して地図に追加
        const marker = new ZDC.Marker(
            new ZDC.LatLng(location.lat, location.lng), // マーカーの位置
            { styleId: pinStyles[index] }              // マーカースタイル
        );
        map.addWidget(marker); // マーカーを地図に追加
    });
}

// ルート情報（所要時間と距離）を表示する関数
function showRouteInfo(rawDuration, rawDistance, stepCount, calorieBurn) {
    const stepInfoArea = document.getElementById('steps');
    const caloriInfoArea = document.getElementById('caloriBurn');
    stepInfoArea.textContent = `${stepCount} 歩`;
    caloriInfoArea.textContent = `${calorieBurn} kcal`;
    convertTime(rawDuration);
    convertDtn(rawDistance);
}


// 所要時間を適切な形式で表示する関数
function convertTime(rawDuration) {
    const timeInfoArea = document.getElementById('time');
    if (timeInfoArea) {
        const hours = Math.floor(rawDuration / 3600);
        const minutes = Math.floor((rawDuration % 3600) / 60);

        if (hours === 0 && minutes > 0) {
            timeInfoArea.textContent = `${minutes} 分`;
        } else if (hours > 0 && minutes === 0) {
            timeInfoArea.textContent = `${hours} 時間`;
        } else if (hours > 0 && minutes > 0) {
            timeInfoArea.textContent = `${hours} 時間${minutes} 分`;
        } else {
            timeInfoArea.textContent = 'すぐに到着します';
        }
    } else {
        console.error("所要時間の表示エリアが見つかりません。");
    }
}

// 距離をキロメートル単位で表示する関数
function convertDtn(rawDistance) {
    const distInfoArea = document.getElementById('dist');
    if (rawDistance && distInfoArea) {
        const distanceInKm = (rawDistance / 1000).toFixed(1);
        distInfoArea.textContent = `${distanceInKm} km`;
    } else {
        console.error("距離の表示エリアが見つかりません。");
    }
}


function calculatePolylineBounds(polylineCoordinates) {
    if (!polylineCoordinates || polylineCoordinates.length === 0) {
        return null; // Handle empty or invalid polyline
    }

    let minLat = Number.POSITIVE_INFINITY;
    let maxLat = Number.NEGATIVE_INFINITY;
    let minLng = Number.POSITIVE_INFINITY;
    let maxLng = Number.NEGATIVE_INFINITY;

    // Iterate over each point in the polyline to determine bounds
    polylineCoordinates.forEach(point => {
        if (point.lat < minLat) minLat = point.lat;
        if (point.lat > maxLat) maxLat = point.lat;
        if (point.lng < minLng) minLng = point.lng;
        if (point.lng > maxLng) maxLng = point.lng;
    });

    // Construct ZDC.LatLng objects for southwest and northeast corners
    const southWest = new ZDC.LatLng(minLat, minLng);
    const northEast = new ZDC.LatLng(maxLat, maxLng);

    const bounds = new ZDC.LatLngBounds(southWest, northEast);

    return bounds;
}

// ルート検索を実行する関数
function performRouteSearch(origin, destination) {
    const startPoint = `${origin.lng},${origin.lat}`;
    const goalPoint = `${destination.lng},${destination.lat}`;
    const api = "/route/route_mbn/walk";
    const params = {
        search_type: 4,
        from: startPoint,
        to: goalPoint,
        calorie: true,
        step_count: true,
    };

    try {
        map.requestAPI(api, params, function (response) {
            if (response.ret && response.ret.status === 'OK') {
                const route = response.ret.message.result.item[0].route;
                const rawDistance = route.distance;
                const rawDuration = route.time;
                const stepCount = route.step_count;
                const calorieBurn = route.calorie;
                showRouteInfo(rawDuration, rawDistance, stepCount, calorieBurn);

                let allCoordinates = [];
                // Process each section and link
                route.section.forEach(section => {
                    section.link.forEach(link => {
                        // Check if link has roof structure (structure_type 1)
                        const hasRoof = link.structure_type && link.structure_type.includes(1);
                        const color = hasRoof ? '#00dc00' : '#ff0000'; // Green or Red

                        // Convert GeoJSON [lng, lat] to LatLng objects
                        const linkCoords = link.line.coordinates.map(coord =>
                            new ZDC.LatLng(coord[1], coord[0])
                        );

                        // Collect all coordinates for bounds calculation
                        allCoordinates = allCoordinates.concat(linkCoords);

                        // Create polyline segment for this link
                        const polyline = new ZDC.Polyline(linkCoords, {
                            color: color,
                            width: 6,
                            pattern: 'solid',
                            opacity: 1
                        });
                        map.addWidget(polyline);
                    });
                });

                // Adjust map view to show entire route
                const bounds = calculatePolylineBounds(allCoordinates);
                if (bounds) {
                    const adjustZoom = map.getAdjustZoom(allCoordinates, { fix: false });
                    map.setCenter(adjustZoom.center);
                    map.setZoom(adjustZoom.zoom - 0.3);
                }

                showMarker(origin, destination);
            } else {
                console.error("ルート検索失敗");
            }
        });
    } catch (error) {
        console.error("ルート検索中にエラーが発生しました:", error);
    }
}

// ZMALoaderを初期化
ZMALoader.setOnLoad(function (mapOptions, error) {
    if (error) {
        console.error(error);
        return;
    }
    mapOptions.mouseWheelReverseZoom = true;
    mapOptions.centerZoom = false;
    mapOptions.center = new ZDC.LatLng(lat, lng); // 中心点の緯度経度を設定
    mapOptions.rotatable = true;
    mapOptions.tiltable = true;
    mapOptions.zipsResolutionWmts = 'high';

    // 地図を生成
    map = new ZDC.Map(
        document.getElementById('ZMap'),
        mapOptions,
        function () {
            const origin = new ZDC.LatLng(35.681406, 139.767132); // 東京駅
            const destination = new ZDC.LatLng(35.66671917430511, 139.75830306946293); // 新橋駅

            performRouteSearch(origin, destination); // ルート検索を実行

        },
        function () {
            console.log("APIエラー");
        }
    );
});


```

</details>

- **地図の初期化とルート検索**:  
  `ZMALoader` を利用して地図を初期化し、東京駅付近を中心としたマップを生成。  
  `performRouteSearch` 関数により、出発地点（例：東京駅）と目的地（例：新橋駅）を指定して、`/route/route_mbn/walk` エンドポイントからルート情報を取得。
- **ルート描画と詳細情報の表示**:  
  取得したレスポンスから、各セクションごとに `structure_type` をチェックし、屋根があるリンクは緑、屋根がないリンクは赤でポリラインを描画。  
  また、全体の所要時間、距離、歩数、消費カロリーを各専用表示エリアに出力します。

### 3.2 Google Maps API の実装例

![gRoute.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3981491/918197ae-de6e-4917-95e6-5887aff55083.png)

### ソースコード

<details>
<summary>HTML</summary>

```HTML
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Google Maps Route Optimization</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
    <link rel="stylesheet" href="style.css">
    <script
        src="https://maps.googleapis.com/maps/api/js?key=[API_KEY]&callback=initMap&libraries=&v=weekly&language=ja"
        defer>
        </script>

</head>

<body>
    <h1>Google Maps Directions API</h1>
    <div class="container-fluid">
        <div id="info">
            <div id="map"> </div>
            <ul class="list-group">
                <li class="list-group-item d-flex justify-content-between align-items-center">
                    所要時間
                    <span class="badge text-bg-primary rounded-pill" id="time"></span>
                </li>
                <li class="list-group-item d-flex justify-content-between align-items-center">
                    距離
                    <span class="badge text-bg-primary rounded-pill" id="dist"></span>
                </li>
            </ul>
        </div>
    </div>

    <script src="script.js"></script>
</body>
</html>

```

</details>

Google Maps のサンプルは、Bootstrap を利用して地図表示エリアとルート情報（所要時間、距離）を配置。  
API キーを利用してスクリプトを読み込み、グローバルに利用できるシンプルなレイアウトです。

<details>

<summary>Javascript</summary>

```Javascript
let map;

/**
 * 総距離と総時間を表示する関数
 * @param {number} totalDistance - 総距離（メートル単位）
 * @param {number} totalDuration - 総時間（秒単位）
 */
function displayRouteInfo(totalDistance, totalDuration) {
    const timeInfoArea = document.getElementById('time');
    const distInfoArea = document.getElementById('dist');

    if (timeInfoArea) {
        timeInfoArea.textContent = formatTime(totalDuration);
    } else {
        console.error("所要時間の情報は取得できません。");
    }

    if (distInfoArea) {
        distInfoArea.textContent = formatDistance(totalDistance);
    } else {
        console.error("距離の情報は取得できません。");
    }
}

/**
 * 時間をフォーマットする関数
 * @param {number} duration - 時間（秒単位）
 * @returns {string} フォーマットされた時間文字列
 */
function formatTime(duration) {
    const hours = Math.floor(duration / 3600);
    const minutes = Math.floor((duration % 3600) / 60);

    if (hours === 0 && minutes > 0) {
        return `${minutes}分`;
    } else if (hours > 0 && minutes === 0) {
        return `${hours}時間`;
    } else if (hours > 0 && minutes > 0) {
        return `${hours}時間${minutes}分`;
    } else {
        return 'すぐに到達!!';
    }
}

/**
 * 距離をフォーマットする関数
 * @param {number} distance - 距離（メートル単位）
 * @returns {string} フォーマットされた距離文字列
 */
function formatDistance(distance) {
    const km = (distance / 1000).toFixed(1);
    return `${km} km`;
}

/**
 * マップを初期化し、ルートを表示する関数
 */
async function initMap() {
    // 出発地、目的地、経由地を定義
    const origin = { lat: 35.681406, lng: 139.767132 }; // 東京駅
    const destination = { lat: 35.66671917430511, lng: 139.75830306946293 }; // 新橋駅

    // マップを東京中心に初期化
    map = new google.maps.Map(document.getElementById("map"), {
        center: origin,
        zoom: 10,
    });

    // DirectionsService と DirectionsRenderer を設定
    const directionsService = new google.maps.DirectionsService();
    const directionsRenderer = new google.maps.DirectionsRenderer({
        map: map,
        polylineOptions: {
            strokeColor: "red", // ルートの色を緑に設定
            strokeWeight: 6, // 線の太さ
            strokeOpacity: 0.8, // 線の不透明度
        },
    });

    // 最適化された経由地でルートをリクエスト
    directionsService.route(
        {
            origin: origin,
            destination: destination,
            travelMode: google.maps.TravelMode.WALKING,
            provideRouteAlternatives: true,
        },
        (response, status) => {
            if (status === google.maps.DirectionsStatus.OK) {
                directionsRenderer.setDirections(response);
                console.log(response);
                const routeLegs = response.routes[0].legs;
                const totalDistance = routeLegs.reduce((sum, leg) => sum + leg.distance.value, 0);
                const totalDuration = routeLegs.reduce((sum, leg) => sum + leg.duration.value, 0);
                displayRouteInfo(totalDistance, totalDuration);
            } else {
                // エラーハンドリング
                console.error("ルートリクエストに失敗しました: " + status);
                document.getElementById("route-info").textContent =
                    "ルートを計算できません。再試行してください。";
            }
        }
    );
}


```

</details>

- **地図の初期化とルート検索**:  
  `initMap` 関数により、地図の中心を東京駅に設定し、`google.maps.Map` オブジェクトを生成。  
  `google.maps.DirectionsService` と `DirectionsRenderer` を利用して、出発地点（東京駅）と目的地（新橋駅）間の歩行者ルートをリクエスト。
- **ルート描画と情報表示**:  
  レスポンスから各経路の距離と所要時間を合計し、専用関数 `displayRouteInfo` にてフォーマットして表示。  
  ルート自体は DirectionsRenderer により自動描画され、全体のポリラインは一色（例：赤）で表示されます。
- **制約点**:  
  各区間の詳細な構造情報（屋根の有無など）は提供されないため、雨天時の安全なルート検索においては、追加実装が必要となります。

---

# 4. 機能比較と考察

以下の表は、両 API の主要な機能と特徴を比較したものです。

| 機能                               | Google Maps API   | ZENRIN Maps API                     |
| ---------------------------------- | ----------------- | ----------------------------------- |
| **グローバル対応**                 | ✅ はい           | ❌ 日本限定                         |
| **基本的な経路情報**               | ✅ 距離・所要時間 | ✅ 距離・所要時間                   |
| **屋根情報**                       | ❌ なし           | ✅ 各区間の屋根有無情報             |
| **追加情報（歩数・消費カロリー）** | ❌ なし           | ✅ 詳細なメトリクス                 |
| **詳細な構造情報**                 | ❌ なし           | ✅ structure_type、street_type など |
| **視覚的な色分け表示**             | ❌ 自前実装が必要 | ✅ 屋根の有無に応じた色分け可能     |

この比較から、雨の日の安全な歩行ルート検索という観点では、ZENRIN Maps API が提供する詳細な区間情報と追加メトリクス（歩数・消費カロリーなど）が大きな強みであることが分かります。逆に、Google Maps API はグローバルな対応や汎用性に優れるものの、雨天対策としては補完的な処理が必要となります。

---

# 5. 利用シーンとおすすめポイント

### Google Maps API を選ぶ場合

- **グローバルな展開**: 国際的なプロジェクトや、複数国でのサービス提供を考えている場合に最適です。
- **基本的なルート情報**: 迅速なルート検索と、複数の移動手段のサポートが求められるシーンに向いています。
- **カスタマイズ可能**: 雨天対策のために、別途自前で屋根情報の補完などを実装することも可能です。

### ZENRIN Maps API を選ぶ場合

- **日本国内向けの高精度な地図情報**: 日本特有の道路事情や交通規制を反映した、精度の高いルート検索が可能です。
- **雨天対策に特化**: 各区間の屋根の有無を判断できるため、雨の日に安心して歩けるルートを提供できます。
- **追加の健康情報**: 歩数や消費カロリーの情報も取得でき、ユーザーにとって実用的な情報が充実しています。

---

# 6. 結論

雨の日に歩行者が濡れることなく安全に目的地へ到達するためには、単に最短経路を検索するだけではなく、ルート内の屋根の有無や各区間の詳細な条件を把握することが重要です。

- **Google Maps API** は、グローバルな対応と基本的な経路情報の提供に優れていますが、雨天時の細かな条件（屋根の有無など）を取得するためには追加実装が必要です。
- **ZENRIN Maps API** は、日本国内向けの高精度な地図データを活用し、雨の日でも安心して歩けるルートの実現に向けた、詳細な構造情報や健康関連のメトリクスを提供します。

開発者は、プロジェクトの対象地域や求める機能に応じて、これらの API を適切に選択することで、より安全で快適な移動体験をユーザーに提供できるでしょう。

---

# 7. 参考リンク

※なお、Google Maps APIは、私自身が公式リファレンスを調査した限りでは、基本的な歩行者ルート情報のみを提供しており、屋根の有無などの詳細なデータは含まれていないです。もし、これに関して新たな情報やご指摘がございましたら、ぜひコメント欄にてお知らせいただけると幸いです。

- **Google Maps API**:  
  [公式サイト](https://developers.google.com/maps/documentation/directions)
- **ZENRIN Maps API**:  
  [公式サイト](https://developers.zmaps-api.com/v20/reference/webAPI/route_mbn/walk.html)