geocoder 0.9.10 → 0.9.11

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of geocoder might be problematic. Click here for more details.

@@ -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
+