drifter 0.1.0
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/.gitignore +3 -0
- data/Gemfile +4 -0
- data/README +102 -0
- data/README.rdoc +102 -0
- data/Rakefile +8 -0
- data/drifter.gemspec +21 -0
- data/lib/drifter.rb +92 -0
- data/lib/drifter/distance/haversine.rb +69 -0
- data/lib/drifter/geocoders.rb +3 -0
- data/lib/drifter/geocoders/base.rb +57 -0
- data/lib/drifter/geocoders/google.rb +91 -0
- data/lib/drifter/geocoders/hostip.rb +92 -0
- data/lib/drifter/geocoders/yahoo.rb +160 -0
- data/lib/drifter/location.rb +43 -0
- data/lib/drifter/location/locatable.rb +35 -0
- data/lib/drifter/version.rb +3 -0
- data/test/google_geocoder_test.rb +85 -0
- data/test/locatable_test.rb +101 -0
- data/test/location_test.rb +27 -0
- data/test/responses/google_error +4 -0
- data/test/responses/google_many_results +478 -0
- data/test/responses/google_no_results +4 -0
- data/test/responses/google_one_result +55 -0
- data/test/responses/yahoo_error +1 -0
- data/test/responses/yahoo_many_results +1 -0
- data/test/responses/yahoo_no_results +1 -0
- data/test/responses/yahoo_one_result +1 -0
- data/test/test_helper.rb +22 -0
- data/test/yahoo_geocoder_test.rb +76 -0
- metadata +110 -0
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'drifter/geocoders/base'
|
2
|
+
|
3
|
+
module Drifter
|
4
|
+
module Geocoders
|
5
|
+
|
6
|
+
# This class adds support for Google's geocoding API:
|
7
|
+
# http://code.google.com/apis/maps/documentation/geocoding/
|
8
|
+
class Google < Drifter::Geocoders::Base
|
9
|
+
|
10
|
+
GOOGLE_BASE_URI = 'http://maps.googleapis.com/maps/api/geocode/json'
|
11
|
+
|
12
|
+
|
13
|
+
# nodoc
|
14
|
+
def self.base_uri
|
15
|
+
GOOGLE_BASE_URI
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
# This method works exactly like Drifter::Geocoders::Yahoo.geocode()
|
20
|
+
# See that method for more info. The returned Drifter::Location objects
|
21
|
+
# have the following attributes:
|
22
|
+
#
|
23
|
+
# :address, :city, :state, :state_code, :country, :country_code,
|
24
|
+
# :post_code, :lat, :lng
|
25
|
+
#
|
26
|
+
# Additional google specific attributes can be accessed using the Location
|
27
|
+
# object's data() method
|
28
|
+
def self.geocode(location, params={})
|
29
|
+
|
30
|
+
params[:address] = location
|
31
|
+
|
32
|
+
# check for reverse gecoding
|
33
|
+
lat, lng = Drifter.extract_latlng(location)
|
34
|
+
if lat && lng
|
35
|
+
params.delete(:address)
|
36
|
+
params[:latlng] = [lat, lng].join(',')
|
37
|
+
end
|
38
|
+
|
39
|
+
uri = query_uri(params)
|
40
|
+
response = fetch(uri)
|
41
|
+
|
42
|
+
# check for errors and return if necassary
|
43
|
+
doc = JSON.parse(response)
|
44
|
+
unless ["OK", "ZERO_RESULTS"].include?(doc["status"])
|
45
|
+
@@last_error = { :code => doc["status"], :message => doc["status"] }
|
46
|
+
return nil
|
47
|
+
end
|
48
|
+
|
49
|
+
# still here so safe to clear errors
|
50
|
+
@@last_error = nil
|
51
|
+
|
52
|
+
# is there anything to parse?
|
53
|
+
return [] if doc["status"] == "ZERO_RESULTS"
|
54
|
+
|
55
|
+
doc["results"].collect do |result|
|
56
|
+
loc = Drifter::Location.new
|
57
|
+
loc.raw_data_format = :hash
|
58
|
+
loc.raw_data = result
|
59
|
+
loc.geocoder = :google
|
60
|
+
|
61
|
+
loc.address = result["formatted_address"]
|
62
|
+
loc.lat = result["geometry"]["location"]["lat"]
|
63
|
+
loc.lng = result["geometry"]["location"]["lng"]
|
64
|
+
|
65
|
+
result["address_components"].each do |comp|
|
66
|
+
loc.country_code = comp["short_name"] if comp["types"].include?("country")
|
67
|
+
loc.country = comp["long_name"] if comp["types"].include?("country")
|
68
|
+
loc.city = comp["long_name"] if comp["types"].include?("locality")
|
69
|
+
loc.post_code = comp["long_name"] if comp["types"].include?("postal_code")
|
70
|
+
loc.state = comp["long_name"] if comp["types"].include?("administrative_area_level_1")
|
71
|
+
loc.state_code = comp["short_name"] if comp["types"].include?("administrative_area_level_1")
|
72
|
+
end
|
73
|
+
loc
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
# Google requires a 'sensor' parameter. If none is set, it defaults to false
|
80
|
+
# See their docs for more info
|
81
|
+
def self.query_uri(params={})
|
82
|
+
params[:sensor] ||= 'false'
|
83
|
+
uri = URI.parse(base_uri)
|
84
|
+
uri.query = hash_to_query_string(params)
|
85
|
+
return uri
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'drifter/geocoders/base'
|
2
|
+
module Drifter
|
3
|
+
module Geocoders
|
4
|
+
|
5
|
+
# This class adds support for basic ip address geocoding using the
|
6
|
+
# free API from hostip.info
|
7
|
+
class HostIP < Drifter::Geocoders::Base
|
8
|
+
|
9
|
+
@@lat_error = nil
|
10
|
+
BASE_URI = 'http://api.hostip.info/get_html.php'
|
11
|
+
IP_PATTERN = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
|
12
|
+
|
13
|
+
|
14
|
+
# nodoc
|
15
|
+
def self.base_uri
|
16
|
+
BASE_URI
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
# geocodes the given IP address.
|
21
|
+
#
|
22
|
+
# On Success: returns an array with one Drifter::Location object
|
23
|
+
# On Failure: returns an empty array
|
24
|
+
# On Error: returns nil. last_error() holds error information
|
25
|
+
def self.geocode(ip, options={})
|
26
|
+
|
27
|
+
# TODO: tests!
|
28
|
+
|
29
|
+
# make sure it's an IP address
|
30
|
+
unless ip.to_s =~ IP_PATTERN
|
31
|
+
@@last_error = { :message => ip.to_s + " is not a valid IP address" }
|
32
|
+
return nil
|
33
|
+
end
|
34
|
+
|
35
|
+
# the position param is needed for lat/lng
|
36
|
+
options[:ip] = ip.to_s
|
37
|
+
options[:position] = true
|
38
|
+
uri = query_uri(options)
|
39
|
+
|
40
|
+
# get the response, should be 5 lines (6 but one is blank)
|
41
|
+
response = fetch(uri)
|
42
|
+
response = response.to_s.split("\n").collect { |line| line.empty?? nil : line }
|
43
|
+
response.compact!
|
44
|
+
unless response.size == 5
|
45
|
+
@@last_error = { :message => "HostIP returned a response that #{name} doesn't understand" }
|
46
|
+
return nil
|
47
|
+
end
|
48
|
+
|
49
|
+
# still here so the errors can be cleared
|
50
|
+
@@last_error = nil
|
51
|
+
|
52
|
+
# however, hostip wont return an error response for bad queries.
|
53
|
+
# It just returns blank values and XX as the country code. Treat that a
|
54
|
+
# a successful request with no results:
|
55
|
+
return [] if response.first =~ /XX/
|
56
|
+
|
57
|
+
# now we can start building the object
|
58
|
+
loc = Drifter::Location.new
|
59
|
+
|
60
|
+
# Country: UNITED KINGDOM (UK)
|
61
|
+
data = response[0].split(': ').last.split(' (')
|
62
|
+
loc.country = data.first
|
63
|
+
loc.country_code = data.last.sub(')', '')
|
64
|
+
|
65
|
+
# City: London
|
66
|
+
data = response[1].split(': ').last
|
67
|
+
loc.city = data
|
68
|
+
|
69
|
+
# Latitude: 51.5
|
70
|
+
data = response[2].split(': ').last
|
71
|
+
loc.lat = data.to_f
|
72
|
+
|
73
|
+
# Longitude: -0.1167
|
74
|
+
data = response[3].split(': ').last
|
75
|
+
loc.lng = data.to_f
|
76
|
+
|
77
|
+
return [loc]
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
# nodoc
|
82
|
+
def self.query_uri(params)
|
83
|
+
uri = URI.parse(base_uri)
|
84
|
+
uri.query = hash_to_query_string(params)
|
85
|
+
return uri
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'drifter/geocoders/base'
|
2
|
+
module Drifter
|
3
|
+
module Geocoders
|
4
|
+
|
5
|
+
# This class adds support for the Yahoo Placefinder API:
|
6
|
+
# http://developer.yahoo.com/geo/placefinder/
|
7
|
+
#
|
8
|
+
# You must set your appid before using this geocoder:
|
9
|
+
# Drifter::Geocoders::Yahoo.api_key = my_yahoo_appid
|
10
|
+
class Yahoo < Drifter::Geocoders::Base
|
11
|
+
|
12
|
+
YAHOO_BASE_URI = "http://where.yahooapis.com/geocode"
|
13
|
+
@@api_key = nil
|
14
|
+
|
15
|
+
|
16
|
+
# returns the API key (also known as appid) if set, or nil
|
17
|
+
def self.api_key
|
18
|
+
@@api_key
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
# sets the api key. Yahoo's API key is known as the appid and
|
23
|
+
# is required for all calls to their Placefinder web service
|
24
|
+
def self.api_key=(value)
|
25
|
+
@@api_key = value
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
# nodoc
|
30
|
+
def self.base_uri
|
31
|
+
YAHOO_BASE_URI
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
# Geocodes 'location'. Returns an array of Drifter::Location objects
|
36
|
+
# To geocode a set of coordinates (known as reverse geocoding), you can
|
37
|
+
# pass a two item array containing lat and lng or an object that responds
|
38
|
+
# to lat() and lng(). Examples:
|
39
|
+
#
|
40
|
+
#
|
41
|
+
# >> Drifter::Geocoders::Yahoo.geocode("Manchester, UK")
|
42
|
+
# >> Drifter::Geocoders::Yahoo.geocode([52.555, -2.123])
|
43
|
+
# >> loc = SomeObject.new :lat => 52.555, :lng => -2.123
|
44
|
+
# >> Drifter::Geocoders::Yahoo.geocode(loc)
|
45
|
+
#
|
46
|
+
#
|
47
|
+
# The returned Drifter::Location objects have the following attributes:
|
48
|
+
#
|
49
|
+
# :address, :city, :state, :state_code, :country, :country_code,
|
50
|
+
# :post_code, :lat, :lng
|
51
|
+
#
|
52
|
+
# Any additional data returned by the geocoder can be accessed via the
|
53
|
+
# Location object's data() method
|
54
|
+
#
|
55
|
+
# You can also customise the type of data Yahoo returns by
|
56
|
+
# modifying the 'flags' parameter . e.g if 'flags' contains a T, yahoo
|
57
|
+
# also returns a 'timezone' attribute for the location:
|
58
|
+
#
|
59
|
+
#
|
60
|
+
# >> results = Drifter::Geocoders::Yahoo.geocode("Manchester, UK", :flags => "T")
|
61
|
+
# >> results.first.data["timezone"]
|
62
|
+
# => "Europe/London"
|
63
|
+
#
|
64
|
+
# Yahoo supports other parameters and flags too, see their docs for more details.
|
65
|
+
# http://developer.yahoo.com/geo/placefinder/guide/requests.html
|
66
|
+
def self.geocode(location, params={})
|
67
|
+
|
68
|
+
# set defaults and build the query
|
69
|
+
params[:location] = location
|
70
|
+
params[:flags] ||= 'J'
|
71
|
+
|
72
|
+
# reverse geocoding?
|
73
|
+
lat, lng = Drifter.extract_latlng(location)
|
74
|
+
if lat && lng
|
75
|
+
params[:location] = [lat, lng].join(',')
|
76
|
+
params[:gflags] = params[:gflags].to_s + 'R'
|
77
|
+
end
|
78
|
+
|
79
|
+
check_flags_parameter!(params)
|
80
|
+
uri = query_uri(params)
|
81
|
+
response = fetch(uri)
|
82
|
+
|
83
|
+
# set @@last_error and return nil on error
|
84
|
+
doc = JSON.parse(response)
|
85
|
+
if doc["ResultSet"]["Error"] != 0
|
86
|
+
@@last_error = {
|
87
|
+
:code => doc["ResultSet"]["Error"],
|
88
|
+
:message => doc["ResultSet"]["ErrorMessage"]
|
89
|
+
}
|
90
|
+
return nil
|
91
|
+
end
|
92
|
+
|
93
|
+
# successful so clear any previous errors
|
94
|
+
@@last_error = nil
|
95
|
+
|
96
|
+
# check for results
|
97
|
+
return [] if doc["ResultSet"]["Found"] == 0
|
98
|
+
|
99
|
+
# build and return an array of Drifter::Location objects
|
100
|
+
doc["ResultSet"]["Results"].collect do |result|
|
101
|
+
loc = Drifter::Location.new
|
102
|
+
|
103
|
+
# add all the standard attributes
|
104
|
+
lines = [result["line1"], result["line2"], result["line3"], result["line4"]]
|
105
|
+
lines.delete_if { |line| line.empty? }
|
106
|
+
loc.address = lines.join(', ')
|
107
|
+
|
108
|
+
loc.city = result["city"]
|
109
|
+
loc.state = result["state"]
|
110
|
+
loc.state_code = result["statecode"]
|
111
|
+
loc.country = result["country"]
|
112
|
+
loc.country_code = result["countrycode"]
|
113
|
+
loc.post_code = result["postal"]
|
114
|
+
loc.lat = result["latitude"]
|
115
|
+
loc.lng = result["longitude"]
|
116
|
+
|
117
|
+
# each Location object can also access the raw data if required
|
118
|
+
loc.raw_data = result
|
119
|
+
loc.raw_data_format = :hash
|
120
|
+
loc.geocoder = :yahoo
|
121
|
+
loc
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
|
127
|
+
# returns a URI object after checking that we have an appid and at least one location parameter.
|
128
|
+
# any parameter that the Yahoo placefinder API supports can be passed in params
|
129
|
+
def self.query_uri(params={})
|
130
|
+
# check we have all required parameters
|
131
|
+
check_api_key!
|
132
|
+
uri = URI.parse(base_uri)
|
133
|
+
uri.query = hash_to_query_string(params)
|
134
|
+
return uri
|
135
|
+
end
|
136
|
+
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
|
141
|
+
# raises ArgumentError if @@api_key is nil
|
142
|
+
def self.check_api_key!
|
143
|
+
return unless api_key.nil?
|
144
|
+
raise ArgumentError, "API Key (yahoo's appid) is missing!\nPlease set it using #{name}.api_key"
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
# geocode() needs responses in JSON format. If flags contains a P or doesn't
|
149
|
+
# contain a J the response wont be JSON. This method fixes 'bad' flags
|
150
|
+
def self.check_flags_parameter!(params)
|
151
|
+
flags = params[:flags].to_s
|
152
|
+
flags = flags.gsub(/p/i, '')
|
153
|
+
flags << 'J' unless flags.index('J')
|
154
|
+
params[:flags] = flags
|
155
|
+
end
|
156
|
+
|
157
|
+
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'drifter/location/locatable'
|
2
|
+
module Drifter
|
3
|
+
# Drifter.geocode() returns an array of Drifter::Location objects
|
4
|
+
# Depending on the geocoder used, Location objects are populated
|
5
|
+
# with a bunch of common attributes - see the docs for individual
|
6
|
+
# geocoders for a list of attributes they set:
|
7
|
+
#
|
8
|
+
# Drifter::Geocoders::Google.geocode()
|
9
|
+
# Drifter::Geocoders::Yahoo.geocode()
|
10
|
+
#
|
11
|
+
# Additional data returned by the geocoder can be accessed via the
|
12
|
+
# data() method
|
13
|
+
class Location
|
14
|
+
include Drifter::Location::Locatable
|
15
|
+
|
16
|
+
attr_accessor :raw_data
|
17
|
+
attr_accessor :raw_data_format
|
18
|
+
attr_accessor :geocoder
|
19
|
+
|
20
|
+
attr_accessor :address
|
21
|
+
attr_accessor :city
|
22
|
+
attr_accessor :state
|
23
|
+
attr_accessor :state_code
|
24
|
+
attr_accessor :post_code
|
25
|
+
attr_accessor :country
|
26
|
+
attr_accessor :country_code
|
27
|
+
attr_accessor :lat
|
28
|
+
attr_accessor :lng
|
29
|
+
|
30
|
+
# returns a Hash containing the geocoder's raw data. This is geocoder
|
31
|
+
# specific and you should read the provider's docs to see what data
|
32
|
+
# they return in each geocoding response
|
33
|
+
def data
|
34
|
+
@data ||= case raw_data_format
|
35
|
+
when :hash then return raw_data
|
36
|
+
when :json then return JSON.parse(raw_data)
|
37
|
+
else nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# classes including this module must repond to lat(), lat=(), lng() and lng=()
|
2
|
+
module Drifter
|
3
|
+
class Location
|
4
|
+
module Locatable
|
5
|
+
|
6
|
+
|
7
|
+
# nodoc
|
8
|
+
def distance_to(loc, options={})
|
9
|
+
Drifter::Distance::Haversine.between(self, loc, options)
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
# returns an empty Drifter:;Location object with lat and lng
|
14
|
+
def location
|
15
|
+
loc = Drifter::Location.new
|
16
|
+
loc.lat = lat
|
17
|
+
loc.lng = lng
|
18
|
+
return loc
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
# sets lat and lng on the receiver using value. value can be any
|
23
|
+
# object that responds to lat() and lng() or a two-item [lat,lng] Array
|
24
|
+
# if value is nil, lat and lng are both set to nil
|
25
|
+
def location=(value)
|
26
|
+
lat, lng = nil, nil
|
27
|
+
lat, lng = Drifter.extract_latlng!(value) unless value.nil?
|
28
|
+
self.lat = lat
|
29
|
+
self.lng = lng
|
30
|
+
return value
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class GoogleGeocoderTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
end
|
7
|
+
|
8
|
+
# google's raw data isn't a simple key => value hash of address attributes
|
9
|
+
# so we have to do a little digging
|
10
|
+
def attribute_matching(key, data, property=:long_name)
|
11
|
+
data["address_components"].each do |comp|
|
12
|
+
return comp[property.to_s] if comp["types"].include?(key)
|
13
|
+
end
|
14
|
+
return nil
|
15
|
+
end
|
16
|
+
|
17
|
+
# checks that the given Location object has the the expected values when Geocoded using google
|
18
|
+
def assert_google_location(data, loc)
|
19
|
+
assert_equal :google, loc.geocoder
|
20
|
+
assert_equal :hash, loc.raw_data_format
|
21
|
+
|
22
|
+
assert_equal attribute_matching("locality", data), loc.city
|
23
|
+
assert_equal attribute_matching("administrative_area_level_1", data), loc.state
|
24
|
+
assert_equal attribute_matching("postal_code", data), loc.post_code
|
25
|
+
assert_equal attribute_matching("country", data, :short_name), loc.country_code
|
26
|
+
assert_equal data["geometry"]["location"]["lat"], loc.lat
|
27
|
+
assert_equal data["geometry"]["location"]["lng"], loc.lng
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
# nodoc
|
32
|
+
def stub_response(response_file)
|
33
|
+
response = open_web_response(response_file)
|
34
|
+
rx = Regexp.new(Drifter::Geocoders::Google.base_uri)
|
35
|
+
FakeWeb.register_uri(:get, rx, :body => response)
|
36
|
+
|
37
|
+
results = Drifter::Geocoders::Google.geocode("springfield")
|
38
|
+
data = JSON.parse(response)
|
39
|
+
error = Drifter::Geocoders::Google.last_error
|
40
|
+
|
41
|
+
return results, data, error
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
# nodoc
|
46
|
+
def test_error
|
47
|
+
results, data, error = stub_response('google_error')
|
48
|
+
assert_nil results
|
49
|
+
assert_equal data["status"], error[:message]
|
50
|
+
assert_equal data["status"], error[:code]
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
# nodoc
|
55
|
+
def test_success_with_no_results
|
56
|
+
results, data, error = stub_response('google_no_results')
|
57
|
+
assert_nil error
|
58
|
+
assert results.is_a?(Array)
|
59
|
+
assert results.empty?
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
# nodoc
|
64
|
+
def test_success_with_one_result
|
65
|
+
results, data, error = stub_response('google_one_result')
|
66
|
+
assert_nil error
|
67
|
+
assert results.is_a?(Array)
|
68
|
+
assert_equal 1, results.size
|
69
|
+
assert_google_location data["results"].first, results.first
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
# ndoc
|
74
|
+
def test_success_with_many_results
|
75
|
+
results, data, error = stub_response('google_many_results')
|
76
|
+
assert_nil error
|
77
|
+
assert results.is_a?(Array)
|
78
|
+
assert_equal 10, results.size # this value not returned by google hence hardcoded
|
79
|
+
results.each_with_index do |loc, i|
|
80
|
+
loc_data = data["results"][i]
|
81
|
+
assert_google_location loc_data, loc
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|