GoogleMapAPIを用いて、Railsで現在地やフォーム内に書いた地名などから近い場所を取得し、表示する方法

##環境
- Ruby 2.6.6
- Rails 6.1.0
- Docker 20.10.2

##前提条件
- gem geocoderを導入済み
- Geocoding API・Maps JavaScript API・GeolocationAPIを導入済み

##この記事の対象の方
- geocoderのnearメソッドよりも自由度を高く取得した投稿を並び替えたい人
- 現在地から一定の範囲内にある投稿を取得 + 検索フォームに書いた地名や住所などから検索のどちらも行いたい人

##コード
###モデルの設定
:latitudeと:longitudeはfloat型にした。

```spot.rb
class Spot < ApplicationRecord
  reverse_geocoded_by :latitude, :longitude
  after_validation :reverse_geocode
  validates :address, presence: true, length: { maximum: 100 }
  validates :latitude, presence: true
  validates :longitude, presence: true

  class << self
    def within_box(distance, latitude, longitude)
      distance = distance
      center_point = [latitude, longitude]
      box = Geocoder::Calculations.bounding_box(center_point, distance)
      self.within_bounding_box(box)
    end
  end
end
```

###ルーティングの設定

```routes.rb
Rails.application.routes.draw do
  root 'spots#index'
  get 'search', to: 'spots#search'
end
```

###viewの設定

```index.html.erb
<%= form_with url: search_path, :method => 'get' do |f| %>

    <%= f.text_field :location, placeholder: "住所や行きたい場所を入力してください。" %>
    <%= f.select :keyword, [
      ['距離が近い順', 'near']
    ] %>
    <%= f.submit '検索', class: "button p-2" %>
  <% end %>
  <button id="get_current_spot" type="button" class="button p-2">現在地から近い順に駐輪場を取得</button>

<script>
  function geoFindMe() {
    function success(position) {
      const latitude  = position.coords.latitude;
      const longitude = position.coords.longitude;
      document.getElementById('location').value = `${latitude},${longitude}`;
    }

    function error() {
      alert('エラーが発生しました。')
    }

    if(!navigator.geolocation) {
       alert('Geolocation is not supported by your browser');
    } else {
      navigator.geolocation.getCurrentPosition(success, error);
    }
  }

  document.querySelector('#get_current_spot').addEventListener('click', geoFindMe);
</script>
```

```search.html.erb
  <div class="flex flex-wrap justify-between w-11/12 mx-auto">
    <% if @spots.any? %>
      <% @spots.each do |parking| %>
        <%= link_to image_tag(spot.image.thumb.url), spot, id: "detail-" + spot.id.to_s, class: 'my-5 mx-auto spot-card' %>
      <% end %>
    <% else %>
      <p class="text-2xl sm:text-3xl mx-auto my-10">検索結果は見つかりませんでした。</p>
    <% end %>
 </div>
```

###コントローラーの設定

```spots_controller.rb
class ParkingsController < ApplicationController
  def index
  end

  def search
    results = Geocoder.search(params[:location])
    if results.empty?
      flash[:notice] = "検索フォームに文字が入っていないか、位置情報を取得できる値でない可能性があります。"
      redirect_to root_path
    else
      selection = params[:keyword]
      latitude = results.first.coordinates[0]
      longitude = results.first.coordinates[1]
      spots = Spot.within_box(20, latitude, longitude)
      case selection
      when 'near'
        @parkings = Parking.near(results.first.coordinates, 20).page(params[:page]).per(10)
      else
        @spots = spots
      end
    end
  end

end

```

##コードの解説
###モデル部分
実はgeocoderではnearというメソッドがあり、nearを使うことで指定した緯度経度から特定の範囲内にある投稿を取得できる。しかしnearメソッドを使用すると、無条件で近い順で投稿が取得されてしまうため、指定した範囲内にある投稿のうち、安い順に投稿を取得したいといったような柔軟なカスタマイズができなかった。そのためモデル内にwithin_boxメソッドを用意した。

```spot.rb
class << self
    def within_box(distance, latitude, longitude)
      distance = distance
      center_point = [latitude, longitude]
      box = Geocoder::Calculations.bounding_box(center_point, distance)
      self.within_bounding_box(box)
    end
  end
```

###view部分
####フォーム部分
下記のようなフォームを用意する。
セレクトボックスを用意することで、検索オプションを指定できるようにした。（今回は距離が近い順だけ）
submitを押すと、コントローラーのsearchアクション内に:locationと:keywordの2つのパラメータが飛ぶようにした。

```index.html.erb
<%= form_with url: search_path, :method => 'get' do |f| %>

    <%= f.text_field :location, placeholder: "住所や行きたい場所を入力してください。" %>
    <%= f.select :keyword, [
      ['距離が近い順', 'near']
    ] %>
    <%= f.submit '検索', class: "button p-2" %>
  <% end %>
  <button id="get_current_spot" type="button" class="button p-2">現在地を取得</button>
```

####JS部分
現在地を取得ボタンを押すと、geoFindMeという関数が動くようになっている。
navigator.geolocationでブラウザがGeolocationAPIに対応しているかを調べている。
対応していたらgeoCurrentPositionで現在地の取得を行う。第一引数には現在地の取得成功時の処理、第二引数には失敗時の処理を書いている。
成功時にはinputタグ（<%= f.text_field :location, placeholder: "住所や行きたい場所を入力してください。" %>）に:locationの形で値を渡している。

```index.html.erb
<script>
  function geoFindMe() {
    function success(position) {
      const latitude  = position.coords.latitude;
      const longitude = position.coords.longitude;
      document.getElementById('location').value = `${latitude},${longitude}`;
    }

    function error() {
      alert('エラーが発生しました。')
    }

    if(!navigator.geolocation) {
       alert('Geolocation is not supported by your browser');
    } else {
      navigator.geolocation.getCurrentPosition(success, error);
    }
  }

  document.querySelector('#get_current_spot').addEventListener('click', geoFindMe);
</script>
```

###コントローラー部分
Geocoder.search（params[:location]）でフォーム内に入っていた地名を使って緯度・経度を取得できる。
次に先ほどモデル内で定義したwithin_boxメソッドを使って、取得した地点の周辺（今回は２０マイルにしています。kmに直したかったらgeocoderの設定ファイルを編集してください。）にある投稿を取得している。
最後にcase文を使って、keywordがnearの場合は現在地から近い順に投稿を取得できるようにした。セレクトボックスとcase文の条件を追加することで別の条件（投稿順・料金が安い順）などで並び替えることもできる。

```spots_controller.rb
def search
    results = Geocoder.search(params[:location])
    if results.empty?
      flash[:notice] = "検索フォームに文字が入っていないか、位置情報を取得できる値でない可能性があります。"
      redirect_to root_path
    else
      selection = params[:keyword]
      latitude = results.first.coordinates[0]
      longitude = results.first.coordinates[1]
      spots = Spot.within_box(20, latitude, longitude)
      case selection
      when 'near'
        get_distance_spots = spots.each { |spot| spot.distance_from([latitude, longitude]) }
        @spots = get_distance_spots.sort_by { |a| a.distance }
      else
        @spots = spots
      end
    end
  end
```

##参考文献
- https://qiita.com/tiara/items/573fe5f1a84ca57dabcd (geocoderの使い方)
- https://qiita.com/Bmouthf/items/1bd0ac7fc9793e699c44#%E7%B7%AF%E5%BA%A6%E7%B5%8C%E5%BA%A6%E3%82%92%E3%81%9D%E3%82%8C%E3%81%9E%E3%82%8C%E3%82%AB%E3%83%A9%E3%83%A0%E3%81%B8%E4%BF%9D%E5%AD%98 (within_boxメソッドの作り方)
- https://developer.mozilla.org/ja/docs/Web/API/Geolocation_API (現在地の取得方法)

##おわりに
ここまでお付き合い頂きありがとうございました！
もしこの記事が役に立ったと感じましたら、LGTMを頂けると幸いです。