Newer
Older
##
# Add geocoding functionality (via Google) to any object that implements
Alex Reisner
committed
# a method (+location+ by default) that returns a string suitable for a
# Google Maps search. The object should also implement reader and writer
# methods for +latitude+ and +longitude+ attributes.
#
module Geocoder
##
# Query Google for the coordinates of the given phrase.
# Returns array [lat,lon] if found.
#
def self.fetch_coordinates(query)
data = self.search(query)
# Make sure search found a result.
return nil unless data['kml']['response']['status']['code'] == "200"
place = data['kml']['response']['placemark']
# If there are multiple results, blindly use the first.
place = place.first if place.is_a?(Array)
coords = place['point']['coordinates']
coords.split(',')[0...2].reverse.map{ |i| i.to_f }
end
##
# Calculate the distance from the object to a point (lat,lon). Valid units
# are defined in <tt>distance_between</tt> class method.
#
def distance_to(lat, lon, units = :mi)
Geocoder.distance_between(latitude, longitude, lat, lon, :units => units)
end
##
# Search Google based on the object's +location+ attribute.
#
Alex Reisner
committed
def fetch_coordinates(attribute = :location)
Geocoder.fetch_coordinates(send(attribute))
Alex Reisner
committed
# Fetch and assign +latitude+ and +longitude+.
Alex Reisner
committed
def fetch_and_assign_coordinates(attribute = :location)
if c = fetch_coordinates(attribute)
self.latitude = c[0]
self.longitude = c[1]
end
end
##
# Calculate the distance between two points (Haversine formula).
#
def self.distance_between(lat1, lon1, lat2, lon2, options = {})
# set default options
options[:units] ||= :mi
# define available units
units = { :mi => 3956, :km => 6371 }
# convert degrees to radians
lat1 *= Math::PI / 180
lon1 *= Math::PI / 180
lat2 *= Math::PI / 180
lon2 *= Math::PI / 180
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 * units[options[:units]]
end
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
##
# Find all records within a radius (in miles) of the given point.
# Taken from excellent tutorial at:
# http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
#
def self.nearby_mysql_query(table, latitude, longitude, radius = 20, options = {})
# Alternate column names.
options[:latitude] ||= 'latitude'
options[:longitude] ||= 'longitude'
# Constrain search to a (radius x radius) square.
factor = (Math::cos(latitude * Math::PI / 180.0) * 69.0).abs
lon_lo = longitude - (radius / factor);
lon_hi = longitude + (radius / factor);
lat_lo = latitude - (radius / 69.0);
lat_hi = latitude + (radius / 69.0);
where = "#{options[:latitude]} BETWEEN #{lat_lo} AND #{lat_hi} AND " +
"#{options[:longitude]} BETWEEN #{lon_lo} AND #{lon_hi}"
# Generate query.
"SELECT *, 3956 * 2 * ASIN(SQRT(" +
"POWER(SIN((#{latitude} - #{options[:latitude]}) * " +
"PI() / 180 / 2), 2) + COS(#{latitude} * PI()/180) * " +
"COS(#{options[:latitude]} * PI() / 180) * " +
"POWER(SIN((#{longitude} - #{options[:longitude]}) * " +
"PI() / 180 / 2), 2) )) as distance " +
"FROM #{table} WHERE #{where} HAVING distance <= #{radius}"
end
##
# Query Google for geographic information about the given phrase.
# Returns the XML response as a hash. This method is not intended for
# general use (prefer Geocoder.search).
#
def self.search(query)
params = { :q => query, :output => "xml" }
url = "http://maps.google.com/maps/geo?" + params.to_query
# Query geocoder and make sure it responds quickly.
begin
resp = nil
timeout(3) do
resp = Net::HTTP.get_response(URI.parse(url))
end
rescue SocketError, TimeoutError
return nil
end
# Google's XML document has incorrect encoding (says UTF-8 but is actually
# ISO 8859-1). Have to fix this or REXML won't parse correctly.
# This may be fixed in the future; see the bug report at:
# http://code.google.com/p/gmaps-api-issues/issues/detail?id=233
doc = resp.body.sub('UTF-8', 'ISO-8859-1')
Hash.from_xml(doc)
end
end