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.
- data/CHANGELOG.txt +8 -0
- data/LICENSE.txt +30 -0
- data/Manifest.txt +44 -0
- data/README.txt +10 -0
- data/Rakefile +16 -0
- data/init.rb +2 -0
- data/lib/graticule.rb +14 -0
- data/lib/graticule/distance.rb +24 -0
- data/lib/graticule/distance/haversine.rb +65 -0
- data/lib/graticule/distance/spherical.rb +30 -0
- data/lib/graticule/distance/vincenty.rb +99 -0
- data/lib/graticule/geocoder.rb +26 -0
- data/lib/graticule/geocoders/bogus.rb +12 -0
- data/lib/graticule/geocoders/geocoder_us.rb +45 -0
- data/lib/graticule/geocoders/google.rb +96 -0
- data/lib/graticule/geocoders/meta_carta.rb +102 -0
- data/lib/graticule/geocoders/rest.rb +98 -0
- data/lib/graticule/geocoders/yahoo.rb +101 -0
- data/lib/graticule/location.rb +28 -0
- data/lib/graticule/version.rb +3 -0
- data/test/fixtures/responses/geocoder_us/success.xml +10 -0
- data/test/fixtures/responses/geocoder_us/unknown.xml +1 -0
- data/test/fixtures/responses/google/badkey.xml +10 -0
- data/test/fixtures/responses/google/limit.xml +10 -0
- data/test/fixtures/responses/google/missing_address.xml +10 -0
- data/test/fixtures/responses/google/server_error.xml +10 -0
- data/test/fixtures/responses/google/success.xml +37 -0
- data/test/fixtures/responses/google/unavailable.xml +10 -0
- data/test/fixtures/responses/google/unknown_address.xml +10 -0
- data/test/fixtures/responses/meta_carta/bad_address.xml +9 -0
- data/test/fixtures/responses/meta_carta/multiple.xml +33 -0
- data/test/fixtures/responses/meta_carta/success.xml +23 -0
- data/test/fixtures/responses/yahoo/success.xml +3 -0
- data/test/fixtures/responses/yahoo/unknown_address.xml +6 -0
- data/test/mocks/uri.rb +51 -0
- data/test/test_helper.rb +31 -0
- data/test/unit/graticule/distance_test.rb +30 -0
- data/test/unit/graticule/geocoder_test.rb +31 -0
- data/test/unit/graticule/geocoders/geocoder_us_test.rb +42 -0
- data/test/unit/graticule/geocoders/geocoders.rb +56 -0
- data/test/unit/graticule/geocoders/google_test.rb +22 -0
- data/test/unit/graticule/geocoders/meta_carta_test.rb +70 -0
- data/test/unit/graticule/geocoders/yahoo_test.rb +49 -0
- data/test/unit/graticule/location_test.rb +38 -0
- 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,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,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>
|