rails-geocoder 0.9.6 → 0.9.7

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc CHANGED
@@ -2,10 +2,15 @@
2
2
 
3
3
  Per-release changes to Geocoder.
4
4
 
5
+ == 0.9.7 (2011 Feb 1)
6
+
7
+ * Add reverse geocoding (+reverse_geocoded_by+).
8
+ * Prevent exception (uninitialized constant Geocoder::Net) when net/http not already required (sleepycat).
9
+
5
10
  == 0.9.6 (2011 Jan 19)
6
11
 
7
12
  * Fix incompatibility with will_paginate gem.
8
- * Include table names in GROUP BY clause of nearby scope to avoid ambiguity in joins.
13
+ * Include table names in GROUP BY clause of nearby scope to avoid ambiguity in joins (matchu).
9
14
 
10
15
  == 0.9.5 (2010 Oct 15)
11
16
 
data/README.rdoc CHANGED
@@ -68,13 +68,13 @@ To find objects by location, use the following scopes:
68
68
  Some utility methods are also available:
69
69
 
70
70
  # distance (in miles) between Eiffel Tower and Empire State Building
71
- Geocoder.distance_between( 48.858205,2.294359, 40.748433,-73.985655 )
71
+ Geocoder::Calculations.distance_between( 48.858205,2.294359, 40.748433,-73.985655 )
72
72
 
73
73
  # look up coordinates of some location (like searching Google Maps)
74
74
  Geocoder.fetch_coordinates("25 Main St, Cooperstown, NY")
75
75
 
76
76
  # find the geographic center (aka center of gravity) of objects or points
77
- Geocoder.geographic_center([ city1, city2, city3, [40.22,-73.99], city4 ])
77
+ Geocoder::Calculations.geographic_center([ city1, city2, city3, [40.22,-73.99], city4 ])
78
78
 
79
79
 
80
80
  == More On Configuration
@@ -101,6 +101,45 @@ If your model has +address+, +city+, +state+, and +country+ attributes you might
101
101
  Please see the code for more methods and detailed information about arguments (eg, working with kilometers).
102
102
 
103
103
 
104
+ == Reverse Geocoding
105
+
106
+ If you need reverse geocoding (lat/long coordinates to address), do something like the following in your model:
107
+
108
+ reverse_geocoded_by :latitude, :longitude
109
+ after_validation :fetch_address
110
+
111
+ and make sure it has +latitude+ and +longitude+ attributes, as well as an +address+ attribute. As with regular geocoding, you can specify alternate names for all of these attributes, for example:
112
+
113
+ reverse_geocoded_by :lat, :lon, :address => :location
114
+
115
+
116
+ == Forward and Reverse Geocoding in the Same Model
117
+
118
+ If you apply both forward and reverse geocoding functionality to the same model, you can provide different methods for storing the fetched address (reverse geocoding) and providing an address to use when fetching coordinates (forward geocoding), for example:
119
+
120
+ class Venue
121
+
122
+ # build an address from street, city, and state attributes
123
+ geocoded_by :address_from_components
124
+
125
+ # store the Google-provided address in the full_address attribute
126
+ reverse_geocoded_by :latitude, :longitude, :address => :full_address
127
+ end
128
+
129
+ However, there can be only one set of latitude/longitude attributes, and whichever you specify last will be used. For example:
130
+
131
+ class Venue
132
+
133
+ geocoded_by :address,
134
+ :latitude => :fetched_latitude, # this will be overridden by the below
135
+ :longitude => :fetched_longitude # same here
136
+
137
+ reverse_geocoded_by :latitude, :longitude
138
+ end
139
+
140
+ The reason for this is that we don't want ambiguity when doing distance calculations. We need a single, authoritative source for coordinates!
141
+
142
+
104
143
  == SQLite
105
144
 
106
145
  SQLite's lack of trigonometric functions requires an alternate implementation of the +near+ method (scope). When using SQLite, Geocoder will automatically use a less accurate algorithm for finding objects near a given point. Results of this algorithm should not be trusted too much as it will return objects that are outside the given radius.
@@ -114,9 +153,9 @@ There are few options for finding objects near a given point in SQLite without i
114
153
 
115
154
  1. Use a square instead of a circle for finding nearby points. For example, if you want to find points near 40.71, 100.23, search for objects with latitude between 39.71 and 41.71 and longitude between 99.23 and 101.23. One degree of latitude or longitude is at most 69 miles so divide your radius (in miles) by 69.0 to get the amount to add and subtract from your center coordinates to get the upper and lower bounds. The results will not be very accurate (you'll get points outside the desired radius--at worst 29% farther away), but you will get all the points within the required radius.
116
155
 
117
- 2. Load all objects into memory and compute distances between them using the <tt>Geocoder.distance_between</tt> method. This will produce accurate results but will be very slow (and use a lot of memory) if you have a lot of objects in your database.
156
+ 2. Load all objects into memory and compute distances between them using the <tt>Geocoder::Calculations.distance_between</tt> method. This will produce accurate results but will be very slow (and use a lot of memory) if you have a lot of objects in your database.
118
157
 
119
- 3. If you have a large number of objects (so you can't use approach #2) and you need accurate results (better than approach #1 will give), you can use a combination of the two. Get all the objects within a square around your center point, and then eliminate the ones that are too far away using <tt>Geocoder.distance_between</tt>.
158
+ 3. If you have a large number of objects (so you can't use approach #2) and you need accurate results (better than approach #1 will give), you can use a combination of the two. Get all the objects within a square around your center point, and then eliminate the ones that are too far away using <tt>Geocoder::Calculations.distance_between</tt>.
120
159
 
121
160
  Because Geocoder needs to provide this functionality as a scope, we must go with option #1, but feel free to implement #2 or #3 if you need more accuracy.
122
161
 
@@ -136,7 +175,6 @@ If anyone has a more elegant solution to this problem I am very interested in se
136
175
  * use completely separate "drivers" for different AR adapters?
137
176
  * seems reasonable since we're using very DB-specific features
138
177
  * also need to make sure 'mysql2' is supported
139
- * add reverse geocoding
140
178
  * make 'near' scope work with AR associations
141
179
  * http://stackoverflow.com/questions/3266358/geocoder-rails-plugin-near-search-problem-with-activerecord
142
180
  * prepend table names to column names in SQL distance expression (required
data/lib/geocoder.rb CHANGED
@@ -1,361 +1,53 @@
1
+ require "geocoder/calculations"
2
+ require "geocoder/lookup"
3
+ require "geocoder/active_record"
4
+
1
5
  ##
2
- # Add geocoding functionality (via Google) to any object.
6
+ # Add geocoded_by method to ActiveRecord::Base so Geocoder is accessible.
3
7
  #
4
- module Geocoder
5
-
6
- ##
7
- # Implementation of 'included' hook method.
8
- #
9
- def self.included(base)
10
- base.extend ClassMethods
11
- base.class_eval do
12
-
13
- # scope: geocoded objects
14
- scope :geocoded,
15
- :conditions => "#{geocoder_options[:latitude]} IS NOT NULL " +
16
- "AND #{geocoder_options[:longitude]} IS NOT NULL"
17
-
18
- # scope: not-geocoded objects
19
- scope :not_geocoded,
20
- :conditions => "#{geocoder_options[:latitude]} IS NULL " +
21
- "OR #{geocoder_options[:longitude]} IS NULL"
22
-
23
- ##
24
- # Find all objects within a radius (in miles) of the given location
25
- # (address string). Location (the first argument) may be either a string
26
- # to geocode or an array of coordinates (<tt>[lat,long]</tt>).
27
- #
28
- scope :near, lambda{ |location, *args|
29
- latitude, longitude = location.is_a?(Array) ?
30
- location : Geocoder.fetch_coordinates(location)
31
- if latitude and longitude
32
- near_scope_options(latitude, longitude, *args)
33
- else
34
- {}
35
- end
36
- }
37
- end
38
- end
39
-
40
- ##
41
- # Methods which will be class methods of the including class.
42
- #
43
- module ClassMethods
44
-
45
- ##
46
- # Get options hash suitable for passing to ActiveRecord.find to get
47
- # records within a radius (in miles) of the given point.
48
- # Options hash may include:
49
- #
50
- # +units+ :: <tt>:mi</tt> (default) or <tt>:km</tt>
51
- # +exclude+ :: an object to exclude (used by the #nearbys method)
52
- # +order+ :: column(s) for ORDER BY SQL clause
53
- # +limit+ :: number of records to return (for LIMIT SQL clause)
54
- # +offset+ :: number of records to skip (for OFFSET SQL clause)
55
- # +select+ :: string with the SELECT SQL fragment (e.g. “id, name”)
56
- #
57
- def near_scope_options(latitude, longitude, radius = 20, options = {})
58
- radius *= km_in_mi if options[:units] == :km
59
- if ActiveRecord::Base.connection.adapter_name == "SQLite"
60
- approx_near_scope_options(latitude, longitude, radius, options)
61
- else
62
- full_near_scope_options(latitude, longitude, radius, options)
63
- end
64
- end
65
-
66
-
67
- private # ----------------------------------------------------------------
68
-
69
- ##
70
- # Scope options hash for use with a database that supports POWER(),
71
- # SQRT(), PI(), and trigonometric functions (SIN(), COS(), and ASIN()).
72
- #
73
- # Taken from the excellent tutorial at:
74
- # http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
75
- #
76
- def full_near_scope_options(latitude, longitude, radius, options)
77
- lat_attr = geocoder_options[:latitude]
78
- lon_attr = geocoder_options[:longitude]
79
- distance = "3956 * 2 * ASIN(SQRT(" +
80
- "POWER(SIN((#{latitude} - #{lat_attr}) * " +
81
- "PI() / 180 / 2), 2) + COS(#{latitude} * PI()/180) * " +
82
- "COS(#{lat_attr} * PI() / 180) * " +
83
- "POWER(SIN((#{longitude} - #{lon_attr}) * " +
84
- "PI() / 180 / 2), 2) ))"
85
- options[:order] ||= "#{distance} ASC"
86
- default_near_scope_options(latitude, longitude, radius, options).merge(
87
- :select => "#{options[:select] || '*'}, #{distance} AS distance",
88
- :having => "#{distance} <= #{radius}"
89
- )
90
- end
91
-
92
- ##
93
- # Scope options hash for use with a database without trigonometric
94
- # functions, like SQLite. Approach is to find objects within a square
95
- # rather than a circle, so results are very approximate (will include
96
- # objects outside the given radius).
97
- #
98
- def approx_near_scope_options(latitude, longitude, radius, options)
99
- default_near_scope_options(latitude, longitude, radius, options).merge(
100
- :select => options[:select] || nil
101
- )
102
- end
103
-
104
- ##
105
- # Options used for any near-like scope.
106
- #
107
- def default_near_scope_options(latitude, longitude, radius, options)
108
- lat_attr = geocoder_options[:latitude]
109
- lon_attr = geocoder_options[:longitude]
110
- conditions = \
111
- ["#{lat_attr} BETWEEN ? AND ? AND #{lon_attr} BETWEEN ? AND ?"] +
112
- coordinate_bounds(latitude, longitude, radius)
113
- if obj = options[:exclude]
114
- conditions[0] << " AND id != ?"
115
- conditions << obj.id
116
- end
117
- {
118
- :group => columns.map{ |c| "#{table_name}.#{c.name}" }.join(','),
119
- :order => options[:order],
120
- :limit => options[:limit],
121
- :offset => options[:offset],
122
- :conditions => conditions
123
- }
124
- end
125
-
126
- ##
127
- # Get the rough high/low lat/long bounds for a geographic point and
128
- # radius. Returns an array: <tt>[lat_lo, lat_hi, lon_lo, lon_hi]</tt>.
129
- # Used to constrain search to a (radius x radius) square.
130
- #
131
- def coordinate_bounds(latitude, longitude, radius)
132
- radius = radius.to_f
133
- factor = (Math::cos(latitude * Math::PI / 180.0) * 69.0).abs
134
- [
135
- latitude - (radius / 69.0),
136
- latitude + (radius / 69.0),
137
- longitude - (radius / factor),
138
- longitude + (radius / factor)
139
- ]
140
- end
141
-
142
- ##
143
- # Conversion factor: km to mi.
144
- #
145
- def km_in_mi
146
- 0.621371192
147
- end
148
- end
149
-
150
- ##
151
- # Read the coordinates [lat,lon] of an object. This is not great but it
152
- # seems cleaner than polluting the instance method namespace.
153
- #
154
- def read_coordinates
155
- [:latitude, :longitude].map{ |i| send self.class.geocoder_options[i] }
156
- end
157
-
158
- ##
159
- # Is this object geocoded? (Does it have latitude and longitude?)
160
- #
161
- def geocoded?
162
- read_coordinates.compact.size > 0
163
- end
164
-
165
- ##
166
- # Calculate the distance from the object to a point (lat,lon).
167
- # Valid units are defined in <tt>distance_between</tt> class method.
168
- #
169
- def distance_to(lat, lon, units = :mi)
170
- return nil unless geocoded?
171
- mylat,mylon = read_coordinates
172
- Geocoder.distance_between(mylat, mylon, lat, lon, :units => units)
173
- end
174
-
175
- ##
176
- # Get other geocoded objects within a given radius.
177
- # Valid units are defined in <tt>distance_between</tt> class method.
178
- #
179
- def nearbys(radius = 20, units = :mi)
180
- return [] unless geocoded?
181
- options = {:exclude => self, :units => units}
182
- self.class.near(read_coordinates, radius, options)
183
- end
8
+ ActiveRecord::Base.class_eval do
184
9
 
185
10
  ##
186
- # Fetch coordinates and assign +latitude+ and +longitude+. Also returns
187
- # coordinates as an array: <tt>[lat, lon]</tt>.
11
+ # Set attribute names and include the Geocoder module.
188
12
  #
189
- def fetch_coordinates(save = false)
190
- coords = Geocoder.fetch_coordinates(
191
- send(self.class.geocoder_options[:method_name])
13
+ def self.geocoded_by(address_attr, options = {})
14
+ _geocoder_init(
15
+ :user_address => address_attr,
16
+ :latitude => options[:latitude] || :latitude,
17
+ :longitude => options[:longitude] || :longitude
192
18
  )
193
- unless coords.blank?
194
- method = (save ? "update" : "write") + "_attribute"
195
- send method, self.class.geocoder_options[:latitude], coords[0]
196
- send method, self.class.geocoder_options[:longitude], coords[1]
197
- end
198
- coords
199
- end
200
-
201
- ##
202
- # Fetch coordinates and update (save) +latitude+ and +longitude+ data.
203
- #
204
- def fetch_coordinates!
205
- fetch_coordinates(true)
206
19
  end
207
20
 
208
21
  ##
209
- # Calculate the distance between two points on Earth (Haversine formula).
210
- # Takes two sets of coordinates and an options hash:
211
- #
212
- # <tt>:units</tt> :: <tt>:mi</tt> (default) or <tt>:km</tt>
22
+ # Set attribute names and include the Geocoder module.
213
23
  #
214
- def self.distance_between(lat1, lon1, lat2, lon2, options = {})
215
-
216
- # set default options
217
- options[:units] ||= :mi
218
-
219
- # define conversion factors
220
- conversions = { :mi => 3956, :km => 6371 }
221
-
222
- # convert degrees to radians
223
- lat1 = to_radians(lat1)
224
- lon1 = to_radians(lon1)
225
- lat2 = to_radians(lat2)
226
- lon2 = to_radians(lon2)
227
-
228
- # compute distances
229
- dlat = (lat1 - lat2).abs
230
- dlon = (lon1 - lon2).abs
231
-
232
- a = (Math.sin(dlat / 2))**2 + Math.cos(lat1) *
233
- (Math.sin(dlon / 2))**2 * Math.cos(lat2)
234
- c = 2 * Math.atan2( Math.sqrt(a), Math.sqrt(1-a))
235
- c * conversions[options[:units]]
24
+ def self.reverse_geocoded_by(latitude_attr, longitude_attr, options = {})
25
+ _geocoder_init(
26
+ :fetched_address => options[:address] || :address,
27
+ :latitude => latitude_attr,
28
+ :longitude => longitude_attr
29
+ )
236
30
  end
237
31
 
238
- ##
239
- # Compute the geographic center (aka geographic midpoint, center of
240
- # gravity) for an array of geocoded objects and/or [lat,lon] arrays
241
- # (can be mixed). Any objects missing coordinates are ignored. Follows
242
- # the procedure documented at http://www.geomidpoint.com/calculation.html.
243
- #
244
- def self.geographic_center(points)
245
-
246
- # convert objects to [lat,lon] arrays and remove nils
247
- points = points.map{ |p|
248
- p.is_a?(Array) ? p : (p.geocoded?? p.read_coordinates : nil)
249
- }.compact
250
-
251
- # convert degrees to radians
252
- points.map!{ |p| [to_radians(p[0]), to_radians(p[1])] }
253
-
254
- # convert to Cartesian coordinates
255
- x = []; y = []; z = []
256
- points.each do |p|
257
- x << Math.cos(p[0]) * Math.cos(p[1])
258
- y << Math.cos(p[0]) * Math.sin(p[1])
259
- z << Math.sin(p[0])
32
+ def self._geocoder_init(options)
33
+ unless _geocoder_initialized?
34
+ class_inheritable_reader :geocoder_options
35
+ class_inheritable_hash_writer :geocoder_options
260
36
  end
261
-
262
- # compute average coordinate values
263
- xa, ya, za = [x,y,z].map do |c|
264
- c.inject(0){ |tot,i| tot += i } / c.size.to_f
37
+ self.geocoder_options = options
38
+ unless _geocoder_initialized?
39
+ include Geocoder::ActiveRecord
265
40
  end
266
-
267
- # convert back to latitude/longitude
268
- lon = Math.atan2(ya, xa)
269
- hyp = Math.sqrt(xa**2 + ya**2)
270
- lat = Math.atan2(za, hyp)
271
-
272
- # return answer in degrees
273
- [to_degrees(lat), to_degrees(lon)]
274
- end
275
-
276
- ##
277
- # Convert degrees to radians.
278
- #
279
- def self.to_radians(degrees)
280
- degrees * (Math::PI / 180)
281
- end
282
-
283
- ##
284
- # Convert radians to degrees.
285
- #
286
- def self.to_degrees(radians)
287
- (radians * 180.0) / Math::PI
288
- end
289
-
290
- ##
291
- # Query Google for geographic information about the given phrase.
292
- # Returns a hash representing a valid geocoder response.
293
- # Returns nil if non-200 HTTP response, timeout, or other error.
294
- #
295
- def self.search(query)
296
- doc = _fetch_parsed_response(query)
297
- doc && doc['status'] == "OK" ? doc : nil
298
41
  end
299
42
 
300
- ##
301
- # Query Google for the coordinates of the given phrase.
302
- # Returns array [lat,lon] if found, nil if not found or if network error.
303
- #
304
- def self.fetch_coordinates(query)
305
- return nil unless doc = self.search(query)
306
- # blindly use the first results (assume they are most accurate)
307
- place = doc['results'].first['geometry']['location']
308
- ['lat', 'lng'].map{ |i| place[i] }
43
+ def self._geocoder_initialized?
44
+ included_modules.include? Geocoder::ActiveRecord
309
45
  end
46
+ end
310
47
 
311
- ##
312
- # Returns a parsed Google geocoder search result (hash).
313
- # This method is not intended for general use (prefer Geocoder.search).
314
- #
315
- def self._fetch_parsed_response(query)
316
- if doc = _fetch_raw_response(query)
317
- ActiveSupport::JSON.decode(doc)
318
- end
319
- end
320
-
321
- ##
322
- # Returns a raw Google geocoder search result (JSON).
323
- # This method is not intended for general use (prefer Geocoder.search).
324
- #
325
- def self._fetch_raw_response(query)
326
- return nil if query.blank?
327
-
328
- # build URL
329
- params = { :address => query, :sensor => "false" }
330
- url = "http://maps.google.com/maps/api/geocode/json?" + params.to_query
331
48
 
332
- # query geocoder and make sure it responds quickly
333
- begin
334
- resp = nil
335
- timeout(3) do
336
- Net::HTTP.get_response(URI.parse(url)).body
337
- end
338
- rescue SocketError, TimeoutError
339
- return nil
340
- end
341
- end
49
+ class GeocoderError < StandardError
342
50
  end
343
51
 
344
- ##
345
- # Add geocoded_by method to ActiveRecord::Base so Geocoder is accessible.
346
- #
347
- ActiveRecord::Base.class_eval do
348
-
349
- ##
350
- # Set attribute names and include the Geocoder module.
351
- #
352
- def self.geocoded_by(method_name = :location, options = {})
353
- class_inheritable_reader :geocoder_options
354
- write_inheritable_attribute :geocoder_options, {
355
- :method_name => method_name,
356
- :latitude => options[:latitude] || :latitude,
357
- :longitude => options[:longitude] || :longitude
358
- }
359
- include Geocoder
360
- end
52
+ class GeocoderConfigurationError < GeocoderError
361
53
  end
@@ -0,0 +1,235 @@
1
+ ##
2
+ # Add geocoding functionality to any ActiveRecord object.
3
+ #
4
+ module Geocoder
5
+ module ActiveRecord
6
+
7
+ ##
8
+ # Implementation of 'included' hook method.
9
+ #
10
+ def self.included(base)
11
+ base.extend ClassMethods
12
+ base.class_eval do
13
+
14
+ # scope: geocoded objects
15
+ scope :geocoded,
16
+ :conditions => "#{geocoder_options[:latitude]} IS NOT NULL " +
17
+ "AND #{geocoder_options[:longitude]} IS NOT NULL"
18
+
19
+ # scope: not-geocoded objects
20
+ scope :not_geocoded,
21
+ :conditions => "#{geocoder_options[:latitude]} IS NULL " +
22
+ "OR #{geocoder_options[:longitude]} IS NULL"
23
+
24
+ ##
25
+ # Find all objects within a radius (in miles) of the given location
26
+ # (address string). Location (the first argument) may be either a string
27
+ # to geocode or an array of coordinates (<tt>[lat,long]</tt>).
28
+ #
29
+ scope :near, lambda{ |location, *args|
30
+ latitude, longitude = location.is_a?(Array) ?
31
+ location : Geocoder::Lookup.coordinates(location)
32
+ if latitude and longitude
33
+ near_scope_options(latitude, longitude, *args)
34
+ else
35
+ {}
36
+ end
37
+ }
38
+ end
39
+ end
40
+
41
+ ##
42
+ # Methods which will be class methods of the including class.
43
+ #
44
+ module ClassMethods
45
+
46
+ ##
47
+ # Get options hash suitable for passing to ActiveRecord.find to get
48
+ # records within a radius (in miles) of the given point.
49
+ # Options hash may include:
50
+ #
51
+ # +units+ :: <tt>:mi</tt> (default) or <tt>:km</tt>
52
+ # +exclude+ :: an object to exclude (used by the #nearbys method)
53
+ # +order+ :: column(s) for ORDER BY SQL clause
54
+ # +limit+ :: number of records to return (for LIMIT SQL clause)
55
+ # +offset+ :: number of records to skip (for OFFSET SQL clause)
56
+ # +select+ :: string with the SELECT SQL fragment (e.g. “id, name”)
57
+ #
58
+ def near_scope_options(latitude, longitude, radius = 20, options = {})
59
+ radius *= Geocoder::Calculations.km_in_mi if options[:units] == :km
60
+ if ::ActiveRecord::Base.connection.adapter_name == "SQLite"
61
+ approx_near_scope_options(latitude, longitude, radius, options)
62
+ else
63
+ full_near_scope_options(latitude, longitude, radius, options)
64
+ end
65
+ end
66
+
67
+
68
+ private # ----------------------------------------------------------------
69
+
70
+ ##
71
+ # Scope options hash for use with a database that supports POWER(),
72
+ # SQRT(), PI(), and trigonometric functions (SIN(), COS(), and ASIN()).
73
+ #
74
+ # Taken from the excellent tutorial at:
75
+ # http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
76
+ #
77
+ def full_near_scope_options(latitude, longitude, radius, options)
78
+ lat_attr = geocoder_options[:latitude]
79
+ lon_attr = geocoder_options[:longitude]
80
+ distance = "3956 * 2 * ASIN(SQRT(" +
81
+ "POWER(SIN((#{latitude} - #{lat_attr}) * " +
82
+ "PI() / 180 / 2), 2) + COS(#{latitude} * PI()/180) * " +
83
+ "COS(#{lat_attr} * PI() / 180) * " +
84
+ "POWER(SIN((#{longitude} - #{lon_attr}) * " +
85
+ "PI() / 180 / 2), 2) ))"
86
+ options[:order] ||= "#{distance} ASC"
87
+ default_near_scope_options(latitude, longitude, radius, options).merge(
88
+ :select => "#{options[:select] || '*'}, #{distance} AS distance",
89
+ :having => "#{distance} <= #{radius}"
90
+ )
91
+ end
92
+
93
+ ##
94
+ # Scope options hash for use with a database without trigonometric
95
+ # functions, like SQLite. Approach is to find objects within a square
96
+ # rather than a circle, so results are very approximate (will include
97
+ # objects outside the given radius).
98
+ #
99
+ def approx_near_scope_options(latitude, longitude, radius, options)
100
+ default_near_scope_options(latitude, longitude, radius, options).merge(
101
+ :select => options[:select] || nil
102
+ )
103
+ end
104
+
105
+ ##
106
+ # Options used for any near-like scope.
107
+ #
108
+ def default_near_scope_options(latitude, longitude, radius, options)
109
+ lat_attr = geocoder_options[:latitude]
110
+ lon_attr = geocoder_options[:longitude]
111
+ conditions = \
112
+ ["#{lat_attr} BETWEEN ? AND ? AND #{lon_attr} BETWEEN ? AND ?"] +
113
+ coordinate_bounds(latitude, longitude, radius)
114
+ if obj = options[:exclude]
115
+ conditions[0] << " AND id != ?"
116
+ conditions << obj.id
117
+ end
118
+ {
119
+ :group => columns.map{ |c| "#{table_name}.#{c.name}" }.join(','),
120
+ :order => options[:order],
121
+ :limit => options[:limit],
122
+ :offset => options[:offset],
123
+ :conditions => conditions
124
+ }
125
+ end
126
+
127
+ ##
128
+ # Get the rough high/low lat/long bounds for a geographic point and
129
+ # radius. Returns an array: <tt>[lat_lo, lat_hi, lon_lo, lon_hi]</tt>.
130
+ # Used to constrain search to a (radius x radius) square.
131
+ #
132
+ def coordinate_bounds(latitude, longitude, radius)
133
+ radius = radius.to_f
134
+ factor = (Math::cos(latitude * Math::PI / 180.0) * 69.0).abs
135
+ [
136
+ latitude - (radius / 69.0),
137
+ latitude + (radius / 69.0),
138
+ longitude - (radius / factor),
139
+ longitude + (radius / factor)
140
+ ]
141
+ end
142
+ end
143
+
144
+ ##
145
+ # Read the coordinates [lat,lon] of an object. This is not great but it
146
+ # seems cleaner than polluting the instance method namespace.
147
+ #
148
+ def read_coordinates
149
+ [:latitude, :longitude].map{ |i| send self.class.geocoder_options[i] }
150
+ end
151
+
152
+ ##
153
+ # Is this object geocoded? (Does it have latitude and longitude?)
154
+ #
155
+ def geocoded?
156
+ read_coordinates.compact.size > 0
157
+ end
158
+
159
+ ##
160
+ # Calculate the distance from the object to a point (lat,lon).
161
+ #
162
+ # <tt>:units</tt> :: <tt>:mi</tt> (default) or <tt>:km</tt>
163
+ #
164
+ def distance_to(lat, lon, units = :mi)
165
+ return nil unless geocoded?
166
+ mylat,mylon = read_coordinates
167
+ Geocoder::Calculations.distance_between(mylat, mylon, lat, lon, :units => units)
168
+ end
169
+
170
+ ##
171
+ # Get other geocoded objects within a given radius.
172
+ #
173
+ # <tt>:units</tt> :: <tt>:mi</tt> (default) or <tt>:km</tt>
174
+ #
175
+ def nearbys(radius = 20, units = :mi)
176
+ return [] unless geocoded?
177
+ options = {:exclude => self, :units => units}
178
+ self.class.near(read_coordinates, radius, options)
179
+ end
180
+
181
+ ##
182
+ # Fetch coordinates and assign +latitude+ and +longitude+. Also returns
183
+ # coordinates as an array: <tt>[lat, lon]</tt>.
184
+ #
185
+ def fetch_coordinates(save = false)
186
+ address_method = self.class.geocoder_options[:user_address]
187
+ unless address_method.is_a? Symbol
188
+ raise GeocoderConfigurationError,
189
+ "You are attempting to fetch coordinates but have not specified " +
190
+ "a method which provides an address for the object."
191
+ end
192
+ coords = Geocoder::Lookup.coordinates(send(address_method))
193
+ unless coords.blank?
194
+ method = (save ? "update" : "write") + "_attribute"
195
+ send method, self.class.geocoder_options[:latitude], coords[0]
196
+ send method, self.class.geocoder_options[:longitude], coords[1]
197
+ end
198
+ coords
199
+ end
200
+
201
+ ##
202
+ # Fetch coordinates and update (save) +latitude+ and +longitude+ data.
203
+ #
204
+ def fetch_coordinates!
205
+ fetch_coordinates(true)
206
+ end
207
+
208
+ ##
209
+ # Fetch address and assign +address+ attribute. Also returns
210
+ # address as a string.
211
+ #
212
+ def fetch_address(save = false)
213
+ lat_attr = self.class.geocoder_options[:latitude]
214
+ lon_attr = self.class.geocoder_options[:longitude]
215
+ unless lat_attr.is_a?(Symbol) and lon_attr.is_a?(Symbol)
216
+ raise GeocoderConfigurationError,
217
+ "You are attempting to fetch an address but have not specified " +
218
+ "attributes which provide coordinates for the object."
219
+ end
220
+ address = Geocoder::Lookup.address(send(lat_attr), send(lon_attr))
221
+ unless address.blank?
222
+ method = (save ? "update" : "write") + "_attribute"
223
+ send method, self.class.geocoder_options[:fetched_address], address
224
+ end
225
+ address
226
+ end
227
+
228
+ ##
229
+ # Fetch address and update (save) +address+ data.
230
+ #
231
+ def fetch_address!
232
+ fetch_address(true)
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,94 @@
1
+ module Geocoder
2
+ module Calculations
3
+ extend self
4
+
5
+ ##
6
+ # Calculate the distance between two points on Earth (Haversine formula).
7
+ # Takes two sets of coordinates and an options hash:
8
+ #
9
+ # <tt>:units</tt> :: <tt>:mi</tt> (default) or <tt>:km</tt>
10
+ #
11
+ def distance_between(lat1, lon1, lat2, lon2, options = {})
12
+
13
+ # set default options
14
+ options[:units] ||= :mi
15
+
16
+ # define conversion factors
17
+ conversions = { :mi => 3956, :km => 6371 }
18
+
19
+ # convert degrees to radians
20
+ lat1 = to_radians(lat1)
21
+ lon1 = to_radians(lon1)
22
+ lat2 = to_radians(lat2)
23
+ lon2 = to_radians(lon2)
24
+
25
+ # compute distances
26
+ dlat = (lat1 - lat2).abs
27
+ dlon = (lon1 - lon2).abs
28
+
29
+ a = (Math.sin(dlat / 2))**2 + Math.cos(lat1) *
30
+ (Math.sin(dlon / 2))**2 * Math.cos(lat2)
31
+ c = 2 * Math.atan2( Math.sqrt(a), Math.sqrt(1-a))
32
+ c * conversions[options[:units]]
33
+ end
34
+
35
+ ##
36
+ # Compute the geographic center (aka geographic midpoint, center of
37
+ # gravity) for an array of geocoded objects and/or [lat,lon] arrays
38
+ # (can be mixed). Any objects missing coordinates are ignored. Follows
39
+ # the procedure documented at http://www.geomidpoint.com/calculation.html.
40
+ #
41
+ def geographic_center(points)
42
+
43
+ # 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
47
+
48
+ # convert degrees to radians
49
+ points.map!{ |p| [to_radians(p[0]), to_radians(p[1])] }
50
+
51
+ # convert to Cartesian coordinates
52
+ x = []; y = []; z = []
53
+ points.each do |p|
54
+ x << Math.cos(p[0]) * Math.cos(p[1])
55
+ y << Math.cos(p[0]) * Math.sin(p[1])
56
+ z << Math.sin(p[0])
57
+ end
58
+
59
+ # compute average coordinate values
60
+ xa, ya, za = [x,y,z].map do |c|
61
+ c.inject(0){ |tot,i| tot += i } / c.size.to_f
62
+ end
63
+
64
+ # convert back to latitude/longitude
65
+ lon = Math.atan2(ya, xa)
66
+ hyp = Math.sqrt(xa**2 + ya**2)
67
+ lat = Math.atan2(za, hyp)
68
+
69
+ # return answer in degrees
70
+ [to_degrees(lat), to_degrees(lon)]
71
+ end
72
+
73
+ ##
74
+ # Convert degrees to radians.
75
+ #
76
+ def to_radians(degrees)
77
+ degrees * (Math::PI / 180)
78
+ end
79
+
80
+ ##
81
+ # Convert radians to degrees.
82
+ #
83
+ def to_degrees(radians)
84
+ (radians * 180.0) / Math::PI
85
+ end
86
+
87
+ ##
88
+ # Conversion factor: km to mi.
89
+ #
90
+ def km_in_mi
91
+ 0.621371192
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,78 @@
1
+ require 'net/http'
2
+
3
+ module Geocoder
4
+ module Lookup
5
+ extend self
6
+
7
+ ##
8
+ # Query Google for the coordinates of the given address.
9
+ # Returns array [lat,lon] if found, nil if not found or if network error.
10
+ #
11
+ def coordinates(address)
12
+ return nil if address.blank?
13
+ return nil unless doc = search(address, false)
14
+ # blindly use first result (assume it is most accurate)
15
+ place = doc['results'].first['geometry']['location']
16
+ ['lat', 'lng'].map{ |i| place[i] }
17
+ end
18
+
19
+ ##
20
+ # Query Google for the address of the given coordinates.
21
+ # Returns string if found, nil if not found or if network error.
22
+ #
23
+ def address(latitude, longitude)
24
+ return nil if latitude.blank? || longitude.blank?
25
+ return nil unless doc = search("#{latitude},#{longitude}", true)
26
+ # blindly use first result (assume it is most accurate)
27
+ doc['results'].first['formatted_address']
28
+ end
29
+
30
+
31
+ private # ---------------------------------------------------------------
32
+
33
+ ##
34
+ # Query Google for geographic information about the given phrase.
35
+ # Returns a hash representing a valid geocoder response.
36
+ # Returns nil if non-200 HTTP response, timeout, or other error.
37
+ #
38
+ def search(query, reverse = false)
39
+ doc = fetch_parsed_response(query, reverse)
40
+ doc && doc['status'] == "OK" ? doc : nil
41
+ end
42
+
43
+ ##
44
+ # Returns a parsed Google geocoder search result (hash).
45
+ # This method is not intended for general use (prefer Geocoder.search).
46
+ #
47
+ def fetch_parsed_response(query, reverse = false)
48
+ if doc = fetch_raw_response(query, reverse)
49
+ ActiveSupport::JSON.decode(doc)
50
+ end
51
+ end
52
+
53
+ ##
54
+ # Returns a raw Google geocoder search result (JSON).
55
+ # This method is not intended for general use (prefer Geocoder.search).
56
+ #
57
+ def fetch_raw_response(query, reverse = false)
58
+ return nil if query.blank?
59
+
60
+ # name parameter based on forward/reverse geocoding
61
+ param = reverse ? :latlng : :address
62
+
63
+ # build URL
64
+ params = { param => query, :sensor => "false" }
65
+ url = "http://maps.google.com/maps/api/geocode/json?" + params.to_query
66
+
67
+ # query geocoder and make sure it responds quickly
68
+ begin
69
+ resp = nil
70
+ timeout(3) do
71
+ Net::HTTP.get_response(URI.parse(url)).body
72
+ end
73
+ rescue SocketError, TimeoutError
74
+ return nil
75
+ end
76
+ end
77
+ end
78
+ end
@@ -10,14 +10,21 @@ class GeocoderTest < Test::Unit::TestCase
10
10
 
11
11
  # sanity check
12
12
  def test_distance_between
13
- assert_equal 69, Geocoder.distance_between(0,0, 0,1).round
13
+ assert_equal 69, Geocoder::Calculations.distance_between(0,0, 0,1).round
14
14
  end
15
15
 
16
16
  # sanity check
17
17
  def test_geographic_center
18
18
  assert_equal [0.0, 0.5],
19
- Geocoder.geographic_center([[0,0], [0,1]])
19
+ Geocoder::Calculations.geographic_center([[0,0], [0,1]])
20
20
  assert_equal [0.0, 1.0],
21
- Geocoder.geographic_center([[0,0], [0,1], [0,2]])
21
+ Geocoder::Calculations.geographic_center([[0,0], [0,1], [0,2]])
22
+ end
23
+
24
+ def test_exception_raised_for_unconfigured_geocoding
25
+ l = Landmark.new("Mount Rushmore", 43.88, -103.46)
26
+ assert_raises GeocoderConfigurationError do
27
+ l.fetch_coordinates
28
+ end
22
29
  end
23
30
  end
data/test/test_helper.rb CHANGED
@@ -35,11 +35,13 @@ end
35
35
  require 'geocoder'
36
36
 
37
37
  ##
38
- # Mock HTTP request to Google.
38
+ # Mock HTTP request to geocoding service.
39
39
  #
40
40
  module Geocoder
41
- def self._fetch_raw_response(query)
42
- File.read(File.join("test", "fixtures", "madison_square_garden.json"))
41
+ module Lookup
42
+ def self.fetch_raw_response(query, reverse = false)
43
+ File.read(File.join("test", "fixtures", "madison_square_garden.json"))
44
+ end
43
45
  end
44
46
  end
45
47
 
@@ -63,6 +65,27 @@ class Venue < ActiveRecord::Base
63
65
  end
64
66
  end
65
67
 
68
+ ##
69
+ # Reverse geocoded model.
70
+ #
71
+ class Landmark < ActiveRecord::Base
72
+ reverse_geocoded_by :latitude, :longitude
73
+
74
+ def initialize(name, latitude, longitude)
75
+ super()
76
+ write_attribute :name, name
77
+ write_attribute :latitude, latitude
78
+ write_attribute :longitude, longitude
79
+ end
80
+
81
+ ##
82
+ # If method not found, assume it's an ActiveRecord attribute reader.
83
+ #
84
+ def method_missing(name, *args, &block)
85
+ @attributes[name]
86
+ end
87
+ end
88
+
66
89
  class Test::Unit::TestCase
67
90
  def venue_params(abbrev)
68
91
  {
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-geocoder
3
3
  version: !ruby/object:Gem::Version
4
- hash: 55
4
+ hash: 53
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 9
9
- - 6
10
- version: 0.9.6
9
+ - 7
10
+ version: 0.9.7
11
11
  platform: ruby
12
12
  authors:
13
13
  - Alex Reisner
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-01-18 00:00:00 -05:00
18
+ date: 2011-02-01 00:00:00 -05:00
19
19
  default_executable:
20
20
  dependencies: []
21
21
 
@@ -30,6 +30,9 @@ extra_rdoc_files: []
30
30
 
31
31
  files:
32
32
  - lib/geocoder.rb
33
+ - lib/geocoder/lookup.rb
34
+ - lib/geocoder/calculations.rb
35
+ - lib/geocoder/active_record.rb
33
36
  - test/test_helper.rb
34
37
  - test/geocoder_test.rb
35
38
  - CHANGELOG.rdoc