[Rails]google mapのAPIを使って地図を表示させる方法

#この記事の目的
自分の投稿したワード（住所など）をフォームに入れて、google mapで検索できるようにする。（今回は例としてカフェの名前を検索できるようにする）

#環境
ruby 2.5.3
rails 5.2.1

(筆者はbundlerを使ってrailsはローカルでインストールして使用が、今回はグローバルでインストールした時のコマンドの打ち方で書く）

#googleAPIを取得

https://console.developers.google.com/?hl=ja
このホームページに登録してAPIを取得する（調べれば出るので今回はカット）

#アプリ作成

```
rails new cafe_map
```

モデルは以下のようにする。
（id,created_at,update_atカラムは省略）

・topicモデル（contentカラムはなくても問題ない）

| カラム名  | 　型 | 
|:-----------------:|:------------------:|
| cafe_name         |  string |  
| content          |   string | 

・mapモデル

| カラム名  | 　型 | 
|:-----------------:|:------------------:|
| topic_id         |  integer |  
| latitude          |   float | 
| longitude    | float |
| address | string |


latitudeは経度。longitudeは緯度という意味。
モデルの作り方はそれぞれ　

```
rails g model topic cafe_mame:string content:string
rails g scaffold map address:string latitude:float longitude:float
```

mapモデルはscaffoldを使って作成する。

modelのアソシエーション

```ruby:topic.rb
has_many :maps
```

```model.rb
belongs_to :topic
```

#geocoderをインストール
Gemfileに以下を追加。

 ```
gem 'geocoder'
```
geocoderは住所から緯度と経度を割り出して取得してくれる。
bundle installを忘れずに。

geocoderを使えるようにするため以下を追加。

```ruby:map.rb
geocoded_by :address
after_validation :geocode, if: :address_changed?
```

これで住所（address）からlatitudeとlongitudeを作成できる。

#ルート、コントローラー、ビュー

流れとして、
topic/newでtopicを作成 =>　topic/:idでそのtopicを表示　=> topic/topic_id/maps/newでtopicに紐づいたmapを作成 => /topics/:topic_id/maps/:idでmapを表示
という風に作る。

・ルート

```
resources :topics do
    resources :maps
  end
```

ネストして設定。これでtopicのidは、mapのtopic_idに紐付く。（簡単に言えば、topic.id == map.topic_id）

・コントローラー

```ruby:topics_controller.rb
def index
    @topics = Topic.all
  end

  def new
    @topic = Topic.new
  end

  def show
    @topic = Topic.find(params[:id])
  end

  def create
    @topic = current_user.topics.new(topic_params)
    if @topic.save
      redirect_to root_path
    else
      render 'topic/new'
    end
  end

  private
    def topic_params
      params.require(:topic).permit(:cafe_name,:content)
    end

```

topicは投稿、一覧の基本的な部分のみ作成。
次はmap。

```ruby:maps_controller.rb
  before_action :set_map, only: [:show, :edit, :update, :destroy]
  before_action :set_topic, only: [:index, :new, :edit, :create, :destroy]
  # GET /topics/:topic_id/maps
  # GET /maps.json
  def index
    @maps = @topic.maps.all
  end

  # GET /topics/:topic_id/maps/:id
  # GET /maps/1.json
  def show
  end

  # GET /topics/:topic_id/maps/new
  def new
    @map = Map.new
  end

  # GET /topics/:topic_id/maps/:id/edit
  def edit
  end

  # POST /topics/:topic_id/maps
  # POST /maps.json
  def create
    @map = @topic.maps.new(map_params)
    respond_to do |format|
      if @map.save
        format.html { redirect_to  topic_map_path(topic_id: @map.topic_id,id: @map.id), notice: 'Map was successfully created.' } #:topic_idと:idを2つの引数でそれぞれ指定
        format.json { render :show, status: :created, location: topic_map_path(topic_id: @map.topic_id,id: @map.id) }
      else
        format.html { render :new }
        format.json { render json: @map.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /topics/:topic_id/maps/:id
  # PATCH/PUT /maps/1.json
  def update
    respond_to do |format|
      if @map.update(map_params)
        format.html { redirect_to topic_map_path(topic_id: @map.topic_id,id: @map.id), notice: 'Map was successfully updated.' }
        format.json { render :show, status: :ok, location: topic_map_path(topic_id: @map.topic_id,id: @map.id) }
      else
        format.html { render :edit }
        format.json { render json: @map.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /topics/:topic_id/maps/:id
  # DELETE /maps/1.json
  def destroy
    @map.destroy
    respond_to do |format|
      format.html { redirect_to topic_maps_path(topic_id: @topic.id), notice: 'Map was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_map
      @map = Map.find(params[:id])
    end

    def set_topic
      @topic = Topic.find_by(id: params[:topic_id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def map_params
      params.require(:map).permit(:address,:latitude,:longitude,:topic_id)
    end
```

topicに紐づいたmapを作成、表示、編集、削除などできるように設定。

最後にビュー。
scaffoldでほとんど生成されているが多少変更する。

```ruby:maps.show.html.rb

<script type="text/javascript">
    function initMap() {

        var test = {lat: <%= @map.latitude %>, lng: <%= @map.longitude %>};
        var map = new google.maps.Map(document.getElementById('map'), {
          center: test,
          zoom: 15
        });

        var transitLayer = new google.maps.TransitLayer();
        transitLayer.setMap(map);

        var contentString = '住所：<%= @map.address %>';
        var infowindow = new google.maps.InfoWindow({
            content: contentString
        });

        var marker = new google.maps.Marker({
            position:test,
            map: map,
            title: contentString
        });

        marker.addListener('click', function() {
            infowindow.open(map, marker);
        });
    }
</script>
<script async defer
        src="https://maps.googleapis.com/maps/api/js?v=3.exp&key=[google map apiのキー]&callback=initMap">
</script>

<p id="notice"><%= notice %></p>

<p>
  <strong>Address:</strong>
  <%= @map.address %>
</p>

<p>
  <strong>Latitude:</strong>
  <%= @map.latitude %>
</p>

<p>
  <strong>Longitude:</strong>
  <%= @map.longitude %>
</p>

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

<%= link_to 'Edit', edit_topic_map_path(topic_id: @map.topic_id,id: @map.id) %> |
<%= link_to 'Index', topic_maps_path %>
```
[google map apiのキー ]のところに取得してapiのキーを書く。google mapが表示されるのは、最後の方の
`<div id="map"></div>`

で表示されている。cssで
```#map{
height: 400px;
width: 400px;
}
```
などと設定しないと地図が表示されないので注意。（%を使って高さや幅を指定すると、親ブロックが高さや横を指定してないと表示されないので、最初はpxで設定するのがおすすめ。）

他のmapsのビューで、link_toの設定でエラーが出るが、rake routes で確認して直せば良いので省略。

最後にformを変更する。

mapのnewアクションとeditアクションの時のフォームは、renderで表示している。この時form_forは二つの引数(@topicと@map)を持ってきてcreateとupdateで分けるようにした。（form_forは引数が既存かどうかでアクションを振り分ける）

```ruby:_form.html.erb
<%= form_for [@topic,@map] do |form| %>
  <% if @map.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@map.errors.count, "error") %> prohibited this map from being saved:</h2>

      <ul>
      <% @map.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <% if @map.address == nil %>
      <%= form.label :address %>
      <%= form.text_field :address,:value => @topic.cafe_name %>
    <% else %>
      <%= form.label :address %>
      <%= form.text_field :address,:value => @map.address %>
    <% end %>
  </div>


  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>
```
text_fieldにvalueを上記のように設定することで、新規の時はカフェの名前を入力されたままの状態にする。

これで完成。


#参考記事

https://qiita.com/enzen/items/9a919a75ebf0a34e7b91

こちらの記事を参考に作りました。

また何か間違いや質問があればぜひお願いします。