├── test
├── fixtures
│ └── responses
│ │ ├── geocoder_us
│ │ ├── unknown.xml
│ │ └── success.xml
│ │ ├── local_search_maps
│ │ ├── not_found.txt
│ │ ├── empty.txt
│ │ └── success.txt
│ │ ├── freethepostcode
│ │ ├── not_found.txt
│ │ └── success.txt
│ │ ├── google
│ │ ├── badkey.json
│ │ ├── limit.json
│ │ ├── server_error.json
│ │ ├── zero_results.json
│ │ ├── country.json
│ │ ├── region.json
│ │ ├── locality.json
│ │ ├── street.json
│ │ ├── address.json
│ │ ├── success.json
│ │ └── success_multiple_results.json
│ │ ├── simple_geo
│ │ ├── error.json
│ │ └── success.json
│ │ ├── host_ip
│ │ ├── private.txt
│ │ ├── unknown.txt
│ │ └── success.txt
│ │ ├── yandex
│ │ ├── badkey.xml
│ │ └── success.xml
│ │ ├── geonames
│ │ ├── missing.xml
│ │ ├── unknown.xml
│ │ └── success.xml
│ │ ├── yahoo
│ │ ├── unknown_address.xml
│ │ └── success.xml
│ │ ├── multimap
│ │ ├── no_matches.xml
│ │ ├── missing_params.xml
│ │ └── success.xml
│ │ ├── geocoder_ca
│ │ └── success.xml
│ │ ├── mapbox
│ │ ├── no_results.json
│ │ ├── empty_results.json
│ │ └── success.json
│ │ └── mapquest
│ │ ├── success.xml
│ │ ├── multi_country_success.xml
│ │ └── multi_result.xml
├── graticule
│ ├── geocoder_test.rb
│ ├── geocoder
│ │ ├── local_search_maps_test.rb
│ │ ├── google_signed_test.rb
│ │ ├── freethepostcode_test.rb
│ │ ├── host_ip_test.rb
│ │ ├── yandex_test.rb
│ │ ├── mapbox_test.rb
│ │ ├── geocoder_ca_test.rb
│ │ ├── geocoder_us_test.rb
│ │ ├── simple_geo_test.rb
│ │ ├── geonames_test.rb
│ │ ├── yahoo_test.rb
│ │ ├── multimap_test.rb
│ │ ├── geocoders.rb
│ │ ├── multi_test.rb
│ │ ├── mapquest_test.rb
│ │ └── google_test.rb
│ ├── precision_test.rb
│ ├── distance_test.rb
│ └── location_test.rb
├── test_helper.rb
├── mocks
│ └── uri.rb
└── config.yml.default
├── bin
└── geocode
├── lib
├── graticule
│ ├── version.rb
│ ├── core_ext.rb
│ ├── distance.rb
│ ├── geocoder
│ │ ├── bogus.rb
│ │ ├── freethepostcode.rb
│ │ ├── host_ip.rb
│ │ ├── mapbox.rb
│ │ ├── geonames.rb
│ │ ├── local_search_maps.rb
│ │ ├── geocoder_us.rb
│ │ ├── simple_geo.rb
│ │ ├── geocoder_ca.rb
│ │ ├── multimap.rb
│ │ ├── multi.rb
│ │ ├── yahoo.rb
│ │ ├── base.rb
│ │ ├── mapquest.rb
│ │ ├── yandex.rb
│ │ └── google.rb
│ ├── geocoder.rb
│ ├── precision.rb
│ ├── distance
│ │ ├── haversine.rb
│ │ ├── spherical.rb
│ │ └── vincenty.rb
│ ├── cli.rb
│ └── location.rb
└── graticule.rb
├── .gitignore
├── Gemfile
├── .autotest
├── .travis.yml
├── graticule.gemspec
├── site
├── stylesheets
│ └── style.css
├── plugin.html
└── index.html
├── LICENSE.txt
├── Rakefile
├── CHANGELOG.txt
└── README.md
/test/fixtures/responses/geocoder_us/unknown.xml:
--------------------------------------------------------------------------------
1 | couldn't find this address! sorry
2 |
--------------------------------------------------------------------------------
/test/fixtures/responses/local_search_maps/not_found.txt:
--------------------------------------------------------------------------------
1 | alert('location not found');
2 |
--------------------------------------------------------------------------------
/test/fixtures/responses/local_search_maps/empty.txt:
--------------------------------------------------------------------------------
1 | alert('Please provide a location');
2 |
--------------------------------------------------------------------------------
/bin/geocode:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require 'graticule/cli'
4 |
5 | Graticule::Cli.start ARGV
--------------------------------------------------------------------------------
/test/fixtures/responses/freethepostcode/not_found.txt:
--------------------------------------------------------------------------------
1 | # looking up postcode z12 9pp
2 |
3 |
4 |
--------------------------------------------------------------------------------
/test/fixtures/responses/google/badkey.json:
--------------------------------------------------------------------------------
1 | {
2 | "results" : [],
3 | "status" : "REQUEST_DENIED"
4 | }
--------------------------------------------------------------------------------
/test/fixtures/responses/google/limit.json:
--------------------------------------------------------------------------------
1 | {
2 | "results" : [],
3 | "status" : "OVER_QUERY_LIMIT"
4 | }
--------------------------------------------------------------------------------
/test/fixtures/responses/google/server_error.json:
--------------------------------------------------------------------------------
1 | {
2 | "results" : [],
3 | "status" : "UNKNOWN_ERROR"
4 | }
--------------------------------------------------------------------------------
/test/fixtures/responses/google/zero_results.json:
--------------------------------------------------------------------------------
1 | {
2 | "results" : [],
3 | "status" : "ZERO_RESULTS"
4 | }
--------------------------------------------------------------------------------
/test/fixtures/responses/local_search_maps/success.txt:
--------------------------------------------------------------------------------
1 | map.centerAndZoom(new GPoint(-0.130427, 51.510036), 4);
2 |
--------------------------------------------------------------------------------
/test/fixtures/responses/freethepostcode/success.txt:
--------------------------------------------------------------------------------
1 | # looking up postcode w12 1aa
2 | 51.503172 -0.241641 W12 9AE
3 |
--------------------------------------------------------------------------------
/test/fixtures/responses/simple_geo/error.json:
--------------------------------------------------------------------------------
1 | {
2 | "code":500,
3 | "message":"Internal Server Error"
4 | }
--------------------------------------------------------------------------------
/lib/graticule/version.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | module Graticule
3 | VERSION = '2.7.1' unless defined?(::Graticule::VERSION)
4 | end
5 |
--------------------------------------------------------------------------------
/test/fixtures/responses/host_ip/private.txt:
--------------------------------------------------------------------------------
1 | Country: (Private Address) (XX)
2 | City: (Private Address)
3 | Latitude:
4 | Longitude:
5 |
--------------------------------------------------------------------------------
/test/fixtures/responses/host_ip/unknown.txt:
--------------------------------------------------------------------------------
1 | Country: (Unknown Country?) (XX)
2 | City: (Unknown City?)
3 | Latitude:
4 | Longitude:
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | Gemfile.lock
2 | test/config.yml
3 | *.gem
4 | pkg
5 | coverage
6 | rdoc
7 | .DS_Store
8 | .*.swp
9 | .*.swo
10 | .rvmrc
11 |
--------------------------------------------------------------------------------
/test/fixtures/responses/host_ip/success.txt:
--------------------------------------------------------------------------------
1 | Country: UNITED STATES (US)
2 | City: Mountain View, CA
3 | Latitude: 37.402
4 | Longitude: -122.078
5 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gemspec
4 |
5 | gem 'mocha'
6 | gem 'rake'
7 | gem 'rdoc'
8 | gem "minitest", "~> 4.0"
9 | gem "test-unit"
--------------------------------------------------------------------------------
/test/fixtures/responses/yandex/badkey.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 401
4 | invalid key
5 |
6 |
--------------------------------------------------------------------------------
/test/fixtures/responses/geonames/missing.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/test/fixtures/responses/geonames/unknown.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/test/fixtures/responses/yahoo/unknown_address.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | The following errors were detected:
4 | unable to parse location
5 |
6 |
7 |
--------------------------------------------------------------------------------
/test/fixtures/responses/multimap/no_matches.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/fixtures/responses/geocoder_ca/success.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 45.418076
4 | -75.693293
5 | K2P1P6
6 |
7 | 200
8 | MUTCALF
9 | ottawa
10 | ON
11 |
12 |
13 |
--------------------------------------------------------------------------------
/test/fixtures/responses/multimap/missing_params.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.autotest:
--------------------------------------------------------------------------------
1 | Autotest.add_hook :initialize do |at|
2 | at.clear_mappings
3 |
4 | at.add_mapping %r%^lib/(.*)\.rb$% do |_, m|
5 | at.files_matching %r%^test/#{m[1]}_test.rb$%
6 | end
7 |
8 | at.add_mapping(%r%^test/.*\.rb$%) {|filename, _| filename }
9 |
10 | at.add_mapping %r%^test/fixtures/(.*)s.yml% do |_, _|
11 | at.files_matching %r%^test/.*\.rb$%
12 | end
13 | end
--------------------------------------------------------------------------------
/test/fixtures/responses/mapbox/no_results.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "FeatureCollection",
3 | "query": [
4 | "asdfjkl"
5 | ],
6 | "features": [],
7 | "attribution": "NOTICE: © 2017 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained."
8 | }
9 |
--------------------------------------------------------------------------------
/test/fixtures/responses/mapbox/empty_results.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "FeatureCollection",
3 | "query": [
4 | "asdfjkl"
5 | ],
6 | "features": [],
7 | "attribution": "NOTICE: © 2017 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained."
8 | }
9 |
--------------------------------------------------------------------------------
/lib/graticule/core_ext.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | module Graticule
3 | module RadiansAndDegrees
4 | # Convert from degrees to radians
5 | def to_radians
6 | ( self / 360.0 ) * Math::PI * 2
7 | end
8 |
9 | # Convert from radians to degrees
10 | def to_degrees
11 | ( self * 360.0 ) / Math::PI / 2
12 | end
13 | end
14 | end
15 |
16 | Numeric.send :include, Graticule::RadiansAndDegrees
17 |
--------------------------------------------------------------------------------
/test/fixtures/responses/geonames/success.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | US
5 | United States
6 | 41.85
7 | -87.65
8 | America/Chicago
9 | -5.0
10 | -6.0
11 | -6.0
12 | 2009-10-07 15:08
13 |
14 |
15 |
--------------------------------------------------------------------------------
/test/fixtures/responses/geocoder_us/success.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1600 Pennsylvania Ave NW, Washington DC 20502
5 | -77.037684
6 | 38.898748
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | rvm:
3 | - 2.1
4 | - 2.2.4
5 | - 2.3.0
6 | - ruby-head
7 | before_install:
8 | - sudo apt-get install libxslt-dev libxml2-dev
9 | - gem install bundler
10 | before_script:
11 | - cp test/config.yml.default test/config.yml
12 | only:
13 | - master
14 | matrix:
15 | allow_failures:
16 | - rvm: ruby-head
17 | notifications:
18 | email: false
19 | webhooks:
20 | urls:
21 | - http://buildlight.collectiveidea.com/
22 | on_start: always
23 |
--------------------------------------------------------------------------------
/lib/graticule/distance.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | module Graticule
3 | module Distance
4 |
5 | EARTH_RADIUS = { :kilometers => 6378.135, :miles => 3963.1676 }
6 | # WGS-84 numbers
7 | EARTH_MAJOR_AXIS_RADIUS = { :kilometers => 6378.137, :miles => 3963.19059 }
8 | EARTH_MINOR_AXIS_RADIUS = { :kilometers => 6356.7523142, :miles => 3949.90276 }
9 |
10 | class DistanceFormula
11 | include Math
12 | extend Math
13 |
14 | def initialize
15 | raise NotImplementedError
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/graticule/geocoder/bogus.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | module Graticule #:nodoc:
3 | module Geocoder #:nodoc:
4 | # Bogus geocoder that can be used for test purposes
5 | class Bogus
6 | # A queue of canned responses
7 | class_attribute :responses
8 | self.responses = []
9 |
10 | # A default location to use if the responses queue is empty
11 | class_attribute :default
12 |
13 | def locate(address)
14 | responses.shift || default || Location.new(:street => address)
15 | end
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/graticule/geocoder.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'happymapper'
3 |
4 | module Graticule
5 |
6 | # Get a geocoder for the given service
7 | #
8 | # geocoder = Graticule.service(:google).new "api_key"
9 | #
10 | # See the documentation for your specific geocoder for more information
11 | #
12 | def self.service(name)
13 | Geocoder.const_get name.to_s.camelize
14 | end
15 |
16 | # Base error class
17 | class Error < RuntimeError; end
18 | class CredentialsError < Error; end
19 |
20 | # Raised when you try to locate an invalid address.
21 | class AddressError < Error; end
22 |
23 | end
--------------------------------------------------------------------------------
/test/graticule/geocoder_test.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'test_helper'
3 |
4 | module Graticule
5 | class GeocoderTest < Test::Unit::TestCase
6 |
7 | def test_bogus_service
8 | assert_equal Geocoder::Bogus, Graticule.service(:bogus)
9 | end
10 |
11 | def test_yahoo_service
12 | assert_equal Geocoder::Yahoo, Graticule.service(:yahoo)
13 | end
14 |
15 | def test_google_service
16 | assert_equal Geocoder::Google, Graticule.service(:google)
17 | end
18 |
19 | def test_geocoder_us_service
20 | assert_equal Geocoder::GeocoderUs, Graticule.service(:geocoder_us)
21 | end
22 |
23 | end
24 | end
--------------------------------------------------------------------------------
/test/fixtures/responses/yahoo/success.xml:
--------------------------------------------------------------------------------
1 |
2 | 37.416397 -122.025055 701 1st Ave Sunnyvale CA 94089-1019 US
3 |
4 |
--------------------------------------------------------------------------------
/test/fixtures/responses/multimap/success.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 | Oxford Street
7 |
8 | LONDON
9 |
10 | W1
11 | Oxford Street, LONDON, W1
12 | GB
13 |
14 |
15 | 51.51452
16 | -0.14839
17 |
18 |
19 |
--------------------------------------------------------------------------------
/test/graticule/geocoder/local_search_maps_test.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'test_helper'
3 |
4 | module Graticule
5 | module Geocoder
6 | class LocalSearchMapsTest < Test::Unit::TestCase
7 |
8 | def setup
9 | @geocoder = LocalSearchMaps.new
10 | URI::HTTP.responses = []
11 | URI::HTTP.uris = []
12 | end
13 |
14 | def test_success
15 | prepare_response :success
16 |
17 | location = Location.new :latitude => 51.510036, :longitude => -0.130427
18 |
19 | assert_equal location, @geocoder.locate(:street => '48 Leicester Square',
20 | :locality => 'London', :country => 'UK')
21 | end
22 |
23 | private
24 |
25 | def prepare_response(id = :success)
26 | URI::HTTP.responses << response('local_search_maps', id, 'txt')
27 | end
28 |
29 | end
30 | end
31 | end
--------------------------------------------------------------------------------
/test/graticule/geocoder/google_signed_test.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'test_helper'
3 |
4 | class GoogleTest < Test::Unit::TestCase
5 | def test_url_is_signed_for_business_accounts
6 | geocoder = Graticule.service(:google).new("e7-fake-account-R911GuLecpVqA=", 'gme-example')
7 | url = geocoder.send :make_url, :address => 'New York'
8 | expected = "https://maps.googleapis.com/maps/api/geocode/json?address=New%20York&client=gme-example&sensor=false&signature=EJNTEh9SqstO1FLcbFsQ0aJrWHA="
9 | assert_equal expected, url.to_s
10 | end
11 |
12 | def test_url_is_not_signed_for_normal_accounts
13 | geocoder = Graticule.service(:google).new()
14 | url = geocoder.send :make_url, :address => 'New York'
15 | expected = "https://maps.googleapis.com/maps/api/geocode/json?address=New%20York&sensor=false"
16 | assert_equal expected, url.to_s
17 | end
18 | end
19 |
20 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | $:.unshift(File.dirname(__FILE__) + '/../lib')
3 |
4 | require 'rubygems'
5 | require 'bundler/setup'
6 | require 'test/unit'
7 | require 'graticule'
8 | require 'mocha/setup'
9 | require 'mocks/uri'
10 |
11 | TEST_RESPONSE_PATH = File.dirname(__FILE__) + '/fixtures/responses'
12 |
13 | module Test
14 | module Unit
15 | module Assertions
16 |
17 | private
18 | def response(geocoder, response, extension = 'xml')
19 | clean_backtrace do
20 | File.read(File.dirname(__FILE__) + "/fixtures/responses/#{geocoder}/#{response}.#{extension}")
21 | end
22 | end
23 |
24 | def clean_backtrace(&block)
25 | yield
26 | rescue AssertionFailedError => e
27 | path = File.expand_path(__FILE__)
28 | raise AssertionFailedError, e.message, e.backtrace.reject { |line| File.expand_path(line) =~ /#{path}/ }
29 | end
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/test/graticule/geocoder/freethepostcode_test.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'test_helper'
3 |
4 | module Graticule
5 | module Geocoder
6 | class FreeThePostcodeTest < Test::Unit::TestCase
7 |
8 | def setup
9 | URI::HTTP.responses = []
10 | URI::HTTP.uris = []
11 | @geocoder = FreeThePostcode.new
12 | end
13 |
14 | def test_success
15 | return unless prepare_response(:success)
16 |
17 | location = Location.new(
18 | :latitude => 51.503172,
19 | :longitude => -0.241641)
20 |
21 | assert_equal location, @geocoder.locate('W1A 1AA')
22 | end
23 |
24 | def test_locate_unknown_address
25 | return unless prepare_response(:not_found)
26 | assert_raises(AddressError) { @geocoder.locate 'Z12 9pp' }
27 | end
28 |
29 | protected
30 |
31 | def prepare_response(id = :success)
32 | URI::HTTP.responses << response('freethepostcode', id, 'txt')
33 | end
34 |
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/graticule.gemspec:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | require File.expand_path("../lib/graticule/version.rb", __FILE__)
4 |
5 | Gem::Specification.new do |spec|
6 | spec.name = "graticule"
7 | spec.version = Graticule::VERSION
8 |
9 | spec.authors = ["Brandon Keepers", "Daniel Morrison", "Jason Roelofs", "Collective Idea"]
10 | spec.email = ["brandon@opensoul.org", "daniel@collectiveidea.com", "jasongroelofs@gmail.com", "code@collectiveidea.com"]
11 | spec.description = "Graticule is a geocoding API that provides a common interface to all the popular services, including Google, Yahoo, Geocoder.us, and MetaCarta."
12 | spec.summary = "API for using all the popular geocoding services"
13 | spec.homepage = "https://github.com/collectiveidea/graticule"
14 | spec.license = "MIT"
15 |
16 | spec.files = `git ls-files`.split($\)
17 | spec.test_files = spec.files.grep(/^test/)
18 | spec.executables = ["geocode"]
19 | spec.require_paths = ["lib"]
20 |
21 | spec.add_dependency "activesupport"
22 | spec.add_dependency "i18n"
23 | spec.add_dependency "nokogiri-happymapper", ">= 0.5.9"
24 | end
25 |
26 |
--------------------------------------------------------------------------------
/test/graticule/geocoder/host_ip_test.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'test_helper'
3 |
4 | module Graticule
5 | module Geocoder
6 | class HostIpTest < Test::Unit::TestCase
7 |
8 | def setup
9 | @geocoder = HostIp.new
10 | URI::HTTP.responses = []
11 | URI::HTTP.uris = []
12 | end
13 |
14 | def test_success
15 | prepare_response :success
16 |
17 | location = Location.new :country => 'US', :locality => 'Mountain View',
18 | :region => 'CA', :latitude => 37.402, :longitude => -122.078
19 |
20 | assert_equal location, @geocoder.locate('64.233.167.99')
21 | end
22 |
23 | def test_unknown
24 | prepare_response :unknown
25 | assert_raises(AddressError) { @geocoder.locate('127.0.0.1') }
26 | end
27 |
28 | def test_private_ip
29 | prepare_response :private
30 | assert_raises(AddressError) { @geocoder.locate('127.0.0.1') }
31 | end
32 |
33 | private
34 |
35 | def prepare_response(id = :success)
36 | URI::HTTP.responses << response('host_ip', id, 'txt')
37 | end
38 |
39 | end
40 | end
41 | end
--------------------------------------------------------------------------------
/test/graticule/geocoder/yandex_test.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'test_helper'
3 |
4 | module Graticule
5 | module Geocoder
6 | class YandexTest < Test::Unit::TestCase
7 | def setup
8 | URI::HTTP.responses = []
9 | URI::HTTP.uris = []
10 | @geocoder = Yandex.new('APP_ID')
11 | end
12 |
13 | def test_success
14 | return unless prepare_response(:success)
15 |
16 | location = Location.new(
17 | :street => "Моховая улица",
18 | :locality => "Москва",
19 | :country => "RU",
20 | :longitude => 37.612281,
21 | :latitude => 55.753342,
22 | :precision => :address
23 | )
24 | assert_equal location, @geocoder.locate('Россия, Москва, ул. Моховая, д.18')
25 | end
26 |
27 | def test_bad_key
28 | return unless prepare_response(:badkey)
29 |
30 | assert_raises(CredentialsError) { @geocoder.locate('x') }
31 | end
32 |
33 | protected
34 |
35 | def prepare_response(id = :success)
36 | URI::HTTP.responses << response('yandex', id)
37 | end
38 |
39 | end
40 | end
41 | end
42 |
43 |
--------------------------------------------------------------------------------
/test/graticule/precision_test.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'test_helper'
3 |
4 | module Graticule
5 | class PrecisionTest < Test::Unit::TestCase
6 | def test_constants_exist
7 | %w(
8 | Unknown
9 | Country
10 | Region
11 | Locality
12 | PostalCode
13 | Street
14 | Address
15 | Premise
16 | ).each do |const|
17 | assert Precision.const_defined?(const), "Can't find #{const}"
18 | end
19 | end
20 |
21 | def test_can_compare_precisions
22 | assert Precision::Country < Precision::Region
23 | assert Precision::Country > Precision::Unknown
24 | assert Precision::Country == Precision::Country
25 | assert Precision::Country == Precision.new(:country)
26 | assert Precision::Country != Precision::Premise
27 | end
28 |
29 | def test_can_compare_against_symbols
30 | assert Precision::Country < :region
31 | end
32 |
33 | def test_can_compare_against_symbols
34 | assert_raise(ArgumentError) { Precision::Unknown > :foo }
35 | assert_raise(ArgumentError) { Precision::Unknown == :bar }
36 | end
37 | end
38 | end
--------------------------------------------------------------------------------
/lib/graticule/geocoder/freethepostcode.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | module Graticule #:nodoc:
3 | module Geocoder #:nodoc:
4 |
5 | # freethepostcode.org (http://www.freethepostcode.org/) is a
6 | # free service to convert UK postcodes into geolocation data.
7 | #
8 | # gg = Graticule.service(:FreeThePostcode).new
9 | # location = gg.locate 'W1A 1AA'
10 | # location.coordinates
11 | # #=> [51.52093, -0.13714]
12 | class FreeThePostcode < Base
13 |
14 | def initialize
15 | @url = URI.parse 'http://www.freethepostcode.org/geocode'
16 | end
17 |
18 | def locate(postcode)
19 | get :postcode => postcode
20 | end
21 |
22 | private
23 |
24 | def prepare_response(response)
25 | response.split("\n")[1]
26 | end
27 |
28 | def parse_response(response)
29 | data = response.split
30 | Location.new(:latitude => data[0].to_f, :longitude => data[1].to_f, :precision => :unknown)
31 | end
32 |
33 | def check_error(response)
34 | raise AddressError, 'unknown address' if response.blank?
35 | end
36 |
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/test/graticule/geocoder/mapbox_test.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'test_helper'
3 |
4 | module Graticule
5 | module Geocoder
6 | class MapboxTest < Test::Unit::TestCase
7 | def setup
8 | URI::HTTP.responses = []
9 | URI::HTTP.uris = []
10 | @geocoder = Mapbox.new("api_key")
11 | end
12 |
13 | def test_locate_success
14 | prepare_response(:success)
15 |
16 | expected = Location.new(
17 | :latitude => 37.331524,
18 | :longitude => -122.03023,
19 | )
20 |
21 | actual = @geocoder.locate("1 Infinite Loop, Cupertino, CA")
22 |
23 | assert_equal(expected, actual)
24 | end
25 |
26 | def test_locate_not_found
27 | prepare_response(:empty_results)
28 |
29 | assert_raises(AddressError) { @geocoder.locate 'asdfjkl' }
30 | end
31 |
32 | def test_no_results_returned
33 | prepare_response(:no_results)
34 |
35 | assert_raises(AddressError) { @geocoder.locate 'asdfjkl' }
36 | end
37 |
38 | protected
39 |
40 | def prepare_response(id = :success)
41 | URI::HTTP.responses << response('mapbox', id, 'json')
42 | end
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/test/graticule/geocoder/geocoder_ca_test.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'test_helper'
3 |
4 | module Graticule
5 | module Geocoder
6 | class GeocoderCaTest < Test::Unit::TestCase
7 |
8 | def setup
9 | URI::HTTP.responses = []
10 | URI::HTTP.uris = []
11 |
12 | @geocoder = GeocoderCa.new
13 | @location = Location.new(
14 | :latitude => 45.418076,
15 | :longitude => -75.693293,
16 | :locality => "ottawa",
17 | :precision => :unknown,
18 | :region => "ON",
19 | :street => "200 MUTCALF "
20 | )
21 | end
22 |
23 | def test_success
24 | prepare_response(:success)
25 | assert_equal @location, @geocoder.locate('200 mutcalf, ottawa on')
26 | end
27 |
28 | def test_url
29 | prepare_response(:success)
30 | @geocoder.locate('200 mutcalf, ottawa on')
31 | assert_equal 'http://geocoder.ca/?geoit=XML&locate=200%20mutcalf,%20ottawa%20on&showpostal=1&standard=1',
32 | URI::HTTP.uris.first
33 | end
34 |
35 | protected
36 | def prepare_response(id)
37 | URI::HTTP.responses << response('geocoder_ca', id)
38 | end
39 |
40 | end
41 | end
42 | end
--------------------------------------------------------------------------------
/site/stylesheets/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: "Lucida Grande", Helvetica, Arial, sans-serif;
3 | font-size: 76%;
4 | background: #2A2A2A;
5 | margin: 0;
6 | padding: 0;
7 | }
8 |
9 | #collectiveidea {
10 | border-bottom: 1px solid #444;
11 | }
12 |
13 | a {
14 | color: #2D5385;
15 | }
16 |
17 | #main {
18 | background-color: #FFF;
19 | width: 700px;
20 | margin: 0 auto;
21 | border: 5px #CCC;
22 | border-left-style: solid;
23 | border-right-style: solid;
24 | padding: 0 1em;
25 | }
26 |
27 | #header {
28 | position: relative;
29 | }
30 |
31 | #header h1 {
32 | margin: 0;
33 | padding: 0.5em 0;
34 | color: #2D5385;
35 | border-bottom: 1px solid #999;
36 | }
37 |
38 | #header h1 a {
39 | text-decoration: none;
40 | }
41 |
42 | #nav {
43 | list-style: none;
44 | position: absolute;
45 | right: 0;
46 | top: 0.6em;
47 | }
48 | #nav li {
49 | display: inline;
50 | padding: 0 0.5em;
51 | }
52 |
53 | #content {
54 | padding: 1em 0;
55 | }
56 |
57 | dl {
58 | background-color: #DDD;
59 | padding: 1em;
60 | border: 1px solid #CCC;
61 | }
62 | dl .pronunciation {
63 | color: #C00;
64 | }
65 | dl .description {
66 | text-transform: uppercase;
67 | font-size: 0.8em;
68 | font-family: fixed;
69 | }
70 |
--------------------------------------------------------------------------------
/test/mocks/uri.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'uri/http'
3 | require 'open-uri'
4 |
5 | ##
6 | # This stub overrides OpenURI's open method to allow programs that use OpenURI
7 | # to be easily tested.
8 | #
9 | # == Usage
10 | #
11 | # require 'rc_rest/uri_stub'
12 | #
13 | # class TestMyClass < Test::Unit::TestCase
14 | #
15 | # def setup
16 | # URI::HTTP.responses = []
17 | # URI::HTTP.uris = []
18 | #
19 | # @obj = MyClass.new
20 | # end
21 | #
22 | # def test_my_method
23 | # URI::HTTP.responses << 'some text open would ordinarily return'
24 | #
25 | # result = @obj.my_method
26 | #
27 | # assert_equal :something_meaninfgul, result
28 | #
29 | # assert_equal true, URI::HTTP.responses.empty?
30 | # assert_equal 1, URI::HTTP.uris.length
31 | # assert_equal 'http://example.com/path', URI::HTTP.uris.first
32 | # end
33 | #
34 | # end
35 |
36 | class URI::HTTP # :nodoc:
37 |
38 | class << self
39 | attr_accessor :responses, :uris
40 | end
41 |
42 | alias original_open open
43 |
44 | def open(*args)
45 | self.class.uris << self.to_s
46 | io = StringIO.new(self.class.responses.shift)
47 | OpenURI::Meta.init(io)
48 | yield io if block_given?
49 | io
50 | end
51 |
52 | end
53 |
54 |
--------------------------------------------------------------------------------
/lib/graticule/geocoder/host_ip.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'yaml'
3 |
4 | module Graticule #:nodoc:
5 | module Geocoder #:nodoc:
6 |
7 | class HostIp < Base
8 |
9 | def initialize
10 | @url = URI.parse 'http://api.hostip.info/get_html.php'
11 | end
12 |
13 | # Geocode an IP address using http://hostip.info
14 | def locate(address)
15 | get :ip => address, :position => true
16 | end
17 |
18 | private
19 |
20 | def prepare_response(response)
21 | # add new line so YAML.load doesn't puke
22 | YAML.load(response + "\n")
23 | end
24 |
25 | def parse_response(response) #:nodoc:
26 | Location.new.tap do |location|
27 | location.latitude = response['Latitude']
28 | location.longitude = response['Longitude']
29 | location.locality, location.region = response['City'].split(', ')
30 | country = response['Country'].match(/\((\w+)\)$/)
31 | location.country = country[1] if country
32 | end
33 | end
34 |
35 | def check_error(response) #:nodoc:
36 | raise AddressError, 'Unknown' if response['City'] =~ /Unknown City/
37 | raise AddressError, 'Private Address' if response['City'] =~ /Private Address/
38 | end
39 |
40 | end
41 | end
42 | end
--------------------------------------------------------------------------------
/test/fixtures/responses/google/country.json:
--------------------------------------------------------------------------------
1 | {
2 | "results" : [
3 | {
4 | "address_components" : [
5 | {
6 | "long_name" : "United States",
7 | "short_name" : "US",
8 | "types" : [ "country", "political" ]
9 | }
10 | ],
11 | "formatted_address" : "United States",
12 | "geometry" : {
13 | "bounds" : {
14 | "northeast" : {
15 | "lat" : 71.3898880,
16 | "lng" : -66.94976079999999
17 | },
18 | "southwest" : {
19 | "lat" : 18.91106420,
20 | "lng" : 172.45469660
21 | }
22 | },
23 | "location" : {
24 | "lat" : 37.090240,
25 | "lng" : -95.7128910
26 | },
27 | "location_type" : "APPROXIMATE",
28 | "viewport" : {
29 | "northeast" : {
30 | "lat" : 49.380,
31 | "lng" : -66.940
32 | },
33 | "southwest" : {
34 | "lat" : 25.820,
35 | "lng" : -124.390
36 | }
37 | }
38 | },
39 | "types" : [ "country", "political" ]
40 | }
41 | ],
42 | "status" : "OK"
43 | }
--------------------------------------------------------------------------------
/lib/graticule/precision.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | module Graticule
3 |
4 | # Used to compare the precision of different geocoded locations
5 | class Precision
6 | include Comparable
7 | attr_reader :name
8 |
9 | NAMES = [
10 | :point,
11 | :unknown,
12 | :country,
13 | :region,
14 | :locality,
15 | :postal_code,
16 | :street,
17 | :address,
18 | :premise
19 | ]
20 |
21 | def initialize(name)
22 | @name = name.to_sym
23 | raise ArgumentError, "#{name} is not a valid precision. Use one of #{NAMES.inspect}" unless NAMES.include?(@name)
24 | end
25 |
26 | Unknown = Precision.new(:unknown)
27 | Point = Precision.new(:point)
28 | Country = Precision.new(:country)
29 | Region = Precision.new(:region)
30 | Locality = Precision.new(:locality)
31 | PostalCode = Precision.new(:postal_code)
32 | Street = Precision.new(:street)
33 | Address = Precision.new(:address)
34 | Premise = Precision.new(:premise)
35 |
36 | def to_s
37 | @name.to_s
38 | end
39 |
40 | def <=>(other)
41 | other = Precision.new(other) unless other.is_a?(Precision)
42 | NAMES.index(self.name) <=> NAMES.index(other.name)
43 | end
44 |
45 | def ==(other)
46 | (self <=> other) == 0
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/test/graticule/geocoder/geocoder_us_test.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'test_helper'
3 |
4 | module Graticule
5 | module Geocoder
6 | class GeocoderUsTest < Test::Unit::TestCase
7 |
8 | def setup
9 | URI::HTTP.responses = []
10 | URI::HTTP.uris = []
11 |
12 | @geocoder = GeocoderUs.new
13 | @location = Location.new(
14 | :street => "1600 Pennsylvania Ave NW, Washington DC 20502",
15 | :longitude => -77.037684,
16 | :latitude => 38.898748
17 | )
18 | end
19 |
20 | def test_success
21 | prepare_response(:success)
22 | assert_equal @location, @geocoder.locate('1600 Pennsylvania Ave, Washington DC')
23 | end
24 |
25 | def test_url
26 | prepare_response(:success)
27 | @geocoder.locate('1600 Pennsylvania Ave, Washington DC')
28 | assert_equal 'http://rpc.geocoder.us/service/rest/geocode?address=1600%20Pennsylvania%20Ave,%20Washington%20DC',
29 | URI::HTTP.uris.first
30 | end
31 |
32 | def test_locate_bad_address
33 | prepare_response(:unknown)
34 | assert_raises(AddressError) { @geocoder.locate('yuck') }
35 | end
36 |
37 | protected
38 | def prepare_response(id)
39 | URI::HTTP.responses << response('geocoder_us', id)
40 | end
41 |
42 | end
43 | end
44 | end
--------------------------------------------------------------------------------
/lib/graticule.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | $:.unshift(File.dirname(__FILE__))
3 |
4 | require 'active_support/inflections'
5 | require 'active_support/core_ext/class/attribute'
6 | require 'active_support/core_ext/hash/keys'
7 | require 'active_support/core_ext/object/blank'
8 | require 'active_support/core_ext/object/with_options'
9 | require 'active_support/core_ext/string/inflections'
10 |
11 | require 'graticule/version'
12 | require 'graticule/core_ext'
13 | require 'graticule/location'
14 | require 'graticule/precision'
15 | require 'graticule/geocoder'
16 | require 'graticule/geocoder/base'
17 | require 'graticule/geocoder/bogus'
18 | require 'graticule/geocoder/google'
19 | require 'graticule/geocoder/yandex'
20 | require 'graticule/geocoder/host_ip'
21 | require 'graticule/geocoder/multi'
22 | require 'graticule/geocoder/yahoo'
23 | require 'graticule/geocoder/geocoder_ca'
24 | require 'graticule/geocoder/geocoder_us'
25 | require 'graticule/geocoder/geonames'
26 | require 'graticule/geocoder/local_search_maps'
27 | require 'graticule/geocoder/freethepostcode'
28 | require 'graticule/geocoder/multimap'
29 | require 'graticule/geocoder/mapquest'
30 | require 'graticule/geocoder/simple_geo'
31 | require 'graticule/geocoder/mapbox'
32 | require 'graticule/distance'
33 | require 'graticule/distance/haversine'
34 | require 'graticule/distance/spherical'
35 | require 'graticule/distance/vincenty'
36 |
--------------------------------------------------------------------------------
/test/graticule/geocoder/simple_geo_test.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'test_helper'
3 |
4 | module Graticule
5 | module Geocoder
6 | class SimpleGeoTest < Test::Unit::TestCase
7 |
8 | def setup
9 | URI::HTTP.responses = []
10 | URI::HTTP.uris = []
11 | @geocoder = SimpleGeo.new('TOKEN')
12 | end
13 |
14 | def test_success
15 | return unless prepare_response(:success)
16 |
17 | location = Location.new(
18 | :longitude => -117.373982,
19 | :latitude => 34.482358,
20 | :precision => :unknown
21 | )
22 | assert_equal location, @geocoder.locate('1600 Amphitheatre Parkway, Mountain View, CA')
23 | end
24 |
25 | def test_error
26 | prepare_response :error
27 | assert_raises(Error) { @geocoder.locate('') }
28 | end
29 |
30 | def test_time_zone
31 | URI::HTTP.uris = []
32 | URI::HTTP.responses = []
33 | URI::HTTP.responses << response('simple_geo', :success, 'json')
34 | los_angeles = Location.new(:latitude => 34.48, :longitude => -117.37)
35 | assert_equal 'America/Los_Angeles', @geocoder.time_zone(los_angeles)
36 | end
37 |
38 | private
39 |
40 | def prepare_response(id = :success)
41 | URI::HTTP.responses << response('simple_geo', id, 'json')
42 | end
43 | end
44 | end
45 | end
--------------------------------------------------------------------------------
/lib/graticule/distance/haversine.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | module Graticule
3 | module Distance
4 | #
5 | # The Haversine Formula works better at small distances than the Spherical Law of Cosines
6 | #
7 | # Thanks to Chris Veness (http://www.movable-type.co.uk/scripts/LatLong.html)
8 | # for distance formulas.
9 | #
10 | class Haversine < DistanceFormula
11 |
12 | # Calculate the distance between two Locations using the Haversine formula
13 | #
14 | # Graticule::Distance::Haversine.distance(
15 | # Graticule::Location.new(:latitude => 42.7654, :longitude => -86.1085),
16 | # Graticule::Location.new(:latitude => 41.849838, :longitude => -87.648193)
17 | # )
18 | # #=> 101.061720831836
19 | #
20 | def self.distance(from, to, units = :miles)
21 | from_longitude = from.longitude.to_radians
22 | from_latitude = from.latitude.to_radians
23 | to_longitude = to.longitude.to_radians
24 | to_latitude = to.latitude.to_radians
25 |
26 | latitude_delta = to_latitude - from_latitude
27 | longitude_delta = to_longitude - from_longitude
28 |
29 | a = sin(latitude_delta/2)**2 +
30 | cos(from_latitude) *
31 | cos(to_latitude) *
32 | sin(longitude_delta/2)**2
33 |
34 | c = 2 * atan2(sqrt(a), sqrt(1-a))
35 |
36 | d = EARTH_RADIUS[units.to_sym] * c
37 | end
38 |
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/lib/graticule/geocoder/mapbox.rb:
--------------------------------------------------------------------------------
1 | module Graticule #:nodoc:
2 | module Geocoder #:nodoc:
3 | class Mapbox < Base
4 | BASE_URL = "http://api.mapbox.com/geocoding/v5/mapbox.places"
5 |
6 | def initialize(api_key)
7 | @api_key = api_key
8 | end
9 |
10 | def locate(address)
11 | get :q => address
12 | end
13 |
14 | protected
15 |
16 | class Result
17 | attr_accessor :lat, :lon, :precision
18 |
19 | def initialize(attributes)
20 | self.precision = ::Graticule::Precision::Unknown
21 | self.lon = attributes["center"][0]
22 | self.lat = attributes["center"][1]
23 | end
24 | end
25 |
26 | def make_url(params)
27 | query = URI.escape(params[:q].to_s)
28 | url = "#{BASE_URL}/#{query}.json?access_token=#{@api_key}"
29 |
30 | URI.parse(url)
31 | end
32 |
33 | def check_error(response)
34 | raise AddressError, 'unknown address' if (response["features"].nil? || response["features"].empty?)
35 | end
36 |
37 | def prepare_response(response)
38 | JSON.parse(response)
39 | end
40 |
41 | def parse_response(response)
42 | # Pull data from the first result since we get a bunch
43 | result = Result.new(response["features"][0])
44 |
45 | Location.new(
46 | :latitude => result.lat,
47 | :longitude => result.lon,
48 | )
49 | end
50 | end
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/test/fixtures/responses/google/region.json:
--------------------------------------------------------------------------------
1 | {
2 | "results" : [
3 | {
4 | "address_components" : [
5 | {
6 | "long_name" : "California",
7 | "short_name" : "CA",
8 | "types" : [ "administrative_area_level_1", "political" ]
9 | },
10 | {
11 | "long_name" : "United States",
12 | "short_name" : "US",
13 | "types" : [ "country", "political" ]
14 | }
15 | ],
16 | "formatted_address" : "California, USA",
17 | "geometry" : {
18 | "bounds" : {
19 | "northeast" : {
20 | "lat" : 42.00951690,
21 | "lng" : -114.1312110
22 | },
23 | "southwest" : {
24 | "lat" : 32.53423210,
25 | "lng" : -124.40961960
26 | }
27 | },
28 | "location" : {
29 | "lat" : 36.7782610,
30 | "lng" : -119.41793240
31 | },
32 | "location_type" : "APPROXIMATE",
33 | "viewport" : {
34 | "northeast" : {
35 | "lat" : 42.00951690,
36 | "lng" : -114.1312110
37 | },
38 | "southwest" : {
39 | "lat" : 32.53423210,
40 | "lng" : -124.40961960
41 | }
42 | }
43 | },
44 | "types" : [ "administrative_area_level_1", "political" ]
45 | }
46 | ],
47 | "status" : "OK"
48 | }
--------------------------------------------------------------------------------
/test/graticule/geocoder/geonames_test.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'test_helper'
3 |
4 | module Graticule
5 | module Geocoder
6 | class GeonamesTest < Test::Unit::TestCase
7 | def setup
8 | URI::HTTP.responses = []
9 | URI::HTTP.uris = []
10 | @geocoder = Geonames.new("demo")
11 | end
12 |
13 | def test_time_zone
14 | URI::HTTP.uris = []
15 | URI::HTTP.responses = []
16 | URI::HTTP.responses << response('geonames', :success)
17 | chicago = Location.new(:latitude => 41.85, :longitude => -87.65)
18 | assert_equal 'America/Chicago', @geocoder.time_zone(chicago)
19 | end
20 |
21 | # def test_locate_server_error
22 | # return unless prepare_response(:server_error)
23 | # assert_raises(Error) { @geocoder.locate 'x' }
24 | # end
25 | #
26 | # def test_locate_too_many_queries
27 | # return unless prepare_response(:limit)
28 | # assert_raises(CredentialsError) { @geocoder.locate 'x' }
29 | # end
30 | #
31 | # def test_locate_unavailable_address
32 | # return unless prepare_response(:unavailable)
33 | # assert_raises(AddressError) { @geocoder.locate 'x' }
34 | # end
35 | #
36 | # def test_locate_unknown_address
37 | # return unless prepare_response(:unknown_address)
38 | # assert_raises(AddressError) { @geocoder.locate 'x' }
39 | # end
40 |
41 | protected
42 |
43 | def prepare_response(id = :success)
44 | URI::HTTP.responses << response('geonames', id)
45 | end
46 |
47 | end
48 | end
49 | end
--------------------------------------------------------------------------------
/test/graticule/geocoder/yahoo_test.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'test_helper'
3 |
4 | module Graticule
5 | module Geocoder
6 | class YahooTest < Test::Unit::TestCase
7 |
8 | def setup
9 | URI::HTTP.responses = []
10 | URI::HTTP.uris = []
11 | @geocoder = Yahoo.new 'APP_ID'
12 | @location = Location.new(
13 | :street => "701 1st Ave",
14 | :locality => "Sunnyvale",
15 | :region => "CA",
16 | :postal_code => "94089-1019",
17 | :country => "US",
18 | :longitude => -122.025055,
19 | :latitude => 37.416397,
20 | :precision => :address,
21 | :warning => "The exact location could not be found, here is the closest match: 701 First Ave, Sunnyvale, CA 94089"
22 | )
23 | end
24 |
25 | def test_locate
26 | prepare_response(:success)
27 | assert_equal @location, @geocoder.locate('701 First Street, Sunnyvale, CA')
28 | end
29 |
30 | def test_url
31 | prepare_response(:success)
32 | @geocoder.locate('701 First Street, Sunnyvale, CA')
33 | assert_equal 'http://api.local.yahoo.com/MapsService/V1/geocode?appid=APP_ID&location=701%20First%20Street,%20Sunnyvale,%20CA&output=xml',
34 | URI::HTTP.uris.first
35 | end
36 |
37 |
38 | def test_locate_bad_address
39 | prepare_response(:unknown_address)
40 | assert_raise(Error) { @geocoder.locate('yucksthoeusthaoeusnhtaosu') }
41 | end
42 |
43 | protected
44 | def prepare_response(id)
45 | URI::HTTP.responses << response('yahoo', id)
46 | end
47 |
48 | end
49 | end
50 | end
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright 2013 Brandon Keepers, Collective Idea. All rights reserved.
2 |
3 | Original geocoding code:
4 | Copyright 2006 Eric Hodel, The Robot Co-op. All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions
8 | are met:
9 |
10 | 1. Redistributions of source code must retain the above copyright
11 | notice, this list of conditions and the following disclaimer.
12 | 2. Redistributions in binary form must reproduce the above copyright
13 | notice, this list of conditions and the following disclaimer in the
14 | documentation and/or other materials provided with the distribution.
15 | 3. Neither the names of the authors nor the names of their contributors
16 | may be used to endorse or promote products derived from this software
17 | without specific prior written permission.
18 |
19 | THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS
20 | OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE
23 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
24 | OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
25 | OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
26 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
27 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
28 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
29 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
31 |
--------------------------------------------------------------------------------
/lib/graticule/geocoder/geonames.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | module Graticule #:nodoc:
3 | module Geocoder #:nodoc:
4 | class Geonames < Base
5 |
6 | def initialize(username)
7 | @url = URI.parse('http://ws.geonames.org/timezone')
8 | @username = URI.encode(username)
9 | end
10 |
11 | def time_zone(location)
12 | get :formatted => 'true',
13 | :style => 'full',
14 | :lat => location.latitude,
15 | :lng => location.longitude,
16 | :username => @username
17 | end
18 |
19 | private
20 | class Status
21 | include HappyMapper
22 | tag 'status'
23 | attribute :message, String
24 | attribute :value, String
25 | end
26 |
27 | class Response
28 | include HappyMapper
29 | tag 'geonames'
30 | element :timezoneId, String, :deep => true
31 | has_one :status, Status
32 | end
33 |
34 | def prepare_response(xml)
35 | Response.parse(xml, :single => true)
36 | end
37 |
38 | def parse_response(response) #:nodoc:
39 | response.timezoneId
40 | end
41 |
42 | # Extracts and raises an error from +xml+, if any.
43 | def check_error(response) #:nodoc:
44 | if response && response.status
45 | case response.status.value
46 | when 14 then
47 | raise Error, reponse.status.message
48 | when 12 then
49 | raise AddressError, reponse.status.message
50 | else
51 | raise Error, "unknown error #{response.status.message}"
52 | end
53 | end
54 | end
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/lib/graticule/geocoder/local_search_maps.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | module Graticule #:nodoc:
3 | module Geocoder #:nodoc:
4 |
5 | # A library for lookup of coordinates with http://geo.localsearchmaps.com/
6 | #
7 | # See http://emad.fano.us/blog/?p=277
8 | class LocalSearchMaps < Base
9 |
10 | def initialize
11 | @url = URI.parse 'http://geo.localsearchmaps.com/'
12 | end
13 |
14 | # This web service will handle some addresses outside the US
15 | # if given more structured arguments than just a string address
16 | # So allow input as a hash for the different arguments (:city, :country, :zip)
17 | def locate(params)
18 | get params.is_a?(String) ? {:loc => params} : map_attributes(location_from_params(params))
19 | end
20 |
21 | private
22 |
23 | def map_attributes(location)
24 | mapping = {:street => :street, :locality => :city, :region => :state, :postal_code => :zip, :country => :country}
25 | mapping.keys.inject({}) do |result,attribute|
26 | result[mapping[attribute]] = location.attributes[attribute] unless location.attributes[attribute].blank?
27 | result
28 | end
29 | end
30 |
31 | def check_error(js)
32 | case js
33 | when nil
34 | raise AddressError, "Empty Response"
35 | when /location not found/
36 | raise AddressError, 'Location not found'
37 | end
38 | end
39 |
40 | def parse_response(js)
41 | coordinates = js.match(/map.centerAndZoom\(new GPoint\((.+?), (.+?)\)/)
42 | Location.new(:longitude => coordinates[1].to_f, :latitude => coordinates[2].to_f)
43 | end
44 |
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/test/graticule/geocoder/multimap_test.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'test_helper'
3 |
4 | module Graticule
5 | module Geocoder
6 | class MultimapTest < Test::Unit::TestCase
7 |
8 | def setup
9 | URI::HTTP.responses = []
10 | URI::HTTP.uris = []
11 | @geocoder = Multimap.new 'API_KEY'
12 | @location = Location.new(
13 | :street => "Oxford Street",
14 | :locality => "LONDON",
15 | :postal_code => "W1",
16 | :country => "GB",
17 | :longitude => -0.14839,
18 | :latitude => 51.51452,
19 | :precision => :address
20 | )
21 | end
22 |
23 | def test_locate
24 | prepare_response(:success)
25 | assert_equal @location, @geocoder.locate('Oxford Street, LONDON, W1')
26 | end
27 |
28 | def test_url_from_string
29 | prepare_response(:success)
30 | @geocoder.locate('Oxford Street, LONDON, W1')
31 | assert_equal 'http://clients.multimap.com/API/geocode/1.2/API_KEY?qs=Oxford%20Street,%20LONDON,%20W1', URI::HTTP.uris.first
32 | end
33 |
34 | def test_url_from_location
35 | prepare_response(:success)
36 | @geocoder.locate(:street => 'Oxford Street', :locality => 'London')
37 | assert_equal 'http://clients.multimap.com/API/geocode/1.2/API_KEY?city=London&countryCode=&postalCode=®ion=&street=Oxford%20Street', URI::HTTP.uris.first
38 | end
39 |
40 |
41 | def test_locate_bad_address
42 | prepare_response(:no_matches)
43 | assert_raise(Error) { @geocoder.locate('yucksthoeusthaoeusnhtaosu') }
44 | end
45 |
46 | protected
47 | def prepare_response(id)
48 | URI::HTTP.responses << response('multimap', id)
49 | end
50 |
51 | end
52 | end
53 | end
--------------------------------------------------------------------------------
/test/graticule/geocoder/geocoders.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'test_helper'
3 |
4 | module Graticule
5 |
6 | # Generic tests for all geocoders (theoretically)
7 | module GeocodersTestCase
8 |
9 | def test_success
10 | return unless prepare_response(:success)
11 |
12 | location = Location.new(
13 | :street => "1600 Amphitheatre Pkwy",
14 | :city => "Mountain View",
15 | :state => "CA",
16 | :zip => "94043",
17 | :country => "US",
18 | :longitude => -122.083739,
19 | :latitude => 37.423021,
20 | :precision => :address
21 | )
22 | assert_equal location, @geocoder.locate('1600 Amphitheatre Parkway, Mountain View, CA')
23 | end
24 |
25 | def test_bad_key
26 | return unless prepare_response(:badkey)
27 | assert_raises(CredentialsError) { @geocoder.locate('x') }
28 | end
29 |
30 | def test_locate_missing_address
31 | return unless prepare_response(:missing_address)
32 | assert_raises(AddressError) { @geocoder.locate 'x' }
33 | end
34 |
35 | def test_locate_server_error
36 | return unless prepare_response(:server_error)
37 | assert_raises(Error) { @geocoder.locate 'x' }
38 | end
39 |
40 | def test_locate_too_many_queries
41 | return unless prepare_response(:limit)
42 | assert_raises(CredentialsError) { @geocoder.locate 'x' }
43 | end
44 |
45 | def test_locate_unavailable_address
46 | return unless prepare_response(:unavailable)
47 | assert_raises(AddressError) { @geocoder.locate 'x' }
48 | end
49 |
50 | def test_locate_unknown_address
51 | return unless prepare_response(:unknown_address)
52 | assert_raises(AddressError) { @geocoder.locate 'x' }
53 | end
54 |
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/test/config.yml.default:
--------------------------------------------------------------------------------
1 | google:
2 | key: PUT YOUR KEY HERE
3 | responses:
4 | success.xml: "http://maps.google.com/maps/geo?q=1600+amphitheatre+mtn+view+ca&output=xml&key=:key"
5 | badkey.xml: http://maps.google.com/maps/geo?q=1600+amphitheatre+mtn+view+ca&output=xml&key=this_is_a_bad_key
6 | missing_address.xml: "http://maps.google.com/maps/geo?output=xml&key=:key"
7 | unknown_address.xml: "http://maps.google.com/maps/geo?q=xxxxx&output=xml&key=:key"
8 | partial.xml: "http://maps.google.com/maps/geo?q=sf+ca&output=xml&key=:key"
9 | #unavailable.xml: no way to reproduce this
10 | #limit.xml: no way to reproduce this
11 | #server_error.xml: no way to reproduce this
12 | geocoder_us:
13 | responses:
14 | success.xml: http://rpc.geocoder.us/service/rest/geocode?address=1600%20Pennsylvania%20Ave%20NW,%20Washington%20DC
15 | unknown.xml: http://rpc.geocoder.us/service/rest/geocode?address=1600
16 | host_ip:
17 | responses:
18 | private.txt: http://api.hostip.info/get_html.php?ip=192.168.0.1&position=true
19 | success.txt: http://api.hostip.info/get_html.php?ip=64.233.167.99&position=true
20 | unknown.txt: http://api.hostip.info/get_html.php?ip=254.254.254.254&position=true
21 | local_search_maps:
22 | responses:
23 | success.txt: http://geo.localsearchmaps.com/?street=48+Leicester+Square&city=London&country=UK
24 | empty.txt: http://geo.localsearchmaps.com/
25 | not_found.txt: http://geo.localsearchmaps.com/?street=48
26 | yahoo:
27 | key: PUT YOUR KEY HERE
28 | responses:
29 | success.xml: http://api.local.yahoo.com/MapsService/V1/geocode?location=701%20First%20Street,%20Sunnyvale,%20CA&output=xml&appid=:key
30 | unknown_address.xml: http://api.local.yahoo.com/MapsService/V1/geocode?location=thisprobablycantbefound&output=xml&appid=:key
31 | geonames:
32 | responses:
33 | success.xml: http://ws.geonames.org/timezone?formatted=true&lat=41.85&lng=-87.65&style=full
34 |
--------------------------------------------------------------------------------
/test/graticule/geocoder/multi_test.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'test_helper'
3 |
4 | module Graticule
5 | module Geocoder
6 | class MultiTest < Test::Unit::TestCase
7 |
8 | def setup
9 | @mock_geocoders = [mock("geocoder 1"), mock("geocoder 2")]
10 | @mock_geocoders.each {|g| g.stubs(:locate) }
11 | @geocoder = Multi.new(*@mock_geocoders)
12 | end
13 |
14 | def test_locate_calls_each_geocoder_and_raises_error
15 | @mock_geocoders.each do |g|
16 | g.expects(:locate).with('test').raises(Graticule::AddressError)
17 | end
18 | assert_raises(Graticule::AddressError) { @geocoder.locate 'test' }
19 | end
20 |
21 | def test_locate_returns_first_result_without_calling_others
22 | result = mock("result")
23 | @mock_geocoders.first.expects(:locate).returns(result)
24 | @mock_geocoders.last.expects(:locate).never
25 | assert_equal result, @geocoder.locate('test')
26 | end
27 |
28 | def test_locate_with_custom_block
29 | @mock_geocoders.first.expects(:locate).returns(1)
30 | @mock_geocoders.last.expects(:locate).returns(2)
31 | @geocoder = Multi.new(*@mock_geocoders) {|r| r == 2 }
32 | assert_equal 2, @geocoder.locate('test')
33 | end
34 |
35 | def test_locate_with_custom_block_and_no_match
36 | @mock_geocoders.first.expects(:locate).returns(1)
37 | @mock_geocoders.last.expects(:locate).returns(2)
38 | @geocoder = Multi.new(*@mock_geocoders) {|r| r == 3 }
39 | assert_raises(Graticule::AddressError) { @geocoder.locate('test') }
40 | end
41 |
42 | def test_timeout
43 | @mock = @mock_geocoders.first
44 | def @mock.locate(*x)
45 | sleep 1
46 | end
47 | @geocoder = Multi.new(@mock, :timeout => 0.1)
48 | assert_raise(Timeout::Error) { @geocoder.locate('foo') }
49 | end
50 | end
51 | end
52 | end
--------------------------------------------------------------------------------
/test/fixtures/responses/google/locality.json:
--------------------------------------------------------------------------------
1 | {
2 | "results" : [
3 | {
4 | "address_components" : [
5 | {
6 | "long_name" : "San Francisco",
7 | "short_name" : "SF",
8 | "types" : [ "locality", "political" ]
9 | },
10 | {
11 | "long_name" : "San Francisco",
12 | "short_name" : "San Francisco",
13 | "types" : [ "administrative_area_level_2", "political" ]
14 | },
15 | {
16 | "long_name" : "California",
17 | "short_name" : "CA",
18 | "types" : [ "administrative_area_level_1", "political" ]
19 | },
20 | {
21 | "long_name" : "United States",
22 | "short_name" : "US",
23 | "types" : [ "country", "political" ]
24 | }
25 | ],
26 | "formatted_address" : "San Francisco, CA, USA",
27 | "geometry" : {
28 | "bounds" : {
29 | "northeast" : {
30 | "lat" : 37.92977070,
31 | "lng" : -122.32791490
32 | },
33 | "southwest" : {
34 | "lat" : 37.69333540,
35 | "lng" : -123.10777330
36 | }
37 | },
38 | "location" : {
39 | "lat" : 37.77492950,
40 | "lng" : -122.41941550
41 | },
42 | "location_type" : "APPROXIMATE",
43 | "viewport" : {
44 | "northeast" : {
45 | "lat" : 37.8120,
46 | "lng" : -122.34820
47 | },
48 | "southwest" : {
49 | "lat" : 37.70339999999999,
50 | "lng" : -122.5270
51 | }
52 | }
53 | },
54 | "types" : [ "locality", "political" ]
55 | }
56 | ],
57 | "status" : "OK"
58 | }
--------------------------------------------------------------------------------
/test/fixtures/responses/google/street.json:
--------------------------------------------------------------------------------
1 | {
2 | "results" : [
3 | {
4 | "address_components" : [
5 | {
6 | "long_name" : "Amphitheatre Parkway",
7 | "short_name" : "Amphitheatre Pkwy",
8 | "types" : [ "route" ]
9 | },
10 | {
11 | "long_name" : "Mountain View",
12 | "short_name" : "Mountain View",
13 | "types" : [ "locality", "political" ]
14 | },
15 | {
16 | "long_name" : "California",
17 | "short_name" : "CA",
18 | "types" : [ "administrative_area_level_1", "political" ]
19 | },
20 | {
21 | "long_name" : "United States",
22 | "short_name" : "US",
23 | "types" : [ "country", "political" ]
24 | }
25 | ],
26 | "formatted_address" : "Amphitheatre Parkway, Mountain View, CA, USA",
27 | "geometry" : {
28 | "bounds" : {
29 | "northeast" : {
30 | "lat" : 37.4267180,
31 | "lng" : -122.07792340
32 | },
33 | "southwest" : {
34 | "lat" : 37.42070340000001,
35 | "lng" : -122.09319780
36 | }
37 | },
38 | "location" : {
39 | "lat" : 37.42325960000001,
40 | "lng" : -122.08563830
41 | },
42 | "location_type" : "GEOMETRIC_CENTER",
43 | "viewport" : {
44 | "northeast" : {
45 | "lat" : 37.4267180,
46 | "lng" : -122.07792340
47 | },
48 | "southwest" : {
49 | "lat" : 37.42070340000001,
50 | "lng" : -122.09319780
51 | }
52 | }
53 | },
54 | "types" : [ "route" ]
55 | }
56 | ],
57 | "status" : "OK"
58 | }
--------------------------------------------------------------------------------
/lib/graticule/distance/spherical.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | module Graticule
3 | module Distance
4 |
5 | #
6 | # The Spherical Law of Cosines is the simplist though least accurate distance
7 | # formula (earth isn't a perfect sphere).
8 | #
9 | class Spherical < DistanceFormula
10 |
11 | # Calculate the distance between two Locations using the Spherical formula
12 | #
13 | # Graticule::Distance::Spherical.distance(
14 | # Graticule::Location.new(:latitude => 42.7654, :longitude => -86.1085),
15 | # Graticule::Location.new(:latitude => 41.849838, :longitude => -87.648193)
16 | # )
17 | # #=> 101.061720831853
18 | #
19 | def self.distance(from, to, units = :miles)
20 | from_longitude = from.longitude.to_radians
21 | from_latitude = from.latitude.to_radians
22 | to_longitude = to.longitude.to_radians
23 | to_latitude = to.latitude.to_radians
24 |
25 | Math.acos(
26 | Math.sin(from_latitude) *
27 | Math.sin(to_latitude) +
28 |
29 | Math.cos(from_latitude) *
30 | Math.cos(to_latitude) *
31 | Math.cos(to_longitude - from_longitude)
32 | ) * EARTH_RADIUS[units.to_sym]
33 | end
34 |
35 | def self.to_sql(options)
36 | options = {
37 | :units => :miles,
38 | :latitude_column => 'latitude',
39 | :longitude_column => 'longitude'
40 | }.merge(options)
41 | %{(ACOS(LEAST(1,
42 | SIN(RADIANS(#{options[:latitude]})) *
43 | SIN(RADIANS(#{options[:latitude_column]})) +
44 | COS(RADIANS(#{options[:latitude]})) *
45 | COS(RADIANS(#{options[:latitude_column]})) *
46 | COS(RADIANS(#{options[:longitude_column]}) - RADIANS(#{options[:longitude]}))
47 | )) * #{Graticule::Distance::EARTH_RADIUS[options[:units].to_sym]})
48 | }.gsub("\n", '').squeeze(" ")
49 | end
50 | end
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/test/fixtures/responses/mapquest/success.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 0
5 |
6 |
7 | http://api.mqcdn.com/res/mqlogo.gif
8 | © 2013 MapQuest, Inc.
9 | © 2013 MapQuest, Inc.
10 |
11 |
12 |
13 |
14 |
15 | 44 Allen Rd., Lovell, ME 04051
16 |
17 |
18 |
19 | 44 Allen Rd
20 | Lovell
21 | ME
22 | Oxford County
23 | 04051-3919
24 | US
25 | POINT
26 | P1AAA
27 | false
28 | L
29 |
30 |
31 | 44.15175
32 | -70.893
33 |
34 |
35 | 0
36 | s
37 |
38 | 44.15218
39 | -70.89297
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | -1
50 | true
51 | false
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/lib/graticule/geocoder/geocoder_us.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | module Graticule #:nodoc:
3 | module Geocoder #:nodoc:
4 |
5 | # A library for lookup up coordinates with geocoder.us' API.
6 | #
7 | # http://geocoder.us/help/
8 | class GeocoderUs < Base
9 |
10 | # Creates a new GeocoderUs object optionally using +username+ and
11 | # +password+.
12 | #
13 | # You can sign up for a geocoder.us account here:
14 | #
15 | # http://geocoder.us/user/signup
16 | def initialize(user = nil, password = nil)
17 | if user && password
18 | @url = URI.parse 'http://geocoder.us/member/service/rest/geocode'
19 | @url.user = user
20 | @url.password = password
21 | else
22 | @url = URI.parse 'http://rpc.geocoder.us/service/rest/geocode'
23 | end
24 | end
25 |
26 | # Locates +address+ and returns the address' latitude and longitude or
27 | # raises an AddressError.
28 | def locate(address)
29 | get :address => address.is_a?(String) ? address : location_from_params(address).to_s(:country => false)
30 | end
31 |
32 | private
33 | class Point
34 | include HappyMapper
35 | tag 'Point'
36 | namespace 'geo'
37 |
38 | element :description, String, :namespace => 'dc'
39 | element :longitude, Float, :tag => 'long'
40 | element :latitude, Float, :tag => 'lat'
41 | end
42 |
43 | def parse_response(xml) #:nodoc:
44 | point = Point.parse(xml, :single => true)
45 | Location.new(
46 | :street => point.description,
47 | :latitude => point.latitude,
48 | :longitude => point.longitude
49 | )
50 | end
51 |
52 | def check_error(response) #:nodoc:
53 | case response
54 | when /geo:Point/
55 | # success
56 | when /couldn't find this address! sorry/
57 | raise AddressError, response
58 | else
59 | raise Error, response
60 | end
61 | end
62 |
63 | end
64 | end
65 | end
--------------------------------------------------------------------------------
/lib/graticule/geocoder/simple_geo.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'json'
3 |
4 | module Graticule
5 | module Geocoder
6 |
7 | # First you need a SimpleGeo JSONP API key. You can register for one here:
8 | # http://simplegeo.com/docs/clients-code-libraries/javascript-sdk
9 | #
10 | # gg = Graticule.service(:SimpleGeo).new(SIMPLEGEO_TOKEN)
11 | # location = gg.locate '1600 Amphitheater Pkwy, Mountain View, CA'
12 | # location.coordinates
13 | # #=> [37.423111, -122.081783]
14 | #
15 | class SimpleGeo < Base
16 |
17 | def initialize(token)
18 | @token = token
19 | @url = URI.parse 'http://api.simplegeo.com/1.0/context/address.json?'
20 | end
21 |
22 | def locate(query)
23 | get :address => "#{query}"
24 | end
25 |
26 | # reimplement Base#get so we can return only the time zone for replacing geonames
27 | def time_zone(query)
28 | response = prepare_response(make_url(:address => "#{query}").open('User-Agent' => USER_AGENT).read)
29 | check_error(response)
30 | return parse_time_zone(response)
31 | rescue OpenURI::HTTPError => e
32 | check_error(prepare_response(e.io.read))
33 | raise
34 | end
35 |
36 | private
37 |
38 | def prepare_response(response)
39 | JSON.parse(response)
40 | end
41 |
42 | def parse_response(response)
43 | Location.new(
44 | :latitude => response["query"]["latitude"],
45 | :longitude => response["query"]["longitude"],
46 | :precision => :unknown
47 | )
48 | end
49 |
50 | def parse_time_zone(response)
51 | response["features"].detect do |feature|
52 | feature["classifiers"].first["category"] == "Time Zone"
53 | end["name"]
54 | end
55 |
56 | def check_error(response)
57 | raise Error, response["message"] unless response["message"].blank?
58 | end
59 |
60 | def make_url(params)
61 | super params.merge(:token => @token)
62 | end
63 | end
64 | end
65 | end
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | $LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
2 |
3 | require 'bundler/setup'
4 | require 'graticule/version'
5 | require 'active_support'
6 | require 'rake/testtask'
7 | require 'yaml'
8 |
9 | desc 'Default: run unit tests.'
10 | task :default => :test
11 |
12 | task :build do
13 | system "gem build graticule.gemspec"
14 | end
15 |
16 | task :release => :build do
17 | system "gem push graticule-#{Graticule::VERSION}.gem"
18 | end
19 |
20 | task :install => :build do
21 | system "gem install graticule-#{Graticule::VERSION}.gem"
22 | end
23 |
24 | desc 'Run the unit tests'
25 | Rake::TestTask.new(:test) do |t|
26 | t.libs << 'lib' << 'test'
27 | t.pattern = 'test/**/*_test.rb'
28 | t.verbose = true
29 | end
30 |
31 | require 'active_support'
32 | require 'net/http'
33 | require 'uri'
34 | RESPONSES_PATH = File.dirname(__FILE__) + '/test/fixtures/responses'
35 |
36 | def cache_responses(service)
37 | test_config[service.to_s]['responses'].each do |file,url|
38 | File.open("#{RESPONSES_PATH}/#{service}/#{file}", 'w') do |f|
39 | f.puts Net::HTTP.get(URI.parse(url))
40 | end
41 | end
42 | end
43 |
44 | def test_config
45 | file = File.dirname(__FILE__) + '/test/config.yml'
46 | unless File.exists?(file)
47 | raise "API keys not found. Add them by:
48 | cp #{file}.default #{file}
49 | vi #{file}
50 | "
51 | end
52 | @test_config ||= YAML.load(File.read(file)).tap do |config|
53 | config.each do |service,values|
54 | values['responses'].each {|f,url| update_placeholders!(values, url) }
55 | end
56 | end
57 | end
58 |
59 | def update_placeholders!(config, thing)
60 | config.each do |option, value|
61 | thing.gsub!(":#{option}", value) if value.is_a?(String)
62 | end
63 | end
64 |
65 | namespace :test do
66 | namespace :cache do
67 | desc 'Cache test responses from all the geocoders'
68 | task :all => test_config.keys
69 |
70 | test_config.keys.each do |service|
71 | desc "Cache test responses for #{service}"
72 | task service do
73 | cache_responses(service)
74 | end
75 | end
76 | end
77 | end
78 |
79 |
--------------------------------------------------------------------------------
/test/fixtures/responses/google/address.json:
--------------------------------------------------------------------------------
1 | {
2 | "results" : [
3 | {
4 | "address_components" : [
5 | {
6 | "long_name" : "1600",
7 | "short_name" : "1600",
8 | "types" : [ "street_number" ]
9 | },
10 | {
11 | "long_name" : "Amphitheatre Parkway",
12 | "short_name" : "Amphitheatre Pkwy",
13 | "types" : [ "route" ]
14 | },
15 | {
16 | "long_name" : "Mountain View",
17 | "short_name" : "Mountain View",
18 | "types" : [ "locality", "political" ]
19 | },
20 | {
21 | "long_name" : "Santa Clara",
22 | "short_name" : "Santa Clara",
23 | "types" : [ "administrative_area_level_2", "political" ]
24 | },
25 | {
26 | "long_name" : "California",
27 | "short_name" : "CA",
28 | "types" : [ "administrative_area_level_1", "political" ]
29 | },
30 | {
31 | "long_name" : "United States",
32 | "short_name" : "US",
33 | "types" : [ "country", "political" ]
34 | },
35 | {
36 | "long_name" : "94043",
37 | "short_name" : "94043",
38 | "types" : [ "postal_code" ]
39 | }
40 | ],
41 | "formatted_address" : "1600 Amphitheatre Parkway, Mountain View, CA 94043, USA",
42 | "geometry" : {
43 | "location" : {
44 | "lat" : 37.4216410,
45 | "lng" : -122.08550160
46 | },
47 | "location_type" : "ROOFTOP",
48 | "viewport" : {
49 | "northeast" : {
50 | "lat" : 37.42298998029150,
51 | "lng" : -122.0841526197085
52 | },
53 | "southwest" : {
54 | "lat" : 37.42029201970850,
55 | "lng" : -122.0868505802915
56 | }
57 | }
58 | },
59 | "partial_match" : true,
60 | "types" : [ "street_address" ]
61 | }
62 | ],
63 | "status" : "OK"
64 | }
--------------------------------------------------------------------------------
/test/fixtures/responses/google/success.json:
--------------------------------------------------------------------------------
1 | {
2 | "results" : [
3 | {
4 | "address_components" : [
5 | {
6 | "long_name" : "1600",
7 | "short_name" : "1600",
8 | "types" : [ "street_number" ]
9 | },
10 | {
11 | "long_name" : "Amphitheatre Parkway",
12 | "short_name" : "Amphitheatre Pkwy",
13 | "types" : [ "route" ]
14 | },
15 | {
16 | "long_name" : "Mountain View",
17 | "short_name" : "Mountain View",
18 | "types" : [ "locality", "political" ]
19 | },
20 | {
21 | "long_name" : "Santa Clara",
22 | "short_name" : "Santa Clara",
23 | "types" : [ "administrative_area_level_2", "political" ]
24 | },
25 | {
26 | "long_name" : "California",
27 | "short_name" : "CA",
28 | "types" : [ "administrative_area_level_1", "political" ]
29 | },
30 | {
31 | "long_name" : "United States",
32 | "short_name" : "US",
33 | "types" : [ "country", "political" ]
34 | },
35 | {
36 | "long_name" : "94043",
37 | "short_name" : "94043",
38 | "types" : [ "postal_code" ]
39 | }
40 | ],
41 | "formatted_address" : "1600 Amphitheatre Parkway, Mountain View, CA 94043, USA",
42 | "geometry" : {
43 | "location" : {
44 | "lat" : 37.4216410,
45 | "lng" : -122.08550160
46 | },
47 | "location_type" : "ROOFTOP",
48 | "viewport" : {
49 | "northeast" : {
50 | "lat" : 37.42298998029150,
51 | "lng" : -122.0841526197085
52 | },
53 | "southwest" : {
54 | "lat" : 37.42029201970850,
55 | "lng" : -122.0868505802915
56 | }
57 | }
58 | },
59 | "partial_match" : true,
60 | "types" : [ "street_address" ]
61 | }
62 | ],
63 | "status" : "OK"
64 | }
--------------------------------------------------------------------------------
/lib/graticule/cli.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'graticule'
3 | require 'optparse'
4 |
5 | module Graticule
6 |
7 | # A command line interface for geocoding. From the command line, run:
8 | #
9 | # geocode 49423
10 | #
11 | # Outputs:
12 | #
13 | # # Holland, MI 49423 US
14 | # # latitude: 42.7654, longitude: -86.1085
15 | #
16 | # == Usage: geocode [options] location
17 | #
18 | # Options:
19 | # -s, --service service Geocoding service
20 | # -a, --apikey apikey API key for the selected service
21 | # -h, --help Help
22 | class Cli
23 |
24 | def self.start(args, out = STDOUT)
25 | options = { :service => :yahoo, :api_key => 'YahooDemo' }
26 | supported_services = %w(yahoo google yandex geocoder_us metacarta)
27 |
28 | OptionParser.new do |opts|
29 | opts.banner = "Usage: geocode [options] location"
30 | opts.separator ""
31 | opts.separator "Options: "
32 |
33 | opts.on("-s service", supported_services, "--service service",
34 | "Geocoding service.", "Currently supported services: #{supported_services.join(", ")}") do |service|
35 | options[:service] = service
36 | end
37 |
38 | opts.on("-a apikey", "--apikey apikey", "API key for the selected service") do |apikey|
39 | options[:api_key] = apikey
40 | end
41 |
42 | opts.on_tail("-h", "--help", "Help") do
43 | puts opts
44 | exit
45 | end
46 | end.parse! args
47 |
48 | options[:location] = args.join(" ")
49 |
50 | result = Graticule.service(options[:service]).new(*options[:api_key].split(',')).locate(options[:location])
51 | location = (result.is_a?(Array) ? result.first : result)
52 | if location
53 | out << location.to_s(:coordinates => true) + "\n"
54 | exit 0
55 | else
56 | out << "Location not found"
57 | exit 1
58 | end
59 | rescue Graticule::CredentialsError
60 | $stderr.puts "Invalid API key. Pass your #{options[:service]} API key using the -a option. "
61 | rescue OptionParser::InvalidArgument, OptionParser::InvalidOption,
62 | Graticule::Error => error
63 | $stderr.puts error.message
64 | end
65 |
66 |
67 | end
68 | end
69 |
70 |
--------------------------------------------------------------------------------
/test/fixtures/responses/google/success_multiple_results.json:
--------------------------------------------------------------------------------
1 | {
2 | "results" : [
3 | {
4 | "address_components" : [
5 | {
6 | "long_name" : "Queen Street West",
7 | "short_name" : "Queen St W",
8 | "types" : [ "route" ]
9 | },
10 | {
11 | "long_name" : "Old Toronto",
12 | "short_name" : "Old Toronto",
13 | "types" : [ "sublocality", "political" ]
14 | },
15 | {
16 | "long_name" : "Toronto",
17 | "short_name" : "Toronto",
18 | "types" : [ "locality", "political" ]
19 | },
20 | {
21 | "long_name" : "Toronto",
22 | "short_name" : "Toronto",
23 | "types" : [ "administrative_area_level_2", "political" ]
24 | },
25 | {
26 | "long_name" : "Ontario",
27 | "short_name" : "ON",
28 | "types" : [ "administrative_area_level_1", "political" ]
29 | },
30 | {
31 | "long_name" : "Canada",
32 | "short_name" : "CA",
33 | "types" : [ "country", "political" ]
34 | }
35 | ],
36 | "formatted_address" : "Queen Street West, Toronto, ON, Canada",
37 | "geometry" : {
38 | "bounds" : {
39 | "northeast" : {
40 | "lat" : 43.65350280,
41 | "lng" : -79.37926539999999
42 | },
43 | "southwest" : {
44 | "lat" : 43.63874020,
45 | "lng" : -79.44593279999999
46 | }
47 | },
48 | "location" : {
49 | "lat" : 43.6453370,
50 | "lng" : -79.4132080
51 | },
52 | "location_type" : "GEOMETRIC_CENTER",
53 | "viewport" : {
54 | "northeast" : {
55 | "lat" : 43.65350280,
56 | "lng" : -79.37926539999999
57 | },
58 | "southwest" : {
59 | "lat" : 43.63874020,
60 | "lng" : -79.44593279999999
61 | }
62 | }
63 | },
64 | "types" : [ "route" ]
65 | }
66 | ],
67 | "status" : "OK"
68 | }
--------------------------------------------------------------------------------
/test/graticule/geocoder/mapquest_test.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'test_helper'
3 |
4 | module Graticule
5 | module Geocoder
6 | class MapquestTest < Test::Unit::TestCase
7 | def setup
8 | URI::HTTP.responses = []
9 | URI::HTTP.uris = []
10 | end
11 |
12 | def test_success
13 | @geocoder = Mapquest.new('api_key')
14 | prepare_response(:success)
15 | location = Location.new(
16 | :country => "US",
17 | :latitude => 44.15175,
18 | :locality => "Lovell",
19 | :longitude => -70.893,
20 | :postal_code => "04051-3919",
21 | :precision => :point,
22 | :region => "ME",
23 | :street => "44 Allen Rd"
24 | )
25 | assert_equal(location, @geocoder.locate('44 Allen Rd., Lovell, ME 04051'))
26 | end
27 |
28 | def test_multi_result
29 | @geocoder = Mapquest.new('api_key')
30 | prepare_response(:multi_result)
31 | location = Location.new(
32 | :country => "US",
33 | :latitude => 40.925598,
34 | :locality => "Stony Brook",
35 | :longitude => -73.141403,
36 | :postal_code => nil,
37 | :precision => :locality,
38 | :region => "NY",
39 | :street => nil
40 | )
41 | assert_equal(location, @geocoder.locate('217 Union St., NY'))
42 | end
43 |
44 | def test_multi_country
45 | @geocoder = Mapquest.new('api_key', false, 'US')
46 | prepare_response(:multi_country_success)
47 | location = Location.new(
48 | :country => "US",
49 | :latitude => 30.280046,
50 | :locality => "",
51 | :longitude => -90.786583,
52 | :postal_code => "12345",
53 | :precision => :postal_code,
54 | :region => "LA",
55 | :street => nil
56 | )
57 | assert_equal(location, @geocoder.locate('12345 us'))
58 | end
59 |
60 | def test_query_construction
61 | request = Mapquest::Request.new("217 Union St., NY", "api_key")
62 | query = %Q{key=api_key&outFormat=xml&inFormat=kvp&location=217%20Union%20St.,%20NY}
63 | assert_equal(query, request.query)
64 | end
65 |
66 | protected
67 |
68 | def prepare_response(id)
69 | URI::HTTP.responses << response('mapquest', id)
70 | end
71 | end
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/lib/graticule/geocoder/geocoder_ca.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | module Graticule #:nodoc:
3 | module Geocoder #:nodoc:
4 |
5 | # TODO: Reverse Geocoding
6 | class GeocoderCa < Base
7 |
8 | def initialize(auth = nil)
9 | @url = URI.parse 'http://geocoder.ca/'
10 | @auth = auth
11 | end
12 |
13 | def locate(address)
14 | get :locate => address.is_a?(String) ? address : location_from_params(address).to_s(:country => false)
15 | end
16 |
17 | private
18 |
19 | class Response
20 | include HappyMapper
21 | tag 'geodata'
22 | element :latitude, Float, :tag => 'latt'
23 | element :longitude, Float, :tag => 'longt'
24 | element :street_number, String, :deep => true, :tag => 'stnumber'
25 | element :street_name, String, :deep => true, :tag => 'staddress'
26 | element :locality, String, :deep => true, :tag => 'city'
27 | element :postal_code, String, :tag => 'postal'
28 | element :region, String, :deep => true, :tag => 'prov'
29 |
30 | class Error
31 | include HappyMapper
32 | tag 'error'
33 | element :code, Integer
34 | element :description, String
35 | end
36 |
37 | has_one :error, Error
38 |
39 | def street
40 | [street_number, street_name].join(' ')
41 | end
42 | end
43 |
44 | def prepare_response(xml)
45 | Response.parse(xml, :single => true)
46 | end
47 |
48 | def parse_response(response) #:nodoc:
49 | Location.new(
50 | :latitude => response.latitude,
51 | :longitude => response.longitude,
52 | :street => response.street,
53 | :locality => response.locality,
54 | :region => response.region
55 | )
56 | end
57 |
58 | def check_error(response) #:nodoc:
59 | if response.error
60 | exception = case response.error.code
61 | when 1..3; CredentialsError
62 | when 4..8; AddressError
63 | else; Error
64 | end
65 | raise exception, response.error.message
66 | end
67 | end
68 |
69 | def make_url(params) #:nodoc:
70 | params[:auth] = @auth if @auth
71 | params[:standard] = 1
72 | params[:showpostal] = 1
73 | params[:geoit] = 'XML'
74 | super params
75 | end
76 |
77 |
78 | end
79 | end
80 | end
--------------------------------------------------------------------------------
/lib/graticule/location.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | module Graticule
3 |
4 | # A geographic location
5 | class Location
6 | attr_accessor :latitude, :longitude, :street, :locality, :region, :postal_code, :country, :precision, :warning
7 | alias_method :city, :locality
8 | alias_method :state, :region
9 | alias_method :zip, :postal_code
10 |
11 | def initialize(attrs = {})
12 | attrs.each do |key,value|
13 | self.send("#{key}=", value.respond_to?(:force_encoding) ? value.force_encoding('UTF-8') : value)
14 | end
15 | self.precision ||= :unknown
16 | end
17 |
18 | def precision=(precision)
19 | @precision = Precision.new(precision.to_s)
20 | end
21 |
22 | def attributes
23 | [:latitude, :longitude, :street, :locality, :region, :postal_code, :country, :precision].inject({}) do |result,attr|
24 | result[attr] = self.send(attr) unless self.send(attr).blank?
25 | result
26 | end
27 | end
28 |
29 | def blank?
30 | attributes.except(:precision).empty?
31 | end
32 |
33 | # Returns an Array with latitude and longitude.
34 | def coordinates
35 | [latitude, longitude]
36 | end
37 |
38 | def ==(other)
39 | other.respond_to?(:attributes) ? attributes == other.attributes : false
40 | end
41 |
42 | # Calculate the distance to another location. See the various Distance formulas
43 | # for more information
44 | def distance_to(destination, options = {})
45 | options = {:formula => :haversine, :units => :miles}.merge(options)
46 | "Graticule::Distance::#{options[:formula].to_s.titleize}".constantize.distance(self, destination, options[:units])
47 | end
48 |
49 | # Where would I be if I dug through the center of the earth?
50 | def antipode
51 | Location.new :latitude => -latitude, :longitude => longitude + (longitude >= 0 ? -180 : 180)
52 | end
53 | alias_method :antipodal_location, :antipode
54 |
55 | def to_s(options = {})
56 | options = {:coordinates => false, :country => true}.merge(options)
57 | result = ""
58 | result << "#{street}\n" if street
59 | result << [locality, [region, postal_code].compact.join(" ")].compact.join(", ")
60 | result << " #{country}" if options[:country] && country
61 | result << "\nlatitude: #{latitude}, longitude: #{longitude}" if options[:coordinates] && [latitude, longitude].any?
62 | result
63 | end
64 |
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/CHANGELOG.txt:
--------------------------------------------------------------------------------
1 | 2.7.1 (2018-05-31)
2 | * Update Google to use https and include API key
3 |
4 | 2.6.0 (2018-05-31)
5 | * Update Mapbox to use the current v5 API
6 |
7 | 2.5.0 (2014-09-04)
8 | * Add Mapbox geocoder
9 | * Add Open MapQuest
10 | * Use Nokogiri
11 |
12 | 2.4.0 (--)
13 | * Update MapQuest handler to use the current API
14 |
15 | 2.3.0 (2013-04-01)
16 | * Google v3 API [Adamlb]
17 | * Escape XML entities in GeoCoder::MapQuest queries [Simon Coffey]
18 |
19 | 2.2.0 (2011-09-14)
20 | * Added yandex geocoder
21 | * Lots of other changes since September 2009.
22 | Check https://github.com/collectiveidea/graticule/compare/v0.2.12...master for more info
23 |
24 | * Added freethepostcode.org geocoder [Chris Lowis]
25 |
26 | 0.2.12 (2009-09-06)
27 | * Fixed geocoder_us [Aubrey Holland]
28 |
29 | 0.2.11 (2009-07-15)
30 | * Add new MapQuest geocoder [Aubrey Holland]
31 |
32 | 0.2.10 (2009-04-17)
33 | * Added #blank? to Location
34 |
35 | 0.2.9 (2009-04-14)
36 | * Remove retired MapQuest geocoder
37 | * Slightly more aggressive error handling for Geocoder.us
38 | * Extend Numeric with #to_radians and #to_degrees
39 |
40 | 0.2.8 (2008-10-03)
41 | * fixed missing files from gem
42 |
43 | 0.2.7 (2008-10-03)
44 | * Adding Multimap geocoder [Tom Taylor]
45 | * Added MapQuest geocoder [Andrew Selder]
46 | * Fix google geocoder for responses that only return coordinates [Andrew Selder]
47 |
48 | 0.2.5
49 | * fixed address mapping for local search maps (again)
50 |
51 | 0.2.4 (2007-05-15)
52 | * fixed address mapping for local search maps (Tom Taylor)
53 |
54 | 0.2.3 (2007-04-27)
55 | * fixed Google for less precise queries
56 | * added User-Agent to coerce Google into returning UTF-8 (Jonathan Tron)
57 |
58 | 0.2.2 (2007-03-27)
59 | * fixed LocalSearchMaps
60 |
61 | 0.2.1 (2007-03-19)
62 | * fixed error in command line interface
63 |
64 | 0.2.0 (2007-03-17)
65 | * changed city to locality, state to region, and zip to postal_code
66 | * added support for PostcodeAnywhere
67 | * added support for Local Search Maps (James Stewart)
68 | * added IP-based geocoder
69 | * moved geocoders to Graticule::Geocoder namespace
70 | * fixed Google geocoder (again)
71 | * made Yahoo geocoder consistent with others by returning 1 result
72 | * geocoders can how take a Hash (:street, :locality, :region, :postal_code, :country)
73 | or a Graticule::Location for the #locate call
74 |
75 | 0.1.3 (2007-02-14)
76 | * fixed Google geocoder
77 | * fixed CLI
78 |
79 | 0.1.2 (2007-02-12)
80 | * added "geocode" executable. See "geocode --help" for more information
81 | * declared dependency on ActiveSupport
82 |
83 | 0.1.1 (2006-12-16)
84 | * fixed bug in Yahoo that raised error when street address not returned
85 | * migrated to Hoe (http://seattlerb.rubyforge.org/hoe/)
86 | * added Haversine, Spherical and Vincenty distance calculations
87 |
88 | 0.1 (2006-10-31)
89 | * Initial release
90 |
--------------------------------------------------------------------------------
/test/graticule/distance_test.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'test_helper'
3 |
4 | module Graticule
5 | module Distance
6 | class DistanceFormulaTest < Test::Unit::TestCase
7 | EARTH_RADIUS_IN_MILES = 3963.1676
8 | EARTH_RADIUS_IN_KILOMETERS = 6378.135
9 |
10 | FORMULAS = [Haversine, Spherical, Vincenty]
11 |
12 | def test_earth_radius
13 | assert_equal EARTH_RADIUS_IN_MILES, EARTH_RADIUS[:miles]
14 | assert_equal EARTH_RADIUS_IN_KILOMETERS, EARTH_RADIUS[:kilometers]
15 | end
16 |
17 | def test_distance
18 | washington_dc = Location.new(:latitude => 38.898748, :longitude => -77.037684)
19 | chicago = Location.new(:latitude => 41.85, :longitude => -87.65)
20 |
21 | FORMULAS.each do |formula|
22 | assert_in_delta formula.distance(washington_dc, chicago), formula.distance(chicago, washington_dc), 0.00001
23 | assert_in_delta 594.820, formula.distance(washington_dc, chicago), 1.0
24 | assert_in_delta 594.820, formula.distance(washington_dc, chicago, :miles), 1.0
25 | assert_in_delta 957.275, formula.distance(washington_dc, chicago, :kilometers), 1.0
26 | end
27 | end
28 |
29 | def test_distance_between_antipodal_points
30 | # The Vincenty formula will be indeterminant with antipodal points.
31 | # See http://mathworld.wolfram.com/AntipodalPoints.html
32 | washington_dc = Location.new(:latitude => 38.898748, :longitude => -77.037684)
33 | chicago = Location.new(:latitude => 41.85, :longitude => -87.65)
34 |
35 | # First, test the deltas.
36 | FORMULAS.each do |formula|
37 | assert_in_delta 12450.6582171051,
38 | formula.distance(chicago, chicago.antipodal_location), 1.0
39 | assert_in_delta 12450.6582171051,
40 | formula.distance(washington_dc, washington_dc.antipodal_location), 1.0
41 | assert_in_delta 12450.6582171051,
42 | formula.distance(chicago, chicago.antipodal_location, :miles), 1.0
43 | assert_in_delta 12450.6582171051,
44 | formula.distance(washington_dc, washington_dc.antipodal_location, :miles), 1.0
45 | assert_in_delta 20037.50205960391,
46 | formula.distance(chicago, chicago.antipodal_location, :kilometers), 1.0
47 | assert_in_delta 20037.5020596039,
48 | formula.distance(washington_dc, washington_dc.antipodal_location, :kilometers), 1.0
49 | end
50 |
51 | # Next, test Vincenty. Vincenty will use haversine instead of returning NaN on antipodal points
52 | assert_equal Haversine.distance(washington_dc, washington_dc.antipodal_location),
53 | Vincenty.distance(washington_dc, washington_dc.antipodal_location)
54 | assert_equal Haversine.distance(chicago, chicago.antipodal_location),
55 | Vincenty.distance(chicago, chicago.antipodal_location)
56 | end
57 | end
58 | end
59 | end
--------------------------------------------------------------------------------
/lib/graticule/geocoder/multimap.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | module Graticule #:nodoc:
3 | module Geocoder #:nodoc:
4 |
5 | # Multimap geocoding API
6 |
7 | class Multimap < Base
8 |
9 | # This precision information is not complete.
10 | # More details should be implemented from:
11 | # http://www.multimap.com/share/documentation/clientzone/gqcodes.htm
12 |
13 | PRECISION = {
14 | 6 => Precision::Country,
15 | 5 => Precision::Region,
16 | 4 => Precision::PostalCode,
17 | 3 => Precision::Locality,
18 | 2 => Precision::Street,
19 | 1 => Precision::Address
20 | }
21 |
22 | # Web services initializer.
23 | #
24 | # The +api_key+ is the Open API key that uniquely identifies your
25 | # application.
26 | #
27 | # See http://www.multimap.com/openapi/
28 |
29 | def initialize(api_key)
30 | @api_key = api_key
31 | @url = URI.parse "http://clients.multimap.com/API/geocode/1.2/#{@api_key}"
32 | end
33 |
34 | # Returns a location for an address in the form of a String, Hash or Location.
35 |
36 | def locate(address)
37 | location = address.is_a?(String) ? address : location_from_params(address)
38 | case location
39 | when String
40 | get :qs => location
41 | when Location
42 | get "street" => location.street,
43 | "region" => location.region,
44 | "city" => location.locality,
45 | "postalCode" => location.postal_code,
46 | "countryCode" => location.country
47 | end
48 | end
49 |
50 | class Address
51 | include HappyMapper
52 | tag 'Location'
53 |
54 | attribute :quality, Integer, :tag => 'geocodeQuality'
55 | element :street, String, :tag => 'Street', :deep => true
56 | element :locality, String, :tag => 'Area', :deep => true
57 | element :region, String, :tag => 'State', :deep => true
58 | element :postal_code, String, :tag => 'PostalCode', :deep => true
59 | element :country, String, :tag => 'CountryCode', :deep => true
60 | element :latitude, Float, :tag => 'Lat', :deep => true
61 | element :longitude, Float, :tag => 'Lon', :deep => true
62 |
63 | def precision
64 | PRECISION[quality] || :unknown
65 | end
66 | end
67 |
68 | class Result
69 | include HappyMapper
70 | tag 'Results'
71 | attribute :error, String, :tag => 'errorCode'
72 | has_many :addresses, Address
73 | end
74 |
75 | def prepare_response(xml)
76 | Result.parse(xml, :single => true)
77 | end
78 |
79 | def parse_response(result)
80 | addr = result.addresses.first
81 | Location.new(
82 | :latitude => addr.latitude,
83 | :longitude => addr.longitude,
84 | :street => addr.street,
85 | :locality => addr.locality,
86 | :region => addr.region,
87 | :postal_code => addr.postal_code,
88 | :country => addr.country,
89 | :precision => addr.precision
90 | )
91 | end
92 |
93 | def check_error(result)
94 | raise Error, result.error unless result.error.blank?
95 | end
96 |
97 | end
98 | end
99 | end
--------------------------------------------------------------------------------
/lib/graticule/distance/vincenty.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | module Graticule
3 | module Distance
4 |
5 | #
6 | # The Vincenty Formula uses an ellipsoidal model of the earth, which is very accurate.
7 | #
8 | # Thanks to Chris Veness (http://www.movable-type.co.uk/scripts/LatLongVincenty.html)
9 | # for distance formulas.
10 | #
11 | class Vincenty < DistanceFormula
12 |
13 | # Calculate the distance between two Locations using the Vincenty formula
14 | #
15 | # Graticule::Distance::Vincenty.distance(
16 | # Graticule::Location.new(:latitude => 42.7654, :longitude => -86.1085),
17 | # Graticule::Location.new(:latitude => 41.849838, :longitude => -87.648193)
18 | # )
19 | # #=> 101.070118000159
20 | #
21 | def self.distance(from, to, units = :miles)
22 | from_longitude = from.longitude.to_radians
23 | from_latitude = from.latitude.to_radians
24 | to_longitude = to.longitude.to_radians
25 | to_latitude = to.latitude.to_radians
26 |
27 | earth_major_axis_radius = EARTH_MAJOR_AXIS_RADIUS[units.to_sym]
28 | earth_minor_axis_radius = EARTH_MINOR_AXIS_RADIUS[units.to_sym]
29 |
30 | f = (earth_major_axis_radius - earth_minor_axis_radius) / earth_major_axis_radius
31 |
32 | l = to_longitude - from_longitude
33 | u1 = atan((1-f) * tan(from_latitude))
34 | u2 = atan((1-f) * tan(to_latitude))
35 | sin_u1 = sin(u1)
36 | cos_u1 = cos(u1)
37 | sin_u2 = sin(u2)
38 | cos_u2 = cos(u2)
39 |
40 | lambda = l
41 | lambda_p = 2 * PI
42 | iteration_limit = 20
43 | while (lambda-lambda_p).abs > 1e-12 && (iteration_limit -= 1) > 0
44 | sin_lambda = sin(lambda)
45 | cos_lambda = cos(lambda)
46 | sin_sigma = sqrt((cos_u2*sin_lambda) * (cos_u2*sin_lambda) +
47 | (cos_u1*sin_u2-sin_u1*cos_u2*cos_lambda) * (cos_u1*sin_u2-sin_u1*cos_u2*cos_lambda))
48 | return 0 if sin_sigma == 0 # co-incident points
49 | cos_sigma = sin_u1*sin_u2 + cos_u1*cos_u2*cos_lambda
50 | sigma = atan2(sin_sigma, cos_sigma)
51 | sin_alpha = cos_u1 * cos_u2 * sin_lambda / sin_sigma
52 | cosSqAlpha = 1 - sin_alpha*sin_alpha
53 | cos2SigmaM = cos_sigma - 2*sin_u1*sin_u2/cosSqAlpha
54 |
55 | cos2SigmaM = 0 if cos2SigmaM.nan? # equatorial line: cosSqAlpha=0 (§6)
56 |
57 | c = f/16*cosSqAlpha*(4+f*(4-3*cosSqAlpha))
58 | lambda_p = lambda
59 | lambda = l + (1-c) * f * sin_alpha *
60 | (sigma + c*sin_sigma*(cos2SigmaM+c*cos_sigma*(-1+2*cos2SigmaM*cos2SigmaM)))
61 | end
62 | # formula failed to converge (happens on antipodal points)
63 | # We'll call Haversine formula instead.
64 | return Haversine.distance(from, to, units) if iteration_limit == 0
65 |
66 | uSq = cosSqAlpha * (earth_major_axis_radius**2 - earth_minor_axis_radius**2) / (earth_minor_axis_radius**2)
67 | a = 1 + uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq)))
68 | b = uSq/1024 * (256+uSq*(-128+uSq*(74-47*uSq)))
69 | delta_sigma = b*sin_sigma*(cos2SigmaM+b/4*(cos_sigma*(-1+2*cos2SigmaM*cos2SigmaM)-
70 | b/6*cos2SigmaM*(-3+4*sin_sigma*sin_sigma)*(-3+4*cos2SigmaM*cos2SigmaM)))
71 |
72 | earth_minor_axis_radius * a * (sigma-delta_sigma)
73 | end
74 | end
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/test/graticule/location_test.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'test_helper'
3 |
4 | module Graticule
5 | class LocationTest < Test::Unit::TestCase
6 |
7 | def setup
8 | @washington_dc = Location.new :latitude => 38.898748, :longitude => -77.037684,
9 | :street => '1600 Pennsylvania Avenue, NW', :locality => 'Washington',
10 | :region => 'DC', :postal_code => 20500, :country => 'US'
11 | end
12 |
13 | def test_distance_to
14 | chicago = Location.new(:latitude => 41.85, :longitude => -87.65)
15 | assert_in_delta 594.820, @washington_dc.distance_to(chicago), 1.0
16 | end
17 |
18 | def test_responds_to
19 | [:latitude, :longitude, :street, :locality, :region, :postal_code, :country, :coordinates, :precision].each do |m|
20 | assert Location.new.respond_to?(m), "should respond to #{m}"
21 | end
22 | end
23 |
24 | def test_coordinates
25 | l = Location.new(:latitude => 100, :longitude => 50)
26 | assert_equal [100, 50], l.coordinates
27 | end
28 |
29 | def test_equal
30 | assert_equal Location.new, Location.new
31 |
32 | attrs = {:latitude => 100.5389, :longitude => -147.5893, :street => '123 A Street',
33 | :locality => 'Somewhere', :region => 'NO', :postal_code => '12345', :country => 'USA'}
34 |
35 | assert_equal Location.new(attrs), Location.new(attrs)
36 | attrs.each do |k,v|
37 | assert_equal Location.new(k => v), Location.new(k => v)
38 | assert_not_equal Location.new, Location.new(k => v)
39 | assert_not_equal Location.new(attrs), Location.new(attrs.update(k => nil))
40 | end
41 | end
42 |
43 | def test_antipode
44 | chicago = Location.new(:latitude => 41.85, :longitude => -87.65)
45 |
46 | assert_equal [-38.898748, 102.962316], @washington_dc.antipode.coordinates
47 | assert_equal [-41.85, 92.35], chicago.antipode.coordinates
48 | assert_equal [-41, -180], Graticule::Location.new(:latitude => 41, :longitude => 0).antipode.coordinates
49 | assert_equal [-41, 179], Graticule::Location.new(:latitude => 41, :longitude => -1).antipode.coordinates
50 | assert_equal [-41, -179], Graticule::Location.new(:latitude => 41, :longitude => 1).antipode.coordinates
51 |
52 | assert_equal @washington_dc.coordinates, @washington_dc.antipode.antipode.coordinates
53 | assert_equal chicago.coordinates, chicago.antipode.antipode.coordinates
54 | end
55 |
56 | def test_to_s
57 | assert_equal "1600 Pennsylvania Avenue, NW\nWashington, DC 20500 US",
58 | @washington_dc.to_s
59 | assert_equal "1600 Pennsylvania Avenue, NW\nWashington, DC 20500",
60 | @washington_dc.to_s(:country => false)
61 | assert_equal "1600 Pennsylvania Avenue, NW\nWashington, DC 20500",
62 | @washington_dc.to_s(:country => false)
63 | assert_equal "1600 Pennsylvania Avenue, NW\nWashington, DC 20500\nlatitude: 38.898748, longitude: -77.037684",
64 | @washington_dc.to_s(:country => false, :coordinates => true)
65 | end
66 |
67 | def test_blank?
68 | assert Location.new.blank?
69 | [:latitude, :longitude, :street, :locality, :region, :postal_code, :country].each do |attr|
70 | assert !Location.new(attr => 'Foo').blank?
71 | end
72 | end
73 |
74 | def test_casts_precision
75 | assert_equal Precision::Region, Location.new(:precision => :region).precision
76 | assert_equal Precision::Street, Location.new(:precision => Precision::Street).precision
77 | end
78 | end
79 | end
--------------------------------------------------------------------------------
/lib/graticule/geocoder/multi.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'timeout'
3 |
4 | module Graticule #:nodoc:
5 | module Geocoder #:nodoc:
6 | class Multi
7 |
8 | # The Multi geocoder allows you to use multiple geocoders in succession.
9 | #
10 | # geocoder = Graticule.service(:multi).new(
11 | # Graticule.service(:google).new("api_key"),
12 | # Graticule.service(:yahoo).new("api_key"),
13 | # )
14 | # geocoder.locate '49423' # <= tries geocoders in succession
15 | #
16 | # The Multi geocoder will try the geocoders in order if a Graticule::AddressError
17 | # is raised. You can customize this behavior by passing in a block to the Multi
18 | # geocoder. For example, to try the geocoders until one returns a result with a
19 | # high enough precision:
20 | #
21 | # geocoder = Graticule.service(:multi).new(geocoders) do |result|
22 | # [:address, :street].include?(result.precision)
23 | # end
24 | #
25 | # Geocoders will be tried in order until the block returned true for one of the results
26 | #
27 | # Use the :timeout option to specify the number of seconds to allow for each
28 | # geocoder before raising a Timout::Error (defaults to 10 seconds).
29 | #
30 | # Graticule.service(:multi).new(geocoders, :timeout => 3)
31 | #
32 | def initialize(*geocoders, &acceptable)
33 | @options = {:timeout => 10, :async => false}.merge(geocoders.extract_options!)
34 | @acceptable = acceptable || Proc.new { true }
35 | @geocoders = geocoders
36 | end
37 |
38 | def locate(address)
39 | @lookup = @options[:async] ? ParallelLookup.new : SerialLookup.new
40 | last_error = nil
41 | @geocoders.each do |geocoder|
42 | @lookup.perform do
43 | begin
44 | result = nil
45 | Timeout.timeout(@options[:timeout]) do
46 | result = geocoder.locate address
47 | end
48 | result if @acceptable.call(result)
49 | rescue => e
50 | last_error = e
51 | nil
52 | end
53 | end
54 | end
55 | @lookup.result || raise(last_error || AddressError.new("Couldn't find '#{address}' with any of the services"))
56 | end
57 |
58 | class SerialLookup #:nodoc:
59 | def initialize
60 | @blocks = []
61 | end
62 |
63 | def perform(&block)
64 | @blocks << block
65 | end
66 |
67 | def result
68 | result = nil
69 | @blocks.detect do |block|
70 | result = block.call
71 | end
72 | result
73 | end
74 | end
75 |
76 | class ParallelLookup #:nodoc:
77 | def initialize
78 | @threads = []
79 | @monitor = Monitor.new
80 | end
81 |
82 | def perform(&block)
83 | @threads << Thread.new do
84 | self.result = block.call
85 | end
86 | end
87 |
88 | def result=(result)
89 | if result
90 | @monitor.synchronize do
91 | @result = result
92 | @threads.each(&:kill)
93 | end
94 | end
95 | end
96 |
97 | def result
98 | @threads.each(&:join)
99 | @result
100 | end
101 | end
102 | end
103 | end
104 | end
--------------------------------------------------------------------------------
/lib/graticule/geocoder/yahoo.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | module Graticule #:nodoc:
3 | module Geocoder #:nodoc:
4 |
5 | # Yahoo geocoding API.
6 | #
7 | # http://developer.yahoo.com/maps/rest/V1/geocode.html
8 | class Yahoo < Base
9 |
10 | PRECISION = {
11 | "country" => Precision::Country,
12 | "state" => Precision::Region,
13 | "city" => Precision::Locality,
14 | "zip+4" => Precision::PostalCode,
15 | "zip+2" => Precision::PostalCode,
16 | "zip" => Precision::PostalCode,
17 | "street" => Precision::Street,
18 | "address" => Precision::Address
19 | }
20 |
21 | # Web services initializer.
22 | #
23 | # The +appid+ is the Application ID that uniquely identifies your
24 | # application. See: http://developer.yahoo.com/faq/index.html#appid
25 | #
26 | # See http://developer.yahoo.com/search/rest.html
27 | def initialize(appid)
28 | @appid = appid
29 | @url = URI.parse "http://api.local.yahoo.com/MapsService/V1/geocode"
30 | end
31 |
32 | # Returns a Location for +address+.
33 | #
34 | # The +address+ can be any of:
35 | # * city, state
36 | # * city, state, zip
37 | # * zip
38 | # * street, city, state
39 | # * street, city, state, zip
40 | # * street, zip
41 | def locate(address)
42 | location = (address.is_a?(String) ? address : location_from_params(address).to_s(:country => false))
43 | # yahoo pukes on line breaks
44 | get :location => location.gsub("\n", ', ')
45 | end
46 |
47 | private
48 |
49 | class Address
50 | include HappyMapper
51 | tag 'Result'
52 |
53 | attribute :precision, String
54 | attribute :warning, String
55 | element :latitude, Float, :tag => 'Latitude'
56 | element :longitude, Float, :tag => 'Longitude'
57 | element :street, String, :tag => 'Address'
58 | element :locality, String, :tag => 'City'
59 | element :region, String, :tag => 'State'
60 | element :postal_code, String, :tag => 'Zip'
61 | element :country, String, :tag => 'Country'
62 |
63 | def precision
64 | PRECISION[@precision] || :unknown
65 | end
66 | end
67 |
68 | class Result
69 | include HappyMapper
70 | tag 'ResultSet'
71 | has_many :addresses, Address
72 | end
73 |
74 | class Error
75 | include HappyMapper
76 | tag 'Error'
77 | element :message, String, :tag => 'Message'
78 | end
79 |
80 | def parse_response(response) # :nodoc:
81 | addr = Result.parse(response, :single => true).addresses.first
82 | Location.new(
83 | :latitude => addr.latitude,
84 | :longitude => addr.longitude,
85 | :street => addr.street,
86 | :locality => addr.locality,
87 | :region => addr.region,
88 | :postal_code => addr.postal_code,
89 | :country => addr.country,
90 | :precision => addr.precision,
91 | :warning => addr.warning
92 | )
93 | end
94 |
95 | # Extracts and raises an error from +xml+, if any.
96 | def check_error(xml) #:nodoc:
97 | if error = Error.parse(xml, :single => true)
98 | raise Graticule::Error, error.message
99 | end
100 | end
101 |
102 | # Creates a URL from the Hash +params+. Automatically adds the appid and
103 | # sets the output type to 'xml'.
104 | def make_url(params) #:nodoc:
105 | params[:appid] = @appid
106 | params[:output] = 'xml'
107 |
108 | super params
109 | end
110 |
111 | end
112 |
113 | end
114 | end
--------------------------------------------------------------------------------
/site/plugin.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 | acts_as_geocodable - Graticule
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
23 |
24 |
25 |
34 |
35 |
acts_as_geocodable is a Rails plugin that makes your applications geo-aware. A picture (er, example) is worth a thousand words:
36 |
37 |
Examples
38 |
39 |
event = Event.create :street => "777 NE Martin Luther King, Jr. Blvd.",
40 | :locality => "Portland", :region => "Oregon", :postal_code => 97232
41 |
42 | # how far am I from RailsConf 2007?
43 | event.distance_to "49423" #=> 1807.66560483205
44 |
45 | # Find our new event, and any other ones in the area
46 | Event.find(:all, :within => 50, :origin => "97232")
47 |
48 | # Find the nearest restaurant with beer
49 | Restaurant.find(:nearest, :origin => event, :conditions => 'beer = true')
50 |
51 |
52 |
See the API documentation for more details.
53 |
54 |
IP-based geocoding
55 |
56 |
acts_as_geocodable adds a remote_location method to your Rails controllers for retrieving a user's location based on their remote IP address.
57 |
58 |
@nearest_store = Store.find(:nearest, :origin => remote_location) if remote_location
59 |
60 |
Don't rely too heavily on remote_location because the location of many IP addresses cannot be determined through HostIP .
61 |
62 |
Installation
63 |
64 |
Install the plugin by executing:
65 |
66 |
script/plugin install -x http://source.collectiveidea.com/public/rails/plugins/acts_as_geocodable
67 |
68 |
Contributing
69 |
70 |
Contributions are welcome and appreciated! Grab the source from:
71 |
72 |
http://source.collectiveidea.com/public/rails/plugins/acts_as_geocodable
73 |
74 |
75 |
77 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/lib/graticule/geocoder/base.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'open-uri'
3 |
4 | module Graticule #:nodoc:
5 | module Geocoder
6 |
7 | # Abstract class for implementing geocoders.
8 | #
9 | # === Example
10 | #
11 | # The following methods must be implemented in sublcasses:
12 | #
13 | # * +initialize+:: Sets @url to the service enpoint.
14 | # * +check_error+:: Checks for errors in the server response.
15 | # * +parse_response+:: Extracts information from the server response.
16 | #
17 | # Optionally, you can also override
18 | #
19 | # * +prepare_response+:: Convert the string response into a different format
20 | # that gets passed on to +check_error+ and +parse_response+.
21 | #
22 | # If you have extra URL paramaters (application id, output type) or need to
23 | # perform URL customization, override +make_url+.
24 | #
25 | # class FakeGeocoder < Base
26 | #
27 | # def initialize(appid)
28 | # @appid = appid
29 | # @url = URI.parse 'http://example.com/test'
30 | # end
31 | #
32 | # def locate(query)
33 | # get :q => query
34 | # end
35 | #
36 | # private
37 | #
38 | # def check_error(xml)
39 | # raise Error, xml.elements['error'].text if xml.elements['error']
40 | # end
41 | #
42 | # def make_url(params)
43 | # params[:appid] = @appid
44 | # super params
45 | # end
46 | #
47 | # def parse_response(response)
48 | # # return Location
49 | # end
50 | #
51 | # end
52 | #
53 | class Base
54 | USER_AGENT = "Mozilla/5.0 (compatible; Graticule; http://graticule.rubyforge.org)"
55 |
56 | def initialize
57 | raise NotImplementedError
58 | end
59 |
60 | private
61 |
62 | def location_from_params(params)
63 | case params
64 | when Location then params
65 | when Hash then Location.new params
66 | else
67 | raise ArgumentError, "Expected a Graticule::Location or a hash with :street, :locality, :region, :postal_code, and :country attributes"
68 | end
69 | end
70 |
71 | # Check for errors in +response+ and raise appropriate error, if any.
72 | # Must return if no error could be found.
73 | def check_error(response)
74 | raise NotImplementedError
75 | end
76 |
77 | # Performs a GET request with +params+. Calls +check_error+ and returns
78 | # the result of +parse_response+.
79 | def get(params = {})
80 | response = prepare_response(make_url(params).open('User-Agent' => USER_AGENT).read)
81 | check_error(response)
82 | return parse_response(response)
83 | rescue OpenURI::HTTPError => e
84 | check_error(prepare_response(e.io.read))
85 | raise
86 | end
87 |
88 | # Creates a URI from the Hash +params+. Override this then call super if
89 | # you need to add extra params like an application id or output type.
90 | def make_url(params)
91 | escaped_params = params.sort_by { |k,v| k.to_s }.map do |k,v|
92 | "#{escape k.to_s}=#{escape v.to_s}"
93 | end
94 | url = @url.dup
95 | url.query = escaped_params.join '&'
96 | return url
97 | end
98 |
99 | # Override to convert the response to something besides a String, which
100 | # will get passed to +check_error+ and +parse_response+.
101 | def prepare_response(response)
102 | response
103 | end
104 |
105 | # Must parse results from +response+ into a Location.
106 | def parse_response(response)
107 | raise NotImplementedError
108 | end
109 |
110 | def escape(string)
111 | if URI.const_defined?(:Parser)
112 | parser = URI::Parser.new
113 | parser.escape string
114 | else
115 | URI.escape string
116 | end
117 | end
118 | end
119 | end
120 | end
121 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Graticule
2 | =========
3 |
4 | ```
5 | grat·i·cule |ˈgratəˌkyoōl|
6 | Navigation. a network of parallels and meridians on a map or chart.
7 | ```
8 |
9 | Graticule is a geocoding API for looking up address coordinates and performing distance calculations. It supports many popular APIs:
10 |
11 | * Mapbox
12 | * Yahoo
13 | * Google
14 | * MapQuest
15 | * Geocoder.ca
16 | * Geocoder.us
17 | * Geonames
18 | * SimpleGeo
19 | * Postcode Anywhere
20 | * MetaCarta
21 | * FreeThePostcode
22 | * LocalSearchMaps
23 | * Yandex
24 |
25 | ### Installation
26 |
27 | ```
28 | gem install graticule
29 | ```
30 |
31 | ### Usage
32 |
33 | There is a companion Rails plugin called [acts_as_geocodable](https://github.com/collectiveidea/acts_as_geocodable) that makes geocoding seem like magic.
34 |
35 | Graticule exposes to main APIs: location search and distance calculations. Graticule also
36 | provides a command line utility.
37 |
38 | #### Location Search / Geocoding
39 |
40 | ```
41 | require 'rubygems'
42 | require 'graticule'
43 |
44 | geocoder = Graticule.service(:google).new "api_key"
45 | location = geocoder.locate("61 East 9th Street, Holland, MI")
46 | ```
47 |
48 | For specific service documentation, please visit the [documentation](http://rdoc.info/github/collectiveidea/graticule).
49 |
50 | #### Distance Calculation
51 |
52 | Graticule includes 3 different distance formulas, Spherical (simplest but least accurate), Vincenty (most accurate and most complicated), and Haversine (somewhere inbetween). The default is Haversine. There are two ways to calculate the distance between two points.
53 |
54 | First is `Location#distance_to`:
55 |
56 | ```
57 | holland = geocoder.locate("Holland, MI")
58 | chicago = geocoder.locate("Chicago, IL")
59 | holland.distance_to(chicago, :formula => :haversine) # or :spherical or :vincenty
60 | # => 101.997458788177
61 | ```
62 |
63 | You can also use the formula classes directly:
64 |
65 | ```
66 | Graticule::Distance::Haversine.distance(holland, chicago)
67 | # => 101.997458788177
68 | ```
69 |
70 | All units are miles by default, but you can switch to kilometers with the `units` option
71 |
72 | ```
73 | holland.distance_to(chicago, :units => :kilometers)
74 | #
75 | Graticule::Distance::Haversine.distance(holland, chicago, :kilometers)
76 | ```
77 |
78 |
79 | #### Command Line
80 |
81 | Graticule includes a command line interface (CLI). The CLI does not currently support all of the implemented services.
82 |
83 | ```
84 | $ geocode -s google -a [api_key] Washington, DC
85 | Washington, DC US
86 | latitude: 38.895222, longitude: -77.036758
87 | ```
88 |
89 | ### Contributing
90 |
91 | In the spirit of [free software](http://www.fsf.org/licensing/essays/free-sw.html), **everyone** is encouraged to help improve this project.
92 |
93 | Here are some ways you can contribute:
94 |
95 | * Reporting bugs
96 | * Suggesting new features
97 | * Writing or editing documentation
98 | * Writing specifications
99 | * Writing code (**no patch is too small**: fix typos, add comments, clean up inconsistent whitespace)
100 | * Refactoring code
101 | * Reviewing patches
102 |
103 | ### Submitting an Issue
104 |
105 | We use the [GitHub issue tracker](https://github.com/collectiveidea/graticule/issues) to track bugs and features. Before submitting a bug report or feature request, check to make sure it hasn't already been submitted. When submitting a bug report, please include a [Gist](https://gist.github.com/) that includes a stack trace and any details that may be necessary to reproduce the bug, including your gem version, Ruby version, and operating system.
106 |
107 | ### Submitting a Pull Request
108 |
109 | 1. Fork the project.
110 | 2. Create a topic branch.
111 | 3. Implement your feature or bug fix.
112 | 4. Add specs for your feature or bug fix.
113 | 5. Run `rake`. If your changes are not 100% covered and passing, go back to step 4.
114 | 6. Commit and push your changes.
115 | 7. Submit a pull request. Please do not include changes to the gemspec, version, or history file. (If you want to create your own version for some reason, please do so in a separate commit.)
116 |
117 | ### Other Links
118 |
119 | [Blog posts about Graticule](http://opensoul.org/tags/geocoding)
120 |
121 | [Geocoder: Alternative Geocoding library](https://github.com/alex.../geocoder)
122 |
123 |
124 |
--------------------------------------------------------------------------------
/lib/graticule/geocoder/mapquest.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | module Graticule #:nodoc:
3 | module Geocoder #:nodoc:
4 |
5 | # Mapquest uses the Licenced Community API which requires an api key. You can sign up an account
6 | # and get an api key by registering at: http://developer.mapquest.com/
7 | #
8 | # mq = Graticule.service(:mapquest).new(API_KEY)
9 | # location = gg.locate('44 Allen Rd., Lovell, ME 04051')
10 | # [42.78942, -86.104424]
11 | #
12 | class Mapquest < Base
13 |
14 | def initialize(api_key, open = false, restrict_to_country = nil)
15 | @api_key = api_key
16 | @url = if open
17 | URI.parse('http://open.mapquestapi.com/geocoding/v1/address')
18 | else
19 | URI.parse('http://www.mapquestapi.com/geocoding/v1/address')
20 | end
21 | @country_filter = restrict_to_country
22 | end
23 |
24 | # Locates +address+ returning a Location
25 | def locate(address)
26 | get :q => address.is_a?(String) ? address : location_from_params(address).to_s
27 | end
28 |
29 | protected
30 |
31 | def make_url(params) #:nodoc
32 | request = Mapquest::Request.new(params[:q], @api_key)
33 | url = @url.dup
34 | url.query = request.query
35 | url
36 | end
37 |
38 | class Request
39 | def initialize(address, api_key)
40 | @address = address
41 | @api_key = api_key
42 | end
43 |
44 | def query
45 | "key=#{URI.escape(@api_key)}&outFormat=xml&inFormat=kvp&location=#{URI.escape(@address)}"
46 | end
47 | end
48 |
49 | # See http://www.mapquestapi.com/geocoding/geocodequality.html#granularity
50 | PRECISION = {
51 | 'P1' => Precision::Point,
52 | 'L1' => Precision::Address,
53 | 'I1' => Precision::Street,
54 | 'B1' => Precision::Street,
55 | 'B2' => Precision::Street,
56 | 'B3' => Precision::Street,
57 | 'Z3' => Precision::PostalCode,
58 | 'Z4' => Precision::PostalCode,
59 | 'Z2' => Precision::PostalCode,
60 | 'Z1' => Precision::PostalCode,
61 | 'A5' => Precision::Locality,
62 | 'A4' => Precision::Region,
63 | 'A3' => Precision::Region,
64 | 'A1' => Precision::Country
65 | }
66 |
67 | class Address
68 | include HappyMapper
69 | tag 'location'
70 | element :latitude, Float, :tag => 'lat', :deep => true
71 | element :longitude, Float, :tag => 'lng', :deep => true
72 | element :street, String, :tag => 'street'
73 | element :locality, String, :tag => 'adminArea5'
74 | element :region, String, :tag => 'adminArea3'
75 | element :postal_code, String, :tag => 'postalCode'
76 | element :country, String, :tag => 'adminArea1'
77 | element :result_code, String, :tag => 'geocodeQualityCode'
78 |
79 | def precision
80 | PRECISION[result_code.to_s[0,2]] || :unknown
81 | end
82 | end
83 |
84 | class Locations
85 | include HappyMapper
86 | has_many :addresses, Address, :tag => "location"
87 | end
88 |
89 | class Result
90 | include HappyMapper
91 | has_one :locations, Locations, :tag => "locations"
92 | end
93 |
94 | class Response
95 | include HappyMapper
96 | has_one :result, Result, :deep => true
97 | end
98 |
99 | def prepare_response(xml)
100 | Response.parse(xml, :single => true)
101 | end
102 |
103 | # Extracts a location from +xml+.
104 | def parse_response(response) #:nodoc:
105 | if @country_filter
106 | addr = response.result.locations.addresses.select{|address| address.country == @country_filter}.first
107 | else
108 | addr = response.result.locations.addresses.first
109 | end
110 | Location.new(
111 | :latitude => addr.latitude,
112 | :longitude => addr.longitude,
113 | :street => addr.street,
114 | :locality => addr.locality,
115 | :region => addr.region,
116 | :postal_code => addr.postal_code,
117 | :country => addr.country,
118 | :precision => addr.precision
119 | )
120 | end
121 |
122 | # Extracts and raises any errors in +xml+
123 | def check_error(xml) #:nodoc
124 | end
125 |
126 | end
127 | end
128 | end
129 |
--------------------------------------------------------------------------------
/test/graticule/geocoder/google_test.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'test_helper'
3 |
4 | module Graticule
5 | module Geocoder
6 | class GoogleTest < Test::Unit::TestCase
7 | def setup
8 | URI::HTTP.responses = []
9 | URI::HTTP.uris = []
10 | @geocoder = Google.new('APP_ID')
11 | end
12 |
13 | def test_success
14 | return unless prepare_response(:success)
15 | location = Location.new(
16 | :latitude=>37.421641,
17 | :longitude=>-122.0855016,
18 | :street=>"1600 Amphitheatre Pkwy",
19 | :locality=>"Mountain View",
20 | :region=>"CA",
21 | :postal_code=>"94043",
22 | :country=>"US",
23 | :precision=>:address
24 | )
25 | assert_equal location, @geocoder.locate('1600 Amphitheatre Parkway, Mountain View, CA')
26 | end
27 |
28 | # The #locate parameters are broad, so the JSON response contains
29 | # multiple results at street-level precision. We expect to get the
30 | # first result back, and it should not contain a postal code.
31 | def test_success_multiple_results
32 | return unless prepare_response(:success_multiple_results)
33 | location = Location.new(
34 | :latitude=>43.645337,
35 | :longitude=>-79.413208,
36 | :street=>"Queen St W",
37 | :locality=>"Toronto",
38 | :region=>"ON",
39 | :country=>"CA",
40 | :precision=>:street
41 | )
42 | assert_equal location, @geocoder.locate('Queen St West, Toronto, ON CA')
43 | end
44 |
45 | def test_precision_region
46 | return unless prepare_response(:region)
47 | location = Location.new(
48 | :latitude=> 36.7782610,
49 | :longitude=>-119.41793240,
50 | :region=>"CA",
51 | :country=>"US",
52 | :precision=>:region
53 | )
54 | assert_equal location, @geocoder.locate('CA US')
55 | end
56 |
57 | def test_precision_country
58 | return unless prepare_response(:country)
59 | location = Location.new(
60 | :latitude=>37.090240,
61 | :longitude=>-95.7128910,
62 | :country=>"US",
63 | :precision=>:country
64 | )
65 | assert_equal location, @geocoder.locate('US')
66 | end
67 |
68 | def test_precision_locality
69 | return unless prepare_response(:locality)
70 | location = Location.new(
71 | :latitude=>37.7749295,
72 | :longitude=>-122.4194155,
73 | :country=>"US",
74 | :region=>"CA",
75 | :locality=>"San Francisco",
76 | :precision=>:locality
77 | )
78 | assert_equal location, @geocoder.locate('San Francisco, CA US')
79 | end
80 |
81 | def test_precision_street
82 | return unless prepare_response(:street)
83 | location = Location.new(
84 | :latitude=>37.42325960000001,
85 | :longitude=>-122.08563830,
86 | :country=>"US",
87 | :region=>"CA",
88 | :street=>"Amphitheatre Pkwy",
89 | :locality=>"Mountain View",
90 | :precision=>:street
91 | )
92 | assert_equal location, @geocoder.locate('Amphitheatre Pkwy, Mountain View CA US')
93 | end
94 |
95 | def test_precision_address
96 | return unless prepare_response(:address)
97 | location = Location.new(
98 | :latitude=>37.421641,
99 | :longitude=>-122.0855016,
100 | :street=>"1600 Amphitheatre Pkwy",
101 | :locality=>"Mountain View",
102 | :region=>"CA",
103 | :postal_code=>"94043",
104 | :country=>"US",
105 | :precision=>:address
106 | )
107 | assert_equal location, @geocoder.locate('1600 Amphitheatre Parkway, Mountain View, CA')
108 | end
109 |
110 | def test_locate_server_error
111 | return unless prepare_response(:server_error)
112 | assert_raises(Error) { @geocoder.locate 'x' }
113 | end
114 |
115 | def test_locate_too_many_queries
116 | return unless prepare_response(:limit)
117 | assert_raises(CredentialsError) { @geocoder.locate 'x' }
118 | end
119 |
120 | def test_locate_zero_results
121 | return unless prepare_response(:zero_results)
122 | assert_raises(AddressError) { @geocoder.locate 'x' }
123 | end
124 |
125 | def test_bad_key
126 | return unless prepare_response(:badkey)
127 | assert_raises(CredentialsError) { @geocoder.locate('x') }
128 | end
129 |
130 | protected
131 |
132 | def prepare_response(id = :success)
133 | URI::HTTP.responses << response('google', id, 'json')
134 | end
135 |
136 | end
137 | end
138 | end
--------------------------------------------------------------------------------
/lib/graticule/geocoder/yandex.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | module Graticule #:nodoc:
3 | module Geocoder #:nodoc:
4 |
5 | # First you need a Yandex Maps API key. You can register for one here:
6 | # http://api.yandex.ru/maps/form.xml
7 | #
8 | # gg = Graticule.service(:yandex).new(MAPS_API_KEY)
9 | # location = gg.locate 'Россия, Москва, ул. Моховая, д.18'
10 | # p location.coordinates
11 | # #=> [37.612281, 55.753342]
12 | #
13 | class Yandex < Base
14 | # http://api.yandex.ru/maps/geocoder/doc/desc/concepts/input_params.xml
15 | # http://api.yandex.ru/maps/geocoder/doc/desc/concepts/response_structure.xml
16 |
17 | PRECISION = {
18 | :country => Precision::Country, # Country level accuracy.
19 | :province => Precision::Region, # Region (state, province, prefecture, etc.) level accuracy.
20 | :area => Precision::Region, # Sub-region (county, municipality, etc.) level accuracy.
21 | :locality => Precision::Locality, # Town (city, village) level accuracy.
22 | :metro => Precision::Street, # Street level accuracy.
23 | :street => Precision::Street, # Intersection level accuracy.
24 | :house => Precision::Address, # Address level accuracy.
25 | }.stringify_keys
26 |
27 | def initialize(key)
28 | @key = key
29 | @url = URI.parse 'http://geocode-maps.yandex.ru/1.x/'
30 | end
31 |
32 | # Locates +address+ returning a Location
33 | def locate(address)
34 | get :geocode => address.is_a?(String) ? address : location_from_params(address).to_s
35 | end
36 |
37 | private
38 |
39 | class Country
40 | include HappyMapper
41 |
42 | register_namespace "xmlns", "urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"
43 |
44 | tag "Country"
45 |
46 | element :street, String, :deep => true, :tag => 'ThoroughfareName'
47 | element :locality, String, :deep => true, :tag => 'LocalityName'
48 | element :region, String, :deep => true, :tag => 'AdministrativeAreaName'
49 | element :postal_code, String, :deep => true, :tag => 'PostalCodeNumber'
50 | element :country, String, :deep => true, :tag => 'CountryNameCode'
51 | end
52 |
53 | class GeocoderMetaData
54 | include HappyMapper
55 |
56 | register_namespace "xmlns", "http://maps.yandex.ru/geocoder/1.x"
57 |
58 | tag "GeocoderMetaData"
59 |
60 | has_one :address, Country
61 |
62 | element :kind, String, :tag => 'kind'
63 | end
64 |
65 | class FeatureMember
66 | include HappyMapper
67 |
68 | register_namespace "xmlns", "http://www.opengis.net/gml"
69 |
70 | tag "featureMember"
71 |
72 | has_one :geocoder_meta_data, GeocoderMetaData
73 |
74 | attr_reader :longitude, :latitude
75 |
76 | element :coordinates, String, :tag => 'pos', :deep => true
77 |
78 | def coordinates=(coordinates)
79 | @longitude, @latitude = coordinates.split(' ').map { |v| v.to_f }
80 | end
81 |
82 | def precision
83 | PRECISION[geocoder_meta_data.kind] || :unknown
84 | end
85 | end
86 |
87 | class Error
88 | include HappyMapper
89 |
90 | tag 'error'
91 |
92 | element :status, Integer, :tag => 'status'
93 | element :message, String, :tag => 'message'
94 | end
95 |
96 | class Response
97 | include HappyMapper
98 |
99 | register_namespace 'xmlns', 'http://maps.yandex.ru/ymaps/1.x'
100 |
101 | tag 'GeoObjectCollection'
102 |
103 | has_many :feature_members, FeatureMember
104 |
105 | with_nokogiri_config do |config|
106 | config.nsclean
107 | config.strict
108 | config.dtdload
109 | config.dtdvalid
110 | end
111 |
112 | def status
113 | 200
114 | end
115 | end
116 |
117 | def prepare_response(xml)
118 | Response.parse(xml, :single => true) || Error.parse(xml, :single => true)
119 | end
120 |
121 | def parse_response(response) #:nodoc:
122 | result = response.feature_members.first
123 | Location.new(
124 | :latitude => result.latitude,
125 | :longitude => result.longitude,
126 | :street => result.geocoder_meta_data.address.street,
127 | :locality => result.geocoder_meta_data.address.locality,
128 | :country => result.geocoder_meta_data.address.country,
129 | :precision => result.precision
130 | )
131 | end
132 |
133 | # Extracts and raises an error from +xml+, if any.
134 | def check_error(response) #:nodoc:
135 | case response.status
136 | when 200 then # ignore, ok
137 | when 401 then
138 | raise CredentialsError, response.message
139 | else
140 | raise Error, response.message
141 | end
142 | end
143 |
144 | # Creates a URL from the Hash +params+.
145 | # sets the output type to 'xml'.
146 | def make_url(params) #:nodoc:
147 | super params.merge(:key => @key)
148 | end
149 | end
150 | end
151 | end
152 |
153 |
--------------------------------------------------------------------------------
/test/fixtures/responses/mapquest/multi_country_success.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 0
4 |
5 |
6 | http://api.mqcdn.com/res/mqlogo.gif
7 | © 2014 MapQuest, Inc.
8 | © 2014 MapQuest, Inc.
9 |
10 |
11 |
12 |
13 |
14 | 12345 US
15 |
16 |
17 |
18 |
19 | Us
20 | Ile-de-France
21 | Pontoise
22 |
23 | FR
24 | CITY
25 | A5XXX
26 | false
27 | N
28 |
29 |
30 | 49.100945
31 | 1.967247
32 |
33 |
34 | 0
35 | s
36 |
37 | 49.100945
38 | 1.967247
39 |
40 |
41 |
44 |
45 |
46 |
47 |
48 | Us
49 | Bourgogne
50 | Louhans
51 |
52 | FR
53 | CITY
54 | A5XXX
55 | false
56 | N
57 |
58 |
59 | 46.818792
60 | 5.281419
61 |
62 |
63 | 0
64 | s
65 |
66 | 46.818792
67 | 5.281419
68 |
69 |
70 |
73 |
74 |
75 |
76 |
77 | Us
78 |
79 |
80 |
81 | ID
82 | CITY
83 | A5XXX
84 | false
85 | N
86 |
87 |
88 | -2.771829
89 | 132.186285
90 |
91 |
92 | 0
93 | s
94 |
95 | -2.771829
96 | 132.186285
97 |
98 |
99 |
102 |
103 |
104 |
105 |
106 |
107 | LA
108 | Ascension Parish
109 | 12345
110 | US
111 | ZIP
112 | Z1XXA
113 | false
114 | N
115 |
116 |
117 | 30.280046
118 | -90.786583
119 |
120 |
121 | 0
122 | s
123 |
124 | 30.280046
125 | -90.786583
126 |
127 |
128 |
131 |
132 |
133 |
134 |
135 | Uz
136 | Midi-Pyrénées
137 | Argelès-Gazost
138 |
139 | FR
140 | CITY
141 | A5XXX
142 | false
143 | N
144 |
145 |
146 | 42.966369
147 | -0.085232
148 |
149 |
150 | 0
151 | s
152 |
153 | 42.966369
154 | -0.085232
155 |
156 |
157 |
160 |
161 |
162 |
163 |
164 |
165 |
166 | -1
167 | true
168 | false
169 |
170 |
171 |
--------------------------------------------------------------------------------
/site/index.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 | Graticule
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
23 |
24 |
25 |
34 |
35 |
36 | grat·i·cule , |ˈgratəˌkyoōl| ,
37 | n.
38 | technical
39 |
40 | Navigation. a network of parallels and meridians on a map or chart.
41 |
42 |
43 |
Graticule is a geocoding API for looking up address coordinates and performing distance calculations. It supports many popular APIs, including:
44 |
45 |
46 | Yahoo
47 | Google
48 | Geocoder.ca
49 | Geocoder.us
50 | PostcodeAnywhere
51 | MetaCarta
52 |
53 |
54 |
There is a companion Rails plugin that makes geocoding seem like magic.
55 |
56 |
Example
57 |
58 |
require 'rubygems'
59 | require 'graticule'
60 | geocoder = Graticule.service(:google).new "api_key"
61 | location = geocoder.locate "1600 Amphitheatre Parkway, Mountain View, CA"
62 | location.coordinates #=> [37.423021, -122.083739]
63 | location.country #=> "US"
64 |
65 |
See the API documentation for more details.
66 |
67 |
International Support
68 |
69 |
Graticule supports several services with international support. The international geocoders require slightly more structured data than the US ones:
70 |
71 |
g = Graticule.service(:local_search_maps).new
72 | location = g.locate :street => '48 Leicester Square', :locality => 'London', :country => 'UK'
73 | location.coordinates #=> [51.510036, -0.130427]
74 |
75 |
Distance Calculation
76 |
77 |
Graticule includes 3 different distance formulas, Spherical (simplest but least accurate), Vincenty (most accurate and most complicated), and Haversine (somewhere inbetween).
78 |
79 |
location = geocoder.locate("Holland, MI")
80 | location.distance_to(geocoder.locate("Chicago, IL"))
81 | #=> 101.997458788177
82 |
83 |
Command Line
84 |
Graticule also includes a command line interface to the various geocoders:
85 |
86 |
87 | $ geocode -s yahoo -a yahookey Washington, DC
88 | Washington, DC US
89 | latitude: 38.895222, longitude: -77.036758
90 |
91 |
Installation
92 |
93 |
Install the gem by executing:
94 |
95 |
gem install graticule
96 |
97 |
Or, download it from RubyForge .
98 |
99 |
Contributing
100 |
101 |
Contributions are welcome and appreciated! Join the mailing list and grab the source from:
102 |
103 |
http://source.collectiveidea.com/public/geocode/trunk/
104 |
105 |
106 |
107 |
109 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/test/fixtures/responses/mapbox/success.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "FeatureCollection",
3 | "query": [
4 | "1",
5 | "infinite",
6 | "loop",
7 | "cupertino",
8 | "ca"
9 | ],
10 | "features": [
11 | {
12 | "id": "address.6395221899832142",
13 | "type": "Feature",
14 | "place_type": [
15 | "address"
16 | ],
17 | "relevance": 1,
18 | "properties": {},
19 | "text": "Infinite Loop",
20 | "place_name": "1 Infinite Loop, Cupertino, California 95014, United States",
21 | "center": [
22 | -122.03023,
23 | 37.331524
24 | ],
25 | "geometry": {
26 | "type": "Point",
27 | "coordinates": [
28 | -122.03023,
29 | 37.331524
30 | ]
31 | },
32 | "address": "1",
33 | "context": [
34 | {
35 | "id": "postcode.17583069324210830",
36 | "text": "95014"
37 | },
38 | {
39 | "id": "place.8489369485676010",
40 | "wikidata": "Q189471",
41 | "text": "Cupertino"
42 | },
43 | {
44 | "id": "region.3591",
45 | "short_code": "US-CA",
46 | "wikidata": "Q99",
47 | "text": "California"
48 | },
49 | {
50 | "id": "country.3145",
51 | "short_code": "us",
52 | "wikidata": "Q30",
53 | "text": "United States"
54 | }
55 | ]
56 | },
57 | {
58 | "id": "place.8489369485676010",
59 | "type": "Feature",
60 | "place_type": [
61 | "place"
62 | ],
63 | "relevance": 0.5,
64 | "properties": {
65 | "wikidata": "Q189471"
66 | },
67 | "text": "Cupertino",
68 | "place_name": "Cupertino, California, United States",
69 | "bbox": [
70 | -122.143825004601,
71 | 37.2479929816717,
72 | -121.995539980292,
73 | 37.3415970152769
74 | ],
75 | "center": [
76 | -122.0323,
77 | 37.323
78 | ],
79 | "geometry": {
80 | "type": "Point",
81 | "coordinates": [
82 | -122.0323,
83 | 37.323
84 | ]
85 | },
86 | "context": [
87 | {
88 | "id": "region.3591",
89 | "short_code": "US-CA",
90 | "wikidata": "Q99",
91 | "text": "California"
92 | },
93 | {
94 | "id": "country.3145",
95 | "short_code": "us",
96 | "wikidata": "Q30",
97 | "text": "United States"
98 | }
99 | ]
100 | },
101 | {
102 | "id": "neighborhood.285073",
103 | "type": "Feature",
104 | "place_type": [
105 | "neighborhood"
106 | ],
107 | "relevance": 0.49,
108 | "properties": {},
109 | "text": "Calvert",
110 | "place_name": "Calvert, Cupertino, California 95014, United States",
111 | "bbox": [
112 | -122.024501,
113 | 37.316342,
114 | -121.986694,
115 | 37.331914
116 | ],
117 | "center": [
118 | -122.0138,
119 | 37.3258
120 | ],
121 | "geometry": {
122 | "type": "Point",
123 | "coordinates": [
124 | -122.0138,
125 | 37.3258
126 | ]
127 | },
128 | "context": [
129 | {
130 | "id": "postcode.17583069324210830",
131 | "text": "95014"
132 | },
133 | {
134 | "id": "place.8489369485676010",
135 | "wikidata": "Q189471",
136 | "text": "Cupertino"
137 | },
138 | {
139 | "id": "region.3591",
140 | "short_code": "US-CA",
141 | "wikidata": "Q99",
142 | "text": "California"
143 | },
144 | {
145 | "id": "country.3145",
146 | "short_code": "us",
147 | "wikidata": "Q30",
148 | "text": "United States"
149 | }
150 | ]
151 | },
152 | {
153 | "id": "country.3179",
154 | "type": "Feature",
155 | "place_type": [
156 | "country"
157 | ],
158 | "relevance": 0.3333333333333333,
159 | "properties": {
160 | "wikidata": "Q16",
161 | "short_code": "ca"
162 | },
163 | "text": "Canada",
164 | "place_name": "Canada",
165 | "bbox": [
166 | -141.10275,
167 | 39.943435,
168 | -47.597809,
169 | 86.553514
170 | ],
171 | "center": [
172 | -105.750596,
173 | 55.585901
174 | ],
175 | "geometry": {
176 | "type": "Point",
177 | "coordinates": [
178 | -105.750596,
179 | 55.585901
180 | ]
181 | }
182 | },
183 | {
184 | "id": "place.7943223081734240",
185 | "type": "Feature",
186 | "place_type": [
187 | "place"
188 | ],
189 | "relevance": 0.3333333333333333,
190 | "properties": {
191 | "wikidata": "Q36312"
192 | },
193 | "text": "Calgary",
194 | "place_name": "Calgary, Alberta, Canada",
195 | "bbox": [
196 | -114.315773841495,
197 | 50.842824406804,
198 | -113.859901837937,
199 | 51.2124248116277
200 | ],
201 | "center": [
202 | -114.0626,
203 | 51.0531
204 | ],
205 | "geometry": {
206 | "type": "Point",
207 | "coordinates": [
208 | -114.0626,
209 | 51.0531
210 | ]
211 | },
212 | "context": [
213 | {
214 | "id": "region.219693",
215 | "short_code": "CA-AB",
216 | "wikidata": "Q1951",
217 | "text": "Alberta"
218 | },
219 | {
220 | "id": "country.3179",
221 | "short_code": "ca",
222 | "wikidata": "Q16",
223 | "text": "Canada"
224 | }
225 | ]
226 | }
227 | ],
228 | "attribution": "NOTICE: © 2017 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained."
229 | }
230 |
--------------------------------------------------------------------------------
/lib/graticule/geocoder/google.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'json'
3 | module Graticule #:nodoc:
4 | module Geocoder #:nodoc:
5 | # gg = Graticule.service(:google).new
6 | # location = gg.locate '1600 Amphitheater Pkwy, Mountain View, CA'
7 | # p location.coordinates
8 | # #=> [37.423111, -122.081783
9 | #
10 | # If you have a Google business account, initialize with:
11 | #
12 | # gg = Graticule.service(:google).new(MAPS_API_KEY, MAPS_CLIENT_ID)
13 | #
14 | class Google < Base
15 | # https://developers.google.com/maps/documentation/geocoding/
16 |
17 | def initialize(key=nil, client_id=nil)
18 | @key = key
19 | @client_id = client_id
20 | @url = URI.parse 'https://maps.googleapis.com/maps/api/geocode/json'
21 | end
22 |
23 | # Locates +address+ returning a Location
24 | def locate(address)
25 | get :key => @key, :address => address.is_a?(String) ? address : location_from_params(address).to_s
26 | end
27 |
28 | private
29 | class Result
30 | attr_accessor :latitude, :longitude, :street_number, :route, :locality, :region, :postal_code, :country, :precision, :formatted_address
31 | def initialize(attribs)
32 | @latitude = attribs["geometry"]["location"]["lat"]
33 | @longitude = attribs["geometry"]["location"]["lng"]
34 | @formatted_address = attribs["formatted_address"]
35 | @precision = determine_precision(attribs["types"])
36 | parse_address_components(attribs["address_components"])
37 | end
38 |
39 | def parse_address_components(components)
40 | components.each do |component|
41 | component["types"].each do |type|
42 | case type
43 | when "street_number"
44 | @street_number = component["short_name"]
45 | when "route"
46 | @route = component["short_name"]
47 | when "locality", "sublocality"
48 | @locality = component["long_name"]
49 | when "administrative_area_level_1"
50 | @region = component["short_name"]
51 | when "country"
52 | @country = component["short_name"]
53 | when "postal_code"
54 | @postal_code = component["long_name"]
55 | end
56 | end
57 | end
58 | end
59 |
60 | def street
61 | "#{@street_number.to_s}#{" " unless @street_number.blank? || @route.blank?}#{@route.to_s}"
62 | end
63 |
64 | def determine_precision(types)
65 | precision = Precision::Unknown
66 | types.each do |type|
67 | precision = case type
68 | when "premise", "subpremise"
69 | Precision::Premise
70 | when "street_address"
71 | Precision::Address
72 | when "route", "intersection"
73 | Precision::Street
74 | when "postal_code"
75 | Precision::PostalCode
76 | when "locality","sublocality","neighborhood"
77 | Precision::Locality
78 | when "administrative_area_level_1", "administrative_area_level_2","administrative_area_level_3"
79 | Precision::Region
80 | when "country"
81 | Precision::Country
82 | else
83 | precision
84 | end
85 | end
86 | return precision
87 | end
88 | end
89 |
90 | class Response
91 | attr_reader :results, :status
92 | def initialize(json)
93 | result = JSON.parse(json)
94 | @results = result["results"].collect{|attribs| Result.new(attribs)}
95 | @status = result["status"]
96 | end
97 | end
98 |
99 | def prepare_response(json)
100 | Response.new(json)
101 | end
102 |
103 | def parse_response(response) #:nodoc:
104 | result = response.results.first
105 | Location.new(
106 | :latitude => result.latitude,
107 | :longitude => result.longitude,
108 | :street => result.street,
109 | :locality => result.locality,
110 | :region => result.region,
111 | :postal_code => result.postal_code,
112 | :country => result.country,
113 | :precision => result.precision
114 | )
115 | end
116 |
117 | # Extracts and raises an error from +json+, if any.
118 | def check_error(response) #:nodoc:
119 | case response.status
120 | when "OK" then # ignore, ok
121 | when "ZERO_RESULTS" then
122 | raise AddressError, 'unknown address'
123 | when "OVER_QUERY_LIMIT"
124 | raise CredentialsError, 'over query limit'
125 | when "REQUEST_DENIED"
126 | raise CredentialsError, 'request denied'
127 | when "INVALID_REQUEST"
128 | raise AddressError, 'missing address'
129 | when "UNKNOWN_ERROR"
130 | raise Error, "unknown server error. Try again."
131 | else
132 | raise Error, "unkown error #{response.status}"
133 | end
134 | end
135 |
136 | # Creates a URL from the Hash +params+..
137 | #
138 | # If initialized with a key and client id for a Business account, signs
139 | # the url as required by v3 of the library:
140 | #
141 | # https://developers.google.com/maps/documentation/business/webservices#digital_signatures
142 | #
143 | def make_url(params) #:nodoc:
144 | if @key && @client_id
145 | url = super params.merge(:sensor => false, :client => @client_id)
146 | make_signed_url(url)
147 | else
148 | super params.merge(:sensor => false)
149 | end
150 | end
151 |
152 | def make_signed_url(original_url) #:nodoc:
153 | require "base64"
154 | require 'openssl'
155 | url_to_sign = "#{original_url.path}?#{original_url.query}"
156 | decoded_key = Base64.decode64(@key.tr("-_", "+/"))
157 | signature = OpenSSL::HMAC.digest('sha1', decoded_key, url_to_sign)
158 | encoded_signature = Base64.encode64(signature).tr("+/", "-_")
159 | signed_url = original_url.to_s + "&signature=#{encoded_signature}"
160 | #puts signed_url
161 | URI.parse signed_url
162 | end
163 |
164 | end
165 | end
166 | end
167 |
--------------------------------------------------------------------------------
/test/fixtures/responses/simple_geo/success.json:
--------------------------------------------------------------------------------
1 | {
2 | "query":
3 | {
4 | "latitude":34.482358,
5 | "longitude":-117.373982,
6 | "address":"1600 Amphitheatre Parkway, Mountain View,CA"
7 | },
8 | "timestamp":1294368549.955,
9 | "features":
10 | [
11 | {
12 | "handle":"SG_1Otdjh18O9anBbxjYr2TqB_34.487151_-117.354916",
13 | "name":"06071009902",
14 | "license":"http://creativecommons.org/publicdomain/mark/1.0/",
15 | "bounds":
16 | [
17 | -117.386259,
18 | 34.470181,
19 | -117.329905,
20 | 34.506723
21 | ],
22 | "href":"http://api.simplegeo.com/1.0/features/SG_1Otdjh18O9anBbxjYr2TqB_34.487151_-117.354916.json",
23 | "abbr":null,
24 | "classifiers":
25 | [
26 | {
27 | "category":"US Census",
28 | "type":"Region",
29 | "subcategory":"Tract"
30 | }
31 | ]
32 | },
33 | {
34 | "handle":"SG_2g7VeqkrzCJCyuCbo1Ff2r_34.486667_-117.371184",
35 | "name":"92392",
36 | "license":"http://creativecommons.org/publicdomain/mark/1.0/",
37 | "bounds":
38 | [
39 | -117.49579,
40 | 34.426755,
41 | -117.250416,
42 | 34.564234
43 | ],
44 | "href":"http://api.simplegeo.com/1.0/features/SG_2g7VeqkrzCJCyuCbo1Ff2r_34.486667_-117.371184.json",
45 | "abbr":null,
46 | "classifiers":
47 | [
48 | {
49 | "category":"Postal Code",
50 | "type":"Region",
51 | "subcategory":null
52 | }
53 | ]
54 | },
55 | {
56 | "handle":"SG_2kp8yDSE3qiOP0dicAHDqg_34.527769_-117.353855",
57 | "name":"Victorville",
58 | "license":"http://creativecommons.org/publicdomain/mark/1.0/",
59 | "bounds":
60 | [
61 | -117.46864,
62 | 34.435848,
63 | -117.253907,
64 | 34.645497
65 | ],
66 | "href":"http://api.simplegeo.com/1.0/features/SG_2kp8yDSE3qiOP0dicAHDqg_34.527769_-117.353855.json",
67 | "abbr":null,
68 | "classifiers":
69 | [
70 | {
71 | "category":"Municipal",
72 | "type":"Region",
73 | "subcategory":"City"
74 | }
75 | ]
76 | },
77 | {
78 | "handle":"SG_1c5T61jEkGOfZvy5LNOIfT_34.526999_-117.353691",
79 | "href":"http://api.simplegeo.com/1.0/features/SG_1c5T61jEkGOfZvy5LNOIfT_34.526999_-117.353691.json",
80 | "abbr":null,
81 | "attribution":"(c) OpenStreetMap (http://openstreetmap.org/) and contributors CC-BY-SA (http://creativecommons.org/licenses/by-sa/2.0/)",
82 | "classifiers":
83 | [
84 | {
85 | "category":"Municipality",
86 | "type":"Region",
87 | "subcategory":"City"
88 | }
89 | ],
90 | "name":"Victorville",
91 | "license":"http://creativecommons.org/licenses/by-sa/2.0/",
92 | "bounds":
93 | [
94 | -117.46864,
95 | 34.435848,
96 | -117.253907,
97 | 34.645497
98 | ]
99 | },
100 | {
101 | "handle":"SG_4Yva8e9vcHH3MScH7KvMZD_34.663870_-117.669800",
102 | "name":"Assembly District 36",
103 | "license":"http://creativecommons.org/publicdomain/mark/1.0/",
104 | "bounds":
105 | [
106 | -118.377303,
107 | 34.326294,
108 | -116.96524,
109 | 34.998637
110 | ],
111 | "href":"http://api.simplegeo.com/1.0/features/SG_4Yva8e9vcHH3MScH7KvMZD_34.663870_-117.669800.json",
112 | "abbr":null,
113 | "classifiers":
114 | [
115 | {
116 | "category":"Legislative District",
117 | "type":"Region",
118 | "subcategory":"Provincial (Lower)"
119 | }
120 | ]
121 | },
122 | {
123 | "handle":"SG_1ZlF3w31JV0eG1ukUUcZGr_34.566552_-118.353652",
124 | "name":"State Senate District 17",
125 | "license":"http://creativecommons.org/publicdomain/mark/1.0/",
126 | "bounds":
127 | [
128 | -119.454582,
129 | 34.210933,
130 | -117.099583,
131 | 34.901274
132 | ],
133 | "href":"http://api.simplegeo.com/1.0/features/SG_1ZlF3w31JV0eG1ukUUcZGr_34.566552_-118.353652.json",
134 | "abbr":null,
135 | "classifiers":
136 | [
137 | {
138 | "category":"Legislative District",
139 | "type":"Region",
140 | "subcategory":"Provincial (Upper)"
141 | }
142 | ]
143 | },
144 | {
145 | "handle":"SG_4JGWCBWthWI7RB522a2Qzb_34.841434_-116.178457",
146 | "name":"San Bernardino",
147 | "license":"http://creativecommons.org/publicdomain/mark/1.0/",
148 | "bounds":
149 | [
150 | -117.802891,
151 | 33.870831,
152 | -114.131211,
153 | 35.809236
154 | ],
155 | "href":"http://api.simplegeo.com/1.0/features/SG_4JGWCBWthWI7RB522a2Qzb_34.841434_-116.178457.json",
156 | "abbr":null,
157 | "classifiers":
158 | [
159 | {
160 | "category":"Administrative",
161 | "type":"Region",
162 | "subcategory":"County"
163 | }
164 | ]
165 | },
166 | {
167 | "handle":"SG_1ZVESZNJmAlJh0isTepcXb_36.218760_-117.499181",
168 | "name":"Congressional District 25",
169 | "license":"http://creativecommons.org/publicdomain/mark/1.0/",
170 | "bounds":
171 | [
172 | -119.651509,
173 | 34.230677,
174 | -115.388796,
175 | 38.713212
176 | ],
177 | "href":"http://api.simplegeo.com/1.0/features/SG_1ZVESZNJmAlJh0isTepcXb_36.218760_-117.499181.json",
178 | "abbr":null,
179 | "classifiers":
180 | [
181 | {
182 | "category":"Legislative District",
183 | "type":"Region",
184 | "subcategory":"National"
185 | }
186 | ]
187 | },
188 | {
189 | "handle":"SG_2MySaPILVQG3MoXrsVehyR_37.215297_-119.663837",
190 | "name":"California",
191 | "license":"http://creativecommons.org/publicdomain/mark/1.0/",
192 | "bounds":
193 | [
194 | -124.482003,
195 | 32.528832,
196 | -114.131211,
197 | 42.009517
198 | ],
199 | "href":"http://api.simplegeo.com/1.0/features/SG_2MySaPILVQG3MoXrsVehyR_37.215297_-119.663837.json",
200 | "abbr":"CA",
201 | "classifiers":
202 | [
203 | {
204 | "category":"Subnational",
205 | "type":"Region",
206 | "subcategory":"State"
207 | }
208 | ]
209 | },
210 | {
211 | "handle":"SG_3tLT0I5cOUWIpoVOBeScOx_41.316130_-119.116571",
212 | "name":"America/Los_Angeles",
213 | "license":"creativecommons.org/publicdomain/zero/1.0/",
214 | "bounds":
215 | [
216 | -124.733253,
217 | 32.534622,
218 | -114.039345,
219 | 49.002892
220 | ],
221 | "href":"http://api.simplegeo.com/1.0/features/SG_3tLT0I5cOUWIpoVOBeScOx_41.316130_-119.116571.json",
222 | "abbr":null,
223 | "classifiers":
224 | [
225 | {
226 | "category":"Time Zone",
227 | "type":"Region",
228 | "subcategory":null
229 | }
230 | ]
231 | },
232 | {
233 | "handle":"SG_3uwSAEdXVBzK1ZER9Nqkdp_45.687160_-112.493107",
234 | "name":"United States of America",
235 | "license":"http://creativecommons.org/publicdomain/mark/1.0/",
236 | "bounds":
237 | [
238 | -179.142471,
239 | 18.930138,
240 | 179.78115,
241 | 71.41218
242 | ],
243 | "href":"http://api.simplegeo.com/1.0/features/SG_3uwSAEdXVBzK1ZER9Nqkdp_45.687160_-112.493107.json",
244 | "abbr":null,
245 | "classifiers":
246 | [
247 | {
248 | "category":"National",
249 | "type":"Region",
250 | "subcategory":null
251 | }
252 | ]
253 | }
254 | ]
255 | }
--------------------------------------------------------------------------------
/test/fixtures/responses/yandex/success.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Россия, Москва, ул. Моховая, д.18
7 | 5
8 | 10
9 |
10 |
11 |
12 |
13 |
14 |
15 | house
16 | Россия, Москва, Моховая улица, 18
17 | exact
18 |
19 |
20 | RU
21 | Россия
22 |
23 | Москва
24 |
25 | Моховая улица
26 |
27 | 18
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | Моховая улица, 18
36 |
37 |
38 | 37.608176 55.751026
39 | 37.616387 55.755657
40 |
41 |
42 |
43 | 37.612281 55.753342
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | house
52 | Россия, Московская область, Орехово-Зуево, Моховая улица, 16
53 | near
54 |
55 |
56 | RU
57 | Россия
58 |
59 | Московская область
60 |
61 | Орехово-Зуево
62 |
63 | Моховая улица
64 |
65 | 16
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | Моховая улица, 16
75 |
76 |
77 | 38.938195 55.805402
78 | 38.946405 55.810026
79 |
80 |
81 |
82 | 38.942300 55.807714
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | house
91 | Россия, Московская область, Шатурский район, Шатура, Моховая улица, 18
92 | exact
93 |
94 |
95 | RU
96 | Россия
97 |
98 | Московская область
99 |
100 | Шатурский район
101 |
102 | Шатура
103 |
104 | Моховая улица
105 |
106 | 18
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 | Моховая улица, 18
117 |
118 |
119 | 39.551106 55.562490
120 | 39.559317 55.567143
121 |
122 |
123 |
124 | 39.555211 55.564816
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | street
133 | Россия, Московская область, Дубна, Моховая улица
134 | street
135 |
136 |
137 | RU
138 | Россия
139 |
140 | Московская область
141 |
142 | Дубна
143 |
144 | Моховая улица
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 | Моховая улица
153 |
154 |
155 | 37.200098 56.746648
156 | 37.204284 56.748163
157 |
158 |
159 |
160 | 37.200745 56.746949
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 | street
169 | Россия, Московская область, Озерский район, Озеры, улица Моховая
170 | street
171 |
172 |
173 | RU
174 | Россия
175 |
176 | Московская область
177 |
178 | Озерский район
179 |
180 | Озеры
181 |
182 | улица Моховая
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 | улица Моховая
192 |
193 |
194 | 38.539415 54.859891
195 | 38.539450 54.861559
196 |
197 |
198 |
199 | 38.539415 54.859891
200 |
201 |
202 |
203 |
204 |
205 |
--------------------------------------------------------------------------------
/test/fixtures/responses/mapquest/multi_result.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 0
5 |
6 |
7 | http://api.mqcdn.com/res/mqlogo.gif
8 | © 2013 MapQuest, Inc.
9 | © 2013 MapQuest, Inc.
10 |
11 |
12 |
13 |
14 |
15 | 217 Union St., NY
16 |
17 |
18 |
19 |
20 |
21 | Stony Brook
22 | NY
23 | Suffolk County
24 |
25 |
26 | US
27 | CITY
28 | A5XCX
29 | false
30 | N
31 |
32 |
33 | 40.925598
34 | -73.141403
35 |
36 |
37 | 0
38 | s
39 |
40 | 40.925598
41 | -73.141403
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | Stony Point
51 | NY
52 | Rockland County
53 |
54 |
55 | US
56 | CITY
57 | A5XCX
58 | false
59 | N
60 |
61 |
62 | 41.229401
63 | -73.987503
64 |
65 |
66 | 0
67 | s
68 |
69 | 41.229401
70 | -73.987503
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | Staatsburg
80 | NY
81 | Dutchess County
82 |
83 |
84 | US
85 | CITY
86 | A5XCX
87 | false
88 | N
89 |
90 |
91 | 41.849701
92 | -73.930603
93 |
94 |
95 | 0
96 | s
97 |
98 | 41.849701
99 | -73.930603
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | Stamford
109 | NY
110 | Delaware County
111 |
112 |
113 | US
114 | CITY
115 | A5XCX
116 | false
117 | N
118 |
119 |
120 | 42.4072
121 | -74.6147
122 |
123 |
124 | 0
125 | s
126 |
127 | 42.4072
128 | -74.6147
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 | Standish
138 | NY
139 | Clinton County
140 |
141 |
142 | US
143 | CITY
144 | A5XCX
145 | false
146 | N
147 |
148 |
149 | 44.689201
150 | -73.949402
151 |
152 |
153 | 0
154 | s
155 |
156 | 44.689201
157 | -73.949402
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 | Stanford Heights
167 | NY
168 | Albany County
169 |
170 |
171 | US
172 | CITY
173 | A5XCX
174 | false
175 | N
176 |
177 |
178 | 42.765598
179 | -73.889397
180 |
181 |
182 | 0
183 | s
184 |
185 | 42.765598
186 | -73.889397
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 | Stanfordville
196 | NY
197 | Dutchess County
198 |
199 |
200 | US
201 | CITY
202 | A5XCX
203 | false
204 | N
205 |
206 |
207 | 41.867199
208 | -73.714699
209 |
210 |
211 | 0
212 | s
213 |
214 | 41.867199
215 | -73.714699
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 | Stannards
225 | NY
226 | Allegany County
227 |
228 |
229 | US
230 | CITY
231 | A5XCX
232 | false
233 | N
234 |
235 |
236 | 42.086399
237 | -77.922501
238 |
239 |
240 | 0
241 | s
242 |
243 | 42.086399
244 | -77.922501
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 | Star Lake
254 | NY
255 | Saint Lawrence County
256 |
257 |
258 | US
259 | CITY
260 | A5XCX
261 | false
262 | N
263 |
264 |
265 | 44.159698
266 | -75.031898
267 |
268 |
269 | 0
270 | s
271 |
272 | 44.159698
273 | -75.031898
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 | Stephentown
283 | NY
284 | Rensselaer County
285 |
286 |
287 | US
288 | CITY
289 | A5XCX
290 | false
291 | N
292 |
293 |
294 | 42.548599
295 | -73.374397
296 |
297 |
298 | 0
299 | s
300 |
301 | 42.548599
302 | -73.374397
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 | -1
313 | true
314 | false
315 |
316 |
317 |
318 |
--------------------------------------------------------------------------------