ZENRIN Maps API の規制時間帯の自動車ルート検索と Google Maps API の違い

# 要約
本記事では、道路の交通規制に対応したルート検索機能について、Google Maps API と ZENRIN Maps API（route_mbn/drive_ptp）の比較を行います。特に、時間帯により規制が変わる神楽坂通りの交通規制を例に、両 API の挙動の違いと実用性について解説します。

# 背景

特定の道路において、時間帯により通行可能な車両や進入可能な方向が変わる交通規制が実施されています。  
例えば、東京都内の神楽坂通りでは、毎週日曜日や祝日の 12:00 ～ 19:00 において歩行者専用となり、車両（自動車・自転車問わず）は通行できません。  
これらの交通規制は、警視庁牛込警察署が公開している交通規制情報サイト（[交通規制の詳細](https://www.keishicho.metro.tokyo.lg.jp/about_mpd/shokai/ichiran/kankatsu/ushigome/about_ps/kotsu_kisei.html)）で確認できます。

# 比較対象

今回の比較対象は、以下の 2 つの API です。

- **Google Maps API (Route API)**  
  グローバルに広く利用されているルート検索機能を提供しますが、地域固有の詳細な交通規制情報が十分に反映されない場合があります。

- **ZENRIN Maps API (route_mbn/drive_ptp)**  
  日本国内の詳細な地図情報と交通規制に基づいたルート検索が可能で、特に時間帯ごとの交通制限や細かな道路ルールを正確に反映する点で優れています。

# 実装例

## Google Maps API の実装例

Google Maps API のデモサイト（[Google Maps Routes Demo](https://developers.google.com/maps/documentation/routes/demo?_gl=1*tehqc0*_up*MQ..*_ga*MTI4MzgwODUzMC4xNzQxNzk0NDIy*_ga_NRWSTWS78N*MTc0MTc5NDQyMi4xLjEuMTc0MTc5NDUwMi4wLjAuMA..)）では、以下の手順でルート検索のテストを実施しました。

1. **入力フォームの設定**

   - フォーム内のトグルボタンをクリックし、「Address as Waypoint」と「Location/Place_ID as Waypoint」を切り替え、Origin と Destination の座標を入力しました。
   - **Origin 緯度経度**：35.703874744612726, 139.73442305859913（神楽坂駅）
   - **Destination 緯度経度**：35.700504770928035, 139.74237209059712（モスバーガー神楽坂下店）

2. **ルートオプションの設定**

   - 「Travel Mode」を「Drive」に設定。
   - 「Departure Time (Your local time)」に出発時刻を入力。
   - 「Route Options」で「Traffic Awareness」を「Traffic aware」に設定。

![gForm.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3981491/e61c7b3c-caff-4e5b-9f26-01d975455f74.png)

3. **デモ実行**

   - 「Get directions」ボタンをクリックすると、地図上にルートが描画されました。
   - しかし、毎週日曜日や祝日の 12:00 ～ 19:00 における神楽坂通りの歩行者専用規制を無視して車両用ルートを返してしまい、実際の交通規制が反映されていないことが確認されました。

![gmap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3981491/3ea87104-e670-4c91-b69c-ef2a66d11fd0.png)

## ZENRIN Maps API の実装例

一方、ZENRIN Maps API を用いた実装例では、以下の HTML と JavaScript を利用して、交通規制に基づいたルート検索を実現しました。

ZENRIN Maps API を利用するためには、検証用 ID とパスワードを取得する必要があります。必要事項を入力して送信すると、下記のフォームから簡単にお試し ID を発行できます（2 か月無料でお試しいただけます）。  
🆓[ZENRIN Maps API 無料お試し ID お申込みフォーム](https://www.zenrin-datacom.net/solution/zenrin-maps-api/trial?fm_cp=6757baf3203e3a00bb118509&fm_mu=67639617bdef2d04981aeb41&utm_campaign=6757baf3203e3a00bb118509&utm_medium=else&utm_source=Qiita22)

![trialForm.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3981491/95e46565-c04a-4be1-967c-dc17b63ad9f8.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>Zenrin Maps API</title>
    <link rel="stylesheet" href="style.css" />
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
      rel="stylesheet"
    />
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css"
    />
    <script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
    <script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/ja.js"></script>
    <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>規制時間帯の自動車ルート</h1>
    <div class="container mt-4">
      <div class="form-floating mb-3">
        <input
          type="text"
          class="form-control"
          id="datetime"
          placeholder="日付と時間を選択"
        />
        <label for="floatingInput">日付と時間を選択</label>
      </div>
      <div class="d-grid gap-2">
        <button id="searchButton" class="btn btn-outline-secondary">
          ルート検索
        </button>
      </div>
    </div>
    <div id="info">
      <div id="ZMap"></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>
    <script src="script.js"></script>
  </body>
</html>
```

</details>

<details>
<summary>JavaScript (script.js)</summary>

```javascript
// マップオブジェクトと中心座標の設定
let map, polyline;
let formattedDepartureTime = "";

// Initialize Flatpickr with Japanese locale
flatpickr("#datetime", {
  enableTime: true,
  enableSeconds: true, // Enable seconds selection
  altInput: true,
  altFormat: "Y年m月d日 H:i:S", // Human-readable format including seconds
  dateFormat: "YmdHis", // API-ready format including seconds
  locale: "ja",
  onChange: function (selectedDates, dateStr) {
    formattedDepartureTime = dateStr;
    console.log("Formatted departure time: " + formattedDepartureTime);
  },
});

// ルート情報（所要時間と距離）を表示する関数
function showRouteInfo(rawDuration, rawDistance) {
  convertTime(rawDuration);
  convertDtn(rawDistance);
}

// 経由地にマーカーを表示する関数
function showMarker(waypoints) {
  waypoints.forEach((location, index) => {
    const marker = new ZDC.Marker(new ZDC.LatLng(location.lat, location.lng), {
      styleId: ZDC.MARKER_COLOR_ID_RED_L,
      contentStyleId: ZDC[`MARKER_NUMBER_ID_${index + 1}_L`],
    });
    map.addWidget(marker);
  });
}

// 経由地の文字列を解析し、最適化された順序で配列を返す関数
function parseWaypoints(waypointString, origin, destination, routeorder) {
  const coordinates = waypointString.split(",").map(Number);
  const waypoints = [origin];
  for (let i = 0; i < coordinates.length; i += 2) {
    const lng = coordinates[i];
    const lat = coordinates[i + 1];
    waypoints.push(new ZDC.LatLng(lat, lng));
  }
  const optWaypts = waypointOpt(waypoints, routeorder);
  optWaypts.push(destination);
  return optWaypts;
}

// 経由地の順序を最適化する関数
function waypointOpt(waypoints, routeorder) {
  let stringArray = routeorder.split(",");
  let integerArray = stringArray.map((num) => parseInt(num, 10));
  integerArray = [0, ...integerArray];
  const optWaypts = [];
  for (let i = 0; i < integerArray.length; i++) {
    optWaypts.push(waypoints[integerArray[i]]);
  }
  return optWaypts;
}

// 所要時間を適切な形式で表示する関数
function convertTime(rawDuration) {
  const timeInfoArea = document.getElementById("time");
  if (timeInfoArea) {
    const hours = Math.floor(rawDuration / 60);
    const minutes = (rawDuration % 60).toFixed(0);
    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("距離の表示エリアが見つかりません。");
  }
}

// departure_time のフォーマットを整える関数
function getFormattedDepartureTime() {
  if (formattedDepartureTime) {
    // もし formattedDepartureTime が 13 文字以下なら、末尾に '0' を追加して 14 文字にする
    if (formattedDepartureTime.length <= 13) {
      return formattedDepartureTime.padEnd(14, "0");
    }
    return formattedDepartureTime;
  } else {
    // ユーザーが日付を選択していない場合、現在時刻を 14 文字に整形して返す
    return new Date()
      .toISOString()
      .replace(/[-:TZ.]/g, "")
      .slice(0, 14);
  }
}

// ルート検索を実行する関数
function performRouteSearch(origin, destination) {
  if (polyline) {
    map.removeWidget(polyline);
  }
  const startPoint = `${origin.lng},${origin.lat}`;
  const goalPoint = `${destination.lng},${destination.lat}`;
  const api = "/route/route_mbn/drive_ptp";
  // ユーザーが選択した出発時刻があれば使用、なければ現在時刻を使用
  const departureTime = getFormattedDepartureTime();
  const params = {
    search_type: 1,
    from: startPoint,
    to: goalPoint,
    time_restriction: true,
    departure_time: departureTime,
  };

  try {
    map.requestAPI(api, params, function (response) {
      if (response.ret && response.ret.status === "OK") {
        console.log(response);
        const route = response.ret.message.result.item[0].route;
        const coordinates = route.link.flatMap((link) =>
          link.line.coordinates.map(
            (coord) => new ZDC.LatLng(coord[1], coord[0])
          )
        );
        const bounds = calculatePolylineBounds(coordinates);
        if (bounds) {
          const adjustZoom = map.getAdjustZoom(coordinates, { fix: false });
          map.setCenter(adjustZoom.center);
          map.setZoom(adjustZoom.zoom - 0.5);
        }
        const rawDuration = route.time;
        const rawDistance = route.distance;
        const waypoints = [origin, destination];
        showMarker(waypoints);
        showRouteInfo(rawDuration, rawDistance);
        polyline = new ZDC.Polyline(coordinates, {
          color: "green",
          width: 4,
          pattern: "solid",
          opacity: 0.7,
        });
        map.addWidget(polyline);
      } else {
        console.error("ルート検索に失敗しました。");
      }
    });
  } catch (error) {
    console.error("ルート検索中にエラーが発生しました:", error);
  }
}

// Function to calculate the bounds of a polyline
function calculatePolylineBounds(polylineCoordinates) {
  if (!polylineCoordinates || polylineCoordinates.length === 0) {
    console.log("ポリラインの座標が無効です。");
  }
  let minLat = Number.POSITIVE_INFINITY;
  let maxLat = Number.NEGATIVE_INFINITY;
  let minLng = Number.POSITIVE_INFINITY;
  let maxLng = Number.NEGATIVE_INFINITY;
  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;
  });
  const southWest = new ZDC.LatLng(minLat, minLng);
  const northEast = new ZDC.LatLng(maxLat, maxLng);
  return new ZDC.LatLngBounds(southWest, northEast);
}

// ZMALoader の初期化
ZMALoader.setOnLoad(function (mapOptions, error) {
  if (error) {
    console.error(error);
    return;
  }
  mapOptions.zoom = 13;
  mapOptions.centerZoom = false; // 地図の中心点を中心に拡大縮小する指定
  mapOptions.mouseWheelReverseZoom = true;
  mapOptions.minZoom = 4.5;
  map = new ZDC.Map(
    document.getElementById("ZMap"),
    mapOptions,
    function () {
      map.addControl(new ZDC.ZoomButton("bottom-left"));
      map.addControl(new ZDC.Compass("top-right"));
      map.addControl(new ZDC.ScaleBar("bottom-left"));
      // ルート検索はボタン押下で実行
      document
        .getElementById("searchButton")
        .addEventListener("click", function () {
          // 出発点：神楽坂駅、目的地：モスバーガー神楽坂下店
          const origin = new ZDC.LatLng(35.703874744612726, 139.73442305859913);
          const destination = new ZDC.LatLng(
            35.700504770928035,
            139.74237209059712
          );
          performRouteSearch(origin, destination);
        });
    },
    function () {
      console.log("APIエラー");
    }
  );
});
```

</details>

![zmap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3981491/c36a10bf-c365-434a-affa-1908d5cb1b76.png)

### [参照サイト](https://orion8-dev.github.io/time_restriction_route/zmap_index.html)

# 実装結果

上記のコードを用いて、同じ日時条件（例：日曜日の 15:00）でルート検索を実行すると、

- **ZENRIN Maps API** は、神楽坂通りにおける交通規制（12:00 ～ 19:00 は車両進入禁止、歩行者専用）を正確に反映し、車両が進入できないルートを自動的に避けた経路を返します。  
  この結果は、詳細な交通規制情報が ZENRIN Maps API に含まれている証拠であり、都市部から地方まで幅広い地域の交通ルールが正確に反映されていることを示しています

# 結論

実装例から明らかなように、**ZENRIN Maps API** は日本国内の交通規制や道路ルールに対する理解が深く、時間帯ごとの交通制限を正確に反映したルート検索が可能です。  
一方、**Google Maps API** はグローバルなルート検索において高い信頼性を誇るものの、地域固有の詳細な交通規制を十分に反映できない場合があることが確認されました。

そのため、神楽坂通りのような複雑な交通規制が存在するエリアでルート検索を行う際には、**ZENRIN Maps API** の利用がより適切であると結論付けられます。