2 require 'rexml/document'
7 # Contains a set of geocoders which can be used independently if desired. The list contains:
9 # * Google Geocoder - requires an API key.
10 # * Yahoo Geocoder - requires an API key.
11 # * Geocoder.us - may require authentication if performing more than the free request limit.
12 # * Geocoder.ca - for Canada; may require authentication as well.
13 # * IP Geocoder - geocodes an IP address using hostip.info's web service.
14 # * Multi Geocoder - provides failover for the physical location geocoders.
16 # Some configuration is required for these geocoders and can be located in the environment
17 # configuration files.
24 @@yahoo = 'REPLACE_WITH_YOUR_YAHOO_KEY'
25 @@google = 'REPLACE_WITH_YOUR_GOOGLE_KEY'
28 @@provider_order = [:google,:us]
30 [:yahoo, :google, :geocoder_us, :geocoder_ca, :provider_order, :timeout,
31 :proxy_addr, :proxy_port, :proxy_user, :proxy_pass].each do |sym|
32 class_eval <<-EOS, __FILE__, __LINE__
34 if defined?(#{sym.to_s.upcase})
47 # Error which is thrown in the event a geocoding error occurs.
48 class GeocodeError < StandardError; end
50 # The Geocoder base class which defines the interface to be used by all
53 # Main method which calls the do_geocode template method which subclasses
54 # are responsible for implementing. Returns a populated GeoLoc or an
55 # empty one with a failed success code.
56 def self.geocode(address)
57 res = do_geocode(address)
58 return res.success ? res : GeoLoc.new
61 # Call the geocoder service using the timeout if configured.
62 def self.call_geocoder_service(url)
63 timeout(GeoKit::Geocoders::timeout) { return self.do_get(url) } if GeoKit::Geocoders::timeout
64 return self.do_get(url)
71 def self.logger() RAILS_DEFAULT_LOGGER; end
75 # Wraps the geocoder call around a proxy if necessary.
77 return Net::HTTP::Proxy(GeoKit::Geocoders::proxy_addr, GeoKit::Geocoders::proxy_port,
78 GeoKit::Geocoders::proxy_user, GeoKit::Geocoders::proxy_pass).get_response(URI.parse(url))
81 # Adds subclass' geocode method making it conveniently available through
83 def self.inherited(clazz)
84 class_name = clazz.name.split('::').last
86 def self.#{class_name.underscore}(address)
87 #{class_name}.geocode(address)
94 # Geocoder CA geocoder implementation. Requires the GeoKit::Geocoders::GEOCODER_CA variable to
95 # contain true or false based upon whether authentication is to occur. Conforms to the
96 # interface set by the Geocoder class.
98 # Returns a response like:
99 # <?xml version="1.0" encoding="UTF-8" ?>
101 # <latt>49.243086</latt>
102 # <longt>-123.153684</longt>
104 class CaGeocoder < Geocoder
108 # Template method which does the geocode lookup.
109 def self.do_geocode(address)
110 raise ArgumentError('Geocoder.ca requires a GeoLoc argument') unless address.is_a?(GeoLoc)
111 url = construct_request(address)
112 res = self.call_geocoder_service(url)
113 return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
115 logger.debug "Geocoder.ca geocoding. Address: #{address}. Result: #{xml}"
116 # Parse the document.
117 doc = REXML::Document.new(xml)
118 address.lat = doc.elements['//latt'].text
119 address.lng = doc.elements['//longt'].text
120 address.success = true
123 logger.error "Caught an error during Geocoder.ca geocoding call: "+$!
127 # Formats the request in the format acceptable by the CA geocoder.
128 def self.construct_request(location)
130 url += add_ampersand(url) + "stno=#{location.street_number}" if location.street_address
131 url += add_ampersand(url) + "addresst=#{CGI.escape(location.street_name)}" if location.street_address
132 url += add_ampersand(url) + "city=#{CGI.escape(location.city)}" if location.city
133 url += add_ampersand(url) + "prov=#{location.state}" if location.state
134 url += add_ampersand(url) + "postal=#{location.zip}" if location.zip
135 url += add_ampersand(url) + "auth=#{GeoKit::Geocoders::geocoder_ca}" if GeoKit::Geocoders::geocoder_ca
136 url += add_ampersand(url) + "geoit=xml"
137 'http://geocoder.ca/?' + url
140 def self.add_ampersand(url)
141 url && url.length > 0 ? "&" : ""
145 # Google geocoder implementation. Requires the GeoKit::Geocoders::GOOGLE variable to
146 # contain a Google API key. Conforms to the interface set by the Geocoder class.
147 class GoogleGeocoder < Geocoder
151 # Template method which does the geocode lookup.
152 def self.do_geocode(address)
153 address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
154 res = self.call_geocoder_service("http://maps.google.com/maps/geo?q=#{CGI.escape(address_str)}&output=xml&key=#{GeoKit::Geocoders::google}&oe=utf-8")
155 # res = Net::HTTP.get_response(URI.parse("http://maps.google.com/maps/geo?q=#{CGI.escape(address_str)}&output=xml&key=#{GeoKit::Geocoders::google}&oe=utf-8"))
156 return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
158 logger.debug "Google geocoding. Address: #{address}. Result: #{xml}"
159 doc=REXML::Document.new(xml)
161 if doc.elements['//kml/Response/Status/code'].text == '200'
163 coordinates=doc.elements['//coordinates'].text.to_s.split(',')
166 res.lat=coordinates[1]
167 res.lng=coordinates[0]
168 res.country_code=doc.elements['//CountryNameCode'].text
169 res.provider='google'
171 #extended -- false if not not available
172 res.city = doc.elements['//LocalityName'].text if doc.elements['//LocalityName']
173 res.state = doc.elements['//AdministrativeAreaName'].text if doc.elements['//AdministrativeAreaName']
174 res.full_address = doc.elements['//address'].text if doc.elements['//address'] # google provides it
175 res.zip = doc.elements['//PostalCodeNumber'].text if doc.elements['//PostalCodeNumber']
176 res.street_address = doc.elements['//ThoroughfareName'].text if doc.elements['//ThoroughfareName']
177 # Translate accuracy into Yahoo-style token address, street, zip, zip+4, city, state, country
178 # For Google, 1=low accuracy, 8=high accuracy
179 address_details=doc.elements['//AddressDetails','urn:oasis:names:tc:ciq:xsdschema:xAL:2.0']
180 accuracy = address_details ? address_details.attributes['Accuracy'].to_i : 0
181 res.precision=%w{unknown country state state city zip zip+4 street address}[accuracy]
186 logger.info "Google was unable to geocode address: "+address
191 logger.error "Caught an error during Google geocoding call: "+$!
196 # Provides geocoding based upon an IP address. The underlying web service is a hostip.info
197 # which sources their data through a combination of publicly available information as well
198 # as community contributions.
199 class IpGeocoder < Geocoder
203 # Given an IP address, returns a GeoLoc instance which contains latitude,
204 # longitude, city, and country code. Sets the success attribute to false if the ip
205 # parameter does not match an ip address.
206 def self.do_geocode(ip)
207 return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip)
208 url = "http://api.hostip.info/get_html.php?ip=#{ip}&position=true"
209 response = self.call_geocoder_service(url)
210 response.is_a?(Net::HTTPSuccess) ? parse_body(response.body) : GeoLoc.new
212 logger.error "Caught an error during HostIp geocoding call: "+$!
216 # Converts the body to YAML since its in the form of:
218 # Country: UNITED STATES (US)
219 # City: Sugar Grove, IL
221 # Longitude: -88.4588
223 # then instantiates a GeoLoc instance to populate with location data.
224 def self.parse_body(body) # :nodoc:
225 yaml = YAML.load(body)
227 res.provider = 'hostip'
228 res.city, res.state = yaml['City'].split(', ')
229 country, res.country_code = yaml['Country'].split(' (')
230 res.lat = yaml['Latitude']
231 res.lng = yaml['Longitude']
232 res.country_code.chop!
233 res.success = res.city != "(Private Address)"
238 # Geocoder Us geocoder implementation. Requires the GeoKit::Geocoders::GEOCODER_US variable to
239 # contain true or false based upon whether authentication is to occur. Conforms to the
240 # interface set by the Geocoder class.
241 class UsGeocoder < Geocoder
245 # For now, the geocoder_method will only geocode full addresses -- not zips or cities in isolation
246 def self.do_geocode(address)
247 address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
248 url = "http://"+(GeoKit::Geocoders::geocoder_us || '')+"geocoder.us/service/csv/geocode?address=#{CGI.escape(address_str)}"
249 res = self.call_geocoder_service(url)
250 return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
252 logger.debug "Geocoder.us geocoding. Address: #{address}. Result: #{data}"
253 array = data.chomp.split(',')
257 res.lat,res.lng,res.street_address,res.city,res.state,res.zip=array
258 res.country_code='US'
262 logger.info "geocoder.us was unable to geocode address: "+address
266 logger.error "Caught an error during geocoder.us geocoding call: "+$!
271 # Yahoo geocoder implementation. Requires the GeoKit::Geocoders::YAHOO variable to
272 # contain a Yahoo API key. Conforms to the interface set by the Geocoder class.
273 class YahooGeocoder < Geocoder
277 # Template method which does the geocode lookup.
278 def self.do_geocode(address)
279 address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
280 url="http://api.local.yahoo.com/MapsService/V1/geocode?appid=#{GeoKit::Geocoders::yahoo}&location=#{CGI.escape(address_str)}"
281 res = self.call_geocoder_service(url)
282 return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
284 doc = REXML::Document.new(xml)
285 logger.debug "Yahoo geocoding. Address: #{address}. Result: #{xml}"
287 if doc.elements['//ResultSet']
291 res.lat=doc.elements['//Latitude'].text
292 res.lng=doc.elements['//Longitude'].text
293 res.country_code=doc.elements['//Country'].text
296 #extended - false if not available
297 res.city=doc.elements['//City'].text if doc.elements['//City'] && doc.elements['//City'].text != nil
298 res.state=doc.elements['//State'].text if doc.elements['//State'] && doc.elements['//State'].text != nil
299 res.zip=doc.elements['//Zip'].text if doc.elements['//Zip'] && doc.elements['//Zip'].text != nil
300 res.street_address=doc.elements['//Address'].text if doc.elements['//Address'] && doc.elements['//Address'].text != nil
301 res.precision=doc.elements['//Result'].attributes['precision'] if doc.elements['//Result']
305 logger.info "Yahoo was unable to geocode address: "+address
310 logger.info "Caught an error during Yahoo geocoding call: "+$!
315 # Provides methods to geocode with a variety of geocoding service providers, plus failover
316 # among providers in the order you configure.
319 # - homogenize the results of multiple geocoders
322 # - currently only provides the first result. Sometimes geocoders will return multiple results.
323 # - currently discards the "accuracy" component of the geocoding calls
324 class MultiGeocoder < Geocoder
327 # This method will call one or more geocoders in the order specified in the
328 # configuration until one of the geocoders work.
330 # The failover approach is crucial for production-grade apps, but is rarely used.
331 # 98% of your geocoding calls will be successful with the first call
332 def self.do_geocode(address)
333 GeoKit::Geocoders::provider_order.each do |provider|
335 klass = GeoKit::Geocoders.const_get "#{provider.to_s.capitalize}Geocoder"
336 res = klass.send :geocode, address
337 return res if res.success
339 logger.error("Something has gone very wrong during geocoding, OR you have configured an invalid class name in GeoKit::Geocoders::provider_order. Address: #{address}. Provider: #{provider}")
342 # If we get here, we failed completely.