GeocoderとGeokitを使用して、現在地周辺検索を行う機能を実装してみた

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

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

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

今回使用したGemは、 geocodergeokit-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/NokiaESRI・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の人気を比較しているサイトがあったので載せておきます。

ruby.libhunt.com

Geocoderの使い方

住所から緯度経度を割り出すことを geocoding といいます。 geocoding を行いたいカラムがあるモデルに以下のように記述します。

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

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

after_validationgeocoding するタイミングを指定します。 上のように書くと、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`を返します。 詳しくは下記を御覧ください。

qiita.com

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

現在地の取得には、HTML5Geolocation 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 はその名のとおり、現在地取得の際のオプション設定です。 enableHighAccuracyGPSなどを使った高精度な現在地の取得を可能にします。 他にもタイムアウト設定やキャッシュの設定なども可能です。

  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

以上が現在地周辺の施設を検索する機能の解説でした。 非常に便利なGemですね。

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

参考