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を利用してみて精度が出ないようだったらリリース後見直すことにしました。

上記の設定を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で緯度経度の距離計算を実装した話をしました。緯度経度を元に町丁目を検索出来るようになりました。

ラクスルの新聞折り込み、ポスティング、DMなどの集客支援商材をメインで担当。エンジニアリングマネージャしつつ、自身も開発に携わってます。ラクスルではRailsの開発がメイン。きちんと効果が見える新しいオフライン広告を目指して日々奮闘中!

2020.07.31 #Technology
1322

Railsで緯度経度の距離計算を実装した話

この記事のURLをコピーする
この記事をシェアする ツイート シェア はてな