2020.07.31
Railsで緯度経度の距離計算を実装した話
こんにちは。ポスティングのサービス開発を担当しているエリアマーケティングチームです。
本日は3回目、Railsで緯度経度の距離計算を実装した事例を紹介します。
今回の記事の担当は、サーバエンジニアのEng.Xです!
はじめに
ポリゴンの刷新プロジェクトを先日リリースしました。その中で、地図画面の検索機能の改善も行いました。検索の例を示します。地図画面( https://raksul.com/posting/map/ )で「目黒区」と検索してみてください。目黒区を中心として半径3000メートル以内の町丁目を返しています。今回のリリースでは、この距離計算を独自に行っていたものを geokit-rails に置き換えました。geokit-railsとは、地球上の2点間の距離計算、矩形の内部に任意の地点があるか判定する等のジオコーディング技術を提供するライブラリです。
検索機能の実現
(緯度、経度) = (35.695172、139.44788)(地点N)を中心として半径3000mの町丁目を検索することを例題とします。既存方法での実現、geokit-railsでの実現の順番で説明します。
既存方法での実現
まず、次の仮定をおきます。
- 地球は平面である
- 緯度1度が111000メートル
- 経度1度が91000メートル
こうした時に地点Nから北へ3000メートル離れた緯度を求めます。
35.695172 – (3000 / 111000) = 35.668144972972975
また、地点Nから南へ3000メートル離れた緯度を求めます。
35.695172 + (3000 / 111000) = 35.722199027027024
同様に、地点Nから東西へ3000メートル離れた経度を求めます。
139.44788 – (3000 / 91000) = 139.41491296703296
139.44788 + (3000 / 91000) = 139.48084703296703
まとめると、緯度35.668144972972975〜 35.722199027027024、経度139.41491296703296〜139.48084703296703の範囲に含まれる町丁目を検索すれば良いことが分かります。以下は、SQLとしてロジックを落とし込んだものです。
SELECT `polygons`.* FROM `polygons` WHERE `polygons`.`lng` BETWEEN(35.695172 + 3000 / 111000) AND (35.695172 - 3000 * / 111000) AND `polygons`.`lat` BETWEEN(139.44788 + 3000 / 91000) AND (139.44788 - 3000 * / 91000)
geokit-railsでの実現
geokit-rails での町丁目の検索方法を紹介します。選定理由は「地球は平面である」ことを仮定していないため距離計算の精度向上が見込めることとAPIのインターフェースが直感的で使いやすいことです。
事前に、次の準備をします。
- 検索対象のモデルに カラムに lat(緯度) と lng(経度)を追加し、lat と lng に複合インデックスを張る。
- acts_as_mappable で検索アルゴリズムや単位などの設定を行う
- default_units
- 距離計算に使う単位の設定で、miles(マイル)とkms(キロメートル)のどちらかを設定出来ます。
- 今回はkmに設定します。
- default_formula
- 距離計算に使うアルゴリズムの指定で、sphere または flat を指定出来ます。
- flatを指定すると、地球はフラットであるとみなし、ピタゴラスの定理が利用されます。計算は簡易的で処理も軽いですが、長距離の計算になると誤差が生じます。
- sphereを指定した場合、ハバシン(Haversine)式 が使われ、精度は高いですが計算量が重くなります。 大円距離 の計算方法の1つです。
- 今回は、精度を重視し、sphereを利用してみて精度が出ないようだったらリリース後見直すことにしました。
- default_units
上記の設定をModelに定義します。
app/models/polygon.rb
# == Schema Information # # Table name: polygons # # id :bigint(8) not null, primary key # polygon :geometry not null # lat :float(24) not null # lng :float(24) not null # created_at :datetime not null # updated_at :datetime not null # # Indexes # # index_polygons_on_lat_and_lng (lat,lng) class Polygon < ApplicationRecord acts_as_mappable default_units: :kms, default_formula: :sphere end
準備が終わったので、指定した緯度・経度から3000メートル以内の町丁目を検索します。
Polygon.within(3.0, origin: [35.695172, 139.44788])
以下のSQLが発行されます。インデックスもきちんと効くため、パフォーマンスも良好です。
SELECT `polygons`.* FROM `polygons` WHERE ( polygons.lat IS NOT NULL AND polygons.lng IS NOT NULL ) AND ( polygons.lat > 35.61521004414393 AND polygons.lat < 35.701951955856075 AND polygons.lng > 139.69205370596865 AND polygons.lng < 139.79881229403134 ) AND (( (ACOS(least(1, COS(0.6223596450390921) * COS(2.4390179204751368) * COS(RADIANS(polygons.lat)) * COS(RADIANS(polygons.lng)) + COS(0.6223596450390921) * SIN(2.4390179204751368) * COS(RADIANS(polygons.lat)) * SIN(RADIANS(polygons.lng)) + SIN(0.6223596450390921) * SIN(RADIANS(polygons.lat)))) * 3963.1899999999996) <= 3.0 ))
まとめ
今回の記事では、Railsで緯度経度の距離計算を実装した話をしました。緯度経度を元に町丁目を検索出来るようになりました。
Railsで緯度経度の距離計算を実装した話
Featured posts
in #Technology