aub-graticule 0.2.11
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/CHANGELOG.txt +61 -0
- data/LICENSE.txt +30 -0
- data/Manifest.txt +84 -0
- data/README.txt +41 -0
- data/Rakefile +143 -0
- data/VERSION +1 -0
- data/bin/geocode +5 -0
- data/graticule.gemspec +143 -0
- data/init.rb +2 -0
- data/lib/graticule/cli.rb +64 -0
- data/lib/graticule/core_ext.rb +15 -0
- data/lib/graticule/distance/haversine.rb +40 -0
- data/lib/graticule/distance/spherical.rb +52 -0
- data/lib/graticule/distance/vincenty.rb +76 -0
- data/lib/graticule/distance.rb +18 -0
- data/lib/graticule/geocoder/base.rb +116 -0
- data/lib/graticule/geocoder/bogus.rb +15 -0
- data/lib/graticule/geocoder/geocoder_ca.rb +54 -0
- data/lib/graticule/geocoder/geocoder_us.rb +51 -0
- data/lib/graticule/geocoder/google.rb +100 -0
- data/lib/graticule/geocoder/host_ip.rb +41 -0
- data/lib/graticule/geocoder/local_search_maps.rb +44 -0
- data/lib/graticule/geocoder/mapquest.rb +96 -0
- data/lib/graticule/geocoder/meta_carta.rb +32 -0
- data/lib/graticule/geocoder/multi.rb +46 -0
- data/lib/graticule/geocoder/multimap.rb +73 -0
- data/lib/graticule/geocoder/postcode_anywhere.rb +63 -0
- data/lib/graticule/geocoder/rest.rb +18 -0
- data/lib/graticule/geocoder/yahoo.rb +84 -0
- data/lib/graticule/geocoder.rb +21 -0
- data/lib/graticule/location.rb +61 -0
- data/lib/graticule/version.rb +9 -0
- data/lib/graticule.rb +26 -0
- data/site/index.html +114 -0
- data/site/plugin.html +82 -0
- data/site/stylesheets/style.css +69 -0
- data/test/config.yml.default +36 -0
- data/test/fixtures/responses/geocoder_us/success.xml +18 -0
- data/test/fixtures/responses/geocoder_us/unknown.xml +1 -0
- data/test/fixtures/responses/google/badkey.xml +1 -0
- data/test/fixtures/responses/google/limit.xml +10 -0
- data/test/fixtures/responses/google/missing_address.xml +1 -0
- data/test/fixtures/responses/google/only_coordinates.xml +1 -0
- data/test/fixtures/responses/google/partial.xml +1 -0
- data/test/fixtures/responses/google/server_error.xml +10 -0
- data/test/fixtures/responses/google/success.xml +1 -0
- data/test/fixtures/responses/google/success_multiple_results.xml +88 -0
- data/test/fixtures/responses/google/unavailable.xml +1 -0
- data/test/fixtures/responses/google/unknown_address.xml +1 -0
- data/test/fixtures/responses/host_ip/private.txt +4 -0
- data/test/fixtures/responses/host_ip/success.txt +4 -0
- data/test/fixtures/responses/host_ip/unknown.txt +4 -0
- data/test/fixtures/responses/local_search_maps/empty.txt +1 -0
- data/test/fixtures/responses/local_search_maps/not_found.txt +1 -0
- data/test/fixtures/responses/local_search_maps/success.txt +1 -0
- data/test/fixtures/responses/mapquest/multi_result.xml +1 -0
- data/test/fixtures/responses/mapquest/success.xml +1 -0
- data/test/fixtures/responses/meta_carta/bad_address.xml +17 -0
- data/test/fixtures/responses/meta_carta/multiple.xml +33 -0
- data/test/fixtures/responses/meta_carta/success.xml +31 -0
- data/test/fixtures/responses/multimap/missing_params.xml +4 -0
- data/test/fixtures/responses/multimap/no_matches.xml +4 -0
- data/test/fixtures/responses/multimap/success.xml +19 -0
- data/test/fixtures/responses/postcode_anywhere/badkey.xml +9 -0
- data/test/fixtures/responses/postcode_anywhere/canada.xml +16 -0
- data/test/fixtures/responses/postcode_anywhere/empty.xml +16 -0
- data/test/fixtures/responses/postcode_anywhere/success.xml +16 -0
- data/test/fixtures/responses/postcode_anywhere/uk.xml +18 -0
- data/test/fixtures/responses/yahoo/success.xml +3 -0
- data/test/fixtures/responses/yahoo/unknown_address.xml +6 -0
- data/test/mocks/uri.rb +52 -0
- data/test/test_helper.rb +31 -0
- data/test/unit/graticule/distance_test.rb +58 -0
- data/test/unit/graticule/geocoder/geocoder_us_test.rb +43 -0
- data/test/unit/graticule/geocoder/geocoders.rb +56 -0
- data/test/unit/graticule/geocoder/google_test.rb +112 -0
- data/test/unit/graticule/geocoder/host_ip_test.rb +40 -0
- data/test/unit/graticule/geocoder/local_search_maps_test.rb +30 -0
- data/test/unit/graticule/geocoder/mapquest_test.rb +61 -0
- data/test/unit/graticule/geocoder/meta_carta_test.rb +44 -0
- data/test/unit/graticule/geocoder/multi_test.rb +43 -0
- data/test/unit/graticule/geocoder/multimap_test.rb +52 -0
- data/test/unit/graticule/geocoder/postcode_anywhere_test.rb +50 -0
- data/test/unit/graticule/geocoder/yahoo_test.rb +48 -0
- data/test/unit/graticule/geocoder_test.rb +27 -0
- data/test/unit/graticule/location_test.rb +73 -0
- metadata +166 -0
@@ -0,0 +1,52 @@
|
|
1
|
+
module Graticule
|
2
|
+
module Distance
|
3
|
+
|
4
|
+
#
|
5
|
+
# The Spherical Law of Cosines is the simplist though least accurate distance
|
6
|
+
# formula (earth isn't a perfect sphere).
|
7
|
+
#
|
8
|
+
class Spherical < DistanceFormula
|
9
|
+
|
10
|
+
# Calculate the distance between two Locations using the Spherical formula
|
11
|
+
#
|
12
|
+
# Graticule::Distance::Spherical.distance(
|
13
|
+
# Graticule::Location.new(:latitude => 42.7654, :longitude => -86.1085),
|
14
|
+
# Graticule::Location.new(:latitude => 41.849838, :longitude => -87.648193)
|
15
|
+
# )
|
16
|
+
# #=> 101.061720831853
|
17
|
+
#
|
18
|
+
def self.distance(from, to, units = :miles)
|
19
|
+
from_longitude = from.longitude.to_radians
|
20
|
+
from_latitude = from.latitude.to_radians
|
21
|
+
to_longitude = to.longitude.to_radians
|
22
|
+
to_latitude = to.latitude.to_radians
|
23
|
+
|
24
|
+
Math.acos(
|
25
|
+
Math.sin(from_latitude) *
|
26
|
+
Math.sin(to_latitude) +
|
27
|
+
|
28
|
+
Math.cos(from_latitude) *
|
29
|
+
Math.cos(to_latitude) *
|
30
|
+
Math.cos(to_longitude - from_longitude)
|
31
|
+
) * EARTH_RADIUS[units.to_sym]
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.to_sql(options)
|
35
|
+
options = {
|
36
|
+
:units => :miles,
|
37
|
+
:latitude_column => 'latitude',
|
38
|
+
:longitude_column => 'longitude'
|
39
|
+
}.merge(options)
|
40
|
+
%{(ACOS(
|
41
|
+
SIN(RADIANS(#{options[:latitude]})) *
|
42
|
+
SIN(RADIANS(#{options[:latitude_column]})) +
|
43
|
+
COS(RADIANS(#{options[:latitude]})) *
|
44
|
+
COS(RADIANS(#{options[:latitude_column]})) *
|
45
|
+
COS(RADIANS(#{options[:longitude_column]}) - RADIANS(#{options[:longitude]}))
|
46
|
+
) * #{Graticule::Distance::EARTH_RADIUS[options[:units].to_sym]})
|
47
|
+
}.gsub("\n", '').squeeze(" ")
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Graticule
|
2
|
+
module Distance
|
3
|
+
|
4
|
+
#
|
5
|
+
# The Vincenty Formula uses an ellipsoidal model of the earth, which is very accurate.
|
6
|
+
#
|
7
|
+
# Thanks to Chris Veness (http://www.movable-type.co.uk/scripts/LatLongVincenty.html)
|
8
|
+
# for distance formulas.
|
9
|
+
#
|
10
|
+
class Vincenty < DistanceFormula
|
11
|
+
|
12
|
+
# Calculate the distance between two Locations using the Vincenty formula
|
13
|
+
#
|
14
|
+
# Graticule::Distance::Vincenty.distance(
|
15
|
+
# Graticule::Location.new(:latitude => 42.7654, :longitude => -86.1085),
|
16
|
+
# Graticule::Location.new(:latitude => 41.849838, :longitude => -87.648193)
|
17
|
+
# )
|
18
|
+
# #=> 101.070118000159
|
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
|
+
earth_major_axis_radius = EARTH_MAJOR_AXIS_RADIUS[units.to_sym]
|
27
|
+
earth_minor_axis_radius = EARTH_MINOR_AXIS_RADIUS[units.to_sym]
|
28
|
+
|
29
|
+
f = (earth_major_axis_radius - earth_minor_axis_radius) / earth_major_axis_radius
|
30
|
+
|
31
|
+
l = to_longitude - from_longitude
|
32
|
+
u1 = atan((1-f) * tan(from_latitude))
|
33
|
+
u2 = atan((1-f) * tan(to_latitude))
|
34
|
+
sin_u1 = sin(u1)
|
35
|
+
cos_u1 = cos(u1)
|
36
|
+
sin_u2 = sin(u2)
|
37
|
+
cos_u2 = cos(u2)
|
38
|
+
|
39
|
+
lambda = l
|
40
|
+
lambda_p = 2 * PI
|
41
|
+
iteration_limit = 20
|
42
|
+
while (lambda-lambda_p).abs > 1e-12 && (iteration_limit -= 1) > 0
|
43
|
+
sin_lambda = sin(lambda)
|
44
|
+
cos_lambda = cos(lambda)
|
45
|
+
sin_sigma = sqrt((cos_u2*sin_lambda) * (cos_u2*sin_lambda) +
|
46
|
+
(cos_u1*sin_u2-sin_u1*cos_u2*cos_lambda) * (cos_u1*sin_u2-sin_u1*cos_u2*cos_lambda))
|
47
|
+
return 0 if sin_sigma == 0 # co-incident points
|
48
|
+
cos_sigma = sin_u1*sin_u2 + cos_u1*cos_u2*cos_lambda
|
49
|
+
sigma = atan2(sin_sigma, cos_sigma)
|
50
|
+
sin_alpha = cos_u1 * cos_u2 * sin_lambda / sin_sigma
|
51
|
+
cosSqAlpha = 1 - sin_alpha*sin_alpha
|
52
|
+
cos2SigmaM = cos_sigma - 2*sin_u1*sin_u2/cosSqAlpha
|
53
|
+
|
54
|
+
cos2SigmaM = 0 if cos2SigmaM.nan? # equatorial line: cosSqAlpha=0 (§6)
|
55
|
+
|
56
|
+
c = f/16*cosSqAlpha*(4+f*(4-3*cosSqAlpha))
|
57
|
+
lambda_p = lambda
|
58
|
+
lambda = l + (1-c) * f * sin_alpha *
|
59
|
+
(sigma + c*sin_sigma*(cos2SigmaM+c*cos_sigma*(-1+2*cos2SigmaM*cos2SigmaM)))
|
60
|
+
end
|
61
|
+
# formula failed to converge (happens on antipodal points)
|
62
|
+
# We'll call Haversine formula instead.
|
63
|
+
return Haversine.distance(from, to, units) if iteration_limit == 0
|
64
|
+
|
65
|
+
uSq = cosSqAlpha * (earth_major_axis_radius**2 - earth_minor_axis_radius**2) / (earth_minor_axis_radius**2)
|
66
|
+
a = 1 + uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq)))
|
67
|
+
b = uSq/1024 * (256+uSq*(-128+uSq*(74-47*uSq)))
|
68
|
+
delta_sigma = b*sin_sigma*(cos2SigmaM+b/4*(cos_sigma*(-1+2*cos2SigmaM*cos2SigmaM)-
|
69
|
+
b/6*cos2SigmaM*(-3+4*sin_sigma*sin_sigma)*(-3+4*cos2SigmaM*cos2SigmaM)))
|
70
|
+
|
71
|
+
earth_minor_axis_radius * a * (sigma-delta_sigma)
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Graticule
|
2
|
+
module Distance
|
3
|
+
|
4
|
+
EARTH_RADIUS = { :kilometers => 6378.135, :miles => 3963.1676 }
|
5
|
+
# WGS-84 numbers
|
6
|
+
EARTH_MAJOR_AXIS_RADIUS = { :kilometers => 6378.137, :miles => 3963.19059 }
|
7
|
+
EARTH_MINOR_AXIS_RADIUS = { :kilometers => 6356.7523142, :miles => 3949.90276 }
|
8
|
+
|
9
|
+
class DistanceFormula
|
10
|
+
include Math
|
11
|
+
extend Math
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
|
3
|
+
module Graticule #:nodoc:
|
4
|
+
module Geocoder
|
5
|
+
|
6
|
+
# Abstract class for implementing geocoders.
|
7
|
+
#
|
8
|
+
# === Example
|
9
|
+
#
|
10
|
+
# The following methods must be implemented in sublcasses:
|
11
|
+
#
|
12
|
+
# * +initialize+:: Sets @url to the service enpoint.
|
13
|
+
# * +check_error+:: Checks for errors in the server response.
|
14
|
+
# * +parse_response+:: Extracts information from the server response.
|
15
|
+
#
|
16
|
+
# Optionally, you can also override
|
17
|
+
#
|
18
|
+
# * +prepare_response+:: Convert the string response into a different format
|
19
|
+
# that gets passed on to +check_error+ and +parse_response+.
|
20
|
+
#
|
21
|
+
# If you have extra URL paramaters (application id, output type) or need to
|
22
|
+
# perform URL customization, override +make_url+.
|
23
|
+
#
|
24
|
+
# class FakeGeocoder < Base
|
25
|
+
#
|
26
|
+
# def initialize(appid)
|
27
|
+
# @appid = appid
|
28
|
+
# @url = URI.parse 'http://example.com/test'
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# def locate(query)
|
32
|
+
# get :q => query
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# private
|
36
|
+
#
|
37
|
+
# def check_error(xml)
|
38
|
+
# raise Error, xml.elements['error'].text if xml.elements['error']
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# def make_url(params)
|
42
|
+
# params[:appid] = @appid
|
43
|
+
# super params
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# def parse_response(response)
|
47
|
+
# # return Location
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
class Base
|
53
|
+
USER_AGENT = "Mozilla/5.0 (compatible; Graticule/#{Graticule::Version::STRING}; http://graticule.rubyforge.org)"
|
54
|
+
|
55
|
+
def initialize
|
56
|
+
raise NotImplementedError
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def location_from_params(params)
|
62
|
+
case params
|
63
|
+
when Location then params
|
64
|
+
when Hash then Location.new params
|
65
|
+
else
|
66
|
+
raise ArgumentError, "Expected a Graticule::Location or a hash with :street, :locality, :region, :postal_code, and :country attributes"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Check for errors in +response+ and raise appropriate error, if any.
|
71
|
+
# Must return if no error could be found.
|
72
|
+
def check_error(response)
|
73
|
+
raise NotImplementedError
|
74
|
+
end
|
75
|
+
|
76
|
+
# Performs a GET request with +params+. Calls +check_error+ and returns
|
77
|
+
# the result of +parse_response+.
|
78
|
+
def get(params = {})
|
79
|
+
response = prepare_response(make_url(params).open('User-Agent' => USER_AGENT).read)
|
80
|
+
check_error(response)
|
81
|
+
if params[:multiple]
|
82
|
+
parse_response(response)
|
83
|
+
else
|
84
|
+
parse_response(response)[0]
|
85
|
+
end
|
86
|
+
rescue OpenURI::HTTPError => e
|
87
|
+
check_error(prepare_response(e.io.read))
|
88
|
+
raise
|
89
|
+
end
|
90
|
+
|
91
|
+
# Creates a URI from the Hash +params+. Override this then call super if
|
92
|
+
# you need to add extra params like an application id or output type.
|
93
|
+
def make_url(params)
|
94
|
+
escaped_params = params.sort_by { |k,v| k.to_s }.map do |k,v|
|
95
|
+
"#{URI.escape k.to_s}=#{URI.escape v.to_s}"
|
96
|
+
end
|
97
|
+
|
98
|
+
url = @url.dup
|
99
|
+
url.query = escaped_params.join '&'
|
100
|
+
return url
|
101
|
+
end
|
102
|
+
|
103
|
+
# Override to convert the response to something besides a String, which
|
104
|
+
# will get passed to +check_error+ and +parse_response+.
|
105
|
+
def prepare_response(response)
|
106
|
+
response
|
107
|
+
end
|
108
|
+
|
109
|
+
# Must parse results from +response+ into a Location.
|
110
|
+
def parse_response(response)
|
111
|
+
raise NotImplementedError
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Graticule #:nodoc:
|
2
|
+
module Geocoder #:nodoc:
|
3
|
+
|
4
|
+
# Bogus geocoder that can be used for test purposes
|
5
|
+
class Bogus
|
6
|
+
|
7
|
+
# returns a new location with the address set to the original query string
|
8
|
+
def locate(address)
|
9
|
+
Location.new :street => address
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Graticule #:nodoc:
|
2
|
+
module Geocoder #:nodoc:
|
3
|
+
|
4
|
+
# TODO: Reverse Geocoding
|
5
|
+
class GeocoderCa < Rest
|
6
|
+
|
7
|
+
def initialize(auth = nil)
|
8
|
+
@url = URI.parse 'http://geocoder.ca/'
|
9
|
+
@auth = auth
|
10
|
+
end
|
11
|
+
|
12
|
+
def locate(address)
|
13
|
+
get :locate => address.is_a?(String) ? address : location_from_params(address).to_s(:country => false)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def parse_response(xml) #:nodoc:
|
19
|
+
location = Location.new
|
20
|
+
location.latitude = xml.elements['geodata/latt'].text.to_f
|
21
|
+
location.longitude = xml.elements['geodata/longt'].text.to_f
|
22
|
+
location.street = xml.elements['geodata/standard/staddress'].text
|
23
|
+
location.locality = xml.elements['geodata/standard/city'].text
|
24
|
+
location.region = xml.elements['geodata/standard/prov'].text
|
25
|
+
[location]
|
26
|
+
end
|
27
|
+
|
28
|
+
def check_error(xml) #:nodoc:
|
29
|
+
error = xml.elements['geodata/error']
|
30
|
+
if error
|
31
|
+
code = error.elements['code'].text.to_i
|
32
|
+
message = error.elements['description'].text
|
33
|
+
if (1..3).include?(code)
|
34
|
+
raise CredentialsError, message
|
35
|
+
elsif (4..8).include?(code)
|
36
|
+
raise AddressError, message
|
37
|
+
else
|
38
|
+
raise Error, message
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def make_url(params) #:nodoc:
|
44
|
+
params[:auth] = @auth if @auth
|
45
|
+
params[:standard] = 1
|
46
|
+
params[:showpostal] = 1
|
47
|
+
params[:geoit] = 'XML'
|
48
|
+
super params
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Graticule #:nodoc:
|
2
|
+
module Geocoder #:nodoc:
|
3
|
+
|
4
|
+
# A library for lookup up coordinates with geocoder.us' API.
|
5
|
+
#
|
6
|
+
# http://geocoder.us/help/
|
7
|
+
class GeocoderUs < Rest
|
8
|
+
|
9
|
+
# Creates a new GeocoderUs object optionally using +username+ and
|
10
|
+
# +password+.
|
11
|
+
#
|
12
|
+
# You can sign up for a geocoder.us account here:
|
13
|
+
#
|
14
|
+
# http://geocoder.us/user/signup
|
15
|
+
def initialize(user = nil, password = nil)
|
16
|
+
if user and password then
|
17
|
+
@url = URI.parse 'http://geocoder.us/member/service/rest/geocode'
|
18
|
+
@url.user = user
|
19
|
+
@url.password = password
|
20
|
+
else
|
21
|
+
@url = URI.parse 'http://rpc.geocoder.us/service/rest/geocode'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Locates +address+ and returns the address' latitude and longitude or
|
26
|
+
# raises an AddressError.
|
27
|
+
def locate(address)
|
28
|
+
get :address => address.is_a?(String) ? address : location_from_params(address).to_s(:country => false)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def parse_response(xml) #:nodoc:
|
34
|
+
location = Location.new
|
35
|
+
location.street = xml.elements['rdf:RDF/geo:Point/dc:description'].text
|
36
|
+
|
37
|
+
location.latitude = xml.elements['rdf:RDF/geo:Point/geo:lat'].text.to_f
|
38
|
+
location.longitude = xml.elements['rdf:RDF/geo:Point/geo:long'].text.to_f
|
39
|
+
|
40
|
+
[location]
|
41
|
+
end
|
42
|
+
|
43
|
+
def check_error(xml) #:nodoc:
|
44
|
+
raise AddressError, xml.text if xml.text =~ /couldn't find this address! sorry/
|
45
|
+
raise Error, xml.text if xml.text =~ /Your browser sent a request that this server could not understand./
|
46
|
+
raise Error, xml.text if !(xml.text =~ /geo:Point/)
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
|
2
|
+
module Graticule #:nodoc:
|
3
|
+
module Geocoder #:nodoc:
|
4
|
+
|
5
|
+
# First you need a Google Maps API key. You can register for one here:
|
6
|
+
# http://www.google.com/apis/maps/signup.html
|
7
|
+
#
|
8
|
+
# gg = Graticule.service(:google).new(MAPS_API_KEY)
|
9
|
+
# location = gg.locate '1600 Amphitheater Pkwy, Mountain View, CA'
|
10
|
+
# p location.coordinates
|
11
|
+
# #=> [37.423111, -122.081783
|
12
|
+
#
|
13
|
+
class Google < Rest
|
14
|
+
# http://www.google.com/apis/maps/documentation/#Geocoding_HTTP_Request
|
15
|
+
|
16
|
+
# http://www.google.com/apis/maps/documentation/reference.html#GGeoAddressAccuracy
|
17
|
+
PRECISION = {
|
18
|
+
0 => :unknown, # Unknown location. (Since 2.59)
|
19
|
+
1 => :country, # Country level accuracy. (Since 2.59)
|
20
|
+
2 => :state, # Region (state, province, prefecture, etc.) level accuracy. (Since 2.59)
|
21
|
+
3 => :state, # Sub-region (county, municipality, etc.) level accuracy. (Since 2.59)
|
22
|
+
4 => :city, # Town (city, village) level accuracy. (Since 2.59)
|
23
|
+
5 => :zip, # Post code (zip code) level accuracy. (Since 2.59)
|
24
|
+
6 => :street, # Street level accuracy. (Since 2.59)
|
25
|
+
7 => :street, # Intersection level accuracy. (Since 2.59)
|
26
|
+
8 => :address # Address level accuracy. (Since 2.59)
|
27
|
+
}
|
28
|
+
|
29
|
+
# Creates a new GoogleGeocode that will use Google Maps API +key+.
|
30
|
+
def initialize(key)
|
31
|
+
@key = key
|
32
|
+
@url = URI.parse 'http://maps.google.com/maps/geo'
|
33
|
+
end
|
34
|
+
|
35
|
+
# Locates +address+ returning a Location
|
36
|
+
def locate(address)
|
37
|
+
get :q => address.is_a?(String) ? address : location_from_params(address).to_s
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Extracts a Location from +xml+.
|
43
|
+
def parse_response(xml) #:nodoc:
|
44
|
+
longitude, latitude, = xml.elements['/kml/Response/Placemark/Point/coordinates'].text.split(',').map { |v| v.to_f }
|
45
|
+
location = Location.new(:latitude => latitude, :longitude => longitude)
|
46
|
+
address = REXML::XPath.first(xml, '//xal:AddressDetails',
|
47
|
+
'xal' => "urn:oasis:names:tc:ciq:xsdschema:xAL:2.0")
|
48
|
+
|
49
|
+
if address
|
50
|
+
location.street = value(address.elements['.//ThoroughfareName/text()'])
|
51
|
+
location.locality = value(address.elements['.//LocalityName/text()'])
|
52
|
+
location.region = value(address.elements['.//AdministrativeAreaName/text()'])
|
53
|
+
location.postal_code = value(address.elements['.//PostalCodeNumber/text()'])
|
54
|
+
location.country = value(address.elements['.//CountryNameCode/text()'])
|
55
|
+
location.precision = PRECISION[address.attribute('Accuracy').value.to_i] || :unknown
|
56
|
+
end
|
57
|
+
[location]
|
58
|
+
end
|
59
|
+
|
60
|
+
# Extracts and raises an error from +xml+, if any.
|
61
|
+
def check_error(xml) #:nodoc:
|
62
|
+
status = xml.elements['/kml/Response/Status/code'].text.to_i
|
63
|
+
case status
|
64
|
+
when 200 then # ignore, ok
|
65
|
+
when 500 then
|
66
|
+
raise Error, 'server error'
|
67
|
+
when 601 then
|
68
|
+
raise AddressError, 'missing address'
|
69
|
+
when 602 then
|
70
|
+
raise AddressError, 'unknown address'
|
71
|
+
when 603 then
|
72
|
+
raise AddressError, 'unavailable address'
|
73
|
+
when 610 then
|
74
|
+
raise CredentialsError, 'invalid key'
|
75
|
+
when 620 then
|
76
|
+
raise CredentialsError, 'too many queries'
|
77
|
+
else
|
78
|
+
raise Error, "unknown error #{status}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Creates a URL from the Hash +params+. Automatically adds the key and
|
83
|
+
# sets the output type to 'xml'.
|
84
|
+
def make_url(params) #:nodoc:
|
85
|
+
params[:key] = @key
|
86
|
+
params[:output] = 'xml'
|
87
|
+
|
88
|
+
super params
|
89
|
+
end
|
90
|
+
|
91
|
+
def value(element)
|
92
|
+
element.value if element
|
93
|
+
end
|
94
|
+
|
95
|
+
def text(element)
|
96
|
+
element.text if element
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Graticule #:nodoc:
|
4
|
+
module Geocoder #:nodoc:
|
5
|
+
|
6
|
+
class HostIp < Base
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@url = URI.parse 'http://api.hostip.info/get_html.php'
|
10
|
+
end
|
11
|
+
|
12
|
+
# Geocode an IP address using http://hostip.info
|
13
|
+
def locate(address)
|
14
|
+
get :ip => address, :position => true
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def prepare_response(response)
|
20
|
+
# add new line so YAML.load doesn't puke
|
21
|
+
YAML.load(response + "\n")
|
22
|
+
end
|
23
|
+
|
24
|
+
def parse_response(response) #:nodoc:
|
25
|
+
location = Location.new
|
26
|
+
location.latitude = response['Latitude']
|
27
|
+
location.longitude = response['Longitude']
|
28
|
+
location.locality, location.region = response['City'].split(', ')
|
29
|
+
country = response['Country'].match(/\((\w+)\)$/)
|
30
|
+
location.country = country[1] if country
|
31
|
+
[location]
|
32
|
+
end
|
33
|
+
|
34
|
+
def check_error(response) #:nodoc:
|
35
|
+
raise AddressError, 'Unknown' if response['City'] =~ /Unknown City/
|
36
|
+
raise AddressError, 'Private Address' if response['City'] =~ /Private Address/
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Graticule #:nodoc:
|
2
|
+
module Geocoder #:nodoc:
|
3
|
+
|
4
|
+
# A library for lookup of coordinates with http://geo.localsearchmaps.com/
|
5
|
+
#
|
6
|
+
# See http://emad.fano.us/blog/?p=277
|
7
|
+
class LocalSearchMaps < Base
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@url = URI.parse 'http://geo.localsearchmaps.com/'
|
11
|
+
end
|
12
|
+
|
13
|
+
# This web service will handle some addresses outside the US
|
14
|
+
# if given more structured arguments than just a string address
|
15
|
+
# So allow input as a hash for the different arguments (:city, :country, :zip)
|
16
|
+
def locate(params)
|
17
|
+
get params.is_a?(String) ? {:loc => params} : map_attributes(location_from_params(params))
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def map_attributes(location)
|
23
|
+
mapping = {:street => :street, :locality => :city, :region => :state, :postal_code => :zip, :country => :country}
|
24
|
+
mapping.keys.inject({}) do |result,attribute|
|
25
|
+
result[mapping[attribute]] = location.attributes[attribute] unless location.attributes[attribute].blank?
|
26
|
+
result
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def check_error(js)
|
31
|
+
raise AddressError, "Empty Response" if js.nil?
|
32
|
+
raise AddressError, 'Location not found' if js =~ /location not found/
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse_response(js)
|
36
|
+
location = Location.new
|
37
|
+
coordinates = js.match(/map.centerAndZoom\(new GPoint\((.+?), (.+?)\)/)
|
38
|
+
location.longitude = coordinates[1].to_f
|
39
|
+
location.latitude = coordinates[2].to_f
|
40
|
+
[location]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'ruby-debug'
|
2
|
+
|
3
|
+
module Graticule #:nodoc:
|
4
|
+
module Geocoder #:nodoc:
|
5
|
+
|
6
|
+
# Mapquest requires both a client id and a password, which you can
|
7
|
+
# get by registering at:
|
8
|
+
# http://developer.mapquest.com/Home/Register?_devAPISignup_WAR_devAPISignup_action=signup&_devAPISignup_WAR_devAPISignup_clientType=Developer
|
9
|
+
#
|
10
|
+
# mq = Graticule.service(:mapquest).new(CLIENT_ID, PASSWORD)
|
11
|
+
# location = gg.locate('44 Allen Rd., Lovell, ME 04051')
|
12
|
+
# [42.78942, -86.104424]
|
13
|
+
#
|
14
|
+
class Mapquest < Rest
|
15
|
+
# I would link to the documentation here, but there is none that will do anything but confuse you.
|
16
|
+
|
17
|
+
PRECISION = {
|
18
|
+
'L1' => :address,
|
19
|
+
'I1' => :street,
|
20
|
+
'B1' => :street,
|
21
|
+
'B2' => :street,
|
22
|
+
'B3' => :street,
|
23
|
+
'Z3' => :zip,
|
24
|
+
'Z4' => :zip,
|
25
|
+
'Z2' => :zip,
|
26
|
+
'Z1' => :zip,
|
27
|
+
'A5' => :city,
|
28
|
+
'A4' => :county,
|
29
|
+
'A3' => :state,
|
30
|
+
'A1' => :country
|
31
|
+
}
|
32
|
+
|
33
|
+
def initialize(client_id, password)
|
34
|
+
@password = password
|
35
|
+
@client_id = client_id
|
36
|
+
@url = URI.parse('http://geocode.dev.mapquest.com/mq/mqserver.dll')
|
37
|
+
end
|
38
|
+
|
39
|
+
# Locates +address+ returning a Location
|
40
|
+
def locate(address)
|
41
|
+
do_locate(address, false)
|
42
|
+
end
|
43
|
+
|
44
|
+
def locate_all(address)
|
45
|
+
do_locate(address, true)
|
46
|
+
end
|
47
|
+
|
48
|
+
protected
|
49
|
+
|
50
|
+
def do_locate(address, multi)
|
51
|
+
get(:q => address.is_a?(String) ? address : location_from_params(address).to_s, :multiple => multi)
|
52
|
+
end
|
53
|
+
|
54
|
+
def make_url(params) #:nodoc
|
55
|
+
query = "e=5&<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><Geocode Version=\"1\">#{address_string(params[:q])}#{authentication_string}</Geocode>"
|
56
|
+
url = @url.dup
|
57
|
+
url.query = URI.escape(query)
|
58
|
+
url
|
59
|
+
end
|
60
|
+
|
61
|
+
# Extracts a location from +xml+.
|
62
|
+
def parse_response(xml) #:nodoc:
|
63
|
+
locations = []
|
64
|
+
REXML::XPath.each(xml, '/GeocodeResponse/LocationCollection/GeoAddress') do |elem|
|
65
|
+
longitude = elem.elements['LatLng/Lng'].text.to_f
|
66
|
+
latitude = elem.elements['LatLng/Lat'].text.to_f
|
67
|
+
location = Location.new(:latitude => latitude, :longitude => longitude)
|
68
|
+
location.street = value(elem.elements['Street'])
|
69
|
+
location.locality = value(elem.elements['AdminArea5'])
|
70
|
+
location.region = value(elem.elements['AdminArea3'])
|
71
|
+
location.postal_code = value(elem.elements['PostalCode'])
|
72
|
+
location.country = value(elem.elements['AdminArea1'])
|
73
|
+
location.precision = PRECISION[value(elem.elements['ResultCode'])[0,2]]
|
74
|
+
locations << location
|
75
|
+
end
|
76
|
+
locations
|
77
|
+
end
|
78
|
+
|
79
|
+
# Extracts and raises any errors in +xml+
|
80
|
+
def check_error(xml) #:nodoc
|
81
|
+
end
|
82
|
+
|
83
|
+
def value(element)
|
84
|
+
element.text if element
|
85
|
+
end
|
86
|
+
|
87
|
+
def authentication_string
|
88
|
+
"<Authentication Version=\"2\"><Password>#{@password}</Password><ClientId>#{@client_id}</ClientId></Authentication>"
|
89
|
+
end
|
90
|
+
|
91
|
+
def address_string(query)
|
92
|
+
"<Address><Street>#{query}</Street></Address><GeocodeOptionsCollection Count=\"0\"/>"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|