[Ruby on Rails 5] Google Maps API と geocoderを使って地図情報を保存し、リストとマーカーを表示する

#動機
ポートフォリオに地図機能を実装しました。
同じように「地図機能をつけたい！」という方の参考になればと思い、記事を作成しました。

#目標物
![ezgif.com-video-to-gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555201/77e5726a-dee8-f7c0-27d6-3f6ff45d4c00.gif)

地名で場所を検索すると、該当の場所に地図が移動し、赤色のマーカーが地図上に立ちます。
連続して検索すると、前の赤いマーカーは削除されます。
検索値が自動でタイトルに入力されます。コメントを添えて保存することができます。
保存された地図情報を元に緑色のマーカーが該当の場所に立ちます。
保存されたタイトルとコメントが地図の下に一覧で表示されます。

#参考と前提
@tiara さんの下記記事を土台とさせていただき、Google Mapの表示と地図検索までを行っています。
こちらの記事では、Google MapのAPI Keyの取得など、画像つきで大変詳しく説明されています。

「Rails5でGoogleMapを表示してみるまで」
https://qiita.com/tiara/items/4a1c98418917a0e74cbb#%E5%9C%B0%E5%90%8D%E3%81%A7map%E3%82%92%E7%A7%BB%E5%8B%95%E3%81%99%E3%82%8B

本記事のスタート地点として、上記参考ページの最終的なコードを記載しておきます
(なお、地図検索には上記記事の「方法1:Google Geocoding APIを利用する。」を参考にしています。
この項目のコードは全て引用です)

```rb:routes.rb
Rails.application.routes.draw do
  root to: 'maps#index'
  resources :maps, only: [:index]
end
```

```erb:index.html.erb
<div class="gmap">
  <h2>gmap</h2>

  <!-- 地名入力用のinputを追加 -->
  <input id="address" type="textbox" value="Sydney, NSW">

  <!-- buttonをクリックしたらcodeAddressを実行　-->
  <input type="button" value="Encode" onclick="codeAddress()">
  <div id='map'>
  </div>
</div>


<script>
/*
mapを関数の外で定義(codeAddressでも使うため)
geocoderを用意
*/

  let map
  let geocoder

  function initMap() {
    // geocoderを初期化
    // 引用者注:google.maps.Geocoderインスタンスを生成
    geocoder = new google.maps.Geocoder()

    // 引用者注:google.maps.Mapインスタンスを生成することで、地図のデフォルト位置(緯度経度、ズーム)を設定
    map = new google.maps.Map(document.getElementById('map'), {
      center: {
        lat: -34.397,
        lng: 150.644
      },
      zoom: 8
    });
  }

  function codeAddress() {
    // 入力を取得
    let inputAddress = document.getElementById('address').value;

    // geocodingしたあとmapを移動
    geocoder.geocode({
      'address': inputAddress
    }, function (results, status) {
      if (status == 'OK') {
        // map.setCenterで地図が移動
        map.setCenter(results[0].geometry.location);

        // google.maps.MarkerでGoogleMap上の指定位置にマーカが立つ
        // google.maps.Markerインスタンスを生成
        var marker = new google.maps.Marker({
          map: map,
          position: results[0].geometry.location
        });
      } else {
        alert('Geocode was not successful for the following reason: ' + status);
      }
    });
  }
</script>
// 引用者の判断でAPI keyは環境変数を利用
<script src="https://maps.googleapis.com/maps/api/js?key=<%= ENV['GOOGLE_API_KEY'] %>&callback=initMap" async defer>
</script>
```

```scss:custom.scss
/*引用元ではindex.html.erbに記載*/
#map {
  height: 400px;
}
```

##前提に関する注記
参考記事では、「API keyの制限」の「ウェブサイトの制限」の項目に「localhost」のみ記載されています。AWSなどにデプロイしているアプリケーションでGoogle Mapを表示したい場合は、こちらに https://www.~~~~.com/* など、URLを追加で記載してください。
また、上記コードではAPI keyを<%= ENV['GOOGLE_API_KEY'] %>として、環境変数からひっぱっています。

##geocoderのインストール
Gemfileに記載して、geocoderというgemをインストールします。

```:Gemfile
gem geocoder
```

```
bundle install
```

##モデル・マイグレーションファイル(テーブル)を作成する

```
rails g model Map address:text latitude:float longitude:float title:text comment:text
rails db:migrate
```
※カラムに関する説明
address: 場所、住所など。フォームに入力されたこの値を元に、geocoder(gem)がlatitude(緯度)とlongitude(経度)を算出し、それぞれの値をデータベースに保存するようにします(後述)。
title・comment: 地図情報をリストとして表示する際に、タイトルとコメントをつけて保存できるようにするためのものです(もちろん任意です)。

```rb:map.rb
class Map < ApplicationRecord
  # 存在性のバリデーション
  validates :latitude, presence: true
  validates :longitude, presence: true
  validates :title, presence: true   

  # バリデーションの前に送信されたaddressの値によってジオコーディング(緯度経度の算出)を行う
  geocoded_by :address
  before_validation :geocode
end

```
ジオコーディングとは「住所や地名から座標（経度緯度）を取得したり、その逆を行ったり」(引用元:『RailsのGeocoderとあそぼ』https://qiita.com/tiara/items/573fe5f1a84ca57dabcd )を指す用語です。
また、geocoded_byについては、geocoder gemの公式( https://github.com/alexreisner/geocoder )で以下のように説明されています。
>In your model, tell geocoder where to find the object's address:
geocoded_by :address


##地図情報を保存するcreateアクションを作成する
```rb:maps_controller.rb
class MapsController < ApplicationController
  def index
    # このあとで@mapに関するフォームを作るので、Mapインスタンスを作っておきます(でないとエラーになる)
    @map = Map.new
    @maps = Map.all
  end

  def create
    @map = Map.new(map_params)
    if @map.save
      redirect_to maps_url
    else
      @maps = Map.all
      render 'maps/index'
    end
  end

  private

  # ストロングパラメーター
  def map_params
    params.require(:map).permit(:address, :latitude, :longitude, :title, :comment)
  end
end

```
```scss:custom.scss
#map {
  height: 400px;
}

.gmap input {
  border: 1px solid #000;
}

.field_with_errors {
  display: inline;
}
```
ルーティングを追加します。

```rb:routes.rb
Rails.application.routes.draw do
  root to: 'maps#index'
  resources :maps, only: [:index, :create]
end
```


## 地図情報を保存するフォームを作成、保存したデータを一覧表示する
```erb:index.html.erb
<div class="gmap">
  <h2>gmap</h2>

  <input id="address" type="textbox" value="Sydney, NSW">
  <input type="button" value="検索" onclick="codeAddress()">

  <!-- 地図情報を保存するフォーム -->
  <%= form_for @map do |f| %>
  <!-- 検索値を隠しデータとして送信-->
  <input type="hidden" name="map[address]" id="hidden_address">
  <%= f.label :title, "タイトル" %>
  <%= f.text_field :title %>
  <%= f.label :comment, "コメント" %>
  <%= f.text_field :comment %>
  <%= f.submit "保存" %>
  <% end %>
  <div id='map'>
  </div>
  <%= render @maps %>
</div>

<script>
  let map
  let geocoder

  function initMap() {
    geocoder = new google.maps.Geocoder()

    map = new google.maps.Map(document.getElementById('map'), {
      center: {
        lat: -34.397,
        lng: 150.644
      },
      zoom: 8
    });
  }

  function codeAddress() {
    let inputAddress = document.getElementById('address').value;

    geocoder.geocode({
      'address': inputAddress
    }, function (results, status) {
      if (status == 'OK') {
        map.setCenter(results[0].geometry.location);

        var marker = new google.maps.Marker({
          map: map,
          position: results[0].geometry.location
        });

        // タイトルフォームにデフォルト値として検索値を設定
        let title = document.getElementById('map_title');
        title.setAttribute("value", inputAddress);

        // 検索値を隠しデータとしてセット
        let hidden_address = document.getElementById('hidden_address');
        hidden_address.setAttribute("value", inputAddress);
      } else {
        alert('Geocode was not successful for the following reason: ' + status);
      }
    });
  }
</script>
<script src="https://maps.googleapis.com/maps/api/js?key=<%= ENV['GOOGLE_API_KEY'] %>&callback=initMap" async defer>
</script>
```

```erb:_map.html.erb
<!--mapパーシャル、render @mapsでmapパーシャルを表示-->
<div><%= map.title%></div>
<div><%= map.comment%></div>
```

## 保存された地図情報を元にピンをさすコードを追加
```erb:index.html.erb

(javascriptのみ抜粋)
<script>
  let map
  let geocoder
  // 変数を追加
  let marker

  function initMap() {
    geocoder = new google.maps.Geocoder()

    // 変数の名前をmapInstanceに変更、デフォルト位置を東京に変更
    mapInstance = new google.maps.Map(document.getElementById('map'), {
      center: {
        lat: 35.681,
        lng: 139.767
      },
      zoom: 8
    });
    
    // 保存された地図情報からピンをさす
    <% @maps.each do |map| %>
    // google.maps.LatLngインスタンスを生成
    pos = new google.maps.LatLng(
    <%=map.latitude%>, //latitude
    <%=map.longitude%> //longitude
    );
    default_marker = new google.maps.Marker({
    map: mapInstance,
    position: pos,
    icon: {
    url: ' https://maps.google.com/mapfiles/ms/icons/green-dot.png', //アイコンのURL
    scaledSize: new google.maps.Size(40, 40) //サイズ
    }
    });
    <% end %>
  }

  function codeAddress() {
    let inputAddress = document.getElementById('address').value;

    geocoder.geocode({
      'address': inputAddress
    }, function (results, status) {
      if (status == 'OK') {
        // map→mapInstanceに変更
        mapInstance.setCenter(results[0].geometry.location);

        // 既存の検索マーカーを削除
        if(marker != null){
        marker.setMap(null);
        }
        marker = null;

        // var marker → markerへ変更
        marker = new google.maps.Marker({
          //map→mapInstanceに変更
          map: mapInstance,
          position: results[0].geometry.location
        });
        
        let title = document.getElementById('map_title');
        title.setAttribute("value", inputAddress);

        let hidden_address = document.getElementById('hidden_address');
        hidden_address.setAttribute("value", inputAddress);
      } else {
        alert('Geocode was not successful for the following reason: ' + status);
      }
    });
  }
</script>
<script src="https://maps.googleapis.com/maps/api/js?key=<%= ENV['GOOGLE_API_KEY'] %>&callback=initMap" async defer>
</script>
```

##環境
Ruby2.6.3 Rails 5.2.4.2

##そのほか
ご指摘などございましたら、ぜひよろしくお願いいたします。