【Rails】現在地を取得し、緯度経度をもとに現在地から近い店舗を検索

こんにちは

今回は、Googlemapを用い、
①現在地の緯度経度を取得し、
②上記をもとに、現在地からテーブルに保存済みの店舗までの距離を計算し、
③距離が近い順番に店舗を表示する

という機能を実装していこうと思います


```Ruby:実装環境
Ruby 3.0.4
Rails 6.1.5
Bootstrap　4.3
```

<h2>⓪まずは下準備</h2>

・APIを用いてgooglemapが投稿できる状態にしておく

Googleマップの投稿機能の作成　➤　[参考記事](https://qiita.com/MandoNarin/items/aa91ffae373a8cfc85d2)
（※後程用いるので、geolocation API も有効可しておくとよい）

・jQueryを導入しておく
※今回はajaxモジュールを用いているので、jQueryは通常使われるslimではなく、uncompressedを用いてください
slimやuncompressedについてはこちら　➤　[参考記事](https://netrandum.com/jquery-slim-build/1995)

※今回は、mapコントローラーとMapモデル（mapsテーブル）を用います
mapsテーブルはカラムとしてlatitudeとlongtudeを持ちます
viewファイルの役割は、以下です
| view file                | role |
|:------------------------:|:------------:|
| maps/index.html.erb      | mapsテーブルの中身一覧表示   |
| maps/search.html.erb     | 現在地の緯度経度を取得ー検索  |
| maps/result.html.erb     | 距離が近い順に結果を表示  |

:::note info
今回はファイルを区分していますが、やっていることは検索機能と全く同じなため、ファイルを区分せず、`index.html.erb`で一覧表示・緯度経度取得ー検索・結果表示のすべてをまとめることもできます
:::

<h2>①geolocation API の有効化</h2>

[Google cloud platform](https://console.cloud.google.com/?hl=ja) から、geolocation API を有効化し、APIキーを取得しておいてください

取得したAPIキーは、appやconfigと同じ階層に`.env`というファイルを作り、以下のように貼り付けておきましょう

```Ruby:.env
GOOGLE_MAP_API_KEY=xxxxxxxxxxxxxxx（←自分のAPIキー）
```


<h2>②現在地を取得し、緯度経度を利用できる状態にする</h2>

今回は、`maps/search.html.erb`に、現在地を取得するための記述をしていこうと思います。

```Ruby:maps/search.html.erb

<button onclick="getLocation()">取得!!!</button>

<script>
function getLocation() {
    if (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition(showPosition);
    } else { 
        alert("Geolocation is not supported by this browser.");
    }
}

function showPosition(position) {

$.ajax({
  type: 'GET',
  url: `https://maps.googleapis.com/maps/api/geocode/json?latlng=${position.coords.latitude},${position.coords.longitude}&sensor=true&key=<%= ENV['GOOGLE_MAP_API_KEY'] %>&language=en`,
}).then(function(response){
  console.log(response);
})

}
</script>
```

これで完成です
`rails s`をして、ディベロッパーツールよりconsoleを確認してみてください
取得ボタンをクリックし、以下の写真のようにトグルを開いていくと、緯度経度（latとlng）が取得できていることが確認できます

![画像1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2429739/d9251072-10b8-7a47-6dff-98f3021c8818.png)


（※トグルを、results ➤ 0 ➤ geometry ➤ location の順に開いていってみてください！）

しかし、現在地は取得できたものの、また緯度経度をデータとして直接扱える状態ではありません
そのため、緯度経度をそれぞれデータとして扱えるよう、記述を変更していきます。

先ほどの記述を、以下の通りに変更します


変更後
```Ruby:maps/search.html.erb

<button onclick="getLocation()">現在地を取得する</button>
<div id="display1"></div>
<div id="display2"></div>
＃↑緯度と経度をview上で表示する場所です

<script>
let display1 = document.getElementById("display1")
let display2 = document.getElementById("display2")

function getLocation() {
    if (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition(showPosition);
    } else { 
        alert("Geolocation is not supported by this browser.");
    }
}

function showPosition(position) {

$.ajax({
  type: 'GET',
  url: `https://maps.googleapis.com/maps/api/geocode/json?latlng=${position.coords.latitude},${position.coords.longitude}&sensor=true&key=<%= ENV['GOOGLE_MAP_API_KEY'] %>&language=en`,
}).then(function(response){
    dis1 = Number(response.results[0].geometry.location.lat)
    dis2 = Number(response.results[0].geometry.location.lng)
    ＃console.log(response)を消し、その代わりにdis1とdis2という変数を定義。それぞれに緯度・経度を代入しています。

    display1.textContent = dis1
    display2.textContent = dis2
    ＃view上のdisplay1とdisplay2にそれぞれ、緯度経度を表示するための記述です
})

}
</script>
```

上の作業により、緯度経度を直接用いることができるようになりました。（view上に表示する記述はおまけのようなものです。次以降では消しています）

最後に、緯度経度から２地点間の距離を計算するために、緯度経度を情報としてコントローラーへ送るための記述をしていこうと思います

```Ruby:maps/search.html.erb

<button onclick="getLocation()" id="get-button">現在地を取得する</button>

<%= form_tag({controller:"maps",action:"result"}, method: :get) do %>
<p>
  <%= hidden_field_tag :lat, :value => "緯度", id: :lat %>
  <%= hidden_field_tag :lng, :value => "軽度", id: :lng %>
</p>
<div id="response">未取得</div>
<%= submit_tag "送信" %>
<% end %>
＃mapsコントローラーのresultアクションに対して、データを送ります

<script>

function getLocation() {
    if (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition(showPosition);
    } else { 
        alert("Geolocation is not supported by this browser.");
    }
}

function showPosition(position) {

$.ajax({
  type: 'GET',
  url: `https://maps.googleapis.com/maps/api/geocode/json?latlng=${position.coords.latitude},${position.coords.longitude}&sensor=true&key=<%= ENV['GOOGLE_MAP_API_KEY'] %>&language=en`,
}).then(function(response){
    dis1 = Number(response.results[0].geometry.location.lat)
    dis2 = Number(response.results[0].geometry.location.lng)

    document.getElementById('lat').value = dis1;
    document.getElementById('lng').value = dis2;
    document.getElementById('get-button').textContent = "取得済";
    ＃hidden_fieldに、取得した現在地の緯度経度を反映させます
})

}
</script>
```

これで完了です！
見てお判りの通り、検索機能と同じ手順を踏んでいます。ただ検索する内容に、緯度経度を用いようとしているだけです。

取得ボタンを押したのち、送信ボタンを押せば、mapsコントローラーのresultアクションに緯度経度の情報を送ることができるようになりました。

<h2>③取得した現在地をもとに、２地点間の距離を算出し、距離が短い順番に並べる</h2>

コントローラーは、いきなり完成形をお示しします。
後程開設を入れます

```Ruby:maps_controller.rb

＃省略

def result
        x1 = params[:lat] * Math::PI / 180
        y1 = params[:lng] * Math::PI / 180

        arounds = []
        Map.all.each do |t|
            x2 = t.latitude * Math::PI / 180
            y2 = t.longitude * Math::PI / 180

            diff_y = (y1 - y2).abs
            
            calc1 = Math.cos(x2) * Math.sin(diff_y)
            calc2 = Math.cos(x1) * Math.sin(x2) - Math.sin(x1) * Math.cos(x2) * Math.cos(diff_y)
            
            numerator = Math.sqrt(calc1 ** 2 + calc2 ** 2)
            denominator = Math.sin(x1) * Math.sin(x2) + Math.cos(x1) * Math.cos(x2) * Math.cos(diff_y)
            degree = Math.atan2(numerator, denominator)

            α = 6378.137
            result = degree * α

            arounds.push( [result, t] )
        end

        @arounds = arounds.sort_by{ |s| s[0] }

    end

＃省略

```

それでは、これらの記述に対して開設を加えていきます

まずは、計算方法の概要ですが、地球は球形をしていることから、２地点間の距離を出すためには大円距離というものを用いる必要がある用です

[大円距離（from Wikipedia）](https://ja.wikipedia.org/wiki/%E5%A4%A7%E5%86%86%E8%B7%9D%E9%9B%A2)

また、大円距離を用いて２地点間の距離を出すための数式はいくつかあるようですが、今回は参考にした記事にならい、Vincenty法というものを用いていきます

[Vincenty法（from Wikipedia）](https://ja.wikipedia.org/wiki/Vincenty%E6%B3%95)

次に、計算式について説明を加えていきます

```Ruby:maps_controller.rb

＃省略

        x1 = params[:lat] * Math::PI / 180
        y1 = params[:lng] * Math::PI / 180

        arounds = []

＃省略
```

まずは```views/maps/search.html.erb```から送られてきた、現在地の緯度経度を、paramsを用いて取得し、それぞれx1とy1に代入します。

その際に、緯度と経度をラジアンに返還しておきます

角度のラジアンへの変換は、
角度「°」×　π　÷　180

のため、rubyが用意してくれているMathというモジュールを用い、この式に当てはめ返還をしていきます

また、距離を算出した結果を格納するための配列を作成し、aroundsと定義しておきます


```Ruby:maps_controller.rb

＃省略


        Map.all.each do |t|
            x2 = t.latitude * Math::PI / 180
            y2 = t.longitude * Math::PI / 180

＃省略
```

次に、現在地との距離を算出する対象（もともとテーブルに保存してあるデータ）を１つずつ呼び出し、それぞれの緯度経度を上と同じ方法を用いx2とyxに代入します

```Ruby:maps_controller.rb

＃省略

            diff_y = (y1 - y2).abs
            
            calc1 = Math.cos(x2) * Math.sin(diff_y)
            calc2 = Math.cos(x1) * Math.sin(x2) - Math.sin(x1) * Math.cos(x2) * Math.cos(diff_y)
            
            numerator = Math.sqrt(calc1 ** 2 + calc2 ** 2)
            denominator = Math.sin(x1) * Math.sin(x2) + Math.cos(x1) * Math.cos(x2) * Math.cos(diff_y)
            degree = Math.atan2(numerator, denominator)

            α = 6378.137
            result = degree * α

＃省略
```

そして、Vincenty法の計算式にならい、緯度経度を用いて２地点間の距離を求めていきます
ちなみにαは、地球の赤道半径（km）です

```Ruby:maps_controller.rb

＃省略
            arounds.push( [result, t] )
        end

        @arounds = arounds.sort_by{ |s| s[0] }

    end

＃省略
```

最後に、もとめた距離を定義した配列aroundsに入れていきます。その際にこの距離が、現在地とどの場所との距離なのかもわかるようにしておくために、t（＝すなわちMapテーブルに格納された場所の情報）も同時にaroundsに入れます

そして、sort_byメソッドを用い、arounds配列の中身を距離の短い順番に並び替え、@aroundsに代入します

ここまでで、現在地ともともとテーブルに入っている場所とのそれぞれの距離を求め、現在地からの距離が短い順に場所を並び替える作業が完了しました

あとは、

```Ruby
<% @arounds.each do |t| %>
  <% t[1].title %>まで
  <% t[0] %>km
<% end %>
```

などのようにして、viewにてその情報を表示してあげたら終わりです



<h3>参考記事</h3>
現在地の取得

[https://off.tokyo/blog/how-to-get-my-location/](https://off.tokyo/blog/how-to-get-my-location/)

2地点間の距離算出
[https://techblog.kyamanak.com/entry/2017/07/09/164052](https://techblog.kyamanak.com/entry/2017/07/09/164052)