Skip to content
Snippets Groups Projects
Commit fbca35bc authored by Alex Reisner's avatar Alex Reisner
Browse files

Create Geocoder::Calculations module.

Move some methods into it (organize!).
parent dce6ddbc
No related branches found
No related tags found
No related merge requests found
......@@ -68,13 +68,13 @@ To find objects by location, use the following scopes:
Some utility methods are also available:
# distance (in miles) between Eiffel Tower and Empire State Building
Geocoder.distance_between( 48.858205,2.294359, 40.748433,-73.985655 )
Geocoder::Calculations.distance_between( 48.858205,2.294359, 40.748433,-73.985655 )
# look up coordinates of some location (like searching Google Maps)
Geocoder.fetch_coordinates("25 Main St, Cooperstown, NY")
# find the geographic center (aka center of gravity) of objects or points
Geocoder.geographic_center([ city1, city2, city3, [40.22,-73.99], city4 ])
Geocoder::Calculations.geographic_center([ city1, city2, city3, [40.22,-73.99], city4 ])
== More On Configuration
......@@ -114,9 +114,9 @@ There are few options for finding objects near a given point in SQLite without i
1. Use a square instead of a circle for finding nearby points. For example, if you want to find points near 40.71, 100.23, search for objects with latitude between 39.71 and 41.71 and longitude between 99.23 and 101.23. One degree of latitude or longitude is at most 69 miles so divide your radius (in miles) by 69.0 to get the amount to add and subtract from your center coordinates to get the upper and lower bounds. The results will not be very accurate (you'll get points outside the desired radius--at worst 29% farther away), but you will get all the points within the required radius.
2. Load all objects into memory and compute distances between them using the <tt>Geocoder.distance_between</tt> method. This will produce accurate results but will be very slow (and use a lot of memory) if you have a lot of objects in your database.
2. Load all objects into memory and compute distances between them using the <tt>Geocoder::Calculations.distance_between</tt> method. This will produce accurate results but will be very slow (and use a lot of memory) if you have a lot of objects in your database.
3. If you have a large number of objects (so you can't use approach #2) and you need accurate results (better than approach #1 will give), you can use a combination of the two. Get all the objects within a square around your center point, and then eliminate the ones that are too far away using <tt>Geocoder.distance_between</tt>.
3. If you have a large number of objects (so you can't use approach #2) and you need accurate results (better than approach #1 will give), you can use a combination of the two. Get all the objects within a square around your center point, and then eliminate the ones that are too far away using <tt>Geocoder::Calculations.distance_between</tt>.
Because Geocoder needs to provide this functionality as a scope, we must go with option #1, but feel free to implement #2 or #3 if you need more accuracy.
......
require "geocoder/calculations"
##
# Add geocoding functionality (via Google) to any object.
#
......@@ -55,7 +57,7 @@ module Geocoder
# +select+ :: string with the SELECT SQL fragment (e.g. “id, name”)
#
def near_scope_options(latitude, longitude, radius = 20, options = {})
radius *= km_in_mi if options[:units] == :km
radius *= Geocoder::Calculations.km_in_mi if options[:units] == :km
if ActiveRecord::Base.connection.adapter_name == "SQLite"
approx_near_scope_options(latitude, longitude, radius, options)
else
......@@ -138,13 +140,6 @@ module Geocoder
longitude + (radius / factor)
]
end
##
# Conversion factor: km to mi.
#
def km_in_mi
0.621371192
end
end
##
......@@ -164,17 +159,19 @@ module Geocoder
##
# Calculate the distance from the object to a point (lat,lon).
# Valid units are defined in <tt>distance_between</tt> class method.
#
# <tt>:units</tt> :: <tt>:mi</tt> (default) or <tt>:km</tt>
#
def distance_to(lat, lon, units = :mi)
return nil unless geocoded?
mylat,mylon = read_coordinates
Geocoder.distance_between(mylat, mylon, lat, lon, :units => units)
Geocoder::Calculations.distance_between(mylat, mylon, lat, lon, :units => units)
end
##
# Get other geocoded objects within a given radius.
# Valid units are defined in <tt>distance_between</tt> class method.
#
# <tt>:units</tt> :: <tt>:mi</tt> (default) or <tt>:km</tt>
#
def nearbys(radius = 20, units = :mi)
return [] unless geocoded?
......@@ -205,88 +202,6 @@ module Geocoder
fetch_coordinates(true)
end
##
# Calculate the distance between two points on Earth (Haversine formula).
# Takes two sets of coordinates and an options hash:
#
# <tt>:units</tt> :: <tt>:mi</tt> (default) or <tt>:km</tt>
#
def self.distance_between(lat1, lon1, lat2, lon2, options = {})
# set default options
options[:units] ||= :mi
# define conversion factors
conversions = { :mi => 3956, :km => 6371 }
# convert degrees to radians
lat1 = to_radians(lat1)
lon1 = to_radians(lon1)
lat2 = to_radians(lat2)
lon2 = to_radians(lon2)
# compute distances
dlat = (lat1 - lat2).abs
dlon = (lon1 - lon2).abs
a = (Math.sin(dlat / 2))**2 + Math.cos(lat1) *
(Math.sin(dlon / 2))**2 * Math.cos(lat2)
c = 2 * Math.atan2( Math.sqrt(a), Math.sqrt(1-a))
c * conversions[options[:units]]
end
##
# Compute the geographic center (aka geographic midpoint, center of
# gravity) for an array of geocoded objects and/or [lat,lon] arrays
# (can be mixed). Any objects missing coordinates are ignored. Follows
# the procedure documented at http://www.geomidpoint.com/calculation.html.
#
def self.geographic_center(points)
# convert objects to [lat,lon] arrays and remove nils
points = points.map{ |p|
p.is_a?(Array) ? p : (p.geocoded?? p.read_coordinates : nil)
}.compact
# convert degrees to radians
points.map!{ |p| [to_radians(p[0]), to_radians(p[1])] }
# convert to Cartesian coordinates
x = []; y = []; z = []
points.each do |p|
x << Math.cos(p[0]) * Math.cos(p[1])
y << Math.cos(p[0]) * Math.sin(p[1])
z << Math.sin(p[0])
end
# compute average coordinate values
xa, ya, za = [x,y,z].map do |c|
c.inject(0){ |tot,i| tot += i } / c.size.to_f
end
# convert back to latitude/longitude
lon = Math.atan2(ya, xa)
hyp = Math.sqrt(xa**2 + ya**2)
lat = Math.atan2(za, hyp)
# return answer in degrees
[to_degrees(lat), to_degrees(lon)]
end
##
# Convert degrees to radians.
#
def self.to_radians(degrees)
degrees * (Math::PI / 180)
end
##
# Convert radians to degrees.
#
def self.to_degrees(radians)
(radians * 180.0) / Math::PI
end
##
# Query Google for geographic information about the given phrase.
# Returns a hash representing a valid geocoder response.
......
module Geocoder
module Calculations
extend self
##
# Calculate the distance between two points on Earth (Haversine formula).
# Takes two sets of coordinates and an options hash:
#
# <tt>:units</tt> :: <tt>:mi</tt> (default) or <tt>:km</tt>
#
def distance_between(lat1, lon1, lat2, lon2, options = {})
# set default options
options[:units] ||= :mi
# define conversion factors
conversions = { :mi => 3956, :km => 6371 }
# convert degrees to radians
lat1 = to_radians(lat1)
lon1 = to_radians(lon1)
lat2 = to_radians(lat2)
lon2 = to_radians(lon2)
# compute distances
dlat = (lat1 - lat2).abs
dlon = (lon1 - lon2).abs
a = (Math.sin(dlat / 2))**2 + Math.cos(lat1) *
(Math.sin(dlon / 2))**2 * Math.cos(lat2)
c = 2 * Math.atan2( Math.sqrt(a), Math.sqrt(1-a))
c * conversions[options[:units]]
end
##
# Compute the geographic center (aka geographic midpoint, center of
# gravity) for an array of geocoded objects and/or [lat,lon] arrays
# (can be mixed). Any objects missing coordinates are ignored. Follows
# the procedure documented at http://www.geomidpoint.com/calculation.html.
#
def geographic_center(points)
# convert objects to [lat,lon] arrays and remove nils
points = points.map{ |p|
p.is_a?(Array) ? p : (p.geocoded?? p.read_coordinates : nil)
}.compact
# convert degrees to radians
points.map!{ |p| [to_radians(p[0]), to_radians(p[1])] }
# convert to Cartesian coordinates
x = []; y = []; z = []
points.each do |p|
x << Math.cos(p[0]) * Math.cos(p[1])
y << Math.cos(p[0]) * Math.sin(p[1])
z << Math.sin(p[0])
end
# compute average coordinate values
xa, ya, za = [x,y,z].map do |c|
c.inject(0){ |tot,i| tot += i } / c.size.to_f
end
# convert back to latitude/longitude
lon = Math.atan2(ya, xa)
hyp = Math.sqrt(xa**2 + ya**2)
lat = Math.atan2(za, hyp)
# return answer in degrees
[to_degrees(lat), to_degrees(lon)]
end
##
# Convert degrees to radians.
#
def to_radians(degrees)
degrees * (Math::PI / 180)
end
##
# Convert radians to degrees.
#
def to_degrees(radians)
(radians * 180.0) / Math::PI
end
##
# Conversion factor: km to mi.
#
def km_in_mi
0.621371192
end
end
end
......@@ -10,14 +10,14 @@ class GeocoderTest < Test::Unit::TestCase
# sanity check
def test_distance_between
assert_equal 69, Geocoder.distance_between(0,0, 0,1).round
assert_equal 69, Geocoder::Calculations.distance_between(0,0, 0,1).round
end
# sanity check
def test_geographic_center
assert_equal [0.0, 0.5],
Geocoder.geographic_center([[0,0], [0,1]])
Geocoder::Calculations.geographic_center([[0,0], [0,1]])
assert_equal [0.0, 1.0],
Geocoder.geographic_center([[0,0], [0,1], [0,2]])
Geocoder::Calculations.geographic_center([[0,0], [0,1], [0,2]])
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment