##
# Add geocoding functionality (via Google) to any object that implements
# 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
##
# Search Google based on the object's +location+ attribute.
#
def fetch_coordinates(attribute = :location)
Geocoder.fetch_coordinates(send(attribute))
end
##
# Fetch and assign +latitude+ and +longitude+.
#
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
##
# 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