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 +2 -1
- data/Rakefile +5 -4
- data/VERSION.yml +2 -2
- data/lib/dm-geokit/acts_as_mappable.rb +436 -0
- data/lib/dm-geokit/merbtasks.rb +33 -0
- data/lib/dm-geokit/resource.rb +2 -14
- data/spec/dm_geokit_spec.rb +141 -0
- data/spec/spec_helper.rb +45 -0
- metadata +20 -28
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
|
|
6
|
-
s.authors = [
|
|
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 = "
|
|
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
|
@@ -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
|
data/lib/dm-geokit/resource.rb
CHANGED
|
@@ -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
|
data/spec/spec_helper.rb
ADDED
|
@@ -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.
|
|
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:
|
|
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.
|
|
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:
|
|
33
|
+
version: "0"
|
|
34
34
|
version:
|
|
35
|
-
|
|
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.
|
|
87
|
+
rubygems_version: 1.3.5
|
|
97
88
|
signing_key:
|
|
98
89
|
specification_version: 2
|
|
99
|
-
summary:
|
|
100
|
-
test_files:
|
|
101
|
-
|
|
90
|
+
summary: DataMapper plugin for geokit
|
|
91
|
+
test_files:
|
|
92
|
+
- spec/dm_geokit_spec.rb
|
|
93
|
+
- spec/spec_helper.rb
|