├── .gitignore ├── autotest └── discover.rb ├── Gemfile ├── Rakefile ├── Manifest.txt ├── Gemfile.lock ├── lib ├── geokit.rb └── geokit │ ├── mappable.rb │ ├── geocoders_mine.rb │ └── geocoders.rb ├── test ├── test_inflector.rb ├── test_multi_ip_geocoder.rb ├── test_ca_geocoder.rb ├── test_geoloc.rb ├── test_base_geocoder.rb ├── test_geoplugin_geocoder.rb ├── test_us_geocoder.rb ├── test_bounds.rb ├── test_ipgeocoder.rb ├── test_multi_geocoder.rb ├── test_yahoo_geocoder.rb ├── test_google_reverse_geocoder.rb ├── test_latlng.rb └── test_google_geocoder.rb ├── spec ├── spec_helper.rb └── geocoder_spec.rb ├── geokit-premier.gemspec ├── History.txt └── README.markdown /.gitignore: -------------------------------------------------------------------------------- 1 | .loadpath 2 | .project 3 | pkg 4 | .idea 5 | .bundle 6 | .rvmrc 7 | /.redcar/ 8 | -------------------------------------------------------------------------------- /autotest/discover.rb: -------------------------------------------------------------------------------- 1 | #Autotest.add_discovery { "rails" } 2 | Autotest.add_discovery { "rspec2" } 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | source 'http://gemcutter.org' 3 | 4 | gem 'rspec', ">= 2.0.0.beta.23" 5 | gem 'rspec-mocks', ">= 2.0.0.beta.23" 6 | gem "rspec-rails", ">= 2.0.0.beta.23" 7 | gem 'autotest' 8 | gem 'autotest-fsevent' 9 | gem 'json_pure' 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | 3 | require 'rubygems' 4 | require 'hoe' 5 | require './lib/geokit.rb' 6 | 7 | # undefined method `empty?' for nil:NilClass 8 | # /Library/Ruby/Site/1.8/rubygems/specification.rb:886:in `validate' 9 | class NilClass 10 | def empty? 11 | true 12 | end 13 | end 14 | 15 | project=Hoe.new('geokit-premier', Geokit::VERSION) do |p| 16 | p.developer('Andrew Forward', 'aforward@gmail.com') 17 | p.summary="Fork of Geokit to provide for Google Premier users" 18 | end 19 | 20 | 21 | # vim: syntax=Ruby 22 | -------------------------------------------------------------------------------- /Manifest.txt: -------------------------------------------------------------------------------- 1 | History.txt 2 | Manifest.txt 3 | README.markdown 4 | Rakefile 5 | geokit-premier.gemspec 6 | lib/geokit.rb 7 | lib/geokit/geocoders.rb 8 | lib/geokit/mappable.rb 9 | spec/geocoder_spec.rb 10 | spec/spec_helper.rb 11 | test/test_base_geocoder.rb 12 | test/test_bounds.rb 13 | test/test_ca_geocoder.rb 14 | test/test_geoloc.rb 15 | test/test_geoplugin_geocoder.rb 16 | test/test_google_geocoder.rb 17 | test/test_google_reverse_geocoder.rb 18 | test/test_inflector.rb 19 | test/test_ipgeocoder.rb 20 | test/test_latlng.rb 21 | test/test_multi_geocoder.rb 22 | test/test_us_geocoder.rb 23 | test/test_yahoo_geocoder.rb 24 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | remote: http://gemcutter.org/ 4 | specs: 5 | autotest (4.4.2) 6 | autotest-fsevent (0.2.3) 7 | sys-uname 8 | diff-lcs (1.1.2) 9 | json_pure (1.4.6) 10 | rspec (2.0.1) 11 | rspec-core (~> 2.0.1) 12 | rspec-expectations (~> 2.0.1) 13 | rspec-mocks (~> 2.0.1) 14 | rspec-core (2.0.1) 15 | rspec-expectations (2.0.1) 16 | diff-lcs (>= 1.1.2) 17 | rspec-mocks (2.0.1) 18 | rspec-core (~> 2.0.1) 19 | rspec-expectations (~> 2.0.1) 20 | rspec-rails (2.0.1) 21 | rspec (~> 2.0.0) 22 | sys-uname (0.8.4) 23 | 24 | PLATFORMS 25 | ruby 26 | 27 | DEPENDENCIES 28 | autotest 29 | autotest-fsevent 30 | json_pure 31 | rspec (>= 2.0.0.beta.23) 32 | rspec-mocks (>= 2.0.0.beta.23) 33 | rspec-rails (>= 2.0.0.beta.23) 34 | -------------------------------------------------------------------------------- /lib/geokit.rb: -------------------------------------------------------------------------------- 1 | module Geokit 2 | VERSION = '0.0.4' 3 | # These defaults are used in Geokit::Mappable.distance_to and in acts_as_mappable 4 | @@default_units = :miles 5 | @@default_formula = :sphere 6 | 7 | [:default_units, :default_formula].each do |sym| 8 | class_eval <<-EOS, __FILE__, __LINE__ 9 | def self.#{sym} 10 | if defined?(#{sym.to_s.upcase}) 11 | #{sym.to_s.upcase} 12 | else 13 | @@#{sym} 14 | end 15 | end 16 | 17 | def self.#{sym}=(obj) 18 | @@#{sym} = obj 19 | end 20 | EOS 21 | end 22 | end 23 | 24 | path = File.expand_path(File.dirname(__FILE__)) 25 | $:.unshift path unless $:.include?(path) 26 | require 'geokit/geocoders' 27 | require 'geokit/mappable' 28 | 29 | # make old-style module name "GeoKit" equivalent to new-style "Geokit" 30 | GeoKit=Geokit 31 | -------------------------------------------------------------------------------- /test/test_inflector.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'test/unit' 4 | require 'lib/geokit' 5 | 6 | class InflectorTest < Test::Unit::TestCase #:nodoc: all 7 | 8 | def test_titleize 9 | assert_equal 'Sugar Grove', Geokit::Inflector.titleize('Sugar Grove') 10 | assert_equal 'Sugar Grove', Geokit::Inflector.titleize('Sugar grove') 11 | assert_equal 'Sugar Grove', Geokit::Inflector.titleize('sugar Grove') 12 | assert_equal 'Sugar Grove', Geokit::Inflector.titleize('sugar grove') 13 | end 14 | 15 | def test_titleize_with_unicode 16 | assert_equal 'Borås', Geokit::Inflector.titleize('Borås') 17 | assert_equal 'Borås', Geokit::Inflector.titleize('borås') 18 | assert_equal 'Borås (Abc)', Geokit::Inflector.titleize('Borås (Abc)') 19 | assert_equal 'Borås (Abc)', Geokit::Inflector.titleize('Borås (abc)') 20 | assert_equal 'Borås (Abc)', Geokit::Inflector.titleize('borås (Abc)') 21 | assert_equal 'Borås (Abc)', Geokit::Inflector.titleize('borås (abc)') 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | require 'lib/geokit' 3 | require 'rubygems' 4 | require 'irb' 5 | require 'irb/completion' 6 | 7 | # Requires supporting ruby files with custom matchers and macros, etc, 8 | # in spec/support/ and its subdirectories. 9 | # Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f} 10 | 11 | RSpec.configure do |config| 12 | 13 | # == Mock Framework 14 | # 15 | # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line: 16 | # 17 | # config.mock_with :mocha 18 | # config.mock_with :flexmock 19 | # config.mock_with :rr 20 | config.mock_with :rspec 21 | # config.before(:each) { Machinist.reset_before_test } 22 | 23 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 24 | # config.fixture_path = "#{::Rails.root}/spec/fixtures" 25 | 26 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 27 | # examples within a transaction, remove the following line or assign false 28 | # instead of true. 29 | # config.use_transactional_fixtures = true 30 | 31 | end 32 | 33 | module IRB 34 | def IRB.parse_opts 35 | # Don't touch ARGV, which belongs to the app which called this module. 36 | end 37 | end 38 | 39 | -------------------------------------------------------------------------------- /test/test_multi_ip_geocoder.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_base_geocoder') 2 | 3 | Geokit::Geocoders::ip_provider_order=[:geo_plugin,:ip] 4 | 5 | class MultiIpGeocoderTest < BaseGeocoderTest #:nodoc: all 6 | 7 | def setup 8 | @ip_address = '10.10.10.10' 9 | @success = Geokit::GeoLoc.new({:city=>"SAN FRANCISCO", :state=>"CA", :country_code=>"US", :lat=>37.7742, :lng=>-122.417068}) 10 | @success.success = true 11 | @failure = Geokit::GeoLoc.new 12 | end 13 | 14 | def test_successful_first 15 | Geokit::Geocoders::GeoPluginGeocoder.expects(:geocode).with(@ip_address, {}).returns(@success) 16 | assert_equal @success, Geokit::Geocoders::MultiGeocoder.geocode(@ip_address) 17 | end 18 | 19 | def test_failover 20 | Geokit::Geocoders::GeoPluginGeocoder.expects(:geocode).with(@ip_address, {}).returns(@failure) 21 | Geokit::Geocoders::IpGeocoder.expects(:geocode).with(@ip_address, {}).returns(@success) 22 | assert_equal @success, Geokit::Geocoders::MultiGeocoder.geocode(@ip_address) 23 | end 24 | 25 | def test_failure 26 | Geokit::Geocoders::GeoPluginGeocoder.expects(:geocode).with(@ip_address, {}).returns(@failure) 27 | Geokit::Geocoders::IpGeocoder.expects(:geocode).with(@ip_address, {}).returns(@failure) 28 | assert_equal @failure, Geokit::Geocoders::MultiGeocoder.geocode(@ip_address) 29 | end 30 | 31 | def test_invalid_provider 32 | temp = Geokit::Geocoders::ip_provider_order 33 | Geokit::Geocoders.ip_provider_order = [:bogus] 34 | assert_equal @failure, Geokit::Geocoders::MultiGeocoder.geocode(@ip_address) 35 | Geokit::Geocoders.ip_provider_order = temp 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /test/test_ca_geocoder.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_base_geocoder') 2 | 3 | Geokit::Geocoders::geocoder_ca = "SOMEKEYVALUE" 4 | 5 | class CaGeocoderTest < BaseGeocoderTest #:nodoc: all 6 | 7 | CA_SUCCESS=<<-EOF 8 | 9 | 49.243086-123.153684 10 | EOF 11 | 12 | def setup 13 | @ca_full_hash = {:street_address=>"2105 West 32nd Avenue",:city=>"Vancouver", :state=>"BC"} 14 | @ca_full_loc = Geokit::GeoLoc.new(@ca_full_hash) 15 | end 16 | 17 | def test_geocoder_with_geo_loc_with_account 18 | response = MockSuccess.new 19 | response.expects(:body).returns(CA_SUCCESS) 20 | url = "http://geocoder.ca/?stno=2105&addresst=West+32nd+Avenue&city=Vancouver&prov=BC&auth=SOMEKEYVALUE&geoit=xml" 21 | Geokit::Geocoders::CaGeocoder.expects(:call_geocoder_service).with(url).returns(response) 22 | verify(Geokit::Geocoders::CaGeocoder.geocode(@ca_full_loc)) 23 | end 24 | 25 | def test_service_unavailable 26 | response = MockFailure.new 27 | #Net::HTTP.expects(:get_response).with(URI.parse("http://geocoder.ca/?stno=2105&addresst=West+32nd+Avenue&city=Vancouver&prov=BC&auth=SOMEKEYVALUE&geoit=xml")).returns(response) 28 | url = "http://geocoder.ca/?stno=2105&addresst=West+32nd+Avenue&city=Vancouver&prov=BC&auth=SOMEKEYVALUE&geoit=xml" 29 | Geokit::Geocoders::CaGeocoder.expects(:call_geocoder_service).with(url).returns(response) 30 | assert !Geokit::Geocoders::CaGeocoder.geocode(@ca_full_loc).success 31 | end 32 | 33 | private 34 | 35 | def verify(location) 36 | assert_equal "BC", location.state 37 | assert_equal "Vancouver", location.city 38 | assert_equal "49.243086,-123.153684", location.ll 39 | assert !location.is_us? 40 | end 41 | end -------------------------------------------------------------------------------- /geokit-premier.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | Gem::Specification.new do |s| 4 | s.name = %q{geokit-premier} 5 | s.version = "0.0.4" 6 | 7 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 8 | s.authors = ["Andrew Forward (forked project from Andre Lewis and Bill Eisenhauer)"] 9 | s.date = %q{2009-08-02} 10 | s.description = %q{Geokit Premier Gem} 11 | s.email = ["aforward@gmail.com"] 12 | s.extra_rdoc_files = ["Manifest.txt", "README.markdown"] 13 | s.files = ["Manifest.txt", "README.markdown", "Rakefile", "lib/geokit/geocoders.rb", "lib/geokit.rb", "lib/geokit/mappable.rb", "spec/geocoder_spec.rb", "spec/spec_helper.rb", "test/test_base_geocoder.rb", "test/test_bounds.rb", "test/test_ca_geocoder.rb", "test/test_geoloc.rb", "test/test_google_geocoder.rb", "test/test_latlng.rb", "test/test_multi_geocoder.rb", "test/test_us_geocoder.rb", "test/test_yahoo_geocoder.rb"] 14 | s.has_rdoc = true 15 | s.homepage = %q{http://github.com/aforward/geokit-gem} 16 | s.rdoc_options = ["--main", "README.markdown"] 17 | s.require_paths = ["lib"] 18 | s.rubygems_version = %q{1.3.5} 19 | s.summary = %q{none} 20 | s.test_files = ["spec/geocoder_spec.rb", "test/test_base_geocoder.rb", "test/test_bounds.rb", "test/test_ca_geocoder.rb", "test/test_geoloc.rb", 21 | "test/test_geoplugin_geocoder.rb", "test/test_google_geocoder.rb", "test/test_google_reverse_geocoder.rb", 22 | "test/test_inflector.rb", "test/test_ipgeocoder.rb", "test/test_latlng.rb", "test/test_multi_geocoder.rb", 23 | "test/test_multi_ip_geocoder.rb", "test/test_us_geocoder.rb", "test/test_yahoo_geocoder.rb"] 24 | 25 | if s.respond_to? :specification_version then 26 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 27 | s.specification_version = 2 28 | end 29 | end 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/test_geoloc.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'lib/geokit' 3 | 4 | class GeoLocTest < Test::Unit::TestCase #:nodoc: all 5 | 6 | def setup 7 | @loc = Geokit::GeoLoc.new 8 | end 9 | 10 | def test_is_us 11 | assert !@loc.is_us? 12 | @loc.country_code = 'US' 13 | assert @loc.is_us? 14 | end 15 | 16 | def test_success 17 | assert !@loc.success? 18 | @loc.success = false 19 | assert !@loc.success? 20 | @loc.success = true 21 | assert @loc.success? 22 | end 23 | 24 | def test_street_number 25 | @loc.street_address = '123 Spear St.' 26 | assert_equal '123', @loc.street_number 27 | end 28 | 29 | def test_street_name 30 | @loc.street_address = '123 Spear St.' 31 | assert_equal 'Spear St.', @loc.street_name 32 | end 33 | 34 | def test_city 35 | @loc.city = "san francisco" 36 | assert_equal 'San Francisco', @loc.city 37 | end 38 | 39 | def test_full_address 40 | @loc.city = 'San Francisco' 41 | @loc.state = 'CA' 42 | @loc.zip = '94105' 43 | @loc.country_code = 'US' 44 | assert_equal 'San Francisco, CA, 94105, US', @loc.full_address 45 | @loc.full_address = 'Irving, TX, 75063, US' 46 | assert_equal 'Irving, TX, 75063, US', @loc.full_address 47 | end 48 | 49 | def test_hash 50 | @loc.city = 'San Francisco' 51 | @loc.state = 'CA' 52 | @loc.zip = '94105' 53 | @loc.country_code = 'US' 54 | @another = Geokit::GeoLoc.new @loc.to_hash 55 | assert_equal @loc, @another 56 | end 57 | 58 | def test_all 59 | assert_equal [@loc], @loc.all 60 | end 61 | 62 | def test_to_yaml 63 | @loc.city = 'San Francisco' 64 | @loc.state = 'CA' 65 | @loc.zip = '94105' 66 | @loc.country_code = 'US' 67 | assert_equal( 68 | "--- !ruby/object:Geokit::GeoLoc \ncity: San Francisco\ncountry_code: US\nfull_address: \nlat: \nlng: \nprecision: unknown\nprovince: \nstate: CA\nstreet_address: \nsuccess: false\nzip: \"94105\"\n", 69 | @loc.to_yaml) 70 | end 71 | 72 | end -------------------------------------------------------------------------------- /test/test_base_geocoder.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'net/http' 3 | require 'rubygems' 4 | require 'mocha' 5 | require 'lib/geokit' 6 | 7 | class MockSuccess < Net::HTTPSuccess #:nodoc: all 8 | def initialize 9 | end 10 | end 11 | 12 | class MockFailure < Net::HTTPServiceUnavailable #:nodoc: all 13 | def initialize 14 | end 15 | end 16 | 17 | # Base class for testing geocoders. 18 | class BaseGeocoderTest < Test::Unit::TestCase #:nodoc: all 19 | 20 | class Geokit::Geocoders::TestGeocoder < Geokit::Geocoders::Geocoder 21 | def self.do_get(url) 22 | sleep(2) 23 | end 24 | end 25 | 26 | # Defines common test fixtures. 27 | def setup 28 | @address = 'San Francisco, CA' 29 | @full_address = '100 Spear St, San Francisco, CA, 94105-1522, US' 30 | @full_address_short_zip = '100 Spear St, San Francisco, CA, 94105, US' 31 | 32 | @latlng = Geokit::LatLng.new(37.7742, -122.417068) 33 | @success = Geokit::GeoLoc.new({:city=>"SAN FRANCISCO", :state=>"CA", :country_code=>"US", :lat=>@latlng.lat, :lng=>@latlng.lng}) 34 | @success.success = true 35 | end 36 | 37 | def test_timeout_call_web_service 38 | url = "http://www.anything.com" 39 | Geokit::Geocoders::request_timeout = 1 40 | assert_nil Geokit::Geocoders::TestGeocoder.call_geocoder_service(url) 41 | end 42 | 43 | def test_successful_call_web_service 44 | url = "http://www.anything.com" 45 | Geokit::Geocoders::Geocoder.expects(:do_get).with(url).returns("SUCCESS") 46 | assert_equal "SUCCESS", Geokit::Geocoders::Geocoder.call_geocoder_service(url) 47 | end 48 | 49 | def test_find_geocoder_methods 50 | public_methods = Geokit::Geocoders::Geocoder.public_methods.map { |m| m.to_s } 51 | assert public_methods.include?("yahoo_geocoder") 52 | assert public_methods.include?("google_geocoder") 53 | assert public_methods.include?("ca_geocoder") 54 | assert public_methods.include?("us_geocoder") 55 | assert public_methods.include?("multi_geocoder") 56 | assert public_methods.include?("ip_geocoder") 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /test/test_geoplugin_geocoder.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require File.join(File.dirname(__FILE__), 'test_base_geocoder') 3 | 4 | class IpGeocoderTest < BaseGeocoderTest #:nodoc: all 5 | 6 | IP_SUCCESS=<<-EOF 7 | 8 | 9 | Belo Horizonte 10 | Minas Gerais 11 | 0 12 | 0 13 | BR 14 | Brazil 15 | SA 16 | -19.916700 17 | -43.933300 18 | BRL 19 | R$ 20 | 2.2575001717 21 | 22 | EOF 23 | 24 | def setup 25 | super 26 | @success.provider = "geoPlugin" 27 | end 28 | 29 | def test_successful_lookup 30 | success = MockSuccess.new 31 | success.expects(:body).returns(IP_SUCCESS) 32 | url = 'http://www.geoplugin.net/xml.gp?ip=200.150.38.66' 33 | GeoKit::Geocoders::GeoPluginGeocoder.expects(:call_geocoder_service).with(url).returns(success) 34 | location = GeoKit::Geocoders::GeoPluginGeocoder.geocode('200.150.38.66') 35 | assert_not_nil location 36 | assert_equal -19.916700, location.lat 37 | assert_equal -43.933300, location.lng 38 | assert_equal "Belo Horizonte", location.city 39 | assert_equal "Minas Gerais", location.state 40 | assert_equal "BR", location.country_code 41 | assert_equal "geoPlugin", location.provider 42 | assert location.success? 43 | end 44 | 45 | def test_invalid_ip 46 | location = GeoKit::Geocoders::GeoPluginGeocoder.geocode("pixrum") 47 | assert_not_nil location 48 | assert !location.success? 49 | end 50 | 51 | def test_service_unavailable 52 | failure = MockFailure.new 53 | url = 'http://www.geoplugin.net/xml.gp?ip=10.10.10.10' 54 | GeoKit::Geocoders::GeoPluginGeocoder.expects(:call_geocoder_service).with(url).returns(failure) 55 | location = GeoKit::Geocoders::GeoPluginGeocoder.geocode("10.10.10.10") 56 | assert_not_nil location 57 | assert !location.success? 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/test_us_geocoder.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_base_geocoder') 2 | 3 | Geokit::Geocoders::geocoder_us = nil 4 | 5 | class UsGeocoderTest < BaseGeocoderTest #:nodoc: all 6 | 7 | GEOCODER_US_FULL='37.792528,-122.393981,100 Spear St,San Francisco,CA,94105' 8 | 9 | def setup 10 | super 11 | @us_full_hash = {:city=>"San Francisco", :state=>"CA"} 12 | @us_full_loc = Geokit::GeoLoc.new(@us_full_hash) 13 | end 14 | 15 | def test_geocoder_us 16 | response = MockSuccess.new 17 | response.expects(:body).returns(GEOCODER_US_FULL) 18 | url = "http://geocoder.us/service/csv/geocode?address=#{Geokit::Inflector.url_escape(@address)}" 19 | Geokit::Geocoders::UsGeocoder.expects(:call_geocoder_service).with(url).returns(response) 20 | verify(Geokit::Geocoders::UsGeocoder.geocode(@address)) 21 | end 22 | 23 | def test_geocoder_with_geo_loc 24 | response = MockSuccess.new 25 | response.expects(:body).returns(GEOCODER_US_FULL) 26 | url = "http://geocoder.us/service/csv/geocode?address=#{Geokit::Inflector.url_escape(@address)}" 27 | Geokit::Geocoders::UsGeocoder.expects(:call_geocoder_service).with(url).returns(response) 28 | verify(Geokit::Geocoders::UsGeocoder.geocode(@us_full_loc)) 29 | end 30 | 31 | def test_service_unavailable 32 | response = MockFailure.new 33 | url = "http://geocoder.us/service/csv/geocode?address=#{Geokit::Inflector.url_escape(@address)}" 34 | Geokit::Geocoders::UsGeocoder.expects(:call_geocoder_service).with(url).returns(response) 35 | assert !Geokit::Geocoders::UsGeocoder.geocode(@us_full_loc).success 36 | end 37 | 38 | def test_all_method 39 | response = MockSuccess.new 40 | response.expects(:body).returns(GEOCODER_US_FULL) 41 | url = "http://geocoder.us/service/csv/geocode?address=#{Geokit::Inflector.url_escape(@address)}" 42 | Geokit::Geocoders::UsGeocoder.expects(:call_geocoder_service).with(url).returns(response) 43 | res=Geokit::Geocoders::UsGeocoder.geocode(@address) 44 | assert_equal 1, res.all.size 45 | end 46 | 47 | private 48 | 49 | def verify(location) 50 | assert_equal "CA", location.state 51 | assert_equal "San Francisco", location.city 52 | assert_equal "37.792528,-122.393981", location.ll 53 | assert location.is_us? 54 | assert_equal "100 Spear St, San Francisco, CA, 94105, US", location.full_address #slightly different from yahoo 55 | end 56 | end -------------------------------------------------------------------------------- /spec/geocoder_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Geocoder" do 4 | 5 | after(:each) do 6 | Geokit::Geocoders.google_client_id = nil 7 | Geokit::Geocoders.google_premier_secret_key = nil 8 | Geokit::Geocoders::google = nil 9 | end 10 | 11 | 12 | describe "self.google_client_id" do 13 | it "should be nil by default and settable" do 14 | Geokit::Geocoders.google_client_id.should == nil 15 | Geokit::Geocoders.google_client_id = 'abc' 16 | Geokit::Geocoders.google_client_id.should == 'abc' 17 | end 18 | end 19 | 20 | describe "self.google_premier_secret_key" do 21 | it "should be nil by default and settable" do 22 | Geokit::Geocoders.google_premier_secret_key.should == nil 23 | Geokit::Geocoders.google_premier_secret_key = 'abc123' 24 | Geokit::Geocoders.google_premier_secret_key.should == 'abc123' 25 | end 26 | end 27 | 28 | describe "#sign_url" do 29 | it "should encrypt the url" do 30 | expected = 'http://maps.googleapis.com/maps/api/geocode/json?address=New+York&sensor=false&client=clientID&signature=KrU1TzVQM7Ur0i8i7K3huiw3MsA=' 31 | actual = Geokit::Geocoders::Geocoder.sign_url('http://maps.googleapis.com/maps/api/geocode/json?address=New+York&sensor=false&client=clientID','vNIXE0xscrmjlyV-12Nj_BvUPaw=') 32 | actual.should == expected 33 | end 34 | 35 | it "xml example" do 36 | secret = 'vNIXE0xscrmjlyV-12Nj_BvUPaw=' 37 | url = "http://maps.googleapis.com/maps/api/geocode/xml?address=1600+Amphitheatre+Parkway,+Mountain+View,+CA&client=gme-cenx&sensor=false" 38 | expected = "http://maps.googleapis.com/maps/api/geocode/xml?address=1600+Amphitheatre+Parkway,+Mountain+View,+CA&client=gme-cenx&sensor=false&signature=1LZ2Iz3gtt-OH0uIv0nJBFGN8E8=" 39 | Geokit::Geocoders::Geocoder.sign_url(url,secret).should == expected 40 | end 41 | 42 | end 43 | 44 | describe "#urlsafe_decode64" do 45 | it "should deal with - and +" do 46 | Geokit::Geocoders::Geocoder.urlsafe_decode64("a-b+c-d+").should == "k\346\376s\347~" 47 | end 48 | end 49 | 50 | describe "#urlsafe_encode64" do 51 | it "should deal with - and +" do 52 | Geokit::Geocoders::Geocoder.urlsafe_encode64("k\346\376s\347~").should == "a-b-c-d-\n" 53 | end 54 | end 55 | 56 | describe "GoogleGeocoder3#geocode_url" do 57 | 58 | it "should use default if not premier" do 59 | Geokit::Geocoders::google = 'abc123' 60 | expected = "http://maps.google.com/maps/api/geocode/json?sensor=false&address=Ottawa" 61 | Geokit::Geocoders::GoogleGeocoder3.geocode_url('Ottawa',{}).should == expected 62 | end 63 | 64 | it "should use client if premier" do 65 | Geokit::Geocoders.google_client_id = 'gme-cenx' 66 | Geokit::Geocoders.google_premier_secret_key = 'ciK-I4AWUmFx5jBRIjtrL6hDC04=' 67 | expected = "http://maps.googleapis.com/maps/api/geocode/json?address=Ottawa&client=gme-cenx&sensor=false&oe=utf-8&signature=VG4njf1Yo59tnEvwPAMlgOoj4_0=" 68 | Geokit::Geocoders::GoogleGeocoder3.geocode_url('Ottawa',{}).should == expected 69 | end 70 | 71 | end 72 | 73 | end -------------------------------------------------------------------------------- /test/test_bounds.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'lib/geokit' 3 | 4 | class BoundsTest < Test::Unit::TestCase #:nodoc: all 5 | 6 | def setup 7 | # This is the area in Texas 8 | @sw = Geokit::LatLng.new(32.91663,-96.982841) 9 | @ne = Geokit::LatLng.new(32.96302,-96.919495) 10 | @bounds=Geokit::Bounds.new(@sw,@ne) 11 | @loc_a=Geokit::LatLng.new(32.918593,-96.958444) # inside bounds 12 | @loc_b=Geokit::LatLng.new(32.914144,-96.958444) # outside bouds 13 | 14 | # this is a cross-meridan area 15 | @cross_meridian=Geokit::Bounds.normalize([30,170],[40,-170]) 16 | @inside_cm=Geokit::LatLng.new(35,175) 17 | @inside_cm_2=Geokit::LatLng.new(35,-175) 18 | @east_of_cm=Geokit::LatLng.new(35,-165) 19 | @west_of_cm=Geokit::LatLng.new(35,165) 20 | 21 | end 22 | 23 | def test_equality 24 | assert_equal Geokit::Bounds.new(@sw,@ne), Geokit::Bounds.new(@sw,@ne) 25 | end 26 | 27 | def test_normalize 28 | res=Geokit::Bounds.normalize(@sw,@ne) 29 | assert_equal res,Geokit::Bounds.new(@sw,@ne) 30 | res=Geokit::Bounds.normalize([@sw,@ne]) 31 | assert_equal res,Geokit::Bounds.new(@sw,@ne) 32 | res=Geokit::Bounds.normalize([@sw.lat,@sw.lng],[@ne.lat,@ne.lng]) 33 | assert_equal res,Geokit::Bounds.new(@sw,@ne) 34 | res=Geokit::Bounds.normalize([[@sw.lat,@sw.lng],[@ne.lat,@ne.lng]]) 35 | assert_equal res,Geokit::Bounds.new(@sw,@ne) 36 | end 37 | 38 | def test_point_inside_bounds 39 | assert @bounds.contains?(@loc_a) 40 | end 41 | 42 | def test_point_outside_bounds 43 | assert !@bounds.contains?(@loc_b) 44 | end 45 | 46 | def test_point_inside_bounds_cross_meridian 47 | assert @cross_meridian.contains?(@inside_cm) 48 | assert @cross_meridian.contains?(@inside_cm_2) 49 | end 50 | 51 | def test_point_outside_bounds_cross_meridian 52 | assert !@cross_meridian.contains?(@east_of_cm) 53 | assert !@cross_meridian.contains?(@west_of_cm) 54 | end 55 | 56 | def test_center 57 | assert_in_delta 32.939828,@bounds.center.lat,0.00005 58 | assert_in_delta(-96.9511763,@bounds.center.lng,0.00005) 59 | end 60 | 61 | def test_center_cross_meridian 62 | assert_in_delta 35.41160, @cross_meridian.center.lat,0.00005 63 | assert_in_delta 179.38112, @cross_meridian.center.lng,0.00005 64 | end 65 | 66 | def test_creation_from_circle 67 | bounds=Geokit::Bounds.from_point_and_radius([32.939829, -96.951176],2.5) 68 | inside=Geokit::LatLng.new 32.9695270000,-96.9901590000 69 | outside=Geokit::LatLng.new 32.8951550000,-96.9584440000 70 | assert bounds.contains?(inside) 71 | assert !bounds.contains?(outside) 72 | end 73 | 74 | def test_bounds_to_span 75 | sw = Geokit::LatLng.new(32, -96) 76 | ne = Geokit::LatLng.new(40, -70) 77 | bounds = Geokit::Bounds.new(sw, ne) 78 | 79 | assert_equal Geokit::LatLng.new(8, 26), bounds.to_span 80 | end 81 | 82 | def test_bounds_to_span_with_bounds_crossing_prime_meridian 83 | sw = Geokit::LatLng.new(20, -70) 84 | ne = Geokit::LatLng.new(40, 100) 85 | bounds = Geokit::Bounds.new(sw, ne) 86 | 87 | assert_equal Geokit::LatLng.new(20, 170), bounds.to_span 88 | end 89 | 90 | def test_bounds_to_span_with_bounds_crossing_dateline 91 | sw = Geokit::LatLng.new(20, 100) 92 | ne = Geokit::LatLng.new(40, -70) 93 | bounds = Geokit::Bounds.new(sw, ne) 94 | 95 | assert_equal Geokit::LatLng.new(20, 190), bounds.to_span 96 | end 97 | end -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 1 | === 1.5.0 / 2009-09-21 2 | * fixed jruby compatibility (thanks manalang) 3 | * added country name to Google reverse geocoder (thanks joahking) 4 | * added DependentLocalityName as district, and SubAdministrativeAreaName as province (google geocoder only) 5 | * Google geocoder throws an error if you exceed geocoding rates (thanks drogus) 6 | 7 | === 1.4.1 / 2009-06-15 8 | * Fixed Ruby 1.9.1 compat and load order (thanks Niels Ganser) 9 | 10 | === 1.4.0 / 2009-05-27 11 | * Added country code/viewport biasing to GoogleGeocoder. Added Bounds#to_span method 12 | * Added suggested_bounds (Geokit::Bounds) property to GeoLoc. (Google geocoder only) 13 | * Added LatLng#reverse_geocode convenience method (thanks Tisho Georgiev for all three) 14 | 15 | === 1.3.2 / 2009-05-27 16 | * Fixed blank address geocoding bug 17 | 18 | === 1.3.1 / 2009-05-21 19 | * Support for External geocoders file (thanks dreamcat4) 20 | * Support multiple ip geocoders, including new setting for ip_provider_order (thanks dreamcat4) 21 | 22 | === 1.3.0 / 2009-04-11 23 | * Added capability to define multiple API keys for different domains that may be pointing to the same application (thanks Glenn Powell) 24 | * Added numeric accuracy accessor for Yahoo and Google geocoders (thanks Andrew Fecheyr Lippens) 25 | * Implement #hash and #eql? on LatLng to allow for using it as a hash key (thanks Luke Melia and Ross Kaffenberger) 26 | * 27 | 28 | === 1.2.6 / 2009-03-19 29 | * misc minor fixes 30 | 31 | === 1.2.5 / 2009-02-25 32 | 33 | * fixed GeoLoc.to_yaml 34 | * fixed minor google geocoding bug 35 | * now periodically publishing the Geokit gem to Rubyforge. Still maintaining development and managing contributions at Github 36 | 37 | === 1.2.4 / 2009-02-25 38 | 39 | * Improved Google geocoder in the Gem: Support for multiple geocoding results from the Google geocoder. (thanks github/pic) 40 | 41 | === 1.2.3 / 2009-02-01 42 | 43 | * Adding GeoPluginGeocoder for IP geocoding (thanks github/xjunior) 44 | * Ruby 1.9.1 compatibility and Unicode fixes (thanks github/Nielsomat) 45 | * various bug fixes 46 | 47 | === 1.2.1 / 2009-01-05 48 | 49 | * minor bug fixes 50 | * reverse geocoding added (Google only): res=Geokit::Geocoders::GoogleGeocoder.reverse_geocode "37.791821,-122.394679" 51 | * nautical miles added (in addition to miles and KM) 52 | 53 | === 1.2.0 / 2008-12-01 54 | 55 | * Improved Geocoder.us support -- respects authentication, and can geocode city names or zipcodes alone 56 | * cross-meridian finds work correctly with bounds conditions 57 | * fixed a problem with columns with "distance" in their name 58 | * added Geonames geocoder 59 | * the gem and plugin are now hosted at Github. 60 | 61 | === 1.1.1 / 2008-01-20 62 | * fixes for distance calculation (in-memory and database) when distances are either very small or 0. 63 | * NOTE: older versions of MySQL/Postgres may not work. See readme for more info. 64 | 65 | === 1.1.0 / 2007-12-07 66 | * Geokit is now Rails 2.0 / Edge friendly. 67 | 68 | === 1.0.0 / 2007-07-22 69 | * see http://earthcode.com/blog/2007/07/new_geokit_release.html 70 | * auto geocoding: an option to automatically geocode a model's address field on create 71 | * in-memory sort-by-distance for arrays of location objects 72 | * bounding box queries: `Location.find :all, :bounds=>[sw,ne]` 73 | * improved performance by automatically adding a bounding box condition to radial queries 74 | * new Bounds class for in-memory bounds-related operations 75 | * ability to calculate heading and midpoint between two points 76 | * ability to calculate endpoint given a point, heading, and distance 77 | 78 | -------------------------------------------------------------------------------- /test/test_ipgeocoder.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require File.join(File.dirname(__FILE__), 'test_base_geocoder') 3 | 4 | class IpGeocoderTest < BaseGeocoderTest #:nodoc: all 5 | 6 | IP_FAILURE=<<-EOF 7 | Country: SWITZERLAND (CH) 8 | City: (Unknown City) 9 | Latitude: 10 | Longitude: 11 | EOF 12 | 13 | IP_SUCCESS=<<-EOF 14 | Country: UNITED STATES (US) 15 | City: Sugar Grove, IL 16 | Latitude: 41.7696 17 | Longitude: -88.4588 18 | EOF 19 | 20 | IP_UNICODED=<<-EOF 21 | Country: SWEDEN (SE) 22 | City: Borås 23 | Latitude: 57.7167 24 | Longitude: 12.9167 25 | EOF 26 | 27 | PRIVATE_IPS_TO_TEST = [ 28 | '10.10.10.10', 29 | '172.16.1.3', 30 | '172.22.3.42', 31 | '172.30.254.164', 32 | '192.168.1.1', 33 | '0.0.0.0', 34 | '127.0.0.1', 35 | '240.3.4.5', 36 | '225.1.6.55' 37 | ].freeze 38 | 39 | def setup 40 | super 41 | @success.provider = "hostip" 42 | end 43 | 44 | def test_successful_lookup 45 | success = MockSuccess.new 46 | success.expects(:body).returns(IP_SUCCESS) 47 | url = 'http://api.hostip.info/get_html.php?ip=12.215.42.19&position=true' 48 | GeoKit::Geocoders::IpGeocoder.expects(:call_geocoder_service).with(url).returns(success) 49 | location = GeoKit::Geocoders::IpGeocoder.geocode('12.215.42.19') 50 | assert_not_nil location 51 | assert_equal 41.7696, location.lat 52 | assert_equal(-88.4588, location.lng) 53 | assert_equal "Sugar Grove", location.city 54 | assert_equal "IL", location.state 55 | assert_equal "US", location.country_code 56 | assert_equal "hostip", location.provider 57 | assert location.success? 58 | end 59 | 60 | def test_unicoded_lookup 61 | success = MockSuccess.new 62 | success.expects(:body).returns(IP_UNICODED) 63 | url = 'http://api.hostip.info/get_html.php?ip=12.215.42.19&position=true' 64 | GeoKit::Geocoders::IpGeocoder.expects(:call_geocoder_service).with(url).returns(success) 65 | location = GeoKit::Geocoders::IpGeocoder.geocode('12.215.42.19') 66 | assert_not_nil location 67 | assert_equal 57.7167, location.lat 68 | assert_equal 12.9167, location.lng 69 | assert_equal "Bor\303\245s", location.city 70 | assert_nil location.state 71 | assert_equal "SE", location.country_code 72 | assert_equal "hostip", location.provider 73 | assert location.success? 74 | end 75 | 76 | def test_failed_lookup 77 | failure = MockSuccess.new 78 | failure.expects(:body).returns(IP_FAILURE) 79 | url = 'http://api.hostip.info/get_html.php?ip=128.178.0.0&position=true' 80 | GeoKit::Geocoders::IpGeocoder.expects(:call_geocoder_service).with(url).returns(failure) 81 | location = GeoKit::Geocoders::IpGeocoder.geocode("128.178.0.0") 82 | assert_not_nil location 83 | assert !location.success? 84 | end 85 | 86 | def test_private_ips 87 | GeoKit::Geocoders::IpGeocoder.expects(:call_geocoder_service).never 88 | PRIVATE_IPS_TO_TEST.each do |ip| 89 | location = GeoKit::Geocoders::IpGeocoder.geocode(ip) 90 | assert_not_nil location 91 | assert !location.success? 92 | end 93 | end 94 | 95 | def test_invalid_ip 96 | GeoKit::Geocoders::IpGeocoder.expects(:call_geocoder_service).never 97 | location = GeoKit::Geocoders::IpGeocoder.geocode("blah") 98 | assert_not_nil location 99 | assert !location.success? 100 | end 101 | 102 | def test_service_unavailable 103 | failure = MockFailure.new 104 | url = 'http://api.hostip.info/get_html.php?ip=12.215.42.19&position=true' 105 | GeoKit::Geocoders::IpGeocoder.expects(:call_geocoder_service).with(url).returns(failure) 106 | location = GeoKit::Geocoders::IpGeocoder.geocode("12.215.42.19") 107 | assert_not_nil location 108 | assert !location.success? 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /test/test_multi_geocoder.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_base_geocoder') 2 | 3 | Geokit::Geocoders::provider_order=[:google,:yahoo,:us] 4 | 5 | class MultiGeocoderTest < BaseGeocoderTest #:nodoc: all 6 | 7 | def setup 8 | super 9 | @failure = Geokit::GeoLoc.new 10 | end 11 | 12 | def test_successful_first 13 | Geokit::Geocoders::GoogleGeocoder.expects(:geocode).with(@address, {}).returns(@success) 14 | assert_equal @success, Geokit::Geocoders::MultiGeocoder.geocode(@address) 15 | end 16 | 17 | def test_failover 18 | Geokit::Geocoders::GoogleGeocoder.expects(:geocode).with(@address, {}).returns(@failure) 19 | Geokit::Geocoders::YahooGeocoder.expects(:geocode).with(@address, {}).returns(@success) 20 | assert_equal @success, Geokit::Geocoders::MultiGeocoder.geocode(@address) 21 | end 22 | 23 | def test_double_failover 24 | Geokit::Geocoders::GoogleGeocoder.expects(:geocode).with(@address, {}).returns(@failure) 25 | Geokit::Geocoders::YahooGeocoder.expects(:geocode).with(@address, {}).returns(@failure) 26 | Geokit::Geocoders::UsGeocoder.expects(:geocode).with(@address, {}).returns(@success) 27 | assert_equal @success, Geokit::Geocoders::MultiGeocoder.geocode(@address) 28 | end 29 | 30 | def test_failure 31 | Geokit::Geocoders::GoogleGeocoder.expects(:geocode).with(@address, {}).returns(@failure) 32 | Geokit::Geocoders::YahooGeocoder.expects(:geocode).with(@address, {}).returns(@failure) 33 | Geokit::Geocoders::UsGeocoder.expects(:geocode).with(@address, {}).returns(@failure) 34 | assert_equal @failure, Geokit::Geocoders::MultiGeocoder.geocode(@address) 35 | end 36 | 37 | def test_invalid_provider 38 | temp = Geokit::Geocoders::provider_order 39 | Geokit::Geocoders.provider_order = [:bogus] 40 | assert_equal @failure, Geokit::Geocoders::MultiGeocoder.geocode(@address) 41 | Geokit::Geocoders.provider_order = temp 42 | end 43 | 44 | def test_blank_address 45 | t1, t2 = Geokit::Geocoders.provider_order, Geokit::Geocoders.ip_provider_order # will need to reset after 46 | Geokit::Geocoders.provider_order = [:google] 47 | Geokit::Geocoders.ip_provider_order = [:geo_plugin] 48 | Geokit::Geocoders::GoogleGeocoder.expects(:geocode).with("", {}).returns(@failure) 49 | Geokit::Geocoders::GeoPluginGeocoder.expects(:geocode).never 50 | assert_equal @failure, Geokit::Geocoders::MultiGeocoder.geocode("") 51 | Geokit::Geocoders.provider_order, Geokit::Geocoders.ip_provider_order = t1, t2 # reset to orig values 52 | end 53 | 54 | def test_reverse_geocode_successful_first 55 | Geokit::Geocoders::GoogleGeocoder.expects(:reverse_geocode).with(@latlng).returns(@success) 56 | assert_equal @success, Geokit::Geocoders::MultiGeocoder.reverse_geocode(@latlng) 57 | end 58 | 59 | def test_reverse_geocode_failover 60 | Geokit::Geocoders::GoogleGeocoder.expects(:reverse_geocode).with(@latlng).returns(@failure) 61 | Geokit::Geocoders::YahooGeocoder.expects(:reverse_geocode).with(@latlng).returns(@success) 62 | assert_equal @success, Geokit::Geocoders::MultiGeocoder.reverse_geocode(@latlng) 63 | end 64 | 65 | def test_reverse_geocode_double_failover 66 | Geokit::Geocoders::GoogleGeocoder.expects(:reverse_geocode).with(@latlng).returns(@failure) 67 | Geokit::Geocoders::YahooGeocoder.expects(:reverse_geocode).with(@latlng).returns(@failure) 68 | Geokit::Geocoders::UsGeocoder.expects(:reverse_geocode).with(@latlng).returns(@success) 69 | assert_equal @success, Geokit::Geocoders::MultiGeocoder.reverse_geocode(@latlng) 70 | end 71 | 72 | def test_reverse_geocode_failure 73 | Geokit::Geocoders::GoogleGeocoder.expects(:reverse_geocode).with(@latlng).returns(@failure) 74 | Geokit::Geocoders::YahooGeocoder.expects(:reverse_geocode).with(@latlng).returns(@failure) 75 | Geokit::Geocoders::UsGeocoder.expects(:reverse_geocode).with(@latlng).returns(@failure) 76 | assert_equal @failure, Geokit::Geocoders::MultiGeocoder.reverse_geocode(@latlng) 77 | end 78 | 79 | def test_reverse_geocode_with_invalid_provider 80 | temp = Geokit::Geocoders::provider_order 81 | Geokit::Geocoders.provider_order = [:bogus] 82 | assert_equal @failure, Geokit::Geocoders::MultiGeocoder.reverse_geocode(@latlng) 83 | Geokit::Geocoders.provider_order = temp 84 | end 85 | 86 | def test_reverse_geocode_with_blank_latlng 87 | t1 = Geokit::Geocoders.provider_order # will need to reset after 88 | Geokit::Geocoders.provider_order = [:google] 89 | Geokit::Geocoders::GoogleGeocoder.expects(:reverse_geocode).with("").returns(@failure) 90 | assert_equal @failure, Geokit::Geocoders::MultiGeocoder.reverse_geocode("") 91 | Geokit::Geocoders.provider_order = t1 # reset to orig values 92 | end 93 | end -------------------------------------------------------------------------------- /test/test_yahoo_geocoder.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_base_geocoder') 2 | 3 | Geokit::Geocoders::yahoo = 'Yahoo' 4 | 5 | class YahooGeocoderTest < BaseGeocoderTest #:nodoc: all 6 | YAHOO_FULL=<<-EOF.strip 7 | 8 | 37.792406-122.39411
100 SPEAR ST
SAN FRANCISCOCA94105-1522US
9 | 10 | EOF 11 | 12 | YAHOO_CITY=<<-EOF.strip 13 | 14 | 37.7742-122.417068
SAN FRANCISCOCAUS
15 | 16 | EOF 17 | 18 | def setup 19 | super 20 | @yahoo_full_hash = {:street_address=>"100 Spear St", :city=>"San Francisco", :state=>"CA", :zip=>"94105-1522", :country_code=>"US"} 21 | @yahoo_city_hash = {:city=>"San Francisco", :state=>"CA"} 22 | @yahoo_full_loc = Geokit::GeoLoc.new(@yahoo_full_hash) 23 | @yahoo_city_loc = Geokit::GeoLoc.new(@yahoo_city_hash) 24 | end 25 | 26 | # the testing methods themselves 27 | def test_yahoo_full_address 28 | response = MockSuccess.new 29 | response.expects(:body).returns(YAHOO_FULL) 30 | url = "http://api.local.yahoo.com/MapsService/V1/geocode?appid=Yahoo&location=#{Geokit::Inflector.url_escape(@address)}" 31 | Geokit::Geocoders::YahooGeocoder.expects(:call_geocoder_service).with(url).returns(response) 32 | do_full_address_assertions(Geokit::Geocoders::YahooGeocoder.geocode(@address)) 33 | end 34 | 35 | def test_yahoo_full_address_accuracy 36 | response = MockSuccess.new 37 | response.expects(:body).returns(YAHOO_FULL) 38 | url = "http://api.local.yahoo.com/MapsService/V1/geocode?appid=Yahoo&location=#{Geokit::Inflector.url_escape(@address)}" 39 | Geokit::Geocoders::YahooGeocoder.expects(:call_geocoder_service).with(url).returns(response) 40 | res = Geokit::Geocoders::YahooGeocoder.geocode(@address) 41 | assert_equal 8, res.accuracy 42 | end 43 | 44 | def test_yahoo_full_address_with_geo_loc 45 | response = MockSuccess.new 46 | response.expects(:body).returns(YAHOO_FULL) 47 | url = "http://api.local.yahoo.com/MapsService/V1/geocode?appid=Yahoo&location=#{Geokit::Inflector.url_escape(@full_address)}" 48 | Geokit::Geocoders::YahooGeocoder.expects(:call_geocoder_service).with(url).returns(response) 49 | do_full_address_assertions(Geokit::Geocoders::YahooGeocoder.geocode(@yahoo_full_loc)) 50 | end 51 | 52 | def test_yahoo_city 53 | response = MockSuccess.new 54 | response.expects(:body).returns(YAHOO_CITY) 55 | url = "http://api.local.yahoo.com/MapsService/V1/geocode?appid=Yahoo&location=#{Geokit::Inflector.url_escape(@address)}" 56 | Geokit::Geocoders::YahooGeocoder.expects(:call_geocoder_service).with(url).returns(response) 57 | do_city_assertions(Geokit::Geocoders::YahooGeocoder.geocode(@address)) 58 | end 59 | 60 | def test_yahoo_city_accuracy 61 | response = MockSuccess.new 62 | response.expects(:body).returns(YAHOO_CITY) 63 | url = "http://api.local.yahoo.com/MapsService/V1/geocode?appid=Yahoo&location=#{Geokit::Inflector.url_escape(@address)}" 64 | Geokit::Geocoders::YahooGeocoder.expects(:call_geocoder_service).with(url).returns(response) 65 | res = Geokit::Geocoders::YahooGeocoder.geocode(@address) 66 | assert_equal 4, res.accuracy 67 | end 68 | 69 | def test_yahoo_city_with_geo_loc 70 | response = MockSuccess.new 71 | response.expects(:body).returns(YAHOO_CITY) 72 | url = "http://api.local.yahoo.com/MapsService/V1/geocode?appid=Yahoo&location=#{Geokit::Inflector.url_escape(@address)}" 73 | Geokit::Geocoders::YahooGeocoder.expects(:call_geocoder_service).with(url).returns(response) 74 | do_city_assertions(Geokit::Geocoders::YahooGeocoder.geocode(@yahoo_city_loc)) 75 | end 76 | 77 | def test_service_unavailable 78 | response = MockFailure.new 79 | url = "http://api.local.yahoo.com/MapsService/V1/geocode?appid=Yahoo&location=#{Geokit::Inflector.url_escape(@address)}" 80 | Geokit::Geocoders::YahooGeocoder.expects(:call_geocoder_service).with(url).returns(response) 81 | assert !Geokit::Geocoders::YahooGeocoder.geocode(@yahoo_city_loc).success 82 | end 83 | 84 | private 85 | 86 | # next two methods do the assertions for both address-level and city-level lookups 87 | def do_full_address_assertions(res) 88 | assert_equal "CA", res.state 89 | assert_equal "San Francisco", res.city 90 | assert_equal "37.792406,-122.39411", res.ll 91 | assert res.is_us? 92 | assert_equal "100 Spear St, San Francisco, CA, 94105-1522, US", res.full_address 93 | assert_equal "yahoo", res.provider 94 | end 95 | 96 | def do_city_assertions(res) 97 | assert_equal "CA", res.state 98 | assert_equal "San Francisco", res.city 99 | assert_equal "37.7742,-122.417068", res.ll 100 | assert res.is_us? 101 | assert_equal "San Francisco, CA, US", res.full_address 102 | assert_nil res.street_address 103 | assert_equal "yahoo", res.provider 104 | end 105 | end -------------------------------------------------------------------------------- /test/test_google_reverse_geocoder.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_base_geocoder') 2 | 3 | Geokit::Geocoders::google = 'Google' 4 | 5 | class GoogleReverseGeocoderTest < BaseGeocoderTest #:nodoc: all 6 | 7 | GOOGLE_REVERSE_FULL=<<-EOF.strip 8 | 51.457833,7.016685200geocode
Porscheplatz 1, 45127 Essen, Deutschland
DEDeutschlandNordrhein-WestfalenEssenEssenStadtkernPorscheplatz 1451277.0166848,51.4578329,0
Stadtkern, Essen, Deutschland
DEDeutschlandNordrhein-WestfalenEssenEssenStadtkern7.0124328,51.4568201,0
45127 Essen, Deutschland
DEDeutschlandNordrhein-WestfalenEssenEssen451277.0104543,51.4556194,0
Essen, Deutschland
DEDeutschlandNordrhein-WestfalenEssenEssen7.0147614,51.4580686,0
Essen, Deutschland
DEDeutschlandNordrhein-WestfalenEssen7.0461136,51.4508381,0
Nordrhein-Westfalen, Deutschland
DEDeutschlandNordrhein-Westfalen7.6615938,51.4332367,0
Deutschland
DEDeutschland10.4515260,51.1656910,0
9 | EOF 10 | 11 | 12 | def test_google_full_address 13 | response = MockSuccess.new 14 | response.expects(:body).returns(GOOGLE_REVERSE_FULL) 15 | 16 | 17 | # http://maps.google.com/maps/geo?output=xml&oe=utf-8&ll=51.4578329,7.0166848&key=asdad 18 | 19 | # # 31 | # 32 | 33 | 34 | @latlng = "51.4578329,7.0166848" 35 | 36 | url = "http://maps.google.com/maps/geo?ll=#{Geokit::Inflector.url_escape(@latlng)}&output=xml&key=Google&oe=utf-8" 37 | Geokit::Geocoders::GoogleGeocoder.expects(:call_geocoder_service).with(url).returns(response) 38 | res=Geokit::Geocoders::GoogleGeocoder.reverse_geocode(@latlng) 39 | assert_equal "Nordrhein-Westfalen", res.state 40 | assert_equal "Essen", res.city 41 | assert_equal "45127", res.zip 42 | assert_equal "51.4578329,7.0166848", res.ll # slightly dif from yahoo 43 | assert res.is_us? == false 44 | assert_equal "Porscheplatz 1, 45127 Essen, Deutschland", res.full_address #slightly different from yahoo 45 | assert_equal "google", res.provider 46 | end 47 | 48 | 49 | end -------------------------------------------------------------------------------- /test/test_latlng.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'lib/geokit' 3 | require 'mocha' 4 | 5 | class LatLngTest < Test::Unit::TestCase #:nodoc: all 6 | 7 | def setup 8 | @loc_a = Geokit::LatLng.new(32.918593,-96.958444) 9 | @loc_e = Geokit::LatLng.new(32.969527,-96.990159) 10 | @point = Geokit::LatLng.new(@loc_a.lat, @loc_a.lng) 11 | end 12 | 13 | def valid_reverse_geocoding_result 14 | location = Geokit::GeoLoc.new({ 15 | :city => "Essen", 16 | :country_code => "DE", 17 | :lat => 51.4578329, 18 | :lng => 7.0166848, 19 | :provider => "google", 20 | :state => "Nordrhein-Westfalen", 21 | :street_address => "Porscheplatz 1", 22 | :zip => "45127" 23 | }) 24 | 25 | location.full_address = "Porscheplatz 1, 45127 Essen, Deutschland" 26 | location.precision = 'address' 27 | location.provider = 'google' 28 | location.success = true 29 | location 30 | end 31 | 32 | def test_distance_between_same_using_defaults 33 | assert_equal 0, Geokit::LatLng.distance_between(@loc_a, @loc_a) 34 | assert_equal 0, @loc_a.distance_to(@loc_a) 35 | end 36 | 37 | def test_distance_between_same_with_miles_and_flat 38 | assert_equal 0, Geokit::LatLng.distance_between(@loc_a, @loc_a, :units => :miles, :formula => :flat) 39 | assert_equal 0, @loc_a.distance_to(@loc_a, :units => :miles, :formula => :flat) 40 | end 41 | 42 | def test_distance_between_same_with_kms_and_flat 43 | assert_equal 0, Geokit::LatLng.distance_between(@loc_a, @loc_a, :units => :kms, :formula => :flat) 44 | assert_equal 0, @loc_a.distance_to(@loc_a, :units => :kms, :formula => :flat) 45 | end 46 | 47 | def test_distance_between_same_with_nms_and_flat 48 | assert_equal 0, Geokit::LatLng.distance_between(@loc_a, @loc_a, :units => :nms, :formula => :flat) 49 | assert_equal 0, @loc_a.distance_to(@loc_a, :units => :nms, :formula => :flat) 50 | end 51 | 52 | def test_distance_between_same_with_miles_and_sphere 53 | assert_equal 0, Geokit::LatLng.distance_between(@loc_a, @loc_a, :units => :miles, :formula => :sphere) 54 | assert_equal 0, @loc_a.distance_to(@loc_a, :units => :miles, :formula => :sphere) 55 | end 56 | 57 | def test_distance_between_same_with_kms_and_sphere 58 | assert_equal 0, Geokit::LatLng.distance_between(@loc_a, @loc_a, :units => :kms, :formula => :sphere) 59 | assert_equal 0, @loc_a.distance_to(@loc_a, :units => :kms, :formula => :sphere) 60 | end 61 | 62 | def test_distance_between_same_with_nms_and_sphere 63 | assert_equal 0, Geokit::LatLng.distance_between(@loc_a, @loc_a, :units => :nms, :formula => :sphere) 64 | assert_equal 0, @loc_a.distance_to(@loc_a, :units => :nms, :formula => :sphere) 65 | end 66 | 67 | def test_distance_between_diff_using_defaults 68 | assert_in_delta 3.97, Geokit::LatLng.distance_between(@loc_a, @loc_e), 0.01 69 | assert_in_delta 3.97, @loc_a.distance_to(@loc_e), 0.01 70 | end 71 | 72 | def test_distance_between_diff_with_miles_and_flat 73 | assert_in_delta 3.97, Geokit::LatLng.distance_between(@loc_a, @loc_e, :units => :miles, :formula => :flat), 0.2 74 | assert_in_delta 3.97, @loc_a.distance_to(@loc_e, :units => :miles, :formula => :flat), 0.2 75 | end 76 | 77 | def test_distance_between_diff_with_kms_and_flat 78 | assert_in_delta 6.39, Geokit::LatLng.distance_between(@loc_a, @loc_e, :units => :kms, :formula => :flat), 0.4 79 | assert_in_delta 6.39, @loc_a.distance_to(@loc_e, :units => :kms, :formula => :flat), 0.4 80 | end 81 | 82 | def test_distance_between_diff_with_nms_and_flat 83 | assert_in_delta 3.334, Geokit::LatLng.distance_between(@loc_a, @loc_e, :units => :nms, :formula => :flat), 0.4 84 | assert_in_delta 3.334, @loc_a.distance_to(@loc_e, :units => :nms, :formula => :flat), 0.4 85 | end 86 | 87 | def test_distance_between_diff_with_miles_and_sphere 88 | assert_in_delta 3.97, Geokit::LatLng.distance_between(@loc_a, @loc_e, :units => :miles, :formula => :sphere), 0.01 89 | assert_in_delta 3.97, @loc_a.distance_to(@loc_e, :units => :miles, :formula => :sphere), 0.01 90 | end 91 | 92 | def test_distance_between_diff_with_kms_and_sphere 93 | assert_in_delta 6.39, Geokit::LatLng.distance_between(@loc_a, @loc_e, :units => :kms, :formula => :sphere), 0.01 94 | assert_in_delta 6.39, @loc_a.distance_to(@loc_e, :units => :kms, :formula => :sphere), 0.01 95 | end 96 | 97 | def test_distance_between_diff_with_nms_and_sphere 98 | assert_in_delta 3.454, Geokit::LatLng.distance_between(@loc_a, @loc_e, :units => :nms, :formula => :sphere), 0.01 99 | assert_in_delta 3.454, @loc_a.distance_to(@loc_e, :units => :nms, :formula => :sphere), 0.01 100 | end 101 | 102 | def test_manually_mixed_in 103 | assert_equal 0, Geokit::LatLng.distance_between(@point, @point) 104 | assert_equal 0, @point.distance_to(@point) 105 | assert_equal 0, @point.distance_to(@loc_a) 106 | assert_in_delta 3.97, @point.distance_to(@loc_e, :units => :miles, :formula => :flat), 0.2 107 | assert_in_delta 6.39, @point.distance_to(@loc_e, :units => :kms, :formula => :flat), 0.4 108 | assert_in_delta 3.334, @point.distance_to(@loc_e, :units => :nms, :formula => :flat), 0.4 109 | end 110 | 111 | def test_heading_between 112 | assert_in_delta 332, Geokit::LatLng.heading_between(@loc_a,@loc_e), 0.5 113 | end 114 | 115 | def test_heading_to 116 | assert_in_delta 332, @loc_a.heading_to(@loc_e), 0.5 117 | end 118 | 119 | def test_class_endpoint 120 | endpoint=Geokit::LatLng.endpoint(@loc_a, 332, 3.97) 121 | assert_in_delta @loc_e.lat, endpoint.lat, 0.0005 122 | assert_in_delta @loc_e.lng, endpoint.lng, 0.0005 123 | end 124 | 125 | def test_instance_endpoint 126 | endpoint=@loc_a.endpoint(332, 3.97) 127 | assert_in_delta @loc_e.lat, endpoint.lat, 0.0005 128 | assert_in_delta @loc_e.lng, endpoint.lng, 0.0005 129 | end 130 | 131 | def test_midpoint 132 | midpoint=@loc_a.midpoint_to(@loc_e) 133 | assert_in_delta 32.944061, midpoint.lat, 0.0005 134 | assert_in_delta(-96.974296, midpoint.lng, 0.0005) 135 | end 136 | 137 | def test_normalize 138 | lat=37.7690 139 | lng=-122.443 140 | res=Geokit::LatLng.normalize(lat,lng) 141 | assert_equal res,Geokit::LatLng.new(lat,lng) 142 | res=Geokit::LatLng.normalize("#{lat}, #{lng}") 143 | assert_equal res,Geokit::LatLng.new(lat,lng) 144 | res=Geokit::LatLng.normalize("#{lat} #{lng}") 145 | assert_equal res,Geokit::LatLng.new(lat,lng) 146 | res=Geokit::LatLng.normalize("#{lat.to_i} #{lng.to_i}") 147 | assert_equal res,Geokit::LatLng.new(lat.to_i,lng.to_i) 148 | res=Geokit::LatLng.normalize([lat,lng]) 149 | assert_equal res,Geokit::LatLng.new(lat,lng) 150 | end 151 | 152 | def test_hash 153 | lat=37.7690 154 | lng=-122.443 155 | first = Geokit::LatLng.new(lat,lng) 156 | second = Geokit::LatLng.new(lat,lng) 157 | assert_equal first.hash, second.hash 158 | end 159 | 160 | def test_eql? 161 | lat=37.7690 162 | lng=-122.443 163 | first = Geokit::LatLng.new(lat,lng) 164 | second = Geokit::LatLng.new(lat,lng) 165 | assert first.eql?(second) 166 | assert second.eql?(first) 167 | end 168 | 169 | def test_reverse_geocode 170 | point = Geokit::LatLng.new(51.4578329, 7.0166848) 171 | Geokit::Geocoders::MultiGeocoder.expects(:reverse_geocode).with(point).returns(valid_reverse_geocoding_result) 172 | res = point.reverse_geocode 173 | 174 | assert_equal "Nordrhein-Westfalen", res.state 175 | assert_equal "Essen", res.city 176 | assert_equal "45127", res.zip 177 | assert_equal "51.4578329,7.0166848", res.ll # slightly dif from yahoo 178 | assert res.is_us? == false 179 | assert_equal "Porscheplatz 1, 45127 Essen, Deutschland", res.full_address #slightly different from yahoo 180 | end 181 | 182 | def test_reverse_geocoding_using_specific_geocoder 183 | point = Geokit::LatLng.new(51.4578329, 7.0166848) 184 | Geokit::Geocoders::GoogleGeocoder.expects(:reverse_geocode).with(point).returns(valid_reverse_geocoding_result) 185 | res = point.reverse_geocode(:using => Geokit::Geocoders::GoogleGeocoder) 186 | 187 | assert_equal "Nordrhein-Westfalen", res.state 188 | assert_equal "Essen", res.city 189 | assert_equal "45127", res.zip 190 | assert_equal "51.4578329,7.0166848", res.ll # slightly dif from yahoo 191 | assert res.is_us? == false 192 | assert_equal "Porscheplatz 1, 45127 Essen, Deutschland", res.full_address #slightly different from yahoo 193 | assert_equal "google", res.provider 194 | end 195 | 196 | def test_reverse_geocoding_using_specific_geocoder_short_syntax 197 | point = Geokit::LatLng.new(51.4578329, 7.0166848) 198 | Geokit::Geocoders::GoogleGeocoder.expects(:reverse_geocode).with(point).returns(valid_reverse_geocoding_result) 199 | res = point.reverse_geocode(:using => :google) 200 | 201 | assert_equal "Nordrhein-Westfalen", res.state 202 | assert_equal "Essen", res.city 203 | assert_equal "45127", res.zip 204 | assert_equal "51.4578329,7.0166848", res.ll # slightly dif from yahoo 205 | assert res.is_us? == false 206 | assert_equal "Porscheplatz 1, 45127 Essen, Deutschland", res.full_address #slightly different from yahoo 207 | assert_equal "google", res.provider 208 | end 209 | end -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | 2 | ## GEOKIT GEM DESCRIPTION 3 | 4 | This clone will provide access to Google API Premier - which uses a client id, and signs the url. 5 | 6 | The Geokit gem provides: 7 | 8 | * Distance calculations between two points on the earth. Calculate the distance in miles, kilometers, or nautical miles, with all the trigonometry abstracted away by GeoKit. 9 | * Geocoding from multiple providers. It supports Google, Yahoo, Geocoder.us, and Geocoder.ca geocoders, and others. It provides a uniform response structure from all of them. 10 | It also provides a fail-over mechanism, in case your input fails to geocode in one service. 11 | * Rectangular bounds calculations: is a point within a given rectangular bounds? 12 | * Heading and midpoint calculations 13 | 14 | Combine this gem with the [geokit-rails plugin](http://github.com/andre/geokit-rails/tree/master) to get location-based finders for your Rails app. 15 | 16 | * Geokit Documentation at Rubyforge [http://geokit.rubyforge.org](http://geokit.rubyforge.org). 17 | * Repository at Github: [http://github.com/andre/geokit-gem/tree/master](http://github.com/andre/geokit-gem/tree/master). 18 | * Follow the Google Group for updates and discussion on Geokit: [http://groups.google.com/group/geokit](http://groups.google.com/group/geokit) 19 | 20 | ## INSTALL 21 | 22 | sudo gem install geokit 23 | 24 | ## QUICK START 25 | 26 | irb> require 'rubygems' 27 | irb> require 'geokit' 28 | irb> a=Geokit::Geocoders::YahooGeocoder.geocode '140 Market St, San Francisco, CA' 29 | irb> a.ll 30 | => 37.79363,-122.396116 31 | irb> b=Geokit::Geocoders::YahooGeocoder.geocode '789 Geary St, San Francisco, CA' 32 | irb> b.ll 33 | => 37.786217,-122.41619 34 | irb> a.distance_to(b) 35 | => 1.21120007413626 36 | irb> a.heading_to(b) 37 | => 244.959832435678 38 | irb(main):006:0> c=a.midpoint_to(b) # what's halfway from a to b? 39 | irb> c.ll 40 | => "37.7899239257175,-122.406153503469" 41 | irb(main):008:0> d=c.endpoint(90,10) # what's 10 miles to the east of c? 42 | irb> d.ll 43 | => "37.7897825005142,-122.223214776155" 44 | 45 | FYI, that `.ll` method means "latitude longitude". 46 | 47 | See the RDOC more more ... there are also operations on rectangular bounds (e.g., determining if a point is within bounds, find the center, etc). 48 | 49 | ## CONFIGURATION 50 | 51 | If you're using this gem by itself, here are the configuration options: 52 | 53 | # These defaults are used in Geokit::Mappable.distance_to and in acts_as_mappable 54 | Geokit::default_units = :miles 55 | Geokit::default_formula = :sphere 56 | 57 | # This is the timeout value in seconds to be used for calls to the geocoder web 58 | # services. For no timeout at all, comment out the setting. The timeout unit 59 | # is in seconds. 60 | Geokit::Geocoders::request_timeout = 3 61 | 62 | # These settings are used if web service calls must be routed through a proxy. 63 | # These setting can be nil if not needed, otherwise, addr and port must be 64 | # filled in at a minimum. If the proxy requires authentication, the username 65 | # and password can be provided as well. 66 | Geokit::Geocoders::proxy_addr = nil 67 | Geokit::Geocoders::proxy_port = nil 68 | Geokit::Geocoders::proxy_user = nil 69 | Geokit::Geocoders::proxy_pass = nil 70 | 71 | # This is your yahoo application key for the Yahoo Geocoder. 72 | # See http://developer.yahoo.com/faq/index.html#appid 73 | # and http://developer.yahoo.com/maps/rest/V1/geocode.html 74 | Geokit::Geocoders::yahoo = 'REPLACE_WITH_YOUR_YAHOO_KEY' 75 | 76 | # This is your Google Maps geocoder key. 77 | # See http://www.google.com/apis/maps/signup.html 78 | # and http://www.google.com/apis/maps/documentation/#Geocoding_Examples 79 | Geokit::Geocoders::google = 'REPLACE_WITH_YOUR_GOOGLE_KEY' 80 | 81 | # You can also set multiple API KEYS for different domains that may be directed to this same application. 82 | # The domain from which the current user is being directed will automatically be updated for Geokit via 83 | # the GeocoderControl class, which gets it's begin filter mixed into the ActionController. 84 | # You define these keys with a Hash as follows: 85 | #Geokit::Geocoders::google = { 'rubyonrails.org' => 'RUBY_ON_RAILS_API_KEY', 'ruby-docs.org' => 'RUBY_DOCS_API_KEY' } 86 | 87 | # This is your username and password for geocoder.us. 88 | # To use the free service, the value can be set to nil or false. For 89 | # usage tied to an account, the value should be set to username:password. 90 | # See http://geocoder.us 91 | # and http://geocoder.us/user/signup 92 | Geokit::Geocoders::geocoder_us = false 93 | 94 | # This is your authorization key for geocoder.ca. 95 | # To use the free service, the value can be set to nil or false. For 96 | # usage tied to an account, set the value to the key obtained from 97 | # Geocoder.ca. 98 | # See http://geocoder.ca 99 | # and http://geocoder.ca/?register=1 100 | Geokit::Geocoders::geocoder_ca = false 101 | 102 | # require "external_geocoder.rb" 103 | # Please see the section "writing your own geocoders" for more information. 104 | # Geokit::Geocoders::external_key = 'REPLACE_WITH_YOUR_API_KEY' 105 | 106 | # This is the order in which the geocoders are called in a failover scenario 107 | # If you only want to use a single geocoder, put a single symbol in the array. 108 | # Valid symbols are :google, :yahoo, :us, and :ca. 109 | # Be aware that there are Terms of Use restrictions on how you can use the 110 | # various geocoders. Make sure you read up on relevant Terms of Use for each 111 | # geocoder you are going to use. 112 | Geokit::Geocoders::provider_order = [:google,:us] 113 | 114 | # The IP provider order. Valid symbols are :ip,:geo_plugin. 115 | # As before, make sure you read up on relevant Terms of Use for each. 116 | # Geokit::Geocoders::ip_provider_order = [:external,:geo_plugin,:ip] 117 | 118 | If you're using this gem with the [geokit-rails plugin](http://github.com/andre/geokit-rails/tree/master), the plugin 119 | creates a template with these settings and places it in `config/initializers/geokit_config.rb`. 120 | 121 | ## SUPPORTED GEOCODERS 122 | 123 | ### "regular" address geocoders 124 | * Yahoo Geocoder - requires an API key. 125 | * Geocoder.us - may require authentication if performing more than the free request limit. 126 | * Geocoder.ca - for Canada; may require authentication as well. 127 | * Geonames - a free geocoder 128 | 129 | ### address geocoders that also provide reverse geocoding 130 | * Google Geocoder - requires an API key. Also supports multiple results and bounding box/country code biasing. 131 | 132 | ### IP address geocoders 133 | * IP Geocoder - geocodes an IP address using hostip.info's web service. 134 | * Geoplugin.net -- another IP address geocoder 135 | 136 | ### Google Geocoder Tricks 137 | 138 | The Google Geocoder sports a number of useful tricks that elevate it a little bit above the rest of the currently supported geocoders. For starters, it returns a `suggested_bounds` property for all your geocoded results, so you can more easily decide where and how to center a map on the places you geocode. Here's a quick example: 139 | 140 | irb> res = Geokit::Geocoders::GoogleGeocoder.geocode('140 Market St, San Francisco, CA') 141 | irb> pp res.suggested_bounds 142 | #, 144 | @sw=#> 145 | 146 | In addition, you can use viewport or country code biasing to make sure the geocoders prefers results within a specific area. Say we wanted to geocode the city of Syracuse in Italy. A normal geocoding query would look like this: 147 | 148 | irb> res = Geokit::Geocoder::GoogleGeocoder.geocode('Syracuse') 149 | irb> res.full_address 150 | => "Syracuse, NY, USA" 151 | 152 | Not exactly what we were looking for. We know that Syracuse is in Italy, so we can tell the Google Geocoder to prefer results from Italy first, and then wander the Syracuses of the world. To do that, we have to pass Italy's ccTLD (country code top-level domain) to the `:bias` option of the `geocode` method. You can find a comprehensive list of all ccTLDs here: http://en.wikipedia.org/wiki/CcTLD. 153 | 154 | irb> res = Geokit::Geocoder::GoogleGeocoder.geocode('Syracuse', :bias => 'it') 155 | irb> res.full_address 156 | => "Syracuse, Italy" 157 | 158 | Alternatively, we can speficy the geocoding bias as a bounding box object. Say we wanted to geocode the Winnetka district in Los Angeles. 159 | 160 | irb> res = Geokit::Geocoder::GoogleGeocoder.geocode('Winnetka') 161 | irb> res.full_address 162 | => "Winnetka, IL, USA" 163 | 164 | Not it. What we can do is tell the geocoder to return results only from in and around LA. 165 | 166 | irb> la_bounds = Geokit::Geocoder::GoogleGeocoder.geocode('Los Angeles').suggested_bounds 167 | irb> res = Geokit::Geocoder::GoogleGeocoder.geocode('Winnetka', :bias => la_bounds) 168 | irb> res.full_address 169 | => "Winnetka, California, USA" 170 | 171 | 172 | ### The Multigeocoder 173 | Multi Geocoder - provides failover for the physical location geocoders, and also IP address geocoders. Its configured by setting Geokit::Geocoders::provider_order, and Geokit::Geocoders::ip_provider_order. You should call the Multi-Geocoder with its :geocode method, supplying one address parameter which is either a real street address, or an ip address. For example: 174 | 175 | Geokit::Geocoders::MultiGeocoder.geocode("900 Sycamore Drive") 176 | 177 | Geokit::Geocoders::MultiGeocoder.geocode("12.12.12.12") 178 | 179 | ## MULTIPLE RESULTS 180 | Some geocoding services will return multple results if the there isn't one clear result. 181 | Geoloc can capture multiple results through its "all" method. Currently only the Google geocoder 182 | supports multiple results: 183 | 184 | irb> geo=Geokit::Geocoders::GoogleGeocoder.geocode("900 Sycamore Drive") 185 | irb> geo.full_address 186 | => "900 Sycamore Dr, Arkadelphia, AR 71923, USA" 187 | irb> geo.all.size 188 | irb> geo.all.each { |e| puts e.full_address } 189 | 900 Sycamore Dr, Arkadelphia, AR 71923, USA 190 | 900 Sycamore Dr, Burkburnett, TX 76354, USA 191 | 900 Sycamore Dr, TN 38361, USA 192 | .... 193 | 194 | geo.all is just an array of additional Geolocs, so do what you want with it. If you call .all on a 195 | geoloc that doesn't have any additional results, you will get an array of one. 196 | 197 | 198 | ## NOTES ON WHAT'S WHERE 199 | 200 | mappable.rb contains the Mappable module, which provides basic 201 | distance calculation methods, i.e., calculating the distance 202 | between two points. 203 | 204 | mappable.rb also contains LatLng, GeoLoc, and Bounds. 205 | LatLng is a simple container for latitude and longitude, but 206 | it's made more powerful by mixing in the above-mentioned Mappable 207 | module -- therefore, you can calculate easily the distance between two 208 | LatLng ojbects with `distance = first.distance_to(other)` 209 | 210 | GeoLoc (also in mappable.rb) represents an address or location which 211 | has been geocoded. You can get the city, zipcode, street address, etc. 212 | from a GeoLoc object. GeoLoc extends LatLng, so you also get lat/lng 213 | AND the Mappable modeule goodness for free. 214 | 215 | geocoders.rb contains all the geocoder implemenations. All the gercoders 216 | inherit from a common base (class Geocoder) and implement the private method 217 | do_geocode. 218 | 219 | ## WRITING YOUR OWN GEOCODERS 220 | 221 | If you would like to write your own geocoders, you can do so by requiring 'geokit' or 'geokit/geocoders.rb' in a new file and subclassing the base class (which is class "Geocoder"). 222 | You must then also require such extenal file back in your main geokit configuration. 223 | 224 | require "geokit" 225 | 226 | module Geokit 227 | module Geocoders 228 | 229 | # Should be overriden as Geokit::Geocoders::external_key in your configuration file 230 | @@external_key = 'REPLACE_WITH_YOUR_API_KEY' 231 | __define_accessors 232 | 233 | # Replace name 'External' (below) with the name of your custom geocoder class 234 | # and use :external to specify this geocoder in your list of geocoders. 235 | class ExternalGeocoder < Geocoder 236 | private 237 | def self.do_geocode(address, options = {}) 238 | # Main geocoding method 239 | end 240 | 241 | def self.parse_http_resp(body) # :nodoc: 242 | # Helper method to parse http response. See geokit/geocoders.rb. 243 | end 244 | end 245 | 246 | end 247 | end 248 | 249 | ## GOOGLE GROUP 250 | 251 | Follow the Google Group for updates and discussion on Geokit: http://groups.google.com/group/geokit 252 | 253 | ## LICENSE 254 | 255 | (The MIT License) 256 | 257 | Copyright (c) 2007-2009 Andre Lewis and Bill Eisenhauer 258 | 259 | Permission is hereby granted, free of charge, to any person obtaining 260 | a copy of this software and associated documentation files (the 261 | 'Software'), to deal in the Software without restriction, including 262 | without limitation the rights to use, copy, modify, merge, publish, 263 | distribute, sublicense, and/or sell copies of the Software, and to 264 | permit persons to whom the Software is furnished to do so, subject to 265 | the following conditions: 266 | 267 | The above copyright notice and this permission notice shall be 268 | included in all copies or substantial portions of the Software. 269 | 270 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 271 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 272 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 273 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 274 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 275 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 276 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 277 | -------------------------------------------------------------------------------- /test/test_google_geocoder.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_base_geocoder') 2 | 3 | Geokit::Geocoders::google = 'Google' 4 | 5 | class GoogleGeocoderTest < BaseGeocoderTest #:nodoc: all 6 | 7 | GOOGLE_FULL=<<-EOF.strip 8 | 100 spear st, san francisco, ca200geocode
100 Spear St, San Francisco, CA 94105, USA
USCASan FranciscoSan Francisco100 Spear St94105-122.393985,37.792501,0
9 | EOF 10 | 11 | GOOGLE_RESULT_WITH_SUGGESTED_BOUNDS=<<-EOF.strip 12 | 100 spear st, san francisco, ca200geocode
100 Spear St, San Francisco, CA 94105, USA
USCASan FranciscoSan Francisco100 Spear St94105-122.393985,37.792501,0
13 | EOF 14 | 15 | GOOGLE_CITY=<<-EOF.strip 16 | San Francisco200geocode
San Francisco, CA, USA
USCASan Francisco-122.418333,37.775000,0
17 | EOF 18 | 19 | GOOGLE_MULTI="\n\n via Sandro Pertini 8, Ossona, MI\n \n 200\n geocode\n \n \n
Via Sandro Pertini, 8, 20010 Mesero MI, Italy
\n ITItalyLombardyMilanMesero8 Via Sandro Pertini20010\n 8.8527131,45.4966243,0\n
\n \n
Via Sandro Pertini, 20010 Ossona MI, Italy
\n ITItalyLombardyMilanOssonaVia Sandro Pertini20010\n 8.9023200,45.5074444,0\n
\n
\n" 20 | 21 | GOOGLE_REVERSE_MADRID="40.416741,-3.703250200geocode
Plaza de la Puerta del Sol, 28013, Madrid, Spain
ESSpainMadridMadridMadridPlaza de la Puerta del Sol28013-3.7032537,40.4168023,0
28013, Madrid, Spain
ESSpainMadridMadridMadrid28013-3.7117806,40.4189645,0
Madrid, Spain
ESSpainMadridMadridMadrid-3.7032498,40.4167413,0
Madrid, Spain
ESSpainMadrid-3.5812692,40.4167088,0
Madrid, Spain
ESSpainMadridMadrid-3.5812692,40.4167088,0
Spain
ESSpain-3.7492200,40.4636670,0
" 22 | 23 | GOOGLE_COUNTRY_CODE_BIASED_RESULT = <<-EOF.strip 24 | Syracuse200geocode
Syracuse, Italy
ITItalySicilySyracuse14.9856176,37.0630218,0
25 | EOF 26 | 27 | GOOGLE_BOUNDS_BIASED_RESULT = <<-EOF.strip 28 | Winnetka200geocode
Winnetka, California, USA
USUSACAWinnetka-118.5710220,34.2131710,0
29 | EOF 30 | 31 | GOOGLE_TOO_MANY=<<-EOF.strip 32 | 100 spear st, san francisco, ca620geocode 33 | EOF 34 | 35 | def setup 36 | super 37 | @google_full_hash = {:street_address=>"100 Spear St", :city=>"San Francisco", :state=>"CA", :zip=>"94105", :country_code=>"US"} 38 | @google_city_hash = {:city=>"San Francisco", :state=>"CA"} 39 | 40 | @google_full_loc = Geokit::GeoLoc.new(@google_full_hash) 41 | @google_city_loc = Geokit::GeoLoc.new(@google_city_hash) 42 | end 43 | 44 | def test_google_full_address 45 | response = MockSuccess.new 46 | response.expects(:body).returns(GOOGLE_FULL) 47 | url = "http://maps.google.com/maps/geo?q=#{Geokit::Inflector.url_escape(@address)}&output=xml&key=Google&oe=utf-8" 48 | Geokit::Geocoders::GoogleGeocoder.expects(:call_geocoder_service).with(url).returns(response) 49 | res=Geokit::Geocoders::GoogleGeocoder.geocode(@address) 50 | assert_equal "CA", res.state 51 | assert_equal "San Francisco", res.city 52 | assert_equal "37.792501,-122.393985", res.ll # slightly dif from yahoo 53 | assert res.is_us? 54 | assert_equal "100 Spear St, San Francisco, CA 94105, USA", res.full_address #slightly different from yahoo 55 | assert_equal "google", res.provider 56 | end 57 | 58 | def test_google_full_address_with_geo_loc 59 | response = MockSuccess.new 60 | response.expects(:body).returns(GOOGLE_FULL) 61 | url = "http://maps.google.com/maps/geo?q=#{Geokit::Inflector.url_escape(@full_address_short_zip)}&output=xml&key=Google&oe=utf-8" 62 | Geokit::Geocoders::GoogleGeocoder.expects(:call_geocoder_service).with(url).returns(response) 63 | res=Geokit::Geocoders::GoogleGeocoder.geocode(@google_full_loc) 64 | assert_equal "CA", res.state 65 | assert_equal "San Francisco", res.city 66 | assert_equal "37.792501,-122.393985", res.ll # slightly dif from yahoo 67 | assert res.is_us? 68 | assert_equal "100 Spear St, San Francisco, CA 94105, USA", res.full_address #slightly different from yahoo 69 | assert_equal "google", res.provider 70 | end 71 | 72 | def test_google_full_address_accuracy 73 | response = MockSuccess.new 74 | response.expects(:body).returns(GOOGLE_FULL) 75 | url = "http://maps.google.com/maps/geo?q=#{Geokit::Inflector.url_escape(@full_address_short_zip)}&output=xml&key=Google&oe=utf-8" 76 | Geokit::Geocoders::GoogleGeocoder.expects(:call_geocoder_service).with(url).returns(response) 77 | res=Geokit::Geocoders::GoogleGeocoder.geocode(@google_full_loc) 78 | assert_equal 8, res.accuracy 79 | end 80 | 81 | def test_google_city 82 | response = MockSuccess.new 83 | response.expects(:body).returns(GOOGLE_CITY) 84 | url = "http://maps.google.com/maps/geo?q=#{Geokit::Inflector.url_escape(@address)}&output=xml&key=Google&oe=utf-8" 85 | Geokit::Geocoders::GoogleGeocoder.expects(:call_geocoder_service).with(url).returns(response) 86 | res=Geokit::Geocoders::GoogleGeocoder.geocode(@address) 87 | assert_equal "CA", res.state 88 | assert_equal "San Francisco", res.city 89 | assert_equal "37.775,-122.418333", res.ll 90 | assert res.is_us? 91 | assert_equal "San Francisco, CA, USA", res.full_address 92 | assert_nil res.street_address 93 | assert_equal "google", res.provider 94 | end 95 | 96 | def test_google_city_accuracy 97 | response = MockSuccess.new 98 | response.expects(:body).returns(GOOGLE_CITY) 99 | url = "http://maps.google.com/maps/geo?q=#{Geokit::Inflector.url_escape(@address)}&output=xml&key=Google&oe=utf-8" 100 | Geokit::Geocoders::GoogleGeocoder.expects(:call_geocoder_service).with(url).returns(response) 101 | res=Geokit::Geocoders::GoogleGeocoder.geocode(@address) 102 | assert_equal 4, res.accuracy 103 | end 104 | 105 | def test_google_city_with_geo_loc 106 | response = MockSuccess.new 107 | response.expects(:body).returns(GOOGLE_CITY) 108 | url = "http://maps.google.com/maps/geo?q=#{Geokit::Inflector.url_escape(@address)}&output=xml&key=Google&oe=utf-8" 109 | Geokit::Geocoders::GoogleGeocoder.expects(:call_geocoder_service).with(url).returns(response) 110 | res=Geokit::Geocoders::GoogleGeocoder.geocode(@google_city_loc) 111 | assert_equal "CA", res.state 112 | assert_equal "San Francisco", res.city 113 | assert_equal "37.775,-122.418333", res.ll 114 | assert res.is_us? 115 | assert_equal "San Francisco, CA, USA", res.full_address 116 | assert_nil res.street_address 117 | assert_equal "google", res.provider 118 | end 119 | 120 | def test_google_suggested_bounds 121 | response = MockSuccess.new 122 | response.expects(:body).returns(GOOGLE_RESULT_WITH_SUGGESTED_BOUNDS) 123 | url = "http://maps.google.com/maps/geo?q=#{Geokit::Inflector.url_escape(@full_address_short_zip)}&output=xml&key=Google&oe=utf-8" 124 | Geokit::Geocoders::GoogleGeocoder.expects(:call_geocoder_service).with(url).returns(response) 125 | res = Geokit::Geocoders::GoogleGeocoder.geocode(@google_full_loc) 126 | 127 | assert_instance_of Geokit::Bounds, res.suggested_bounds 128 | assert_equal Geokit::Bounds.new(Geokit::LatLng.new(37.7893376, -122.3971525), Geokit::LatLng.new(37.7956328, -122.3908573)), res.suggested_bounds 129 | end 130 | 131 | def test_service_unavailable 132 | response = MockFailure.new 133 | url = "http://maps.google.com/maps/geo?q=#{Geokit::Inflector.url_escape(@address)}&output=xml&key=Google&oe=utf-8" 134 | Geokit::Geocoders::GoogleGeocoder.expects(:call_geocoder_service).with(url).returns(response) 135 | assert !Geokit::Geocoders::GoogleGeocoder.geocode(@google_city_loc).success 136 | end 137 | 138 | def test_multiple_results 139 | #Geokit::Geocoders::GoogleGeocoder.do_geocode('via Sandro Pertini 8, Ossona, MI') 140 | response = MockSuccess.new 141 | response.expects(:body).returns(GOOGLE_MULTI) 142 | url = "http://maps.google.com/maps/geo?q=#{Geokit::Inflector.url_escape('via Sandro Pertini 8, Ossona, MI')}&output=xml&key=Google&oe=utf-8" 143 | Geokit::Geocoders::GoogleGeocoder.expects(:call_geocoder_service).with(url).returns(response) 144 | res=Geokit::Geocoders::GoogleGeocoder.geocode('via Sandro Pertini 8, Ossona, MI') 145 | assert_equal "Lombardy", res.state 146 | assert_equal "Mesero", res.city 147 | assert_equal "45.4966243,8.8527131", res.ll 148 | assert !res.is_us? 149 | assert_equal "Via Sandro Pertini, 8, 20010 Mesero MI, Italy", res.full_address 150 | assert_equal "8 Via Sandro Pertini", res.street_address 151 | assert_equal "google", res.provider 152 | 153 | assert_equal 2, res.all.size 154 | res = res.all[1] 155 | assert_equal "Lombardy", res.state 156 | assert_equal "Ossona", res.city 157 | assert_equal "45.5074444,8.90232", res.ll 158 | assert !res.is_us? 159 | assert_equal "Via Sandro Pertini, 20010 Ossona MI, Italy", res.full_address 160 | assert_equal "Via Sandro Pertini", res.street_address 161 | assert_equal "google", res.provider 162 | end 163 | 164 | def test_reverse_geocode 165 | #Geokit::Geocoders::GoogleGeocoder.do_reverse_geocode("40.4167413, -3.7032498") 166 | madrid = Geokit::GeoLoc.new 167 | madrid.lat, madrid.lng = "40.4167413", "-3.7032498" 168 | response = MockSuccess.new 169 | response.expects(:body).returns(GOOGLE_REVERSE_MADRID) 170 | url = "http://maps.google.com/maps/geo?ll=#{Geokit::Inflector::url_escape(madrid.ll)}&output=xml&key=#{Geokit::Geocoders::google}&oe=utf-8" 171 | Geokit::Geocoders::GoogleGeocoder.expects(:call_geocoder_service).with(url). 172 | returns(response) 173 | res=Geokit::Geocoders::GoogleGeocoder.do_reverse_geocode(madrid.ll) 174 | 175 | assert_equal madrid.lat.to_s.slice(1..5), res.lat.to_s.slice(1..5) 176 | assert_equal madrid.lng.to_s.slice(1..5), res.lng.to_s.slice(1..5) 177 | assert_equal "ES", res.country_code 178 | assert_equal "google", res.provider 179 | 180 | assert_equal "Madrid", res.city 181 | assert_equal "Madrid", res.state 182 | 183 | assert_equal "Spain", res.country 184 | assert_equal "zip+4", res.precision 185 | assert_equal true, res.success 186 | 187 | assert_equal "Plaza de la Puerta del Sol, 28013, Madrid, Spain", res.full_address 188 | assert_equal "28013", res.zip 189 | assert_equal "Plaza De La Puerta Del Sol", res.street_address 190 | end 191 | 192 | def test_country_code_biasing 193 | response = MockSuccess.new 194 | response.expects(:body).returns(GOOGLE_COUNTRY_CODE_BIASED_RESULT) 195 | 196 | url = "http://maps.google.com/maps/geo?q=Syracuse&output=xml&gl=it&key=Google&oe=utf-8" 197 | Geokit::Geocoders::GoogleGeocoder.expects(:call_geocoder_service).with(url).returns(response) 198 | biased_result = Geokit::Geocoders::GoogleGeocoder.geocode('Syracuse', :bias => 'it') 199 | 200 | assert_equal 'IT', biased_result.country_code 201 | assert_equal 'Sicily', biased_result.state 202 | end 203 | 204 | def test_bounds_biasing 205 | response = MockSuccess.new 206 | response.expects(:body).returns(GOOGLE_BOUNDS_BIASED_RESULT) 207 | 208 | url = "http://maps.google.com/maps/geo?q=Winnetka&output=xml&ll=34.197693208849,-118.547160027785&spn=0.247047999999999,0.294914000000006&key=Google&oe=utf-8" 209 | Geokit::Geocoders::GoogleGeocoder.expects(:call_geocoder_service).with(url).returns(response) 210 | 211 | bounds = Geokit::Bounds.normalize([34.074081, -118.694401], [34.321129, -118.399487]) 212 | biased_result = Geokit::Geocoders::GoogleGeocoder.geocode('Winnetka', :bias => bounds) 213 | 214 | assert_equal 'US', biased_result.country_code 215 | assert_equal 'CA', biased_result.state 216 | end 217 | 218 | def test_too_many_queries 219 | response = MockSuccess.new 220 | response.expects(:body).returns(GOOGLE_TOO_MANY) 221 | url = "http://maps.google.com/maps/geo?q=#{Geokit::Inflector.url_escape(@address)}&output=xml&key=Google&oe=utf-8" 222 | Geokit::Geocoders::GoogleGeocoder.expects(:call_geocoder_service).with(url).returns(response) 223 | assert_raise Geokit::TooManyQueriesError do 224 | res=Geokit::Geocoders::GoogleGeocoder.geocode(@address) 225 | end 226 | end 227 | end 228 | -------------------------------------------------------------------------------- /lib/geokit/mappable.rb: -------------------------------------------------------------------------------- 1 | #require 'forwardable' 2 | 3 | module Geokit 4 | # Contains class and instance methods providing distance calcuation services. This 5 | # module is meant to be mixed into classes containing lat and lng attributes where 6 | # distance calculation is desired. 7 | # 8 | # At present, two forms of distance calculations are provided: 9 | # 10 | # * Pythagorean Theory (flat Earth) - which assumes the world is flat and loses accuracy over long distances. 11 | # * Haversine (sphere) - which is fairly accurate, but at a performance cost. 12 | # 13 | # Distance units supported are :miles, :kms, and :nms. 14 | module Mappable 15 | PI_DIV_RAD = 0.0174 16 | KMS_PER_MILE = 1.609 17 | NMS_PER_MILE = 0.868976242 18 | EARTH_RADIUS_IN_MILES = 3963.19 19 | EARTH_RADIUS_IN_KMS = EARTH_RADIUS_IN_MILES * KMS_PER_MILE 20 | EARTH_RADIUS_IN_NMS = EARTH_RADIUS_IN_MILES * NMS_PER_MILE 21 | MILES_PER_LATITUDE_DEGREE = 69.1 22 | KMS_PER_LATITUDE_DEGREE = MILES_PER_LATITUDE_DEGREE * KMS_PER_MILE 23 | NMS_PER_LATITUDE_DEGREE = MILES_PER_LATITUDE_DEGREE * NMS_PER_MILE 24 | LATITUDE_DEGREES = EARTH_RADIUS_IN_MILES / MILES_PER_LATITUDE_DEGREE 25 | 26 | # Mix below class methods into the includer. 27 | def self.included(receiver) # :nodoc: 28 | receiver.extend ClassMethods 29 | end 30 | 31 | module ClassMethods #:nodoc: 32 | # Returns the distance between two points. The from and to parameters are 33 | # required to have lat and lng attributes. Valid options are: 34 | # :units - valid values are :miles, :kms, :nms (Geokit::default_units is the default) 35 | # :formula - valid values are :flat or :sphere (Geokit::default_formula is the default) 36 | def distance_between(from, to, options={}) 37 | from=Geokit::LatLng.normalize(from) 38 | to=Geokit::LatLng.normalize(to) 39 | return 0.0 if from == to # fixes a "zero-distance" bug 40 | units = options[:units] || Geokit::default_units 41 | formula = options[:formula] || Geokit::default_formula 42 | case formula 43 | when :sphere 44 | begin 45 | units_sphere_multiplier(units) * 46 | Math.acos( Math.sin(deg2rad(from.lat)) * Math.sin(deg2rad(to.lat)) + 47 | Math.cos(deg2rad(from.lat)) * Math.cos(deg2rad(to.lat)) * 48 | Math.cos(deg2rad(to.lng) - deg2rad(from.lng))) 49 | rescue Errno::EDOM 50 | 0.0 51 | end 52 | when :flat 53 | Math.sqrt((units_per_latitude_degree(units)*(from.lat-to.lat))**2 + 54 | (units_per_longitude_degree(from.lat, units)*(from.lng-to.lng))**2) 55 | end 56 | end 57 | 58 | # Returns heading in degrees (0 is north, 90 is east, 180 is south, etc) 59 | # from the first point to the second point. Typicaly, the instance methods will be used 60 | # instead of this method. 61 | def heading_between(from,to) 62 | from=Geokit::LatLng.normalize(from) 63 | to=Geokit::LatLng.normalize(to) 64 | 65 | d_lng=deg2rad(to.lng-from.lng) 66 | from_lat=deg2rad(from.lat) 67 | to_lat=deg2rad(to.lat) 68 | y=Math.sin(d_lng) * Math.cos(to_lat) 69 | x=Math.cos(from_lat)*Math.sin(to_lat)-Math.sin(from_lat)*Math.cos(to_lat)*Math.cos(d_lng) 70 | heading=to_heading(Math.atan2(y,x)) 71 | end 72 | 73 | # Given a start point, distance, and heading (in degrees), provides 74 | # an endpoint. Returns a LatLng instance. Typically, the instance method 75 | # will be used instead of this method. 76 | def endpoint(start,heading, distance, options={}) 77 | units = options[:units] || Geokit::default_units 78 | radius = case units 79 | when :kms; EARTH_RADIUS_IN_KMS 80 | when :nms; EARTH_RADIUS_IN_NMS 81 | else EARTH_RADIUS_IN_MILES 82 | end 83 | start=Geokit::LatLng.normalize(start) 84 | lat=deg2rad(start.lat) 85 | lng=deg2rad(start.lng) 86 | heading=deg2rad(heading) 87 | distance=distance.to_f 88 | 89 | end_lat=Math.asin(Math.sin(lat)*Math.cos(distance/radius) + 90 | Math.cos(lat)*Math.sin(distance/radius)*Math.cos(heading)) 91 | 92 | end_lng=lng+Math.atan2(Math.sin(heading)*Math.sin(distance/radius)*Math.cos(lat), 93 | Math.cos(distance/radius)-Math.sin(lat)*Math.sin(end_lat)) 94 | 95 | LatLng.new(rad2deg(end_lat),rad2deg(end_lng)) 96 | end 97 | 98 | # Returns the midpoint, given two points. Returns a LatLng. 99 | # Typically, the instance method will be used instead of this method. 100 | # Valid option: 101 | # :units - valid values are :miles, :kms, or :nms (:miles is the default) 102 | def midpoint_between(from,to,options={}) 103 | from=Geokit::LatLng.normalize(from) 104 | 105 | units = options[:units] || Geokit::default_units 106 | 107 | heading=from.heading_to(to) 108 | distance=from.distance_to(to,options) 109 | midpoint=from.endpoint(heading,distance/2,options) 110 | end 111 | 112 | # Geocodes a location using the multi geocoder. 113 | def geocode(location, options = {}) 114 | res = Geocoders::MultiGeocoder.geocode(location, options) 115 | return res if res.success? 116 | raise Geokit::Geocoders::GeocodeError 117 | end 118 | 119 | protected 120 | 121 | def deg2rad(degrees) 122 | degrees.to_f / 180.0 * Math::PI 123 | end 124 | 125 | def rad2deg(rad) 126 | rad.to_f * 180.0 / Math::PI 127 | end 128 | 129 | def to_heading(rad) 130 | (rad2deg(rad)+360)%360 131 | end 132 | 133 | # Returns the multiplier used to obtain the correct distance units. 134 | def units_sphere_multiplier(units) 135 | case units 136 | when :kms; EARTH_RADIUS_IN_KMS 137 | when :nms; EARTH_RADIUS_IN_NMS 138 | else EARTH_RADIUS_IN_MILES 139 | end 140 | end 141 | 142 | # Returns the number of units per latitude degree. 143 | def units_per_latitude_degree(units) 144 | case units 145 | when :kms; KMS_PER_LATITUDE_DEGREE 146 | when :nms; NMS_PER_LATITUDE_DEGREE 147 | else MILES_PER_LATITUDE_DEGREE 148 | end 149 | end 150 | 151 | # Returns the number units per longitude degree. 152 | def units_per_longitude_degree(lat, units) 153 | miles_per_longitude_degree = (LATITUDE_DEGREES * Math.cos(lat * PI_DIV_RAD)).abs 154 | case units 155 | when :kms; miles_per_longitude_degree * KMS_PER_MILE 156 | when :nms; miles_per_longitude_degree * NMS_PER_MILE 157 | else miles_per_longitude_degree 158 | end 159 | end 160 | end 161 | 162 | # ----------------------------------------------------------------------------------------------- 163 | # Instance methods below here 164 | # ----------------------------------------------------------------------------------------------- 165 | 166 | # Extracts a LatLng instance. Use with models that are acts_as_mappable 167 | def to_lat_lng 168 | return self if instance_of?(Geokit::LatLng) || instance_of?(Geokit::GeoLoc) 169 | return LatLng.new(send(self.class.lat_column_name),send(self.class.lng_column_name)) if self.class.respond_to?(:acts_as_mappable) 170 | nil 171 | end 172 | 173 | # Returns the distance from another point. The other point parameter is 174 | # required to have lat and lng attributes. Valid options are: 175 | # :units - valid values are :miles, :kms, :or :nms (:miles is the default) 176 | # :formula - valid values are :flat or :sphere (:sphere is the default) 177 | def distance_to(other, options={}) 178 | self.class.distance_between(self, other, options) 179 | end 180 | alias distance_from distance_to 181 | 182 | # Returns heading in degrees (0 is north, 90 is east, 180 is south, etc) 183 | # to the given point. The given point can be a LatLng or a string to be Geocoded 184 | def heading_to(other) 185 | self.class.heading_between(self,other) 186 | end 187 | 188 | # Returns heading in degrees (0 is north, 90 is east, 180 is south, etc) 189 | # FROM the given point. The given point can be a LatLng or a string to be Geocoded 190 | def heading_from(other) 191 | self.class.heading_between(other,self) 192 | end 193 | 194 | # Returns the endpoint, given a heading (in degrees) and distance. 195 | # Valid option: 196 | # :units - valid values are :miles, :kms, or :nms (:miles is the default) 197 | def endpoint(heading,distance,options={}) 198 | self.class.endpoint(self,heading,distance,options) 199 | end 200 | 201 | # Returns the midpoint, given another point on the map. 202 | # Valid option: 203 | # :units - valid values are :miles, :kms, or :nms (:miles is the default) 204 | def midpoint_to(other, options={}) 205 | self.class.midpoint_between(self,other,options) 206 | end 207 | 208 | end 209 | 210 | class LatLng 211 | include Mappable 212 | 213 | attr_accessor :lat, :lng 214 | 215 | # Accepts latitude and longitude or instantiates an empty instance 216 | # if lat and lng are not provided. Converted to floats if provided 217 | def initialize(lat=nil, lng=nil) 218 | lat = lat.to_f if lat && !lat.is_a?(Numeric) 219 | lng = lng.to_f if lng && !lng.is_a?(Numeric) 220 | @lat = lat 221 | @lng = lng 222 | end 223 | 224 | # Latitude attribute setter; stored as a float. 225 | def lat=(lat) 226 | @lat = lat.to_f if lat 227 | end 228 | 229 | # Longitude attribute setter; stored as a float; 230 | def lng=(lng) 231 | @lng=lng.to_f if lng 232 | end 233 | 234 | # Returns the lat and lng attributes as a comma-separated string. 235 | def ll 236 | "#{lat},#{lng}" 237 | end 238 | 239 | #returns a string with comma-separated lat,lng values 240 | def to_s 241 | ll 242 | end 243 | 244 | #returns a two-element array 245 | def to_a 246 | [lat,lng] 247 | end 248 | # Returns true if the candidate object is logically equal. Logical equivalence 249 | # is true if the lat and lng attributes are the same for both objects. 250 | def ==(other) 251 | other.is_a?(LatLng) ? self.lat == other.lat && self.lng == other.lng : false 252 | end 253 | 254 | def hash 255 | lat.hash + lng.hash 256 | end 257 | 258 | def eql?(other) 259 | self == other 260 | end 261 | 262 | # A *class* method to take anything which can be inferred as a point and generate 263 | # a LatLng from it. You should use this anything you're not sure what the input is, 264 | # and want to deal with it as a LatLng if at all possible. Can take: 265 | # 1) two arguments (lat,lng) 266 | # 2) a string in the format "37.1234,-129.1234" or "37.1234 -129.1234" 267 | # 3) a string which can be geocoded on the fly 268 | # 4) an array in the format [37.1234,-129.1234] 269 | # 5) a LatLng or GeoLoc (which is just passed through as-is) 270 | # 6) anything which acts_as_mappable -- a LatLng will be extracted from it 271 | def self.normalize(thing,other=nil) 272 | # if an 'other' thing is supplied, normalize the input by creating an array of two elements 273 | thing=[thing,other] if other 274 | 275 | if thing.is_a?(String) 276 | thing.strip! 277 | if match=thing.match(/(\-?\d+\.?\d*)[, ] ?(\-?\d+\.?\d*)$/) 278 | return Geokit::LatLng.new(match[1],match[2]) 279 | else 280 | res = Geokit::Geocoders::MultiGeocoder.geocode(thing) 281 | return res if res.success? 282 | raise Geokit::Geocoders::GeocodeError 283 | end 284 | elsif thing.is_a?(Array) && thing.size==2 285 | return Geokit::LatLng.new(thing[0],thing[1]) 286 | elsif thing.is_a?(LatLng) # will also be true for GeoLocs 287 | return thing 288 | elsif thing.class.respond_to?(:acts_as_mappable) && thing.class.respond_to?(:distance_column_name) 289 | return thing.to_lat_lng 290 | end 291 | 292 | raise ArgumentError.new("#{thing} (#{thing.class}) cannot be normalized to a LatLng. We tried interpreting it as an array, string, Mappable, etc., but no dice.") 293 | end 294 | 295 | # Reverse geocodes a LatLng object using the MultiGeocoder (default), or optionally 296 | # using a geocoder of your choosing. Returns a new Geokit::GeoLoc object 297 | # 298 | # ==== Options 299 | # * :using - Specifies the geocoder to use for reverse geocoding. Defaults to 300 | # MultiGeocoder. Can be either the geocoder class (or any class that 301 | # implements do_reverse_geocode for that matter), or the name of 302 | # the class without the "Geocoder" part (e.g. :google) 303 | # 304 | # ==== Examples 305 | # LatLng.new(51.4578329, 7.0166848).reverse_geocode # => # 306 | # LatLng.new(51.4578329, 7.0166848).reverse_geocode(:using => :google) # => # 307 | # LatLng.new(51.4578329, 7.0166848).reverse_geocode(:using => Geokit::Geocoders::GoogleGeocoder) # => # 308 | def reverse_geocode(options = { :using => Geokit::Geocoders::MultiGeocoder }) 309 | if options[:using].is_a?(String) or options[:using].is_a?(Symbol) 310 | provider = Geokit::Geocoders.const_get("#{Geokit::Inflector::camelize(options[:using].to_s)}Geocoder") 311 | elsif options[:using].respond_to?(:do_reverse_geocode) 312 | provider = options[:using] 313 | else 314 | raise ArgumentError.new("#{options[:using]} is not a valid geocoder.") 315 | end 316 | 317 | provider.send(:reverse_geocode, self) 318 | end 319 | end 320 | 321 | # This class encapsulates the result of a geocoding call. 322 | # It's primary purpose is to homogenize the results of multiple 323 | # geocoding providers. It also provides some additional functionality, such as 324 | # the "full address" method for geocoders that do not provide a 325 | # full address in their results (for example, Yahoo), and the "is_us" method. 326 | # 327 | # Some geocoders can return multple results. Geoloc can capture multiple results through 328 | # its "all" method. 329 | # 330 | # For the geocoder setting the results, it would look something like this: 331 | # geo=GeoLoc.new(first_result) 332 | # geo.all.push(second_result) 333 | # geo.all.push(third_result) 334 | # 335 | # Then, for the user of the result: 336 | # 337 | # puts geo.full_address # just like usual 338 | # puts geo.all.size => 3 # there's three results total 339 | # puts geo.all.first # all is just an array or additional geolocs, 340 | # so do what you want with it 341 | class GeoLoc < LatLng 342 | 343 | # Location attributes. Full address is a concatenation of all values. For example: 344 | # 100 Spear St, San Francisco, CA, 94101, US 345 | # Street number and street name are extracted from the street address attribute if they don't exist 346 | attr_accessor :street_number,:street_name,:street_address, :city, :state, :zip, :country_code, :country, :full_address, :all, :district, :province 347 | # Attributes set upon return from geocoding. Success will be true for successful 348 | # geocode lookups. The provider will be set to the name of the providing geocoder. 349 | # Finally, precision is an indicator of the accuracy of the geocoding. 350 | attr_accessor :success, :provider, :precision, :suggested_bounds 351 | # accuracy is set for Yahoo and Google geocoders, it is a numeric value of the 352 | # precision. see http://code.google.com/apis/maps/documentation/geocoding/#GeocodingAccuracy 353 | attr_accessor :accuracy 354 | 355 | # Constructor expects a hash of symbols to correspond with attributes. 356 | def initialize(h={}) 357 | @all = [self] 358 | 359 | @street_address=h[:street_address] 360 | @street_number=nil 361 | @street_name=nil 362 | @city=h[:city] 363 | @state=h[:state] 364 | @zip=h[:zip] 365 | @country_code=h[:country_code] 366 | @province = h[:province] 367 | @success=false 368 | @precision='unknown' 369 | @full_address=nil 370 | super(h[:lat],h[:lng]) 371 | end 372 | 373 | # Returns true if geocoded to the United States. 374 | def is_us? 375 | country_code == 'US' 376 | end 377 | 378 | def success? 379 | success == true 380 | end 381 | 382 | # full_address is provided by google but not by yahoo. It is intended that the google 383 | # geocoding method will provide the full address, whereas for yahoo it will be derived 384 | # from the parts of the address we do have. 385 | def full_address 386 | @full_address ? @full_address : to_geocodeable_s 387 | end 388 | 389 | # Extracts the street number from the street address where possible. 390 | def street_number 391 | @street_number ||= street_address[/(\d*)/] if street_address 392 | @street_number 393 | end 394 | 395 | # Returns the street name portion of the street address where possible 396 | def street_name 397 | @street_name||=street_address[street_number.length, street_address.length].strip if street_address 398 | @street_name 399 | end 400 | 401 | # gives you all the important fields as key-value pairs 402 | def hash 403 | res={} 404 | [:success,:lat,:lng,:country_code,:city,:state,:zip,:street_address,:province,:district,:provider,:full_address,:is_us?,:ll,:precision].each { |s| res[s] = self.send(s.to_s) } 405 | res 406 | end 407 | alias to_hash hash 408 | 409 | # Sets the city after capitalizing each word within the city name. 410 | def city=(city) 411 | @city = Geokit::Inflector::titleize(city) if city 412 | end 413 | 414 | # Sets the street address after capitalizing each word within the street address. 415 | def street_address=(address) 416 | if address and not ['google','google3'].include?(self.provider) 417 | @street_address = Geokit::Inflector::titleize(address) 418 | else 419 | @street_address = address 420 | end 421 | end 422 | 423 | # Returns a comma-delimited string consisting of the street address, city, state, 424 | # zip, and country code. Only includes those attributes that are non-blank. 425 | def to_geocodeable_s 426 | a=[street_address, district, city, province, state, zip, country_code].compact 427 | a.delete_if { |e| !e || e == '' } 428 | a.join(', ') 429 | end 430 | 431 | def to_yaml_properties 432 | (instance_variables - ['@all']).sort 433 | end 434 | 435 | # Returns a string representation of the instance. 436 | def to_s 437 | "Provider: #{provider}\nStreet: #{street_address}\nCity: #{city}\nState: #{state}\nZip: #{zip}\nLatitude: #{lat}\nLongitude: #{lng}\nCountry: #{country_code}\nSuccess: #{success}" 438 | end 439 | end 440 | 441 | # Bounds represents a rectangular bounds, defined by the SW and NE corners 442 | class Bounds 443 | # sw and ne are LatLng objects 444 | attr_accessor :sw, :ne 445 | 446 | # provide sw and ne to instantiate a new Bounds instance 447 | def initialize(sw,ne) 448 | raise ArgumentError if !(sw.is_a?(Geokit::LatLng) && ne.is_a?(Geokit::LatLng)) 449 | @sw,@ne=sw,ne 450 | end 451 | 452 | #returns the a single point which is the center of the rectangular bounds 453 | def center 454 | @sw.midpoint_to(@ne) 455 | end 456 | 457 | # a simple string representation:sw,ne 458 | def to_s 459 | "#{@sw.to_s},#{@ne.to_s}" 460 | end 461 | 462 | # a two-element array of two-element arrays: sw,ne 463 | def to_a 464 | [@sw.to_a, @ne.to_a] 465 | end 466 | 467 | # Returns true if the bounds contain the passed point. 468 | # allows for bounds which cross the meridian 469 | def contains?(point) 470 | point=Geokit::LatLng.normalize(point) 471 | res = point.lat > @sw.lat && point.lat < @ne.lat 472 | if crosses_meridian? 473 | res &= point.lng < @ne.lng || point.lng > @sw.lng 474 | else 475 | res &= point.lng < @ne.lng && point.lng > @sw.lng 476 | end 477 | res 478 | end 479 | 480 | # returns true if the bounds crosses the international dateline 481 | def crosses_meridian? 482 | @sw.lng > @ne.lng 483 | end 484 | 485 | # Returns true if the candidate object is logically equal. Logical equivalence 486 | # is true if the lat and lng attributes are the same for both objects. 487 | def ==(other) 488 | other.is_a?(Bounds) ? self.sw == other.sw && self.ne == other.ne : false 489 | end 490 | 491 | # Equivalent to Google Maps API's .toSpan() method on GLatLng's. 492 | # 493 | # Returns a LatLng object, whose coordinates represent the size of a rectangle 494 | # defined by these bounds. 495 | def to_span 496 | lat_span = (@ne.lat - @sw.lat).abs 497 | lng_span = (crosses_meridian? ? 360 + @ne.lng - @sw.lng : @ne.lng - @sw.lng).abs 498 | Geokit::LatLng.new(lat_span, lng_span) 499 | end 500 | 501 | class < 227 | # 228 | # 49.243086 229 | # -123.153684 230 | # 231 | class CaGeocoder < Geocoder 232 | 233 | private 234 | 235 | # Template method which does the geocode lookup. 236 | def self.do_geocode(address, options = {}) 237 | raise ArgumentError('Geocoder.ca requires a GeoLoc argument') unless address.is_a?(GeoLoc) 238 | url = construct_request(address) 239 | res = self.call_geocoder_service(url) 240 | return GeoLoc.new if !res.is_a?(Net::HTTPSuccess) 241 | xml = res.body 242 | logger.debug "Geocoder.ca geocoding. Address: #{address}. Result: #{xml}" 243 | # Parse the document. 244 | doc = REXML::Document.new(xml) 245 | address.lat = doc.elements['//latt'].text 246 | address.lng = doc.elements['//longt'].text 247 | address.success = true 248 | return address 249 | rescue 250 | logger.error "Caught an error during Geocoder.ca geocoding call: "+$! 251 | return GeoLoc.new 252 | end 253 | 254 | # Formats the request in the format acceptable by the CA geocoder. 255 | def self.construct_request(location) 256 | url = "" 257 | url += add_ampersand(url) + "stno=#{location.street_number}" if location.street_address 258 | url += add_ampersand(url) + "addresst=#{Geokit::Inflector::url_escape(location.street_name)}" if location.street_address 259 | url += add_ampersand(url) + "city=#{Geokit::Inflector::url_escape(location.city)}" if location.city 260 | url += add_ampersand(url) + "prov=#{location.state}" if location.state 261 | url += add_ampersand(url) + "postal=#{location.zip}" if location.zip 262 | url += add_ampersand(url) + "auth=#{Geokit::Geocoders::geocoder_ca}" if Geokit::Geocoders::geocoder_ca 263 | url += add_ampersand(url) + "geoit=xml" 264 | 'http://geocoder.ca/?' + url 265 | end 266 | 267 | def self.add_ampersand(url) 268 | url && url.length > 0 ? "&" : "" 269 | end 270 | end 271 | 272 | # Geocoder Us geocoder implementation. Requires the Geokit::Geocoders::GEOCODER_US variable to 273 | # contain true or false based upon whether authentication is to occur. Conforms to the 274 | # interface set by the Geocoder class. 275 | class UsGeocoder < Geocoder 276 | 277 | private 278 | def self.do_geocode(address, options = {}) 279 | address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address 280 | 281 | query = (address_str =~ /^\d{5}(?:-\d{4})?$/ ? "zip" : "address") + "=#{Geokit::Inflector::url_escape(address_str)}" 282 | url = if GeoKit::Geocoders::geocoder_us 283 | "http://#{GeoKit::Geocoders::geocoder_us}@geocoder.us/member/service/csv/geocode" 284 | else 285 | "http://geocoder.us/service/csv/geocode" 286 | end 287 | 288 | url = "#{url}?#{query}" 289 | res = self.call_geocoder_service(url) 290 | 291 | return GeoLoc.new if !res.is_a?(Net::HTTPSuccess) 292 | data = res.body 293 | logger.debug "Geocoder.us geocoding. Address: #{address}. Result: #{data}" 294 | array = data.chomp.split(',') 295 | 296 | if array.length == 5 297 | res=GeoLoc.new 298 | res.lat,res.lng,res.city,res.state,res.zip=array 299 | res.country_code='US' 300 | res.success=true 301 | return res 302 | elsif array.length == 6 303 | res=GeoLoc.new 304 | res.lat,res.lng,res.street_address,res.city,res.state,res.zip=array 305 | res.country_code='US' 306 | res.success=true 307 | return res 308 | else 309 | logger.info "geocoder.us was unable to geocode address: "+address 310 | return GeoLoc.new 311 | end 312 | rescue 313 | logger.error "Caught an error during geocoder.us geocoding call: "+$! 314 | return GeoLoc.new 315 | 316 | end 317 | end 318 | 319 | # Yahoo geocoder implementation. Requires the Geokit::Geocoders::YAHOO variable to 320 | # contain a Yahoo API key. Conforms to the interface set by the Geocoder class. 321 | class YahooGeocoder < Geocoder 322 | 323 | private 324 | 325 | # Template method which does the geocode lookup. 326 | def self.do_geocode(address, options = {}) 327 | address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address 328 | url="http://api.local.yahoo.com/MapsService/V1/geocode?appid=#{Geokit::Geocoders::yahoo}&location=#{Geokit::Inflector::url_escape(address_str)}" 329 | res = self.call_geocoder_service(url) 330 | return GeoLoc.new if !res.is_a?(Net::HTTPSuccess) 331 | xml = res.body 332 | doc = REXML::Document.new(xml) 333 | logger.debug "Yahoo geocoding. Address: #{address}. Result: #{xml}" 334 | 335 | if doc.elements['//ResultSet'] 336 | res=GeoLoc.new 337 | 338 | #basic 339 | res.lat=doc.elements['//Latitude'].text 340 | res.lng=doc.elements['//Longitude'].text 341 | res.country_code=doc.elements['//Country'].text 342 | res.provider='yahoo' 343 | 344 | #extended - false if not available 345 | res.city=doc.elements['//City'].text if doc.elements['//City'] && doc.elements['//City'].text != nil 346 | res.state=doc.elements['//State'].text if doc.elements['//State'] && doc.elements['//State'].text != nil 347 | res.zip=doc.elements['//Zip'].text if doc.elements['//Zip'] && doc.elements['//Zip'].text != nil 348 | res.street_address=doc.elements['//Address'].text if doc.elements['//Address'] && doc.elements['//Address'].text != nil 349 | res.precision=doc.elements['//Result'].attributes['precision'] if doc.elements['//Result'] 350 | # set the accuracy as google does (added by Andruby) 351 | res.accuracy=%w{unknown country state state city zip zip+4 street address building}.index(res.precision) 352 | res.success=true 353 | return res 354 | else 355 | logger.info "Yahoo was unable to geocode address: "+address 356 | return GeoLoc.new 357 | end 358 | 359 | rescue 360 | logger.info "Caught an error during Yahoo geocoding call: "+$! 361 | return GeoLoc.new 362 | end 363 | end 364 | 365 | # Another geocoding web service 366 | # http://www.geonames.org 367 | class GeonamesGeocoder < Geocoder 368 | 369 | private 370 | 371 | # Template method which does the geocode lookup. 372 | def self.do_geocode(address, options = {}) 373 | address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address 374 | # geonames need a space seperated search string 375 | address_str.gsub!(/,/, " ") 376 | params = "/postalCodeSearch?placename=#{Geokit::Inflector::url_escape(address_str)}&maxRows=10" 377 | 378 | if(GeoKit::Geocoders::geonames) 379 | url = "http://ws.geonames.net#{params}&username=#{GeoKit::Geocoders::geonames}" 380 | else 381 | url = "http://ws.geonames.org#{params}" 382 | end 383 | 384 | res = self.call_geocoder_service(url) 385 | 386 | return GeoLoc.new if !res.is_a?(Net::HTTPSuccess) 387 | 388 | xml=res.body 389 | logger.debug "Geonames geocoding. Address: #{address}. Result: #{xml}" 390 | doc=REXML::Document.new(xml) 391 | 392 | if(doc.elements['//geonames/totalResultsCount'].text.to_i > 0) 393 | res=GeoLoc.new 394 | 395 | # only take the first result 396 | res.lat=doc.elements['//code/lat'].text if doc.elements['//code/lat'] 397 | res.lng=doc.elements['//code/lng'].text if doc.elements['//code/lng'] 398 | res.country_code=doc.elements['//code/countryCode'].text if doc.elements['//code/countryCode'] 399 | res.provider='genomes' 400 | res.city=doc.elements['//code/name'].text if doc.elements['//code/name'] 401 | res.state=doc.elements['//code/adminName1'].text if doc.elements['//code/adminName1'] 402 | res.zip=doc.elements['//code/postalcode'].text if doc.elements['//code/postalcode'] 403 | res.success=true 404 | return res 405 | else 406 | logger.info "Geonames was unable to geocode address: "+address 407 | return GeoLoc.new 408 | end 409 | 410 | rescue 411 | logger.error "Caught an error during Geonames geocoding call: "+$! 412 | end 413 | end 414 | 415 | # ------------------------------------------------------------------------------------------- 416 | # Address geocoders that also provide reverse geocoding 417 | # ------------------------------------------------------------------------------------------- 418 | 419 | # Google geocoder implementation. Requires the Geokit::Geocoders::GOOGLE variable to 420 | # contain a Google API key. Conforms to the interface set by the Geocoder class. 421 | class GoogleGeocoder < Geocoder 422 | 423 | private 424 | 425 | # Template method which does the reverse-geocode lookup. 426 | def self.do_reverse_geocode(latlng) 427 | latlng=LatLng.normalize(latlng) 428 | 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") 429 | # 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")) 430 | return GeoLoc.new unless (res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPOK)) 431 | xml = res.body 432 | logger.debug "Google reverse-geocoding. LL: #{latlng}. Result: #{xml}" 433 | return self.xml2GeoLoc(xml) 434 | end 435 | 436 | # Template method which does the geocode lookup. 437 | # 438 | # Supports viewport/country code biasing 439 | # 440 | # ==== OPTIONS 441 | # * :bias - This option makes the Google Geocoder return results biased to a particular 442 | # country or viewport. Country code biasing is achieved by passing the ccTLD 443 | # ('uk' for .co.uk, for example) as a :bias value. For a list of ccTLD's, 444 | # look here: http://en.wikipedia.org/wiki/CcTLD. By default, the geocoder 445 | # will be biased to results within the US (ccTLD .com). 446 | # 447 | # If you'd like the Google Geocoder to prefer results within a given viewport, 448 | # you can pass a Geokit::Bounds object as the :bias value. 449 | # 450 | # ==== EXAMPLES 451 | # # By default, the geocoder will return Syracuse, NY 452 | # Geokit::Geocoders::GoogleGeocoder.geocode('Syracuse').country_code # => 'US' 453 | # # With country code biasing, it returns Syracuse in Sicily, Italy 454 | # Geokit::Geocoders::GoogleGeocoder.geocode('Syracuse', :bias => :it).country_code # => 'IT' 455 | # 456 | # # By default, the geocoder will return Winnetka, IL 457 | # Geokit::Geocoders::GoogleGeocoder.geocode('Winnetka').state # => 'IL' 458 | # # When biased to an bounding box around California, it will now return the Winnetka neighbourhood, CA 459 | # bounds = Geokit::Bounds.normalize([34.074081, -118.694401], [34.321129, -118.399487]) 460 | # Geokit::Geocoders::GoogleGeocoder.geocode('Winnetka', :bias => bounds).state # => 'CA' 461 | def self.do_geocode(address, options = {}) 462 | res = self.call_geocoder_service(self.geocode_url(address,options)) 463 | return GeoLoc.new if !res.is_a?(Net::HTTPSuccess) 464 | xml = res.body 465 | logger.debug "Google geocoding. Address: #{address}. Result: #{xml}" 466 | return self.xml2GeoLoc(xml, address) 467 | end 468 | 469 | # Determine the Google API url based on the google api key, or based on the client / private key for premier users 470 | def self.geocode_url(address,options = {}) 471 | bias_str = options[:bias] ? construct_bias_string_from_options(options[:bias]) : '' 472 | address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address 473 | 474 | if !Geokit::Geocoders::google_client_id.nil? && !Geokit::Geocoders::google_premier_secret_key.nil? 475 | url = "http://maps.googleapis.com/maps/api/geocode/xml?address=#{Geokit::Inflector::url_escape(address_str)}&client=#{Geokit::Geocoders::google_client_id}&sensor=false&oe=utf-8" 476 | Geokit::Geocoders::Geocoder.sign_url(url,Geokit::Geocoders::google_premier_secret_key) 477 | else 478 | "http://maps.google.com/maps/geo?q=#{Geokit::Inflector::url_escape(address_str)}&output=xml#{bias_str}&key=#{Geokit::Geocoders::google}&oe=utf-8" 479 | end 480 | end 481 | 482 | 483 | def self.construct_bias_string_from_options(bias) 484 | if bias.is_a?(String) or bias.is_a?(Symbol) 485 | # country code biasing 486 | "&gl=#{bias.to_s.downcase}" 487 | elsif bias.is_a?(Bounds) 488 | # viewport biasing 489 | "&ll=#{bias.center.ll}&spn=#{bias.to_span.ll}" 490 | end 491 | end 492 | 493 | def self.xml2GeoLoc(xml, address="") 494 | doc=REXML::Document.new(xml) 495 | 496 | if doc.elements['//kml/Response/Status/code'].text == '200' 497 | geoloc = nil 498 | # Google can return multiple results as //Placemark elements. 499 | # iterate through each and extract each placemark as a geoloc 500 | doc.each_element('//Placemark') do |e| 501 | extracted_geoloc = extract_placemark(e) # g is now an instance of GeoLoc 502 | if geoloc.nil? 503 | # first time through, geoloc is still nil, so we make it the geoloc we just extracted 504 | geoloc = extracted_geoloc 505 | else 506 | # second (and subsequent) iterations, we push additional 507 | # geolocs onto "geoloc.all" 508 | geoloc.all.push(extracted_geoloc) 509 | end 510 | end 511 | return geoloc 512 | elsif doc.elements['//kml/Response/Status/code'].text == '620' 513 | raise Geokit::TooManyQueriesError 514 | else 515 | logger.info "Google was unable to geocode address: "+address 516 | return GeoLoc.new 517 | end 518 | 519 | rescue Geokit::TooManyQueriesError 520 | # re-raise because of other rescue 521 | 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." 522 | rescue 523 | logger.error "Caught an error during Google geocoding call: "+$! 524 | return GeoLoc.new 525 | end 526 | 527 | # extracts a single geoloc from a //placemark element in the google results xml 528 | def self.extract_placemark(doc) 529 | res = GeoLoc.new 530 | coordinates=doc.elements['.//coordinates'].text.to_s.split(',') 531 | 532 | #basics 533 | res.lat=coordinates[1] 534 | res.lng=coordinates[0] 535 | res.country_code=doc.elements['.//CountryNameCode'].text if doc.elements['.//CountryNameCode'] 536 | res.provider='google' 537 | 538 | #extended -- false if not not available 539 | res.city = doc.elements['.//LocalityName'].text if doc.elements['.//LocalityName'] 540 | res.state = doc.elements['.//AdministrativeAreaName'].text if doc.elements['.//AdministrativeAreaName'] 541 | res.province = doc.elements['.//SubAdministrativeAreaName'].text if doc.elements['.//SubAdministrativeAreaName'] 542 | res.full_address = doc.elements['.//address'].text if doc.elements['.//address'] # google provides it 543 | res.zip = doc.elements['.//PostalCodeNumber'].text if doc.elements['.//PostalCodeNumber'] 544 | res.street_address = doc.elements['.//ThoroughfareName'].text if doc.elements['.//ThoroughfareName'] 545 | res.country = doc.elements['.//CountryName'].text if doc.elements['.//CountryName'] 546 | res.district = doc.elements['.//DependentLocalityName'].text if doc.elements['.//DependentLocalityName'] 547 | # Translate accuracy into Yahoo-style token address, street, zip, zip+4, city, state, country 548 | # For Google, 1=low accuracy, 8=high accuracy 549 | address_details=doc.elements['.//*[local-name() = "AddressDetails"]'] 550 | res.accuracy = address_details ? address_details.attributes['Accuracy'].to_i : 0 551 | res.precision=%w{unknown country state state city zip zip+4 street address building}[res.accuracy] 552 | 553 | # google returns a set of suggested boundaries for the geocoded result 554 | if suggested_bounds = doc.elements['//LatLonBox'] 555 | res.suggested_bounds = Bounds.normalize( 556 | [suggested_bounds.attributes['south'], suggested_bounds.attributes['west']], 557 | [suggested_bounds.attributes['north'], suggested_bounds.attributes['east']]) 558 | end 559 | 560 | res.success=true 561 | 562 | return res 563 | end 564 | end 565 | 566 | 567 | # ------------------------------------------------------------------------------------------- 568 | # IP Geocoders 569 | # ------------------------------------------------------------------------------------------- 570 | 571 | # Provides geocoding based upon an IP address. The underlying web service is geoplugin.net 572 | class GeoPluginGeocoder < Geocoder 573 | private 574 | 575 | def self.do_geocode(ip, options = {}) 576 | return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip) 577 | response = self.call_geocoder_service("http://www.geoplugin.net/xml.gp?ip=#{ip}") 578 | return response.is_a?(Net::HTTPSuccess) ? parse_xml(response.body) : GeoLoc.new 579 | rescue 580 | logger.error "Caught an error during GeoPluginGeocoder geocoding call: "+$! 581 | return GeoLoc.new 582 | end 583 | 584 | def self.parse_xml(xml) 585 | xml = REXML::Document.new(xml) 586 | geo = GeoLoc.new 587 | geo.provider='geoPlugin' 588 | geo.city = xml.elements['//geoplugin_city'].text 589 | geo.state = xml.elements['//geoplugin_region'].text 590 | geo.country_code = xml.elements['//geoplugin_countryCode'].text 591 | geo.lat = xml.elements['//geoplugin_latitude'].text.to_f 592 | geo.lng = xml.elements['//geoplugin_longitude'].text.to_f 593 | geo.success = !!geo.city && !geo.city.empty? 594 | return geo 595 | end 596 | end 597 | 598 | # Provides geocoding based upon an IP address. The underlying web service is a hostip.info 599 | # which sources their data through a combination of publicly available information as well 600 | # as community contributions. 601 | class IpGeocoder < Geocoder 602 | 603 | # A number of non-routable IP ranges. 604 | # 605 | # -- 606 | # Sources for these: 607 | # RFC 3330: Special-Use IPv4 Addresses 608 | # The bogon list: http://www.cymru.com/Documents/bogon-list.html 609 | 610 | NON_ROUTABLE_IP_RANGES = [ 611 | IPAddr.new('0.0.0.0/8'), # "This" Network 612 | IPAddr.new('10.0.0.0/8'), # Private-Use Networks 613 | IPAddr.new('14.0.0.0/8'), # Public-Data Networks 614 | IPAddr.new('127.0.0.0/8'), # Loopback 615 | IPAddr.new('169.254.0.0/16'), # Link local 616 | IPAddr.new('172.16.0.0/12'), # Private-Use Networks 617 | IPAddr.new('192.0.2.0/24'), # Test-Net 618 | IPAddr.new('192.168.0.0/16'), # Private-Use Networks 619 | IPAddr.new('198.18.0.0/15'), # Network Interconnect Device Benchmark Testing 620 | IPAddr.new('224.0.0.0/4'), # Multicast 621 | IPAddr.new('240.0.0.0/4') # Reserved for future use 622 | ].freeze 623 | 624 | private 625 | 626 | # Given an IP address, returns a GeoLoc instance which contains latitude, 627 | # longitude, city, and country code. Sets the success attribute to false if the ip 628 | # parameter does not match an ip address. 629 | def self.do_geocode(ip, options = {}) 630 | return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip) 631 | return GeoLoc.new if self.private_ip_address?(ip) 632 | url = "http://api.hostip.info/get_html.php?ip=#{ip}&position=true" 633 | response = self.call_geocoder_service(url) 634 | response.is_a?(Net::HTTPSuccess) ? parse_body(response.body) : GeoLoc.new 635 | rescue 636 | logger.error "Caught an error during HostIp geocoding call: "+$! 637 | return GeoLoc.new 638 | end 639 | 640 | # Converts the body to YAML since its in the form of: 641 | # 642 | # Country: UNITED STATES (US) 643 | # City: Sugar Grove, IL 644 | # Latitude: 41.7696 645 | # Longitude: -88.4588 646 | # 647 | # then instantiates a GeoLoc instance to populate with location data. 648 | def self.parse_body(body) # :nodoc: 649 | yaml = YAML.load(body) 650 | res = GeoLoc.new 651 | res.provider = 'hostip' 652 | res.city, res.state = yaml['City'].split(', ') 653 | country, res.country_code = yaml['Country'].split(' (') 654 | res.lat = yaml['Latitude'] 655 | res.lng = yaml['Longitude'] 656 | res.country_code.chop! 657 | res.success = !(res.city =~ /\(.+\)/) 658 | res 659 | end 660 | 661 | # Checks whether the IP address belongs to a private address range. 662 | # 663 | # This function is used to reduce the number of useless queries made to 664 | # the geocoding service. Such queries can occur frequently during 665 | # integration tests. 666 | def self.private_ip_address?(ip) 667 | return NON_ROUTABLE_IP_RANGES.any? { |range| range.include?(ip) } 668 | end 669 | end 670 | 671 | # ------------------------------------------------------------------------------------------- 672 | # The Multi Geocoder 673 | # ------------------------------------------------------------------------------------------- 674 | 675 | # Provides methods to geocode with a variety of geocoding service providers, plus failover 676 | # among providers in the order you configure. When 2nd parameter is set 'true', perform 677 | # ip location lookup with 'address' as the ip address. 678 | # 679 | # Goal: 680 | # - homogenize the results of multiple geocoders 681 | # 682 | # Limitations: 683 | # - currently only provides the first result. Sometimes geocoders will return multiple results. 684 | # - currently discards the "accuracy" component of the geocoding calls 685 | class MultiGeocoder < Geocoder 686 | 687 | private 688 | # This method will call one or more geocoders in the order specified in the 689 | # configuration until one of the geocoders work. 690 | # 691 | # The failover approach is crucial for production-grade apps, but is rarely used. 692 | # 98% of your geocoding calls will be successful with the first call 693 | def self.do_geocode(address, options = {}) 694 | geocode_ip = /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.match(address) 695 | provider_order = geocode_ip ? Geokit::Geocoders::ip_provider_order : Geokit::Geocoders::provider_order 696 | 697 | provider_order.each do |provider| 698 | begin 699 | klass = Geokit::Geocoders.const_get "#{Geokit::Inflector::camelize(provider.to_s)}Geocoder" 700 | res = klass.send :geocode, address, options 701 | return res if res.success? 702 | rescue 703 | 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}") 704 | end 705 | end 706 | # If we get here, we failed completely. 707 | GeoLoc.new 708 | end 709 | 710 | # This method will call one or more geocoders in the order specified in the 711 | # configuration until one of the geocoders work, only this time it's going 712 | # to try to reverse geocode a geographical point. 713 | def self.do_reverse_geocode(latlng) 714 | Geokit::Geocoders::provider_order.each do |provider| 715 | begin 716 | klass = Geokit::Geocoders.const_get "#{Geokit::Inflector::camelize(provider.to_s)}Geocoder" 717 | res = klass.send :reverse_geocode, latlng 718 | return res if res.success? 719 | rescue 720 | logger.error("Something has gone very wrong during reverse geocoding, OR you have configured an invalid class name in Geokit::Geocoders::provider_order. LatLng: #{latlng}. Provider: #{provider}") 721 | end 722 | end 723 | # If we get here, we failed completely. 724 | GeoLoc.new 725 | end 726 | end 727 | end 728 | end 729 | -------------------------------------------------------------------------------- /lib/geokit/geocoders.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'ipaddr' 3 | require 'rexml/document' 4 | require 'yaml' 5 | require 'timeout' 6 | require 'logger' 7 | require 'base64' 8 | 9 | # do this just in case 10 | begin 11 | ActiveSupport.nil? 12 | rescue NameError 13 | require 'json/pure' 14 | end 15 | 16 | module Geokit 17 | 18 | class TooManyQueriesError < StandardError; end 19 | 20 | module Inflector 21 | 22 | extend self 23 | 24 | def titleize(word) 25 | humanize(underscore(word)).gsub(/\b([a-z])/u) { $1.capitalize } 26 | end 27 | 28 | def underscore(camel_cased_word) 29 | camel_cased_word.to_s.gsub(/::/, '/'). 30 | gsub(/([A-Z]+)([A-Z][a-z])/u,'\1_\2'). 31 | gsub(/([a-z\d])([A-Z])/u,'\1_\2'). 32 | tr("-", "_"). 33 | downcase 34 | end 35 | 36 | def humanize(lower_case_and_underscored_word) 37 | lower_case_and_underscored_word.to_s.gsub(/_id$/, "").gsub(/_/, " ").capitalize 38 | end 39 | 40 | def snake_case(s) 41 | return s.downcase if s =~ /^[A-Z]+$/u 42 | s.gsub(/([A-Z]+)(?=[A-Z][a-z]?)|\B[A-Z]/u, '_\&') =~ /_*(.*)/ 43 | return $+.downcase 44 | 45 | end 46 | 47 | def camelize(str) 48 | str.split('_').map {|w| w.capitalize}.join 49 | end 50 | end 51 | 52 | # Contains a range of geocoders: 53 | # 54 | # ### "regular" address geocoders 55 | # * Yahoo Geocoder - requires an API key. 56 | # * Geocoder.us - may require authentication if performing more than the free request limit. 57 | # * Geocoder.ca - for Canada; may require authentication as well. 58 | # * Geonames - a free geocoder 59 | # 60 | # ### address geocoders that also provide reverse geocoding 61 | # * Google Geocoder - requires an API key. 62 | # 63 | # ### IP address geocoders 64 | # * IP Geocoder - geocodes an IP address using hostip.info's web service. 65 | # * Geoplugin.net -- another IP address geocoder 66 | # 67 | # ### The Multigeocoder 68 | # * Multi Geocoder - provides failover for the physical location geocoders. 69 | # 70 | # Some of these geocoders require configuration. You don't have to provide it here. See the README. 71 | module Geocoders 72 | @@proxy_addr = nil 73 | @@proxy_port = nil 74 | @@proxy_user = nil 75 | @@proxy_pass = nil 76 | @@request_timeout = nil 77 | @@yahoo = 'REPLACE_WITH_YOUR_YAHOO_KEY' 78 | @@google = 'REPLACE_WITH_YOUR_GOOGLE_KEY' 79 | @@google_client_id = nil #only used for premier accounts 80 | @@google_premier_secret_key = nil 81 | @@geocoder_us = false 82 | @@geocoder_ca = false 83 | @@geonames = false 84 | @@provider_order = [:google,:us] 85 | @@ip_provider_order = [:geo_plugin,:ip] 86 | @@logger=Logger.new(STDOUT) 87 | @@logger.level=Logger::INFO 88 | @@domain = nil 89 | 90 | def self.__define_accessors 91 | class_variables.each do |v| 92 | sym = v.to_s.delete("@").to_sym 93 | unless self.respond_to? sym 94 | module_eval <<-EOS, __FILE__, __LINE__ 95 | def self.#{sym} 96 | value = if defined?(#{sym.to_s.upcase}) 97 | #{sym.to_s.upcase} 98 | else 99 | @@#{sym} 100 | end 101 | if value.is_a?(Hash) 102 | value = (self.domain.nil? ? nil : value[self.domain]) || value.values.first 103 | end 104 | value 105 | end 106 | 107 | def self.#{sym}=(obj) 108 | @@#{sym} = obj 109 | end 110 | EOS 111 | end 112 | end 113 | end 114 | 115 | __define_accessors 116 | 117 | # Error which is thrown in the event a geocoding error occurs. 118 | class GeocodeError < StandardError; end 119 | 120 | # ------------------------------------------------------------------------------------------- 121 | # Geocoder Base class -- every geocoder should inherit from this 122 | # ------------------------------------------------------------------------------------------- 123 | 124 | # The Geocoder base class which defines the interface to be used by all 125 | # other geocoders. 126 | class Geocoder 127 | # Main method which calls the do_geocode template method which subclasses 128 | # are responsible for implementing. Returns a populated GeoLoc or an 129 | # empty one with a failed success code. 130 | def self.geocode(address, options = {}) 131 | res = do_geocode(address, options) 132 | return res.nil? ? GeoLoc.new : res 133 | end 134 | # Main method which calls the do_reverse_geocode template method which subclasses 135 | # are responsible for implementing. Returns a populated GeoLoc or an 136 | # empty one with a failed success code. 137 | def self.reverse_geocode(latlng) 138 | res = do_reverse_geocode(latlng) 139 | return res.success? ? res : GeoLoc.new 140 | end 141 | 142 | # Call the geocoder service using the timeout if configured. 143 | def self.call_geocoder_service(url) 144 | Timeout::timeout(Geokit::Geocoders::request_timeout) { return self.do_get(url) } if Geokit::Geocoders::request_timeout 145 | return self.do_get(url) 146 | rescue TimeoutError 147 | return nil 148 | end 149 | 150 | # Not all geocoders can do reverse geocoding. So, unless the subclass explicitly overrides this method, 151 | # a call to reverse_geocode will return an empty GeoLoc. If you happen to be using MultiGeocoder, 152 | # this will cause it to failover to the next geocoder, which will hopefully be one which supports reverse geocoding. 153 | def self.do_reverse_geocode(latlng) 154 | return GeoLoc.new 155 | end 156 | 157 | # This will sign a raw url with a private key 158 | def self.sign_url(raw_url,private_key) 159 | uri = URI.parse(raw_url) 160 | url_to_sign = uri.path + "?" + uri.query 161 | decoded_key = Geocoder.urlsafe_decode64(private_key) 162 | 163 | sha1_digest = OpenSSL::Digest::Digest.new('sha1') 164 | signature = OpenSSL::HMAC.digest(sha1_digest,decoded_key,url_to_sign) 165 | encoded_signature = Geocoder.urlsafe_encode64(signature) 166 | signed_url = "#{uri.scheme}://#{uri.host}#{uri.path}?#{uri.query}&signature=#{encoded_signature}".strip! 167 | signed_url 168 | end 169 | 170 | # This will provide url safe base64 decoding 171 | def self.urlsafe_decode64(raw_text) 172 | decoded_text = raw_text.gsub('-','+').gsub('_', '/') 173 | decoded_text = Base64.decode64(decoded_text) 174 | decoded_text 175 | end 176 | 177 | # This will provide url safe base64 encoding 178 | def self.urlsafe_encode64(raw_text) 179 | encoded_text = Base64.encode64(raw_text) 180 | encoded_text = encoded_text.gsub('+','-').gsub('/', '_') 181 | encoded_text 182 | end 183 | 184 | protected 185 | 186 | def self.logger() 187 | Geokit::Geocoders::logger 188 | end 189 | 190 | private 191 | 192 | # Wraps the geocoder call around a proxy if necessary. 193 | def self.do_get(url) 194 | uri = URI.parse(url) 195 | req = Net::HTTP::Get.new(url) 196 | req.basic_auth(uri.user, uri.password) if uri.userinfo 197 | res = Net::HTTP::Proxy(GeoKit::Geocoders::proxy_addr, 198 | GeoKit::Geocoders::proxy_port, 199 | GeoKit::Geocoders::proxy_user, 200 | GeoKit::Geocoders::proxy_pass).start(uri.host, uri.port) { |http| http.get(uri.path + "?" + uri.query) } 201 | return res 202 | end 203 | 204 | # Adds subclass' geocode method making it conveniently available through 205 | # the base class. 206 | def self.inherited(clazz) 207 | class_name = clazz.name.split('::').last 208 | src = <<-END_SRC 209 | def self.#{Geokit::Inflector.underscore(class_name)}(address, options = {}) 210 | #{class_name}.geocode(address, options) 211 | end 212 | END_SRC 213 | class_eval(src) 214 | end 215 | end 216 | 217 | # ------------------------------------------------------------------------------------------- 218 | # "Regular" Address geocoders 219 | # ------------------------------------------------------------------------------------------- 220 | 221 | # Geocoder CA geocoder implementation. Requires the Geokit::Geocoders::GEOCODER_CA variable to 222 | # contain true or false based upon whether authentication is to occur. Conforms to the 223 | # interface set by the Geocoder class. 224 | # 225 | # Returns a response like: 226 | # 227 | # 228 | # 49.243086 229 | # -123.153684 230 | # 231 | class CaGeocoder < Geocoder 232 | 233 | private 234 | 235 | # Template method which does the geocode lookup. 236 | def self.do_geocode(address, options = {}) 237 | raise ArgumentError('Geocoder.ca requires a GeoLoc argument') unless address.is_a?(GeoLoc) 238 | url = construct_request(address) 239 | res = self.call_geocoder_service(url) 240 | return GeoLoc.new if !res.is_a?(Net::HTTPSuccess) 241 | xml = res.body 242 | logger.debug "Geocoder.ca geocoding. Address: #{address}. Result: #{xml}" 243 | # Parse the document. 244 | doc = REXML::Document.new(xml) 245 | address.lat = doc.elements['//latt'].text 246 | address.lng = doc.elements['//longt'].text 247 | address.success = true 248 | return address 249 | rescue 250 | logger.error "Caught an error during Geocoder.ca geocoding call: "+$! 251 | return GeoLoc.new 252 | end 253 | 254 | # Formats the request in the format acceptable by the CA geocoder. 255 | def self.construct_request(location) 256 | url = "" 257 | url += add_ampersand(url) + "stno=#{location.street_number}" if location.street_address 258 | url += add_ampersand(url) + "addresst=#{URI.escape(location.street_name)}" if location.street_address 259 | url += add_ampersand(url) + "city=#{URI.escape(location.city)}" if location.city 260 | url += add_ampersand(url) + "prov=#{location.state}" if location.state 261 | url += add_ampersand(url) + "postal=#{location.zip}" if location.zip 262 | url += add_ampersand(url) + "auth=#{Geokit::Geocoders::geocoder_ca}" if Geokit::Geocoders::geocoder_ca 263 | url += add_ampersand(url) + "geoit=xml" 264 | 'http://geocoder.ca/?' + url 265 | end 266 | 267 | def self.add_ampersand(url) 268 | url && url.length > 0 ? "&" : "" 269 | end 270 | end 271 | 272 | # Geocoder Us geocoder implementation. Requires the Geokit::Geocoders::GEOCODER_US variable to 273 | # contain true or false based upon whether authentication is to occur. Conforms to the 274 | # interface set by the Geocoder class. 275 | class UsGeocoder < Geocoder 276 | 277 | private 278 | def self.do_geocode(address, options = {}) 279 | address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address 280 | 281 | query = (address_str =~ /^\d{5}(?:-\d{4})?$/ ? "zip" : "address") + "=#{URI.escape(address_str)}" 282 | url = if GeoKit::Geocoders::geocoder_us 283 | "http://#{GeoKit::Geocoders::geocoder_us}@geocoder.us/member/service/csv/geocode" 284 | else 285 | "http://geocoder.us/service/csv/geocode" 286 | end 287 | 288 | url = "#{url}?#{query}" 289 | res = self.call_geocoder_service(url) 290 | 291 | return GeoLoc.new if !res.is_a?(Net::HTTPSuccess) 292 | data = res.body 293 | logger.debug "Geocoder.us geocoding. Address: #{address}. Result: #{data}" 294 | array = data.chomp.split(',') 295 | 296 | if array.length == 5 297 | res=GeoLoc.new 298 | res.lat,res.lng,res.city,res.state,res.zip=array 299 | res.country_code='US' 300 | res.success=true 301 | return res 302 | elsif array.length == 6 303 | res=GeoLoc.new 304 | res.lat,res.lng,res.street_address,res.city,res.state,res.zip=array 305 | res.country_code='US' 306 | res.success=true 307 | return res 308 | else 309 | logger.info "geocoder.us was unable to geocode address: "+address 310 | return GeoLoc.new 311 | end 312 | rescue 313 | logger.error "Caught an error during geocoder.us geocoding call: "+$! 314 | return GeoLoc.new 315 | 316 | end 317 | end 318 | 319 | # Yahoo geocoder implementation. Requires the Geokit::Geocoders::YAHOO variable to 320 | # contain a Yahoo API key. Conforms to the interface set by the Geocoder class. 321 | class YahooGeocoder < Geocoder 322 | 323 | private 324 | 325 | # Template method which does the geocode lookup. 326 | def self.do_geocode(address, options = {}) 327 | address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address 328 | url="http://api.local.yahoo.com/MapsService/V1/geocode?appid=#{Geokit::Geocoders::yahoo}&location=#{URI.escape(address_str)}" 329 | res = self.call_geocoder_service(url) 330 | return GeoLoc.new if !res.is_a?(Net::HTTPSuccess) 331 | xml = res.body 332 | doc = REXML::Document.new(xml) 333 | logger.debug "Yahoo geocoding. Address: #{address}. Result: #{xml}" 334 | 335 | if doc.elements['//ResultSet'] 336 | res=GeoLoc.new 337 | 338 | #basic 339 | res.lat=doc.elements['//Latitude'].text 340 | res.lng=doc.elements['//Longitude'].text 341 | res.country_code=doc.elements['//Country'].text 342 | res.provider='yahoo' 343 | 344 | #extended - false if not available 345 | res.city=doc.elements['//City'].text if doc.elements['//City'] && doc.elements['//City'].text != nil 346 | res.state=doc.elements['//State'].text if doc.elements['//State'] && doc.elements['//State'].text != nil 347 | res.zip=doc.elements['//Zip'].text if doc.elements['//Zip'] && doc.elements['//Zip'].text != nil 348 | res.street_address=doc.elements['//Address'].text if doc.elements['//Address'] && doc.elements['//Address'].text != nil 349 | res.precision=doc.elements['//Result'].attributes['precision'] if doc.elements['//Result'] 350 | # set the accuracy as google does (added by Andruby) 351 | res.accuracy=%w{unknown country state state city zip zip+4 street address building}.index(res.precision) 352 | res.success=true 353 | return res 354 | else 355 | logger.info "Yahoo was unable to geocode address: "+address 356 | return GeoLoc.new 357 | end 358 | 359 | rescue 360 | logger.info "Caught an error during Yahoo geocoding call: "+$! 361 | return GeoLoc.new 362 | end 363 | end 364 | 365 | # Another geocoding web service 366 | # http://www.geonames.org 367 | class GeonamesGeocoder < Geocoder 368 | 369 | private 370 | 371 | # Template method which does the geocode lookup. 372 | def self.do_geocode(address, options = {}) 373 | address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address 374 | # geonames need a space seperated search string 375 | address_str.gsub!(/,/, " ") 376 | params = "/postalCodeSearch?placename=#{URI.escape(address_str)}&maxRows=10" 377 | 378 | if(GeoKit::Geocoders::geonames) 379 | url = "http://ws.geonames.net#{params}&username=#{GeoKit::Geocoders::geonames}" 380 | else 381 | url = "http://ws.geonames.org#{params}" 382 | end 383 | 384 | res = self.call_geocoder_service(url) 385 | 386 | return GeoLoc.new if !res.is_a?(Net::HTTPSuccess) 387 | 388 | xml=res.body 389 | logger.debug "Geonames geocoding. Address: #{address}. Result: #{xml}" 390 | doc=REXML::Document.new(xml) 391 | 392 | if(doc.elements['//geonames/totalResultsCount'].text.to_i > 0) 393 | res=GeoLoc.new 394 | 395 | # only take the first result 396 | res.lat=doc.elements['//code/lat'].text if doc.elements['//code/lat'] 397 | res.lng=doc.elements['//code/lng'].text if doc.elements['//code/lng'] 398 | res.country_code=doc.elements['//code/countryCode'].text if doc.elements['//code/countryCode'] 399 | res.provider='genomes' 400 | res.city=doc.elements['//code/name'].text if doc.elements['//code/name'] 401 | res.state=doc.elements['//code/adminName1'].text if doc.elements['//code/adminName1'] 402 | res.zip=doc.elements['//code/postalcode'].text if doc.elements['//code/postalcode'] 403 | res.success=true 404 | return res 405 | else 406 | logger.info "Geonames was unable to geocode address: "+address 407 | return GeoLoc.new 408 | end 409 | 410 | rescue 411 | logger.error "Caught an error during Geonames geocoding call: "+$! 412 | end 413 | end 414 | 415 | # ------------------------------------------------------------------------------------------- 416 | # Address geocoders that also provide reverse geocoding 417 | # ------------------------------------------------------------------------------------------- 418 | 419 | # Google geocoder implementation. Requires the Geokit::Geocoders::GOOGLE variable to 420 | # contain a Google API key. Conforms to the interface set by the Geocoder class. 421 | class GoogleGeocoder < Geocoder 422 | 423 | private 424 | 425 | # Template method which does the reverse-geocode lookup. 426 | def self.do_reverse_geocode(latlng) 427 | latlng=LatLng.normalize(latlng) 428 | res = self.call_geocoder_service("http://maps.google.com/maps/geo?ll=#{URI.escape(latlng.ll)}&output=xml&key=#{Geokit::Geocoders::google}&oe=utf-8") 429 | # 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")) 430 | return GeoLoc.new unless (res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPOK)) 431 | xml = res.body 432 | logger.debug "Google reverse-geocoding. LL: #{latlng}. Result: #{xml}" 433 | return self.xml2GeoLoc(xml) 434 | end 435 | 436 | # Template method which does the geocode lookup. 437 | # 438 | # Supports viewport/country code biasing 439 | # 440 | # ==== OPTIONS 441 | # * :bias - This option makes the Google Geocoder return results biased to a particular 442 | # country or viewport. Country code biasing is achieved by passing the ccTLD 443 | # ('uk' for .co.uk, for example) as a :bias value. For a list of ccTLD's, 444 | # look here: http://en.wikipedia.org/wiki/CcTLD. By default, the geocoder 445 | # will be biased to results within the US (ccTLD .com). 446 | # 447 | # If you'd like the Google Geocoder to prefer results within a given viewport, 448 | # you can pass a Geokit::Bounds object as the :bias value. 449 | # 450 | # ==== EXAMPLES 451 | # # By default, the geocoder will return Syracuse, NY 452 | # Geokit::Geocoders::GoogleGeocoder.geocode('Syracuse').country_code # => 'US' 453 | # # With country code biasing, it returns Syracuse in Sicily, Italy 454 | # Geokit::Geocoders::GoogleGeocoder.geocode('Syracuse', :bias => :it).country_code # => 'IT' 455 | # 456 | # # By default, the geocoder will return Winnetka, IL 457 | # Geokit::Geocoders::GoogleGeocoder.geocode('Winnetka').state # => 'IL' 458 | # # When biased to an bounding box around California, it will now return the Winnetka neighbourhood, CA 459 | # bounds = Geokit::Bounds.normalize([34.074081, -118.694401], [34.321129, -118.399487]) 460 | # Geokit::Geocoders::GoogleGeocoder.geocode('Winnetka', :bias => bounds).state # => 'CA' 461 | def self.do_geocode(address, options = {}) 462 | bias_str = options[:bias] ? construct_bias_string_from_options(options[:bias]) : '' 463 | address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address 464 | res = self.call_geocoder_service("http://maps.google.com/maps/geo?q=#{URI.escape(address_str)}&output=xml#{bias_str}&key=#{Geokit::Geocoders::google}&oe=utf-8") 465 | return GeoLoc.new if !res.is_a?(Net::HTTPSuccess) 466 | xml = res.body.force_encoding('utf-8') 467 | logger.debug "Google geocoding. Address: #{address}. Result: #{xml}" 468 | return self.xml2GeoLoc(xml, address) 469 | end 470 | 471 | def self.construct_bias_string_from_options(bias) 472 | if bias.is_a?(String) or bias.is_a?(Symbol) 473 | # country code biasing 474 | "&gl=#{bias.to_s.downcase}" 475 | elsif bias.is_a?(Bounds) 476 | # viewport biasing 477 | "&ll=#{bias.center.ll}&spn=#{bias.to_span.ll}" 478 | end 479 | end 480 | 481 | def self.xml2GeoLoc(xml, address="") 482 | doc=REXML::Document.new(xml) 483 | 484 | if doc.elements['//kml/Response/Status/code'].text == '200' 485 | geoloc = nil 486 | # Google can return multiple results as //Placemark elements. 487 | # iterate through each and extract each placemark as a geoloc 488 | doc.each_element('//Placemark') do |e| 489 | extracted_geoloc = extract_placemark(e) # g is now an instance of GeoLoc 490 | if geoloc.nil? 491 | # first time through, geoloc is still nil, so we make it the geoloc we just extracted 492 | geoloc = extracted_geoloc 493 | else 494 | # second (and subsequent) iterations, we push additional 495 | # geolocs onto "geoloc.all" 496 | geoloc.all.push(extracted_geoloc) 497 | end 498 | end 499 | return geoloc 500 | elsif doc.elements['//kml/Response/Status/code'].text == '620' 501 | raise Geokit::TooManyQueriesError 502 | else 503 | logger.info "Google was unable to geocode address: "+address 504 | return GeoLoc.new 505 | end 506 | 507 | rescue Geokit::TooManyQueriesError 508 | # re-raise because of other rescue 509 | 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." 510 | rescue 511 | logger.error "Caught an error during Google geocoding call: "+$! 512 | return GeoLoc.new 513 | end 514 | 515 | # extracts a single geoloc from a //placemark element in the google results xml 516 | def self.extract_placemark(doc) 517 | res = GeoLoc.new 518 | coordinates=doc.elements['.//coordinates'].text.to_s.split(',') 519 | 520 | #basics 521 | res.lat=coordinates[1] 522 | res.lng=coordinates[0] 523 | res.country_code=doc.elements['.//CountryNameCode'].text if doc.elements['.//CountryNameCode'] 524 | res.provider='google' 525 | 526 | #extended -- false if not not available 527 | res.city = doc.elements['.//LocalityName'].text if doc.elements['.//LocalityName'] 528 | res.state = doc.elements['.//AdministrativeAreaName'].text if doc.elements['.//AdministrativeAreaName'] 529 | res.province = doc.elements['.//SubAdministrativeAreaName'].text if doc.elements['.//SubAdministrativeAreaName'] 530 | res.full_address = doc.elements['.//address'].text if doc.elements['.//address'] # google provides it 531 | res.zip = doc.elements['.//PostalCodeNumber'].text if doc.elements['.//PostalCodeNumber'] 532 | res.street_address = doc.elements['.//ThoroughfareName'].text if doc.elements['.//ThoroughfareName'] 533 | res.country = doc.elements['.//CountryName'].text if doc.elements['.//CountryName'] 534 | res.district = doc.elements['.//DependentLocalityName'].text if doc.elements['.//DependentLocalityName'] 535 | # Translate accuracy into Yahoo-style token address, street, zip, zip+4, city, state, country 536 | # For Google, 1=low accuracy, 8=high accuracy 537 | address_details=doc.elements['.//*[local-name() = "AddressDetails"]'] 538 | res.accuracy = address_details ? address_details.attributes['Accuracy'].to_i : 0 539 | res.precision=%w{unknown country state state city zip zip+4 street address building}[res.accuracy] 540 | 541 | # google returns a set of suggested boundaries for the geocoded result 542 | if suggested_bounds = doc.elements['//LatLonBox'] 543 | res.suggested_bounds = Bounds.normalize( 544 | [suggested_bounds.attributes['south'], suggested_bounds.attributes['west']], 545 | [suggested_bounds.attributes['north'], suggested_bounds.attributes['east']]) 546 | end 547 | 548 | res.success=true 549 | 550 | return res 551 | end 552 | end 553 | 554 | class GoogleGeocoder3 < Geocoder 555 | 556 | private 557 | # Template method which does the reverse-geocode lookup. 558 | def self.do_reverse_geocode(latlng) 559 | latlng=LatLng.normalize(latlng) 560 | res = self.call_geocoder_service("http://maps.google.com/maps/api/geocode/json?sensor=false&latlng=#{URI.escape(latlng.ll)}") 561 | return GeoLoc.new unless (res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPOK)) 562 | json = res.body 563 | logger.debug "Google reverse-geocoding. LL: #{latlng}. Result: #{json}" 564 | return self.json2GeoLoc(json) 565 | end 566 | 567 | # Template method which does the geocode lookup. 568 | # 569 | # Supports viewport/country code biasing 570 | # 571 | # ==== OPTIONS 572 | # * :bias - This option makes the Google Geocoder return results biased to a particular 573 | # country or viewport. Country code biasing is achieved by passing the ccTLD 574 | # ('uk' for .co.uk, for example) as a :bias value. For a list of ccTLD's, 575 | # look here: http://en.wikipedia.org/wiki/CcTLD. By default, the geocoder 576 | # will be biased to results within the US (ccTLD .com). 577 | # 578 | # If you'd like the Google Geocoder to prefer results within a given viewport, 579 | # you can pass a Geokit::Bounds object as the :bias value. 580 | # 581 | # ==== EXAMPLES 582 | # # By default, the geocoder will return Syracuse, NY 583 | # Geokit::Geocoders::GoogleGeocoder.geocode('Syracuse').country_code # => 'US' 584 | # # With country code biasing, it returns Syracuse in Sicily, Italy 585 | # Geokit::Geocoders::GoogleGeocoder.geocode('Syracuse', :bias => :it).country_code # => 'IT' 586 | # 587 | # # By default, the geocoder will return Winnetka, IL 588 | # Geokit::Geocoders::GoogleGeocoder.geocode('Winnetka').state # => 'IL' 589 | # # When biased to an bounding box around California, it will now return the Winnetka neighbourhood, CA 590 | # bounds = Geokit::Bounds.normalize([34.074081, -118.694401], [34.321129, -118.399487]) 591 | # Geokit::Geocoders::GoogleGeocoder.geocode('Winnetka', :bias => bounds).state # => 'CA' 592 | def self.do_geocode(address, options = {}) 593 | res = res = self.call_geocoder_service(self.geocode_url(address,options)) 594 | return GeoLoc.new if !res.is_a?(Net::HTTPSuccess) 595 | json = res.body 596 | logger.debug "Google geocoding. Address: #{address}. Result: #{json}" 597 | return self.json2GeoLoc(json, address) 598 | end 599 | 600 | # Determine the Google API url based on the google api key, or based on the client / private key for premier users 601 | def self.geocode_url(address,options = {}) 602 | bias_str = options[:bias] ? construct_bias_string_from_options(options[:bias]) : '' 603 | address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address 604 | 605 | if !Geokit::Geocoders::google_client_id.nil? && !Geokit::Geocoders::google_premier_secret_key.nil? 606 | url = "http://maps.googleapis.com/maps/api/geocode/json?address=#{URI.escape(address_str)}#{bias_str}&client=#{Geokit::Geocoders::google_client_id}&sensor=false&oe=utf-8" 607 | Geokit::Geocoders::Geocoder.sign_url(url,Geokit::Geocoders::google_premier_secret_key) 608 | else 609 | "http://maps.google.com/maps/api/geocode/json?sensor=false&address=#{URI.escape(address_str)}#{bias_str}" 610 | end 611 | end 612 | 613 | 614 | def self.construct_bias_string_from_options(bias) 615 | if bias.is_a?(String) or bias.is_a?(Symbol) 616 | # country code biasing 617 | "®ion=#{bias.to_s.downcase}" 618 | elsif bias.is_a?(Bounds) 619 | # viewport biasing 620 | URI.escape("&bounds=#{bias.sw.to_s}|#{bias.ne.to_s}") 621 | end 622 | end 623 | 624 | def self.json2GeoLoc(json, address="") 625 | ret=nil 626 | begin 627 | results=::ActiveSupport::JSON.decode(json) 628 | rescue NameError => e 629 | results=JSON.parse(json) 630 | end 631 | 632 | 633 | if results['status'] == 'OVER_QUERY_LIMIT' 634 | raise Geokit::TooManyQueriesError 635 | end 636 | if results['status'] == 'ZERO_RESULTS' 637 | return GeoLoc.new 638 | end 639 | # this should probably be smarter. 640 | if !results['status'] == 'OK' 641 | raise Geokit::Geocoders::GeocodeError 642 | end 643 | # location_type stores additional data about the specified location. 644 | # The following values are currently supported: 645 | # "ROOFTOP" indicates that the returned result is a precise geocode 646 | # for which we have location information accurate down to street 647 | # address precision. 648 | # "RANGE_INTERPOLATED" indicates that the returned result reflects an 649 | # approximation (usually on a road) interpolated between two precise 650 | # points (such as intersections). Interpolated results are generally 651 | # returned when rooftop geocodes are unavailable for a street address. 652 | # "GEOMETRIC_CENTER" indicates that the returned result is the 653 | # geometric center of a result such as a polyline (for example, a 654 | # street) or polygon (region). 655 | # "APPROXIMATE" indicates that the returned result is approximate 656 | 657 | # these do not map well. Perhaps we should guess better based on size 658 | # of bounding box where it exists? Does it really matter? 659 | accuracy = { 660 | "ROOFTOP" => 9, 661 | "RANGE_INTERPOLATED" => 8, 662 | "GEOMETRIC_CENTER" => 5, 663 | "APPROXIMATE" => 4 664 | } 665 | results['results'].sort_by{|a|accuracy[a['geometry']['location_type']]}.reverse.each do |addr| 666 | res=GeoLoc.new 667 | res.provider = 'google3' 668 | res.success = true 669 | res.full_address = addr['formatted_address'] 670 | addr['address_components'].each do |comp| 671 | case 672 | when comp['types'].include?("street_number") 673 | res.street_number = comp['short_name'] 674 | when comp['types'].include?("route") 675 | res.street_name = comp['long_name'] 676 | when comp['types'].include?("locality") 677 | res.city = comp['long_name'] 678 | when comp['types'].include?("administrative_area_level_1") 679 | res.state = comp['short_name'] 680 | res.province = comp['short_name'] 681 | when comp['types'].include?("postal_code") 682 | res.zip = comp['long_name'] 683 | when comp['types'].include?("country") 684 | res.country_code = comp['short_name'] 685 | res.country = comp['long_name'] 686 | when comp['types'].include?("administrative_area_level_2") 687 | res.district = comp['long_name'] 688 | end 689 | end 690 | if res.street_name 691 | res.street_address=[res.street_number,res.street_name].join(' ').strip 692 | end 693 | res.accuracy = accuracy[addr['geometry']['location_type']] 694 | res.precision=%w{unknown country state state city zip zip+4 street address building}[res.accuracy] 695 | # try a few overrides where we can 696 | if res.street_name && res.precision=='city' 697 | res.precision = 'street' 698 | res.accuracy = 7 699 | end 700 | 701 | res.lat=addr['geometry']['location']['lat'].to_f 702 | res.lng=addr['geometry']['location']['lng'].to_f 703 | 704 | ne=Geokit::LatLng.new( 705 | addr['geometry']['viewport']['northeast']['lat'].to_f, 706 | addr['geometry']['viewport']['northeast']['lng'].to_f 707 | ) 708 | sw=Geokit::LatLng.new( 709 | addr['geometry']['viewport']['southwest']['lat'].to_f, 710 | addr['geometry']['viewport']['southwest']['lng'].to_f 711 | ) 712 | res.suggested_bounds = Geokit::Bounds.new(sw,ne) 713 | 714 | if ret 715 | ret.all.push(res) 716 | else 717 | ret=res 718 | end 719 | end 720 | return ret 721 | end 722 | end 723 | # ------------------------------------------------------------------------------------------- 724 | # IP Geocoders 725 | # ------------------------------------------------------------------------------------------- 726 | 727 | # Provides geocoding based upon an IP address. The underlying web service is geoplugin.net 728 | class GeoPluginGeocoder < Geocoder 729 | private 730 | 731 | def self.do_geocode(ip, options = {}) 732 | return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip) 733 | response = self.call_geocoder_service("http://www.geoplugin.net/xml.gp?ip=#{ip}") 734 | return response.is_a?(Net::HTTPSuccess) ? parse_xml(response.body) : GeoLoc.new 735 | rescue 736 | logger.error "Caught an error during GeoPluginGeocoder geocoding call: "+$! 737 | return GeoLoc.new 738 | end 739 | 740 | def self.parse_xml(xml) 741 | xml = REXML::Document.new(xml) 742 | geo = GeoLoc.new 743 | geo.provider='geoPlugin' 744 | geo.city = xml.elements['//geoplugin_city'].text 745 | geo.state = xml.elements['//geoplugin_region'].text 746 | geo.country_code = xml.elements['//geoplugin_countryCode'].text 747 | geo.lat = xml.elements['//geoplugin_latitude'].text.to_f 748 | geo.lng = xml.elements['//geoplugin_longitude'].text.to_f 749 | geo.success = !!geo.city && !geo.city.empty? 750 | return geo 751 | end 752 | end 753 | 754 | # Provides geocoding based upon an IP address. The underlying web service is a hostip.info 755 | # which sources their data through a combination of publicly available information as well 756 | # as community contributions. 757 | class IpGeocoder < Geocoder 758 | 759 | # A number of non-routable IP ranges. 760 | # 761 | # -- 762 | # Sources for these: 763 | # RFC 3330: Special-Use IPv4 Addresses 764 | # The bogon list: http://www.cymru.com/Documents/bogon-list.html 765 | 766 | NON_ROUTABLE_IP_RANGES = [ 767 | IPAddr.new('0.0.0.0/8'), # "This" Network 768 | IPAddr.new('10.0.0.0/8'), # Private-Use Networks 769 | IPAddr.new('14.0.0.0/8'), # Public-Data Networks 770 | IPAddr.new('127.0.0.0/8'), # Loopback 771 | IPAddr.new('169.254.0.0/16'), # Link local 772 | IPAddr.new('172.16.0.0/12'), # Private-Use Networks 773 | IPAddr.new('192.0.2.0/24'), # Test-Net 774 | IPAddr.new('192.168.0.0/16'), # Private-Use Networks 775 | IPAddr.new('198.18.0.0/15'), # Network Interconnect Device Benchmark Testing 776 | IPAddr.new('224.0.0.0/4'), # Multicast 777 | IPAddr.new('240.0.0.0/4') # Reserved for future use 778 | ].freeze 779 | 780 | private 781 | 782 | # Given an IP address, returns a GeoLoc instance which contains latitude, 783 | # longitude, city, and country code. Sets the success attribute to false if the ip 784 | # parameter does not match an ip address. 785 | def self.do_geocode(ip, options = {}) 786 | return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip) 787 | return GeoLoc.new if self.private_ip_address?(ip) 788 | url = "http://api.hostip.info/get_html.php?ip=#{ip}&position=true" 789 | response = self.call_geocoder_service(url) 790 | response.is_a?(Net::HTTPSuccess) ? parse_body(response.body) : GeoLoc.new 791 | rescue 792 | logger.error "Caught an error during HostIp geocoding call: "+$! 793 | return GeoLoc.new 794 | end 795 | 796 | # Converts the body to YAML since its in the form of: 797 | # 798 | # Country: UNITED STATES (US) 799 | # City: Sugar Grove, IL 800 | # Latitude: 41.7696 801 | # Longitude: -88.4588 802 | # 803 | # then instantiates a GeoLoc instance to populate with location data. 804 | def self.parse_body(body) # :nodoc: 805 | yaml = YAML.load(body) 806 | res = GeoLoc.new 807 | res.provider = 'hostip' 808 | res.city, res.state = yaml['City'].split(', ') 809 | country, res.country_code = yaml['Country'].split(' (') 810 | res.lat = yaml['Latitude'] 811 | res.lng = yaml['Longitude'] 812 | res.country_code.chop! 813 | res.success = !(res.city =~ /\(.+\)/) 814 | res 815 | end 816 | 817 | # Checks whether the IP address belongs to a private address range. 818 | # 819 | # This function is used to reduce the number of useless queries made to 820 | # the geocoding service. Such queries can occur frequently during 821 | # integration tests. 822 | def self.private_ip_address?(ip) 823 | return NON_ROUTABLE_IP_RANGES.any? { |range| range.include?(ip) } 824 | end 825 | end 826 | 827 | # ------------------------------------------------------------------------------------------- 828 | # The Multi Geocoder 829 | # ------------------------------------------------------------------------------------------- 830 | 831 | # Provides methods to geocode with a variety of geocoding service providers, plus failover 832 | # among providers in the order you configure. When 2nd parameter is set 'true', perform 833 | # ip location lookup with 'address' as the ip address. 834 | # 835 | # Goal: 836 | # - homogenize the results of multiple geocoders 837 | # 838 | # Limitations: 839 | # - currently only provides the first result. Sometimes geocoders will return multiple results. 840 | # - currently discards the "accuracy" component of the geocoding calls 841 | class MultiGeocoder < Geocoder 842 | 843 | private 844 | # This method will call one or more geocoders in the order specified in the 845 | # configuration until one of the geocoders work. 846 | # 847 | # The failover approach is crucial for production-grade apps, but is rarely used. 848 | # 98% of your geocoding calls will be successful with the first call 849 | def self.do_geocode(address, options = {}) 850 | geocode_ip = /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.match(address) 851 | provider_order = geocode_ip ? Geokit::Geocoders::ip_provider_order : Geokit::Geocoders::provider_order 852 | 853 | provider_order.each do |provider| 854 | begin 855 | klass = Geokit::Geocoders.const_get "#{Geokit::Inflector::camelize(provider.to_s)}Geocoder" 856 | res = klass.send :geocode, address, options 857 | return res if res.success? 858 | rescue 859 | 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}") 860 | end 861 | end 862 | # If we get here, we failed completely. 863 | GeoLoc.new 864 | end 865 | 866 | # This method will call one or more geocoders in the order specified in the 867 | # configuration until one of the geocoders work, only this time it's going 868 | # to try to reverse geocode a geographical point. 869 | def self.do_reverse_geocode(latlng) 870 | Geokit::Geocoders::provider_order.each do |provider| 871 | begin 872 | klass = Geokit::Geocoders.const_get "#{Geokit::Inflector::camelize(provider.to_s)}Geocoder" 873 | res = klass.send :reverse_geocode, latlng 874 | return res if res.success? 875 | rescue 876 | logger.error("Something has gone very wrong during reverse geocoding, OR you have configured an invalid class name in Geokit::Geocoders::provider_order. LatLng: #{latlng}. Provider: #{provider}") 877 | end 878 | end 879 | # If we get here, we failed completely. 880 | GeoLoc.new 881 | end 882 | end 883 | end 884 | end 885 | --------------------------------------------------------------------------------