GoogleMapsAPIを使用した開発4（ルート検索機能）

# 1.はじめに
投稿された情報（緯度経度）からGooleMapsAPIを使用して地図を表示、地点間のルート検索機能、周辺情報の取得を実装したので知識の定着、復習のために記事を書きます。
今回はその４回目でルート検索機能を実装していきます。

# 2.環境
Ruby (2.6.4)
Rails (6.1.7)
Maps JavaScript API	

# 3.前提条件
APIキー取得済み
投稿機能を実装している
下記を実装している
[GoogleMapsAPIを使用した開発1（地図の表示）](https://qiita.com/uuuDi_97722/items/6a064cf7e40d91a517c0)
[GoogleMapsAPIを使用した開発2(現在地取得)](https://qiita.com/uuuDi_97722/items/7acf76187459ee627421)
[GoogleMapsAPIを使用した開発3（周辺情報取得機能）](https://qiita.com/uuuDi_97722/items/b9733b7e820fc7aa39fb)

# 4.実装
## 1.viewの修正
```diff_ruby
      <div class="map">
        <div id="map_index"></div>
+       <div class="map-search">
+           <select id="travel-mode-selector" class="form-control mb-2 mb-md-2">
+             <option value="WALKING">徒歩</option>
+             <option value="DRIVING">車</option>
+           </select>
+           <%= button_tag "ルートを検索する", id: "btn-search", class: "btn btn-primary", onclick: "search()" %>
            <button id="current-location-button" class="btn btn-primary">現在地を取得する</button>
+           <button class="btn btn-secondary ml-md-3 m-md-1" onclick="resetRoute()">検索結果をリセット</button>
+       </div>
+       <div class="map-route">
+         <h5>ルート検索リスト</h5>
+         <ul id="route-list" class="list-group"></ul>
+       </div>
+       <div class="mt-3">
+         <h5>各地点間の距離・時間</h5>
+         <div id="directions-panel">
+           <ul id="display-list" class="list-group"></ul>
+         </div>
+       </div>
      </div>
```

## 2.スポットのinfowidowに追加ボタンを追加
```diff_javascript
      google.maps.event.addListener(marker[i], 'click', function () {
        infoWindow.setContent(
+         `<a href='/posts/${id}'>${spots[i]['location']}</a><button onclick="addPost(${i})">追加</button>`);
        infoWindow.open(map, marker[i]);
      });
```
- `<button onclick="addPost(${i})">追加</button>`の追加
- `<button onclick="addPost(${i})">追加</button>`は、ボタンがクリックされると、addPost 関数が呼び出されます。引数にはスポットのインデックス i が渡されます。


## 3.現在地のinfowidowに追加ボタンを追加
```diff_javascript
        currentLocationMarker.addListener('click', function () {
          infoWindow.setContent(
+           `<strong> 現在地 </strong><button onclick="addPost(-1)">追加</button>`
          );
          infoWindow.open(map, currentLocationMarker);
        });
```

## 4.周辺情報のinfowidowに追加ボタンを追加
```diff_javascript
    content +=
      `<button onclick="addPostFromInfoWindow(${placeLoc.lat()}, ${placeLoc.lng()}, '${placeDetails.name}')">追加</button>`;
```

## 5.検索リストにスポットを追加(周辺情報)
```javascript
  function addPostFromInfoWindow(lat, lng, location) {
    const li = document.createElement('li'); // ①
    li.textContent = location; // ②
    li.className = "list-group-item"; // ③
    li.setAttribute("data-lat", lat); // ④
    li.setAttribute("data-lng", lng);

    const routeList = document.getElementById('route-list'); // ⑤
    routeList.appendChild(li);

    infoWindow.close(); // ⑥
  }
```
①新しいリストアイテム`<li>`要素を作成します。

②そのリストアイテムに指定された場所の名前をテキストコンテンツとして設定します。

③list-group-itemクラスを追加して、リストアイテムにスタイリングを適用します。

④リストアイテムに緯度と経度を格納するための data-lat と data-lng 属性を設定します。

⑤HTML内の route-list というIDを持つ要素を取得します。
- 取得した要素に新しいリストアイテムを子要素として追加します。

⑥情報ウィンドウを閉じます。

## 6.検索リストにスポットを追加（現在地、投稿された各スポット）
```javascript
  function addPost(number) {
    const li = document.createElement('li'); // ①
    if (number === -1) { // ②
      li.textContent = "現在置";
    } else {
      li.textContent = spots_location[number];
    }

    li.className = "list-group-item"; // ③

    // li要素にdata-lat,lngのデータ属性を追加
    if (number === -1) { // ④
      li.setAttribute("data-lat", currentLocationMarker.getPosition().lat());
      li.setAttribute("data-lng", currentLocationMarker.getPosition().lng());
    } else {
      li.setAttribute("data-lat", spots_lat[number]);
      li.setAttribute("data-lng", spots_lng[number]);
    }

    const routeList = document.getElementById('route-list'); // ⑤
    routeList.appendChild(li);

    infoWindow.close(); // ⑥
  }
```
①新しいリストアイテム`<li>`要素を作成します。

②リストアイテムのテキストコンテンツを設定します。
- 条件によって、「現在置」と表示するか、特定のスポットの場所名を表示します。

③リストアイテムにクラス名 "list-group-item" を設定します。これは、Bootstrapなどのスタイルフレームワークが提供するスタイルを利用するためのものです。これにより、リストアイテムが特定のスタイルを持つことができます。

④リストアイテムに緯度と経度のデータ属性 (data-lat および data-lng) を追加します。これにより、後でJavaScriptからこれらの値にアクセスできるようになります。

⑤document.getElementById メソッドを使用して、リストを取得し、新しいリストアイテムをリストに追加します。これにより、地図上の特定の場所に対応するリストが作成されます。

⑥infoWindow.close() を呼び出して、InfoWindow を閉じます。 InfoWindow は通常、マーカーをクリックしたときにポップアップする情報ウィンドウです。この関数が呼ばれると、InfoWindowが閉じられ、新しい情報が表示されます。

## 7.ルートを検索する関数の作成
```javascript
  function search() {
    const points = $('#route-list li'); // ①
    if (points.length >= 2) { // ②
      let origin;
      let destination;
      const waypoints = [];

      for (let i = 0; i < points.length; i++) { // ②
        points[i] = new google.maps.LatLng($(points[i]).attr("data-lat"), $(points[i]).attr("data-lng")); // ③
        if (i == 0) { // ④
          origin = points[i];
        } else if (i == points.length - 1) { // ⑤
          destination = points[i];
        } else { // ⑥
          waypoints.push({
            location: points[i],
            stopover: true
          });
        }
      }
```
①`const points = $('#route-list li');`
- #route-list 内の全てのリストアイテムを取得して、points という変数に格納します。

②`if (points.length >= 2) {`
- 取得したリストアイテムの数が2以上であるかを確認します。
- 2つ以上の地点が必要なため、この条件が満たされている場合にのみ処理が続行されます。
- `let origin`,`let destination;`で出発地と目的地のための変数を宣言しています。
- `const waypoints = [];`で経由地点を格納するための空の配列を作成しています。

②`for (let i = 0; i < points.length; i++) {`
- 取得した各リストアイテムに対してループを実行します。

③`points[i] = new google.maps.LatLng($(points[i]).attr("data-lat"), $(points[i]).attr("data-lng"));`
- 各リストアイテムから緯度と経度を取得して、google.maps.LatLng オブジェクトを作成し、points[i] に格納します。

④`if (i == 0) {`
- １つ目のリストアイテムの場合、それを出発地 (origin) として設定します。

⑤`else if (i == points.length - 1) {`
- 最後のリストアイテムの場合、それを目的地 (destination) として設定します。

⑥`else {`
- それ以外の場合は、経由地点として設定し、waypoints 配列に追加します。

このループが終了すると、origin には最初の地点が、destination には最後の地点が、waypoints にはそれ経由地が設定されます。
これにより、DirectionsService によるルート検索のためのデータが用意されます。

```javascript
      const selectedTravelMode = $('#travel-mode-selector').val();
```
- HTML要素`#travel-mode-selector`から選択された移動手段の値を取得しています。
- 具体的には、$('#travel-mode-selector') で、idが "travel-mode-selector" であるHTML要素を取得しています。
- その後、`.val()`を使用してその要素の現在の値を取得しています。
- この値はユーザーが選択した移動手段(徒歩、車)に対応しています。

## 8.Directions APIに送信されるリクエストを構築
```javascript
      const request = {
        origin: origin, // ①
        destination: destination, // ②
        waypoints: waypoints, // ③
        travelMode: google.maps.TravelMode[selectedTravelMode], // ④
      };
```
①②③出発地点、目的地、経由地の座標を指定しています。

④移動手段を指定します。
- ユーザーが選択した移動手段に対応しています。

これらの情報を含むrequestオブジェクトは、DirectionsAPIに対してルート検索のリクエストを行う際に使用されます。

## 9.地図上に経路を描画する

```javascript
      // 変数宣言
      let directionsRenderer = null;

      if (directionsRenderer) { // ①
        directionsRenderer.setMap(null);
      }

      directionsRenderer = new google.maps.DirectionsRenderer({ // ②
        map: map,
        suppressMarkers: true,
        panel: document.getElementById('directions-panel'),
        polylineOptions: {
          strokeColor: '#00ffdd',
          strokeOpacity: 1,
          strokeWeight: 5
        }
      });
```

①既存のdirectionsRendererオブジェクトが存在するかを確認しています。
もし存在すれば、setMap(null)を呼び出して、既存のルートを地図上からリセットします。

②新しいDirectionsRendererオブジェクトを作成します。
- map: ルートを描画する地図を指定します。
- suppressMarkers: マーカーを表示しないように設定します。
- panel: ルートの詳細な情報を表示するためのHTML要素を指定します。
- polylineOptions: ポリラインの描画に関するオプションです。色や太さなどを指定しています。

新しいdirectionsRendererオブジェクトが作成され、地図に追加されます。これによりルートが描画されます。

```javascript
      new google.maps.DirectionsService().route(request, function (response, status) { // ①
        if (status == google.maps.DirectionsStatus.OK) { // ②
          directionsRenderer.setDirections(response);
        } else {
          alert('ルートを見つけることができませんでした: ' + status); // ③
        }
      });
    } else {
      alert('2つ以上の地点を追加してください。'); // ④
    }
  }
```
①DirectionsServiceの新しいインスタンスを作成します。
- DirectionsServiceのrouteメソッドを呼び出し、経路を検索します。
- このメソッドは、検索のパラメータとしてrequestオブジェクトを受け取ります。
- 経路検索が完了すると、指定されたコールバック関数が呼び出されます。
- このコールバック関数には、responseとstatusが渡されます。

②statusが`google.maps.DirectionsStatus.OK`であればdirectionsRenderer.setDirections(response)を呼び出して、DirectionsRendererに経路情報をセットします。
- これにより、地図上に新しい経路が描画されます。

③経路が見つからなかった場合、alertメッセージが表示されます。

④2つ以上の地点が追加されていない場合、alertメッセージが表示されます。

## 10.検索結果とリストをリセットする関数を作成
```javascript
  function resetRoute() {
    if (directionsRenderer) { // ①
      directionsRenderer.setMap(null); 
    }

    const routeList = document.getElementById('route-list'); // ②
    routeList.innerHTML = '';

    const directionsPanel = document.getElementById('directions-panel'); // ③
    directionsPanel.innerHTML = '';
  }
```
①directionsRendererが存在する場合、`directionsRenderer.setMap(null)`を呼び出して地図上の経路をリセットします。
これにより、以前に描画された経路が地図上から削除されます。

②地図上の経路を表示しているリスト（route-list）の内容をリセットします。

③検索結果を表示しているパネル（directions-panel）の内容をリセットします。

# 11.おわりに
これでルート検索機能が実装されました。
今回までで、投稿された情報（緯度経度）からGooleMapsAPIを使用して地図を表示、地点間のルート検索機能、周辺情報の取得を実装できました。