【Rails】住所から地図と最寄り駅を表示する

## はじめに
* エンジニア転職用のRailsポートフォリオに、住所から緯度経度情報を取得して地図とその地点の最寄り駅を表示する機能を導入した手順の記録です。
* プログラミング学習3か月目の初心者です。誤りなどありましたらお教えいただけると幸いです。

## 環境
* Ruby 3.1.2
* Rails 6.1.7.3
* IDE: Cloud9

## 完成イメージ
![ezgif-4-2c32ac29a4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3296175/c9d58c6d-0dd8-7288-9f04-a1fa2c1e48d5.gif)
実装した機能の動作デモです

## 流れ
* ①gem geocorderを使用して住所から緯度経度を取得
* ②取得した緯度経度からgoogle mapAPIで地図を表示し、ピンを立てる。
* ③取得した緯度経度からHeartrailsAPIで最寄り駅・路線情報を取得し表示する。

## 参考URLなど
* [Rails5でGoogleMapを表示してみるまで](https://qiita.com/tiara/items/4a1c98418917a0e74cbb )
* [RailsのGeocoderとあそぼ](https://qiita.com/tiara/items/573fe5f1a84ca57dabcd ) （地図表示までの手順はほぼこれら二つの記事のままです）
* [rubyで住所文字列から最寄駅までの距離を取得してみた](https://qiita.com/ushknn/items/7facb047799a370cc8c1) （緯度経度から駅情報を取得するURLを拝借しました）
* [【Ruby on Rails】rails で api を叩いてみよう（初心者から中級者向け）Use an API with Ruby on Rails](https://www.youtube.com/watch?v=0fFx9BrP_8g)（こちらの動画を参考にAPIから情報を取り出しています）

## 実装
### ①モデルの準備
テーブルに住所、緯度、経度、最寄り駅、最寄り駅路線のカラムが必要になるため追加します。
今回はJobモデルに追加します。
```bash:bash
$ rails g migration AddAddressesToJobs address:string latitude:double longitude:double near_station:string near_station_line:string
```

### ②Geocoderの準備
[こちら](https://qiita.com/tiara/items/4a1c98418917a0e74cbb )と[こちら](https://qiita.com/tiara/items/573fe5f1a84ca57dabcd ) の記事を参考に、gem「geocoder」を使えるようにしていきます。
APIkeyの取得などは参考記事をご確認ください。
今回はGeocodingAPIとMaps JavaScript APIを有効化しておきます。
<br>
取得したAPIkeyは環境変数化しておきます。
```:.env
MAP_API_KEY = "取得したAPIキー"
```
<br>

続いてgem

```:gemfile
gem 'geocoder'
```
```:bash
$ bundle install
```
<br>

続いてそのほかの記述をしていきます
```ruby:app/models/job.rb
geocoded_by :address
after_validation :geocode, if: :address_changed? 
# 住所が保存・更新されたら緯度経度を自動取得してlatitude,longitudeに入れてくれる
```
```:bash
$ rails g geocoder:config
```
※上のコマンドでconfig/initialize/geocorder.rbが生成されるので、以下の通り書き換えます。
```ruby:config/initialize/geocorder.rb
# frozen_string_literal: true

Geocoder.configure(
  # Geocoding options
  # timeout: 3,                 # geocoding service timeout (secs)
  lookup: :google,         # name of geocoding service (symbol)
  # ip_lookup: :ipinfo_io,      # name of IP address geocoding service (symbol)
  # language: :en,              # ISO-639 language code
  use_https: true,           # use HTTPS for lookup requests? (if supported)
  # http_proxy: nil,            # HTTP proxy server (user:pass@host:port)
  # https_proxy: nil,           # HTTPS proxy server (user:pass@host:port)
  api_key: ENV["MAP_API_KEY"],               # API key for geocoding service
  # cache: nil,                 # cache object (must respond to #[], #[]=, and #del)

  # Exceptions that should not be rescued by default
  # (if you want to implement custom error handling);
  # supports SocketError and Timeout::Error
  # always_raise: [],

  # Calculation options
  # units: :mi,                 # :km for kilometers or :mi for miles
  # distances: :linear          # :spherical or :linear

  # Cache configuration
  # cache_options: {
  #   expiration: 2.days,
  #   prefix: 'geocoder:'
  # }
)

```
以上でGeocoderの準備は完了です。

### ③GoogleMapAPIで地図表示
次に、erbファイルに以下の記述を追加します。可読性を考慮して今回はパーシャル化しています。
スタイルや倍率、中心位置などカスタマイズ可能です。
```erb:app/views/jobs/_map.html.erb
<div id="map"></div>

<style>
/*マップのスタイル*/
#map{
  height: 300px;
  width:100%;
}
</style>

<script>
// マップ表示スクリプト
function initMap(){

  //マップの設定
  let map = new google.maps.Map(document.getElementById('map'), {
  center: {lat: <%=job.latitude%>, lng: <%= job.longitude %>},       //地図表示の中心位置を指定
  zoom: 15                                                           //倍率
  });

  //マーカーの設定
  let marker= new google.maps.Marker({
     position: { lat: <%=job.latitude%>,lng:  <%= job.longitude %>}, //マーカーの位置を指定
     map: map
  });
}
</script>
<script src="https://maps.googleapis.com/maps/api/js?key=<%= ENV['MAP_API_KEY'] %>&callback=initMap" async defer></script>
```
以上で、入力された住所に対応したマーカー付地図を表示できました。

### ④Heartrails APIで最寄り駅表示
続いてGeocoderで取得した緯度経度から、HeartrailsAPIを叩いて最寄り駅・最寄り駅路線の情報を取得していきます。特にキーの取得などは必要ありません。
jobsコントローラのcreate,updateアクションで動作させます。緯度経度を入れてAPIのURLにアクセスするとレスポンスが返ってくるので、緯度経度保存→最寄り情報取得→再度保存、の流れを踏みます。例：（https://express.heartrails.com/api/json?method=getStations&x=139.6986798793053&y=35.6596004）

```ruby:app/controllers/jobs_controller.rb
def create
    job=Job.new(job_params)
    job.near_station_line = ""
    job.near_station = "" # 一度保存しないとgeocoderが動かないため最寄りは空欄でいったん保存
    if job.save
        # 最寄り駅情報を取得。
        uri = URI.parse("http://express.heartrails.com/api/json?method=getStations&x=#{job.longitude}&y=#{job.latitude}")
        response = Net::HTTP.get_response(uri)
        result = JSON.parse(response.body)
        
        #カラムにそれぞれ代入する。最寄り駅が存在しない座標の場合にエラーになるためunless以下も必須
        job.near_station = result["response"]["station"][0]["name"] unless result["response"]["station"].blank?
        job.near_station_line =  result["response"]["station"][0]["line"] unless result["response"]["station"].blank?

        # 再度保存する
        job.save
        redirect_to job_path(job),flash: {notice: "募集を公開しました"}
    else
        @job = job
        flash.now[:error] = "作成に失敗しました"
        render :new
    end
end


def update
    job=Job.find(params[:id])
    if job.update(job_params)
        # 最寄り駅情報を取得。
        uri = URI.parse("http://express.heartrails.com/api/json?method=getStations&x=#{job.longitude}&y=#{job.latitude}")
        response = Net::HTTP.get_response(uri)
        result = JSON.parse(response.body)
        
        #カラムにそれぞれ代入する。最寄り駅が存在しない座標の場合にエラーになるためunless以下も必須
        job.near_station = result["response"]["station"][0]["name"] unless result["response"]["station"].blank?
        job.near_station_line =  result["response"]["station"][0]["line"] unless result["response"]["station"].blank?

        # 再度保存する
        job.save          
        redirect_to job_path(job),flash: {notice: "募集を公開しました"}
    else
      @job=job
      flash.now[:error] = "公開できませんでした"
      render :edit
    end
end
```
これで動作しますが、ごちゃついてしまうので最寄り取得部分をapp/models/job.rbに切り出して整理します。
```ruby:app/models/job.rb
 # 最寄り駅を取得して保存するメソッド
  def addStation
    # 最寄り駅情報を取得。
    uri = URI.parse("http://express.heartrails.com/api/json?method=getStations&x=#{self.longitude}&y=#{self.latitude}")
    response = Net::HTTP.get_response(uri)
    result = JSON.parse(response.body)

    #カラムにそれぞれ代入する。最寄り駅が存在しない座標の場合にエラーになるためunless以下も必須
    self.near_station = result["response"]["station"][0]["name"] unless result["response"]["station"].blank?
    self.near_station_line =  result["response"]["station"][0]["line"] unless result["response"]["station"].blank?

    # 再度保存する
    self.save
  end
```

コントローラは以下のように修正。
```ruby:app/controllers/jobs_controller.rb
 def create
    job=Job.new(job_params)
    job.near_station_line = ""
    job.near_station = "" # 一度保存しないとgeocoderが動かないため最寄り駅空欄でいったん保存
    if job.save
        # 最寄り駅情報を取得して再保存
        job.addStation
        redirect_to job_path(job),flash: {notice: "募集を公開しました"}
    else
        @job = job
        flash.now[:error] = "作成に失敗しました"
        render :new
    end
    
end

def update
    job=Job.find(params[:id])
    if job.update(job_params)
        # 最寄り駅情報を取得して再保存
        job.addStation
        redirect_to job_path(job),flash: {notice: "募集を公開しました"}
    else
        @job=job
        flash.now[:error] = "公開できませんでした"
        render :edit
    end

end
```
以上で最寄り駅・路線を取得してテーブルに保存することが出来ました。ビューは適宜実装してください。