lib/geokit/geocoders/yahoo.rb
module Geokit
module Geocoders
# 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
config :key, :secret
self.secure = true
private
def self.submit_url(address)
address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
query_string = "?q=#{Geokit::Inflector.url_escape(address_str)}&flags=J"
o = OauthUtil.new
o.consumer_key = key
o.consumer_secret = secret
base = "#{protocol}://yboss.yahooapis.com/geo/placefinder"
parsed_url = URI.parse("#{base}#{query_string}")
"#{base}?#{o.sign(parsed_url).query_string}"
end
# Template method which does the geocode lookup.
def self.do_geocode(address, _=nil)
process :json, submit_url(address)
end
def self.parse_json(results)
boss_results = results && results['bossresponse'] && results['bossresponse']['placefinder'] && results['bossresponse']['placefinder']['results']
return GeoLoc.new unless boss_results && boss_results.first
loc = nil
boss_results.each do |result|
extracted_geoloc = extract_geoloc(result)
if loc.nil?
loc = extracted_geoloc
else
loc.all.push(extracted_geoloc)
end
end
loc
end
def self.extract_geoloc(result_json)
loc = new_loc
loc.lat = result_json['latitude']
loc.lng = result_json['longitude']
set_address_components(result_json, loc)
set_precision(result_json, loc)
loc.success = true
loc
end
def self.set_address_components(result_json, loc)
loc.country_code = result_json['countrycode']
loc.street_address = result_json['line1'].to_s.empty? ? nil : result_json['line1']
loc.city = result_json['city']
loc.state = loc.is_us? ? result_json['statecode'] : result_json['state']
loc.zip = result_json['postal']
end
def self.set_precision(result_json, loc)
loc.precision = case result_json['quality'].to_i
when 9, 10 then 'country'
when 19..30 then 'state'
when 39, 40 then 'city'
when 49, 50 then 'neighborhood'
when 59, 60, 64 then 'zip'
when 74, 75 then 'zip+4'
when 70..72 then 'street'
when 80..87 then 'address'
when 62, 63, 90, 99 then 'building'
else 'unknown'
end
loc.accuracy = %w(unknown country state state city zip zip+4 street address building).index(loc.precision)
end
end
end
end
# Oauth Util
# from gist: https://gist.github.com/erikeldridge/383159
# A utility for signing an url using OAuth in a way that's convenient for debugging
# Note: the standard Ruby OAuth lib is here http://github.com/mojodna/oauth
# License: http://gist.github.com/375593
# Usage: see example.rb below
require 'uri'
require 'cgi'
require 'openssl'
require 'base64'
class OauthUtil
attr_accessor :consumer_key, :consumer_secret, :token, :token_secret, :req_method,
:sig_method, :oauth_version, :callback_url, :params, :req_url, :base_str
def initialize
@consumer_key = ''
@consumer_secret = ''
@token = ''
@token_secret = ''
@req_method = 'GET'
@sig_method = 'HMAC-SHA1'
@oauth_version = '1.0'
@callback_url = ''
end
# openssl::random_bytes returns non-word chars, which need to be removed. using alt method to get length
# ref http://snippets.dzone.com/posts/show/491
def nonce
Array.new(5) { rand(256) }.pack('C*').unpack('H*').first
end
def percent_encode(string)
# ref http://snippets.dzone.com/posts/show/1260
# puts string
# puts "---------\n\n\n\n"
# (URI::Parser.new).escape(string, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")).gsub('*', '%2A')
CGI.escape(string)
end
# @ref http://oauth.net/core/1.0/#rfc.section.9.2
def signature
key = percent_encode(@consumer_secret) + '&' + percent_encode(@token_secret)
# ref: http://blog.nathanielbibler.com/post/63031273/openssl-hmac-vs-ruby-hmac-benchmarks
digest = OpenSSL::Digest.new('sha1')
hmac = OpenSSL::HMAC.digest(digest, key, @base_str)
# ref http://groups.google.com/group/oauth-ruby/browse_thread/thread/9110ed8c8f3cae81
Base64.encode64(hmac).chomp.gsub(/\n/, '')
end
# sort (very important as it affects the signature), concat, and percent encode
# @ref http://oauth.net/core/1.0/#rfc.section.9.1.1
# @ref http://oauth.net/core/1.0/#9.2.1
# @ref http://oauth.net/core/1.0/#rfc.section.A.5.1
def query_string
pairs = []
@params.sort.each do |key, val|
pairs.push("#{ percent_encode(key) }=#{ percent_encode(val.to_s) }")
end
pairs.join '&'
end
def timestamp
Time.now.to_i.to_s
end
# organize params & create signature
def sign(parsed_url)
@params = {
'oauth_consumer_key' => @consumer_key,
'oauth_nonce' => nonce,
'oauth_signature_method' => @sig_method,
'oauth_timestamp' => timestamp,
'oauth_version' => @oauth_version,
}
# if url has query, merge key/values into params obj overwriting defaults
if parsed_url.query
CGI.parse(parsed_url.query).each do |k, v|
if v.is_a?(Array) && v.count == 1
@params[k] = v.first
else
@params[k] = v
end
end
end
# @ref http://oauth.net/core/1.0/#rfc.section.9.1.2
@req_url = parsed_url.scheme + '://' + parsed_url.host + parsed_url.path
# create base str. make it an object attr for ez debugging
# ref http://oauth.net/core/1.0/#anchor14
@base_str = [
@req_method,
percent_encode(req_url),
# normalization is just x-www-form-urlencoded
percent_encode(query_string),
].join('&')
# add signature
@params['oauth_signature'] = signature
self
end
end