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.
@@ -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,3 @@
1
+ module Drifter
2
+ VERSION = "0.1.0"
3
+ 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