graticule 0.1.1

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 (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>