geokit-ar 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/geokit-ar.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'geokit'
2
+
3
+ require 'geokit-ar/core_extensions'
4
+
5
+ require 'geokit-ar/defaults'
6
+ require 'geokit-ar/adapters/abstract'
7
+ require 'geokit-ar/acts_as_mappable'
8
+
9
+ ActiveRecord::Base.send(:include, Geokit::ActsAsMappable::Glue)
10
+ Geokit::Geocoders.logger = ActiveRecord::Base.logger
@@ -0,0 +1,318 @@
1
+ require 'active_record'
2
+ require 'active_support/concern'
3
+
4
+ module Geokit
5
+ module ActsAsMappable
6
+
7
+ class UnsupportedAdapter < StandardError ; end
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
28
+ end
29
+ end
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
51
+ end
52
+ end
53
+ end
54
+ end # Glue
55
+
56
+ extend ActiveSupport::Concern
57
+
58
+ included do
59
+ include Geokit::Mappable
60
+ end
61
+
62
+ # Class methods included in models when +acts_as_mappable+ is called
63
+ module ClassMethods
64
+
65
+ # A proxy to an instance of a finder adapter, inferred from the connection's adapter.
66
+ def adapter
67
+ @adapter ||= begin
68
+ require File.join(File.dirname(__FILE__), 'adapters', connection.adapter_name.downcase)
69
+ klass = Adapters.const_get(connection.adapter_name.camelcase)
70
+ klass.load(self) unless klass.loaded
71
+ klass.new(self)
72
+ rescue LoadError
73
+ raise UnsupportedAdapter, "`#{connection.adapter_name.downcase}` is not a supported adapter."
74
+ end
75
+ end
76
+
77
+ def within(distance, options = {})
78
+ options[:within] = distance
79
+ geo_scope(options)
80
+ end
81
+ alias inside within
82
+
83
+ def beyond(distance, options = {})
84
+ options[:beyond] = distance
85
+ geo_scope(options)
86
+ end
87
+ alias outside beyond
88
+
89
+ def in_range(range, options = {})
90
+ options[:range] = range
91
+ geo_scope(options)
92
+ end
93
+
94
+ def in_bounds(bounds, options = {})
95
+ options[:bounds] = bounds
96
+ geo_scope(options)
97
+ end
98
+
99
+ def closest(options = {})
100
+ geo_scope(options).order("#{distance_column_name} asc").limit(1)
101
+ end
102
+ alias nearest closest
103
+
104
+ def farthest(options = {})
105
+ geo_scope(options).order("#{distance_column_name} desc").limit(1)
106
+ end
107
+
108
+ def geo_scope(options = {})
109
+ arel = self.is_a?(ActiveRecord::Relation) ? self : self.scoped
110
+
111
+ origin = extract_origin_from_options(options)
112
+ units = extract_units_from_options(options)
113
+ formula = extract_formula_from_options(options)
114
+ bounds = extract_bounds_from_options(options)
115
+
116
+ if origin || bounds
117
+ bounds = formulate_bounds_from_distance(options, origin, units) unless bounds
118
+
119
+ if origin
120
+ @distance_formula = distance_sql(origin, units, formula)
121
+
122
+ if arel.select_values.blank?
123
+ star_select = Arel::SqlLiteral.new(arel.quoted_table_name + '.*')
124
+ arel = arel.select(star_select)
125
+ end
126
+
127
+ distance_select = Arel::SqlLiteral.new("#{@distance_formula} AS #{distance_column_name}")
128
+ arel = arel.select(distance_select)
129
+ end
130
+
131
+ if bounds
132
+ bound_conditions = bound_conditions(bounds)
133
+ arel = arel.where(bound_conditions) if bound_conditions
134
+ end
135
+
136
+ distance_conditions = distance_conditions(options)
137
+ arel = arel.where(distance_conditions) if distance_conditions
138
+
139
+ if origin
140
+ arel = substitute_distance_in_where_values(arel, origin, units, formula)
141
+ end
142
+ end
143
+
144
+ arel
145
+ end
146
+
147
+ # Returns the distance calculation to be used as a display column or a condition. This
148
+ # is provide for anyone wanting access to the raw SQL.
149
+ def distance_sql(origin, units=default_units, formula=default_formula)
150
+ case formula
151
+ when :sphere
152
+ sql = sphere_distance_sql(origin, units)
153
+ when :flat
154
+ sql = flat_distance_sql(origin, units)
155
+ end
156
+ sql
157
+ end
158
+
159
+ private
160
+
161
+ # If it's a :within query, add a bounding box to improve performance.
162
+ # This only gets called if a :bounds argument is not otherwise supplied.
163
+ def formulate_bounds_from_distance(options, origin, units)
164
+ distance = options[:within] if options.has_key?(:within)
165
+ distance = options[:range].last-(options[:range].exclude_end?? 1 : 0) if options.has_key?(:range)
166
+ if distance
167
+ res=Geokit::Bounds.from_point_and_radius(origin,distance,:units=>units)
168
+ else
169
+ nil
170
+ end
171
+ end
172
+
173
+ def distance_conditions(options)
174
+ res = if options.has_key?(:within)
175
+ "#{distance_column_name} <= #{options[:within]}"
176
+ elsif options.has_key?(:beyond)
177
+ "#{distance_column_name} > #{options[:beyond]}"
178
+ elsif options.has_key?(:range)
179
+ "#{distance_column_name} >= #{options[:range].first} AND #{distance_column_name} <#{'=' unless options[:range].exclude_end?} #{options[:range].last}"
180
+ end
181
+ Arel::SqlLiteral.new("(#{res})") if res.present?
182
+ end
183
+
184
+ def bound_conditions(bounds)
185
+ sw,ne = bounds.sw, bounds.ne
186
+ 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}"
187
+ res = "#{qualified_lat_column_name}>#{sw.lat} AND #{qualified_lat_column_name}<#{ne.lat} AND #{lng_sql}"
188
+ Arel::SqlLiteral.new("(#{res})") if res.present?
189
+ end
190
+
191
+ # Extracts the origin instance out of the options if it exists and returns
192
+ # it. If there is no origin, looks for latitude and longitude values to
193
+ # create an origin. The side-effect of the method is to remove these
194
+ # option keys from the hash.
195
+ def extract_origin_from_options(options)
196
+ origin = options.delete(:origin)
197
+ res = normalize_point_to_lat_lng(origin) if origin
198
+ res
199
+ end
200
+
201
+ # Extract the units out of the options if it exists and returns it. If
202
+ # there is no :units key, it uses the default. The side effect of the
203
+ # method is to remove the :units key from the options hash.
204
+ def extract_units_from_options(options)
205
+ units = options[:units] || default_units
206
+ options.delete(:units)
207
+ units
208
+ end
209
+
210
+ # Extract the formula out of the options if it exists and returns it. If
211
+ # there is no :formula key, it uses the default. The side effect of the
212
+ # method is to remove the :formula key from the options hash.
213
+ def extract_formula_from_options(options)
214
+ formula = options[:formula] || default_formula
215
+ options.delete(:formula)
216
+ formula
217
+ end
218
+
219
+ def extract_bounds_from_options(options)
220
+ bounds = options.delete(:bounds)
221
+ bounds = Geokit::Bounds.normalize(bounds) if bounds
222
+ end
223
+
224
+ # Geocode IP address.
225
+ def geocode_ip_address(origin)
226
+ geo_location = Geokit::Geocoders::MultiGeocoder.geocode(origin)
227
+ return geo_location if geo_location.success
228
+ raise Geokit::Geocoders::GeocodeError
229
+ end
230
+
231
+ # Given a point in a variety of (an address to geocode,
232
+ # an array of [lat,lng], or an object with appropriate lat/lng methods, an IP addres)
233
+ # this method will normalize it into a Geokit::LatLng instance. The only thing this
234
+ # method adds on top of LatLng#normalize is handling of IP addresses
235
+ def normalize_point_to_lat_lng(point)
236
+ res = geocode_ip_address(point) if point.is_a?(String) && /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(point)
237
+ res = Geokit::LatLng.normalize(point) unless res
238
+ res
239
+ end
240
+
241
+ # Looks for the distance column and replaces it with the distance sql. If an origin was not
242
+ # passed in and the distance column exists, we leave it to be flagged as bad SQL by the database.
243
+ # Conditions are either a string or an array. In the case of an array, the first entry contains
244
+ # the condition.
245
+ def substitute_distance_in_where_values(arel, origin, units=default_units, formula=default_formula)
246
+ pattern = Regexp.new("\\b#{distance_column_name}\\b")
247
+ value = distance_sql(origin, units, formula)
248
+ arel.where_values.map! do |where_value|
249
+ if where_value.is_a?(String)
250
+ where_value.gsub(pattern, value)
251
+ else
252
+ where_value
253
+ end
254
+ end
255
+ arel
256
+ end
257
+
258
+ # Returns the distance SQL using the spherical world formula (Haversine). The SQL is tuned
259
+ # to the database in use.
260
+ def sphere_distance_sql(origin, units)
261
+ lat = deg2rad(origin.lat)
262
+ lng = deg2rad(origin.lng)
263
+ multiplier = units_sphere_multiplier(units)
264
+
265
+ adapter.sphere_distance_sql(lat, lng, multiplier) if adapter
266
+ end
267
+
268
+ # Returns the distance SQL using the flat-world formula (Phythagorean Theory). The SQL is tuned
269
+ # to the database in use.
270
+ def flat_distance_sql(origin, units)
271
+ lat_degree_units = units_per_latitude_degree(units)
272
+ lng_degree_units = units_per_longitude_degree(origin.lat, units)
273
+
274
+ adapter.flat_distance_sql(origin, lat_degree_units, lng_degree_units)
275
+ end
276
+
277
+ end # ClassMethods
278
+
279
+ # this is the callback for auto_geocoding
280
+ def auto_geocode_address
281
+ address=self.send(auto_geocode_field).to_s
282
+ geo=Geokit::Geocoders::MultiGeocoder.geocode(address)
283
+
284
+ if geo.success
285
+ self.send("#{lat_column_name}=", geo.lat)
286
+ self.send("#{lng_column_name}=", geo.lng)
287
+ else
288
+ errors.add(auto_geocode_field, auto_geocode_error_message)
289
+ end
290
+
291
+ geo.success
292
+ end
293
+
294
+ def self.end_of_reflection_chain(through, klass)
295
+ while through
296
+ reflection = nil
297
+ if through.is_a?(Hash)
298
+ association, through = through.to_a.first
299
+ else
300
+ association, through = through, nil
301
+ end
302
+
303
+ if reflection = klass.reflect_on_association(association)
304
+ klass = reflection.klass
305
+ else
306
+ raise ArgumentError, "You gave #{association} in :through, but I could not find it on #{klass}."
307
+ end
308
+ end
309
+
310
+ reflection
311
+ end
312
+
313
+ end # ActsAsMappable
314
+ end # Geokit
315
+
316
+
317
+
318
+ # ActiveRecord::Base.extend Geokit::ActsAsMappable
@@ -0,0 +1,31 @@
1
+ module Geokit
2
+ module Adapters
3
+ class Abstract
4
+ class NotImplementedError < StandardError ; end
5
+
6
+ cattr_accessor :loaded
7
+
8
+ class << self
9
+ def load(klass) ; end
10
+ end
11
+
12
+ def initialize(klass)
13
+ @owner = klass
14
+ end
15
+
16
+ def method_missing(method, *args, &block)
17
+ return @owner.send(method, *args, &block) if @owner.respond_to?(method)
18
+ super
19
+ end
20
+
21
+ def sphere_distance_sql(lat, lng, multiplier)
22
+ raise NotImplementedError, '#sphere_distance_sql is not implemented'
23
+ end
24
+
25
+ def flat_distance_sql(origin, lat_degree_units, lng_degree_units)
26
+ raise NotImplementedError, '#flat_distance_sql is not implemented'
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,22 @@
1
+ module Geokit
2
+ module Adapters
3
+ class MySQL < 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
@@ -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
@@ -0,0 +1,22 @@
1
+ module Geokit
2
+ module Adapters
3
+ class PostgreSQL < 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
@@ -0,0 +1,53 @@
1
+ module Geokit
2
+ module Adapters
3
+ class SQLite < Abstract
4
+
5
+ def self.add_numeric(name)
6
+ @@connection.create_function name, 1, :numeric do |func, *args|
7
+ func.result = yield(*args)
8
+ end
9
+ end
10
+
11
+ def self.add_math(name)
12
+ add_numeric name do |*n|
13
+ Math.send name, *n
14
+ end
15
+ end
16
+
17
+ class << self
18
+ def load(klass)
19
+ @@connection = klass.connection.raw_connection
20
+ # Define the functions needed
21
+ add_math 'sqrt'
22
+ add_math 'cos'
23
+ add_math 'acos'
24
+ add_math 'sin'
25
+
26
+ add_numeric('pow') { |n, m| n**m }
27
+ add_numeric('radians') { |n| n * Math::PI / 180 }
28
+ add_numeric('least') { |*args| args.min }
29
+ end
30
+ end
31
+
32
+ def sphere_distance_sql(lat, lng, multiplier)
33
+ %|
34
+ (CASE WHEN #{qualified_lat_column_name} IS NULL OR #{qualified_lng_column_name} IS NULL THEN NULL ELSE
35
+ (ACOS(least(1,COS(#{lat})*COS(#{lng})*COS(RADIANS(#{qualified_lat_column_name}))*COS(RADIANS(#{qualified_lng_column_name}))+
36
+ COS(#{lat})*SIN(#{lng})*COS(RADIANS(#{qualified_lat_column_name}))*SIN(RADIANS(#{qualified_lng_column_name}))+
37
+ SIN(#{lat})*SIN(RADIANS(#{qualified_lat_column_name}))))*#{multiplier})
38
+ END)
39
+ |
40
+ end
41
+
42
+ def flat_distance_sql(origin, lat_degree_units, lng_degree_units)
43
+ %|
44
+ (CASE WHEN #{qualified_lat_column_name} IS NULL OR #{qualified_lng_column_name} IS NULL THEN NULL ELSE
45
+ SQRT(POW(#{lat_degree_units}*(#{origin.lat}-#{qualified_lat_column_name}),2)+
46
+ POW(#{lng_degree_units}*(#{origin.lng}-#{qualified_lng_column_name}),2))
47
+ END)
48
+ |
49
+ end
50
+
51
+ end
52
+ end
53
+ end