aub-graticule 0.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|