Google Maps API で『食べログ ラーメン 百名店 WEST 2019』に選ばれたラーメン屋さんを現在位置から近い順にマッピングするアプリケーションの作成：序

## はじめに

Google マップで現在位置から目的地までの距離を取得するアプリケーションのつくり方を紹介します。要件は現在位置の取得、現在位置から店舗までの距離の取得やマッピングを行ないます。今回のアプリケーションの作成にあたり、Google Cloud Platform[^1]（以下、GCP という）を利用できる Google アカウントがあらかじめ必要になります。また、GCP のサービスの中で [Google Maps Platform](https://cloud.google.com/maps-platform) を利用しますので、Google Maps Platform の有効化を事前に行なってください。早速ですが、先述した要件をもとに以下の手順で作成します。

1. 現在位置の取得
2. 現在位置から目的地までの距離の取得
3. マッピング

また、今回作成するサンプルコードは [GitHub](https://github.com/toshifumiimanishi/200328-GCP-Qiita-demo) にアップロードしています。併せてご活用ください。

## 現在位置の取得

現在位置の取得には、[Geolocation API](https://developer.mozilla.org/ja/docs/Web/API/Geolocation/Using_geolocation) を利用します。Geolocation API は、ユーザーの同意のもと、現在位置をウェブアプリケーションに通知できます。しかしながら、多くのブラウザで Geolocation API を利用するには、安全なコンテキスト ( HTTPS ) でなければいけません。ローカル開発環境において、常時 SSL 化するには Browsersync[^2] が便利です。以下のコマンドを実行して、Browsersync をインストールしましょう。

```
$ npm install browser-sync --save-dev
```

無事にインストールが完了すれば、以下のコマンドを実行してローカルサーバーを立ち上げましょう。

```
$ npx browser-sync start --server --https
```

ローカル開発で SSL が有効になっているので、問題なく Geolocation API が利用できます。Local と External が起動したローカルサーバーの URL になります。余談ですが、同じネットワーク環境下にある PC やスマートフォンのブラウザから External URL にアクセスすれば、すべてのブラウザで同期されます。では、Geolocation API の `getCurrentPosition()` メソッドを呼び出して、現在位置を取得しましょう。こちらのメソッドは非同期通信で情報を取得するため、このあとの処理を async / await 式で非同期処理を書けるように `Promise` を返しましょう。

```js:main.js
const getCurrentPosition = () => {
  if ('geolocation' in navigator) {
    return new Promise((resolve, reject) => {
      navigator.geolocation.getCurrentPosition(resolve, reject);
    });
  }
};
```

たとえば、以下のような即時関数を実行すれば現在地の緯度経度を確認できます。

```js
(async () => {
  const currentPosition = await getCurrentPosition();
  const {
    coords: { latitude, longitude }
  } = currentPosition;

  console.log({latitude}, {longitude}); // => {latitude: xx.xxxxxxxx}, {longitude: xxx.xxxxxxxx}
})()
```

## 現在位置から目的地までの距離の取得

複数の出発地と目的地間の移動距離を取得するには、Maps JavaScript API の [Distance Matrix Service](https://developers.google.com/maps/documentation/javascript/distancematrix) を利用します。まずは、Distance Matrix Service を使用できるようにしましょう。Google Cloud Platform の各種画面から新規プロジェクトを作成しましょう。次に API ライブラリ画面から Maps JavaScript API と Distance Matrix Service を有効にしましょう。
![スクリーンショット 2020-03-28 16.01.18.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88525/1d80bbed-df3a-ba3a-5738-1325e695fe1c.jpeg)
認証情報画面から API キーを作成しましょう。本番環境での不正利用を回避するため、かならずキーを制限してください。たとえば、HTTP リファラーでアプリケーションの制限をすれば、指定した Web サイト以外で API キーを利用できません。`<script>` 要素で Maps JavaScript API を以下のように読み込ませましょう。GOOGLE_MAPS_API_KEY には先ほど作成した API キーを指定します。

```html:index.html
<script src="https://maps.googleapis.com/maps/api/js?key=GOOGLE_MAPS_API_KEY"></script>
```

これで Maps JavaScript API、および、Distance Matrix Service を利用できます。試しに `google.maps.DistanceMatrixService` コンストラクタを介してインスタンスを作成してみましょう。無事に作成できたら API が正しく読み込まれています。

```js:main.js
const service = new google.maps.DistanceMatrixService();
console.log({service}); // => インスタンス
```

それでは、本題の現在位置から目的地までの距離を取得する開発を行ないます。今回のサンプルは『[食べログ ラーメン 百名店 WEST 2019](https://award.tabelog.com/hyakumeiten/ramen_west)』に選ばれたラーメン屋さんの位置情報を用意しました。

```json:ramen_deta.json
[
  {
    "id": "001",
    "name": "燃えよ麺助",
    "latLng": {
      "lat": 34.696335,
      "lng": 135.48689
    }
  },
  {
    "id": "002",
    "name": "人類みな麺類",
    "latLng": {
      "lat": 34.725463,
      "lng": 135.499181
    }
  },
  {
    "id": "003",
    "name": "ラーメン人生JET",
    "latLng": {
      "lat": 34.698637,
      "lng": 135.486647
    }
  },
  {
    "id": "004",
    "name": "総大醤",
    "latLng": {
      "lat": 34.710304,
      "lng": 135.507695
    }
  },
  {
    "id": "005",
    "name": "ラーメン家 みつ葉",
    "latLng": {
      "lat": 34.692421,
      "lng": 135.731966
    }
  }
]
```

では、現在位置から各店舗の距離を返す `fetchDistanceMatrix()` メソッドを作成しましょう。少し長くなりますが、一行一行のコードを読めば大してむずかしくないかと思います。`travelMode` の `DRIVING` は、時間と距離を計算するときに使う交通手段の指定です。`DRIVING` は、道路網を使用した標準の運転ルートになります。ほかのオプションの詳しい内容は[公式ドキュメント](https://developers.google.com/maps/documentation/javascript/distancematrix#distance_matrix_requests)をご参照ください。

```js:main.js
const fetchDistanceMatrix = async () => {
  // 現在位置情報の取得
  const currentPosition = await getCurrentPosition();

  // ラーメンデータの取得
  const ramenData = await fetch('ramen_data.json').then(response =>
    response.json()
  );

  // 緯度経度の取得
  const {
    coords: { latitude: lat, longitude: lng }
  } = currentPosition;

  // インスタンスの作成
  const service = new google.maps.DistanceMatrixService();

  // ラーメン各店舗の緯度経度の取得
  const destinations = ramenData.map(({ latLng }) => latLng);

  // DistanceMatrixService.getDistanceMatrix() メソッドのオプション
  const options = {
    origins: [{lat, lng}], // 出発地
    destinations, // 目的地
    travelMode: 'DRIVING' // 交通手段
  };

  return new Promise((resolve, reject) => {
    service.getDistanceMatrix(options, (response, status) => {
      if (status === 'OK') {
        const { rows } = response;
        const { elements } = rows[0];
        resolve(elements);
      } else {
        reject(status);
      }
    });
  });
};
```
Distance Matrix Service の利用にあたり、いくつか注意点があります。以下のような場合、エラーが起こります。

- `MAX_DIMENSIONS_EXCEEDED`：一度に 25 を超える出発地、または、目的地を含めたリクエスト
- `OVER_QUERY_LIMIT`：アプリケーションが許可された期間内に多すぎるリクエスト

2 つめのレート制限は、特定の期間内にリクエストが多すぎる場合、API が `OVER_QUERY_LIMIT` ステータスコードを返します。妥当な時間をおいて再試行すると、リクエストは成功します。1 つ目の `MAX_DIMENSIONS_EXCEEDED` は、配列内容の要素を 25 ごとに分割すれば対処できます。サンプル JSON データを 25 を超える店舗数に増やしてみました。

<details><summary>**サンプル JSON データはこちら**</summary><div>

```json:ramen_deta.json
[
  {
    "id": "001",
    "name": "燃えよ麺助",
    "latLng": {
      "lat": 34.696335,
      "lng": 135.48689
    }
  },
  {
    "id": "002",
    "name": "人類みな麺類",
    "latLng": {
      "lat": 34.725463,
      "lng": 135.499181
    }
  },
  {
    "id": "003",
    "name": "ラーメン人生JET",
    "latLng": {
      "lat": 34.698637,
      "lng": 135.486647
    }
  },
  {
    "id": "004",
    "name": "総大醤",
    "latLng": {
      "lat": 34.710304,
      "lng": 135.507695
    }
  },
  {
    "id": "005",
    "name": "ラーメン家 みつ葉",
    "latLng": {
      "lat": 34.692421,
      "lng": 135.731966
    }
  },
  {
    "id": "006",
    "name": "烈火 本店",
    "latLng": {
      "lat": 34.748931,
      "lng": 135.475763
    }
  },
  {
    "id": "007",
    "name": "群青",
    "latLng": {
      "lat": 34.710019,
      "lng": 135.512582
    }
  },
  {
    "id": "008",
    "name": "中華そば いぶき",
    "latLng": {
      "lat": 34.707547,
      "lng": 135.509922
    }
  },
  {
    "id": "009",
    "name": "中華そば 無限",
    "latLng": {
      "lat": 34.69663,
      "lng": 135.47216
    }
  },
  {
    "id": "010",
    "name": "彩色ラーメン きんせい総本家 夢風",
    "latLng": {
      "lat": 34.828435,
      "lng": 135.60315
    }
  },
  {
    "id": "011",
    "name": "麺屋 八海山",
    "latLng": {
      "lat": 34.853487,
      "lng": 135.616982
    }
  },
  {
    "id": "012",
    "name": "麺匠而今",
    "latLng": {
      "lat": 34.727874,
      "lng": 135.5568
    }
  },
  {
    "id": "013",
    "name": "極麺 青二犀",
    "latLng": {
      "lat": 34.765328,
      "lng": 135.533446
    }
  },
  {
    "id": "014",
    "name": "麺や 青雲志",
    "latLng": {
      "lat": 34.622521,
      "lng": 136.483616
    }
  },
  {
    "id": "015",
    "name": "烈志笑魚油 麺香房 三く",
    "latLng": {
      "lat": 34.693758,
      "lng": 135.485974
    }
  },
  {
    "id": "016",
    "name": "麺哲",
    "latLng": {
      "lat": 34.784835,
      "lng": 135.463407
    }
  },
  {
    "id": "017",
    "name": "らーめん弥七",
    "latLng": {
      "lat": 34.711282,
      "lng": 135.499696
    }
  },
  {
    "id": "018",
    "name": "らーめん颯人",
    "latLng": {
      "lat": 34.695737,
      "lng": 135.510028
    }
  },
  {
    "id": "019",
    "name": "麺屋 そにどり",
    "latLng": {
      "lat": 34.973983,
      "lng": 136.617343
    }
  },
  {
    "id": "020",
    "name": "ロックンビリーS1",
    "latLng": {
      "lat": 34.74822,
      "lng": 135.420927
    }
  },
  {
    "id": "021",
    "name": "麺と心 7",
    "latLng": {
      "lat": 34.639573,
      "lng": 135.511245
    }
  },
  {
    "id": "022",
    "name": "あいつのラーメン かたぐるま",
    "latLng": {
      "lat": 34.99282,
      "lng": 135.736196
    }
  },
  {
    "id": "023",
    "name": "和 dining 清乃",
    "latLng": {
      "lat": 34.078622,
      "lng": 135.130528
    }
  },
  {
    "id": "024",
    "name": "麺屋 たけ井 本店",
    "latLng": {
      "lat": 34.835569,
      "lng": 135.791763
    }
  },
  {
    "id": "025",
    "name": "つけ麺無心",
    "latLng": {
      "lat": 34.674597,
      "lng": 135.752122
    }
  },
  {
    "id": "026",
    "name": "麺屋NOROMA",
    "latLng": {
      "lat": 34.666327,
      "lng": 135.819132
    }
  }
]
```
</div></details>

また、以下のようなヘルパー関数 `arrayChunk()` を作成しました。第 1 引数に対象となる配列、第 2 引数に分割数を指定します。

```js:main.js
  const MAX_LIMITED_DIMENSIONS = 25;
  const arrayChunk = (array, size = 1) => {
    return array.reduce(
      (acc, value, i) =>
        i % size ? acc : [...acc, array.slice(i, i + size)],
      []
    );
  };
  const chunkedDestinations = arrayChunk(destinations, MAX_LIMITED_DIMENSIONS);
```

25 分割した店舗情報を `Promise.all()` メソッドを利用して、すべての完了を待ってから `Promise` を返すようにしましょう。また、`values` は多階層の配列になっているので `flat()` メソッドを利用して一階層にしましょう。

```js:main.js
  const promises = chunkedDestinations.map((destinations) => {
    return new Promise((resolve, reject) => {
      const options = {
        origins: [{ lat, lng }], // 出発地
        destinations, // 目的地
        travelMode: 'DRIVING' // 交通手段
      };
      
      service.getDistanceMatrix(options, (response, status) => {
        if (status === 'OK') {
          const { rows } = response;
          const { elements } = rows[0];
          resolve(elements);
        } else {
          reject(status);
        }
      });
    });
  })
  
  return Promise.all(promises).then((values) => values.flat());
```

## マッピング

Maps JavaScript API を利用して Google マップ（以下、地図という）を作成します。まずは、地図を埋め込むための HTML 要素の用意、地図をブラウザ全画面に描画するようなスタイルを作成しましょう。

```html:index.html
<style>
  #map {
    height: 100%;
  }
  html, body {
    height: 100%;
    margin: 0;
    padding: 0;
  }
</style>

<!-- 以下、省略 -->

<div id="map"></div>
```

HTML の準備が完了したので、現在位置が中央になるように地図を作成しましょう。また、各ラーメン店舗を現在位置から近い順にナンバリングしたマーカーを作成しましょう。マーカーの作成には、`google.maps.Marker` コンストラクタを実行します。

```main.js
const initMap = async () => {
  const currentPosition = await getCurrentPosition();
  const ramenData = await fetch('ramen_data.json').then(response =>
    response.json()
  );
  const distanceMatrix = await fetchDistanceMatrix();
  const {
    coords: { latitude: lat, longitude: lng }
  } = currentPosition;
  const embedElement = document.getElementById('map');
  const options = {
    center: { lat, lng },
    zoom: 12
  };

  // ラーメンデータに距離情報の設定します
  ramenData.map((data, i) => {
    data.distanceMatrix = distanceMatrix[i];
  });

  // ラーメンデータを距離の昇べきの順でソートします
  ramenData.sort((a, b) => {
    return a.distanceMatrix.distance.value - b.distanceMatrix.distance.value;
  });

  // 地図の描画
  const map = new google.maps.Map(embedElement, options);

  // マーカーの作成
  ramenData.map(({ latLng }, i) => {
    const label = (i + 1).toString();
    const options = {
      position: latLng,
      label,
      map
    };
    const marker = new google.maps.Marker(options);
  });
};

google.maps.event.addDomListener(window, 'load', initMap);
```

少し駆け足になりましたが、一通り完成です。現在位置から近い順にナンバリングされたマーカーが作成された地図が描画されているかと思います。

## さいごに

今回は、現在位置の取得、現在位置から店舗までの距離の取得やマッピングにおける Google Maps API の使い方を中心に解説しました。次回は、[React](https://ja.reactjs.org/) や [Vue.js](https://jp.vuejs.org/index.html) のような宣言的な View を構築できるライブラリと合わせてアプリケーションを作成しようかと思います。

## 参考文献

- [Geolocation API - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/Geolocation_API)
- [Overview  |  Maps JavaScript API  |  Google Developers](https://developers.google.com/maps/documentation/javascript/tutorial)
- [Markers  |  Maps JavaScript API  |  Google Developers](https://developers.google.com/maps/documentation/javascript/markers)
- [Distance Matrix Service  |  Maps JavaScript API  |  Google Developers](https://developers.google.com/maps/documentation/javascript/distancematrix)
- [食べログ ラーメン 百名店 WEST 2019](https://award.tabelog.com/hyakumeiten/ramen_west)

[^1]: [Google Cloud Platform](https://cloud.google.com/) とは、Google が提供しているクラウドコンピューティングサービスです。
[^2]: [Browsersync](https://www.browsersync.io/) は、ファイル変更の監視やブラウザを自動でリロードするツールです。Web プラットフォーム、ビルドツール（e.g. [gulp](https://gulpjs.com/)）、および、その他の Node.js プロジェクトとカンタンに統合できます。