lib/geokit/geocoders.rb in geokit-premier-0.0.7 vs lib/geokit/geocoders.rb in geokit-premier-0.1.0

- old
+ new

@@ -4,82 +4,82 @@ require 'yaml' require 'timeout' require 'logger' require 'base64' -# do this just in case -begin +# do this just in case +begin ActiveSupport.nil? rescue NameError require 'json/pure' end module Geokit class TooManyQueriesError < StandardError; end module Inflector - + extend self - + def titleize(word) humanize(underscore(word)).gsub(/\b([a-z])/u) { $1.capitalize } end - + def underscore(camel_cased_word) camel_cased_word.to_s.gsub(/::/, '/'). gsub(/([A-Z]+)([A-Z][a-z])/u,'\1_\2'). gsub(/([a-z\d])([A-Z])/u,'\1_\2'). tr("-", "_"). downcase end - + def humanize(lower_case_and_underscored_word) lower_case_and_underscored_word.to_s.gsub(/_id$/, "").gsub(/_/, " ").capitalize end - + def snake_case(s) return s.downcase if s =~ /^[A-Z]+$/u s.gsub(/([A-Z]+)(?=[A-Z][a-z]?)|\B[A-Z]/u, '_\&') =~ /_*(.*)/ return $+.downcase - + end - + def url_escape(s) URI.escape(s) end - + def camelize(str) str.split('_').map {|w| w.capitalize}.join end - end - + end + # Contains a range of geocoders: - # - # ### "regular" address geocoders + # + # ### "regular" address geocoders # * Yahoo Geocoder - requires an API key. # * Geocoder.us - may require authentication if performing more than the free request limit. # * Geocoder.ca - for Canada; may require authentication as well. # * Geonames - a free geocoder # - # ### address geocoders that also provide reverse geocoding + # ### address geocoders that also provide reverse geocoding # * Google Geocoder - requires an API key. - # - # ### IP address geocoders + # + # ### IP address geocoders # * IP Geocoder - geocodes an IP address using hostip.info's web service. # * Geoplugin.net -- another IP address geocoder # # ### The Multigeocoder # * Multi Geocoder - provides failover for the physical location geocoders. - # + # # Some of these geocoders require configuration. You don't have to provide it here. See the README. module Geocoders @@proxy_addr = nil @@proxy_port = nil @@proxy_user = nil @@proxy_pass = nil - @@request_timeout = nil + @@request_timeout = nil @@yahoo = 'REPLACE_WITH_YOUR_YAHOO_KEY' @@google = 'REPLACE_WITH_YOUR_GOOGLE_KEY' @@google_client_id = nil #only used for premier accounts @@google_premier_secret_key = nil @@geocoder_us = false @@ -88,13 +88,13 @@ @@provider_order = [:google,:us] @@ip_provider_order = [:geo_plugin,:ip] @@logger=Logger.new(STDOUT) @@logger.level=Logger::INFO @@domain = nil - + def self.__define_accessors - class_variables.each do |v| + class_variables.each do |v| sym = v.to_s.delete("@").to_sym unless self.respond_to? sym module_eval <<-EOS, __FILE__, __LINE__ def self.#{sym} value = if defined?(#{sym.to_s.upcase}) @@ -105,68 +105,68 @@ if value.is_a?(Hash) value = (self.domain.nil? ? nil : value[self.domain]) || value.values.first end value end - + def self.#{sym}=(obj) @@#{sym} = obj end EOS end end end __define_accessors - + # Error which is thrown in the event a geocoding error occurs. class GeocodeError < StandardError; end # ------------------------------------------------------------------------------------------- # Geocoder Base class -- every geocoder should inherit from this - # ------------------------------------------------------------------------------------------- - + # ------------------------------------------------------------------------------------------- + # The Geocoder base class which defines the interface to be used by all # other geocoders. - class Geocoder + class Geocoder # Main method which calls the do_geocode template method which subclasses # are responsible for implementing. Returns a populated GeoLoc or an # empty one with a failed success code. - def self.geocode(address, options = {}) + def self.geocode(address, options = {}) res = do_geocode(address, options) return res.nil? ? GeoLoc.new : res - end + end # Main method which calls the do_reverse_geocode template method which subclasses # are responsible for implementing. Returns a populated GeoLoc or an # empty one with a failed success code. def self.reverse_geocode(latlng) res = do_reverse_geocode(latlng) - return res.success? ? res : GeoLoc.new + return res.success? ? res : GeoLoc.new end - + # Call the geocoder service using the timeout if configured. def self.call_geocoder_service(url) - Timeout::timeout(Geokit::Geocoders::request_timeout) { return self.do_get(url) } if Geokit::Geocoders::request_timeout + Timeout::timeout(Geokit::Geocoders::request_timeout) { return self.do_get(url) } if Geokit::Geocoders::request_timeout return self.do_get(url) rescue TimeoutError - return nil + return nil end # Not all geocoders can do reverse geocoding. So, unless the subclass explicitly overrides this method, # a call to reverse_geocode will return an empty GeoLoc. If you happen to be using MultiGeocoder, # this will cause it to failover to the next geocoder, which will hopefully be one which supports reverse geocoding. def self.do_reverse_geocode(latlng) return GeoLoc.new end - + # This will sign a raw url with a private key def self.sign_url(raw_url,private_key) uri = URI.parse(raw_url) url_to_sign = uri.path + "?" + uri.query decoded_key = Geocoder.urlsafe_decode64(private_key) - sha1_digest = OpenSSL::Digest::Digest.new('sha1') + sha1_digest = OpenSSL::Digest.new('sha1') signature = OpenSSL::HMAC.digest(sha1_digest,decoded_key,url_to_sign) encoded_signature = Geocoder.urlsafe_encode64(signature) signed_url = "#{uri.scheme}://#{uri.host}#{uri.path}?#{uri.query}&signature=#{encoded_signature}".strip! signed_url end @@ -181,33 +181,33 @@ # This will provide url safe base64 encoding def self.urlsafe_encode64(raw_text) encoded_text = Base64.encode64(raw_text) encoded_text = encoded_text.gsub('+','-').gsub('/', '_') encoded_text - end + end protected - def self.logger() + def self.logger() Geokit::Geocoders::logger end - + private - + # Wraps the geocoder call around a proxy if necessary. - def self.do_get(url) + def self.do_get(url) uri = URI.parse(url) req = Net::HTTP::Get.new(url) req.basic_auth(uri.user, uri.password) if uri.userinfo res = Net::HTTP::Proxy(GeoKit::Geocoders::proxy_addr, GeoKit::Geocoders::proxy_port, GeoKit::Geocoders::proxy_user, GeoKit::Geocoders::proxy_pass).start(uri.host, uri.port) { |http| http.get(uri.path + "?" + uri.query) } return res end - - # Adds subclass' geocode method making it conveniently available through + + # Adds subclass' geocode method making it conveniently available through # the base class. def self.inherited(clazz) class_name = clazz.name.split('::').last src = <<-END_SRC def self.#{Geokit::Inflector.underscore(class_name)}(address, options = {}) @@ -218,14 +218,14 @@ end end # ------------------------------------------------------------------------------------------- # "Regular" Address geocoders - # ------------------------------------------------------------------------------------------- - + # ------------------------------------------------------------------------------------------- + # Geocoder CA geocoder implementation. Requires the Geokit::Geocoders::GEOCODER_CA variable to - # contain true or false based upon whether authentication is to occur. Conforms to the + # contain true or false based upon whether authentication is to occur. Conforms to the # interface set by the Geocoder class. # # Returns a response like: # <?xml version="1.0" encoding="UTF-8" ?> # <geodata> @@ -243,19 +243,19 @@ res = self.call_geocoder_service(url) return GeoLoc.new if !res.is_a?(Net::HTTPSuccess) xml = res.body logger.debug "Geocoder.ca geocoding. Address: #{address}. Result: #{xml}" # Parse the document. - doc = REXML::Document.new(xml) + doc = REXML::Document.new(xml) address.lat = doc.elements['//latt'].text address.lng = doc.elements['//longt'].text address.success = true return address rescue logger.error "Caught an error during Geocoder.ca geocoding call: "+$! - return GeoLoc.new - end + return GeoLoc.new + end # Formats the request in the format acceptable by the CA geocoder. def self.construct_request(location) url = "" url += add_ampersand(url) + "stno=#{location.street_number}" if location.street_address @@ -269,64 +269,64 @@ end def self.add_ampersand(url) url && url.length > 0 ? "&" : "" end - end - + end + # Geocoder Us geocoder implementation. Requires the Geokit::Geocoders::GEOCODER_US variable to - # contain true or false based upon whether authentication is to occur. Conforms to the + # contain true or false based upon whether authentication is to occur. Conforms to the # interface set by the Geocoder class. class UsGeocoder < Geocoder private def self.do_geocode(address, options = {}) address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address - + query = (address_str =~ /^\d{5}(?:-\d{4})?$/ ? "zip" : "address") + "=#{Geokit::Inflector::url_escape(address_str)}" - url = if GeoKit::Geocoders::geocoder_us + url = if GeoKit::Geocoders::geocoder_us "http://#{GeoKit::Geocoders::geocoder_us}@geocoder.us/member/service/csv/geocode" else "http://geocoder.us/service/csv/geocode" end - - url = "#{url}?#{query}" + + url = "#{url}?#{query}" res = self.call_geocoder_service(url) - + return GeoLoc.new if !res.is_a?(Net::HTTPSuccess) data = res.body logger.debug "Geocoder.us geocoding. Address: #{address}. Result: #{data}" array = data.chomp.split(',') - + if array.length == 5 res=GeoLoc.new res.lat,res.lng,res.city,res.state,res.zip=array res.country_code='US' res.success=true return res - elsif array.length == 6 - res=GeoLoc.new + elsif array.length == 6 + res=GeoLoc.new res.lat,res.lng,res.street_address,res.city,res.state,res.zip=array res.country_code='US' - res.success=true + res.success=true return res - else + else logger.info "geocoder.us was unable to geocode address: "+address - return GeoLoc.new + return GeoLoc.new end - rescue + rescue logger.error "Caught an error during geocoder.us geocoding call: "+$! return GeoLoc.new end end - + # Yahoo geocoder implementation. Requires the Geokit::Geocoders::YAHOO variable to # contain a Yahoo API key. Conforms to the interface set by the Geocoder class. class YahooGeocoder < Geocoder - private + private # Template method which does the geocode lookup. def self.do_geocode(address, options = {}) address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address url="http://api.local.yahoo.com/MapsService/V1/geocode?appid=#{Geokit::Geocoders::yahoo}&location=#{Geokit::Inflector::url_escape(address_str)}" @@ -337,15 +337,15 @@ logger.debug "Yahoo geocoding. Address: #{address}. Result: #{xml}" if doc.elements['//ResultSet'] res=GeoLoc.new - #basic + #basic res.lat=doc.elements['//Latitude'].text res.lng=doc.elements['//Longitude'].text res.country_code=doc.elements['//Country'].text - res.provider='yahoo' + res.provider='yahoo' #extended - false if not available res.city=doc.elements['//City'].text if doc.elements['//City'] && doc.elements['//City'].text != nil res.state=doc.elements['//State'].text if doc.elements['//State'] && doc.elements['//State'].text != nil res.zip=doc.elements['//Zip'].text if doc.elements['//Zip'] && doc.elements['//Zip'].text != nil @@ -353,66 +353,66 @@ res.precision=doc.elements['//Result'].attributes['precision'] if doc.elements['//Result'] # set the accuracy as google does (added by Andruby) res.accuracy=%w{unknown country state state city zip zip+4 street address building}.index(res.precision) res.success=true return res - else + else logger.info "Yahoo was unable to geocode address: "+address return GeoLoc.new - end + end - rescue + rescue logger.info "Caught an error during Yahoo geocoding call: "+$! return GeoLoc.new end end # Another geocoding web service # http://www.geonames.org class GeonamesGeocoder < Geocoder - private - + private + # Template method which does the geocode lookup. def self.do_geocode(address, options = {}) address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address # geonames need a space seperated search string address_str.gsub!(/,/, " ") params = "/postalCodeSearch?placename=#{Geokit::Inflector::url_escape(address_str)}&maxRows=10" - + if(GeoKit::Geocoders::geonames) url = "http://ws.geonames.net#{params}&username=#{GeoKit::Geocoders::geonames}" else url = "http://ws.geonames.org#{params}" end - + res = self.call_geocoder_service(url) - + return GeoLoc.new if !res.is_a?(Net::HTTPSuccess) - + xml=res.body logger.debug "Geonames geocoding. Address: #{address}. Result: #{xml}" doc=REXML::Document.new(xml) - + if(doc.elements['//geonames/totalResultsCount'].text.to_i > 0) res=GeoLoc.new - + # only take the first result res.lat=doc.elements['//code/lat'].text if doc.elements['//code/lat'] res.lng=doc.elements['//code/lng'].text if doc.elements['//code/lng'] res.country_code=doc.elements['//code/countryCode'].text if doc.elements['//code/countryCode'] - res.provider='genomes' + res.provider='genomes' res.city=doc.elements['//code/name'].text if doc.elements['//code/name'] res.state=doc.elements['//code/adminName1'].text if doc.elements['//code/adminName1'] res.zip=doc.elements['//code/postalcode'].text if doc.elements['//code/postalcode'] res.success=true return res - else + else logger.info "Geonames was unable to geocode address: "+address return GeoLoc.new end - + rescue logger.error "Caught an error during Geonames geocoding call: "+$! end end @@ -422,31 +422,31 @@ # Google geocoder implementation. Requires the Geokit::Geocoders::GOOGLE variable to # contain a Google API key. Conforms to the interface set by the Geocoder class. class GoogleGeocoder < Geocoder - private - + private + # Template method which does the reverse-geocode lookup. - def self.do_reverse_geocode(latlng) + def self.do_reverse_geocode(latlng) latlng=LatLng.normalize(latlng) res = self.call_geocoder_service("http://maps.google.com/maps/geo?ll=#{Geokit::Inflector::url_escape(latlng.ll)}&output=xml&key=#{Geokit::Geocoders::google}&oe=utf-8") # res = Net::HTTP.get_response(URI.parse("http://maps.google.com/maps/geo?ll=#{Geokit::Inflector::url_escape(address_str)}&output=xml&key=#{Geokit::Geocoders::google}&oe=utf-8")) return GeoLoc.new unless (res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPOK)) xml = res.body logger.debug "Google reverse-geocoding. LL: #{latlng}. Result: #{xml}" - return self.xml2GeoLoc(xml) - end + return self.xml2GeoLoc(xml) + end # Template method which does the geocode lookup. # # Supports viewport/country code biasing # # ==== OPTIONS # * :bias - This option makes the Google Geocoder return results biased to a particular # country or viewport. Country code biasing is achieved by passing the ccTLD - # ('uk' for .co.uk, for example) as a :bias value. For a list of ccTLD's, + # ('uk' for .co.uk, for example) as a :bias value. For a list of ccTLD's, # look here: http://en.wikipedia.org/wiki/CcTLD. By default, the geocoder # will be biased to results within the US (ccTLD .com). # # If you'd like the Google Geocoder to prefer results within a given viewport, # you can pass a Geokit::Bounds object as the :bias value. @@ -467,40 +467,40 @@ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address res = self.call_geocoder_service("http://maps.google.com/maps/geo?q=#{Geokit::Inflector::url_escape(address_str)}&output=xml#{bias_str}&key=#{Geokit::Geocoders::google}&oe=utf-8") return GeoLoc.new if !res.is_a?(Net::HTTPSuccess) xml = res.body logger.debug "Google geocoding. Address: #{address}. Result: #{xml}" - return self.xml2GeoLoc(xml, address) + return self.xml2GeoLoc(xml, address) end - + def self.construct_bias_string_from_options(bias) if bias.is_a?(String) or bias.is_a?(Symbol) # country code biasing "&gl=#{bias.to_s.downcase}" elsif bias.is_a?(Bounds) # viewport biasing "&ll=#{bias.center.ll}&spn=#{bias.to_span.ll}" end end - + def self.xml2GeoLoc(xml, address="") doc=REXML::Document.new(xml) if doc.elements['//kml/Response/Status/code'].text == '200' geoloc = nil - # Google can return multiple results as //Placemark elements. + # Google can return multiple results as //Placemark elements. # iterate through each and extract each placemark as a geoloc doc.each_element('//Placemark') do |e| extracted_geoloc = extract_placemark(e) # g is now an instance of GeoLoc - if geoloc.nil? + if geoloc.nil? # first time through, geoloc is still nil, so we make it the geoloc we just extracted - geoloc = extracted_geoloc + geoloc = extracted_geoloc else - # second (and subsequent) iterations, we push additional - # geolocs onto "geoloc.all" - geoloc.all.push(extracted_geoloc) - end + # second (and subsequent) iterations, we push additional + # geolocs onto "geoloc.all" + geoloc.all.push(extracted_geoloc) + end end return geoloc elsif doc.elements['//kml/Response/Status/code'].text == '620' raise Geokit::TooManyQueriesError else @@ -512,11 +512,11 @@ # re-raise because of other rescue raise Geokit::TooManyQueriesError, "Google returned a 620 status, too many queries. The given key has gone over the requests limit in the 24 hour period or has submitted too many requests in too short a period of time. If you're sending multiple requests in parallel or in a tight loop, use a timer or pause in your code to make sure you don't send the requests too quickly." rescue logger.error "Caught an error during Google geocoding call: "+$! return GeoLoc.new - end + end # extracts a single geoloc from a //placemark element in the google results xml def self.extract_placemark(doc) res = GeoLoc.new coordinates=doc.elements['.//coordinates'].text.to_s.split(',') @@ -539,45 +539,45 @@ # Translate accuracy into Yahoo-style token address, street, zip, zip+4, city, state, country # For Google, 1=low accuracy, 8=high accuracy address_details=doc.elements['.//*[local-name() = "AddressDetails"]'] res.accuracy = address_details ? address_details.attributes['Accuracy'].to_i : 0 res.precision=%w{unknown country state state city zip zip+4 street address building}[res.accuracy] - + # google returns a set of suggested boundaries for the geocoded result - if suggested_bounds = doc.elements['//LatLonBox'] + if suggested_bounds = doc.elements['//LatLonBox'] res.suggested_bounds = Bounds.normalize( - [suggested_bounds.attributes['south'], suggested_bounds.attributes['west']], + [suggested_bounds.attributes['south'], suggested_bounds.attributes['west']], [suggested_bounds.attributes['north'], suggested_bounds.attributes['east']]) end - + res.success=true return res end end class GoogleGeocoder3 < Geocoder - private + private # Template method which does the reverse-geocode lookup. - def self.do_reverse_geocode(latlng) + def self.do_reverse_geocode(latlng) latlng=LatLng.normalize(latlng) res = self.call_geocoder_service("http://maps.google.com/maps/api/geocode/json?sensor=false&latlng=#{Geokit::Inflector::url_escape(latlng.ll)}") return GeoLoc.new unless (res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPOK)) json = res.body logger.debug "Google reverse-geocoding. LL: #{latlng}. Result: #{json}" - return self.json2GeoLoc(json) - end + return self.json2GeoLoc(json) + end # Template method which does the geocode lookup. # # Supports viewport/country code biasing # # ==== OPTIONS # * :bias - This option makes the Google Geocoder return results biased to a particular # country or viewport. Country code biasing is achieved by passing the ccTLD - # ('uk' for .co.uk, for example) as a :bias value. For a list of ccTLD's, + # ('uk' for .co.uk, for example) as a :bias value. For a list of ccTLD's, # look here: http://en.wikipedia.org/wiki/CcTLD. By default, the geocoder # will be biased to results within the US (ccTLD .com). # # If you'd like the Google Geocoder to prefer results within a given viewport, # you can pass a Geokit::Bounds object as the :bias value. @@ -596,13 +596,13 @@ def self.do_geocode(address, options = {}) res = res = self.call_geocoder_service(self.geocode_url(address,options)) return GeoLoc.new if !res.is_a?(Net::HTTPSuccess) json = res.body # logger.debug "Google geocoding. Address: #{address}. Result: #{json}" - return self.json2GeoLoc(json, address) + return self.json2GeoLoc(json, address) end - + # Determine the Google API url based on the google api key, or based on the client / private key for premier users def self.geocode_url(address,options = {}) bias_str = options[:bias] ? construct_bias_string_from_options(options[:bias]) : '' address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address @@ -611,64 +611,77 @@ Geokit::Geocoders::Geocoder.sign_url(url,Geokit::Geocoders::google_premier_secret_key) else "http://maps.google.com/maps/api/geocode/json?sensor=false&address=#{Geokit::Inflector::url_escape(address_str)}#{bias_str}" end end - - + + def self.construct_bias_string_from_options(bias) if bias.is_a?(String) or bias.is_a?(Symbol) # country code biasing "&region=#{bias.to_s.downcase}" elsif bias.is_a?(Bounds) # viewport biasing Geokit::Inflector::url_escape("&bounds=#{bias.sw.to_s}|#{bias.ne.to_s}") end end + # location_type stores additional data about the specified location. + # The following values are currently supported: + # "ROOFTOP" indicates that the returned result is a precise geocode + # for which we have location information accurate down to street + # address precision. + # "RANGE_INTERPOLATED" indicates that the returned result reflects an + # approximation (usually on a road) interpolated between two precise + # points (such as intersections). Interpolated results are generally + # returned when rooftop geocodes are unavailable for a street address. + # "GEOMETRIC_CENTER" indicates that the returned result is the + # geometric center of a result such as a polyline (for example, a + # street) or polygon (region). + # "APPROXIMATE" indicates that the returned result is approximate + + # these do not map well. Perhaps we should guess better based on size + # of bounding box where it exists? Does it really matter? + def self.accuracy + { + "ROOFTOP" => 9, + "RANGE_INTERPOLATED" => 8, + "GEOMETRIC_CENTER" => 5, + "APPROXIMATE" => 4 + } + end + + # Grouped by accuracy, then sorts the groups by accuracy, and + # joins the results. This means that if google returns 5 results, + # and of those 5, 3 of them are the same accuracy, we will maintain + # the order of those three as we've received them, within our sorted + # results that are returned. + def self.grouped_and_sorted_by_accuracy(results) + results.group_by{|a| accuracy[ a['geometry']['location_type'] ]}.sort_by {|accuracy,x| accuracy }.reverse.map {|x,locations| locations}.flatten + end + def self.json2GeoLoc(json, address="") ret=nil begin results=::ActiveSupport::JSON.decode(json) rescue NameError => e results=JSON.parse(json) end - - + + if results['status'] == 'OVER_QUERY_LIMIT' raise Geokit::TooManyQueriesError end if results['status'] == 'ZERO_RESULTS' return GeoLoc.new end # this should probably be smarter. if !results['status'] == 'OK' raise Geokit::Geocoders::GeocodeError end - # location_type stores additional data about the specified location. - # The following values are currently supported: - # "ROOFTOP" indicates that the returned result is a precise geocode - # for which we have location information accurate down to street - # address precision. - # "RANGE_INTERPOLATED" indicates that the returned result reflects an - # approximation (usually on a road) interpolated between two precise - # points (such as intersections). Interpolated results are generally - # returned when rooftop geocodes are unavailable for a street address. - # "GEOMETRIC_CENTER" indicates that the returned result is the - # geometric center of a result such as a polyline (for example, a - # street) or polygon (region). - # "APPROXIMATE" indicates that the returned result is approximate - # these do not map well. Perhaps we should guess better based on size - # of bounding box where it exists? Does it really matter? - accuracy = { - "ROOFTOP" => 9, - "RANGE_INTERPOLATED" => 8, - "GEOMETRIC_CENTER" => 5, - "APPROXIMATE" => 4 - } - results['results'].sort_by{|a|accuracy[a['geometry']['location_type']]}.reverse.each do |addr| + grouped_and_sorted_by_accuracy(results['results']).each do |addr| res=GeoLoc.new res.provider = 'google3' res.success = true res.full_address = addr['formatted_address'] addr['address_components'].each do |comp| @@ -699,17 +712,17 @@ # try a few overrides where we can if res.street_name && res.precision=='city' res.precision = 'street' res.accuracy = 7 end - + res.lat=addr['geometry']['location']['lat'].to_f res.lng=addr['geometry']['location']['lng'].to_f if addr['geometry'].include?('viewport') ne=Geokit::LatLng.new( - addr['geometry']['viewport']['northeast']['lat'].to_f, + addr['geometry']['viewport']['northeast']['lat'].to_f, addr['geometry']['viewport']['northeast']['lng'].to_f ) sw=Geokit::LatLng.new( addr['geometry']['viewport']['southwest']['lat'].to_f, addr['geometry']['viewport']['southwest']['lng'].to_f @@ -727,15 +740,15 @@ end end # ------------------------------------------------------------------------------------------- # IP Geocoders # ------------------------------------------------------------------------------------------- - + # Provides geocoding based upon an IP address. The underlying web service is geoplugin.net class GeoPluginGeocoder < Geocoder private - + def self.do_geocode(ip, options = {}) return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip) response = self.call_geocoder_service("http://www.geoplugin.net/xml.gp?ip=#{ip}") return response.is_a?(Net::HTTPSuccess) ? parse_xml(response.body) : GeoLoc.new rescue @@ -758,11 +771,11 @@ end # Provides geocoding based upon an IP address. The underlying web service is a hostip.info # which sources their data through a combination of publicly available information as well # as community contributions. - class IpGeocoder < Geocoder + class IpGeocoder < Geocoder # A number of non-routable IP ranges. # # -- # Sources for these: @@ -781,15 +794,15 @@ IPAddr.new('198.18.0.0/15'), # Network Interconnect Device Benchmark Testing IPAddr.new('224.0.0.0/4'), # Multicast IPAddr.new('240.0.0.0/4') # Reserved for future use ].freeze - private + private # Given an IP address, returns a GeoLoc instance which contains latitude, - # longitude, city, and country code. Sets the success attribute to false if the ip - # parameter does not match an ip address. + # longitude, city, and country code. Sets the success attribute to false if the ip + # parameter does not match an ip address. def self.do_geocode(ip, options = {}) return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip) return GeoLoc.new if self.private_ip_address?(ip) url = "http://api.hostip.info/get_html.php?ip=#{ip}&position=true" response = self.call_geocoder_service(url) @@ -811,11 +824,11 @@ yaml = YAML.load(body) res = GeoLoc.new res.provider = 'hostip' res.city, res.state = yaml['City'].split(', ') country, res.country_code = yaml['Country'].split(' (') - res.lat = yaml['Latitude'] + res.lat = yaml['Latitude'] res.lng = yaml['Longitude'] res.country_code.chop! res.success = !(res.city =~ /\(.+\)/) res end @@ -827,37 +840,37 @@ # integration tests. def self.private_ip_address?(ip) return NON_ROUTABLE_IP_RANGES.any? { |range| range.include?(ip) } end end - + # ------------------------------------------------------------------------------------------- # The Multi Geocoder - # ------------------------------------------------------------------------------------------- - + # ------------------------------------------------------------------------------------------- + # Provides methods to geocode with a variety of geocoding service providers, plus failover # among providers in the order you configure. When 2nd parameter is set 'true', perform # ip location lookup with 'address' as the ip address. - # + # # Goal: # - homogenize the results of multiple geocoders - # + # # Limitations: # - currently only provides the first result. Sometimes geocoders will return multiple results. # - currently discards the "accuracy" component of the geocoding calls - class MultiGeocoder < Geocoder + class MultiGeocoder < Geocoder private - # This method will call one or more geocoders in the order specified in the + # This method will call one or more geocoders in the order specified in the # configuration until one of the geocoders work. - # + # # The failover approach is crucial for production-grade apps, but is rarely used. - # 98% of your geocoding calls will be successful with the first call + # 98% of your geocoding calls will be successful with the first call def self.do_geocode(address, options = {}) geocode_ip = /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.match(address) provider_order = geocode_ip ? Geokit::Geocoders::ip_provider_order : Geokit::Geocoders::provider_order - + provider_order.each do |provider| begin klass = Geokit::Geocoders.const_get "#{Geokit::Inflector::camelize(provider.to_s)}Geocoder" res = klass.send :geocode, address, options return res if res.success? @@ -866,12 +879,12 @@ end end # If we get here, we failed completely. GeoLoc.new end - - # This method will call one or more geocoders in the order specified in the + + # This method will call one or more geocoders in the order specified in the # configuration until one of the geocoders work, only this time it's going # to try to reverse geocode a geographical point. def self.do_reverse_geocode(latlng) Geokit::Geocoders::provider_order.each do |provider| begin @@ -883,8 +896,8 @@ end end # If we get here, we failed completely. GeoLoc.new end - end + end end end