[Rails6] GoogleMapを使って、マーカーが立っている位置をDBに保存する！

前提
-
・Railsのアプリケーションであること
※私のRailsのバージョンは6.0.3です
・記事作成機能が存在すること
・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

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

目標
-
記事作成の際、マップにマーカーを1つだけ立てられるようにし、その位置情報をDBに保存すること。
マーカーの立て方としては以下を想定。

・マップをクリック
・位置を検索（GeocodingAPIを使用）

※記事最下部に完成版のgifあり

それではやっていきましょう！


gemのインストール　※いらない方はスキップしてください
-


```ruby:Gemfile
gem 'geocoder'
gem 'gon'
```

※この記事においてはこの2つのgemは使用しません(次の記事で使用予定)。しかし、これらのgemが入っているという前提で進めていきます。

geocoderは便利なメソッドが豊富です。
RailsでGoogleMapを使用のであれば入れておいたほうが良いです。
gonはController内でセットした変数をJavascript内で使う事ができます。
DBに保存した位置情報を記事詳細画面で表示させる際に非常に便利です。

geocoder
https://github.com/alexreisner/geocoder

gon
https://github.com/gazay/gon

これらを記述した後、bundle install します。

マップの情報保存用テーブル作成
-

指定した緯度、経度を保存するテーブルを作成します。
まず、マイグレーションファイルを作成しましょう。
私の場合は、記事と地図を関連付けさせたいのでこのようになっています。
マップ単品の方は
 t.references :article, null: false, foreign_key: true
の部分はいらないかと思います。

```ruby:timestamp_xxx.rb（マイグレーションファイル）
class CreateMaps < ActiveRecord::Migration[6.0]
  def change
    create_table :maps do |t|
    
    t.references :article, null: false, foreign_key: true
    t.string :address
    t.float :latitude
    t.float :longitude

    t.timestamps
    end
  end
end
```

その後、rails db:migrate します。

モデルの関連付け
-
関連付けさせなくて良い方はスキップしても大丈夫です。

```ruby:map.rb
class Map < ApplicationRecord
  belongs_to :article
  geocoded_by :address
  after_validation :geocode
end
```

```ruby:article.rb
has_one :map, dependent: :destroy
```


マップの表示
-

表示だけならこれだけでできると思います。

```html:〇〇.html.erb
<div id='map'></div>   
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=<%= ENV['GOOGLE_MAP_API'] %>&callback=initMap" async defer></script>   
<script>
let map

function initMap (){
  myLatLng = {lat: 35.68090045006303, lng: 139.76690798417752}
  map = new google.maps.Map(document.getElementById('map'), {
  center: myLatLng,
  zoom: 12
  });
</script>
    
```


マップ上でクリックしたところにマーカーを立てることができるようにする
-
記事作成の際に、マップ上でクリックした箇所にマーカーを立てられるようにします。
また、マーカーが立っている箇所の経度、緯度を保存できるようにします。
ついでにマーカーを削除できるボタンも追加しました。


```erb:_article_form.html.erb

<div id="article_form" class="field">
  <%= f.label :title, "タイトル" %>
  <%= f.text_area :title %>
  マップ
  <div id='map'></div>
  <%= f.fields_for :map, @article.build_map do |map| %>   
    <%= map.hidden_field :latitude %>
    <%= map.hidden_field :longitude %>
  <% end %>
  <%= f.label :content, "本文" %>
  <%= f.rich_text_area :content,placeholder:"入力してください" %>
  <%= f.label :tag_list %>
  <%= f.file_field :image, accept: "image/jpeg,image/gif,image/png" %>
</div>
<div id="article_button"><%= f.submit "投稿！",class:"btn"%></div>
<% end %>
<button id="del" class="btn" onclick="deleteMarker();">マーカー削除</button>

<script>
var marker
var myLatLng
var map
var map_lat
var map_lng

function initMap(){
   myLatLng = {lat: 35.68090045006303, lng: 139.76690798417752}
   marker = new google.maps.Marker();
   map_lat = document.getElementById('article_map_latitude')
   map_lng = document.getElementById('article_map_longitude')
   map = new google.maps.Map(document.getElementById('map'), {
  center: myLatLng,
  zoom: 8
  });

 google.maps.event.addListener(map, 'click', mylistener);

    //クリックしたときの処理
  function mylistener(event){
    //markerの位置を設定
    //event.latLng.lat()でクリックしたところの緯度を取得
    marker.setPosition(new google.maps.LatLng(event.latLng.lat(), event.latLng.lng()));
    //marker設置
    marker.setMap(map);    
    map_lat.value = event.latLng.lat();
    map_lng.value = event.latLng.lng();
  }
}
function deleteMarker(){
  marker.setMap(null);
  map_lat.value = "";
  map_lng.value = "";
}



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

下記のコードによってarticleと関連付けたmapにデータを渡すことができます。


```erb
<%= f.fields_for :map, @article.build_map do |map| %>
<%= map.hidden_field :latitude %>
<%= map.hidden_field :longitude %>
<% end %>
```

また、これらに値を入れるコードはこの部分です。

```js:
map_lat = document.getElementById('article_map_latitude')
map_lng = document.getElementById('article_map_longitude')
//↑map.hidden_fieldによって生成された<input>のidです。
//人によって違うかもしれないのでブラウザの開発者モードで確認してください。

//〜〜省略〜〜

map_lat.value = event.latLng.lat();
map_lng.value = event.latLng.lng();
```


そして、値をコントローラに渡すため、StrongParameterの設定も必要です。
binding.pryなどで、どのようなparamsが渡ってきているか確認しておいたほうが良いかと思います。

```ruby:article_controller.rb
    def article_params
      params.require(:article)
            .permit(:content, :title,
             map_attributes: [:id, :latitude, :longitude])
    end

```

これを保存するコードはこちらです。

```ruby:article_controller.rb
def create
  @article = current_user.articles.build(article_params)
  if @article.save
    latitude = params[:article][:map][:latitude]
    longitude = params[:article][:map][:longitude]
    unless latitude.empty && longitude.empty?
      @map = @article.build_map(
        latitude: latitude,
        longitude: longitude
      )
      @map.save
    end
#〜〜〜〜〜〜省略〜〜〜〜〜〜〜〜〜〜〜
end
```

位置検索でもマーカーを立てることができるようにする
-


この場合、GoogleMapAPIだけでなく、Geocoding APIも必要になってきます。
紆余曲折あり、最終的に完成したのがこちらです。


```erb:
<div class="row">
  <div class="col s12 m10 push-m1">
    <div class="card">
      <div class="card-content article-card">        
        <%= form_with(model:@article,local: true,class:"margin-30") do |f|%>
        <%= render 'shared/error_messages',object: f.object %>        
          <div id="article_form" class="field">
            <div class="input-field">              
              <%= f.text_field :title,class: "materialize-textarea" %>
              <%= f.label :title, "タイトル" %>
            </div>
            <div class="input-field">              
              <%= f.text_field :tag_list, value: @article.tag_list.join(','), class: "materialize-textarea" %>
              <%= f.label :tag_list,"タグ ※コンマ区切りで記入してください" %>
            </div>

        <!-- 追加したイベント -->　　　　

            <div class="input-field">
              <input type="text" id="address" placeholder="地名、施設名などを入力するか、地図をクリックしてマーカーを立ててください">
              <label>場所</label>
              <a class="btn" onclick="codeAddress()">地図検索</a>
              <a id="del" class="btn marker-delete right" onclick="deleteMarker()">
              <i class="fas fa-map-marker-alt fas-2x" style="color:red"></i>削除
              </a>
            </div>

        <!-- ここまで -->

            <div id='map' class="margin-t-b-14"></div>          
            <%= f.fields_for :map, @article.build_map do |map| %>   
            <%= map.hidden_field :latitude %>
            <%= map.hidden_field :longitude %>
            <% end %>            
              <%= f.rich_text_area :content,placeholder:"入力してください",class: "materialize-textarea" %>
              <%= f.label :content, "本文" %>
            <%= f.label :thumbnail, "サムネイル" %>
            <%= f.file_field :thumbnail, accept: "image/jpeg,image/gif,image/png",class:"file_field" %>
            <div id="article_button"><%= f.submit "投稿！",class:"btn col s6 m6 article-form-btn push-s3 push-m3"%></div>
          </div>
        <% end %> 
      </div>
    </div>
  </div>
</div>

<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=<%= ENV['GOOGLE_MAP_API'] %>&callback=initMap" async defer></script>
<script>
var marker
var myLatLng
var map_lat
var map_lng
var geocoder
var map_result
let map

function initMap (){
  myLatLng = {lat: 35.68090045006303, lng: 139.76690798417752}
  map_lat = document.getElementById('article_map_latitude')
  map_lng = document.getElementById('article_map_longitude')
  marker = new google.maps.Marker();

  map = new google.maps.Map(document.getElementById('map'), {
  center: myLatLng,
  zoom: 12
  });

google.maps.event.addListener(map, 'click', mylistener);

     //クリックしたときの処理
function mylistener(event){
    marker.setPosition(new google.maps.LatLng(event.latLng.lat(), event.latLng.lng()));
    marker.setMap(map);    
    console.log(event.latLng.lat(), event.latLng.lng());
    map_lat.value = event.latLng.lat();
    map_lng.value = event.latLng.lng();
  }
}

  function deleteMarker(){
    marker.setMap(null);
    map_lat.value = "";
    map_lng.value = "";
}

//追加した関数

function codeAddress(){
  geocoder = new google.maps.Geocoder()
  let inputAddress = document.getElementById('address').value;
  geocoder.geocode( { 
    'address': inputAddress,
     'region': 'jp'
    }, function(results, status) {
    if (status == 'OK') {
      map.setCenter(results[0].geometry.location);
      map_result = results[0].geometry.location;
      map_lat.value = map_result.lat();
      map_lng.value = map_result.lng();
      marker.setPosition(new google.maps.LatLng(map_result.lat(), map_result.lng()));
      marker.setMap(map);
      console.log(map_lat.value,map_lng.value);
    } else {
      alert('該当する結果がありませんでした');
      initMap();
    }
  });   
}
</script>
```

随分長いコードになってしまいました。
しかし、実際は関数とイベントが1つ増えただけなのでその部分を見てもらえればわかるかと思います！
これによって、マップをクリックor位置を検索することで地図上にマーカーを立て、その位置情報をDBに保存することができるようになりました！
これらを利用する方法についてはまた後日記事を書こうかと思います。



![map-demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/970702/1dd4b2db-2cbf-1bf1-392b-e9ae84542063.gif)