graticule 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/CHANGELOG.txt +11 -0
  2. data/Manifest.txt +26 -11
  3. data/README.txt +1 -1
  4. data/bin/geocode +1 -1
  5. data/lib/graticule.rb +10 -8
  6. data/lib/graticule/cli.rb +1 -0
  7. data/lib/graticule/geocoder.rb +1 -12
  8. data/lib/graticule/geocoder/bogus.rb +15 -0
  9. data/lib/graticule/geocoder/geocoder_ca.rb +52 -0
  10. data/lib/graticule/geocoder/geocoder_us.rb +47 -0
  11. data/lib/graticule/geocoder/google.rb +106 -0
  12. data/lib/graticule/geocoder/host_ip.rb +50 -0
  13. data/lib/graticule/geocoder/local_search_maps.rb +45 -0
  14. data/lib/graticule/geocoder/meta_carta.rb +38 -0
  15. data/lib/graticule/geocoder/postcode_anywhere.rb +63 -0
  16. data/lib/graticule/geocoder/rest.rb +110 -0
  17. data/lib/graticule/geocoder/yahoo.rb +96 -0
  18. data/lib/graticule/location.rb +19 -7
  19. data/lib/graticule/version.rb +2 -2
  20. data/test/fixtures/responses/host_ip/private.txt +4 -0
  21. data/test/fixtures/responses/host_ip/success.txt +4 -0
  22. data/test/fixtures/responses/host_ip/unknown.txt +4 -0
  23. data/test/fixtures/responses/local_search_maps/success.txt +1 -0
  24. data/test/fixtures/responses/postcode_anywhere/badkey.xml +9 -0
  25. data/test/fixtures/responses/postcode_anywhere/canada.xml +16 -0
  26. data/test/fixtures/responses/postcode_anywhere/empty.xml +16 -0
  27. data/test/fixtures/responses/postcode_anywhere/success.xml +16 -0
  28. data/test/fixtures/responses/postcode_anywhere/uk.xml +18 -0
  29. data/test/test_helper.rb +4 -2
  30. data/test/unit/graticule/geocoder/geocoder_us_test.rb +43 -0
  31. data/test/unit/graticule/geocoder/google_test.rb +66 -0
  32. data/test/unit/graticule/geocoder/host_ip_test.rb +39 -0
  33. data/test/unit/graticule/geocoder/local_search_maps_test.rb +30 -0
  34. data/test/unit/graticule/geocoder/meta_carta_test.rb +44 -0
  35. data/test/unit/graticule/geocoder/postcode_anywhere_test.rb +50 -0
  36. data/test/unit/graticule/geocoder/yahoo_test.rb +48 -0
  37. data/test/unit/graticule/geocoder_test.rb +5 -9
  38. data/test/unit/graticule/location_test.rb +22 -7
  39. metadata +37 -18
  40. data/lib/graticule/geocoders/bogus.rb +0 -11
  41. data/lib/graticule/geocoders/geocoder_us.rb +0 -45
  42. data/lib/graticule/geocoders/google.rb +0 -99
  43. data/lib/graticule/geocoders/meta_carta.rb +0 -102
  44. data/lib/graticule/geocoders/rest.rb +0 -98
  45. data/lib/graticule/geocoders/yahoo.rb +0 -101
  46. data/test/unit/graticule/geocoders/geocoder_us_test.rb +0 -42
  47. data/test/unit/graticule/geocoders/geocoders.rb +0 -56
  48. data/test/unit/graticule/geocoders/google_test.rb +0 -22
  49. data/test/unit/graticule/geocoders/meta_carta_test.rb +0 -70
  50. data/test/unit/graticule/geocoders/yahoo_test.rb +0 -49
@@ -0,0 +1,38 @@
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
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 make_url(params) # :nodoc:
26
+ params[:output] = 'locations'
27
+ super params
28
+ end
29
+
30
+ def parse_response(xml) # :nodoc:
31
+ result = xml.elements['/Locations/Location[1]']
32
+ coords = result.elements['Centroid/gml:Point/gml:coordinates'].text.split ','
33
+ Location.new :latitude => coords.first.to_f, :longitude => coords.last.to_f
34
+ end
35
+
36
+ end
37
+ end
38
+ 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(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,110 @@
1
+ require 'open-uri'
2
+ require 'rexml/document'
3
+
4
+ module Graticule #:nodoc:
5
+ module Geocoder #:nodoc:
6
+
7
+ # Abstract class for implementing REST APIs.
8
+ #
9
+ # === Example
10
+ #
11
+ # The following methods must be implemented in sublcasses:
12
+ #
13
+ # +initialize+:: Sets @url to the service enpoint.
14
+ # +check_error+:: Checks for errors in the server response.
15
+ # +parse_response+:: Extracts information from the server response.
16
+ #
17
+ # If you have extra URL paramaters (application id, output type) or need to
18
+ # perform URL customization, override +make_url+.
19
+ #
20
+ # class FakeService < RCRest
21
+ #
22
+ # class Error < RCRest::Error; end
23
+ #
24
+ # def initialize(appid)
25
+ # @appid = appid
26
+ # @url = URI.parse 'http://example.com/test'
27
+ # end
28
+ #
29
+ # def check_error(xml)
30
+ # raise Error, xml.elements['error'].text if xml.elements['error']
31
+ # end
32
+ #
33
+ # def make_url(params)
34
+ # params[:appid] = @appid
35
+ # super params
36
+ # end
37
+ #
38
+ # def parse_response(xml)
39
+ # # return Location
40
+ # end
41
+ #
42
+ # def test(query)
43
+ # get :q => query
44
+ # end
45
+ #
46
+ # end
47
+ class Rest
48
+
49
+ # Web services initializer.
50
+ #
51
+ # Concrete web services implementations must set the +url+ instance
52
+ # variable which must be a URI.
53
+ def initialize
54
+ raise NotImplementedError
55
+ end
56
+
57
+ private
58
+
59
+ def location_from_params(params)
60
+ case params
61
+ when Location then params
62
+ when Hash then Location.new params
63
+ else
64
+ raise ArgumentError, "Expected a Graticule::Location or a hash with :street, :locality, :region, :postal_code, and :country attributes"
65
+ end
66
+ end
67
+
68
+ # Must extract and raise an error from +xml+, an REXML::Document, if any.
69
+ # Must returns if no error could be found.
70
+ def check_error(xml)
71
+ raise NotImplementedError
72
+ end
73
+
74
+ # Performs a GET request with +params+. Calls the parse_response method on
75
+ # the concrete class with an REXML::Document instance and returns its
76
+ # result.
77
+ def get(params = {})
78
+ url = make_url params
79
+
80
+ url.open do |response|
81
+ res = REXML::Document.new response.read
82
+ check_error(res)
83
+ return parse_response(res)
84
+ end
85
+ rescue OpenURI::HTTPError => e
86
+ response = REXML::Document.new e.io.read
87
+ check_error response
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
+ # Must parse results from +xml+, an REXML::Document, into a Location.
104
+ def parse_response(xml)
105
+ raise NotImplementedError
106
+ end
107
+
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,96 @@
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
+ # Concrete web services implementations need to set the following instance
26
+ # variables then call super:
27
+ #
28
+ # +host+:: API endpoint hostname
29
+ # +service_name+:: service name
30
+ # +version+:: service name version number
31
+ # +method+:: service method call
32
+ #
33
+ # See http://developer.yahoo.com/search/rest.html
34
+ def initialize(appid)
35
+ @host = 'api.local.yahoo.com'
36
+ @service_name = 'MapsService'
37
+ @version = 'V1'
38
+ @method = 'geocode'
39
+ @appid = appid
40
+ @url = URI.parse "http://#{@host}/#{@service_name}/#{@version}/#{@method}"
41
+ end
42
+
43
+ # Returns a Location for +address+.
44
+ #
45
+ # The +address+ can be any of:
46
+ # * city, state
47
+ # * city, state, zip
48
+ # * zip
49
+ # * street, city, state
50
+ # * street, city, state, zip
51
+ # * street, zip
52
+ def locate(address)
53
+ location = (address.is_a?(String) ? address : location_from_params(address).to_s(:country => false))
54
+ # yahoo pukes on line breaks
55
+ get :location => location.gsub("\n", ', ')
56
+ end
57
+
58
+ def parse_response(xml) # :nodoc:
59
+ r = xml.elements['ResultSet/Result[1]']
60
+ returning Location.new do |location|
61
+ location.precision = PRECISION[r.attributes['precision']] || :unknown
62
+
63
+ if r.attributes.include? 'warning' then
64
+ location.warning = r.attributes['warning']
65
+ end
66
+
67
+ location.latitude = r.elements['Latitude'].text.to_f
68
+ location.longitude = r.elements['Longitude'].text.to_f
69
+
70
+ location.street = r.elements['Address'].text.titleize unless r.elements['Address'].text.blank?
71
+ location.locality = r.elements['City'].text.titleize unless r.elements['City'].text.blank?
72
+ location.region = r.elements['State'].text
73
+ location.postal_code = r.elements['Zip'].text
74
+ location.country = r.elements['Country'].text
75
+ end
76
+ end
77
+
78
+ # Extracts and raises an error from +xml+, if any.
79
+ def check_error(xml) #:nodoc:
80
+ err = xml.elements['Error']
81
+ raise Error, err.elements['Message'].text if err
82
+ end
83
+
84
+ # Creates a URL from the Hash +params+. Automatically adds the appid and
85
+ # sets the output type to 'xml'.
86
+ def make_url(params) #:nodoc:
87
+ params[:appid] = @appid
88
+ params[:output] = 'xml'
89
+
90
+ super params
91
+ end
92
+
93
+ end
94
+
95
+ end
96
+ end
@@ -2,12 +2,23 @@ module Graticule
2
2
 
3
3
  # A geographic location
4
4
  class Location
5
- attr_accessor :latitude, :longitude, :street, :city, :state, :zip, :country, :precision, :warning
5
+ attr_accessor :latitude, :longitude, :street, :locality, :region, :postal_code, :country, :precision, :warning
6
+ alias_method :city, :locality
7
+ alias_method :state, :region
8
+ alias_method :zip, :postal_code
6
9
 
7
10
  def initialize(attrs = {})
8
11
  attrs.each do |key,value|
9
12
  instance_variable_set "@#{key}", value
10
13
  end
14
+ self.precision ||= :unknown
15
+ end
16
+
17
+ def attributes
18
+ [:latitude, :longitude, :street, :locality, :region, :postal_code, :country].inject({}) do |result,attr|
19
+ result[attr] = self.send(attr) if self.send(attr)
20
+ result
21
+ end
11
22
  end
12
23
 
13
24
  # Returns an Array with latitude and longitude.
@@ -16,7 +27,7 @@ module Graticule
16
27
  end
17
28
 
18
29
  def ==(object)
19
- super(object) || [:latitude, :longitude, :street, :city, :state, :zip, :country, :precision].all? do |m|
30
+ super(object) || [:latitude, :longitude, :street, :locality, :region, :postal_code, :country, :precision].all? do |m|
20
31
  object.respond_to?(m) && self.send(m) == object.send(m)
21
32
  end
22
33
  end
@@ -29,16 +40,17 @@ module Graticule
29
40
 
30
41
  # Where would I be if I dug through the center of the earth?
31
42
  def antipode
32
- new_longitude = longitude + (longitude >= 0 ? -180 : 180)
33
- Location.new(:latitude => -latitude, :longitude => new_longitude)
43
+ Location.new :latitude => -latitude, :longitude => longitude + (longitude >= 0 ? -180 : 180)
34
44
  end
35
45
  alias_method :antipodal_location, :antipode
36
46
 
37
- def to_s(coordinates = false)
47
+ def to_s(options = {})
48
+ options = {:coordinates => false, :country => true}.merge(options)
38
49
  result = ""
39
50
  result << "#{street}\n" if street
40
- result << [city, [state, zip, country].compact.join(" ")].compact.join(", ")
41
- result << "\nlatitude: #{latitude}, longitude: #{longitude}" if coordinates && [latitude, longitude].any?
51
+ result << [locality, [region, postal_code].compact.join(" ")].compact.join(", ")
52
+ result << " #{country}" if options[:country] && country
53
+ result << "\nlatitude: #{latitude}, longitude: #{longitude}" if options[:coordinates] && [latitude, longitude].any?
42
54
  result
43
55
  end
44
56
 
@@ -1,8 +1,8 @@
1
1
  module Graticule #:nodoc:
2
2
  module Version #:nodoc:
3
3
  MAJOR = 0
4
- MINOR = 1
5
- TINY = 3
4
+ MINOR = 2
5
+ TINY = 0
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
@@ -0,0 +1,4 @@
1
+ Country: (Private Address) (XX)
2
+ City: (Private Address)
3
+ Latitude:
4
+ Longitude:
@@ -0,0 +1,4 @@
1
+ Country: UNITED STATES (US)
2
+ City: Mountain View, CA
3
+ Latitude: 37.402
4
+ Longitude: -122.078
@@ -0,0 +1,4 @@
1
+ Country: (Unknown Country?) (XX)
2
+ City: (Unknown City?)
3
+ Latitude:
4
+ Longitude:
@@ -0,0 +1 @@
1
+ map.centerAndZoom(new GPoint(-0.130427, 51.510036), 4);
@@ -0,0 +1,9 @@
1
+ <PostcodeAnywhere Server="WS12" Version="3.0" Date="12/03/2007 15:43:33" Duration="0.016s">
2
+ <Schema Items="2">
3
+ <Field Name="error_number"/>
4
+ <Field Name="message"/>
5
+ </Schema>
6
+ <Data Items="1">
7
+ <Item error_number="6" message="License key was not recognised"/>
8
+ </Data>
9
+ </PostcodeAnywhere>
@@ -0,0 +1,16 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <PostcodeAnywhere Server="WS12" Version="3.0" Date="17/03/2007 19:01:05" Duration="1.109s">
3
+ <Schema Items="8">
4
+ <Field Name="line1"/>
5
+ <Field Name="line2"/>
6
+ <Field Name="city"/>
7
+ <Field Name="state"/>
8
+ <Field Name="postal_code"/>
9
+ <Field Name="country"/>
10
+ <Field Name="longitude"/>
11
+ <Field Name="latitude"/>
12
+ </Schema>
13
+ <Data Items="1">
14
+ <Item line1="204 Campbell Ave" city="Revelstoke" state="BC" postal_code="V0E" country="Canada" longitude="-118.196970002204" latitude="50.9997350418267"/>
15
+ </Data>
16
+ </PostcodeAnywhere>
@@ -0,0 +1,16 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <PostcodeAnywhere Version="3.0" Server="WS03" Date="17/03/2007 19:26:33" Duration="0.016s">
3
+ <Schema Items="10">
4
+ <Field Name="id"/>
5
+ <Field Name="seq"/>
6
+ <Field Name="location"/>
7
+ <Field Name="grid_east_m"/>
8
+ <Field Name="grid_north_m"/>
9
+ <Field Name="longitude"/>
10
+ <Field Name="latitude"/>
11
+ <Field Name="os_reference"/>
12
+ <Field Name="wgs84_longitude"/>
13
+ <Field Name="wgs84_latitude"/>
14
+ </Schema>
15
+ <Data Items="0"/>
16
+ </PostcodeAnywhere>