geokit-rails3 0.0.3 → 0.0.4

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,437 @@
1
+ require 'active_record'
2
+ require 'active_support/concern'
3
+
4
+ module Geokit
5
+ # Contains the class method acts_as_mappable targeted to be mixed into ActiveRecord.
6
+ # When mixed in, augments find services such that they provide distance calculation
7
+ # query services. The find method accepts additional options:
8
+ #
9
+ # * :origin - can be
10
+ # 1. a two-element array of latititude/longitude -- :origin=>[37.792,-122.393]
11
+ # 2. a geocodeable string -- :origin=>'100 Spear st, San Francisco, CA'
12
+ # 3. an object which responds to lat and lng methods, or latitude and longitude methods,
13
+ # or whatever methods you have specified for lng_column_name and lat_column_name
14
+ #
15
+ # Other finder methods are provided for specific queries. These are:
16
+ #
17
+ # * find_within (alias: find_inside)
18
+ # * find_beyond (alias: find_outside)
19
+ # * find_closest (alias: find_nearest)
20
+ # * find_farthest
21
+ #
22
+ # Counter methods are available and work similarly to finders.
23
+ #
24
+ # If raw SQL is desired, the distance_sql method can be used to obtain SQL appropriate
25
+ # to use in a find_by_sql call.
26
+ module ActsAsMappable
27
+
28
+ class UnsupportedAdapter < StandardError ; end
29
+
30
+ # Add the +acts_as_mappable+ method into ActiveRecord subclasses
31
+ module Glue # :nodoc:
32
+ extend ActiveSupport::Concern
33
+
34
+ module ClassMethods # :nodoc:
35
+ def acts_as_mappable(options = {})
36
+ metaclass = (class << self; self; end)
37
+
38
+ self.send :include, Geokit::ActsAsMappable
39
+
40
+ cattr_accessor :through
41
+ self.through = options[:through]
42
+
43
+ if reflection = Geokit::ActsAsMappable.end_of_reflection_chain(self.through, self)
44
+ metaclass.instance_eval do
45
+ [ :distance_column_name, :default_units, :default_formula, :lat_column_name, :lng_column_name, :qualified_lat_column_name, :qualified_lng_column_name ].each do |method_name|
46
+ define_method method_name do
47
+ reflection.klass.send(method_name)
48
+ end
49
+ end
50
+ end
51
+ else
52
+ cattr_accessor :distance_column_name, :default_units, :default_formula, :lat_column_name, :lng_column_name, :qualified_lat_column_name, :qualified_lng_column_name
53
+
54
+ self.distance_column_name = options[:distance_column_name] || 'distance'
55
+ self.default_units = options[:default_units] || Geokit::default_units
56
+ self.default_formula = options[:default_formula] || Geokit::default_formula
57
+ self.lat_column_name = options[:lat_column_name] || 'lat'
58
+ self.lng_column_name = options[:lng_column_name] || 'lng'
59
+ self.qualified_lat_column_name = "#{table_name}.#{lat_column_name}"
60
+ self.qualified_lng_column_name = "#{table_name}.#{lng_column_name}"
61
+
62
+ if options.include?(:auto_geocode) && options[:auto_geocode]
63
+ # if the form auto_geocode=>true is used, let the defaults take over by suppling an empty hash
64
+ options[:auto_geocode] = {} if options[:auto_geocode] == true
65
+ cattr_accessor :auto_geocode_field, :auto_geocode_error_message
66
+ self.auto_geocode_field = options[:auto_geocode][:field] || 'address'
67
+ self.auto_geocode_error_message = options[:auto_geocode][:error_message] || 'could not locate address'
68
+
69
+ # set the actual callback here
70
+ before_validation :auto_geocode_address, :on => :create
71
+ end
72
+
73
+ scope :nearest, lambda { |origin|
74
+ limit(1)
75
+ }
76
+ end
77
+ end
78
+ end
79
+ end # Glue
80
+
81
+ extend ActiveSupport::Concern
82
+
83
+ included do
84
+ include Geokit::Mappable
85
+ end
86
+
87
+ # Class methods included in models when +acts_as_mappable+ is called
88
+ module ClassMethods
89
+
90
+ # A proxy to an instance of a finder adapter, inferred from the connection's adapter.
91
+ def adapter
92
+ @adapter ||= begin
93
+ require File.join(File.dirname(__FILE__), 'adapters', connection.adapter_name.downcase)
94
+ klass = Adapters.const_get(connection.adapter_name.camelcase)
95
+ klass.load(self) unless klass.loaded
96
+ klass.new(self)
97
+ rescue LoadError
98
+ raise UnsupportedAdapter, "`#{connection.adapter_name.downcase}` is not a supported adapter."
99
+ end
100
+ end
101
+
102
+ # Extends the existing find method in potentially two ways:
103
+ # - If a mappable instance exists in the options, adds a distance column.
104
+ # - If a mappable instance exists in the options and the distance column exists in the
105
+ # conditions, substitutes the distance sql for the distance column -- this saves
106
+ # having to write the gory SQL.
107
+ def find(*args)
108
+ prepare_for_find_or_count(:find, args)
109
+ super(*args)
110
+ end
111
+
112
+ # Extends the existing count method by:
113
+ # - If a mappable instance exists in the options and the distance column exists in the
114
+ # conditions, substitutes the distance sql for the distance column -- this saves
115
+ # having to write the gory SQL.
116
+ def count(*args)
117
+ prepare_for_find_or_count(:count, args)
118
+ super(*args)
119
+ end
120
+
121
+ # Finds within a distance radius.
122
+ def find_within(distance, options={})
123
+ options[:within] = distance
124
+ find(:all, options)
125
+ end
126
+ alias find_inside find_within
127
+
128
+ # Finds beyond a distance radius.
129
+ def find_beyond(distance, options={})
130
+ options[:beyond] = distance
131
+ find(:all, options)
132
+ end
133
+ alias find_outside find_beyond
134
+
135
+ # Finds according to a range. Accepts inclusive or exclusive ranges.
136
+ def find_by_range(range, options={})
137
+ options[:range] = range
138
+ find(:all, options)
139
+ end
140
+
141
+ # Finds the closest to the origin.
142
+ def find_closest(options={})
143
+ find(:nearest, options)
144
+ end
145
+ alias find_nearest find_closest
146
+
147
+ # Finds the farthest from the origin.
148
+ def find_farthest(options={})
149
+ find(:farthest, options)
150
+ end
151
+
152
+ # Finds within rectangular bounds (sw,ne).
153
+ def find_within_bounds(bounds, options={})
154
+ options[:bounds] = bounds
155
+ find(:all, options)
156
+ end
157
+
158
+ # counts within a distance radius.
159
+ def count_within(distance, options={})
160
+ options[:within] = distance
161
+ count(options)
162
+ end
163
+ alias count_inside count_within
164
+
165
+ # Counts beyond a distance radius.
166
+ def count_beyond(distance, options={})
167
+ options[:beyond] = distance
168
+ count(options)
169
+ end
170
+ alias count_outside count_beyond
171
+
172
+ # Counts according to a range. Accepts inclusive or exclusive ranges.
173
+ def count_by_range(range, options={})
174
+ options[:range] = range
175
+ count(options)
176
+ end
177
+
178
+ # Finds within rectangular bounds (sw,ne).
179
+ def count_within_bounds(bounds, options={})
180
+ options[:bounds] = bounds
181
+ count(options)
182
+ end
183
+
184
+ # Returns the distance calculation to be used as a display column or a condition. This
185
+ # is provide for anyone wanting access to the raw SQL.
186
+ def distance_sql(origin, units=default_units, formula=default_formula)
187
+ case formula
188
+ when :sphere
189
+ sql = sphere_distance_sql(origin, units)
190
+ when :flat
191
+ sql = flat_distance_sql(origin, units)
192
+ end
193
+ sql
194
+ end
195
+
196
+ private
197
+
198
+ # Prepares either a find or a count action by parsing through the options and
199
+ # conditionally adding to the select clause for finders.
200
+ def prepare_for_find_or_count(action, args)
201
+ options = args.extract_options!
202
+ #options = defined?(args.extract_options!) ? args.extract_options! : extract_options_from_args!(args)
203
+ # Obtain items affecting distance condition.
204
+ origin = extract_origin_from_options(options)
205
+ units = extract_units_from_options(options)
206
+ formula = extract_formula_from_options(options)
207
+ bounds = extract_bounds_from_options(options)
208
+
209
+ # Only proceed if this is a geokit-related query
210
+ if origin || bounds
211
+ # if no explicit bounds were given, try formulating them from the point and distance given
212
+ bounds = formulate_bounds_from_distance(options, origin, units) unless bounds
213
+ # Apply select adjustments based upon action.
214
+ add_distance_to_select(options, origin, units, formula) if origin && action == :find
215
+ # Apply the conditions for a bounding rectangle if applicable
216
+ apply_bounds_conditions(options,bounds) if bounds
217
+ # Apply distance scoping and perform substitutions.
218
+ apply_distance_scope(options)
219
+ substitute_distance_in_conditions(options, origin, units, formula) if origin && options.has_key?(:conditions)
220
+ # Order by scoping for find action.
221
+ apply_find_scope(args, options) if action == :find
222
+ # Handle :through
223
+ apply_include_for_through(options)
224
+ # Unfortunatley, we need to do extra work if you use an :include. See the method for more info.
225
+ handle_order_with_include(options,origin,units,formula) if options.include?(:include) && options.include?(:order) && origin
226
+ end
227
+
228
+ # Restore options minus the extra options that we used for the
229
+ # Geokit API.
230
+ args.push(options)
231
+ end
232
+
233
+ def apply_include_for_through(options)
234
+ if self.through
235
+ case options[:include]
236
+ when Array
237
+ options[:include] << self.through
238
+ when Hash, String, Symbol
239
+ options[:include] = [ self.through, options[:include] ]
240
+ else
241
+ options[:include] = [ self.through ]
242
+ end
243
+ end
244
+ end
245
+
246
+ # If we're here, it means that 1) an origin argument, 2) an :include, 3) an :order clause were supplied.
247
+ # Now we have to sub some SQL into the :order clause. The reason is that when you do an :include,
248
+ # ActiveRecord drops the psuedo-column (specificically, distance) which we supplied for :select.
249
+ # So, the 'distance' column isn't available for the :order clause to reference when we use :include.
250
+ def handle_order_with_include(options, origin, units, formula)
251
+ # replace the distance_column_name with the distance sql in order clause
252
+ options[:order].sub!(distance_column_name, distance_sql(origin, units, formula))
253
+ end
254
+
255
+ # Looks for mapping-specific tokens and makes appropriate translations so that the
256
+ # original finder has its expected arguments. Resets the the scope argument to
257
+ # :first and ensures the limit is set to one.
258
+ def apply_find_scope(args, options)
259
+ case args.first
260
+ when :nearest, :closest
261
+ args[0] = :first
262
+ options[:limit] = 1
263
+ options[:order] = "#{distance_column_name} ASC"
264
+ when :farthest
265
+ args[0] = :first
266
+ options[:limit] = 1
267
+ options[:order] = "#{distance_column_name} DESC"
268
+ end
269
+ end
270
+
271
+ # If it's a :within query, add a bounding box to improve performance.
272
+ # This only gets called if a :bounds argument is not otherwise supplied.
273
+ def formulate_bounds_from_distance(options, origin, units)
274
+ distance = options[:within] if options.has_key?(:within)
275
+ distance = options[:range].last-(options[:range].exclude_end?? 1 : 0) if options.has_key?(:range)
276
+ if distance
277
+ res=Geokit::Bounds.from_point_and_radius(origin,distance,:units=>units)
278
+ else
279
+ nil
280
+ end
281
+ end
282
+
283
+ # Replace :within, :beyond and :range distance tokens with the appropriate distance
284
+ # where clauses. Removes these tokens from the options hash.
285
+ def apply_distance_scope(options)
286
+ distance_condition = if options.has_key?(:within)
287
+ "#{distance_column_name} <= #{options[:within]}"
288
+ elsif options.has_key?(:beyond)
289
+ "#{distance_column_name} > #{options[:beyond]}"
290
+ elsif options.has_key?(:range)
291
+ "#{distance_column_name} >= #{options[:range].first} AND #{distance_column_name} <#{'=' unless options[:range].exclude_end?} #{options[:range].last}"
292
+ end
293
+
294
+ if distance_condition
295
+ [:within, :beyond, :range].each { |option| options.delete(option) }
296
+ options[:conditions] = merge_conditions(options[:conditions], distance_condition)
297
+ end
298
+ end
299
+
300
+ # Alters the conditions to include rectangular bounds conditions.
301
+ def apply_bounds_conditions(options,bounds)
302
+ sw,ne = bounds.sw, bounds.ne
303
+ lng_sql = bounds.crosses_meridian? ? "(#{qualified_lng_column_name}<#{ne.lng} OR #{qualified_lng_column_name}>#{sw.lng})" : "#{qualified_lng_column_name}>#{sw.lng} AND #{qualified_lng_column_name}<#{ne.lng}"
304
+ bounds_sql = "#{qualified_lat_column_name}>#{sw.lat} AND #{qualified_lat_column_name}<#{ne.lat} AND #{lng_sql}"
305
+ options[:conditions] = merge_conditions(options[:conditions], bounds_sql)
306
+ end
307
+
308
+ # Extracts the origin instance out of the options if it exists and returns
309
+ # it. If there is no origin, looks for latitude and longitude values to
310
+ # create an origin. The side-effect of the method is to remove these
311
+ # option keys from the hash.
312
+ def extract_origin_from_options(options)
313
+ origin = options.delete(:origin)
314
+ res = normalize_point_to_lat_lng(origin) if origin
315
+ res
316
+ end
317
+
318
+ # Extract the units out of the options if it exists and returns it. If
319
+ # there is no :units key, it uses the default. The side effect of the
320
+ # method is to remove the :units key from the options hash.
321
+ def extract_units_from_options(options)
322
+ units = options[:units] || default_units
323
+ options.delete(:units)
324
+ units
325
+ end
326
+
327
+ # Extract the formula out of the options if it exists and returns it. If
328
+ # there is no :formula key, it uses the default. The side effect of the
329
+ # method is to remove the :formula key from the options hash.
330
+ def extract_formula_from_options(options)
331
+ formula = options[:formula] || default_formula
332
+ options.delete(:formula)
333
+ formula
334
+ end
335
+
336
+ def extract_bounds_from_options(options)
337
+ bounds = options.delete(:bounds)
338
+ bounds = Geokit::Bounds.normalize(bounds) if bounds
339
+ end
340
+
341
+ # Geocode IP address.
342
+ def geocode_ip_address(origin)
343
+ geo_location = Geokit::Geocoders::MultiGeocoder.geocode(origin)
344
+ return geo_location if geo_location.success
345
+ raise Geokit::Geocoders::GeocodeError
346
+ end
347
+
348
+ # Given a point in a variety of (an address to geocode,
349
+ # an array of [lat,lng], or an object with appropriate lat/lng methods, an IP addres)
350
+ # this method will normalize it into a Geokit::LatLng instance. The only thing this
351
+ # method adds on top of LatLng#normalize is handling of IP addresses
352
+ def normalize_point_to_lat_lng(point)
353
+ res = geocode_ip_address(point) if point.is_a?(String) && /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(point)
354
+ res = Geokit::LatLng.normalize(point) unless res
355
+ res
356
+ end
357
+
358
+ # Augments the select with the distance SQL.
359
+ def add_distance_to_select(options, origin, units=default_units, formula=default_formula)
360
+ if origin
361
+ distance_selector = distance_sql(origin, units, formula) + " AS #{distance_column_name}"
362
+ selector = options.has_key?(:select) && options[:select] ? options[:select] : "*"
363
+ options[:select] = "#{selector}, #{distance_selector}"
364
+ end
365
+ end
366
+
367
+ # Looks for the distance column and replaces it with the distance sql. If an origin was not
368
+ # passed in and the distance column exists, we leave it to be flagged as bad SQL by the database.
369
+ # Conditions are either a string or an array. In the case of an array, the first entry contains
370
+ # the condition.
371
+ def substitute_distance_in_conditions(options, origin, units=default_units, formula=default_formula)
372
+ condition = options[:conditions].is_a?(String) ? options[:conditions] : options[:conditions].first
373
+ pattern = Regexp.new("\\b#{distance_column_name}\\b")
374
+ condition.gsub!(pattern, distance_sql(origin, units, formula))
375
+ end
376
+
377
+ # Returns the distance SQL using the spherical world formula (Haversine). The SQL is tuned
378
+ # to the database in use.
379
+ def sphere_distance_sql(origin, units)
380
+ lat = deg2rad(origin.lat)
381
+ lng = deg2rad(origin.lng)
382
+ multiplier = units_sphere_multiplier(units)
383
+
384
+ adapter.sphere_distance_sql(lat, lng, multiplier) if adapter
385
+ end
386
+
387
+ # Returns the distance SQL using the flat-world formula (Phythagorean Theory). The SQL is tuned
388
+ # to the database in use.
389
+ def flat_distance_sql(origin, units)
390
+ lat_degree_units = units_per_latitude_degree(units)
391
+ lng_degree_units = units_per_longitude_degree(origin.lat, units)
392
+
393
+ adapter.flat_distance_sql(origin, lat_degree_units, lng_degree_units)
394
+ end
395
+
396
+ end # ClassMethods
397
+
398
+ # this is the callback for auto_geocoding
399
+ def auto_geocode_address
400
+ address=self.send(auto_geocode_field).to_s
401
+ geo=Geokit::Geocoders::MultiGeocoder.geocode(address)
402
+
403
+ if geo.success
404
+ self.send("#{lat_column_name}=", geo.lat)
405
+ self.send("#{lng_column_name}=", geo.lng)
406
+ else
407
+ errors.add(auto_geocode_field, auto_geocode_error_message)
408
+ end
409
+
410
+ geo.success
411
+ end
412
+
413
+ def self.end_of_reflection_chain(through, klass)
414
+ while through
415
+ reflection = nil
416
+ if through.is_a?(Hash)
417
+ association, through = through.to_a.first
418
+ else
419
+ association, through = through, nil
420
+ end
421
+
422
+ if reflection = klass.reflect_on_association(association)
423
+ klass = reflection.klass
424
+ else
425
+ raise ArgumentError, "You gave #{association} in :through, but I could not find it on #{klass}."
426
+ end
427
+ end
428
+
429
+ reflection
430
+ end
431
+
432
+ end # ActsAsMappable
433
+ end # Geokit
434
+
435
+
436
+
437
+ # ActiveRecord::Base.extend Geokit::ActsAsMappable
@@ -2,27 +2,6 @@ require 'active_record'
2
2
  require 'active_support/concern'
3
3
 
4
4
  module Geokit
5
- # Contains the class method acts_as_mappable targeted to be mixed into ActiveRecord.
6
- # When mixed in, augments find services such that they provide distance calculation
7
- # query services. The find method accepts additional options:
8
- #
9
- # * :origin - can be
10
- # 1. a two-element array of latititude/longitude -- :origin=>[37.792,-122.393]
11
- # 2. a geocodeable string -- :origin=>'100 Spear st, San Francisco, CA'
12
- # 3. an object which responds to lat and lng methods, or latitude and longitude methods,
13
- # or whatever methods you have specified for lng_column_name and lat_column_name
14
- #
15
- # Other finder methods are provided for specific queries. These are:
16
- #
17
- # * find_within (alias: find_inside)
18
- # * find_beyond (alias: find_outside)
19
- # * find_closest (alias: find_nearest)
20
- # * find_farthest
21
- #
22
- # Counter methods are available and work similarly to finders.
23
- #
24
- # If raw SQL is desired, the distance_sql method can be used to obtain SQL appropriate
25
- # to use in a find_by_sql call.
26
5
  module ActsAsMappable
27
6
 
28
7
  class UnsupportedAdapter < StandardError ; end
@@ -69,6 +48,38 @@ module Geokit
69
48
  # set the actual callback here
70
49
  before_validation :auto_geocode_address, :on => :create
71
50
  end
51
+
52
+ scope :select_with_distance, lambda { |origin, units, formula|
53
+ distance_formula = distance_sql(origin, units, formula)
54
+
55
+ select('*').
56
+ select("#{distance_formula} AS #{distance_column_name}")
57
+ }
58
+
59
+ scope :within, lambda { |options|
60
+ origin = extract_origin_from_options(options)
61
+ units = extract_units_from_options(options)
62
+ formula = extract_formula_from_options(options)
63
+
64
+ distance_formula = distance_sql(origin, units, formula)
65
+
66
+ within = options.delete(:within)
67
+
68
+ select_with_distance(origin, units, formula).
69
+ where("#{distance_formula} < #{within}")
70
+ }
71
+
72
+ scope :closest, lambda { |options|
73
+ origin = extract_origin_from_options(options)
74
+ units = extract_units_from_options(options)
75
+ formula = extract_formula_from_options(options)
76
+
77
+ distance_formula = distance_sql(origin, units, formula)
78
+
79
+ select_with_distance(origin, units, formula).
80
+ order("#{distance_formula} ASC").
81
+ limit(1)
82
+ }
72
83
  end
73
84
  end
74
85
  end
@@ -95,88 +106,6 @@ module Geokit
95
106
  end
96
107
  end
97
108
 
98
- # Extends the existing find method in potentially two ways:
99
- # - If a mappable instance exists in the options, adds a distance column.
100
- # - If a mappable instance exists in the options and the distance column exists in the
101
- # conditions, substitutes the distance sql for the distance column -- this saves
102
- # having to write the gory SQL.
103
- def find(*args)
104
- prepare_for_find_or_count(:find, args)
105
- super(*args)
106
- end
107
-
108
- # Extends the existing count method by:
109
- # - If a mappable instance exists in the options and the distance column exists in the
110
- # conditions, substitutes the distance sql for the distance column -- this saves
111
- # having to write the gory SQL.
112
- def count(*args)
113
- prepare_for_find_or_count(:count, args)
114
- super(*args)
115
- end
116
-
117
- # Finds within a distance radius.
118
- def find_within(distance, options={})
119
- options[:within] = distance
120
- find(:all, options)
121
- end
122
- alias find_inside find_within
123
-
124
- # Finds beyond a distance radius.
125
- def find_beyond(distance, options={})
126
- options[:beyond] = distance
127
- find(:all, options)
128
- end
129
- alias find_outside find_beyond
130
-
131
- # Finds according to a range. Accepts inclusive or exclusive ranges.
132
- def find_by_range(range, options={})
133
- options[:range] = range
134
- find(:all, options)
135
- end
136
-
137
- # Finds the closest to the origin.
138
- def find_closest(options={})
139
- find(:nearest, options)
140
- end
141
- alias find_nearest find_closest
142
-
143
- # Finds the farthest from the origin.
144
- def find_farthest(options={})
145
- find(:farthest, options)
146
- end
147
-
148
- # Finds within rectangular bounds (sw,ne).
149
- def find_within_bounds(bounds, options={})
150
- options[:bounds] = bounds
151
- find(:all, options)
152
- end
153
-
154
- # counts within a distance radius.
155
- def count_within(distance, options={})
156
- options[:within] = distance
157
- count(options)
158
- end
159
- alias count_inside count_within
160
-
161
- # Counts beyond a distance radius.
162
- def count_beyond(distance, options={})
163
- options[:beyond] = distance
164
- count(options)
165
- end
166
- alias count_outside count_beyond
167
-
168
- # Counts according to a range. Accepts inclusive or exclusive ranges.
169
- def count_by_range(range, options={})
170
- options[:range] = range
171
- count(options)
172
- end
173
-
174
- # Finds within rectangular bounds (sw,ne).
175
- def count_within_bounds(bounds, options={})
176
- options[:bounds] = bounds
177
- count(options)
178
- end
179
-
180
109
  # Returns the distance calculation to be used as a display column or a condition. This
181
110
  # is provide for anyone wanting access to the raw SQL.
182
111
  def distance_sql(origin, units=default_units, formula=default_formula)
@@ -191,79 +120,6 @@ module Geokit
191
120
 
192
121
  private
193
122
 
194
- # Prepares either a find or a count action by parsing through the options and
195
- # conditionally adding to the select clause for finders.
196
- def prepare_for_find_or_count(action, args)
197
- options = args.extract_options!
198
- #options = defined?(args.extract_options!) ? args.extract_options! : extract_options_from_args!(args)
199
- # Obtain items affecting distance condition.
200
- origin = extract_origin_from_options(options)
201
- units = extract_units_from_options(options)
202
- formula = extract_formula_from_options(options)
203
- bounds = extract_bounds_from_options(options)
204
-
205
- # Only proceed if this is a geokit-related query
206
- if origin || bounds
207
- # if no explicit bounds were given, try formulating them from the point and distance given
208
- bounds = formulate_bounds_from_distance(options, origin, units) unless bounds
209
- # Apply select adjustments based upon action.
210
- add_distance_to_select(options, origin, units, formula) if origin && action == :find
211
- # Apply the conditions for a bounding rectangle if applicable
212
- apply_bounds_conditions(options,bounds) if bounds
213
- # Apply distance scoping and perform substitutions.
214
- apply_distance_scope(options)
215
- substitute_distance_in_conditions(options, origin, units, formula) if origin && options.has_key?(:conditions)
216
- # Order by scoping for find action.
217
- apply_find_scope(args, options) if action == :find
218
- # Handle :through
219
- apply_include_for_through(options)
220
- # Unfortunatley, we need to do extra work if you use an :include. See the method for more info.
221
- handle_order_with_include(options,origin,units,formula) if options.include?(:include) && options.include?(:order) && origin
222
- end
223
-
224
- # Restore options minus the extra options that we used for the
225
- # Geokit API.
226
- args.push(options)
227
- end
228
-
229
- def apply_include_for_through(options)
230
- if self.through
231
- case options[:include]
232
- when Array
233
- options[:include] << self.through
234
- when Hash, String, Symbol
235
- options[:include] = [ self.through, options[:include] ]
236
- else
237
- options[:include] = [ self.through ]
238
- end
239
- end
240
- end
241
-
242
- # If we're here, it means that 1) an origin argument, 2) an :include, 3) an :order clause were supplied.
243
- # Now we have to sub some SQL into the :order clause. The reason is that when you do an :include,
244
- # ActiveRecord drops the psuedo-column (specificically, distance) which we supplied for :select.
245
- # So, the 'distance' column isn't available for the :order clause to reference when we use :include.
246
- def handle_order_with_include(options, origin, units, formula)
247
- # replace the distance_column_name with the distance sql in order clause
248
- options[:order].sub!(distance_column_name, distance_sql(origin, units, formula))
249
- end
250
-
251
- # Looks for mapping-specific tokens and makes appropriate translations so that the
252
- # original finder has its expected arguments. Resets the the scope argument to
253
- # :first and ensures the limit is set to one.
254
- def apply_find_scope(args, options)
255
- case args.first
256
- when :nearest, :closest
257
- args[0] = :first
258
- options[:limit] = 1
259
- options[:order] = "#{distance_column_name} ASC"
260
- when :farthest
261
- args[0] = :first
262
- options[:limit] = 1
263
- options[:order] = "#{distance_column_name} DESC"
264
- end
265
- end
266
-
267
123
  # If it's a :within query, add a bounding box to improve performance.
268
124
  # This only gets called if a :bounds argument is not otherwise supplied.
269
125
  def formulate_bounds_from_distance(options, origin, units)
@@ -0,0 +1,22 @@
1
+ module Geokit
2
+ module Adapters
3
+ class Mysql2 < Abstract
4
+
5
+ def sphere_distance_sql(lat, lng, multiplier)
6
+ %|
7
+ (ACOS(least(1,COS(#{lat})*COS(#{lng})*COS(RADIANS(#{qualified_lat_column_name}))*COS(RADIANS(#{qualified_lng_column_name}))+
8
+ COS(#{lat})*SIN(#{lng})*COS(RADIANS(#{qualified_lat_column_name}))*SIN(RADIANS(#{qualified_lng_column_name}))+
9
+ SIN(#{lat})*SIN(RADIANS(#{qualified_lat_column_name}))))*#{multiplier})
10
+ |
11
+ end
12
+
13
+ def flat_distance_sql(origin, lat_degree_units, lng_degree_units)
14
+ %|
15
+ SQRT(POW(#{lat_degree_units}*(#{origin.lat}-#{qualified_lat_column_name}),2)+
16
+ POW(#{lng_degree_units}*(#{origin.lng}-#{qualified_lng_column_name}),2))
17
+ |
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -1,3 +1,3 @@
1
1
  module GeokitRails3
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.4"
3
3
  end