Rails ポートフォリオにGoogleMap APIを導入 複数の住所を表示

# はじめに
ポートフォリオにGoogleMap APIを導入しました
一つの投稿に複数の住所が登録されている可能性があるため
複数の住所が表示できるように実装しました！
アウトプットします✍️

※旅行やデートのプランを共有するアプリなので、投稿機能はPostではなくPlanを使用しています！
また、Plansの子要素としてPlanDetailsモデルがあり、そこにaddressカラムがあります！

# GoogleMap APIキーとGeocodingAPIを取得
導入手順は割愛させていただきます！
こちらの記事に詳しく手順が紹介されていましたので、
ご確認ください！

https://qiita.com/nagase_toya/items/e49977efb686ed05eadb

https://qiita.com/mattan5271/items/1b784dbbe5f1c336ac70

# ER図

![スクリーンショット 2024-06-17 21.15.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782860/6c2e8873-05c4-4158-9763-da8bc09cf87e.png)

# 事前準備
#### gemをインストール

```Gemfile
:
gem 'dotenv-rails'
gem 'geocoder' 
```

```
$ bundel install
```

Javascriptで環境変数を読み込めるようにするため以下のコマンドを実行
```
$ yarn add dotenv
```

#### 先ほど取得したAPIキーをenvファイルに記述

```.env
Maps_API_Key="ここにAPIキーを記述"
Geocoding_API_Key="ここにAPIキーを記述"
```

#### Javascriptで環境変数を読み込めるように記述
```config/webpack/environment.js
// この行を追加
require("dotenv").config(); 

const { environment } = require('@rails/webpacker')

module.exports = environment

const webpack = require('webpack')
environment.plugins.prepend(
  'Provide',
  new webpack.ProvidePlugin({
    $: 'jquery',
    jQuery: 'jquery',
    Popper: 'popper.js'
  })
)
```

#### geocoderの設定
geocoderは位置情報を扱うGemです
住所カラムから緯度、経度を変換する機能を使用するため導入します

```
$ rails g geocoder:config
```
上記のコマンドを実行するとファイルが生成されます
もし生成されない場合は、手動作成しても問題ありません

```config/intializers/geocoder.rb
Geocoder.configure(
  lookup: :google,
  use_https: true,
  api_key: ENV["Geocoding_API_Key"],
  units: :km
)
```
コメントアウトを戻すなどして、上記の設定をしましょう

# 住所・緯度・経度カラムの追加

PlanDetailモデルに住所と、緯度と経度のカラムを追加します

```
$ rails g migration AddGeocodingColumnToPlanDetails address:string latitude:float longitude:float
```

```xxxxx_add_geocoding_colum_to_plan_details.rb
class AddGeocodingColumnToPlanDetails < ActiveRecord::Migration[6.1]
  def change
    add_column :plan_details, :address, :string, null: false, default: ""
    add_column :plan_details, :latitude, :float, null: false, default: 0
    add_column :plan_details, :longitude, :float, null: false, default: 0
  end
end
```

```
$ rails db:migrate
```

# PlanDetailモデルでgeocodingを扱えるようにする
```app/models/plan_detail.rb
class PlanDetail < ApplicationRecord

  # アソシエーション
  belongs_to :plan

  # Google.map API
  # addressカラムを緯度経度に変換
  geocoded_by :address
  # latitudeカラム・longitudeカラムに緯度・経度の値を入力
  after_validation :geocode

end
```

# 入力フォームに住所を登録できるようにする
```new.html.erb
<%= form_with model: @plan do |f| %>
:
  <%= plan_detail.text_field :address, class: "form-control mb-3", placeholder: "住所" %>
:
<%= f.submit '投稿', class: 'btn btn-warning mx-auto px-4' %>
```

# viewにmapを表示させるため記述
今回は投稿画面に表示させたいため、show.html.erbに記述します！

```show.html.erb
<div id="map"></div>

<script>
  function initMap(){
    <% map = true %>
    <% @plan_details.each do |pd| %>
      <% next if pd.address.blank? %>
    let mapPosition<%= pd.id %> = {lat: <%= pd.latitude %> , lng: <%= pd.longitude %> };

    <% if map %>
    let map = new google.maps.Map(document.getElementById('map'), {
      zoom: 12,
      center: mapPosition<%= pd.id %>
    });
    let transitLayer = new google.maps.TransitLayer();
    transitLayer.setMap(map);
    <% map = false %>
    <% end %>

    let contentString<%= pd.id %> = '【住所】<%= pd.address %>';
    let infowindow<%= pd.id %> = new google.maps.InfoWindow({
      content: contentString<%= pd.id %>
    });

    let marker<%= pd.id %> = new google.maps.Marker({
      position: mapPosition<%= pd.id %>,
      map: map,
      title: contentString<%= pd.id %>
    });

    marker<%= pd.id %>.addListener('click', function(){
      infowindow<%= pd.id %>.open(map, marker<%= pd.id %>);
    });

    <% end %>
  }
</script>

<script src="https://maps.googleapis.com/maps/api/js?key=<%= ENV['Maps_API_Key'] %>&callback=initMap" async defer></script>
```

![スクリーンショット 2024-06-17 22.20.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782860/49016fd1-9c12-aa28-4a0d-a9a423181851.png)

`<div id="map"></div>`
mapを表示させたい場所に記述します
大きさを変えたい場合はbootstrapやcssを使用して修正してください
例)`<div id="map" class="col-sm-12 col-md-10 col-lg-8 mx-auto" style="height: 300px;"></div>`

```show.html.erb
<% @plan_details.each do |pd| %>
  <% next if pd.address.blank? %>
    let mapPosition<%= pd.id %> = {lat: <%= pd.latitude %> , lng: <%= pd.longitude %> };
:
<% end %>
```

`<% @plan_details.each do |pd| %>`
viewにJavascriptを記述しているため、railsの記述をすることができます
今回はplan_detailの数だけaddressがあるため（正確には、必須ではないためaddressはない可能性もあります)each文を使用してaddress分のピンを立てます

`<% next if pd.address.blank? %>`
each分の中で使用できるif文の書き方です
addressがブランクの場合飛ばして次にいきます（plan_detail全てにaddressが入っているわけではないため）<% end %>の記述は不要です！

`let mapPosition<%= pd.id %> = {lat: <%= pd.latitude %> , lng: <%= pd.longitude %> };`
`let mapPosition<%= pd.id %>`という形で、各プラン詳細ごとに異なる位置情報（緯度・経度）をを定義しています`lat`にはeach文から緯度を取得し、`lng`には経度を取得

```show.html.erb
    let map = new google.maps.Map(document.getElementById('map'), {
      zoom: 12,
      center: mapPosition<%= pd.id %>
    });
```

`new google.maps.Map`
新しい地図オブジェクトを作成するための記述です

`(document.getElementById('map')`
HTMlドキュメント内のid属性が map である要素を取得します
今回では`<div id="map"></div>`の部分です

`center: mapPosition<%= pd.id %>`
先ほど取得した mapPosition を地図の中心に設定します

```show.html.erb
    let transitLayer = new google.maps.TransitLayer();
    transitLayer.setMap(map);
```

`google.maps.TransitLayer`
地図上に公共交通機関の情報（電車やバスの路線、停留所)を追加するためのクラス

`transitLayer.setMap(map);`
先に作成した mapオブジェクトに公共交通機関の情報が表示される

```show.html.erb
<% map = true %>
  <% @plan_details.each do |pd| %>
    
<% if map %>
: １回目に取得したaddressをマップの中心にする記述
<% map = false %>
<% end %>
:
  <% end %>
```
each文の最初の1回だけ地図を初期化し、その後は地図の中心を変更しないための記述
`map = true`の時のみ`center:`が実行され、さいごに`map = false`に変更することにより再度中心を変更することを防いでいます！

```show.html.erb
    let contentString<%= pd.id %> = '【住所】<%= pd.address %>';
    let infowindow<%= pd.id %> = new google.maps.InfoWindow({
      content: contentString<%= pd.id %>
    });
```

`let contentString<%= pd.id %> = '【住所】<%= pd.address %>';`
マーカーをクリックしたときに出てくるウィンドウに表示する情報を格納しています
今回はpdから取得したaddressです

`let infowindow<%= pd.id %> = new google.maps.InfoWindow({`
情報ウィンドウオブジェクトを格納する変数に、ウィンドウを作成し代入しています

`content: contentString<%= pd.id %>`
ここで、先ほど取得したpdのaddressをウィンドウに表示されるよう指定しています

```show.html.erb
    let marker<%= pd.id %> = new google.maps.Marker({
      position: mapPosition<%= pd.id %>,
      map: map,
      title: contentString<%= pd.id %>
    });
```

`new google.maps.Marker`
この記述でマーカーを作成しています（赤いピンの部分です）
![スクリーンショット 2024-06-17 23.25.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782860/f8eedcde-4d25-aa39-039b-f92bc23a6ce9.png)

`position: mapPosition<%= pd.id %>,`
マーカーの位置を指定します
ここでは、先に作成した mapPosition<%= pd.id %> を指定しています

`map: map,`
マーカーやウィンドウなどの地図要素をどのmapインスタンスに関連付けるかを指定しています
今回は先ほど作成した map です

`title: contentString<%= pd.id %>`
マーカーのタイトルを設定します
ここでは、先に作成した contentString<%= pd.id %> を指定しています

```show.html.erb
    marker<%= pd.id %>.addListener('click', function(){
      infowindow<%= pd.id %>.open(map, marker<%= pd.id %>);
    });
```

`marker<%= pd.id %>.addListener('click', function(){…})`
マーカーがクリックされたときに指定された関数が実行されます

`infowindow<%= pd.id %>.open(map, marker<%= pd.id %>);`
マーカーがクリックされたときに指定された関数そのものです
今回はクリックされたときに情報ウィンドウが表示されます

# さいごに
複数の住所がある場合の、GoogleMap表示方法でした！
難しかったです😭

# 参照記事
参考にさせていただきました！
ありがとうございました！

https://naskanoheya.com/catch-up/g-map/

https://zenn.dev/airiin/articles/d1d80f3e842646#%E7%9B%AE%E6%A8%99

https://qiita.com/da1002/items/816c65cc59a0cae9836d