]> projects.mako.cc - selectricity/blob - vendor/plugins/geokit/lib/geo_kit/geocoders.rb
merged back from live
[selectricity] / vendor / plugins / geokit / lib / geo_kit / geocoders.rb
1 require 'net/http'
2 require 'rexml/document'
3 require 'yaml'
4 require 'timeout'
5
6 module GeoKit
7   # Contains a set of geocoders which can be used independently if desired.  The list contains:
8   # 
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.
15   # 
16   # Some configuration is required for these geocoders and can be located in the environment
17   # configuration files.
18   module Geocoders
19     @@proxy_addr = nil
20     @@proxy_port = nil
21     @@proxy_user = nil
22     @@proxy_pass = nil
23     @@timeout = nil    
24     @@yahoo = 'REPLACE_WITH_YOUR_YAHOO_KEY'
25     @@google = 'REPLACE_WITH_YOUR_GOOGLE_KEY'
26     @@geocoder_us = false
27     @@geocoder_ca = false
28     @@provider_order = [:google,:us]
29     
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__
33         def self.#{sym}
34           if defined?(#{sym.to_s.upcase})
35             #{sym.to_s.upcase}
36           else
37             @@#{sym}
38           end
39         end
40
41         def self.#{sym}=(obj)
42           @@#{sym} = obj
43         end
44       EOS
45     end
46     
47     # Error which is thrown in the event a geocoding error occurs.
48     class GeocodeError < StandardError; end
49     
50     # The Geocoder base class which defines the interface to be used by all
51     # other geocoders.
52     class Geocoder   
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
59       end  
60       
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)
65       rescue TimeoutError
66         return nil  
67       end
68
69       protected
70
71       def self.logger() RAILS_DEFAULT_LOGGER; end
72       
73       private
74       
75       # Wraps the geocoder call around a proxy if necessary.
76       def self.do_get(url)     
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))          
79       end
80       
81       # Adds subclass' geocode method making it conveniently available through 
82       # the base class.
83       def self.inherited(clazz)
84         class_name = clazz.name.split('::').last
85         src = <<-END_SRC
86           def self.#{class_name.underscore}(address)
87             #{class_name}.geocode(address)
88           end
89         END_SRC
90         class_eval(src)
91       end
92     end
93     
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.
97     #
98     # Returns a response like:
99     # <?xml version="1.0" encoding="UTF-8" ?>
100     # <geodata>
101     #   <latt>49.243086</latt>
102     #   <longt>-123.153684</longt>
103     # </geodata>
104     class CaGeocoder < Geocoder
105
106       private
107
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)
114         xml = res.body
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
121         return address
122       rescue
123         logger.error "Caught an error during Geocoder.ca geocoding call: "+$!
124         return GeoLoc.new  
125       end  
126
127       # Formats the request in the format acceptable by the CA geocoder.
128       def self.construct_request(location)
129         url = ""
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
138       end
139
140       def self.add_ampersand(url)
141         url && url.length > 0 ? "&" : ""
142       end
143     end    
144     
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
148
149       private 
150
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)
157         xml=res.body
158         logger.debug "Google geocoding. Address: #{address}. Result: #{xml}"
159         doc=REXML::Document.new(xml)
160
161         if doc.elements['//kml/Response/Status/code'].text == '200'
162           res = GeoLoc.new
163           coordinates=doc.elements['//coordinates'].text.to_s.split(',')
164
165           #basics
166           res.lat=coordinates[1]
167           res.lng=coordinates[0]
168           res.country_code=doc.elements['//CountryNameCode'].text
169           res.provider='google'
170
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]
182           res.success=true
183           
184           return res
185         else 
186           logger.info "Google was unable to geocode address: "+address
187           return GeoLoc.new
188         end
189
190         rescue
191           logger.error "Caught an error during Google geocoding call: "+$!
192           return GeoLoc.new
193       end  
194     end
195     
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 
200
201       private 
202
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
211       rescue
212         logger.error "Caught an error during HostIp geocoding call: "+$!
213         return GeoLoc.new
214       end
215
216       # Converts the body to YAML since its in the form of:
217       #
218       # Country: UNITED STATES (US)
219       # City: Sugar Grove, IL
220       # Latitude: 41.7696
221       # Longitude: -88.4588
222       #
223       # then instantiates a GeoLoc instance to populate with location data.
224       def self.parse_body(body) # :nodoc:
225         yaml = YAML.load(body)
226         res = GeoLoc.new
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)"
234         res
235       end
236     end
237     
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
242
243       private
244
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)
251         data = res.body
252         logger.debug "Geocoder.us geocoding. Address: #{address}. Result: #{data}"
253         array = data.chomp.split(',')
254
255         if array.length == 6  
256           res=GeoLoc.new
257           res.lat,res.lng,res.street_address,res.city,res.state,res.zip=array
258           res.country_code='US'
259           res.success=true 
260           return res
261         else 
262           logger.info "geocoder.us was unable to geocode address: "+address
263           return GeoLoc.new      
264         end
265         rescue 
266           logger.error "Caught an error during geocoder.us geocoding call: "+$!
267           return GeoLoc.new
268       end
269     end
270     
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
274
275       private 
276
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)
283         xml = res.body
284         doc = REXML::Document.new(xml)
285         logger.debug "Yahoo geocoding. Address: #{address}. Result: #{xml}"
286
287         if doc.elements['//ResultSet']
288           res=GeoLoc.new
289
290           #basic      
291           res.lat=doc.elements['//Latitude'].text
292           res.lng=doc.elements['//Longitude'].text
293           res.country_code=doc.elements['//Country'].text
294           res.provider='yahoo'  
295
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']
302           res.success=true
303           return res
304         else 
305           logger.info "Yahoo was unable to geocode address: "+address
306           return GeoLoc.new
307         end   
308
309         rescue 
310           logger.info "Caught an error during Yahoo geocoding call: "+$!
311           return GeoLoc.new
312       end
313     end
314     
315     # Provides methods to geocode with a variety of geocoding service providers, plus failover
316     # among providers in the order you configure.
317     # 
318     # Goal:
319     # - homogenize the results of multiple geocoders
320     # 
321     # Limitations:
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 
325       private
326
327       # This method will call one or more geocoders in the order specified in the 
328       # configuration until one of the geocoders work.
329       # 
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|
334           begin
335             klass = GeoKit::Geocoders.const_get "#{provider.to_s.capitalize}Geocoder"
336             res = klass.send :geocode, address
337             return res if res.success
338           rescue
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}")
340           end
341         end
342         # If we get here, we failed completely.
343         GeoLoc.new
344       end
345     end   
346   end
347 end

Benjamin Mako Hill || Want to submit a patch?