Maps JavaScript APIのDirections APIに関してエラーハンドリングしてみた

# はじめに

今回JavaScriptのAdventCalenderに投稿させて頂くんですけど、どちらかと言えばライブラリ系の話になってしまいました...ごめんなさい...
とりあえず頑張って書くのでよろしくお願いします！

## 概要

Google Map APIに関して業務で触ることがあって、色々触っているときに感じたことを書きます。
リクエストを送る方法は色々あるんですけど、今回はその中でMaps JavaScript APIを利用した
方法を書きます。またその中でも、Directions（経路検索）APIにフォーカスして、リクエストを送るところから、レスポンスパラメータのエラーハンドリングもやろうと思います。

## 事前準備

事前の準備として、Google Cloud Platformに登録をしてAPIキーを取得してください。
そして、HTML内に記述してGoogle Maps APIを読み込んでください。
下記の記事が参考になります。

https://qiita.com/Haruka-Ogawa/items/997401a2edcd20e61037

# リクエスト/レスポンス

## リクエスト

DirectionsServiceオブジェクトを生成して、そこにDirectionsRequestパラメータをセットしてリクエストを送ります。詳細は後ほど。

### DirectionsRequestパラメータ（今回使用する一部を抜粋）

```json
{
  origin: LatLng | String | google.maps.Place,
  destination: LatLng | String | google.maps.Place,
  travelMode: TravelMode,
  waypoints[]: DirectionsWaypoint
}
```

### パラメータの詳細（今回使用する一部を抜粋）

| フィールド       |       説明　　 |
|:-----------------|:------------------|
|origin（必須）|出発地。基本的にLatLngオブジェクト。|
|destination（必須）|目的地。基本的にLatLngオブジェクト。|
|travelMode（必須）|交通手段。以下の中から選択。<br> ・DRIVING（車）デフォルト<br>・BICYCLING（自転車）<br> ・TRANSIT（公共交通機関）<br>・WALKING（歩行）|
|waypoints[]|経由地配列。経由地オブジェクトの中身は<br>・location（経由地の位置情報）<br>・stopover（途中降機フラグ。デフォルトはtrue）|

この中でstopoverがわかりにくいと思うんですけど、

- 地点Aに寄り道して行きたい→true
- 地点Aを通る道で行きたい→false

みたいな感じで使い分ければ大丈夫です。
今回はstopoverをfalseにして送る予定なので、ソースコード内で整形します。

## レスポンス

### responseパラメータ
MapsJavascriptAPIを使用したリクエストのresponseパラメータは２つあります。
それぞれコールバック関数にて基本的に指定します。

| フィールド       |       説明　　 |
|:-----------------|:------------------|
|第１引数（DirectionResult）|検索結果|
|第２引数（DirectionStatus）|ステータス。ここで検索がうまくいったか判別する。|

### resultの中身

検索結果の中身は複雑になっています。
基本的には以下のような感じに思っていただければ大丈夫です。

```json

{
  request: {} // リクエストパラメータ
  routes: [
    {
      legs: [
        {
          steps: [
            {
              path: LatLng[], // 検索経路
              distance: {
                text: String, // 移動距離文字列 例) "0.2 km"
                value: Number // 移動距離の値(メートル単位) 例) 244
              },
              duration: {
                text: String, // 経過時間文字列 例) "1分"
                value: Number // 経過時間の値(秒単位) 例) 59
              },
              instructions: String, // 経路の説明
              maneuver: String // 次にどの方向に曲がるか（左折、右折、直進等）
            },
            ...（同じものが続く）
          ],
          ...
        },
        ...（同じものが続く）
      ],
      ...
    },
    ...（同じものが続く）
  ],
  ...
}
```

#### 配列で返ってくるものに関して説明

| フィールド       |       説明　　 |
|:-----------------|:------------------|
|routes|provideRouteAlternativesパラメータをtrueにした場合のみ複数格納されます。基本的にroutes[0]のみ使用すればOKです。|
|legs|waypointを設定し、stopoverパラメータをtrueにした場合にのみ、出発地〜経由地までの経路と経由地〜目的地の経路に分けて経路が格納されます。経由地がない、または途中降機なしの経由地のみの場合はlegs[0]のみ使用すればOKです。|
|steps|目的地までの経路で、進行方向を変えた時に経路が分割されて格納されます。基本的に複数の値が格納されています。|

### statusの中身

statusの中身はDirectionsStatusオブジェクトの要素（文字列）になっています。
このstatusを判定して、エラーハンドリングします。

| ステータス      |       説明　　 |
|:-----------------|:------------------|
|OK|検索成功|
|NOT_FOUND|リクエストパラメータに設定された場所のいずれかの場所が見つかりませんでした|
|ZERO_RESULTS|出発地と目的地の間にルートが見つかりませんでした|
|MAX_WAYPOINTS_EXCEEDED|設定した経由地の数が多すぎます|
|MAX_ROUTE_LENGTH_EXCEEDED|検索要求されたルートが長すぎて処理できません。|
|INVALID_REQUEST|リクエストパラメータが間違っています。|
|OVER_QUERY_LIMIT|一定期間内に送られたリクエストが多すぎます。|
|REQUEST_DENIED|このWebサービスではリクエストが許可されていません。|
|UNKNOWN_ERROR|サーバーエラー|

今回は上記の中で、OVER_QUERY_LIMITがよく引っかかる（何度もリクエストを送る）ため、
OVER_QUERY_LIMITが返ってきた場合に一定時間後に再度リクエストを送るようにハンドリングしました。

# ソースコード

## ステータスハンドラー

返却されたstatusに対して、[directionStatus](#statusの中身)に応じて
返却値を変えて返します。

```javascript

const googleStatusHandler = (status, directionStatus) =>
  status === directionStatus.OK
    ? {
        status: RequestStatus.OK,
        log: '正常取得'
      }
    : directionStatus.NOT_FOUND
    ? {
        status: RequestStatus.NG,
        log: '出発地、目的地、経由地のいずれかがジオコーディングできません。'
      }
    : directionStatus.ZERO_RESULTS
    ? {
      status: RequestStatus.NG,
        log: '検索ルートが見つかりません。'
      }
    : directionStatus.MAX_WAYPOINTS_EXCEEDED
    ? {
        status: RequestStatus.NG,
        log: '経由地の数が上限を超えました。'
      }
    : directionStatus.MAX_ROUTE_LENGTH_EXCEEDED
    ? {
        status: RequestStatus.NG,
        log: '要求されたルートが長すぎて処理できません。'
      }
    : directionStatus.INVALID_REQUEST
    ? {
        status: RequestStatus.NG,
        log: 'リクエストに問題が発生しました。リクエストを確認してください。'
      }
    : directionStatus.OVER_QUERY_LIMIT
    ? {
        status: RequestStatus.OTHER,
        log: 'クエリ数が上限に達しました。少し時間を置いて下さい。'
      }
    : directionStatus.REQUEST_DENIED
    ? {
        status: RequestStatus.NG,
        log: 'このページでは、リクエストが拒否されています。'
      }
    : directionStatus.UNKNOWN_ERROR
    ? {
        status: RequestStatus.NG,
        log: 'サーバー側でエラーが発生しました。'
      }
    : {
        status: RequestStatus.NG,
        log: 'エラーの原因が見つかりません。'
      }
```

## APIハンドラー
実際にAPIを送る関数。OVER_QUERY_LIMITが返ってきた場合に、
時間を空けてもう一度リクエストを送るように実装。

```javascript

/* 
  promiseCallback: DirectionAPIへのリクエスト
  ROOP_COUNT : リクエストを送る回数
  REQUEST_INTERVAL : リクエスト間隔（秒単位）
*/
const getGoogleApiResponse = async promiseCallback => {

  let result

  for (let i = 0; i < ROOP_COUNT; i++) {
    result = await new Promise(promiseCallback).catch(error => {
      throw new Error(error) // RequestStatus.NGの場合にエラー出力
    })
    if (result) break
    await new Promise(resolve =>
      setTimeout(() => resolve(), REQUEST_INTERVAL * 1000)
    )
  }

  if (result) return result
  else throw new Error('リクエストループ回数が上限を超えました。')

}

```

## Directions APIへのリクエスト関数
Direction APIへのリクエストを送る関数。
ログの出力はコンソール上にしています。

- [requestパラメータ](#パラメータの詳細（今回使用する一部抜粋）)
- [responseパラメータ](#responseパラメータ)
- [resultの中身](#resultの中身)

```javascript

export const getDirection = (
  dept, // 出発地
  dest, // 目的地
  waypoints = null, // 経由地
  stopover = false // 途中降機フラグ
) => {

  // stopoverをfalseに設定
  let convertWayPoints
  if (waypoints && waypoints.length > 0) {
    convertWayPoints = waypoints.map(value => {
      return {
        location: value,
        stopover: stopover
      }
    })
  }
  const request = {
    origin: dept,
    destination: dest,
    waypoints: convertWayPoints,
    travelMode: 'DRIVING'
  }

  return getGoogleApiResponse((resolve, reject) => {

    new google.maps.DirectionsService().route(request, (result, status) => {
      const response = googleStatusHandler(status, google.maps.DirectionsStatus) // エラーハンドリング

      switch (response.status) {
        case RequestStatus.OK: // 正常取得
          console.log(response.log)
           /*
            resultに関して処理を加えたい場合はここに記述
           */
          resolve(result) // 検索結果を返却
          break
        case RequestStatus.NG: // エラー
          console.error(response.log)
          reject(response.log) // promiseオブジェクト上でもエラーとして返す
          break
        case RequestStatus.OTHER: // エラーだけどまたリクエスト送りたい（OVER_QUERY_LIMIT）
          console.log(response.log)
          resolve(null) // promiseオブジェクト上は正常終了
          break
        default:
          console.error(response.log)
          reject(response.log)
          break
      }
    })
  })
}
```

# 最後に

駆け足での説明になってしまい申し訳ありません。
他のリクエスト(geocoding, distanceMatrix等)も、同様にエラーハンドリングして
使用すれば基本的にうまくいくと思います。
自分は当初、OVER_QUERY_LIMITに苦しめられましたが、しっかりハンドリングするようにしてからはうまくいくようになりました。
もしよろしければ参考にしてください。

## 参考リンク

- [Maps JavaScript API | Directions Service](https://developers.google.com/maps/documentation/javascript/directions?hl=ja)