graticule 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/CHANGELOG.txt +8 -0
  2. data/LICENSE.txt +30 -0
  3. data/Manifest.txt +44 -0
  4. data/README.txt +10 -0
  5. data/Rakefile +16 -0
  6. data/init.rb +2 -0
  7. data/lib/graticule.rb +14 -0
  8. data/lib/graticule/distance.rb +24 -0
  9. data/lib/graticule/distance/haversine.rb +65 -0
  10. data/lib/graticule/distance/spherical.rb +30 -0
  11. data/lib/graticule/distance/vincenty.rb +99 -0
  12. data/lib/graticule/geocoder.rb +26 -0
  13. data/lib/graticule/geocoders/bogus.rb +12 -0
  14. data/lib/graticule/geocoders/geocoder_us.rb +45 -0
  15. data/lib/graticule/geocoders/google.rb +96 -0
  16. data/lib/graticule/geocoders/meta_carta.rb +102 -0
  17. data/lib/graticule/geocoders/rest.rb +98 -0
  18. data/lib/graticule/geocoders/yahoo.rb +101 -0
  19. data/lib/graticule/location.rb +28 -0
  20. data/lib/graticule/version.rb +3 -0
  21. data/test/fixtures/responses/geocoder_us/success.xml +10 -0
  22. data/test/fixtures/responses/geocoder_us/unknown.xml +1 -0
  23. data/test/fixtures/responses/google/badkey.xml +10 -0
  24. data/test/fixtures/responses/google/limit.xml +10 -0
  25. data/test/fixtures/responses/google/missing_address.xml +10 -0
  26. data/test/fixtures/responses/google/server_error.xml +10 -0
  27. data/test/fixtures/responses/google/success.xml +37 -0
  28. data/test/fixtures/responses/google/unavailable.xml +10 -0
  29. data/test/fixtures/responses/google/unknown_address.xml +10 -0
  30. data/test/fixtures/responses/meta_carta/bad_address.xml +9 -0
  31. data/test/fixtures/responses/meta_carta/multiple.xml +33 -0
  32. data/test/fixtures/responses/meta_carta/success.xml +23 -0
  33. data/test/fixtures/responses/yahoo/success.xml +3 -0
  34. data/test/fixtures/responses/yahoo/unknown_address.xml +6 -0
  35. data/test/mocks/uri.rb +51 -0
  36. data/test/test_helper.rb +31 -0
  37. data/test/unit/graticule/distance_test.rb +30 -0
  38. data/test/unit/graticule/geocoder_test.rb +31 -0
  39. data/test/unit/graticule/geocoders/geocoder_us_test.rb +42 -0
  40. data/test/unit/graticule/geocoders/geocoders.rb +56 -0
  41. data/test/unit/graticule/geocoders/google_test.rb +22 -0
  42. data/test/unit/graticule/geocoders/meta_carta_test.rb +70 -0
  43. data/test/unit/graticule/geocoders/yahoo_test.rb +49 -0
  44. data/test/unit/graticule/location_test.rb +38 -0
  45. metadata +102 -0
@@ -0,0 +1,8 @@
1
+
2
+ 0.1.1
3
+ * fixed bug in Yahoo that raised error when street address not returned
4
+ * migrated to Hoe (http://seattlerb.rubyforge.org/hoe/)
5
+ * added Haversine, Spherical and Vincenty distance calculations
6
+
7
+ 0.1 (2006-10-31)
8
+ * Initial release
@@ -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
+
@@ -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
@@ -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"
@@ -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
@@ -0,0 +1,2 @@
1
+
2
+ require 'graticule'
@@ -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,12 @@
1
+
2
+ module Graticule #:nodoc:
3
+
4
+ # Bogus geocoder that can be used for test purposes
5
+ class BogusGeocoder < Geocoder
6
+
7
+ def locate(address)
8
+ Location.new
9
+ end
10
+
11
+ end
12
+ 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