Created
January 7, 2011 19:07
-
-
Save gilles/769944 to your computer and use it in GitHub Desktop.
Some geo functions for Ruby (haversine) and a mixin for spherical search in Mongo (tested with mongoid)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# geo.rb | |
# Formulas from | |
# | |
# haversine formula to compute the great circle distance between two points given their latitude and longitudes | |
# | |
# Copyright (C) 2008, 360VL, Inc | |
# Copyright (C) 2008, Landon Cox | |
# | |
# http://www.esawdust.com (Landon Cox) | |
# contact: | |
# http://www.esawdust.com/blog/businesscard/businesscard.html | |
# | |
# LICENSE: GNU Affero GPL v3 | |
# The ruby implementation of the Haversine formula is free software: you can redistribute it and/or modify | |
# it under the terms of the GNU Affero General Public License version 3 as published by the Free Software Foundation. | |
# | |
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the | |
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public | |
# License version 3 for more details. http://www.gnu.org/licenses/ | |
# | |
# Landon Cox - 9/25/08 | |
# | |
# Notes: | |
# | |
# translated into Ruby based on information contained in: | |
# http://mathforum.org/library/drmath/view/51879.html Doctors Rick and Peterson - 4/20/99 | |
# http://www.movable-type.co.uk/scripts/latlong.html | |
# http://en.wikipedia.org/wiki/Haversine_formula | |
# | |
# This formula can compute accurate distances between two points given latitude and longitude, even for | |
# short distances. | |
# | |
# The rest shamelessly inspired by my friend tbhar https://gist.github.com/559482 | |
# | |
# PI = 3.1415926535 | |
RAD_PER_DEG = 0.017453293 # PI/180 | |
KMS_PER_MILE = 1.609 | |
NMS_PER_MILE = 0.868976242 | |
EARTH_RADIUS_IN_MILES = 3963.19 | |
EARTH_RADIUS_IN_KMS = EARTH_RADIUS_IN_MILES * KMS_PER_MILE | |
EARTH_RADIUS_IN_NMS = EARTH_RADIUS_IN_MILES * NMS_PER_MILE | |
MILES_PER_LATITUDE_DEGREE = 69.1 | |
KMS_PER_LATITUDE_DEGREE = MILES_PER_LATITUDE_DEGREE * KMS_PER_MILE | |
NMS_PER_LATITUDE_DEGREE = MILES_PER_LATITUDE_DEGREE * NMS_PER_MILE | |
LATITUDE_DEGREES = EARTH_RADIUS_IN_MILES / MILES_PER_LATITUDE_DEGREE | |
@distances = Hash.new # this is global because if computing lots of track point distances, it didn't make | |
# sense to new a Hash each time over potentially 100's of thousands of points | |
=begin rdoc | |
given two lat/lon points, compute the distance between the two points using the haversine formula | |
the result will be a Hash of distances which are key'd by 'mi','km','ft', and 'm' | |
=end | |
def haversine_distance(lat1, lon1, lat2, lon2, unit=:mi) | |
dlon = lon2 - lon1 | |
dlat = lat2 - lat1 | |
dlon_rad = dlon * RAD_PER_DEG | |
dlat_rad = dlat * RAD_PER_DEG | |
lat1_rad = lat1 * RAD_PER_DEG | |
lat2_rad = lat2 * RAD_PER_DEG | |
a = (Math.sin(dlat_rad/2))**2 + Math.cos(lat1_rad) * Math.cos(lat2_rad) * (Math.sin(dlon_rad/2))**2 | |
c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)) | |
if unit == :km | |
EARTH_RADIUS_IN_KMS * c # delta in kilometers | |
else | |
EARTH_RADIUS_IN_MILES * c # delta between the two points in miles | |
end | |
end | |
def units_per_longitude_degree(lat, units=:mi) | |
miles_per_longitude_degree = (LATITUDE_DEGREES * Math.cos(lat * RAD_PER_DEG)).abs | |
case units | |
when :km; | |
miles_per_longitude_degree * KMS_PER_MILE | |
else | |
miles_per_longitude_degree | |
end | |
end | |
module Geo | |
# Mixin for a better nearby search on mongo < 1.8 (can't handle spherical search, do it here) | |
# Tested with mongoid && allow_dynamic_fields=true | |
module NearbyFinder | |
#In a mongo based model: | |
# class MongoModel | |
# extend ::Geo::NearbyFinder | |
# @location_field = 'coords' | |
# | |
# def self.collection | |
# #must be there (but defined by mongoid if you are using it) | |
# end | |
# end | |
# | |
# res = MongoModel.find_near([lon, lat], radius...) | |
# res.first.['distance'] | |
# | |
# coord field is an array representing [X, Y] -> [lon, lat] | |
# | |
# Given a class, try to extrapolate an appropriate location field based on a | |
# class instance variable or `location_field` method. | |
def location_field_from_klass(klass) | |
klass.instance_variable_get(:@location_field) || | |
(klass.respond_to?(:location_field) and klass.location_field) | |
end | |
def find_near(center, radius=75, unit=:mi, filter={}) | |
radius = radius/units_per_longitude_degree(center[1], unit) | |
field = location_field_from_klass(self) | |
query = {field => {"$within" => {"$center" => [center, radius]}}}.merge(filter) | |
self.collection.find(query).sort_by do |r| | |
r['distance'] = haversine_distance(r[field][0], r[field][1], center[0], center[1]) | |
end | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment