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.
Files changed (88) hide show
  1. data/.gitignore +4 -0
  2. data/CHANGELOG.txt +61 -0
  3. data/LICENSE.txt +30 -0
  4. data/Manifest.txt +84 -0
  5. data/README.txt +41 -0
  6. data/Rakefile +143 -0
  7. data/VERSION +1 -0
  8. data/bin/geocode +5 -0
  9. data/graticule.gemspec +143 -0
  10. data/init.rb +2 -0
  11. data/lib/graticule/cli.rb +64 -0
  12. data/lib/graticule/core_ext.rb +15 -0
  13. data/lib/graticule/distance/haversine.rb +40 -0
  14. data/lib/graticule/distance/spherical.rb +52 -0
  15. data/lib/graticule/distance/vincenty.rb +76 -0
  16. data/lib/graticule/distance.rb +18 -0
  17. data/lib/graticule/geocoder/base.rb +116 -0
  18. data/lib/graticule/geocoder/bogus.rb +15 -0
  19. data/lib/graticule/geocoder/geocoder_ca.rb +54 -0
  20. data/lib/graticule/geocoder/geocoder_us.rb +51 -0
  21. data/lib/graticule/geocoder/google.rb +100 -0
  22. data/lib/graticule/geocoder/host_ip.rb +41 -0
  23. data/lib/graticule/geocoder/local_search_maps.rb +44 -0
  24. data/lib/graticule/geocoder/mapquest.rb +96 -0
  25. data/lib/graticule/geocoder/meta_carta.rb +32 -0
  26. data/lib/graticule/geocoder/multi.rb +46 -0
  27. data/lib/graticule/geocoder/multimap.rb +73 -0
  28. data/lib/graticule/geocoder/postcode_anywhere.rb +63 -0
  29. data/lib/graticule/geocoder/rest.rb +18 -0
  30. data/lib/graticule/geocoder/yahoo.rb +84 -0
  31. data/lib/graticule/geocoder.rb +21 -0
  32. data/lib/graticule/location.rb +61 -0
  33. data/lib/graticule/version.rb +9 -0
  34. data/lib/graticule.rb +26 -0
  35. data/site/index.html +114 -0
  36. data/site/plugin.html +82 -0
  37. data/site/stylesheets/style.css +69 -0
  38. data/test/config.yml.default +36 -0
  39. data/test/fixtures/responses/geocoder_us/success.xml +18 -0
  40. data/test/fixtures/responses/geocoder_us/unknown.xml +1 -0
  41. data/test/fixtures/responses/google/badkey.xml +1 -0
  42. data/test/fixtures/responses/google/limit.xml +10 -0
  43. data/test/fixtures/responses/google/missing_address.xml +1 -0
  44. data/test/fixtures/responses/google/only_coordinates.xml +1 -0
  45. data/test/fixtures/responses/google/partial.xml +1 -0
  46. data/test/fixtures/responses/google/server_error.xml +10 -0
  47. data/test/fixtures/responses/google/success.xml +1 -0
  48. data/test/fixtures/responses/google/success_multiple_results.xml +88 -0
  49. data/test/fixtures/responses/google/unavailable.xml +1 -0
  50. data/test/fixtures/responses/google/unknown_address.xml +1 -0
  51. data/test/fixtures/responses/host_ip/private.txt +4 -0
  52. data/test/fixtures/responses/host_ip/success.txt +4 -0
  53. data/test/fixtures/responses/host_ip/unknown.txt +4 -0
  54. data/test/fixtures/responses/local_search_maps/empty.txt +1 -0
  55. data/test/fixtures/responses/local_search_maps/not_found.txt +1 -0
  56. data/test/fixtures/responses/local_search_maps/success.txt +1 -0
  57. data/test/fixtures/responses/mapquest/multi_result.xml +1 -0
  58. data/test/fixtures/responses/mapquest/success.xml +1 -0
  59. data/test/fixtures/responses/meta_carta/bad_address.xml +17 -0
  60. data/test/fixtures/responses/meta_carta/multiple.xml +33 -0
  61. data/test/fixtures/responses/meta_carta/success.xml +31 -0
  62. data/test/fixtures/responses/multimap/missing_params.xml +4 -0
  63. data/test/fixtures/responses/multimap/no_matches.xml +4 -0
  64. data/test/fixtures/responses/multimap/success.xml +19 -0
  65. data/test/fixtures/responses/postcode_anywhere/badkey.xml +9 -0
  66. data/test/fixtures/responses/postcode_anywhere/canada.xml +16 -0
  67. data/test/fixtures/responses/postcode_anywhere/empty.xml +16 -0
  68. data/test/fixtures/responses/postcode_anywhere/success.xml +16 -0
  69. data/test/fixtures/responses/postcode_anywhere/uk.xml +18 -0
  70. data/test/fixtures/responses/yahoo/success.xml +3 -0
  71. data/test/fixtures/responses/yahoo/unknown_address.xml +6 -0
  72. data/test/mocks/uri.rb +52 -0
  73. data/test/test_helper.rb +31 -0
  74. data/test/unit/graticule/distance_test.rb +58 -0
  75. data/test/unit/graticule/geocoder/geocoder_us_test.rb +43 -0
  76. data/test/unit/graticule/geocoder/geocoders.rb +56 -0
  77. data/test/unit/graticule/geocoder/google_test.rb +112 -0
  78. data/test/unit/graticule/geocoder/host_ip_test.rb +40 -0
  79. data/test/unit/graticule/geocoder/local_search_maps_test.rb +30 -0
  80. data/test/unit/graticule/geocoder/mapquest_test.rb +61 -0
  81. data/test/unit/graticule/geocoder/meta_carta_test.rb +44 -0
  82. data/test/unit/graticule/geocoder/multi_test.rb +43 -0
  83. data/test/unit/graticule/geocoder/multimap_test.rb +52 -0
  84. data/test/unit/graticule/geocoder/postcode_anywhere_test.rb +50 -0
  85. data/test/unit/graticule/geocoder/yahoo_test.rb +48 -0
  86. data/test/unit/graticule/geocoder_test.rb +27 -0
  87. data/test/unit/graticule/location_test.rb +73 -0
  88. 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