lib/geokit/geocoders/google_geocoder.rb in steveh-geokit-1.6.0 vs lib/geokit/geocoders/google_geocoder.rb in steveh-geokit-1.6.1

- old
+ new

@@ -2,137 +2,144 @@ module Geocoders # 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 + ENDPOINT = "http://maps.google.com/maps/geo" + private - # Template method which does the reverse-geocode lookup. - 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 + # Template method which does the reverse-geocode lookup. + def self.do_reverse_geocode(latlng) + latlng = LatLng.normalize(latlng) + params = { :ll => latlng.ll, :output => "xml", :key => Geokit::Geocoders::google, :oe => "utf-8" } + url = "#{ENDPOINT}?" + params.to_query + res = self.call_geocoder_service(url) + 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 - # 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, - # 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. - # - # ==== EXAMPLES - # # By default, the geocoder will return Syracuse, NY - # Geokit::Geocoders::GoogleGeocoder.geocode('Syracuse').country_code # => 'US' - # # With country code biasing, it returns Syracuse in Sicily, Italy - # Geokit::Geocoders::GoogleGeocoder.geocode('Syracuse', :bias => :it).country_code # => 'IT' - # - # # By default, the geocoder will return Winnetka, IL - # Geokit::Geocoders::GoogleGeocoder.geocode('Winnetka').state # => 'IL' - # # When biased to an bounding box around California, it will now return the Winnetka neighbourhood, CA - # bounds = Geokit::Bounds.normalize([34.074081, -118.694401], [34.321129, -118.399487]) - # Geokit::Geocoders::GoogleGeocoder.geocode('Winnetka', :bias => bounds).state # => 'CA' - def self.do_geocode(address, options = {}) - bias_str = options[:bias] ? construct_bias_string_from_options(options[:bias]) : '' - 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) - 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, + # 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. + # + # ==== EXAMPLES + # # By default, the geocoder will return Syracuse, NY + # Geokit::Geocoders::GoogleGeocoder.geocode('Syracuse').country_code # => 'US' + # # With country code biasing, it returns Syracuse in Sicily, Italy + # Geokit::Geocoders::GoogleGeocoder.geocode('Syracuse', :bias => :it).country_code # => 'IT' + # + # # By default, the geocoder will return Winnetka, IL + # Geokit::Geocoders::GoogleGeocoder.geocode('Winnetka').state # => 'IL' + # # When biased to an bounding box around California, it will now return the Winnetka neighbourhood, CA + # bounds = Geokit::Bounds.normalize([34.074081, -118.694401], [34.321129, -118.399487]) + # Geokit::Geocoders::GoogleGeocoder.geocode('Winnetka', :bias => bounds).state # => 'CA' + def self.do_geocode(address, options = {}) + address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address + params = { :q => address_str, :output => "xml", :key => Geokit::Geocoders::google, :oe => "utf-8" }.merge(bias_options(options[:bias])) + url = "#{ENDPOINT}?" + params.to_query + res = self.call_geocoder_service(url) + 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) + 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}" + def self.bias_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 } + else + {} + end end - end - def self.xml2GeoLoc(xml, address="") - doc=REXML::Document.new(xml) + 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. - # 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? - # first time through, geoloc is still nil, so we make it the geoloc we just extracted - geoloc = extracted_geoloc - else - # second (and subsequent) iterations, we push additional - # geolocs onto "geoloc.all" - geoloc.all.push(extracted_geoloc) + if doc.elements['//kml/Response/Status/code'].text == '200' + geoloc = nil + # 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? + # first time through, geoloc is still nil, so we make it the geoloc we just extracted + geoloc = extracted_geoloc + else + # 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 + logger.info "Google was unable to geocode address: "+address + return GeoLoc.new end - return geoloc - elsif doc.elements['//kml/Response/Status/code'].text == '620' - raise Geokit::TooManyQueriesError - else - logger.info "Google was unable to geocode address: "+address + + rescue Geokit::TooManyQueriesError + # 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 - rescue Geokit::TooManyQueriesError - # 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 + # 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(',') - # 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(',') + #basics + res.lat=coordinates[1] + res.lng=coordinates[0] + res.country_code=doc.elements['.//CountryNameCode'].text if doc.elements['.//CountryNameCode'] + res.provider='google' - #basics - res.lat=coordinates[1] - res.lng=coordinates[0] - res.country_code=doc.elements['.//CountryNameCode'].text if doc.elements['.//CountryNameCode'] - res.provider='google' + #extended -- false if not not available + res.city = doc.elements['.//LocalityName'].text if doc.elements['.//LocalityName'] + res.state = doc.elements['.//AdministrativeAreaName'].text if doc.elements['.//AdministrativeAreaName'] + res.province = doc.elements['.//SubAdministrativeAreaName'].text if doc.elements['.//SubAdministrativeAreaName'] + res.full_address = doc.elements['.//address'].text if doc.elements['.//address'] # google provides it + res.zip = doc.elements['.//PostalCodeNumber'].text if doc.elements['.//PostalCodeNumber'] + res.street_address = doc.elements['.//ThoroughfareName'].text if doc.elements['.//ThoroughfareName'] + res.country = doc.elements['.//CountryName'].text if doc.elements['.//CountryName'] + res.district = doc.elements['.//DependentLocalityName'].text if doc.elements['.//DependentLocalityName'] + # 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] - #extended -- false if not not available - res.city = doc.elements['.//LocalityName'].text if doc.elements['.//LocalityName'] - res.state = doc.elements['.//AdministrativeAreaName'].text if doc.elements['.//AdministrativeAreaName'] - res.province = doc.elements['.//SubAdministrativeAreaName'].text if doc.elements['.//SubAdministrativeAreaName'] - res.full_address = doc.elements['.//address'].text if doc.elements['.//address'] # google provides it - res.zip = doc.elements['.//PostalCodeNumber'].text if doc.elements['.//PostalCodeNumber'] - res.street_address = doc.elements['.//ThoroughfareName'].text if doc.elements['.//ThoroughfareName'] - res.country = doc.elements['.//CountryName'].text if doc.elements['.//CountryName'] - res.district = doc.elements['.//DependentLocalityName'].text if doc.elements['.//DependentLocalityName'] - # 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'] + res.suggested_bounds = Bounds.normalize( + [suggested_bounds.attributes['south'], suggested_bounds.attributes['west']], + [suggested_bounds.attributes['north'], suggested_bounds.attributes['east']]) + end - # google returns a set of suggested boundaries for the geocoded result - if suggested_bounds = doc.elements['//LatLonBox'] - res.suggested_bounds = Bounds.normalize( - [suggested_bounds.attributes['south'], suggested_bounds.attributes['west']], - [suggested_bounds.attributes['north'], suggested_bounds.attributes['east']]) + res.success=true + + return res end - res.success=true - - return res - end end end end \ No newline at end of file