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,102 @@
1
+
2
+ module Graticule
3
+
4
+ # Library for looking up coordinates with MetaCarta's GeoParser API.
5
+ #
6
+ # http://labs.metacarta.com/GeoParser/documentation.html
7
+ class MetaCartaGeocoder < RestGeocoder
8
+ Location = Struct.new :name, :type, :population, :hierarchy,
9
+ :latitude, :longitude, :confidence, :viewbox
10
+
11
+ def initialize # :nodoc:
12
+ @url = URI.parse 'http://labs.metacarta.com/GeoParser/'
13
+ end
14
+
15
+ # Locates +place+ and returns a Location object.
16
+ def locate(place)
17
+ locations, = get :q => place
18
+ return locations.first
19
+ end
20
+
21
+ # Retrieve all locations matching +place+.
22
+ #
23
+ # Returns an Array of Location objects and a pair of coordinates that will
24
+ # surround them.
25
+ def locations(place)
26
+ get :loc => place
27
+ end
28
+
29
+ def check_error(xml) # :nodoc:
30
+ raise AddressError, 'bad location' unless xml.elements['Locations/Location']
31
+ end
32
+
33
+ def make_url(params) # :nodoc:
34
+ params[:output] = 'locations'
35
+
36
+ super params
37
+ end
38
+
39
+ def parse_response(xml) # :nodoc:
40
+ locations = []
41
+
42
+ xml.elements['/Locations'].each do |l|
43
+ next if REXML::Text === l or l.name == 'ViewBox'
44
+ location = Location.new
45
+
46
+ location.viewbox = viewbox_coords l.elements['ViewBox/gml:Box/gml:coordinates']
47
+
48
+ location.name = l.attributes['Name']
49
+ location.type = l.attributes['Type']
50
+ population = l.attributes['Population'].to_i
51
+ location.population = population > 0 ? population : nil
52
+ location.hierarchy = l.attributes['Hierarchy']
53
+
54
+ coords = l.elements['Centroid/gml:Point/gml:coordinates'].text.split ','
55
+ location.latitude = coords.first.to_f
56
+ location.longitude = coords.last.to_f
57
+
58
+ confidence = l.elements['Confidence']
59
+ location.confidence = confidence.text.to_f if confidence
60
+
61
+ locations << location
62
+ end
63
+
64
+ query_viewbox = xml.elements['/Locations/ViewBox/gml:Box/gml:coordinates']
65
+
66
+ return locations, viewbox_coords(query_viewbox)
67
+ end
68
+
69
+ # Turns a element containing a pair of coordinates into a pair of coordinate
70
+ # Arrays.
71
+ def viewbox_coords(viewbox) # :nodoc:
72
+ return viewbox.text.split(' ').map do |coords|
73
+ coords.split(',').map { |c| c.to_f }
74
+ end
75
+ end
76
+
77
+ end
78
+
79
+ # A Location contains the following fields:
80
+ #
81
+ # +name+:: The name of this location
82
+ # +type+:: The type of this location (no clue what it means)
83
+ # +population+:: The number of people who live here or nil
84
+ # +hierarchy+:: The places above this place
85
+ # +latitude+:: Latitude of the location
86
+ # +longitude+:: Longitude of the location
87
+ # +confidence+:: Accuracy confidence (if any)
88
+ # +viewbox+:: Pair of coordinates forming a box around this place
89
+ #
90
+ # viewbox runs from lower left to upper right.
91
+ class MetaCartaGeocoder::Location
92
+
93
+ ##
94
+ # The latitude and longitude for this location.
95
+
96
+ def coordinates
97
+ [latitude, longitude]
98
+ end
99
+
100
+ end
101
+
102
+ end
@@ -0,0 +1,98 @@
1
+ require 'open-uri'
2
+ require 'rexml/document'
3
+
4
+ module Graticule #:nodoc:
5
+
6
+ # Abstract class for implementing REST APIs.
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
+ # If you have extra URL paramaters (application id, output type) or need to
17
+ # perform URL customization, override +make_url+.
18
+ #
19
+ # class FakeService < RCRest
20
+ #
21
+ # class Error < RCRest::Error; end
22
+ #
23
+ # def initialize(appid)
24
+ # @appid = appid
25
+ # @url = URI.parse 'http://example.com/test'
26
+ # end
27
+ #
28
+ # def check_error(xml)
29
+ # raise Error, xml.elements['error'].text if xml.elements['error']
30
+ # end
31
+ #
32
+ # def make_url(params)
33
+ # params[:appid] = @appid
34
+ # super params
35
+ # end
36
+ #
37
+ # def parse_response(xml)
38
+ # return xml
39
+ # end
40
+ #
41
+ # def test(query)
42
+ # get :q => query
43
+ # end
44
+ #
45
+ # end
46
+ class RestGeocoder < Geocoder
47
+
48
+ # Web services initializer.
49
+ #
50
+ # Concrete web services implementations must set the +url+ instance
51
+ # variable which must be a URI.
52
+ def initialize
53
+ raise NotImplementedError
54
+ end
55
+
56
+ # Must extract and raise an error from +xml+, an REXML::Document, if any.
57
+ # Must returns if no error could be found.
58
+ def check_error(xml)
59
+ raise NotImplementedError
60
+ end
61
+
62
+ # Performs a GET request with +params+. Calls the parse_response method on
63
+ # the concrete class with an REXML::Document instance and returns its
64
+ # result.
65
+ def get(params = {})
66
+ url = make_url params
67
+
68
+ url.open do |response|
69
+ res = REXML::Document.new response.read
70
+ check_error(res)
71
+ return parse_response(res)
72
+ end
73
+ rescue OpenURI::HTTPError => e
74
+ response = REXML::Document.new e.io.read
75
+ check_error response
76
+ raise
77
+ end
78
+
79
+ # Creates a URI from the Hash +params+. Override this then call super if
80
+ # you need to add extra params like an application id or output type.
81
+ def make_url(params)
82
+ escaped_params = params.sort_by { |k,v| k.to_s }.map do |k,v|
83
+ "#{URI.escape k.to_s}=#{URI.escape v.to_s}"
84
+ end
85
+
86
+ url = @url.dup
87
+ url.query = escaped_params.join '&'
88
+ return url
89
+ end
90
+
91
+ # Must parse results from +xml+, an REXML::Document, into something sensible
92
+ # for the API.
93
+ def parse_response(xml)
94
+ raise NotImplementedError
95
+ end
96
+
97
+ end
98
+ end
@@ -0,0 +1,101 @@
1
+ module Graticule #:nodoc:
2
+
3
+ # Yahoo geocoding API.
4
+ #
5
+ # http://developer.yahoo.com/maps/rest/V1/geocode.html
6
+ class YahooGeocoder < RestGeocoder
7
+
8
+ PRECISION = {
9
+ "country"=> :country,
10
+ "state" => :state,
11
+ "city" => :city,
12
+ "zip+4" => :zip,
13
+ "zip+2" => :zip,
14
+ "zip" => :zip,
15
+ "street" => :street,
16
+ "address" => :address
17
+ }
18
+
19
+ # Web services initializer.
20
+ #
21
+ # The +appid+ is the Application ID that uniquely identifies your
22
+ # application. See: http://developer.yahoo.com/faq/index.html#appid
23
+ #
24
+ # Concrete web services implementations need to set the following instance
25
+ # variables then call super:
26
+ #
27
+ # +host+:: API endpoint hostname
28
+ # +service_name+:: service name
29
+ # +version+:: service name version number
30
+ # +method+:: service method call
31
+ #
32
+ # See http://developer.yahoo.com/search/rest.html
33
+ def initialize(appid)
34
+ @host = 'api.local.yahoo.com'
35
+ @service_name = 'MapsService'
36
+ @version = 'V1'
37
+ @method = 'geocode'
38
+ @appid = appid
39
+ @url = URI.parse "http://#{@host}/#{@service_name}/#{@version}/#{@method}"
40
+ end
41
+
42
+ # Returns a Location for +address+.
43
+ #
44
+ # The +address+ can be any of:
45
+ # * city, state
46
+ # * city, state, zip
47
+ # * zip
48
+ # * street, city, state
49
+ # * street, city, state, zip
50
+ # * street, zip
51
+ def locate(address)
52
+ get :location => address
53
+ end
54
+
55
+ def parse_response(xml) # :nodoc:
56
+ locations = []
57
+
58
+ xml.elements['ResultSet'].each do |r|
59
+ location = Location.new
60
+
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.city = r.elements['City'].text.titleize unless r.elements['City'].text.blank?
72
+ location.state = r.elements['State'].text
73
+ location.zip = r.elements['Zip'].text
74
+ location.country = r.elements['Country'].text
75
+
76
+ locations << location
77
+ end
78
+
79
+ # FIXME: make API consistent and only return 1 location
80
+ return locations
81
+ end
82
+
83
+ # Extracts and raises an error from +xml+, if any.
84
+ def check_error(xml)
85
+ err = xml.elements['Error']
86
+ raise Error, err.elements['Message'].text if err
87
+ end
88
+
89
+ # Creates a URL from the Hash +params+. Automatically adds the appid and
90
+ # sets the output type to 'xml'.
91
+ def make_url(params)
92
+ params[:appid] = @appid
93
+ params[:output] = 'xml'
94
+
95
+ super params
96
+ end
97
+
98
+ end
99
+
100
+
101
+ end
@@ -0,0 +1,28 @@
1
+
2
+ module Graticule
3
+ class Location
4
+ attr_accessor :latitude, :longitude, :street, :city, :state, :zip, :country, :precision, :warning
5
+
6
+ def initialize(attrs = {})
7
+ attrs.each do |key,value|
8
+ instance_variable_set "@#{key}", value
9
+ end
10
+ end
11
+
12
+ # Returns an Array with latitude and longitude.
13
+ def coordinates
14
+ [latitude, longitude]
15
+ end
16
+
17
+ def ==(object)
18
+ super(object) || [:latitude, :longitude, :street, :city, :state, :zip, :country, :precision].all? do |m|
19
+ object.respond_to?(m) && self.send(m) == object.send(m)
20
+ end
21
+ end
22
+
23
+ def distance_to(destination, units = :miles, formula = :haversine)
24
+ "Graticule::Distance::#{formula.to_s.titleize}".constantize.distance(self, destination)
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ module Graticule
2
+ Version = '0.1.1'
3
+ end
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0"?>
2
+ <rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/"
3
+ xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#"
4
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
5
+ <geo:Point rdf:nodeID="aid86937982">
6
+ <dc:description>1600 Pennsylvania Ave NW, Washington DC 20502</dc:description>
7
+ <geo:long>-77.037684</geo:long>
8
+ <geo:lat>38.898748</geo:lat>
9
+ </geo:Point>
10
+ </rdf:RDF>
@@ -0,0 +1 @@
1
+ couldn't find this address! sorry
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <kml xmlns="http://earth.google.com/kml/2.0">
3
+ <Response>
4
+ <name>1600 Amphitheater Pkwy, Mountain View, CA</name>
5
+ <Status>
6
+ <code>610</code>
7
+ <request>geocode</request>
8
+ </Status>
9
+ </Response>
10
+ </kml>
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <kml xmlns="http://earth.google.com/kml/2.0">
3
+ <Response>
4
+ <name>1600 Amphitheater Pkwy, Mountain View, CA</name>
5
+ <Status>
6
+ <code>620</code>
7
+ <request>geocode</request>
8
+ </Status>
9
+ </Response>
10
+ </kml>
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <kml xmlns="http://earth.google.com/kml/2.0">
3
+ <Response>
4
+ <name>1600</name>
5
+ <Status>
6
+ <code>601</code>
7
+ <request>geocode</request>
8
+ </Status>
9
+ </Response>
10
+ </kml>
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <kml xmlns="http://earth.google.com/kml/2.0">
3
+ <Response>
4
+ <name>1600 Amphitheater Pkwy, Mountain View, CA</name>
5
+ <Status>
6
+ <code>500</code>
7
+ <request>geocode</request>
8
+ </Status>
9
+ </Response>
10
+ </kml>
@@ -0,0 +1,37 @@
1
+ <kml>
2
+ <Response>
3
+ <name>1600 amphitheatre mtn view ca</name>
4
+ <Status>
5
+ <code>200</code>
6
+ <request>geocode</request>
7
+ </Status>
8
+ <Placemark>
9
+ <address>
10
+ 1600 Amphitheatre Pkwy, Mountain View, CA 94043, USA
11
+ </address>
12
+ <AddressDetails Accuracy="8">
13
+ <Country>
14
+ <CountryNameCode>US</CountryNameCode>
15
+ <AdministrativeArea>
16
+ <AdministrativeAreaName>CA</AdministrativeAreaName>
17
+ <SubAdministrativeArea>
18
+ <SubAdministrativeAreaName>Santa Clara</SubAdministrativeAreaName>
19
+ <Locality>
20
+ <LocalityName>Mountain View</LocalityName>
21
+ <Thoroughfare>
22
+ <ThoroughfareName>1600 Amphitheatre Pkwy</ThoroughfareName>
23
+ </Thoroughfare>
24
+ <PostalCode>
25
+ <PostalCodeNumber>94043</PostalCodeNumber>
26
+ </PostalCode>
27
+ </Locality>
28
+ </SubAdministrativeArea>
29
+ </AdministrativeArea>
30
+ </Country>
31
+ </AddressDetails>
32
+ <Point>
33
+ <coordinates>-122.083739,37.423021,0</coordinates>
34
+ </Point>
35
+ </Placemark>
36
+ </Response>
37
+ </kml>
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <kml xmlns="http://earth.google.com/kml/2.0">
3
+ <Response>
4
+ <name>42-44 Hanway Street, London</name>
5
+ <Status>
6
+ <code>603</code>
7
+ <request>geocode</request>
8
+ </Status>
9
+ </Response>
10
+ </kml>