graticule 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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>
|