dm-geokit 0.10.1 → 0.10.2

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.
data/README CHANGED
@@ -14,6 +14,7 @@ Basic Class Definition:
14
14
 
15
15
  class Location
16
16
  include DataMapper::Resource
17
+ include DataMapper::GeoKit
17
18
  property :id, Serial
18
19
  has_geographic_location :address
19
20
  end
@@ -48,4 +49,4 @@ Requirements
48
49
 
49
50
  * geokit >= 1.5.0
50
51
  * dm-core >= 0.10.1
51
- * dm-aggregates >= 0.10.1
52
+ * dm-aggregates >= 0.10.1
data/Rakefile CHANGED
@@ -2,11 +2,11 @@ begin
2
2
  require 'jeweler'
3
3
  Jeweler::Tasks.new do |s|
4
4
  s.name = 'dm-geokit'
5
- s.summary = "DataMapper plugin for geokit stuff forked from Foy Savas's project. Now relies on the geokit gem rather than Foy's gem."
6
- s.authors = ['Foy Savas', 'Daniel Higginbotham', 'Matt King']
5
+ s.summary = "DataMapper plugin for geokit"
6
+ s.authors = ["Matt King"]
7
7
  s.email = 'matt@mattking.org'
8
8
  s.homepage = "http://github.com/mattking17/dm-geokit/tree/master"
9
- s.description = "Simple and opinionated helper for creating Rubygem projects on GitHub"
9
+ s.description = "Geographic Property support for DataMapper"
10
10
  s.files = FileList["[A-Z]*", "{bin,generators,lib,test}/**/*", 'lib/jeweler/templates/.gitignore']
11
11
  s.require_path = 'lib'
12
12
  s.has_rdoc = true
@@ -14,7 +14,8 @@ begin
14
14
  s.extra_rdoc_files = %w[ README LICENSE TODO ]
15
15
  s.add_dependency 'dm-core'
16
16
  s.add_dependency 'andre-geokit'
17
+ s.rubyforge_project = "dm-geokit"
17
18
  end
18
19
  rescue LoadError
19
20
  puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
20
- end
21
+ end
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
- :major: 0
3
2
  :minor: 10
4
- :patch: 1
3
+ :patch: 2
4
+ :major: 0
@@ -0,0 +1,436 @@
1
+ module DataMapper::GeoKit
2
+ # Contains the class method acts_as_mappable targeted to be mixed into DataMapper.
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 DataMapper.
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 self.included_modules.include?(GeoKit::ActsAsMappable::InstanceMethods)
52
+ send :include, GeoKit::ActsAsMappable::InstanceMethods
53
+ # include the Mappable module.
54
+ send :include, Mappable
55
+
56
+ # Handle class variables.
57
+ cattr_accessor :distance_column_name, :default_units, :default_formula, :lat_column_name, :lng_column_name, :qualified_lat_column_name, :qualified_lng_column_name
58
+ self.distance_column_name = options[:distance_column_name] || 'distance'
59
+ self.default_units = options[:default_units] || GeoKit::default_units
60
+ self.default_formula = options[:default_formula] || GeoKit::default_formula
61
+ self.lat_column_name = options[:lat_column_name] || 'lat'
62
+ self.lng_column_name = options[:lng_column_name] || 'lng'
63
+ self.qualified_lat_column_name = "#{table_name}.#{lat_column_name}"
64
+ self.qualified_lng_column_name = "#{table_name}.#{lng_column_name}"
65
+ if options.include?(:auto_geocode) && options[:auto_geocode]
66
+ # if the form auto_geocode=>true is used, let the defaults take over by suppling an empty hash
67
+ options[:auto_geocode] = {} if options[:auto_geocode] == true
68
+ cattr_accessor :auto_geocode_field, :auto_geocode_error_message
69
+ self.auto_geocode_field = options[:auto_geocode][:field] || 'address'
70
+ self.auto_geocode_error_message = options[:auto_geocode][:error_message] || 'could not locate address'
71
+
72
+ # set the actual callback here
73
+ before_validation_on_create :auto_geocode_address
74
+ end
75
+ end
76
+ end
77
+
78
+ # this is the callback for auto_geocoding
79
+ def auto_geocode_address
80
+ address=self.send(auto_geocode_field)
81
+ geo=GeoKit::Geocoders::MultiGeocoder.geocode(address)
82
+
83
+ if geo.success
84
+ self.send("#{lat_column_name}=", geo.lat)
85
+ self.send("#{lng_column_name}=", geo.lng)
86
+ else
87
+ errors.add(auto_geocode_field, auto_geocode_error_message)
88
+ end
89
+
90
+ geo.success
91
+ end
92
+
93
+ # Instance methods to mix into ActiveRecord.
94
+ module InstanceMethods #:nodoc:
95
+ # Mix class methods into module.
96
+ def self.included(base) # :nodoc:
97
+ base.extend SingletonMethods
98
+ end
99
+
100
+ # Class singleton methods to mix into ActiveRecord.
101
+ module SingletonMethods # :nodoc:
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 = defined?(args.extract_options!) ? args.extract_options! : extract_options_from_args!(args)
202
+ # Obtain items affecting distance condition.
203
+ origin = extract_origin_from_options(options)
204
+ units = extract_units_from_options(options)
205
+ formula = extract_formula_from_options(options)
206
+ bounds = extract_bounds_from_options(options)
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
+ # Unfortunatley, we need to do extra work if you use an :include. See the method for more info.
219
+ handle_order_with_include(options,origin,units,formula) if options.include?(:include) && options.include?(:order) && origin
220
+ # Restore options minus the extra options that we used for the
221
+ # GeoKit API.
222
+ args.push(options)
223
+ end
224
+
225
+ # If we're here, it means that 1) an origin argument, 2) an :include, 3) an :order clause were supplied.
226
+ # Now we have to sub some SQL into the :order clause. The reason is that when you do an :include,
227
+ # ActiveRecord drops the psuedo-column (specificically, distance) which we supplied for :select.
228
+ # So, the 'distance' column isn't available for the :order clause to reference when we use :include.
229
+ def handle_order_with_include(options, origin, units, formula)
230
+ # replace the distance_column_name with the distance sql in order clause
231
+ options[:order].sub!(distance_column_name, distance_sql(origin, units, formula))
232
+ end
233
+
234
+ # Looks for mapping-specific tokens and makes appropriate translations so that the
235
+ # original finder has its expected arguments. Resets the the scope argument to
236
+ # :first and ensures the limit is set to one.
237
+ def apply_find_scope(args, options)
238
+ case args.first
239
+ when :nearest, :closest
240
+ args[0] = :first
241
+ options[:limit] = 1
242
+ options[:order] = "#{distance_column_name} ASC"
243
+ when :farthest
244
+ args[0] = :first
245
+ options[:limit] = 1
246
+ options[:order] = "#{distance_column_name} DESC"
247
+ end
248
+ end
249
+
250
+ # If it's a :within query, add a bounding box to improve performance.
251
+ # This only gets called if a :bounds argument is not otherwise supplied.
252
+ def formulate_bounds_from_distance(options, origin, units)
253
+ distance = options[:within] if options.has_key?(:within)
254
+ distance = options[:range].last-(options[:range].exclude_end?? 1 : 0) if options.has_key?(:range)
255
+ if distance
256
+ res=GeoKit::Bounds.from_point_and_radius(origin,distance,:units=>units)
257
+ else
258
+ nil
259
+ end
260
+ end
261
+
262
+ # Replace :within, :beyond and :range distance tokens with the appropriate distance
263
+ # where clauses. Removes these tokens from the options hash.
264
+ def apply_distance_scope(options)
265
+ distance_condition = "#{distance_column_name} <= #{options[:within]}" if options.has_key?(:within)
266
+ distance_condition = "#{distance_column_name} > #{options[:beyond]}" if options.has_key?(:beyond)
267
+ 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)
268
+ [:within, :beyond, :range].each { |option| options.delete(option) } if distance_condition
269
+
270
+ options[:conditions]=augment_conditions(options[:conditions],distance_condition) if distance_condition
271
+ end
272
+
273
+ # This method lets you transparently add a new condition to a query without
274
+ # worrying about whether it currently has conditions, or what kind of conditions they are
275
+ # (string or array).
276
+ #
277
+ # Takes the current conditions (which can be an array or a string, or can be nil/false),
278
+ # and a SQL string. It inserts the sql into the existing conditions, and returns new conditions
279
+ # (which can be a string or an array
280
+ def augment_conditions(current_conditions,sql)
281
+ if current_conditions && current_conditions.is_a?(String)
282
+ res="#{current_conditions} AND #{sql}"
283
+ elsif current_conditions && current_conditions.is_a?(Array)
284
+ current_conditions[0]="#{current_conditions[0]} AND #{sql}"
285
+ res=current_conditions
286
+ else
287
+ res=sql
288
+ end
289
+ res
290
+ end
291
+
292
+ # Alters the conditions to include rectangular bounds conditions.
293
+ def apply_bounds_conditions(options,bounds)
294
+ sw,ne=bounds.sw,bounds.ne
295
+ lng_sql= bounds.crosses_meridian? ? "(#{qualified_lng_column_name}<#{sw.lng} OR #{qualified_lng_column_name}>#{ne.lng})" : "#{qualified_lng_column_name}>#{sw.lng} AND #{qualified_lng_column_name}<#{ne.lng}"
296
+ bounds_sql="#{qualified_lat_column_name}>#{sw.lat} AND #{qualified_lat_column_name}<#{ne.lat} AND #{lng_sql}"
297
+ options[:conditions]=augment_conditions(options[:conditions],bounds_sql)
298
+ end
299
+
300
+ # Extracts the origin instance out of the options if it exists and returns
301
+ # it. If there is no origin, looks for latitude and longitude values to
302
+ # create an origin. The side-effect of the method is to remove these
303
+ # option keys from the hash.
304
+ def extract_origin_from_options(options)
305
+ origin = options.delete(:origin)
306
+ res = normalize_point_to_lat_lng(origin) if origin
307
+ res
308
+ end
309
+
310
+ # Extract the units out of the options if it exists and returns it. If
311
+ # there is no :units key, it uses the default. The side effect of the
312
+ # method is to remove the :units key from the options hash.
313
+ def extract_units_from_options(options)
314
+ units = options[:units] || default_units
315
+ options.delete(:units)
316
+ units
317
+ end
318
+
319
+ # Extract the formula out of the options if it exists and returns it. If
320
+ # there is no :formula key, it uses the default. The side effect of the
321
+ # method is to remove the :formula key from the options hash.
322
+ def extract_formula_from_options(options)
323
+ formula = options[:formula] || default_formula
324
+ options.delete(:formula)
325
+ formula
326
+ end
327
+
328
+ def extract_bounds_from_options(options)
329
+ bounds = options.delete(:bounds)
330
+ bounds = GeoKit::Bounds.normalize(bounds) if bounds
331
+ end
332
+
333
+ # Geocode IP address.
334
+ def geocode_ip_address(origin)
335
+ geo_location = GeoKit::Geocoders::IpGeocoder.geocode(origin)
336
+ return geo_location if geo_location.success
337
+ raise GeoKit::Geocoders::GeocodeError
338
+ end
339
+
340
+
341
+ # Given a point in a variety of (an address to geocode,
342
+ # an array of [lat,lng], or an object with appropriate lat/lng methods, an IP addres)
343
+ # this method will normalize it into a GeoKit::LatLng instance. The only thing this
344
+ # method adds on top of LatLng#normalize is handling of IP addresses
345
+ def normalize_point_to_lat_lng(point)
346
+ res = geocode_ip_address(point) if point.is_a?(String) && /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(point)
347
+ res = GeoKit::LatLng.normalize(point) unless res
348
+ res
349
+ end
350
+
351
+ # Augments the select with the distance SQL.
352
+ def add_distance_to_select(options, origin, units=default_units, formula=default_formula)
353
+ if origin
354
+ distance_selector = distance_sql(origin, units, formula) + " AS #{distance_column_name}"
355
+ selector = options.has_key?(:select) && options[:select] ? options[:select] : "*"
356
+ options[:select] = "#{selector}, #{distance_selector}"
357
+ end
358
+ end
359
+
360
+ # Looks for the distance column and replaces it with the distance sql. If an origin was not
361
+ # passed in and the distance column exists, we leave it to be flagged as bad SQL by the database.
362
+ # Conditions are either a string or an array. In the case of an array, the first entry contains
363
+ # the condition.
364
+ def substitute_distance_in_conditions(options, origin, units=default_units, formula=default_formula)
365
+ original_conditions = options[:conditions]
366
+ condition = original_conditions.is_a?(String) ? original_conditions : original_conditions.first
367
+ pattern = Regexp.new("\s*#{distance_column_name}(\s<>=)*")
368
+ condition = condition.gsub(pattern, distance_sql(origin, units, formula))
369
+ original_conditions = condition if original_conditions.is_a?(String)
370
+ original_conditions[0] = condition if original_conditions.is_a?(Array)
371
+ options[:conditions] = original_conditions
372
+ end
373
+
374
+ # Returns the distance SQL using the spherical world formula (Haversine). The SQL is tuned
375
+ # to the database in use.
376
+ def sphere_distance_sql(origin, units)
377
+ lat = deg2rad(origin.lat)
378
+ lng = deg2rad(origin.lng)
379
+ multiplier = units_sphere_multiplier(units)
380
+ case connection.adapter_name.downcase
381
+ when "mysql"
382
+ sql=<<-SQL_END
383
+ (ACOS(least(1,COS(#{lat})*COS(#{lng})*COS(RADIANS(#{qualified_lat_column_name}))*COS(RADIANS(#{qualified_lng_column_name}))+
384
+ COS(#{lat})*SIN(#{lng})*COS(RADIANS(#{qualified_lat_column_name}))*SIN(RADIANS(#{qualified_lng_column_name}))+
385
+ SIN(#{lat})*SIN(RADIANS(#{qualified_lat_column_name}))))*#{multiplier})
386
+ SQL_END
387
+ when "postgresql"
388
+ sql=<<-SQL_END
389
+ (ACOS(least(1,COS(#{lat})*COS(#{lng})*COS(RADIANS(#{qualified_lat_column_name}))*COS(RADIANS(#{qualified_lng_column_name}))+
390
+ COS(#{lat})*SIN(#{lng})*COS(RADIANS(#{qualified_lat_column_name}))*SIN(RADIANS(#{qualified_lng_column_name}))+
391
+ SIN(#{lat})*SIN(RADIANS(#{qualified_lat_column_name}))))*#{multiplier})
392
+ SQL_END
393
+ else
394
+ sql = "unhandled #{connection.adapter_name.downcase} adapter"
395
+ end
396
+ end
397
+
398
+ # Returns the distance SQL using the flat-world formula (Phythagorean Theory). The SQL is tuned
399
+ # to the database in use.
400
+ def flat_distance_sql(origin, units)
401
+ lat_degree_units = units_per_latitude_degree(units)
402
+ lng_degree_units = units_per_longitude_degree(origin.lat, units)
403
+ case connection.adapter_name.downcase
404
+ when "mysql"
405
+ sql=<<-SQL_END
406
+ SQRT(POW(#{lat_degree_units}*(#{origin.lat}-#{qualified_lat_column_name}),2)+
407
+ POW(#{lng_degree_units}*(#{origin.lng}-#{qualified_lng_column_name}),2))
408
+ SQL_END
409
+ when "postgresql"
410
+ sql=<<-SQL_END
411
+ SQRT(POW(#{lat_degree_units}*(#{origin.lat}-#{qualified_lat_column_name}),2)+
412
+ POW(#{lng_degree_units}*(#{origin.lng}-#{qualified_lng_column_name}),2))
413
+ SQL_END
414
+ else
415
+ sql = "unhandled #{connection.adapter_name.downcase} adapter"
416
+ end
417
+ end
418
+ end
419
+ end
420
+ end
421
+ end
422
+
423
+ # Extend Array with a sort_by_distance method.
424
+ # This method creates a "distance" attribute on each object,
425
+ # calculates the distance from the passed origin,
426
+ # and finally sorts the array by the resulting distance.
427
+ class Array
428
+ def sort_by_distance_from(origin, opts={})
429
+ distance_attribute_name = opts.delete(:distance_attribute_name) || 'distance'
430
+ self.each do |e|
431
+ e.class.send(:attr_accessor, distance_attribute_name) if !e.respond_to? "#{distance_attribute_name}="
432
+ e.send("#{distance_attribute_name}=", origin.distance_to(e,opts))
433
+ end
434
+ self.sort!{|a,b|a.send(distance_attribute_name) <=> b.send(distance_attribute_name)}
435
+ end
436
+ end
@@ -0,0 +1,33 @@
1
+ namespace :geo do
2
+
3
+ desc "Look up an address using the failover"
4
+ task :multi do
5
+ puts GeoKit::Geocoders::MultiGeocoder.geocode(ENV["ADDR"])
6
+ end
7
+
8
+ desc "Look up an address on google"
9
+ task :google do
10
+ puts GeoKit::Geocoders::GoogleGeocoder.geocode(ENV["ADDR"])
11
+ end
12
+
13
+ desc "Look up an address on yahoo"
14
+ task :yahoo do
15
+ puts GeoKit::Geocoders::YahooGeocoder.geocode(ENV["ADDR"])
16
+ end
17
+
18
+ desc "Look up an address on us_geocoder"
19
+ task :us do
20
+ puts GeoKit::Geocoders::UsGeocoder.geocode(ENV["ADDR"])
21
+ end
22
+
23
+ desc "Look up an address on ca_geocoder"
24
+ task :ca do
25
+ puts GeoKit::Geocoders::CaGeocoder.geocode(ENV["ADDR"])
26
+ end
27
+
28
+ desc "Lookup up the address of an IP"
29
+ task :ip do
30
+ puts GeoKit::Geocoders::IpGeocoder.geocode(ENV["ADDR"])
31
+ end
32
+
33
+ end
@@ -191,11 +191,11 @@ module DataMapper
191
191
  RUBY
192
192
  end
193
193
 
194
- def property_to_column_name_with_distance(property, qualify)
194
+ def property_to_column_name_with_distance(property, qualify, qualifier = nil)
195
195
  if property.is_a?(DataMapper::Property) and property.type == DataMapper::Types::Distance
196
196
  property.field
197
197
  else
198
- property_to_column_name_without_distance(property, qualify)
198
+ property_to_column_name_without_distance(property, qualify, qualifier)
199
199
  end
200
200
  end
201
201
  end
@@ -222,16 +222,4 @@ module DataMapper
222
222
  end
223
223
  end
224
224
 
225
- module Aggregates
226
- module Model
227
- def size
228
- count
229
- end
230
- end
231
- module Collection
232
- def size
233
- loaded? ? super : count
234
- end
235
- end
236
- end
237
225
  end
@@ -0,0 +1,141 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe "dm-geokit" do
4
+ it "should add address fields after calling has_geographic_location" do
5
+ u = UninitializedLocation.new
6
+ u.should_not respond_to(:address)
7
+ DataMapper::GeoKit::PROPERTY_NAMES.each do |p|
8
+ u.should_not respond_to("address_#{p}".to_sym)
9
+ end
10
+ UninitializedLocation.send(:has_geographic_location, :address)
11
+ u = UninitializedLocation.new
12
+ u.should respond_to(:address)
13
+ DataMapper::GeoKit::PROPERTY_NAMES.each do |p|
14
+ u.should respond_to("address_#{p}".to_sym)
15
+ end
16
+ end
17
+
18
+ it "should set auto_geocode? method" do
19
+ Location.instance_methods.map{|i| i.to_sym}.should include(:auto_geocode?)
20
+ end
21
+
22
+ it "should set auto_geocode? method to true without option set" do
23
+ Location.new.send("auto_geocode?".to_sym).should be_true
24
+ end
25
+
26
+ it "should set auto_geocode? method to true on option" do
27
+ DefaultGeocodeLocation.new.send("auto_geocode?".to_sym).should be_true
28
+ end
29
+
30
+ it "should set auto_geocode? method to false on option" do
31
+ NoDefaultGeocodeLocation.new.send("auto_geocode?".to_sym).should be_false
32
+ end
33
+
34
+ it "should respond to acts_as_mappable" do
35
+ Location.should respond_to(:acts_as_mappable)
36
+ end
37
+
38
+ it "should have a geocode method" do
39
+ Location.should respond_to(:geocode)
40
+ end
41
+
42
+ it "should have the address field return a GeographicLocation object" do
43
+ l = Location.create(:address => "5119 NE 27th ave portland, or 97211")
44
+ l.address.should be_a(DataMapper::GeoKit::GeographicLocation)
45
+ DataMapper::GeoKit::PROPERTY_NAMES.each do |p|
46
+ l.address.should respond_to("#{p}".to_sym)
47
+ end
48
+ l.address.should respond_to(:distance)
49
+ end
50
+
51
+ it "should set address fields on geocode" do
52
+ l = Location.new
53
+ l.address.should be(nil)
54
+ DataMapper::GeoKit::PROPERTY_NAMES.each do |p|
55
+ l.send("address_#{p}").should be(nil)
56
+ end
57
+ l.address = '5119 NE 27th ave portland, or 97211'
58
+ DataMapper::GeoKit::PROPERTY_NAMES.each do |p|
59
+ l.send("address_#{p}").should_not be(nil)
60
+ end
61
+ end
62
+
63
+ it "should convert to LatLng" do
64
+ l = Location.create(:address => "5119 NE 27th ave portland, or 97211")
65
+ l.address.should respond_to(:to_lat_lng)
66
+ l.address.to_lat_lng.should be_a(::GeoKit::LatLng)
67
+ l.address.to_lat_lng.lat.should == l.address.lat
68
+ l.address.to_lat_lng.lng.should == l.address.lng
69
+ end
70
+
71
+ it "should find a location with LatLng Object" do
72
+ Location.all(:address.near => {:origin => ::GeoKit::LatLng.new(45.5767359,-122.670399), :distance => 3.mi}).size.should == 2
73
+ end
74
+
75
+ it "should find a location with a String" do
76
+ Location.all(:address.near => {:origin => 'portland, or', :distance => 10.mi}).size.should == 2
77
+ end
78
+
79
+ it "should find a location with LatLng Object in KM" do
80
+ Location.all(:address.near => {:origin => ::GeoKit::LatLng.new(45.5767359,-122.670399), :distance => 10.km}).size.should == 2
81
+ end
82
+
83
+ it "should find a location with a String in KM" do
84
+ Location.all(:address.near => {:origin => 'portland, or', :distance => 10.km}).size.should == 2
85
+ end
86
+
87
+ it "should respect other conditions (array)" do
88
+ Location.all(:conditions => ["id > 1000000000"], :address.near => {:origin => 'portland, or', :distance => 5.mi}).size.should == 0
89
+ end
90
+
91
+ it "should respect other conditions (hash)" do
92
+ Location.all(:conditions => {:id => 33}, :address.near => {:origin => 'portland, or', :distance => 5.mi}).size.should == 0
93
+ end
94
+
95
+ it "should respect other conditions (array with placeholders)" do
96
+ Location.all(:conditions => ["id = ?", 33], :address.near => {:origin => 'portland, or', :distance => 10.mi}).size.should == 0
97
+ end
98
+
99
+ it "should count locations" do
100
+ Location.count(:address.near => {:origin => 'portland, or', :distance => 5.mi}).should == 2
101
+ end
102
+
103
+ it "should include distance field and have a float value" do
104
+ Location.all(:address.near => {:origin => 'portland, or', :distance => 5.mi}).first.should respond_to(:address_distance)
105
+ Location.all(:address.near => {:origin => 'portland, or', :distance => 5.mi}).first.address_distance.should be_a(Float)
106
+ Location.all(:address.near => {:origin => 'portland, or', :distance => 5.mi}).first.address.should respond_to(:distance)
107
+ Location.all(:address.near => {:origin => 'portland, or', :distance => 5.mi}).first.address.distance.should be_a(Float)
108
+ end
109
+
110
+ it "should include distance field that changes with distance" do
111
+ Location.all(:address.near => {:origin => '97211', :distance => 5.mi}).first.address_distance.should_not == Location.all(:address.near => {:origin => 'portland, or', :distance => 5.mi}).first.address_distance
112
+ end
113
+
114
+ it "should order by distance desc" do
115
+ seattle = Location.create(:address => "Seattle, WA USA")
116
+ tacoma = Location.create(:address => "Tacoma, WA USA")
117
+ locations = Location.all(:address.near => {:origin => '97211', :distance => 500.mi}, :order => [:address_distance.desc])
118
+ locations.first.address_distance.should > locations.last.address_distance
119
+ end
120
+
121
+ it "should order by distance asc" do
122
+ locations = Location.all(:address.near => {:origin => '97211', :distance => 500.mi}, :order => [:address_distance.asc])
123
+ locations.first.address_distance.should < locations.last.address_distance
124
+ end
125
+
126
+ it "should filter on association search" do
127
+ Comment.create(:location_id => Location.first.id, :name => 'Example')
128
+ locations = Location.all(:address.near => {:origin => '97211', :distance => 500.mi}, :order => [:address_distance.asc], 'comments.name' => 'Example')
129
+ locations.size.should == 1
130
+ end
131
+
132
+ it "should find a location with LatLng Object using .first" do
133
+ Location.first(:address.near => {:origin => ::GeoKit::LatLng.new(45.5767359,-122.670399), :distance => 3.mi}).should be_a(Location)
134
+ end
135
+
136
+ it "should not geocode when auto_geocode is set to false" do
137
+ another = NoDefaultGeocodeLocation.create(:address => "San Francisco, CA USA")
138
+ another.address.should_not be_nil
139
+ end
140
+
141
+ end
@@ -0,0 +1,45 @@
1
+ $TESTING=true
2
+ $:.push File.join(File.dirname(__FILE__), '..', 'lib')
3
+ %w(dm-geokit).each{|l| require l}
4
+
5
+ DataMapper::Logger.new(STDOUT, :debug)
6
+ DataMapper.setup(:default, "mysql://root@localhost/dm_geokit_test")
7
+ GeoKit::Geocoders::google = 'ABQIAAAAdh4tQvHsPhXZm0lCnIiqQxQK9-uvPXgtXTy8QpRnjVVz0_XBmRQRzegmnZqycC7ewqw26GJSVik0_w'
8
+ class Location
9
+ include DataMapper::Resource
10
+ include DataMapper::GeoKit
11
+ property :id, Serial
12
+ has_geographic_location :address
13
+ has n, :comments
14
+ end
15
+
16
+ class Comment
17
+ include DataMapper::Resource
18
+ property :id, Serial
19
+ property :name, String
20
+ property :location_id, Integer
21
+ belongs_to :location
22
+ end
23
+
24
+ class UninitializedLocation
25
+ include DataMapper::Resource
26
+ include DataMapper::GeoKit
27
+ property :id, Serial
28
+ end
29
+
30
+ class NoDefaultGeocodeLocation
31
+ include DataMapper::Resource
32
+ include DataMapper::GeoKit
33
+ property :id, Serial
34
+ has_geographic_location :address, :auto_geocode => false
35
+ end
36
+
37
+ class DefaultGeocodeLocation
38
+ include DataMapper::Resource
39
+ include DataMapper::GeoKit
40
+ property :id, Serial
41
+ has_geographic_location :address, :auto_geocode => true
42
+ end
43
+
44
+ DataMapper.auto_migrate!
45
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dm-geokit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.1
4
+ version: 0.10.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt King
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-10-25 00:00:00 -07:00
12
+ date: 2010-01-15 00:00:00 -08:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -18,9 +18,9 @@ dependencies:
18
18
  version_requirement:
19
19
  version_requirements: !ruby/object:Gem::Requirement
20
20
  requirements:
21
- - - ">="
21
+ - - "="
22
22
  - !ruby/object:Gem::Version
23
- version: 0.10.1
23
+ version: 0.10.2
24
24
  version:
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: geokit
@@ -30,51 +30,42 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 1.5.0
33
+ version: "0"
34
34
  version:
35
- - !ruby/object:Gem::Dependency
36
- name: dm-aggregates
37
- type: :runtime
38
- version_requirement:
39
- version_requirements: !ruby/object:Gem::Requirement
40
- requirements:
41
- - - ">="
42
- - !ruby/object:Gem::Version
43
- version: 0.10.1
44
- version:
45
- description: Adds geographic functionality to DataMapper objects
35
+ description: Geographic Property support for DataMapper
46
36
  email: matt@mattking.org
47
37
  executables: []
48
38
 
49
39
  extensions: []
50
40
 
51
41
  extra_rdoc_files:
52
- - README
53
42
  - LICENSE
43
+ - README
54
44
  - TODO
55
45
  files:
56
46
  - LICENSE
57
- - Rakefile
58
47
  - README
48
+ - Rakefile
59
49
  - TODO
60
50
  - VERSION.yml
61
- - lib/dm-geokit
51
+ - lib/dm-geokit.rb
52
+ - lib/dm-geokit/acts_as_mappable.rb
62
53
  - lib/dm-geokit/ip_geocode_lookup.rb
54
+ - lib/dm-geokit/merbtasks.rb
63
55
  - lib/dm-geokit/resource.rb
64
- - lib/dm-geokit.rb
65
- - lib/skeleton
66
- - lib/skeleton/api_keys_template
67
- - lib/jeweler/templates/.gitignore
68
56
  - lib/dm-geokit/support/distance_measurement.rb
69
57
  - lib/dm-geokit/support/distance_support.rb
70
58
  - lib/dm-geokit/support/float.rb
71
59
  - lib/dm-geokit/support/integer.rb
72
60
  - lib/dm-geokit/support/symbol.rb
61
+ - lib/jeweler/templates/.gitignore
62
+ - lib/skeleton/api_keys_template
73
63
  has_rdoc: true
74
64
  homepage: http://github.com/mattking17/dm-geokit/tree/master
65
+ licenses: []
66
+
75
67
  post_install_message:
76
68
  rdoc_options:
77
- - --inline-source
78
69
  - --charset=UTF-8
79
70
  require_paths:
80
71
  - lib
@@ -93,9 +84,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
93
84
  requirements: []
94
85
 
95
86
  rubyforge_project: dm-geokit
96
- rubygems_version: 1.3.1
87
+ rubygems_version: 1.3.5
97
88
  signing_key:
98
89
  specification_version: 2
99
- summary: Adds geographic functionality to DataMapper objects, relying on the Geokit gem for geocoding and searching by geographic location.
100
- test_files: []
101
-
90
+ summary: DataMapper plugin for geokit
91
+ test_files:
92
+ - spec/dm_geokit_spec.rb
93
+ - spec/spec_helper.rb