[Rails]Google Maps APIによるGoogle Mapの表示と複数地点間のルート検索

# はじめに
参考になる記事がとても少なく、ポートフォリオ作成で一番苦戦した部分なので、自分の学習のためアウトプットとして残す、とともにだれかの役に立てればいいなと思ったので書きました！
初学者なりにこの記述の意味はなんだ？と思う部分はしっかり説明したつもりです。
当たり前だろ！と思う部分も多々あると思いますがご了承ください。

# 目標
Google Maps APIでGoogle Mapを表示させるとともに、マーカーの吹き出しから任意にルート検索リストに追加でき、複数地点のルートを検索する機能を目標とします。
![ezgif com-optimize-2](https://user-images.githubusercontent.com/56623611/97084273-e0d23780-1650-11eb-884b-e4e26774b2b0.gif)


# 開発環境
・Ruby: 2.5.1
・Rails: 5.2.1
・OS: macOS
# 前提
・Slimの導入
・公式のGoogle Maps Platformで以下のAPIの有効化
　・Maps JavaScript API
　　→ GoogleMapの表示
　・Geocoding API
　　→ 住所から緯度経度の算出
　・Directions API
　　→ ルート検索

##設定
###1. 必要なgemをインストール

```ruby:Gemfile
gem 'dotenv-rails' # APIキーを環境変数化
gem 'gon' # コントローラーで定義したインスタンス変数をJavaScript内で使用出来るようにする。
gem 'geocoder' # 住所から緯度経度を算出する。
```

```:ターミナル
$ bundle install
```


###2. APIキーを環境変数化

アプリケーション直下に「.env」ファイルを作成

```:ターミナル
$ touch .env 
```

自身のAPIキーを' 'の中に記述

```ruby:.env
GOOGLE_MAP_API = '自身のコピーしたAPIキー'
```

```:.gitignore
/.env
```

###3. turbolinksの無効化

```ruby:Gemfile
gem 'turbolinks' # この行を削除
```

```javascript:app/assets/javascripts/application.js
//= require turbolinks // この行を削除
```


data-turbolinks-track':'reload'属性の削除

```ruby:app/views/layouts/application.html.slim
= stylesheet_link_tag    'application', media: 'all'
= javascript_include_tag 'application'
```


###4. Geocoding APIを使用できるようにする
geocorderの設定ファイルを作成し、編集

```:ターミナル
$ touch config/initializers/geocoder.rb
```

```ruby:config/initializers/geocoder.rb
# 追記
Geocoder.configure(
  lookup: :google,
  api_key: ENV['GOOGLE_MAP_API']
)
```

これで設定は終了です。ここからGoogleMapを表示していく実装に入ります。

## GoogleMapの表示

###1. 追加したいモデルにカラムを追加
自分のアプリの場合はPlaceモデルにaddressカラムを追加します。
latitude, longitudeカラムはGeocoding APIによってaddressカラムの値から算出された経度・緯度の値です。小数の値なので型はfloatを使います。

```:ターミナル
$ rails g migration AddColumnsToPlaces address:string latitude:float longitude:float
```

```:ターミナル
$ rails db:migrate
```

###2. モデルを編集

```ruby:models/place.rb
  # 追記
  geocoded_by :address # addressカラムを基準に緯度経度を算出する。
  after_validation :geocode # 住所変更時に緯度経度も変更する。
```

###3. コントローラーを編集

```ruby:controllers/places_controller.rb
def index
  @place = Place.all
  gon.place = @place # 追記
end

private
  def place_params
    # ストロングパラメーターに「address」を追加
    params.require(:place).permit(:name, :description, :image, :address)
  end
```


###4. ビューを編集

①application.html.slimを編集
CSSとJavaScriptより先に、gonを読み込むよう記述します。

```ruby:views/layouts/application.html.slim
doctype html
html
  head
    title
      | app_name
    = csrf_meta_tags
    = csp_meta_tag
    = include_gon # 追記
    = stylesheet_link_tag    'application', media: 'all'
    = javascript_include_tag 'application'
```

②新規登録画面に住所入力フォームを追加

```ruby:views/places/new.html.slim
= f.label :address, '住所'
= f.text_field :address, class: 'form-control'
```
③GoogleMapを表示するファイルに記述

```ruby:views/places/index.html.slim
div id = 'map_index' # idを付与, この部分にjsファイルで記述したGoogle Mapが埋め込まれる
- google_api = "https://maps.googleapis.com/maps/api/js?key=#{ ENV['GOOGLE_MAP_API'] }&callback=initMap".html_safe
script{ async src = google_api }

.map-route
  < ルート検索リスト >
  ul id = "route-list" class = "list-group" # jsファイルで吹き出しの追加ボタンによってその場所がli要素に追加される


div id = 'directions-panel' # 距離・時間が埋め込まれる
  < 各地点間の距離・時間 >
  ul id = "display-list" class = "display-group"

.map-search
   = button_tag "ルート検索", id: "btn-search", class: "btn btn-primary", onclick:     "search()" # クリック処理でsearch()関数を呼び出す
```

[ google_api = 〜〜〜〜の部分について ]
→ callback処理で読み込み時にinitMap関数を呼び出す。
→ .html_safeはエスケープ処理
→ async属性によって非同期でJavaScriptを読み込みレンダリングを早くする。

④GoogleMapで表示したいサイズをscssに記述

```scss:stylesheets/application.scss
#map_index{
  height: 400px;
  width: 400px; 
}
```


###5. JavaScriptのファイルを編集

ここが肝です。
assets/javascripts直下に新たなファイルを作成し、記述します。
だいぶ長く見にくいかと思いますが、変数を定義したのち、関数の定義をそれぞれ行っているだけです。
関数は、
・initMap
・markerEvent( i ) 
・addPlace(name, lat, lng, number)
・search()
の順で4つがあります。
わかりにくい部分やポイントは、コメントアウトで説明していますので参考にしてください。




```javascript:assets/javascripts/googlemap.js
var map
var geocoder
var marker = [];
var infoWindow = [];
var markerData = gon.places; // コントローラーで定義したインスタンス変数を変数に代入
var place_name = [];
var place_lat = [];
var place_lng = [];

// GoogleMapを表示する関数(callback処理で呼び出される)
function initMap(){
    geocoder = new google.maps.Geocoder()
    // ビューのid='map_index'の部分にGoogleMapを埋め込む
    map = new google.maps.Map(document.getElementById('map_index'), {
      center: { lat: 35.6585, lng: 139.7486 }, // 東京タワーを中心
      zoom: 9,
    });

    // 繰り返し処理でマーカーと吹き出しを複数表示させる
    for (var i = 0; i < markerData.length; i++) {
      // 各地点の緯度経度を算出
      markerLatLng = new google.maps.LatLng({
        lat: markerData[i]['latitude'],
        lng: markerData[i]['longitude']
      });
      
      // マーカーの表示
      marker[i] = new google.maps.Marker({
        position: markerLatLng,
        map: map
      });

      // 吹き出しの表示
      let id = markerData[i]['id']
      place_name[i]= markerData[i]['name'];
      place_lat[i]= markerData[i]['latitude'];
      place_lng[i]= markerData[i]['longitude'];
      infoWindow[i] = new google.maps.InfoWindow({
        // 吹き出しの中身, 引数で各属性の配列と配列番号を渡す
        content: `<a href='/places/${ id }'>${ markerData[i]['name'] }</a><input type="button" value="追加" onclick="addPlace(place_name, place_lat, place_lng, ${i})">`
      });
      markerEvent(i);
    }
  }
}

// マーカーをクリックしたら吹き出しを表示
function markerEvent(i) {
  marker[i].addListener('click', function () {
    infoWindow[i].open(map, marker[i]);
  });
}

// リストに追加する
function addPlace(name, lat, lng, number){
  var li = $('<li>', {
    text: name[number],
    "class": "list-group-item"
  });
  li.attr("data-lat", lat[number]); // data-latという属性にlat[number]を入れる
  li.attr("data-lng", lng[number]); // data-lngという属性にlng[number]を入れる
  $('#route-list').append(li); // idがroute-listの要素の一番後ろにliを追加
}

// ルートを検索する
function search() {
  var points = $('#route-list li');

  // 2地点以上のとき
  if (points.length >= 2){
      var origin; // 開始地点
      var destination; // 終了地点
      var waypoints = []; // 経由地点

      // origin, destination, waypointsを設定する
      for (var 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 });
          }
      }
      // リクエストの作成
      var request = {
        origin:      origin,
        destination: destination,
        waypoints: waypoints,
        travelMode:  google.maps.TravelMode.DRIVING
      };
      // ルートサービスのリクエスト
      new google.maps.DirectionsService().route(request, function(response, status) {
        if (status == google.maps.DirectionsStatus.OK) {
          new google.maps.DirectionsRenderer({
            map: map,
            suppressMarkers : true,
            polylineOptions: { // 描画される線についての設定
              strokeColor: '#00ffdd',
              strokeOpacity: 1,
              strokeWeight: 5
            }
          }).setDirections(response);//ライン描画部分
          
            // 距離、時間を表示する
            var data = response.routes[0].legs;
            for (var i = 0; i < data.length; i++) {
                // 距離
                var li = $('<li>', {
                  text: data[i].distance.text,
                  "class": "display-group-item"
                });
                $('#display-list').append(li);

                // 時間
                var li = $('<li>', {
                  text: data[i].duration.text,
                  "class": "display-group-item"
                });
                $('#display-list').append(li);
            }
            const route = response.routes[0];
            // ビューのid='directions-panel'の部分に埋め込む
            const summaryPanel = document.getElementById("directions-panel");
            summaryPanel.innerHTML = "";

            // 各地点間の距離・時間を表示
            for (let i = 0; i < route.legs.length; i++) {
              const routeSegment = i + 1;
              summaryPanel.innerHTML +=
                "<b>Route Segment: " + routeSegment + "</b><br>";
              summaryPanel.innerHTML += route.legs[i].start_address + "<br>" + " ↓ " + "<br>";
              summaryPanel.innerHTML += route.legs[i].end_address + "<br>";
              summaryPanel.innerHTML += "<" + route.legs[i].distance.text + ",";
              summaryPanel.innerHTML += route.legs[i].duration.text + ">" + "<br>";
            }
        }
      });
  }
}



```

__吹き出しの内容のcontent部分の補足:(データの受け渡しの方法で苦戦したので)__

```
content: `<a href='/places/${ id }'>${ markerData[i]['name'] }</a><input type="button" value="追加" onclick="addPlace(place_name, place_lat, place_lng, ${i})">`
```

addPlace(place_name, place_lat, place_lng, ${i})
この関数の呼び出しでは前の3つの引数は配列として渡しています。4つ目の引数は配列の中でどの情報かを表すための番号(インデックスと呼びます。)を式展開したものです。JavaScriptでの式展開はこの形だそうです。
このような引数を用意することで、関数addPlace(name, lat, lng, number)は正常にどのデータであるかという情報を処理できるのです。

# 最後に
最後まで読んでくださり、ありがとうございます。
自分自身、現在ポートフォリオが完成に近づき就職活動を本格的に始め出したような状態です！
目標を持ってポートフォリオ作成、転職活動など行っている方を心から応援しています、共に頑張りましょう！！

# 参考
- [Google Mapに複数マーカーを表示し、クリックしたら吹き出しを出す方法](https://qiita.com/matsubishi5/items/2c1abe7e5d19ae5697b9)

- [Google Maps APIでリストに追加した複数地点のルートを検索する]
(https://qiita.com/yoshi_01/items/d3848e4e7c854fe585bd)