Google Mapを「投稿画面」と「詳細画面」の2か所に実装してみた。

# はじめに
こんにちは！現在、アウトプットの一環として個人開発を行っているんですが、エラーでボコボコにされてる今日この頃です！
という話は置いといて、本記事ではレビューの投稿機能にGoogleMapを表示させてみた話をしたいと思います！

# 実装したいこと
- 投稿ページ(new)にて住所(または地名)を入力してもらう。
- 投稿ページのGoogleMapにマーカーを落とす。
- 詳細ページ(show)のGoogleMapを表示。投稿時に指定した場所にマーカーがある。

# 開発条件
Ruby 2.5.1
Rails 5.2.3
Haml・Sass記法

# データベース
## reviewテーブル
|Column|Type|Options|
|------|----|-------|
|title|string|null: false|
|description|text|null: false|

### Association
- has_one :spot

## spotテーブル
|Column|Type|Options|
|------|----|-------|
|address|string|null: false|
|latitude|float|null: false|
|longitude|float|null: false|
|review_id|references|foreign_key: true, null: false|
### Association
- belongs_to :review

# 実装手順
それでは、実装です。実装手順は参考記事に則ってます。
## APIの利用
まず、GoogleMapのAPIを取得する必要があります。そのため、下記のリンクにアクセスしてAPIのkeyを取得してください。発行されたkeyは後々使用します。

[Google Maps Platform](https://cloud.google.com/maps-platform/?hl=ja)

今回、利用するAPIは次の２つです。

- Maps JavaScript API
- Geocoding API

この２つを有効に設定しておいてください。

## gemのインストール
必要なgemをインストールします。

```html:Gemfile
gem "gmaps4rails"
gem "geocoder"
gem "gon"
gem "dotenv-rails"
```

記入できたら`bundle install`をしてください。
それぞれの役割をしては

- GoogleMapを簡単に作成できるgem "gmaps4rails"
- 地名から緯度経度に変換できるgem "geocoder"
- JSでcontrollerの変数を使えるようにするgem "gon"
- GoogleMapAPIのkeyを隠すためのgem "dotenv-rails"

になります。gem無しでも実装はできるみたいですが、結構複雑みたいなのでgemを使用しました。

## JSを導入
### application.html.hamlを編集

```ruby:application.html.haml
%head
  《中略》
  = include_gon
  = javascript_include_tag 'application', 'data-turbolinks-track': 'reload'
%body
  = yield
  %script{src: "https://maps.googleapis.com/maps/api/js?key=#{ENV["GOOGLE_MAP_KEY"]}&callback=initMap"}
  %script{src: "//cdn.rawgit.com/mahnunchik/markerclustererplus/master/dist/markerclusterer.min.js"}
  %script{src: "//cdn.rawgit.com/printercu/google-maps-utility-library-v3-read-only/master/infobox/src/infobox_packed.js", type:"text/javascript"}
```

`%head`には`gem "gon"`を使えるようにするための記述をします。
また、`%body`にはJSを使うための記述をしています。はじめは`%script`も`%head`に記述していましたが、GoogleMapが表示されないというエラーが起きたので`%body`の最後に記述しました。おそらくは読み込みの順番の問題かと思います。
`ENV["GOOGLE_MAP_KEY"]`には`.env`ファイルに隠したAPIkeyを入れています。

```html:.env
GOOGLE_MAP_KEY = "あなたが取得したkeyを記述してください"
```

### underscore.jsを作成

その後、app/assets/javascripts下にunderscore.jsを作成してください。作成したファイルに次のリンク先のコードをコピペして貼り付けるらしい。

[underscore.js](https://underscorejs.org/underscore-min.js)

### application.jsで読み込み設定

application.jsに次の記述を追記します。

```javascript:application.js
//= require underscore
//= require gmaps/google
```
## modelを編集
modelを次のように編集していきます。

```ruby:review.rb
class Review < ApplicationRecord
  has_one :spot, dependent: :destroy
  accepts_nested_attributes_for :spot
end
```

```ruby:spot.rb
class Spot < ApplicationRecord
  belongs_to :review

  geocoded_by :address
  after_validation :geocode
end
```

## viewを編集
投稿ページを作成します。GoogleMapを表示させる記述以外は省略してます。

```ruby:new.html.haml
= form_with(model: @review, local: true, multipart: true) do |f|
  .spot
    = f.fields_for :spot do |s|
      = s.label :address, "レビュー場所(Google Mapで検索)", class: 'spot__title'
      = s.text_field :address, placeholder: "スポットを入力", id: "address", class: 'spot__text'
    %input{onclick: "codeAddress()", type: "button", value: "検索する"}
    .map{id: "map", style: "height: 320px; width: 640px;"}
```
次に、投稿したレビューを表示させるページを作成します。

```ruby:show.html.haml
.show
  .show__address
    = @review.spot.address
  .show__maps{id: "show_map", style: "height: 320px; width: 400px;"}
```

## controllerを編集
controllerも編集しておきます。

```ruby:reviews_controller.rb
def new
  @review = Review.new
  @review.spot.build
end

def create
  @review = Review.new(review_params)
  if @review.save
    redirect_to root_path
  else
    redirect_to new_review_path
  end
end

def show
  @review = Review.find(params[:id])
  @lat = @review.spot.latitude
  @lng = @review.spot.longitude
  gon.lat = @lat
  gon.lng = @lng
end

private

def review_params
  params.require(:review).permit(
    :title,
    :description,
    spot_attributes: [:address]
  )
end
```

showアクションで記述している

> @lat = @review.spot.latitude
> @lng = @review.spot.longitude
> gon.lat = @lat
> gon.lng = @lng

では、controllerで定義した`@lat`と`@lng`の変数をJavaScriptでも扱えるように、それぞれ`gon.lat`と`gon.lng`に代入しています。

## JavaScriptの編集

いよいよJavaScriptでGoogleMapを表示させていきます。

```javascript:googlemap.js
let map //変数の定義
let geocoder //変数の定義

function initMap(){ //コールバック関数
  geocoder = new google.maps.Geocoder() //GoogleMapsAPIジオコーディングサービスにアクセス
  if(document.getElementById('map')){ //'map'というidを取得できたら実行
    map = new google.maps.Map(document.getElementById('map'), { //'map'というidを取得してマップを表示
      center: {lat: 35.6594666, lng: 139.7005536}, //最初に表示する場所（今回は「渋谷スクランブル交差点」が初期値）
      zoom: 15, //拡大率（1〜21まで設定可能）
    });
  }else{ //'map'というidが無かった場合
    map = new google.maps.Map(document.getElementById('show_map'), { //'show_map'というidを取得してマップを表示
      center: {lat: gon.lat, lng: gon.lng}, //controllerで定義した変数を緯度・経度の値とする（値はDBに入っている）
      zoom: 15, //拡大率（1〜21まで設定可能）
    });
  
    marker = new google.maps.Marker({ //GoogleMapにマーカーを落とす
      position:  {lat: gon.lat, lng: gon.lng}, //マーカーを落とす位置を決める（値はDBに入っている）
      map: map //マーカーを落とすマップを指定
    });
  }
}

function codeAddress(){ //コールバック関数
  let inputAddress = document.getElementById('address').value; //'address'というidの値（value）を取得

  geocoder.geocode( { 'address': inputAddress}, function(results, status) { //ジオコードしたい住所を引数として渡す
    if (status == 'OK') {
      let lat = results[0].geometry.location.lat(); //ジオコードした結果の緯度
      let lng = results[0].geometry.location.lng(); //ジオコードした結果の経度
      let mark = {
          lat: lat, //緯度
          lng: lng  //経度
      };
      map.setCenter(results[0].geometry.location); //最も近い、判読可能な住所を取得したい場所の緯度・経度
      let marker = new google.maps.Marker({
          map: map, //マーカーを落とすマップを指定
          position: results[0].geometry.location //マーカーを落とす位置を決める
      });
    } else {
      alert('該当する結果がありませんでした');
    }
  });   
}
```
今回は、地図を表示させる場所が２か所あり、それぞれ別の場所をマップの中心にしたかったので、initMap関数内で「指定の`id`の有無」による条件分岐を設けることで対応しています。

JavaScriptについては知識が浅く、コメントアウトで解説した部分で解釈の違いがあるかもしれません。その際にはご指摘頂けると有り難いです。

# おわりに
GoogleMapは多くのサイトで利用されているので、どのように実装すればいいかを多少なり知ることができて良かったです！GoogleMapAPIにはまだまだ使ってない機能が沢山あるので、時間があれば色々チャレンジしていきたいと思います！！

# 参考記事
[gonを使ったRailsとJavascriptの連携について]
(https://qiita.com/s_nakamura/items/5d153f7d9db1b1190296)
[ジオコーディング備忘録]
(https://qiita.com/SignHack/items/d6df5a5abf72e6feffe9)
[RailsでGoogleMapを表示させる(gem 'gmaps4rails'の使い方)](https://qiita.com/nakanoyoshiki/items/af9f37e9d2653518d6b0)
[Rails Google Mapを表示させる方法]
(https://matto.work/rails-google-map%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%95%E3%81%9B%E3%82%8B%E6%96%B9%E6%B3%95-230.html)