├── .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.39411100 SPEAR STSAN FRANCISCOCA94105-1522US
9 |
10 | EOF
11 |
12 | YAHOO_CITY=<<-EOF.strip
13 |
14 | 37.7742-122.417068SAN 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.016685200geocodePorscheplatz 1, 45127 Essen, DeutschlandDEDeutschlandNordrhein-WestfalenEssenEssenStadtkernPorscheplatz 1451277.0166848,51.4578329,0Stadtkern, Essen, DeutschlandDEDeutschlandNordrhein-WestfalenEssenEssenStadtkern7.0124328,51.4568201,045127 Essen, DeutschlandDEDeutschlandNordrhein-WestfalenEssenEssen451277.0104543,51.4556194,0Essen, DeutschlandDEDeutschlandNordrhein-WestfalenEssenEssen7.0147614,51.4580686,0Essen, DeutschlandDEDeutschlandNordrhein-WestfalenEssen7.0461136,51.4508381,0Nordrhein-Westfalen, DeutschlandDEDeutschlandNordrhein-Westfalen7.6615938,51.4332367,0DeutschlandDEDeutschland10.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, ca200geocode100 Spear St, San Francisco, CA 94105, USAUSCASan 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, ca200geocode100 Spear St, San Francisco, CA 94105, USAUSCASan FranciscoSan Francisco100 Spear St94105-122.393985,37.792501,0
13 | EOF
14 |
15 | GOOGLE_CITY=<<-EOF.strip
16 | San Francisco200geocodeSan Francisco, CA, USAUSCASan 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.703250200geocodePlaza de la Puerta del Sol, 28013, Madrid, SpainESSpainMadridMadridMadridPlaza de la Puerta del Sol28013-3.7032537,40.4168023,028013, Madrid, SpainESSpainMadridMadridMadrid28013-3.7117806,40.4189645,0Madrid, SpainESSpainMadridMadridMadrid-3.7032498,40.4167413,0Madrid, SpainESSpainMadrid-3.5812692,40.4167088,0Madrid, SpainESSpainMadridMadrid-3.5812692,40.4167088,0SpainESSpain-3.7492200,40.4636670,0"
22 |
23 | GOOGLE_COUNTRY_CODE_BIASED_RESULT = <<-EOF.strip
24 | Syracuse200geocodeSyracuse, ItalyITItalySicilySyracuse14.9856176,37.0630218,0
25 | EOF
26 |
27 | GOOGLE_BOUNDS_BIASED_RESULT = <<-EOF.strip
28 | Winnetka200geocodeWinnetka, California, USAUSUSACAWinnetka-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 |
--------------------------------------------------------------------------------