geokit/geokit

View on GitHub
lib/geokit/geocoders/yahoo.rb

Summary

Maintainability
A
1 hr
Test Coverage
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
    URI.escape(string, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")).gsub('*', '%2A')
  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