GoogleMapsAPI と ZENRINMapsAPI の雨の日の屋根がある歩行者ルート検索を Python で比較してみた

# 要約

本記事では、Python と Flask を用いて、Google Maps API（Directions API）と ZENRIN Maps API（route_mbn/walk）による、雨の日でも濡れずに安全に移動できる歩行者ルート検索機能を比較しました。旧記事では JavaScript、HTML、CSS による実装を紹介しており、詳細は[こちら](https://qiita.com/orion8/items/dd8e5c862d352608f57f)からご覧いただけます。今回は出発地点を東京駅、目的地を東京タワーとしています。

# 1. プロジェクト概要

### 使用ライブラリ

#### Google Maps API 側

- **Flask**: 軽量な Web フレームワークとしてサーバー構築に使用
- **python-dotenv**: 環境変数（API キーなど）の管理
- **googlemaps**: Google の Directions API を利用するための公式 Python クライアントライブラリ
- **gmplot**: 取得したルート情報を地図上に描画するために利用

#### ZENRIN Maps API 側

- **Flask**: シンプルなサーバー構築に利用
- **requests**: HTTP リクエストを送信し、ZENRIN Maps API からのレスポンスを取得するために使用
- **python-dotenv** と **os**: 環境変数管理および API キーの取得に使用  
  （ZENRIN Maps API には公式の Python ライブラリが存在しないため、API からのレスポンス取得は Python の requests ライブラリを利用し、HTML と JavaScript によるレンダリングで実装しています）

# 2. API キーの取得

### 2.1 Google Maps API キーの取得方法

[参考サイト](https://qiita.com/Haruka-Ogawa/items/997401a2edcd20e61037 "Google APIキーの取得")

### 2.2 Zenrin Maps API キーの取得方法

#### 検証用 ID とパスワード（PW）取得

ZENRIN Maps API を使用するには、検証用 ID と PW を取得する必要があります。  
必要事項を入力して送信すると、お試し ID を簡単に発行でき（2 か月間無料で試用可能）、[こちらのフォーム](https://www.zenrin-datacom.net/solution/zenrin-maps-api/trial?fm_cp=6757baf3203e3a00bb118509&fm_mu=676395c70dbf8e057bd5f18d&utm_campaign=6757baf3203e3a00bb118509&utm_medium=else&utm_source=Qiita20 "Zenrin APIキーの取得フォーム")から申し込みます。  
  ![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 "Zenrin APIキーの取得")の手順に従い、コンソール内で API キーや認証方式を設定してください。

# 3. 各 API の特徴と実装の違い

### 3.1 Google Maps API（Directions API）

Google Maps API の Directions API は、出発地と目的地から最適なルートを計算します。Google が提供する公式の Python クライアントライブラリ（googlemaps）を用いることで API と簡単に連携でき、またサードパーティ製の gmplot を利用して地図上にルートやマーカーを描画することが可能です。ただし、API では「屋根がある歩行者ルート」などの特定条件（雨天時に濡れないルート）の指定はできません。  
[Directions API レファレンス](https://developers.google.com/maps/documentation/directions/overview?hl=ja)

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

ZENRIN Maps API は日本国内向けに高精度な徒歩ルート検索を提供します。今回の例では、出発地を東京駅、目的地を東京タワーとし、"search_type": 4 を指定することで、屋根がある道を優先するルートが検索されます。さらに、"step_count" と "calorie" のパラメータにより、歩数と消費カロリーの計算が可能です。レスポンスには "structure_type" も含まれており、これを利用して地図上で屋根のある区間とない区間を色分けすることができます。  
[歩行者ルート検索 2.0 Web API レファレンス](https://developers.zmaps-api.com/v20/reference/webAPI/route_mbn/walk.html "Zenrin API Web Api Reference")

# 4. 実装コードの紹介

### 4.1 Google Maps API（Directions API）

以下は、Google Maps API（Directions API）を用いて歩行者ルートを計算し、gmplot で地図に描画する Python コードの一例です。  
Python の gmplot ライブラリを使用して地図を作成する際、生成された HTML から JavaScript 部分を抽出し、gScript.js という外部ファイルとして保存しています。

#### ソースコード

<details>
<summary>gmap.py</summary>

```python
import googlemaps
import gmplot
from flask import Flask, render_template
from dotenv import load_dotenv
import os
import string

# 環境変数をロード
load_dotenv()

# Flaskアプリケーションを初期化
app = Flask(__name__)

# Google Maps APIキーを取得
API_KEY = os.getenv("GMAPS_API_KEY")
if not API_KEY:
    raise ValueError("環境にAPI_KEYが見つかりません。")

# Google Mapsクライアントを初期化
gmaps = googlemaps.Client(key=API_KEY)

# 出発地、目的地を定義
origin = {"lat": 35.68128312054809, "lng": 139.76683165211222}      # 東京駅
destination = {"lat": 35.658711231010265, "lng": 139.74543289660156}   # 東京タワー

def add_markers(gmap, origin, destination):
    # 出発地、目的地を1つのリストにまとめる
    opt_markers = [origin, destination]

    # リストをループしてラベル付きマーカーを追加
    for idx, location in enumerate(opt_markers):
        # 26文字以内はアルファベット、それ以降はWaypoint番号
        label = string.ascii_uppercase[idx] if idx < 26 else f'Waypoint {idx + 1}'
        # 出発地と目的地には特別なタイトルを設定
        title = (
            "Start: Tokyo Station" if idx == 0
            else "End: Tokyo Tower" if idx == len(opt_markers) - 1
            else f"Waypoint {label}"
        )
        color = 'cyan'
        gmap.marker(
            location["lat"], location["lng"],
            title=title,
            label=label,
            color=color
        )

# 最適化されたウェイポイントを使用してルートの指示をリクエスト
directions_result = gmaps.directions(
    origin,
    destination,
    mode="walking",
)

# ルートの各ポイントを抽出
route_points = []
for leg in directions_result[0]["legs"]:
    for step in leg["steps"]:
        polyline = step["polyline"]["points"]
        decoded_points = googlemaps.convert.decode_polyline(polyline)
        route_points.extend(decoded_points)

# 合計距離（km）と所要時間（分）を計算（今回は最初の脚の情報を利用）
distance = directions_result[0]["legs"][0]["distance"]["text"]
duration = directions_result[0]["legs"][0]["duration"]["text"]

# ルート全体の境界（bounds）から中心座標を計算
bounds = directions_result[0]["bounds"]
northeast = bounds["northeast"]
southwest = bounds["southwest"]
center_lat = (northeast["lat"] + southwest["lat"]) / 2
center_lng = (northeast["lng"] + southwest["lng"]) / 2

# gmplotを初期化（中心座標とズームレベル15を使用）
gmap = gmplot.GoogleMapPlotter(center_lat, center_lng, 15, apikey=API_KEY)

# ルート描画のため、ルートポイントから緯度・経度のリストを生成
lats = [point["lat"] for point in route_points]
lngs = [point["lng"] for point in route_points]

# ルートを描画（赤色、幅4、透明度0.8）
gmap.plot(lats, lngs, 'red', edge_width=4, alpha=0.8)

# 各地点にマーカーを追加
add_markers(gmap, origin, destination)

# HTMLコンテンツを生成
html_content = gmap.get()

# JavaScript部分を抽出
start_marker = '<script type="text/javascript">'
end_marker = '</script>'
start_index = html_content.find(start_marker) + len(start_marker)
end_index = html_content.find(end_marker, start_index)
javascript_content = html_content[start_index:end_index].strip()

# JavaScriptコンテンツをファイルに保存
js_filename = "static/gScript.js"
with open(js_filename, "w", encoding="utf-8") as js_file:
    js_file.write(javascript_content)

@app.route('/')
def home():
    return render_template(
        'gmap_index.html',
        api_key=API_KEY,
        total_distance=distance,
        total_duration=duration,
        js_file=js_filename  # JSファイルのパスを渡す
    )

if __name__ == "__main__":
    app.run(debug=True)

```

</details>

<details>
<summary>templates/gmap_index.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="static/gStyle.css" />
    <script src="{{ js_file }}"></script>
    <script
      src="https://maps.googleapis.com/maps/api/js?key={{ api_key }}&callback=initialize&v=weekly&language=ja"
      async
      defer
    ></script>
  </head>
  <body>
    <h1>Google Maps Directions API</h1>
    <div class="container-fluid">
      <div id="info">
        <div id="map_canvas"></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"
              >{{ total_duration }}</span
            >
          </li>
          <li
            class="list-group-item d-flex justify-content-between align-items-center"
          >
            距離
            <span class="badge text-bg-primary rounded-pill"
              >{{ total_distance }}</span
            >
          </li>
        </ul>
      </div>
    </div>
  </body>
</html>
```

</details>

#### 地図上ルート描画

![GRoute.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3981491/077c883b-d1fe-4de8-9637-21215b9193d6.png)

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

ZENRIN Maps API 用のコードは、Flask と python-dotenv を用いてシンプルに実装しています。  
ここでは、search_type パラメータ `"search_type": 4` を指定し、屋根がある道を優先してルート情報を取得しています。さらに、"step_count" と "calorie" のパラメータにより歩数と消費カロリーの計算も行え、レスポンスにはルート上の屋根有無情報（structure_type）が含まれるため、地図上で屋根のある区間とない区間を分けて表示することが可能です。

#### ソースコード

<details>
<summary>zmap.py</summary>

```python
from flask import Flask, render_template
import requests
from dotenv import load_dotenv
import os
import json

# 環境変数を読み込む
load_dotenv()

app = Flask(__name__)

# Zenrin Maps APIのエンドポイント

# 環境変数からAPIキーを取得
API_KEY = os.getenv('ZMAPS_API_KEY')
if not API_KEY:
    raise ValueError("環境にAPI_KEYが見つかりません。'.env'ファイルに設定してください。")

def getApiResponse():
    url = "https://test-web.zmaps-api.com/route/route_mbn/walk"

    # 出発地、目的地の設定
    origin = "139.76683165211222,35.68128312054809"  # 東京駅
    destination = "139.74543289660156,35.658711231010265"  # 東京タワー

    # APIリクエストのパラメータ
    params = {
        "search_type": 4,
        "from": origin,
        "to": destination,
        "priority": [8, 9],
        "calorie": "true",
        "step_count": "true",
    }

    print(API_KEY)
    # ヘッダーの設定
    headers = {
        "x-api-key": API_KEY,  # APIキー
        "Authorization": "ip"  # 認証ヘッダー
    }
    # APIリクエストの送信
    response = requests.get(url, params=params, headers=headers)

    # レスポンスのステータスコードを確認
    if response.status_code == 200:
        route_data = response.json()
        print("レスポンス取得完了")
        # レスポンスデータをファイルに保存
        file_path = os.path.join(os.path.dirname(__file__), 'static/walk_data.json')
        with open(file_path, 'w', encoding='utf-8') as f:
            json.dump(route_data, f, ensure_ascii=False, indent=4)
    else:
        print("ルート計算に失敗しました。")
        print(response.json)

getApiResponse()

@app.route('/')
def home():
    # テンプレートにAPIキーを渡してレンダリング
    return render_template('zmap_index.html', api_key=API_KEY)

if __name__ == "__main__":
    # アプリケーションをデバッグモードで実行
    app.run(debug=True)
```

</details>

<details>
<summary>templates/zmap_index.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="static/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://test-js.zmaps-api.com/zma_loader.js?key={{ api_key }}&auth=ip"></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="static/script.js"></script>
  </body>
</html>
```

</details>

#### 地図上ルート描画

![ZDCRoute.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3981491/52dbb939-1afd-4171-8b0a-1fbfad2a8c7b.png)

# 5. 両 API の比較と考察

| 機能            | 所要時間 | 距離   | ステップ | 消費カロリー |
| --------------- | -------- | ------ | -------- | ------------ |
| Google Maps API | 54 分    | 3.7 km | －       | －           |
| Zenrin Maps API | 53 分    | 4.1 km | 5893 歩  | 232 kcal     |

※ Google Maps API は基本的な歩行ルートのみを提供するのに対し、ZENRIN Maps API は屋根がある道優先で検索や、歩数・消費カロリーの計算、さらにルート上の屋根有無情報を提供する点で優れています。

# 6. 結論

今回の比較から、一般的な歩行者ルート検索においては Google Maps API が使いやすく、公式 Python ライブラリによる手軽な実装が魅力である一方、雨の日に濡れずに移動するための「屋根があるルート」を求める場合は、ZENRIN Maps API が優れた選択肢であることが明らかになりました。ZENRIN Maps API は、"search_type": 4 により屋根のある道を優先し、さらに歩数（step_count）や消費カロリー（calorie）の計算、そしてルート上の屋根有無情報（structure_type）の提供により、ユーザーに対してより詳細なルート情報を提供します。用途や目的に応じて、実装の簡便さとグローバルな利用を重視するなら Google Maps API を、雨天時の快適な移動をサポートするための付加情報が必要な場合は ZENRIN Maps API を選択するのが望ましいでしょう。