GeocoderとGeokitを使用して、現在地周辺検索を行う機能を実装してみた
こんにちは、エンジニアの神山です。最近大豆製品ばっかり食べています。
今回はWebサービスでよく見かける、現在地周辺検索を行う機能の実装について書きました。
例えば現在地から半径2km内にあるレストランを検索したり、現在地より最寄りの駅を探したりできる機能です。
今回使用したGemは、 geocoder と 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: 現在地周辺検索を行う
の様な使い分けをしました。
ちなみにgeocoder
とgeokit
の人気を比較しているサイトがあったので載せておきます。
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`を返します。
詳しくは下記を御覧ください。
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
の詳しい情報は以下のリンク先にも書かれているので参考にしてみてください。
以上が現在地周辺の施設を検索する機能の解説でした。 非常に便利なGemですね。
改良点等アドバイスいただけたら大変幸いです。