geokit-rails 1.1.4 → 2.0.0.rc1

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

Potentially problematic release.


This version of geokit-rails might be problematic. Click here for more details.

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