norman-graticule 0.2.7

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.
Files changed (71) hide show
  1. data/CHANGELOG.txt +46 -0
  2. data/LICENSE.txt +30 -0
  3. data/Manifest.txt +73 -0
  4. data/README.txt +29 -0
  5. data/Rakefile +86 -0
  6. data/bin/geocode +5 -0
  7. data/init.rb +2 -0
  8. data/lib/graticule/cli.rb +64 -0
  9. data/lib/graticule/distance/haversine.rb +40 -0
  10. data/lib/graticule/distance/spherical.rb +52 -0
  11. data/lib/graticule/distance/vincenty.rb +76 -0
  12. data/lib/graticule/distance.rb +29 -0
  13. data/lib/graticule/geocoder/base.rb +113 -0
  14. data/lib/graticule/geocoder/bogus.rb +15 -0
  15. data/lib/graticule/geocoder/geocoder_ca.rb +54 -0
  16. data/lib/graticule/geocoder/geocoder_us.rb +49 -0
  17. data/lib/graticule/geocoder/google.rb +120 -0
  18. data/lib/graticule/geocoder/host_ip.rb +41 -0
  19. data/lib/graticule/geocoder/local_search_maps.rb +45 -0
  20. data/lib/graticule/geocoder/map_quest.rb +109 -0
  21. data/lib/graticule/geocoder/meta_carta.rb +33 -0
  22. data/lib/graticule/geocoder/multi.rb +46 -0
  23. data/lib/graticule/geocoder/postcode_anywhere.rb +63 -0
  24. data/lib/graticule/geocoder/rest.rb +18 -0
  25. data/lib/graticule/geocoder/yahoo.rb +102 -0
  26. data/lib/graticule/geocoder.rb +21 -0
  27. data/lib/graticule/location.rb +57 -0
  28. data/lib/graticule/version.rb +9 -0
  29. data/lib/graticule.rb +24 -0
  30. data/test/config.yml.default +36 -0
  31. data/test/fixtures/responses/geocoder_us/success.xml +18 -0
  32. data/test/fixtures/responses/geocoder_us/unknown.xml +1 -0
  33. data/test/fixtures/responses/google/badkey.xml +1 -0
  34. data/test/fixtures/responses/google/limit.xml +10 -0
  35. data/test/fixtures/responses/google/missing_address.xml +1 -0
  36. data/test/fixtures/responses/google/only_coordinates.xml +1 -0
  37. data/test/fixtures/responses/google/partial.xml +1 -0
  38. data/test/fixtures/responses/google/server_error.xml +10 -0
  39. data/test/fixtures/responses/google/success.xml +1 -0
  40. data/test/fixtures/responses/google/unavailable.xml +1 -0
  41. data/test/fixtures/responses/google/unknown_address.xml +1 -0
  42. data/test/fixtures/responses/host_ip/private.txt +4 -0
  43. data/test/fixtures/responses/host_ip/success.txt +4 -0
  44. data/test/fixtures/responses/host_ip/unknown.txt +4 -0
  45. data/test/fixtures/responses/local_search_maps/empty.txt +1 -0
  46. data/test/fixtures/responses/local_search_maps/not_found.txt +1 -0
  47. data/test/fixtures/responses/local_search_maps/success.txt +1 -0
  48. data/test/fixtures/responses/meta_carta/bad_address.xml +17 -0
  49. data/test/fixtures/responses/meta_carta/multiple.xml +33 -0
  50. data/test/fixtures/responses/meta_carta/success.xml +31 -0
  51. data/test/fixtures/responses/postcode_anywhere/badkey.xml +9 -0
  52. data/test/fixtures/responses/postcode_anywhere/canada.xml +16 -0
  53. data/test/fixtures/responses/postcode_anywhere/empty.xml +16 -0
  54. data/test/fixtures/responses/postcode_anywhere/success.xml +16 -0
  55. data/test/fixtures/responses/postcode_anywhere/uk.xml +18 -0
  56. data/test/fixtures/responses/yahoo/success.xml +3 -0
  57. data/test/fixtures/responses/yahoo/unknown_address.xml +6 -0
  58. data/test/mocks/uri.rb +52 -0
  59. data/test/test_helper.rb +32 -0
  60. data/test/unit/graticule/distance_test.rb +58 -0
  61. data/test/unit/graticule/geocoder/geocoder_us_test.rb +43 -0
  62. data/test/unit/graticule/geocoder/google_test.rb +107 -0
  63. data/test/unit/graticule/geocoder/host_ip_test.rb +40 -0
  64. data/test/unit/graticule/geocoder/local_search_maps_test.rb +30 -0
  65. data/test/unit/graticule/geocoder/meta_carta_test.rb +44 -0
  66. data/test/unit/graticule/geocoder/multi_test.rb +43 -0
  67. data/test/unit/graticule/geocoder/postcode_anywhere_test.rb +50 -0
  68. data/test/unit/graticule/geocoder/yahoo_test.rb +58 -0
  69. data/test/unit/graticule/geocoder_test.rb +27 -0
  70. data/test/unit/graticule/location_test.rb +66 -0
  71. metadata +123 -0
@@ -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(parse_type, xml) #:nodoc:
19
+ returning Location.new do |location|
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
+ end
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,49 @@
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(parse_type, 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
+ return 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
+ end
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,120 @@
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(*args)
37
+ case args.first
38
+ when :first then get :first, :location => extract_location(args[1])
39
+ when :all then get :all, :location => extract_location(args[1])
40
+ else get :first, :location => extract_location(args.first)
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def extract_location(address)
47
+ address.is_a?(String) ? address : location_from_params(address).to_s
48
+ end
49
+
50
+ # Extracts a Location from +xml+.
51
+ def parse_response(parse_type, xml) #:nodoc:
52
+ # puts xml.elements['//Placemark[1]'].to_s
53
+ if parse_type == :all
54
+ locations = []
55
+ xml.elements['//Placemark'].each do |element|
56
+ locations << parse_location_xml(element)
57
+ end
58
+ return locations
59
+ else
60
+ return parse_location_xml(xml.elements['//Placemark[1]'])
61
+ end
62
+ end
63
+
64
+ def parse_location_xml(xml)
65
+ longitude, latitude, = xml.elements['//Point/coordinates'].text.split(',').map { |v| v.to_f }
66
+ returning Location.new(:latitude => latitude, :longitude => longitude) do |l|
67
+ address = REXML::XPath.first(xml, '//xal:AddressDetails',
68
+ 'xal' => "urn:oasis:names:tc:ciq:xsdschema:xAL:2.0")
69
+ if address
70
+ l.street = value(address.elements['//ThoroughfareName/text()'])
71
+ l.locality = value(address.elements['//LocalityName/text()'])
72
+ l.region = value(address.elements['//AdministrativeAreaName/text()'])
73
+ l.postal_code = value(address.elements['//PostalCodeNumber/text()'])
74
+ l.country = value(address.elements['//CountryNameCode/text()'])
75
+ l.precision = PRECISION[address.attribute('Accuracy').value.to_i] || :unknown
76
+ end
77
+ end
78
+ end
79
+
80
+ # Extracts and raises an error from +xml+, if any.
81
+ def check_error(xml) #:nodoc:
82
+ status = xml.elements['/kml/Response/Status/code'].text.to_i
83
+ case status
84
+ when 200 then # ignore, ok
85
+ when 500 then
86
+ raise Error, 'server error'
87
+ when 601 then
88
+ raise AddressError, 'missing address'
89
+ when 602 then
90
+ raise AddressError, 'unknown address'
91
+ when 603 then
92
+ raise AddressError, 'unavailable address'
93
+ when 610 then
94
+ raise CredentialsError, 'invalid key'
95
+ when 620 then
96
+ raise CredentialsError, 'too many queries'
97
+ else
98
+ raise Error, "unknown error #{status}"
99
+ end
100
+ end
101
+
102
+ # Creates a URL from the Hash +params+. Automatically adds the key and
103
+ # sets the output type to 'xml'.
104
+ def make_url(params) #:nodoc:
105
+ params[:key] = @key
106
+ params[:output] = 'xml'
107
+
108
+ super params
109
+ end
110
+
111
+ def value(element)
112
+ element.value if element
113
+ end
114
+
115
+ def text(element)
116
+ element.text if element
117
+ end
118
+ end
119
+ end
120
+ 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(parse_type, response) #:nodoc:
25
+ returning Location.new do |location|
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
+ end
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,45 @@
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(parse_type, js)
36
+ returning Location.new do |location|
37
+ coordinates = js.match(/map.centerAndZoom\(new GPoint\((.+?), (.+?)\)/)
38
+ location.longitude = coordinates[1].to_f
39
+ location.latitude = coordinates[2].to_f
40
+ end
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,109 @@
1
+ module Graticule #:nodoc:
2
+ module Geocoder #:nodoc:
3
+
4
+ # First you need a Mapquest API key. You can register for one here:
5
+ # http://www.mapquest.com/features/main.adp?page=developer_tools_oapi
6
+ #
7
+ # Then you create a MapquestGeocode object and start locating addresses:
8
+ #
9
+ # gg = Graticule.service(:map_quest).new(MAPS_API_KEY)
10
+ # location = gg.locate :street => '1600 Amphitheater Pkwy', :locality => 'Mountain View', :region => 'CA'
11
+ # p location.coordinates
12
+ #
13
+ class MapQuest < Rest
14
+ # http://trc.mapquest.com
15
+
16
+ PRECISION = {
17
+ 0 => :unknown,
18
+ 'COUNTRY' => :country,
19
+ 'STATE' => :state,
20
+ 'COUNTY' => :state,
21
+ 'CITY' => :city,
22
+ 'ZIP' => :zip,'ZIP7' => :zip,'ZIP9' => :zip,
23
+ 'INTERSECTIONS' => :street,
24
+ 'STREET' => :street,
25
+ 'ADDRESS' => :address
26
+ }
27
+
28
+ # Creates a new MapquestGeocode that will use Mapquest API key +key+.
29
+ #
30
+ # WARNING: The MapQuest API Keys tend to be already URI encoded. If this is the case
31
+ # be sure to URI.unescape the key in the call to new
32
+ def initialize(key)
33
+ @key = key
34
+ @url = URI.parse 'http://web.openapi.mapquest.com/oapi/transaction'
35
+ end
36
+
37
+ # Locates +address+ returning a Location
38
+ def locate(address)
39
+ get map_attributes(location_from_params(address))
40
+ end
41
+
42
+ private
43
+
44
+ def map_attributes(location)
45
+ mapping = {:street => :address, :locality => :city, :region => :stateProvince, :postal_code => :postalcoe, :country => :country}
46
+ mapping.keys.inject({}) do |result,attribute|
47
+ result[mapping[attribute]] = location.attributes[attribute] unless location.attributes[attribute].blank?
48
+ result
49
+ end
50
+ end
51
+
52
+ # Extracts a Location from +xml+.
53
+ def parse_response(parse_type, xml) #:nodoc:
54
+ address = REXML::XPath.first(xml, '/advantage/geocode/locations/location')
55
+
56
+ Location.new \
57
+ :street => value(address.elements['//address/text()']),
58
+ :locality => value(address.elements['//city/text()']),
59
+ :region => value(address.elements['//stateProvince/text()']),
60
+ :postal_code => value(address.elements['//postalCode/text()']),
61
+ :country => value(address.elements['//country/text()']),
62
+ :latitude => value(address.elements['//latitude/text()']),
63
+ :longitude => value(address.elements['//longitude/text()']),
64
+ :precision => PRECISION[address.elements['//geocodeQuality'].text] || :unknown
65
+ end
66
+
67
+ # Extracts and raises an error from +xml+, if any.
68
+ def check_error(xml) #:nodoc:
69
+ return unless xml.elements['//error']
70
+ status = xml.elements['//error/code'].text.to_i
71
+ msg = xml.elements['//error/text'].text
72
+ case status
73
+ when 255..299 then
74
+ raise CredentialsError, msg
75
+ when 400..500 then
76
+ raise AddressError, msg
77
+ when 600.699 then
78
+ raise Error, msg
79
+ when 900 then
80
+ raise AddressError, 'invalid latitude'
81
+ when 901 then
82
+ raise AddressError, 'invalid longitude'
83
+ when 902 then
84
+ raise AddressError, 'error parsing params'
85
+ when 9902 then
86
+ raise CredentialsError, 'invalid key'
87
+ when 9904 then
88
+ raise CredentialsError, 'key missing'
89
+ else
90
+ raise Error, "unknown error #{status}: #{msg}"
91
+ end
92
+ end
93
+
94
+ # Creates a URL from the Hash +params+. Automatically adds the key and
95
+ # sets the output type to 'xml'.
96
+ def make_url(params) #:nodoc:
97
+ super params.merge({:key => @key, :transaction => 'geocode', :ambiguities => 0})
98
+ end
99
+
100
+ def value(element)
101
+ element.value if element
102
+ end
103
+
104
+ def text(element)
105
+ element.text if element
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,33 @@
1
+
2
+ module Graticule
3
+ module Geocoder
4
+
5
+ # Library for looking up coordinates with MetaCarta's GeoParser API.
6
+ #
7
+ # http://labs.metacarta.com/GeoParser/documentation.html
8
+ class MetaCarta < Rest
9
+
10
+ def initialize # :nodoc:
11
+ @url = URI.parse 'http://labs.metacarta.com/GeoParser/'
12
+ end
13
+
14
+ # Finds +location+ and returns a Location object.
15
+ def locate(location)
16
+ get :q => location.is_a?(String) ? location : location_from_params(location).to_s, :output => 'locations'
17
+ end
18
+
19
+ private
20
+
21
+ def check_error(xml) # :nodoc:
22
+ raise AddressError, 'bad location' unless xml.elements['Locations/Location']
23
+ end
24
+
25
+ def parse_response(parse_type, xml) # :nodoc:
26
+ result = xml.elements['/Locations/Location[1]']
27
+ coords = result.elements['Centroid/gml:Point/gml:coordinates'].text.split ','
28
+ Location.new :latitude => coords.first.to_f, :longitude => coords.last.to_f
29
+ end
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,46 @@
1
+ module Graticule #:nodoc:
2
+ module Geocoder #:nodoc:
3
+ class Multi
4
+
5
+ # The Multi geocoder allows you to use multiple geocoders in succession.
6
+ #
7
+ # geocoder = Graticule.service(:multi).new(
8
+ # Graticule.service(:google).new("api_key"),
9
+ # Graticule.service(:yahoo).new("api_key"),
10
+ # )
11
+ # geocoder.locate '49423' # <= tries geocoders in succession
12
+ #
13
+ # The Multi geocoder will try the geocoders in order if a Graticule::AddressError
14
+ # is raised. You can customize this behavior by passing in a block to the Multi
15
+ # geocoder. For example, to try the geocoders until one returns a result with a
16
+ # high enough precision:
17
+ #
18
+ # geocoder = Graticule.service(:multi).new(geocoders) do |result|
19
+ # [:address, :street].include?(result.precision)
20
+ # end
21
+ #
22
+ # Geocoders will be tried in order until the block returned true for one of the results
23
+ #
24
+ def initialize(*geocoders, &acceptable)
25
+ @acceptable = acceptable || lambda { true }
26
+ @geocoders = geocoders.flatten
27
+ end
28
+
29
+ def locate(address)
30
+ last_error = nil
31
+ @geocoders.each do |geocoder|
32
+ begin
33
+ result = geocoder.locate address
34
+ return result if @acceptable.call(result)
35
+ rescue Error => e
36
+ last_error = e
37
+ rescue Errno::ECONNREFUSED
38
+ logger.error("Connection refused to #{service}")
39
+ end
40
+ end
41
+ raise last_error || AddressError.new("Couldn't find '#{address}' with any of the services")
42
+ end
43
+
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,63 @@
1
+ module Graticule #:nodoc:
2
+ module Geocoder #:nodoc:
3
+
4
+ class PostcodeAnywhere < Rest
5
+
6
+ # http://www.postcodeanywhere.com/register/
7
+ def initialize(account_code, license_code)
8
+ @url = URI.parse 'http://services.postcodeanywhere.co.uk/xml.aspx'
9
+ @account_code = account_code
10
+ @license_code = license_code
11
+ end
12
+
13
+ def locate(params)
14
+ location = location_from_params(params)
15
+ get :address => location.to_s(:country => false), :country => location.country
16
+ end
17
+
18
+ private
19
+
20
+ def make_url(params) #:nodoc:
21
+ params[:account_code] = @account_code
22
+ params[:license_code] = @license_code
23
+ params[:action] = 'geocode'
24
+ super params
25
+ end
26
+
27
+ def parse_response(parse_type, xml) #:nodoc:
28
+ result = xml.elements['/PostcodeAnywhere/Data/Item[1]']
29
+ returning Location.new do |location|
30
+ location.latitude = result.attribute('latitude').value.to_f
31
+ location.longitude = result.attribute('longitude').value.to_f
32
+ location.street = value(result.attribute('line1'))
33
+ location.locality = value(result.attribute('city'))
34
+ location.region = value(result.attribute('state'))
35
+ location.postal_code = value(result.attribute('postal_code'))
36
+ location.country = value(result.attribute('country'))
37
+ end
38
+ end
39
+
40
+ def value(attribute)
41
+ attribute.value if attribute
42
+ end
43
+
44
+ # http://www.postcodeanywhere.co.uk/developers/documentation/errors.aspx
45
+ def check_error(xml) #:nodoc:
46
+ #raise AddressError, xml.text if xml.text == 'couldn\'t find this address! sorry'
47
+ if error = xml.elements['/PostcodeAnywhere/Data/Item[@error_number][1]']
48
+ error_number = error.attribute('error_number').value.to_i
49
+ message = error.attribute('message').value
50
+ if (1..11).include?(error_number) || (34..38).include?(error_number)
51
+ raise CredentialsError, message
52
+ else
53
+ raise Error, message
54
+ end
55
+ elsif xml.elements['/PostcodeAnywhere/Data'].elements.empty?
56
+ raise AddressError, 'No results returned'
57
+ end
58
+
59
+ end
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,18 @@
1
+ require 'rexml/document'
2
+
3
+ module Graticule #:nodoc:
4
+ module Geocoder #:nodoc:
5
+
6
+ # Abstract class for implementing REST geocoders. Passes on a REXML::Document
7
+ # to +check_errors+ and +parse_response+.
8
+ class Rest < Base
9
+
10
+ private
11
+
12
+ def prepare_response(response)
13
+ REXML::Document.new(response)
14
+ end
15
+
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,102 @@
1
+ module Graticule #:nodoc:
2
+ module Geocoder #:nodoc:
3
+
4
+ # Yahoo geocoding API.
5
+ #
6
+ # http://developer.yahoo.com/maps/rest/V1/geocode.html
7
+ class Yahoo < Rest
8
+
9
+ PRECISION = {
10
+ "country"=> :country,
11
+ "state" => :state,
12
+ "city" => :city,
13
+ "zip+4" => :zip,
14
+ "zip+2" => :zip,
15
+ "zip" => :zip,
16
+ "street" => :street,
17
+ "address" => :address
18
+ }
19
+
20
+ # Web services initializer.
21
+ #
22
+ # The +appid+ is the Application ID that uniquely identifies your
23
+ # application. See: http://developer.yahoo.com/faq/index.html#appid
24
+ #
25
+ # See http://developer.yahoo.com/search/rest.html
26
+ def initialize(appid)
27
+ @appid = appid
28
+ @url = URI.parse "http://api.local.yahoo.com/MapsService/V1/geocode"
29
+ end
30
+
31
+ # Returns a Location for +address+.
32
+ #
33
+ # The +address+ can be any of:
34
+ # * city, state
35
+ # * city, state, zip
36
+ # * zip
37
+ # * street, city, state
38
+ # * street, city, state, zip
39
+ # * street, zip
40
+ def locate(*args)
41
+ case args.first
42
+ when :first then get :first, :location => extract_location(args[1])
43
+ when :all then get :all, :location => extract_location(args[1])
44
+ else get :first, :location => extract_location(args.first)
45
+ end
46
+ end
47
+
48
+ def extract_location(address)
49
+ location = (address.is_a?(String) ? address : location_from_params(address).to_s(:country => false))
50
+ # yahoo pukes on line breaks
51
+ location.gsub("\n", ', ')
52
+ end
53
+
54
+ def parse_response(parse_type, xml)
55
+ if parse_type == :all
56
+ locations = []
57
+ xml.elements['ResultSet'].each do |element|
58
+ locations << parse_location_xml(element)
59
+ end
60
+ return locations
61
+ else
62
+ return parse_location_xml(xml.elements['ResultSet/Result[1]'])
63
+ end
64
+ end
65
+
66
+ def parse_location_xml(element)
67
+ returning Location.new do |location|
68
+ location.precision = PRECISION[element.attributes['precision']] || :unknown
69
+
70
+ if element.attributes.include? 'warning' then
71
+ location.warning = element.attributes['warning']
72
+ end
73
+ location.latitude = element.elements['Latitude'].text.to_f
74
+ location.longitude = element.elements['Longitude'].text.to_f
75
+
76
+ location.street = element.elements['Address'].text.titleize unless element.elements['Address'].text.blank?
77
+ location.locality = element.elements['City'].text.titleize unless element.elements['City'].text.blank?
78
+ location.region = element.elements['State'].text
79
+ location.postal_code = element.elements['Zip'].text
80
+ location.country = element.elements['Country'].text
81
+ end
82
+ end
83
+
84
+ # Extracts and raises an error from +xml+, if any.
85
+ def check_error(xml) #:nodoc:
86
+ err = xml.elements['Error']
87
+ raise Error, err.elements['Message'].text if err
88
+ end
89
+
90
+ # Creates a URL from the Hash +params+. Automatically adds the appid and
91
+ # sets the output type to 'xml'.
92
+ def make_url(params) #:nodoc:
93
+ params[:appid] = @appid
94
+ params[:output] = 'xml'
95
+
96
+ super params
97
+ end
98
+
99
+ end
100
+
101
+ end
102
+ end