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, nil if not found or if network error.
#
def self.fetch_coordinates(query)
data = self.search(query)
# Make sure search found a result.
unless data and data['kml']['response']['status']['code'] == "200"
return nil
end
# Isolate the relevant part of the result.
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
# Fetch coordinates based on the object's object's +location+. Returns an
# array <tt>[lat,lon]</tt>.
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)
Alex Reisner
committed
return c
else
return nil
# Calculate the distance between two points (Haversine formula). Takes two
# sets of coordinates and an options hash:
Alex Reisner
committed
#
# :units : <tt>:mi</tt> for miles (default), <tt>:km</tt> for kilometers
#
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
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
132
133
134
135
136
137
138
139
140
141
142
##
# 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