目的
レコードにある緯度経度と入力値の緯度経度から距離を計算し
一定距離内のレコードのみを取得したかった
計算方法とそれぞれへの所感
ざっとググってみると主に3つの方法が見つかりました
-
- 三角関数を駆使する方法
-
-
LINESTRING()
やGLENGTH()
を使う方法
-
-
-
ST_Distance_Sphere()
を使う方法
-
最終的に採用したのは3番のST_Distance_Sphere()
を使う方法でした
1. 三角関数を駆使する方法について
ぶっちゃけめんどい
他にいい方法がなければ仕方なく使う最終手段
2. LINESTRING()
やGLENGTH()
を使う方法
試してみたけどちょっと精度が悪い
580m程度の距離を計算した時に120m程度の誤差が出て許容できなかった
主に1km前後の距離を精度よく計算することが求められていたので不採用
3. ST_Distance_Sphere()
を使う方法(採用)
MySQLにある関数を使うだけなので非常に使い勝手が良く
高精度の距離計算ができたのでこれを採用!!
試してないけどST_Distance_Spheroid()
っていう速度は落ちるがさらに高精度な関数もあるらしい
ちなみに計算結果の単位はメートル
インチとかヤードとかじゃなくて本当に良かった
実際の実装では
Railsを使っていたため緯度経度のカラムの型にgeometry型が扱えず
代わりにdouble型のカラムで緯度経度を保持していました
geometry型の代わりにdouble型のカラムで十分だと判断した理由については別の記事でまた書きます
計算する時はPOINT()
を使って緯度経度をキャストしたものをST_Distance_Sphere()
の引数に渡して計算しました
Railsでの実装
前提として、店舗(Store)と住所(Address)という1:1のモデルがあり、
Addressモデルが持つ緯度経度と入力値の緯度経度から距離を計算して
一定距離内の店舗一覧を取得する処理を実装した
DISPLAYABLE_METER = 1000
DISTANCE_METER_COLUMN = 'distance_meter'
scope :select_with_distance_meter, lambda { |lat:, lng:|
joins(:address).select(
"
#{self.table_name}.*,
ST_Distance_Sphere(
POINT(lng, lat),
POINT(#{lng}, #{lat})
) AS #{DISTANCE_METER_COLUMN}
",
)
}
Store
.select_with_distance_meter(lat: lat, lng: lng)
.having("#{Store::DISTANCE_METER_COLUMN} < #{Store::DISPLAYABLE_METER}")
AS
でエイリアスにしているカラムに対してはwhere
使えないのでhaving
使ってます