graticule 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.txt +8 -0
- data/LICENSE.txt +30 -0
- data/Manifest.txt +44 -0
- data/README.txt +10 -0
- data/Rakefile +16 -0
- data/init.rb +2 -0
- data/lib/graticule.rb +14 -0
- data/lib/graticule/distance.rb +24 -0
- data/lib/graticule/distance/haversine.rb +65 -0
- data/lib/graticule/distance/spherical.rb +30 -0
- data/lib/graticule/distance/vincenty.rb +99 -0
- data/lib/graticule/geocoder.rb +26 -0
- data/lib/graticule/geocoders/bogus.rb +12 -0
- data/lib/graticule/geocoders/geocoder_us.rb +45 -0
- data/lib/graticule/geocoders/google.rb +96 -0
- data/lib/graticule/geocoders/meta_carta.rb +102 -0
- data/lib/graticule/geocoders/rest.rb +98 -0
- data/lib/graticule/geocoders/yahoo.rb +101 -0
- data/lib/graticule/location.rb +28 -0
- data/lib/graticule/version.rb +3 -0
- data/test/fixtures/responses/geocoder_us/success.xml +10 -0
- data/test/fixtures/responses/geocoder_us/unknown.xml +1 -0
- data/test/fixtures/responses/google/badkey.xml +10 -0
- data/test/fixtures/responses/google/limit.xml +10 -0
- data/test/fixtures/responses/google/missing_address.xml +10 -0
- data/test/fixtures/responses/google/server_error.xml +10 -0
- data/test/fixtures/responses/google/success.xml +37 -0
- data/test/fixtures/responses/google/unavailable.xml +10 -0
- data/test/fixtures/responses/google/unknown_address.xml +10 -0
- data/test/fixtures/responses/meta_carta/bad_address.xml +9 -0
- data/test/fixtures/responses/meta_carta/multiple.xml +33 -0
- data/test/fixtures/responses/meta_carta/success.xml +23 -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 +51 -0
- data/test/test_helper.rb +31 -0
- data/test/unit/graticule/distance_test.rb +30 -0
- data/test/unit/graticule/geocoder_test.rb +31 -0
- data/test/unit/graticule/geocoders/geocoder_us_test.rb +42 -0
- data/test/unit/graticule/geocoders/geocoders.rb +56 -0
- data/test/unit/graticule/geocoders/google_test.rb +22 -0
- data/test/unit/graticule/geocoders/meta_carta_test.rb +70 -0
- data/test/unit/graticule/geocoders/yahoo_test.rb +49 -0
- data/test/unit/graticule/location_test.rb +38 -0
- metadata +102 -0
data/CHANGELOG.txt
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
Copyright 2006 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
|
+
|
data/Manifest.txt
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
CHANGELOG.txt
|
2
|
+
LICENSE.txt
|
3
|
+
Manifest.txt
|
4
|
+
README.txt
|
5
|
+
Rakefile
|
6
|
+
init.rb
|
7
|
+
lib/graticule.rb
|
8
|
+
lib/graticule/distance.rb
|
9
|
+
lib/graticule/distance/haversine.rb
|
10
|
+
lib/graticule/distance/spherical.rb
|
11
|
+
lib/graticule/distance/vincenty.rb
|
12
|
+
lib/graticule/geocoder.rb
|
13
|
+
lib/graticule/geocoders/bogus.rb
|
14
|
+
lib/graticule/geocoders/geocoder_us.rb
|
15
|
+
lib/graticule/geocoders/google.rb
|
16
|
+
lib/graticule/geocoders/meta_carta.rb
|
17
|
+
lib/graticule/geocoders/rest.rb
|
18
|
+
lib/graticule/geocoders/yahoo.rb
|
19
|
+
lib/graticule/location.rb
|
20
|
+
lib/graticule/version.rb
|
21
|
+
test/fixtures/responses/geocoder_us/success.xml
|
22
|
+
test/fixtures/responses/geocoder_us/unknown.xml
|
23
|
+
test/fixtures/responses/google/badkey.xml
|
24
|
+
test/fixtures/responses/google/limit.xml
|
25
|
+
test/fixtures/responses/google/missing_address.xml
|
26
|
+
test/fixtures/responses/google/server_error.xml
|
27
|
+
test/fixtures/responses/google/success.xml
|
28
|
+
test/fixtures/responses/google/unavailable.xml
|
29
|
+
test/fixtures/responses/google/unknown_address.xml
|
30
|
+
test/fixtures/responses/meta_carta/bad_address.xml
|
31
|
+
test/fixtures/responses/meta_carta/multiple.xml
|
32
|
+
test/fixtures/responses/meta_carta/success.xml
|
33
|
+
test/fixtures/responses/yahoo/success.xml
|
34
|
+
test/fixtures/responses/yahoo/unknown_address.xml
|
35
|
+
test/mocks/uri.rb
|
36
|
+
test/test_helper.rb
|
37
|
+
test/unit/graticule/distance_test.rb
|
38
|
+
test/unit/graticule/geocoder_test.rb
|
39
|
+
test/unit/graticule/geocoders/geocoder_us_test.rb
|
40
|
+
test/unit/graticule/geocoders/geocoders.rb
|
41
|
+
test/unit/graticule/geocoders/google_test.rb
|
42
|
+
test/unit/graticule/geocoders/meta_carta_test.rb
|
43
|
+
test/unit/graticule/geocoders/yahoo_test.rb
|
44
|
+
test/unit/graticule/location_test.rb
|
data/README.txt
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
= Graticule
|
2
|
+
|
3
|
+
Graticule is a geocoding API for looking up address coordinates. It supports supports the Yahoo, Google, Geocoder.us, and MetaCarta APIs.
|
4
|
+
|
5
|
+
= Usage
|
6
|
+
|
7
|
+
require 'rubygems'
|
8
|
+
require 'graticule'
|
9
|
+
geocoder = Graticule.service(:google).new "api_key"
|
10
|
+
location = geocoder.locate "61 East 9th Street, Holland, MI"
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'hoe'
|
3
|
+
require File.join(File.dirname(__FILE__), 'lib', 'graticule', 'version.rb')
|
4
|
+
|
5
|
+
Hoe.new("graticule", Graticule::Version) do |p|
|
6
|
+
p.rubyforge_name = "graticule"
|
7
|
+
p.author = 'Brandon Keepers'
|
8
|
+
p.email = 'brandon@opensoul.org'
|
9
|
+
p.summary = "API for using all the popular geocoding services."
|
10
|
+
p.description = 'Graticule is a geocoding API that provides a common interface to all the popular services, including Google, Yahoo, Geocoder.us, and MetaCarta.'
|
11
|
+
p.url = 'http://graticule.rubyforge.org'
|
12
|
+
p.need_tar = true
|
13
|
+
p.need_zip = true
|
14
|
+
p.test_globs = ['test/**/*_test.rb']
|
15
|
+
p.changes = p.paragraphs_of('CHANGELOG.txt', 0..1).join("\n\n")
|
16
|
+
end
|
data/init.rb
ADDED
data/lib/graticule.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require 'graticule/location'
|
4
|
+
require 'graticule/geocoder'
|
5
|
+
require 'graticule/geocoders/bogus'
|
6
|
+
require 'graticule/geocoders/rest'
|
7
|
+
require 'graticule/geocoders/google'
|
8
|
+
require 'graticule/geocoders/yahoo'
|
9
|
+
require 'graticule/geocoders/geocoder_us'
|
10
|
+
require 'graticule/geocoders/meta_carta'
|
11
|
+
require 'graticule/distance'
|
12
|
+
require 'graticule/distance/haversine'
|
13
|
+
require 'graticule/distance/spherical'
|
14
|
+
require 'graticule/distance/vincenty'
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Graticule
|
2
|
+
module Distance
|
3
|
+
EARTH_RADIUS = { :kilometers => 6378.135, :miles => 3963.1676 }
|
4
|
+
# WGS-84 numbers
|
5
|
+
EARTH_MAJOR_AXIS_RADIUS = { :kilometers => 6378.137, :miles => 3963.19059 }
|
6
|
+
EARTH_MINOR_AXIS_RADIUS = { :kilometers => 6356.7523142, :miles => 3949.90276 }
|
7
|
+
|
8
|
+
class DistanceFormula
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
raise NotImplementedError
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.deg2rad(deg)
|
15
|
+
(deg * Math::PI / 180)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.rad2deg(rad)
|
19
|
+
(rad * 180 / Math::PI)
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
|
2
|
+
module Graticule
|
3
|
+
module Distance
|
4
|
+
|
5
|
+
#
|
6
|
+
# Thanks to Chris Veness for distance formulas.
|
7
|
+
# * http://www.movable-type.co.uk/scripts/LatLong.html
|
8
|
+
#
|
9
|
+
# Distance Measured usign the Haversine Formula
|
10
|
+
# Works better at small distances than the Spherical Law of Cosines
|
11
|
+
# R = earth’s radius (mean radius = 6,371km)
|
12
|
+
# Δlat = lat2− lat1
|
13
|
+
# Δlong = long2− long1
|
14
|
+
# a = sin²(Δlat/2) + cos(lat1).cos(lat2).sin²(Δlong/2)
|
15
|
+
# c = 2.atan2(√a, √(1−a))
|
16
|
+
# d = R.c
|
17
|
+
#
|
18
|
+
class Haversine < DistanceFormula
|
19
|
+
|
20
|
+
def self.distance(from, to, units = :miles)
|
21
|
+
from_longitude = deg2rad(from.longitude)
|
22
|
+
from_latitude = deg2rad(from.latitude)
|
23
|
+
to_longitude = deg2rad(to.longitude)
|
24
|
+
to_latitude = deg2rad(to.latitude)
|
25
|
+
|
26
|
+
latitude_delta = to_latitude - from_latitude
|
27
|
+
longitude_delta = to_longitude - from_longitude
|
28
|
+
|
29
|
+
a = Math.sin(latitude_delta/2)**2 +
|
30
|
+
Math.cos(from_latitude) *
|
31
|
+
Math.cos(to_latitude) *
|
32
|
+
Math.sin(longitude_delta/2)**2
|
33
|
+
|
34
|
+
c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))
|
35
|
+
|
36
|
+
d = EARTH_RADIUS[units.to_sym] * c
|
37
|
+
end
|
38
|
+
|
39
|
+
# # What formula is this?
|
40
|
+
# def self.distance(from, to, units = :miles)
|
41
|
+
# from_longitude = deg2rad(from.longitude)
|
42
|
+
# from_latitude = deg2rad(from.latitude)
|
43
|
+
# to_longitude = deg2rad(to.longitude)
|
44
|
+
# to_latitude = deg2rad(to.latitude)
|
45
|
+
#
|
46
|
+
# Math.acos(
|
47
|
+
# Math.cos(from_longitude) *
|
48
|
+
# Math.cos(to_longitude) *
|
49
|
+
# Math.cos(from_latitude) *
|
50
|
+
# Math.cos(to_latitude) +
|
51
|
+
#
|
52
|
+
# Math.cos(from_latitude) *
|
53
|
+
# Math.sin(from_longitude) *
|
54
|
+
# Math.cos(to_latitude) *
|
55
|
+
# Math.sin(to_longitude) +
|
56
|
+
#
|
57
|
+
# Math.sin(from_latitude) *
|
58
|
+
# Math.sin(to_latitude)
|
59
|
+
# ) * EARTH_RADIUS[units.to_sym]
|
60
|
+
# end
|
61
|
+
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Graticule
|
2
|
+
module Distance
|
3
|
+
|
4
|
+
#
|
5
|
+
# Distance Measured usign the Spherical Law of Cosines
|
6
|
+
# Simplist though least accurate (earth isn't a perfect sphere)
|
7
|
+
# d = acos(sin(lat1).sin(lat2)+cos(lat1).cos(lat2).cos(long2−long1)).R
|
8
|
+
#
|
9
|
+
class Spherical < DistanceFormula
|
10
|
+
|
11
|
+
def self.distance(from, to, units = :miles)
|
12
|
+
from_longitude = deg2rad(from.longitude)
|
13
|
+
from_latitude = deg2rad(from.latitude)
|
14
|
+
to_longitude = deg2rad(to.longitude)
|
15
|
+
to_latitude = deg2rad(to.latitude)
|
16
|
+
|
17
|
+
Math.acos(
|
18
|
+
Math.sin(from_latitude) *
|
19
|
+
Math.sin(to_latitude) +
|
20
|
+
|
21
|
+
Math.cos(from_latitude) *
|
22
|
+
Math.cos(to_latitude) *
|
23
|
+
Math.cos(to_longitude - from_longitude)
|
24
|
+
) * EARTH_RADIUS[units.to_sym]
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Graticule
|
2
|
+
module Distance
|
3
|
+
|
4
|
+
#
|
5
|
+
# Thanks to Chris Veness for distance formulas.
|
6
|
+
# * http://www.movable-type.co.uk/scripts/LatLongVincenty.html
|
7
|
+
#
|
8
|
+
# Distance Measured usign the Vincenty Formula
|
9
|
+
# Very accurate, using an accurate ellipsoidal model of the earth
|
10
|
+
# a, b = major & minor semiaxes of the ellipsoid
|
11
|
+
# f = flattening (a−b)/a
|
12
|
+
# φ1, φ2 = geodetic latitude
|
13
|
+
# L = difference in longitude
|
14
|
+
# U1 = atan((1−f).tanφ1) (U is ‘reduced latitude’)
|
15
|
+
# U2 = atan((1−f).tanφ2)
|
16
|
+
# λ = L, λ′ = 2π
|
17
|
+
# while abs(λ−λ′) > 10-12 { (i.e. 0.06mm)
|
18
|
+
# sinσ = √[ (cosU2.sinλ)² + (cosU1.sinU2 − sinU1.cosU2.cosλ)² ] (14)
|
19
|
+
# cosσ = sinU1.sinU2 + cosU1.cosU2.cosλ (15)
|
20
|
+
# σ = atan2(sinσ, cosσ) (16)
|
21
|
+
# sinα = cosU1.cosU2.sinλ / sinσ (17)
|
22
|
+
# cos²α = 1 − sin²α (trig identity; §6)
|
23
|
+
# cos2σm = cosσ − 2.sinU1.sinU2/cos²α (18)
|
24
|
+
# C = f/16.cos²α.[4+f.(4−3.cos²α)] (10)
|
25
|
+
# λ′ = λ
|
26
|
+
# λ = L + (1−C).f.sinα.{σ+C.sinσ.[cos2σm+C.cosσ.(−1+2.cos²2σm)]} (11)
|
27
|
+
# }
|
28
|
+
# u² = cos²α.(a²−b²)/b²
|
29
|
+
# A = 1+u²/16384.{4096+u².[−768+u².(320−175.u²)]} (3)
|
30
|
+
# B = u²/1024.{256+u².[−128+u².(74−47.u²)]} (4)
|
31
|
+
# Δσ = B.sinσ.{cos2σm+B/4.[cosσ.(−1+2.cos²2σm) − B/6.cos2σm.(−3+4.sin²σ).(−3+4.cos²2σm)]} (6)
|
32
|
+
# s = b.A.(σ−Δσ) (19)
|
33
|
+
# α1 = atan2(cosU2.sinλ, cosU1.sinU2 − sinU1.cosU2.cosλ) (20)
|
34
|
+
# α2 = atan2(cosU1.sinλ, −sinU1.cosU2 + cosU1.sinU2.cosλ) (21)
|
35
|
+
# Where:
|
36
|
+
#
|
37
|
+
# s is the distance (in the same units as a & b)
|
38
|
+
# α1 is the initial bearing, or forward azimuth
|
39
|
+
# α2 is the final bearing (in direction p1→p2)
|
40
|
+
#
|
41
|
+
class Vincenty < DistanceFormula
|
42
|
+
|
43
|
+
def self.distance(from, to, units = :miles)
|
44
|
+
from_longitude = deg2rad(from.longitude)
|
45
|
+
from_latitude = deg2rad(from.latitude)
|
46
|
+
to_longitude = deg2rad(to.longitude)
|
47
|
+
to_latitude = deg2rad(to.latitude)
|
48
|
+
|
49
|
+
earth_major_axis_radius = EARTH_MAJOR_AXIS_RADIUS[units.to_sym]
|
50
|
+
earth_minor_axis_radius = EARTH_MINOR_AXIS_RADIUS[units.to_sym]
|
51
|
+
|
52
|
+
f = (earth_major_axis_radius - earth_minor_axis_radius) / earth_major_axis_radius
|
53
|
+
|
54
|
+
l = to_longitude - from_longitude
|
55
|
+
u1 = Math.atan((1-f) * Math.tan(from_latitude))
|
56
|
+
u2 = Math.atan((1-f) * Math.tan(to_latitude))
|
57
|
+
sinU1 = Math.sin(u1)
|
58
|
+
cosU1 = Math.cos(u1)
|
59
|
+
sinU2 = Math.sin(u2)
|
60
|
+
cosU2 = Math.cos(u2)
|
61
|
+
|
62
|
+
lambda = l
|
63
|
+
lambdaP = 2*Math::PI
|
64
|
+
iterLimit = 20;
|
65
|
+
while (lambda-lambdaP).abs > 1e-12 && --iterLimit>0
|
66
|
+
sinLambda = Math.sin(lambda)
|
67
|
+
cosLambda = Math.cos(lambda)
|
68
|
+
sinSigma = Math.sqrt((cosU2*sinLambda) * (cosU2*sinLambda) +
|
69
|
+
(cosU1*sinU2-sinU1*cosU2*cosLambda) * (cosU1*sinU2-sinU1*cosU2*cosLambda))
|
70
|
+
return 0 if sinSigma==0 # co-incident points
|
71
|
+
cosSigma = sinU1*sinU2 + cosU1*cosU2*cosLambda
|
72
|
+
sigma = Math.atan2(sinSigma, cosSigma)
|
73
|
+
sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma
|
74
|
+
cosSqAlpha = 1 - sinAlpha*sinAlpha
|
75
|
+
cos2SigmaM = cosSigma - 2*sinU1*sinU2/cosSqAlpha
|
76
|
+
|
77
|
+
cos2SigmaM = 0 if cos2SigmaM.nan? # equatorial line: cosSqAlpha=0 (§6)
|
78
|
+
|
79
|
+
c = f/16*cosSqAlpha*(4+f*(4-3*cosSqAlpha))
|
80
|
+
lambdaP = lambda
|
81
|
+
lambda = l + (1-c) * f * sinAlpha *
|
82
|
+
(sigma + c*sinSigma*(cos2SigmaM+c*cosSigma*(-1+2*cos2SigmaM*cos2SigmaM)))
|
83
|
+
end
|
84
|
+
return NaN if (iterLimit==0) # formula failed to converge
|
85
|
+
|
86
|
+
uSq = cosSqAlpha * (earth_major_axis_radius**2 - earth_minor_axis_radius**2) / (earth_minor_axis_radius**2);
|
87
|
+
bigA = 1 + uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq)));
|
88
|
+
bigB = uSq/1024 * (256+uSq*(-128+uSq*(74-47*uSq)));
|
89
|
+
deltaSigma = bigB*sinSigma*(cos2SigmaM+bigB/4*(cosSigma*(-1+2*cos2SigmaM*cos2SigmaM)-
|
90
|
+
bigB/6*cos2SigmaM*(-3+4*sinSigma*sinSigma)*(-3+4*cos2SigmaM*cos2SigmaM)));
|
91
|
+
s = earth_minor_axis_radius*bigA*(sigma-deltaSigma);
|
92
|
+
|
93
|
+
#s = s.toFixed(3) # round to 1mm precision
|
94
|
+
return s
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
|
2
|
+
module Graticule
|
3
|
+
|
4
|
+
def self.service(name)
|
5
|
+
self.const_get "#{name}_geocoder".camelize
|
6
|
+
end
|
7
|
+
|
8
|
+
# The Geocode class is the base class for all geocoder implementations. The
|
9
|
+
# geocoders must implement:
|
10
|
+
#
|
11
|
+
# * locate(address)
|
12
|
+
#
|
13
|
+
class Geocoder
|
14
|
+
def initialize
|
15
|
+
raise NotImplementedError
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Base error class
|
20
|
+
class Error < RuntimeError; end
|
21
|
+
class CredentialsError < Error; end
|
22
|
+
|
23
|
+
# Raised when you try to locate an invalid address.
|
24
|
+
class AddressError < Error; end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Graticule #:nodoc:
|
2
|
+
|
3
|
+
# A library for lookup up coordinates with geocoder.us' API.
|
4
|
+
#
|
5
|
+
# http://geocoder.us/help/
|
6
|
+
class GeocoderUsGeocoder < RestGeocoder
|
7
|
+
|
8
|
+
# Creates a new GeocoderUs object optionally using +username+ and
|
9
|
+
# +password+.
|
10
|
+
#
|
11
|
+
# You can sign up for a geocoder.us account here:
|
12
|
+
#
|
13
|
+
# http://geocoder.us/user/signup
|
14
|
+
def initialize(user = nil, password = nil)
|
15
|
+
if user and password then
|
16
|
+
@url = URI.parse 'http://geocoder.us/member/service/rest/geocode'
|
17
|
+
@url.user = user
|
18
|
+
@url.password = password
|
19
|
+
else
|
20
|
+
@url = URI.parse 'http://rpc.geocoder.us/service/rest/geocode'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Locates +address+ and returns the address' latitude and longitude or
|
25
|
+
# raises an AddressError.
|
26
|
+
def locate(address)
|
27
|
+
get :address => address
|
28
|
+
end
|
29
|
+
|
30
|
+
def parse_response(xml)
|
31
|
+
location = Location.new
|
32
|
+
location.street = xml.elements['rdf:RDF/geo:Point/dc:description'].text
|
33
|
+
|
34
|
+
location.latitude = xml.elements['rdf:RDF/geo:Point/geo:lat'].text.to_f
|
35
|
+
location.longitude = xml.elements['rdf:RDF/geo:Point/geo:long'].text.to_f
|
36
|
+
|
37
|
+
return location
|
38
|
+
end
|
39
|
+
|
40
|
+
def check_error(xml)
|
41
|
+
raise AddressError, xml.text if xml.text == 'couldn\'t find this address! sorry'
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
|
2
|
+
module Graticule
|
3
|
+
|
4
|
+
# First you need a Google Maps API key. You can register for one here:
|
5
|
+
# http://www.google.com/apis/maps/signup.html
|
6
|
+
#
|
7
|
+
# Then you create a GoogleGeocode object and start locating addresses:
|
8
|
+
#
|
9
|
+
# require 'rubygems'
|
10
|
+
# require 'graticule'
|
11
|
+
#
|
12
|
+
# gg = Graticule.service(:google).new(:key => MAPS_API_KEY)
|
13
|
+
# location = gg.locate '1600 Amphitheater Pkwy, Mountain View, CA'
|
14
|
+
# p location.coordinates
|
15
|
+
#
|
16
|
+
class GoogleGeocoder < RestGeocoder
|
17
|
+
# http://www.google.com/apis/maps/documentation/#Geocoding_HTTP_Request
|
18
|
+
|
19
|
+
# http://www.google.com/apis/maps/documentation/reference.html#GGeoAddressAccuracy
|
20
|
+
PRECISION = {
|
21
|
+
0 => :unknown, # Unknown location. (Since 2.59)
|
22
|
+
1 => :country, # Country level accuracy. (Since 2.59)
|
23
|
+
2 => :state, # Region (state, province, prefecture, etc.) level accuracy. (Since 2.59)
|
24
|
+
3 => :state, # Sub-region (county, municipality, etc.) level accuracy. (Since 2.59)
|
25
|
+
4 => :city, # Town (city, village) level accuracy. (Since 2.59)
|
26
|
+
5 => :zip, # Post code (zip code) level accuracy. (Since 2.59)
|
27
|
+
6 => :street, # Street level accuracy. (Since 2.59)
|
28
|
+
7 => :street, # Intersection level accuracy. (Since 2.59)
|
29
|
+
8 => :address # Address level accuracy. (Since 2.59)
|
30
|
+
}
|
31
|
+
|
32
|
+
# Creates a new GoogleGeocode that will use Google Maps API key +key+. You
|
33
|
+
# can sign up for an API key here:
|
34
|
+
#
|
35
|
+
# http://www.google.com/apis/maps/signup.html
|
36
|
+
def initialize(key)
|
37
|
+
@key = key
|
38
|
+
@url = URI.parse 'http://maps.google.com/maps/geo'
|
39
|
+
end
|
40
|
+
|
41
|
+
# Locates +address+ returning a Location
|
42
|
+
def locate(address)
|
43
|
+
get :q => address
|
44
|
+
end
|
45
|
+
|
46
|
+
# Extracts a Location from +xml+.
|
47
|
+
def parse_response(xml)
|
48
|
+
longitude, latitude, = xml.elements['/kml/Response/Placemark/Point/coordinates'].text.split(',').map { |v| v.to_f }
|
49
|
+
Location.new \
|
50
|
+
:street => text(xml.elements['/kml/Response/Placemark/AddressDetails/Country/AdministrativeArea/SubAdministrativeArea/Locality/Thoroughfare/ThoroughfareName']),
|
51
|
+
:city => text(xml.elements['/kml/Response/Placemark/AddressDetails/Country/AdministrativeArea/SubAdministrativeArea/Locality/LocalityName']),
|
52
|
+
:state => text(xml.elements['/kml/Response/Placemark/AddressDetails/Country/AdministrativeArea/AdministrativeAreaName']),
|
53
|
+
:zip => text(xml.elements['/kml/Response/Placemark/AddressDetails/Country/AdministrativeArea/SubAdministrativeArea/Locality/PostalCode/PostalCodeNumber']),
|
54
|
+
:country => text(xml.elements['/kml/Response/Placemark/AddressDetails/Country/CountryNameCode']),
|
55
|
+
:latitude => latitude,
|
56
|
+
:longitude => longitude,
|
57
|
+
:precision => PRECISION[xml.elements['/kml/Response/Placemark/AddressDetails'].attribute('Accuracy').value.to_i] || :unknown
|
58
|
+
end
|
59
|
+
|
60
|
+
# Extracts and raises an error from +xml+, if any.
|
61
|
+
def check_error(xml)
|
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)
|
85
|
+
params[:key] = @key
|
86
|
+
params[:output] = 'xml'
|
87
|
+
|
88
|
+
super params
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
def text(element)
|
93
|
+
element.text if element
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|