├── 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 | 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
SunnyvaleCA94089-1019US
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 | --------------------------------------------------------------------------------