albanpeignier-geokit-rails 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,58 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the GeoKit plugin.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.pattern = 'test/**/*_test.rb'
12
+ t.verbose = true
13
+ end
14
+
15
+ desc 'Generate documentation for the GeoKit plugin.'
16
+ Rake::RDocTask.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'GeoKit'
19
+ rdoc.options << '--line-numbers' << '--inline-source'
20
+ rdoc.rdoc_files.include('README')
21
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
+ end
23
+
24
+
25
+ require 'yaml'
26
+ about = YAML.load(IO.read('about.yml'))
27
+
28
+ %w[rubygems hoe].each { |f| require f }
29
+ # Generate all the Rake tasks
30
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
31
+ $hoe = Hoe.new('geokit-rails', about['version']) do |p|
32
+ p.developer("Andre Lewis", "andre@earthcode.com")
33
+ p.developer("Bill Eisenhauer", "bill@billeisenhauer.com")
34
+ p.changes = p.paragraphs_of("CHANGELOG.rdoc", 0..1).join("\n\n")
35
+ p.summary = about['summary']
36
+
37
+ p.rubyforge_name = p.name # TODO this is default value
38
+ p.extra_deps = [
39
+ ['geokit', '>=1.2.6']
40
+ ]
41
+
42
+ p.clean_globs |= %w[**/.DS_Store tmp *.log]
43
+ path = (p.rubyforge_name == p.name) ? p.rubyforge_name : "\#{p.rubyforge_name}/\#{p.name}"
44
+ p.remote_rdoc_dir = File.join(path.gsub(/^#{p.rubyforge_name}\/?/,''), 'rdoc')
45
+ p.rsync_args = '-av --delete --ignore-errors'
46
+ end
47
+
48
+ desc 'Recreate Manifest.txt to include ALL files'
49
+ task :manifest do
50
+ `rake check_manifest | patch -p0 > Manifest.txt`
51
+ end
52
+
53
+ desc "Generate a #{$hoe.name}.gemspec file"
54
+ task :gemspec do
55
+ File.open("#{$hoe.name}.gemspec", "w") do |file|
56
+ file.puts $hoe.spec.to_ruby
57
+ end
58
+ end
@@ -0,0 +1,9 @@
1
+ author:
2
+ name_1: Bill Eisenhauer
3
+ homepage_1: http://blog.billeisenhauer.com
4
+ name_2: Andre Lewis
5
+ homepage_2: http://www.earthcode.com
6
+ summary: Geo distance calculations, distance calculation query support, geocoding for physical and ip addresses.
7
+ version: 1.1.0
8
+ rails_version: 1.0+
9
+ license: MIT
@@ -0,0 +1,56 @@
1
+ if defined? Geokit
2
+
3
+ # These defaults are used in Geokit::Mappable.distance_to and in acts_as_mappable
4
+ Geokit::default_units = :miles
5
+ Geokit::default_formula = :sphere
6
+
7
+ # This is the timeout value in seconds to be used for calls to the geocoder web
8
+ # services. For no timeout at all, comment out the setting. The timeout unit
9
+ # is in seconds.
10
+ Geokit::Geocoders::timeout = 3
11
+
12
+ # These settings are used if web service calls must be routed through a proxy.
13
+ # These setting can be nil if not needed, otherwise, addr and port must be
14
+ # filled in at a minimum. If the proxy requires authentication, the username
15
+ # and password can be provided as well.
16
+ Geokit::Geocoders::proxy_addr = nil
17
+ Geokit::Geocoders::proxy_port = nil
18
+ Geokit::Geocoders::proxy_user = nil
19
+ Geokit::Geocoders::proxy_pass = nil
20
+
21
+ # This is your yahoo application key for the Yahoo Geocoder.
22
+ # See http://developer.yahoo.com/faq/index.html#appid
23
+ # and http://developer.yahoo.com/maps/rest/V1/geocode.html
24
+ Geokit::Geocoders::yahoo = 'REPLACE_WITH_YOUR_YAHOO_KEY'
25
+
26
+ # This is your Google Maps geocoder key.
27
+ # See http://www.google.com/apis/maps/signup.html
28
+ # and http://www.google.com/apis/maps/documentation/#Geocoding_Examples
29
+ Geokit::Geocoders::google = 'REPLACE_WITH_YOUR_GOOGLE_KEY'
30
+
31
+ # This is your username and password for geocoder.us.
32
+ # To use the free service, the value can be set to nil or false. For
33
+ # usage tied to an account, the value should be set to username:password.
34
+ # See http://geocoder.us
35
+ # and http://geocoder.us/user/signup
36
+ Geokit::Geocoders::geocoder_us = false
37
+
38
+ # This is your authorization key for geocoder.ca.
39
+ # To use the free service, the value can be set to nil or false. For
40
+ # usage tied to an account, set the value to the key obtained from
41
+ # Geocoder.ca.
42
+ # See http://geocoder.ca
43
+ # and http://geocoder.ca/?register=1
44
+ Geokit::Geocoders::geocoder_ca = false
45
+
46
+ # Uncomment to use a username with the Geonames geocoder
47
+ #Geokit::Geocoders::geonames="REPLACE_WITH_YOUR_GEONAMES_USERNAME"
48
+
49
+ # This is the order in which the geocoders are called in a failover scenario
50
+ # If you only want to use a single geocoder, put a single symbol in the array.
51
+ # Valid symbols are :google, :yahoo, :us, and :ca.
52
+ # Be aware that there are Terms of Use restrictions on how you can use the
53
+ # various geocoders. Make sure you read up on relevant Terms of Use for each
54
+ # geocoder you are going to use.
55
+ Geokit::Geocoders::provider_order = [:google,:us]
56
+ end
@@ -0,0 +1,36 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{geokit-rails}
5
+ s.version = "1.1.0"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Andre Lewis", "Bill Eisenhauer"]
9
+ s.date = %q{2009-03-31}
10
+ s.email = ["andre@earthcode.com", "bill@billeisenhauer.com"]
11
+ s.extra_rdoc_files = ["Manifest.txt"]
12
+ s.files = ["CHANGELOG.rdoc", "MIT-LICENSE", "Manifest.txt", "README.markdown", "Rakefile", "about.yml", "assets/api_keys_template", "geokit-rails.gemspec", "init.rb", "install.rb", "lib/geokit-rails.rb", "lib/geokit-rails/acts_as_mappable.rb", "lib/geokit-rails/defaults.rb", "lib/geokit-rails/ip_geocode_lookup.rb", "test/acts_as_mappable_test.rb", "test/database.yml", "test/fixtures/companies.yml", "test/fixtures/custom_locations.yml", "test/fixtures/locations.yml", "test/fixtures/mock_addresses.yml", "test/fixtures/mock_organizations.yml", "test/fixtures/stores.yml", "test/ip_geocode_lookup_test.rb", "test/schema.rb", "test/test_helper.rb"]
13
+ s.has_rdoc = true
14
+ s.rdoc_options = ["--main", "README.txt"]
15
+ s.require_paths = ["lib"]
16
+ s.rubyforge_project = %q{geokit-rails}
17
+ s.rubygems_version = %q{1.3.1}
18
+ s.summary = %q{Geo distance calculations, distance calculation query support, geocoding for physical and ip addresses.}
19
+ s.test_files = ["test/test_helper.rb"]
20
+
21
+ if s.respond_to? :specification_version then
22
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
23
+ s.specification_version = 2
24
+
25
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
26
+ s.add_runtime_dependency(%q<geokit>, [">= 1.2.6"])
27
+ s.add_development_dependency(%q<hoe>, [">= 1.11.0"])
28
+ else
29
+ s.add_dependency(%q<geokit>, [">= 1.2.6"])
30
+ s.add_dependency(%q<hoe>, [">= 1.11.0"])
31
+ end
32
+ else
33
+ s.add_dependency(%q<geokit>, [">= 1.2.6"])
34
+ s.add_dependency(%q<hoe>, [">= 1.11.0"])
35
+ end
36
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'geokit-rails'
@@ -0,0 +1,14 @@
1
+ # Display to the console the contents of the README file.
2
+ puts IO.read(File.join(File.dirname(__FILE__), 'README.markdown'))
3
+
4
+ # place the api_keys_template in the application's /config/initializers/geokit_config.rb
5
+ path=File.expand_path(File.join(File.dirname(__FILE__), '../../../config/initializers/geokit_config.rb'))
6
+ template_path=File.join(File.dirname(__FILE__), '/assets/api_keys_template')
7
+ if File.exists?(path)
8
+ puts "It looks like you already have a configuration file at #{path}. We've left it as-is. Recommended: check #{template_path} to see if anything has changed, and update config file accordingly."
9
+ else
10
+ File.open(path, "w") do |f|
11
+ f.puts IO.read(template_path)
12
+ puts "We created a configuration file for you in config/initializers/geokit_config.rb. Add your Google API keys, etc there."
13
+ end
14
+ end
@@ -0,0 +1,18 @@
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. You should specify gem dependencies in your config/environment.rb
6
+ # with this line: config.gem "andre-geokit", :lib=>'geokit', :source => 'http://gems.github.com'
7
+ #
8
+ if defined? Geokit
9
+ require 'geokit-rails/defaults'
10
+ require 'geokit-rails/acts_as_mappable'
11
+ require 'geokit-rails/ip_geocode_lookup'
12
+
13
+ # Automatically mix in distance finder support into ActiveRecord classes.
14
+ ActiveRecord::Base.send :include, GeoKit::ActsAsMappable
15
+
16
+ # Automatically mix in ip geocoding helpers into ActionController classes.
17
+ ActionController::Base.send :include, GeoKit::IpGeocodeLookup
18
+ end
@@ -0,0 +1,465 @@
1
+ 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
+ module ActsAsMappable
24
+ # Mix below class methods into ActiveRecord.
25
+ def self.included(base) # :nodoc:
26
+ base.extend ClassMethods
27
+ end
28
+
29
+ # Class method to mix into active record.
30
+ module ClassMethods # :nodoc:
31
+ # Class method to bring distance query support into ActiveRecord models. By default
32
+ # uses :miles for distance units and performs calculations based upon the Haversine
33
+ # (sphere) formula. These can be changed by setting Geokit::default_units and
34
+ # Geokit::default_formula. Also, by default, uses lat, lng, and distance for respective
35
+ # column names. All of these can be overridden using the :default_units, :default_formula,
36
+ # :lat_column_name, :lng_column_name, and :distance_column_name hash keys.
37
+ #
38
+ # Can also use to auto-geocode a specific column on create. Syntax;
39
+ #
40
+ # acts_as_mappable :auto_geocode=>true
41
+ #
42
+ # By default, it tries to geocode the "address" field. Or, for more customized behavior:
43
+ #
44
+ # acts_as_mappable :auto_geocode=>{:field=>:address,:error_message=>'bad address'}
45
+ #
46
+ # In both cases, it creates a before_validation_on_create callback to geocode the given column.
47
+ # For anything more customized, we recommend you forgo the auto_geocode option
48
+ # and create your own AR callback to handle geocoding.
49
+ def acts_as_mappable(options = {})
50
+ # Mix in the module, but ensure to do so just once.
51
+ return if !defined?(Geokit::Mappable) || self.included_modules.include?(Geokit::ActsAsMappable::InstanceMethods)
52
+ send :include, Geokit::ActsAsMappable::InstanceMethods
53
+ # include the Mappable module.
54
+ send :include, Geokit::Mappable
55
+
56
+ # Handle class variables.
57
+ cattr_accessor :through
58
+ if self.through = options[:through]
59
+ if reflection = self.reflect_on_association(self.through)
60
+ (class << self; self; end).instance_eval do
61
+ [ :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|
62
+ define_method method_name do
63
+ reflection.klass.send(method_name)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ else
69
+ cattr_accessor :distance_column_name, :default_units, :default_formula, :lat_column_name, :lng_column_name, :qualified_lat_column_name, :qualified_lng_column_name
70
+ self.distance_column_name = options[:distance_column_name] || 'distance'
71
+ self.default_units = options[:default_units] || Geokit::default_units
72
+ self.default_formula = options[:default_formula] || Geokit::default_formula
73
+ self.lat_column_name = options[:lat_column_name] || 'lat'
74
+ self.lng_column_name = options[:lng_column_name] || 'lng'
75
+ self.qualified_lat_column_name = "#{table_name}.#{lat_column_name}"
76
+ self.qualified_lng_column_name = "#{table_name}.#{lng_column_name}"
77
+ if options.include?(:auto_geocode) && options[:auto_geocode]
78
+ # if the form auto_geocode=>true is used, let the defaults take over by suppling an empty hash
79
+ options[:auto_geocode] = {} if options[:auto_geocode] == true
80
+ cattr_accessor :auto_geocode_field, :auto_geocode_error_message
81
+ self.auto_geocode_field = options[:auto_geocode][:field] || 'address'
82
+ self.auto_geocode_error_message = options[:auto_geocode][:error_message] || 'could not locate address'
83
+
84
+ # set the actual callback here
85
+ before_validation_on_create :auto_geocode_address
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ # this is the callback for auto_geocoding
92
+ def auto_geocode_address
93
+ address=self.send(auto_geocode_field).to_s
94
+ geo=Geokit::Geocoders::MultiGeocoder.geocode(address)
95
+
96
+ if geo.success
97
+ self.send("#{lat_column_name}=", geo.lat)
98
+ self.send("#{lng_column_name}=", geo.lng)
99
+ else
100
+ errors.add(auto_geocode_field, auto_geocode_error_message)
101
+ end
102
+
103
+ geo.success
104
+ end
105
+
106
+ # Instance methods to mix into ActiveRecord.
107
+ module InstanceMethods #:nodoc:
108
+ # Mix class methods into module.
109
+ def self.included(base) # :nodoc:
110
+ base.extend SingletonMethods
111
+ end
112
+
113
+ # Class singleton methods to mix into ActiveRecord.
114
+ module SingletonMethods # :nodoc:
115
+ # Extends the existing find method in potentially two ways:
116
+ # - If a mappable instance exists in the options, adds a distance column.
117
+ # - If a mappable instance exists in the options and the distance column exists in the
118
+ # conditions, substitutes the distance sql for the distance column -- this saves
119
+ # having to write the gory SQL.
120
+ def find(*args)
121
+ prepare_for_find_or_count(:find, args)
122
+ super(*args)
123
+ end
124
+
125
+ # Extends the existing count method by:
126
+ # - If a mappable instance exists in the options and the distance column exists in the
127
+ # conditions, substitutes the distance sql for the distance column -- this saves
128
+ # having to write the gory SQL.
129
+ def count(*args)
130
+ prepare_for_find_or_count(:count, args)
131
+ super(*args)
132
+ end
133
+
134
+ # Finds within a distance radius.
135
+ def find_within(distance, options={})
136
+ options[:within] = distance
137
+ find(:all, options)
138
+ end
139
+ alias find_inside find_within
140
+
141
+ # Finds beyond a distance radius.
142
+ def find_beyond(distance, options={})
143
+ options[:beyond] = distance
144
+ find(:all, options)
145
+ end
146
+ alias find_outside find_beyond
147
+
148
+ # Finds according to a range. Accepts inclusive or exclusive ranges.
149
+ def find_by_range(range, options={})
150
+ options[:range] = range
151
+ find(:all, options)
152
+ end
153
+
154
+ # Finds the closest to the origin.
155
+ def find_closest(options={})
156
+ find(:nearest, options)
157
+ end
158
+ alias find_nearest find_closest
159
+
160
+ # Finds the farthest from the origin.
161
+ def find_farthest(options={})
162
+ find(:farthest, options)
163
+ end
164
+
165
+ # Finds within rectangular bounds (sw,ne).
166
+ def find_within_bounds(bounds, options={})
167
+ options[:bounds] = bounds
168
+ find(:all, options)
169
+ end
170
+
171
+ # counts within a distance radius.
172
+ def count_within(distance, options={})
173
+ options[:within] = distance
174
+ count(options)
175
+ end
176
+ alias count_inside count_within
177
+
178
+ # Counts beyond a distance radius.
179
+ def count_beyond(distance, options={})
180
+ options[:beyond] = distance
181
+ count(options)
182
+ end
183
+ alias count_outside count_beyond
184
+
185
+ # Counts according to a range. Accepts inclusive or exclusive ranges.
186
+ def count_by_range(range, options={})
187
+ options[:range] = range
188
+ count(options)
189
+ end
190
+
191
+ # Finds within rectangular bounds (sw,ne).
192
+ def count_within_bounds(bounds, options={})
193
+ options[:bounds] = bounds
194
+ count(options)
195
+ end
196
+
197
+ # Returns the distance calculation to be used as a display column or a condition. This
198
+ # is provide for anyone wanting access to the raw SQL.
199
+ def distance_sql(origin, units=default_units, formula=default_formula)
200
+ case formula
201
+ when :sphere
202
+ sql = sphere_distance_sql(origin, units)
203
+ when :flat
204
+ sql = flat_distance_sql(origin, units)
205
+ end
206
+ sql
207
+ end
208
+
209
+ private
210
+
211
+ # Prepares either a find or a count action by parsing through the options and
212
+ # conditionally adding to the select clause for finders.
213
+ def prepare_for_find_or_count(action, args)
214
+ options = args.extract_options!
215
+ #options = defined?(args.extract_options!) ? args.extract_options! : extract_options_from_args!(args)
216
+ # Handle :through
217
+ apply_include_for_through(options)
218
+ # Obtain items affecting distance condition.
219
+ origin = extract_origin_from_options(options)
220
+ units = extract_units_from_options(options)
221
+ formula = extract_formula_from_options(options)
222
+ bounds = extract_bounds_from_options(options)
223
+ # if no explicit bounds were given, try formulating them from the point and distance given
224
+ bounds = formulate_bounds_from_distance(options, origin, units) unless bounds
225
+ # Apply select adjustments based upon action.
226
+ add_distance_to_select(options, origin, units, formula) if origin && action == :find
227
+ # Apply the conditions for a bounding rectangle if applicable
228
+ apply_bounds_conditions(options,bounds) if bounds
229
+ # Apply distance scoping and perform substitutions.
230
+ apply_distance_scope(options)
231
+ substitute_distance_in_conditions(options, origin, units, formula) if origin && options.has_key?(:conditions)
232
+ # Order by scoping for find action.
233
+ apply_find_scope(args, options) if action == :find
234
+ # Unfortunatley, we need to do extra work if you use an :include. See the method for more info.
235
+ handle_order_with_include(options,origin,units,formula) if options.include?(:include) && options.include?(:order) && origin
236
+ # Restore options minus the extra options that we used for the
237
+ # Geokit API.
238
+ args.push(options)
239
+ end
240
+
241
+ def apply_include_for_through(options)
242
+ if self.through
243
+ case options[:include]
244
+ when Array
245
+ options[:include] << self.through
246
+ when Hash, String, Symbol
247
+ options[:include] = [ self.through, options[:include] ]
248
+ else
249
+ options[:include] = self.through
250
+ end
251
+ end
252
+ end
253
+
254
+ # If we're here, it means that 1) an origin argument, 2) an :include, 3) an :order clause were supplied.
255
+ # Now we have to sub some SQL into the :order clause. The reason is that when you do an :include,
256
+ # ActiveRecord drops the psuedo-column (specificically, distance) which we supplied for :select.
257
+ # So, the 'distance' column isn't available for the :order clause to reference when we use :include.
258
+ def handle_order_with_include(options, origin, units, formula)
259
+ # replace the distance_column_name with the distance sql in order clause
260
+ options[:order].sub!(distance_column_name, distance_sql(origin, units, formula))
261
+ end
262
+
263
+ # Looks for mapping-specific tokens and makes appropriate translations so that the
264
+ # original finder has its expected arguments. Resets the the scope argument to
265
+ # :first and ensures the limit is set to one.
266
+ def apply_find_scope(args, options)
267
+ case args.first
268
+ when :nearest, :closest
269
+ args[0] = :first
270
+ options[:limit] = 1
271
+ options[:order] = "#{distance_column_name} ASC"
272
+ when :farthest
273
+ args[0] = :first
274
+ options[:limit] = 1
275
+ options[:order] = "#{distance_column_name} DESC"
276
+ end
277
+ end
278
+
279
+ # If it's a :within query, add a bounding box to improve performance.
280
+ # This only gets called if a :bounds argument is not otherwise supplied.
281
+ def formulate_bounds_from_distance(options, origin, units)
282
+ distance = options[:within] if options.has_key?(:within)
283
+ distance = options[:range].last-(options[:range].exclude_end?? 1 : 0) if options.has_key?(:range)
284
+ if distance
285
+ res=Geokit::Bounds.from_point_and_radius(origin,distance,:units=>units)
286
+ else
287
+ nil
288
+ end
289
+ end
290
+
291
+ # Replace :within, :beyond and :range distance tokens with the appropriate distance
292
+ # where clauses. Removes these tokens from the options hash.
293
+ def apply_distance_scope(options)
294
+ distance_condition = "#{distance_column_name} <= #{options[:within]}" if options.has_key?(:within)
295
+ distance_condition = "#{distance_column_name} > #{options[:beyond]}" if options.has_key?(:beyond)
296
+ distance_condition = "#{distance_column_name} >= #{options[:range].first} AND #{distance_column_name} <#{'=' unless options[:range].exclude_end?} #{options[:range].last}" if options.has_key?(:range)
297
+ [:within, :beyond, :range].each { |option| options.delete(option) } if distance_condition
298
+
299
+ options[:conditions]=augment_conditions(options[:conditions],distance_condition) if distance_condition
300
+ end
301
+
302
+ # This method lets you transparently add a new condition to a query without
303
+ # worrying about whether it currently has conditions, or what kind of conditions they are
304
+ # (string or array).
305
+ #
306
+ # Takes the current conditions (which can be an array or a string, or can be nil/false),
307
+ # and a SQL string. It inserts the sql into the existing conditions, and returns new conditions
308
+ # (which can be a string or an array
309
+ def augment_conditions(current_conditions,sql)
310
+ if current_conditions && current_conditions.is_a?(String)
311
+ res="#{current_conditions} AND #{sql}"
312
+ elsif current_conditions && current_conditions.is_a?(Array)
313
+ current_conditions[0]="#{current_conditions[0]} AND #{sql}"
314
+ res=current_conditions
315
+ else
316
+ res=sql
317
+ end
318
+ res
319
+ end
320
+
321
+ # Alters the conditions to include rectangular bounds conditions.
322
+ def apply_bounds_conditions(options,bounds)
323
+ sw,ne=bounds.sw,bounds.ne
324
+ 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}"
325
+ bounds_sql="#{qualified_lat_column_name}>#{sw.lat} AND #{qualified_lat_column_name}<#{ne.lat} AND #{lng_sql}"
326
+ options[:conditions]=augment_conditions(options[:conditions],bounds_sql)
327
+ end
328
+
329
+ # Extracts the origin instance out of the options if it exists and returns
330
+ # it. If there is no origin, looks for latitude and longitude values to
331
+ # create an origin. The side-effect of the method is to remove these
332
+ # option keys from the hash.
333
+ def extract_origin_from_options(options)
334
+ origin = options.delete(:origin)
335
+ res = normalize_point_to_lat_lng(origin) if origin
336
+ res
337
+ end
338
+
339
+ # Extract the units out of the options if it exists and returns it. If
340
+ # there is no :units key, it uses the default. The side effect of the
341
+ # method is to remove the :units key from the options hash.
342
+ def extract_units_from_options(options)
343
+ units = options[:units] || default_units
344
+ options.delete(:units)
345
+ units
346
+ end
347
+
348
+ # Extract the formula out of the options if it exists and returns it. If
349
+ # there is no :formula key, it uses the default. The side effect of the
350
+ # method is to remove the :formula key from the options hash.
351
+ def extract_formula_from_options(options)
352
+ formula = options[:formula] || default_formula
353
+ options.delete(:formula)
354
+ formula
355
+ end
356
+
357
+ def extract_bounds_from_options(options)
358
+ bounds = options.delete(:bounds)
359
+ bounds = Geokit::Bounds.normalize(bounds) if bounds
360
+ end
361
+
362
+ # Geocode IP address.
363
+ def geocode_ip_address(origin)
364
+ geo_location = Geokit::Geocoders::IpGeocoder.geocode(origin)
365
+ return geo_location if geo_location.success
366
+ raise Geokit::Geocoders::GeocodeError
367
+ end
368
+
369
+
370
+ # Given a point in a variety of (an address to geocode,
371
+ # an array of [lat,lng], or an object with appropriate lat/lng methods, an IP addres)
372
+ # this method will normalize it into a Geokit::LatLng instance. The only thing this
373
+ # method adds on top of LatLng#normalize is handling of IP addresses
374
+ def normalize_point_to_lat_lng(point)
375
+ res = geocode_ip_address(point) if point.is_a?(String) && /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(point)
376
+ res = Geokit::LatLng.normalize(point) unless res
377
+ res
378
+ end
379
+
380
+ # Augments the select with the distance SQL.
381
+ def add_distance_to_select(options, origin, units=default_units, formula=default_formula)
382
+ if origin
383
+ distance_selector = distance_sql(origin, units, formula) + " AS #{distance_column_name}"
384
+ selector = options.has_key?(:select) && options[:select] ? options[:select] : "*"
385
+ options[:select] = "#{selector}, #{distance_selector}"
386
+ end
387
+ end
388
+
389
+ # Looks for the distance column and replaces it with the distance sql. If an origin was not
390
+ # passed in and the distance column exists, we leave it to be flagged as bad SQL by the database.
391
+ # Conditions are either a string or an array. In the case of an array, the first entry contains
392
+ # the condition.
393
+ def substitute_distance_in_conditions(options, origin, units=default_units, formula=default_formula)
394
+ original_conditions = options[:conditions]
395
+ condition = original_conditions.is_a?(String) ? original_conditions : original_conditions.first
396
+ pattern = Regexp.new("\\b#{distance_column_name}\\b")
397
+ condition = condition.gsub(pattern, distance_sql(origin, units, formula))
398
+ original_conditions = condition if original_conditions.is_a?(String)
399
+ original_conditions[0] = condition if original_conditions.is_a?(Array)
400
+ options[:conditions] = original_conditions
401
+ end
402
+
403
+ # Returns the distance SQL using the spherical world formula (Haversine). The SQL is tuned
404
+ # to the database in use.
405
+ def sphere_distance_sql(origin, units)
406
+ lat = deg2rad(origin.lat)
407
+ lng = deg2rad(origin.lng)
408
+ multiplier = units_sphere_multiplier(units)
409
+ case connection.adapter_name.downcase
410
+ when "mysql"
411
+ sql=<<-SQL_END
412
+ (ACOS(least(1,COS(#{lat})*COS(#{lng})*COS(RADIANS(#{qualified_lat_column_name}))*COS(RADIANS(#{qualified_lng_column_name}))+
413
+ COS(#{lat})*SIN(#{lng})*COS(RADIANS(#{qualified_lat_column_name}))*SIN(RADIANS(#{qualified_lng_column_name}))+
414
+ SIN(#{lat})*SIN(RADIANS(#{qualified_lat_column_name}))))*#{multiplier})
415
+ SQL_END
416
+ when "postgresql"
417
+ sql=<<-SQL_END
418
+ (ACOS(least(1,COS(#{lat})*COS(#{lng})*COS(RADIANS(#{qualified_lat_column_name}))*COS(RADIANS(#{qualified_lng_column_name}))+
419
+ COS(#{lat})*SIN(#{lng})*COS(RADIANS(#{qualified_lat_column_name}))*SIN(RADIANS(#{qualified_lng_column_name}))+
420
+ SIN(#{lat})*SIN(RADIANS(#{qualified_lat_column_name}))))*#{multiplier})
421
+ SQL_END
422
+ else
423
+ sql = "unhandled #{connection.adapter_name.downcase} adapter"
424
+ end
425
+ end
426
+
427
+ # Returns the distance SQL using the flat-world formula (Phythagorean Theory). The SQL is tuned
428
+ # to the database in use.
429
+ def flat_distance_sql(origin, units)
430
+ lat_degree_units = units_per_latitude_degree(units)
431
+ lng_degree_units = units_per_longitude_degree(origin.lat, units)
432
+ case connection.adapter_name.downcase
433
+ when "mysql"
434
+ sql=<<-SQL_END
435
+ SQRT(POW(#{lat_degree_units}*(#{origin.lat}-#{qualified_lat_column_name}),2)+
436
+ POW(#{lng_degree_units}*(#{origin.lng}-#{qualified_lng_column_name}),2))
437
+ SQL_END
438
+ when "postgresql"
439
+ sql=<<-SQL_END
440
+ SQRT(POW(#{lat_degree_units}*(#{origin.lat}-#{qualified_lat_column_name}),2)+
441
+ POW(#{lng_degree_units}*(#{origin.lng}-#{qualified_lng_column_name}),2))
442
+ SQL_END
443
+ else
444
+ sql = "unhandled #{connection.adapter_name.downcase} adapter"
445
+ end
446
+ end
447
+ end
448
+ end
449
+ end
450
+ end
451
+
452
+ # Extend Array with a sort_by_distance method.
453
+ # This method creates a "distance" attribute on each object,
454
+ # calculates the distance from the passed origin,
455
+ # and finally sorts the array by the resulting distance.
456
+ class Array
457
+ def sort_by_distance_from(origin, opts={})
458
+ distance_attribute_name = opts.delete(:distance_attribute_name) || 'distance'
459
+ self.each do |e|
460
+ e.class.send(:attr_accessor, distance_attribute_name) if !e.respond_to? "#{distance_attribute_name}="
461
+ e.send("#{distance_attribute_name}=", e.distance_to(origin,opts))
462
+ end
463
+ self.sort!{|a,b|a.send(distance_attribute_name) <=> b.send(distance_attribute_name)}
464
+ end
465
+ end