rails-geocoder 0.9.10 → 0.9.11

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,34 +2,113 @@ module Geocoder
2
2
  module Calculations
3
3
  extend self
4
4
 
5
+ ##
6
+ # Compass point names, listed clockwise starting at North.
7
+ #
8
+ # If you want bearings named using more, fewer, or different points
9
+ # override Geocoder::Calculations.COMPASS_POINTS with your own array.
10
+ #
11
+ COMPASS_POINTS = %w[N NE E SE S SW W NW]
12
+
13
+ ##
14
+ # Radius of the Earth, in kilometers.
15
+ # Value taken from: http://en.wikipedia.org/wiki/Earth_radius
16
+ #
17
+ EARTH_RADIUS = 6371.0
18
+
19
+ ##
20
+ # Conversion factor: multiply by kilometers to get miles.
21
+ #
22
+ KM_IN_MI = 0.621371192
23
+
24
+ ##
25
+ # Calculate the distance spanned by one
26
+ # degree of latitude in the given units.
27
+ #
28
+ def latitude_degree_distance(units = :mi)
29
+ 2 * Math::PI * earth_radius(units) / 360
30
+ end
31
+
32
+ ##
33
+ # Calculate the distance spanned by one degree of longitude
34
+ # at the given latitude. This ranges from around 69 miles at
35
+ # the equator to zero at the poles.
36
+ #
37
+ def longitude_degree_distance(latitude, units = :mi)
38
+ latitude_degree_distance(units) * Math.cos(to_radians(latitude))
39
+ end
40
+
5
41
  ##
6
42
  # Calculate the distance between two points on Earth (Haversine formula).
7
43
  # Takes two sets of coordinates and an options hash:
8
44
  #
9
- # <tt>:units</tt> :: <tt>:mi</tt> (default) or <tt>:km</tt>
45
+ # * <tt>:units</tt> - <tt>:mi</tt> (default) or <tt>:km</tt>
10
46
  #
11
47
  def distance_between(lat1, lon1, lat2, lon2, options = {})
12
48
 
13
49
  # set default options
14
50
  options[:units] ||= :mi
15
51
 
16
- # define conversion factors
17
- conversions = { :mi => 3956, :km => 6371 }
18
-
19
52
  # convert degrees to radians
20
- lat1 = to_radians(lat1)
21
- lon1 = to_radians(lon1)
22
- lat2 = to_radians(lat2)
23
- lon2 = to_radians(lon2)
53
+ lat1, lon1, lat2, lon2 = to_radians(lat1, lon1, lat2, lon2)
24
54
 
25
- # compute distances
26
- dlat = (lat1 - lat2).abs
27
- dlon = (lon1 - lon2).abs
55
+ # compute deltas
56
+ dlat = lat2 - lat1
57
+ dlon = lon2 - lon1
28
58
 
29
59
  a = (Math.sin(dlat / 2))**2 + Math.cos(lat1) *
30
60
  (Math.sin(dlon / 2))**2 * Math.cos(lat2)
31
61
  c = 2 * Math.atan2( Math.sqrt(a), Math.sqrt(1-a))
32
- c * conversions[options[:units]]
62
+ c * earth_radius(options[:units])
63
+ end
64
+
65
+ ##
66
+ # Calculate bearing between two sets of coordinates.
67
+ # Returns a number of degrees from due north (clockwise).
68
+ #
69
+ # Also accepts an options hash:
70
+ #
71
+ # * <tt>:method</tt> - <tt>:linear</tt> (default) or <tt>:spherical</tt>;
72
+ # the spherical method is "correct" in that it returns the shortest path
73
+ # (one along a great circle) but the linear method is the default as it
74
+ # is less confusing (returns due east or west when given two points with
75
+ # the same latitude)
76
+ #
77
+ # Based on: http://www.movable-type.co.uk/scripts/latlong.html
78
+ #
79
+ def bearing_between(lat1, lon1, lat2, lon2, options = {})
80
+ options[:method] = :linear unless options[:method] == :spherical
81
+
82
+ # convert degrees to radians
83
+ lat1, lon1, lat2, lon2 = to_radians(lat1, lon1, lat2, lon2)
84
+
85
+ # compute deltas
86
+ dlat = lat2 - lat1
87
+ dlon = lon2 - lon1
88
+
89
+ case options[:method]
90
+ when :linear
91
+ y = dlon
92
+ x = dlat
93
+
94
+ when :spherical
95
+ y = Math.sin(dlon) * Math.cos(lat2)
96
+ x = Math.cos(lat1) * Math.sin(lat2) -
97
+ Math.sin(lat1) * Math.cos(lat2) * Math.cos(dlon)
98
+ end
99
+
100
+ bearing = Math.atan2(x,y)
101
+ # Answer is in radians counterclockwise from due east.
102
+ # Convert to degrees clockwise from due north:
103
+ (90 - to_degrees(bearing) + 360) % 360
104
+ end
105
+
106
+ ##
107
+ # Translate a bearing (float) into a compass direction (string, eg "North").
108
+ #
109
+ def compass_point(bearing, points = COMPASS_POINTS)
110
+ seg_size = 360 / points.size
111
+ points[((bearing + (seg_size / 2)) % 360) / seg_size]
33
112
  end
34
113
 
35
114
  ##
@@ -41,12 +120,10 @@ module Geocoder
41
120
  def geographic_center(points)
42
121
 
43
122
  # convert objects to [lat,lon] arrays and remove nils
44
- points = points.map{ |p|
45
- p.is_a?(Array) ? p : (p.geocoded?? p.read_coordinates : nil)
46
- }.compact
123
+ points.map!{ |p| p.is_a?(Array) ? p : p.to_coordinates }.compact
47
124
 
48
125
  # convert degrees to radians
49
- points.map!{ |p| [to_radians(p[0]), to_radians(p[1])] }
126
+ points.map!{ |p| to_radians(p) }
50
127
 
51
128
  # convert to Cartesian coordinates
52
129
  x = []; y = []; z = []
@@ -67,28 +144,91 @@ module Geocoder
67
144
  lat = Math.atan2(za, hyp)
68
145
 
69
146
  # return answer in degrees
70
- [to_degrees(lat), to_degrees(lon)]
147
+ to_degrees [lat, lon]
148
+ end
149
+
150
+ ##
151
+ # Returns coordinates of the lower-left and upper-right corners of a box
152
+ # with the given point at its center. The radius is the shortest distance
153
+ # from the center point to any side of the box (the length of each side
154
+ # is twice the radius).
155
+ #
156
+ # This is useful for finding corner points of a map viewport, or for
157
+ # roughly limiting the possible solutions in a geo-spatial search
158
+ # (ActiveRecord queries use it thusly).
159
+ #
160
+ def bounding_box(latitude, longitude, radius, options = {})
161
+ units = options[:units] || :mi
162
+ radius = radius.to_f
163
+ [
164
+ latitude - (radius / latitude_degree_distance(units)),
165
+ longitude - (radius / longitude_degree_distance(latitude, units)),
166
+ latitude + (radius / latitude_degree_distance(units)),
167
+ longitude + (radius / longitude_degree_distance(latitude, units))
168
+ ]
71
169
  end
72
170
 
73
171
  ##
74
172
  # Convert degrees to radians.
173
+ # If an array (or multiple arguments) is passed,
174
+ # converts each value and returns array.
75
175
  #
76
- def to_radians(degrees)
77
- degrees * (Math::PI / 180)
176
+ def to_radians(*args)
177
+ args = args.first if args.first.is_a?(Array)
178
+ if args.size == 1
179
+ args.first * (Math::PI / 180)
180
+ else
181
+ args.map{ |i| to_radians(i) }
182
+ end
78
183
  end
79
184
 
80
185
  ##
81
186
  # Convert radians to degrees.
187
+ # If an array (or multiple arguments) is passed,
188
+ # converts each value and returns array.
189
+ #
190
+ def to_degrees(*args)
191
+ args = args.first if args.first.is_a?(Array)
192
+ if args.size == 1
193
+ (args.first * 180.0) / Math::PI
194
+ else
195
+ args.map{ |i| to_degrees(i) }
196
+ end
197
+ end
198
+
199
+ ##
200
+ # Convert miles to kilometers.
201
+ #
202
+ def to_kilometers(mi)
203
+ mi * mi_in_km
204
+ end
205
+
206
+ ##
207
+ # Convert kilometers to miles.
82
208
  #
83
- def to_degrees(radians)
84
- (radians * 180.0) / Math::PI
209
+ def to_miles(km)
210
+ km * km_in_mi
211
+ end
212
+
213
+ ##
214
+ # Radius of the Earth in the given units (:mi or :km). Default is :mi.
215
+ #
216
+ def earth_radius(units = :mi)
217
+ units == :km ? EARTH_RADIUS : to_miles(EARTH_RADIUS)
85
218
  end
86
219
 
87
220
  ##
88
221
  # Conversion factor: km to mi.
89
222
  #
90
223
  def km_in_mi
91
- 0.621371192
224
+ KM_IN_MI
225
+ end
226
+
227
+ ##
228
+ # Conversion factor: mi to km.
229
+ #
230
+ def mi_in_km
231
+ 1.0 / KM_IN_MI
92
232
  end
93
233
  end
94
234
  end
@@ -1,16 +1,53 @@
1
1
  module Geocoder
2
2
  class Configuration
3
- def self.timeout; @@timeout; end
4
- def self.timeout=(obj); @@timeout = obj; end
5
3
 
6
- def self.lookup; @@lookup; end
7
- def self.lookup=(obj); @@lookup = obj; end
4
+ def self.options_and_defaults
5
+ [
6
+ # geocoding service timeout (secs)
7
+ [:timeout, 3],
8
8
 
9
- def self.yahoo_appid; @@yahoo_appid; end
10
- def self.yahoo_appid=(obj); @@yahoo_appid = obj; end
9
+ # name of geocoding service (symbol)
10
+ [:lookup, :google],
11
+
12
+ # ISO-639 language code
13
+ [:language, :en],
14
+
15
+ # use HTTPS for lookup requests? (if supported)
16
+ [:use_https, false],
17
+
18
+ # API key for geocoding service
19
+ [:api_key, nil],
20
+
21
+ # cache object (must respond to #[], #[]=, and #keys)
22
+ [:cache, nil],
23
+
24
+ # prefix (string) to use for all cache keys
25
+ [:cache_prefix, "geocoder:"]
26
+ ]
27
+ end
28
+
29
+ # define getters and setters for all configuration settings
30
+ self.options_and_defaults.each do |o,d|
31
+ eval("def self.#{o}; @@#{o}; end")
32
+ eval("def self.#{o}=(obj); @@#{o} = obj; end")
33
+ end
34
+
35
+ # legacy support
36
+ def self.yahoo_app_id=(value)
37
+ warn "DEPRECATION WARNING: Geocoder's 'yahoo_app_id' setting has been replaced by 'api_key'. " +
38
+ "This method will be removed in Geocoder v1.0."
39
+ @@api_key = value
40
+ end
41
+
42
+ ##
43
+ # Set all values to default.
44
+ #
45
+ def self.set_defaults
46
+ self.options_and_defaults.each do |o,d|
47
+ self.send("#{o}=", d)
48
+ end
49
+ end
11
50
  end
12
51
  end
13
52
 
14
- Geocoder::Configuration.timeout = 3
15
- Geocoder::Configuration.lookup = :google
16
- Geocoder::Configuration.yahoo_appid = ""
53
+ Geocoder::Configuration.set_defaults
@@ -1,6 +1,7 @@
1
1
  require 'net/http'
2
2
  unless defined?(ActiveSupport::JSON)
3
3
  begin
4
+ require 'rubygems' # for Ruby 1.8
4
5
  require 'json'
5
6
  rescue LoadError
6
7
  raise LoadError, "Please install the 'json' or 'json_pure' gem to parse geocoder results."
@@ -17,12 +18,11 @@ module Geocoder
17
18
  #
18
19
  # Takes a search string (eg: "Mississippi Coast Coliseumf, Biloxi, MS",
19
20
  # "205.128.54.202") for geocoding, or coordinates (latitude, longitude)
20
- # for reverse geocoding.
21
+ # for reverse geocoding. Returns an array of <tt>Geocoder::Result</tt>s.
21
22
  #
22
23
  def search(*args)
23
- if res = result(args.join(","), args.size == 2)
24
- result_class.new(res)
25
- end
24
+ reverse = (args.size == 2) || coordinates?(args.first)
25
+ results(args.join(","), reverse).map{ |r| result_class.new(r) }
26
26
  end
27
27
 
28
28
 
@@ -31,7 +31,7 @@ module Geocoder
31
31
  ##
32
32
  # Geocoder::Result object or nil on timeout or other error.
33
33
  #
34
- def result(query, reverse = false)
34
+ def results(query, reverse = false)
35
35
  fail
36
36
  end
37
37
 
@@ -60,7 +60,7 @@ module Geocoder
60
60
  rescue TimeoutError
61
61
  warn "Geocoding API not responding fast enough " +
62
62
  "(see Geocoder::Configuration.timeout to set limit)."
63
- end
63
+ end
64
64
  end
65
65
 
66
66
  ##
@@ -78,16 +78,37 @@ module Geocoder
78
78
  end
79
79
  end
80
80
 
81
+ ##
82
+ # Protocol to use for communication with geocoding services.
83
+ # Set in configuration but not available for every service.
84
+ #
85
+ def protocol
86
+ "http" + (Geocoder::Configuration.use_https ? "s" : "")
87
+ end
88
+
81
89
  ##
82
90
  # Fetches a raw search result (JSON string).
83
91
  #
84
92
  def fetch_raw_data(query, reverse = false)
85
- url = query_url(query, reverse)
86
93
  timeout(Geocoder::Configuration.timeout) do
87
- Net::HTTP.get_response(URI.parse(url)).body
94
+ url = query_url(query, reverse)
95
+ unless cache and response = cache[url]
96
+ response = Net::HTTP.get_response(URI.parse(url)).body
97
+ if cache
98
+ cache[url] = response
99
+ end
100
+ end
101
+ response
88
102
  end
89
103
  end
90
104
 
105
+ ##
106
+ # The working Cache object.
107
+ #
108
+ def cache
109
+ Geocoder.cache
110
+ end
111
+
91
112
  ##
92
113
  # Is the given string a loopback IP address?
93
114
  #
@@ -95,12 +116,22 @@ module Geocoder
95
116
  !!(ip == "0.0.0.0" or ip.match(/^127/))
96
117
  end
97
118
 
119
+ ##
120
+ # Does the given string look like latitude/longitude coordinates?
121
+ #
122
+ def coordinates?(value)
123
+ !!value.to_s.match(/^[0-9\.\-]+, ?[0-9\.\-]+$/)
124
+ end
125
+
98
126
  ##
99
127
  # Simulate ActiveSupport's Object#to_query.
128
+ # Removes any keys with nil value.
100
129
  #
101
130
  def hash_to_query(hash)
102
131
  require 'cgi' unless defined?(CGI) && defined?(CGI.escape)
103
- hash.collect{ |p| p.map{ |i| CGI.escape i.to_s } * '=' }.sort * '&'
132
+ hash.collect{ |p|
133
+ p[1].nil? ? nil : p.map{ |i| CGI.escape i.to_s } * '='
134
+ }.compact.sort * '&'
104
135
  end
105
136
  end
106
137
  end
@@ -6,15 +6,13 @@ module Geocoder::Lookup
6
6
 
7
7
  private # ---------------------------------------------------------------
8
8
 
9
- def result(query, reverse = false)
9
+ def results(query, reverse = false)
10
10
  # don't look up a loopback address, just return the stored result
11
- return reserved_result(query) if loopback_address?(query)
11
+ return [reserved_result(query)] if loopback_address?(query)
12
12
  begin
13
- if doc = fetch_data(query, reverse)
14
- doc
15
- end
13
+ return [fetch_data(query, reverse)]
16
14
  rescue StandardError # Freegeoip.net returns HTML on bad request
17
- nil
15
+ return []
18
16
  end
19
17
  end
20
18
 
@@ -0,0 +1,44 @@
1
+ require 'geocoder/lookups/base'
2
+ require "geocoder/results/geocoder_ca"
3
+
4
+ module Geocoder::Lookup
5
+ class GeocoderCa < Base
6
+
7
+ private # ---------------------------------------------------------------
8
+
9
+ def results(query, reverse = false)
10
+ return [] unless doc = fetch_data(query, reverse)
11
+ if doc['error'].nil?
12
+ return [doc]
13
+ elsif doc['error']['code'] == "005"
14
+ # "Postal Code is not in the proper Format" => no results, just shut up
15
+ else
16
+ warn "Geocoder.ca service error: #{doc['error']['code']} (#{doc['error']['description']})."
17
+ end
18
+ return []
19
+ end
20
+
21
+ def query_url(query, reverse = false)
22
+ params = {
23
+ :geoit => "xml",
24
+ :jsonp => 1,
25
+ :callback => "test"
26
+ }
27
+ if reverse
28
+ lat,lon = query.split(',')
29
+ params[:latt] = lat
30
+ params[:longt] = lon
31
+ params[:corner] = 1
32
+ params[:reverse] = 1
33
+ else
34
+ params[:locate] = query
35
+ end
36
+ "http://geocoder.ca/?" + hash_to_query(params)
37
+ end
38
+
39
+ def parse_raw_data(raw_data)
40
+ super raw_data[/^test\((.*)\)\;\s*$/, 1]
41
+ end
42
+ end
43
+ end
44
+