1
/
5

【Rails】現在地の取得ってどうやるの?GeokitとGeocoderを使って現在地周辺施設の検索機能を実装してみた!

こんにちは、エンジニアの神山です。最近大豆製品ばっかり食べています。

今回はWebサービスでよく見かける、現在地周辺検索を行う機能の実装について書きました。

例えば現在地から半径2km内にあるレストランを検索したり、現在地より最寄りの駅を探したりできる機能です。


今回使用したGemは、 [geocoder](https://github.com/alexreisner/geocoder) と [geokit-rails](https://github.com/geokit/geokit-rails) です。

まずこの2つのGemにフォーカスして話を進めていきます。

GeocoderとGeokit

これらのGemは住所から緯度経度を割り出したり、緯度経度を用いた検索ロジックを提供しております。

具体的には以下の様な事ができます。

  • 住所から緯度経度を割り出す
  • 緯度経度から住所を割り出す
  • IPアドレスから緯度経度を割り出す
  • 指定したオブジェクトまたは緯度経度を中心に、指定した距離内や範囲(ex: 2km~5km)にある施設を検索する
  • 2点間の距離を算出する
  • あるオブジェクトに対して指定したオブジェクトが位置する方角を示す
  • 指定したオブジェクトまたは緯度経度を中心に、長方形状に指定した範囲内ある検索する


住所から緯度経度を割り出すとは、「東京都中央区◯◯」という文字列を判別し、北緯35度、東経139度というように緯度経度に直すことです。


GeocoderとGeokitの比較

Geocoderの利点

Geocoderを使うメリットは、住所から緯度経度を割り出す機能が優秀という点です。

GeocoderはGeokitより多くのAPIを網羅しております。


GeokitはGoogle・Yahoo・Bing・FCC・MapQuest・Mapbox・OpenCageの7つに対して、

Geocoderは上のFCCを除いたものに加えて、Google Maps API for Work・Google Maps API for Work・Nominatim・Yandex・Geocoder.ca・Geocoder.us・Ovi/Nokia・Here/Nokia・ESRI・Mapzen・Pelias・Data Science Toolkit・Geocodio・SmartyStreets・OKF Geocoder・Geoportail.lu・PostcodeAnywhere Uk・LatLon.ioの25個網羅しています。


使用できるAPIの数が多いほうが、最適なものを選べるので使い勝手がいいですね。


Geokitの利点

一方、Geokitを使うメリットは、オブジェクトを絞るメソッドを使用する際メソッドチェーンが使えるという点です。


Geocoderだと可能な場合と不可能な場合があるようです。


例えば、Geokitのメソッドである`within`を使うとき、

prefecure_id = 1
shop_type = restaurant
latitude = 33.33
longitude = 140.44

Shop.within(2, [latitude, longitude]).tap do |scoped|
  scoped.where!(prefecure_id: prefecure_id) if prefecure_id
  scoped.where!(shop_type: shop_type) if shop_type
end

のようなことが出来ます。

例えば絞込を行う等、メソッドチェーンを使用したい場面を想定するとGeokitのほうが使い勝手が良いですね。


上記の理由により今回は、

Geocoder: 緯度経度の割り出しを行う

Geokit: 現在地周辺検索を行う


の様な使い分けをしました。


ちなみにgeocodergeokitの人気を比較しているサイトがあったので載せておきます。


Geokit vs Geocoder | LibHunt
Compare Geokit and Geocoder's popularity and activity. Categories: Geolocation. Geokit is less popular than Geocoder.
https://ruby.libhunt.com/project/geokit/vs/geocoder


Geocoderの使い方

住所から緯度経度を割り出すことをgeocodingといいます。

geocodingを行いたいカラムがあるモデルに以下のように記述します。

geocoded_by :address
after_validation :geocode, if: :address_changed?

geocoded_byはgeocodingを行いたいカラムを指定します。

addressというカラムに住所(ex: 東京都港区〇〇)が入っており、その住所を基に検索を行いたいような場面です。


after_validationはgeocodingするタイミングを指定します。

上のように書くと、addressカラムに変更があった際に、自動でgeocodingされるようになります。

Geokitの使い方

緯度経度を使って出来ることを、GitHubより抜粋しました。

# 指定した緯度経度から半径5km内のオブジェクトを検索
Shop.within(5, origin: [33.33, 140.33]) 

# 指定した緯度経度から半径5km外のオブジェクトを検索
Shop.beyond(5, origin: [33.33, 140.33])  

# 指定した緯度経度から2km~5km内のオブジェクトを検索
Shop.in_range(2..5, origin: [33.33, 140.33]) 

# 緯度が30.33~35.55内でかつ、経度が135.55度~145.55度内にあるオブジェクトを検索
Shop.in_bound([[30.33, 135.55], [35.55, 145.55]]) 

# 指定した緯度経度から一番近いオブジェクトを検索
Shop.closest(origin: [33.33, 140.33])


様々な検索方法がありますね。

因みにGeocoderでも同じようなこと検索は可能です。


現在地の検索機能

では、現在地より半径2km以内にある店を検索する機能を実装していきます。

「現在地から探す」ボタンを押した時に、上記の条件で検索が行われるような実装です。


「現在地から探す」ボタン押下後の挙動としては、

Javascriptを用いて現在地の緯度経度情報を取得→Geokitのメソッドを用いて緯度経度付近にある施設を検索して、Viewに表示させる

という流れになります。


Model

Shopモデルの`address`カラムをgeocodingします。


models/shop.rb

geocoded_by :address
after_validation :geocode, if: :address_changed?

address_changed?というメソッドはActiveRecord::Dirtyの機能です。{column_name}_changed?で該当するカラムが変更された時に`true`を返します。

詳しくは下記を御覧ください。


ActiveModel::Dirtyのメソッドを使って変更を感知しよう - Qiita
ActiveRecordを使っていると時々プログラム内でModelオブジェクトの値を変更してから、変更前の値が欲しくなったりすることがある。 そんなときはActiveModel::Dirtyのメソッドを使うとModelのオブジェクトに変更があった時に変更の情報を取得出来る。 # たとえばUserのレコードを一個引っ張ってきて u = User.last => # # 変...
http://qiita.com/kakkunpakkun/items/b286f789ca8590104ad1

Controller

Geokitのwithinメソッドを用いることにより、現在地から2km以内の店が@placesに渡されます。

shops_controller

def search
  latitude = params[:latitude]
  longitude = params[:longitude]

  @places = Shop.all.within(2, origin: [latitude, longitude])
end


View

shops/index

「現在地から探す」ボタン部分になります。

.currentLocation
  = button_tag ‘現在地から探す’, type: ‘button’

Javascript

現在地の取得には、HTML5の`Geolocation API`を使用します。

以下の様なコードになります。

class @CurrentLocations
  @getCurrentLocation: ->
    $('.currentLocation').on 'click', ->
      if navigator.geolocation
        navigator.geolocation.getCurrentPosition(successGetPosition, failGetPosition, options)
      else
        message = 'ご使用中のブラウザは現在地検索に対応されておりません。'
        Alert.set('warning', message)

  successGetPosition = (position) ->
    window.location.href = "/shops/searches?latitude=#{position.coords.latitude}&longitude=#{position.coords.longitude}"

  options =
    enableHighAccuracy: true

  failGetPosition = (error) ->
    switch error.code
      when 1
        message = '位置情報の提供を許可してください。'
      when 2
        message = '位置情報の取得に失敗しました。'
    Alert.set('warning', message)

では上から見ていきます。

@getCurrentLocation: ->
    $('.currentLocation').on 'click', ->
      if navigator.geolocation
        navigator.geolocation.getCurrentPosition(successGetPosition, failGetPosition, options)
      else
        message = 'ご使用中のブラウザは現在地検索に対応されておりません。'
        Alert.set('warning', message)

@getCurrentLocationが現在地を取得する関数です。


navigatorはブラウザの情報を取得するオブジェクトです。

そして.geolocationでブラウザが Geolocation APIに対応しているか調べています。

対応していないブラウザもあるので注意して下さい。(IE8.0など)


ブラウザが Geolocation API`に対応している時、geoCurrentPositionで現在地の取得を試みます。

geoCurrentPositionの第一引数は現在地の取得に成功した時に実行され、第二引数は失敗した時に実行されます。

第三引数は現在地の取得の際のオプション設定をしております。


ちなみにブラウザが Geolocation APIに対応していてもユーザーが位置情報の提供を許可していない状況も考えられます。

そのような場合、geoCurrentPositionが呼ばれると自動で許可するか否かのポップアップが出るようになっております。これまた便利ですね。


ブラウザがGeolocation APIに未対応である場合は、elseの部分が実行されます。

ここでは「対応していません」というアラートを出すようにしています。

  successGetPosition = (position) ->
    window.location.href = "/places/searches?latitude=#{position.coords.latitude}&longitude=#{position.coords.longitude}"


successGetPositionは現在地の取得に成功した時の関数です。

positionオブジェクトに現在地の情報が入っており、position.coordsで様々な情報を取り出せます。


例えば、position.coords.latitudeで緯度を取得できたり、position.coords.logitudeで経度を取得できます。


そして現在地の取得に成功した時に、現在地周辺の店の一覧ページに移動します。

またControllerに緯度経度の値を渡しております。

options =
    enableHighAccuracy: true

optionsはその名のとおり、現在地取得の際のオプション設定です。

enableHighAccuracyはGPSなどを使った高精度な現在地の取得を可能にします。

他にもタイムアウト設定やキャッシュの設定なども可能です。

 failGetPosition = (error) ->
    switch error.code
      when 1
        message = '位置情報の提供を許可してください。'
      when 2
        message = '位置情報の取得に失敗しました。'
    Alert.set('warning', message)


failGetPositionは現在地の取得に失敗した時に実行される関数です。

errorというオブジェクトに失敗の原因が入っております。

今回は、エラーの原因ごとに別々のアラートを出したいので、error.codeごとに条件分岐をしております。


error.code = 1はユーザーが位置情報の許可をしていない時を指しております。

error.code = 2は現在地の取得に失敗した時を指しております。



Geolocation APIの詳しい情報は以下のリンク先にも書かれているので参考にしてみてください。


HTML5 Geolocation
Well organized and easy to understand Web building tutorials with lots of examples of how to use HTML, CSS, JavaScript, SQL, PHP, and XML.
http://www.w3schools.com/html/html5_geolocation.asp


以上が現在地周辺の施設を検索する機能の解説でした。

非常に便利なGemですね。


改良点等アドバイスいただけたら大変幸いです。


参考

https://github.com/geokit/geokit-rails

https://github.com/alexreisner/geocoder

https://ruby.libhunt.com/project/geokit/vs/geocoder

http://www.w3schools.com/html/html5_geolocation.asp

http://hakuhin.jp/js/geolocation.html

株式会社クルイトでは一緒に働く仲間を募集しています
12 いいね!
12 いいね!

今週のランキング

神山 奎吾さんにいいねを伝えよう
神山 奎吾さんや会社があなたに興味を持つかも