rpbertp13-dm-geokit 0.9.11

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Foy Savas
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,49 @@
1
+ dm-geokit
2
+ =========
3
+
4
+ A mixin for Datamapper models that enables geographic functionality.
5
+
6
+ * Search for content via DataMapper-style query methods, e.g. Location.all(:address.near => {:origin => 'Portland, OR', :distance => 5.mi})
7
+ * Sort by distance easily: Location.all(:address.near => {:origin => 'Portland, OR', :distance => 5.mi}, :order => [:address_distance.desc])
8
+ * Ability to specify multiple fields that are geocodable (mostly)
9
+
10
+ Usage
11
+ =====
12
+
13
+ Basic Class Definition:
14
+
15
+ class Location
16
+ include DataMapper::Resource
17
+ property :id, Serial
18
+ has_geographic_location :address
19
+ end
20
+
21
+ This will automatically generate fields and methods for use with the DM Object, prefixed with the field name specified.
22
+ Since the above example used the field :address, the following fields would be generated:
23
+
24
+ * address_street_address
25
+ * address_city
26
+ * address_state
27
+ * address_zip
28
+ * address_country_code
29
+ * address_full_address
30
+ * address_lat
31
+ * address_lng
32
+
33
+ You can either reference those fields directly, or use the proxy object returned by calling .address on your object:
34
+
35
+ l = Location.all(:address.near => {:origin => 'Portland, OR', :distance => 5.mi})
36
+
37
+ l.each do |loc|
38
+ puts loc.address # .to_s yields string representation of full address, e.g. "12345 My St. Portland, OR USA"
39
+ puts loc.address.inspect # the proxy object, GeographicLocation, with matching methods for each property
40
+ puts loc.address.street_address # getting the street_address from the proxy object
41
+ puts loc.address_street_address # directly access the SQL column
42
+ end
43
+
44
+ The GeographicLocation proxy object is a convenience to allow you to compare and sort results in Ruby.
45
+
46
+ Requirements
47
+ ===========
48
+
49
+ Requires the GeoKit gem.
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ begin
2
+ require 'jeweler'
3
+ Jeweler::Tasks.new do |s|
4
+ s.name = 'rpbertp13-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', 'Roberto Thais']
7
+ s.email = 'roberto@robertothais.com'
8
+ s.homepage = "http://github.com/rpbertp13/dm-geokit/tree/master"
9
+ s.description = "Simple and opinionated helper for creating Rubygem projects on GitHub"
10
+ s.files = FileList["[A-Z]*", "{bin,generators,lib,test}/**/*", 'lib/jeweler/templates/.gitignore']
11
+ s.require_path = 'lib'
12
+ s.has_rdoc = true
13
+ s.platform = Gem::Platform::RUBY
14
+ s.extra_rdoc_files = %w[ README LICENSE TODO ]
15
+ s.add_dependency 'dm-core'
16
+ s.add_dependency 'geokit'
17
+ end
18
+ rescue LoadError
19
+ 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
data/TODO ADDED
@@ -0,0 +1,8 @@
1
+ TODO
2
+ ====
3
+
4
+
5
+
6
+ ---
7
+ TODO tickets may also be found in the DataMapper Issue Tracker:
8
+ http://wm.lighthouseapp.com/projects/4819-datamapper/overview
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 9
4
+ :patch: 11
data/lib/dm-geokit.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'rubygems'
2
+ require 'geokit'
3
+ require 'dm-core'
4
+
5
+ %w(distance_measurement distance_support symbol integer float).each{|f|
6
+ require File.join(File.dirname(__FILE__),'dm-geokit','support',f)
7
+ }
8
+ require File.join(File.dirname(__FILE__),'dm-geokit','resource')
@@ -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,35 @@
1
+ require 'yaml'
2
+
3
+ module GeoKit
4
+ # Contains a class method geocode_ip_address which can be used to enable automatic geocoding
5
+ # for request IP addresses. The geocoded information is stored in a cookie and in the
6
+ # session to minimize web service calls. The point of the helper is to enable location-based
7
+ # websites to have a best-guess for new visitors.
8
+ module IpGeocodeLookup
9
+
10
+ private
11
+
12
+ # Places the IP address' geocode location into the session if it
13
+ # can be found. Otherwise, looks for a geo location cookie and
14
+ # uses that value. The last resort is to call the web service to
15
+ # get the value.
16
+ def store_ip_location
17
+ session[:geo_location] ||= retrieve_location_from_cookie_or_service
18
+ cookies[:geo_location] = { :value => session[:geo_location].to_yaml, :expires => 30.days.from_now } if session[:geo_location]
19
+ end
20
+
21
+ # Uses the stored location value from the cookie if it exists. If
22
+ # no cookie exists, calls out to the web service to get the location.
23
+ def retrieve_location_from_cookie_or_service
24
+ return YAML.load(cookies[:geo_location]) if cookies[:geo_location]
25
+ location = Geocoders::IpGeocoder.geocode(get_ip_address)
26
+ return location.success ? location : nil
27
+ end
28
+
29
+ # Returns the real ip address, though this could be the localhost ip
30
+ # address. No special handling here anymore.
31
+ def get_ip_address
32
+ request.remote_ip
33
+ end
34
+ end
35
+ 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
@@ -0,0 +1,169 @@
1
+ module DataMapper
2
+ module GeoKit
3
+ PROPERTY_NAMES = %w(lat lng street_address city state zip country_code full_address)
4
+
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ module ClassMethods
10
+ def has_geographic_location(name, options = {})
11
+ return if self.included_modules.include?(DataMapper::GeoKit::InstanceMethods)
12
+ send :include, InstanceMethods
13
+ send :include, ::GeoKit::Mappable
14
+
15
+ property name.to_sym, String, :size => 255
16
+ property "#{name}_distance".to_sym, Float
17
+
18
+ PROPERTY_NAMES.each do |p|
19
+ if p.match(/l(at|ng)/)
20
+ property "#{name}_#{p}".to_sym, Float, :precision => 15, :scale => 12, :index => true
21
+ else
22
+ property "#{name}_#{p}".to_sym, String, :size => 255
23
+ end
24
+ end
25
+
26
+ DataMapper.auto_upgrade!
27
+
28
+ define_method "#{name}" do
29
+ if(value = attribute_get(name.to_sym)).nil?
30
+ nil
31
+ else
32
+ GeographicLocation.new(name, self)
33
+ end
34
+ end
35
+
36
+ define_method "#{name}=" do |value|
37
+ if value.nil?
38
+ nil
39
+ else value.is_a?(String)
40
+ geo = ::GeoKit::Geocoders::MultiGeocoder.geocode(value)
41
+ if geo.success?
42
+ attribute_set(name.to_sym, geo.full_address)
43
+ PROPERTY_NAMES.each do |p|
44
+ attribute_set("#{name}_#{p}".to_sym, geo.send(p.to_sym))
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ alias acts_as_mappable has_geographic_location
51
+ end
52
+
53
+ module InstanceMethods
54
+ def self.included(base) # :nodoc:
55
+ base.extend SingletonMethods
56
+ end
57
+
58
+ module SingletonMethods # :nodoc:
59
+ def all(query = {})
60
+ super(prepare_query(query))
61
+ end
62
+
63
+ # Required dm-aggregates to work
64
+ def count(query = {})
65
+ super(prepare_query(query))
66
+ end
67
+
68
+ private
69
+
70
+ # Looks in the query for keys that are a DistanceOperator, then extracts the keys/values and turns them into conditions
71
+ def prepare_query(query)
72
+ query.each_pair do |k,v|
73
+ next if not k.is_a?(DistanceOperator)
74
+ field = k.target
75
+ origin = v[:origin].is_a?(String) ? ::GeoKit::Geocoders::MultiGeocoder.geocode(v[:origin]) : v[:origin]
76
+ distance = v[:distance]
77
+ query[:conditions] = expand_conditions(query[:conditions], "#{sphere_distance_sql(field, origin, distance.measurement)}", distance.to_f)
78
+ query[:fields] = expand_fields(query[:fields], field, "#{sphere_distance_sql(field, origin, distance.measurement)}")
79
+ query.delete(k)
80
+ end
81
+ query
82
+ end
83
+
84
+ # Spherical distance sql
85
+ def sphere_distance_sql(field, origin, units)
86
+ lat = deg2rad(origin.lat)
87
+ lng = deg2rad(origin.lng)
88
+ qualified_lat_column = "`#{storage_name}`.`#{field}_lat`"
89
+ qualified_lng_column = "`#{storage_name}`.`#{field}_lng`"
90
+ "(ACOS(least(1,COS(#{lat})*COS(#{lng})*COS(RADIANS(#{qualified_lat_column}))*COS(RADIANS(#{qualified_lng_column}))+COS(#{lat})*SIN(#{lng})*COS(RADIANS(#{qualified_lat_column}))*SIN(RADIANS(#{qualified_lng_column}))+SIN(#{lat})*SIN(RADIANS(#{qualified_lat_column}))))*#{units_sphere_multiplier(units)})"
91
+ end
92
+
93
+ # in case conditions were altered by other means
94
+ def expand_conditions(conditions, sql, value)
95
+ if conditions.is_a?(Hash)
96
+ [conditions.keys.inject(''){|m,k|
97
+ m << "#{k} = ?"
98
+ } << " AND #{sql} < ?"] + ([conditions.values] << value)
99
+ elsif conditions.is_a?(Array)
100
+ if conditions.size == 1
101
+ ["#{conditions[0]} AND #{sql} < ?", value]
102
+ else
103
+ conditions[0] = ["#{conditions[0]} AND #{sql} < ?"]
104
+ conditions << value
105
+ conditions
106
+ end
107
+ else
108
+ ["#{sql} < ?", value]
109
+ end
110
+ end
111
+
112
+ # Hack in the distance field by adding the :fields option to the query
113
+ def expand_fields(fields, distance_field, sql)
114
+ f = DataMapper::Property.new(self, "#{distance_field}_distance".to_sym, DataMapper::Types::Distance, :field => "#{sql} as #{distance_field}_distance")
115
+ if fields.is_a?(Array) # user specified fields, just tack this onto the end
116
+ fields + [f]
117
+ else # otherwise since we specify :fields, we have to add back in the original fields it would have selected
118
+ self.properties(repository.name).defaults + [f]
119
+ end
120
+ end
121
+
122
+ end
123
+ end
124
+
125
+ class GeographicLocation
126
+ attr_accessor :full_address, :lat, :lng, :street_address, :city, :state, :zip, :country_code, :distance
127
+ def initialize(field, obj)
128
+ PROPERTY_NAMES.each do |p|
129
+ instance_variable_set("@#{p}",obj.send("#{field}_#{p}"))
130
+ end
131
+ @distance = obj.send("#{field}_distance") if obj.respond_to?("#{field}_distance".to_sym)
132
+ end
133
+ def to_s
134
+ @full_address
135
+ end
136
+ def to_lat_lng
137
+ ::GeoKit::LatLng.new(@lat,@lng)
138
+ end
139
+ end
140
+
141
+ class DistanceOperator < DataMapper::Query::Operator
142
+ end
143
+
144
+ end
145
+
146
+ module Adapters
147
+ class DataObjectsAdapter
148
+ module SQL
149
+ alias old_property_to_column_name property_to_column_name
150
+
151
+ def property_to_column_name(repository, property, qualify)
152
+ if property.respond_to?(:type) and property.type == DataMapper::Types::Distance
153
+ property.field
154
+ else
155
+ old_property_to_column_name(repository, property, qualify)
156
+ end
157
+ end
158
+
159
+ end
160
+ include SQL
161
+ end
162
+ end
163
+
164
+ module Types
165
+ class Distance < DataMapper::Type
166
+ primitive Float
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,16 @@
1
+ class DistanceMeasurement
2
+ attr_accessor :measurement
3
+ def initialize(value,measurement)
4
+ @value = value.to_f
5
+ @measurement = measurement
6
+ end
7
+ def to_s
8
+ @value.to_s
9
+ end
10
+ def to_i
11
+ @value.to_i
12
+ end
13
+ def to_f
14
+ @value.to_f
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ module DistanceSupport
2
+
3
+ def mi
4
+ DistanceMeasurement.new(self, :miles)
5
+ end
6
+
7
+ def km
8
+ DistanceMeasurement.new(self, :kms)
9
+ end
10
+
11
+ end
@@ -0,0 +1 @@
1
+ Float.send(:include, DistanceSupport)
@@ -0,0 +1 @@
1
+ Integer.send(:include, DistanceSupport)
@@ -0,0 +1,11 @@
1
+ class Symbol
2
+
3
+ def near
4
+ DataMapper::GeoKit::DistanceOperator.new(self, :near)
5
+ end
6
+
7
+ def outside
8
+ DataMapper::GeoKit::DistanceOperator.new(self, :outside)
9
+ end
10
+
11
+ end
File without changes
@@ -0,0 +1,50 @@
1
+ # These defaults are used in GeoKit::Mappable.distance_to and in acts_as_mappable
2
+ GeoKit::default_units = :miles
3
+ GeoKit::default_formula = :sphere
4
+
5
+ # This is the timeout value in seconds to be used for calls to the geocoder web
6
+ # services. For no timeout at all, comment out the setting. The timeout unit
7
+ # is in seconds.
8
+ GeoKit::Geocoders::timeout = 3
9
+
10
+ # These settings are used if web service calls must be routed through a proxy.
11
+ # These setting can be nil if not needed, otherwise, addr and port must be
12
+ # filled in at a minimum. If the proxy requires authentication, the username
13
+ # and password can be provided as well.
14
+ GeoKit::Geocoders::proxy_addr = nil
15
+ GeoKit::Geocoders::proxy_port = nil
16
+ GeoKit::Geocoders::proxy_user = nil
17
+ GeoKit::Geocoders::proxy_pass = nil
18
+
19
+ # This is your yahoo application key for the Yahoo Geocoder.
20
+ # See http://developer.yahoo.com/faq/index.html#appid
21
+ # and http://developer.yahoo.com/maps/rest/V1/geocode.html
22
+ GeoKit::Geocoders::yahoo = 'REPLACE_WITH_YOUR_YAHOO_KEY'
23
+
24
+ # This is your Google Maps geocoder key.
25
+ # See http://www.google.com/apis/maps/signup.html
26
+ # and http://www.google.com/apis/maps/documentation/#Geocoding_Examples
27
+ GeoKit::Geocoders::google = 'REPLACE_WITH_YOUR_GOOGLE_KEY'
28
+
29
+ # This is your username and password for geocoder.us.
30
+ # To use the free service, the value can be set to nil or false. For
31
+ # usage tied to an account, the value should be set to username:password.
32
+ # See http://geocoder.us
33
+ # and http://geocoder.us/user/signup
34
+ GeoKit::Geocoders::geocoder_us = false
35
+
36
+ # This is your authorization key for geocoder.ca.
37
+ # To use the free service, the value can be set to nil or false. For
38
+ # usage tied to an account, set the value to the key obtained from
39
+ # Geocoder.ca.
40
+ # See http://geocoder.ca
41
+ # and http://geocoder.ca/?register=1
42
+ GeoKit::Geocoders::geocoder_ca = false
43
+
44
+ # This is the order in which the geocoders are called in a failover scenario
45
+ # If you only want to use a single geocoder, put a single symbol in the array.
46
+ # Valid symbols are :google, :yahoo, :us, and :ca.
47
+ # Be aware that there are Terms of Use restrictions on how you can use the
48
+ # various geocoders. Make sure you read up on relevant Terms of Use for each
49
+ # geocoder you are going to use.
50
+ GeoKit::Geocoders::provider_order = [:google,:us]
@@ -0,0 +1,116 @@
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 respond to acts_as_mappable" do
19
+ Location.should respond_to(:acts_as_mappable)
20
+ end
21
+
22
+ it "should have a geocode method" do
23
+ Location.should respond_to(:geocode)
24
+ end
25
+
26
+ it "should have the address field return a GeographicLocation object" do
27
+ l = Location.create(:address => "5119 NE 27th ave portland, or 97211")
28
+ l.address.should be_a(DataMapper::GeoKit::GeographicLocation)
29
+ DataMapper::GeoKit::PROPERTY_NAMES.each do |p|
30
+ l.address.should respond_to("#{p}".to_sym)
31
+ end
32
+ l.address.should respond_to(:distance)
33
+ end
34
+
35
+ it "should set address fields on geocode" do
36
+ l = Location.new
37
+ l.address.should be(nil)
38
+ DataMapper::GeoKit::PROPERTY_NAMES.each do |p|
39
+ l.send("address_#{p}").should be(nil)
40
+ end
41
+ l.address = '5119 NE 27th ave portland, or 97211'
42
+ DataMapper::GeoKit::PROPERTY_NAMES.each do |p|
43
+ l.send("address_#{p}").should_not be(nil)
44
+ end
45
+ end
46
+
47
+ it "should convert to LatLng" do
48
+ l = Location.create(:address => "5119 NE 27th ave portland, or 97211")
49
+ l.address.should respond_to(:to_lat_lng)
50
+ l.address.to_lat_lng.should be_a(::GeoKit::LatLng)
51
+ l.address.to_lat_lng.lat.should == l.address.lat
52
+ l.address.to_lat_lng.lng.should == l.address.lng
53
+ end
54
+
55
+ it "should find a location with LatLng Object" do
56
+ Location.all(:address.near => {:origin => ::GeoKit::LatLng.new(45.5767359,-122.670399), :distance => 3.mi}).size.should == 2
57
+ end
58
+
59
+ it "should find a location with a String" do
60
+ Location.all(:address.near => {:origin => 'portland, or', :distance => 3.mi}).size.should == 2
61
+ end
62
+
63
+ it "should find a location with LatLng Object in KM" do
64
+ Location.all(:address.near => {:origin => ::GeoKit::LatLng.new(45.5767359,-122.670399), :distance => 4.km}).size.should == 2
65
+ end
66
+
67
+ it "should find a location with a String in KM" do
68
+ Location.all(:address.near => {:origin => 'portland, or', :distance => 5.km}).size.should == 2
69
+ end
70
+
71
+ it "should respect other conditions (array)" do
72
+ Location.all(:conditions => ["id > 1000000000"], :address.near => {:origin => 'portland, or', :distance => 5.mi}).size.should == 0
73
+ end
74
+
75
+ it "should respect other conditions (hash)" do
76
+ Location.all(:conditions => {:id => 33}, :address.near => {:origin => 'portland, or', :distance => 5.mi}).size.should == 0
77
+ end
78
+
79
+ it "should respect other conditions (array with placeholders)" do
80
+ Location.all(:conditions => ["id = ?", 33], :address.near => {:origin => 'portland, or', :distance => 5.mi}).size.should == 0
81
+ end
82
+
83
+ it "should count locations" do
84
+ Location.count(:address.near => {:origin => 'portland, or', :distance => 5.mi}).should == 2
85
+ end
86
+
87
+ it "should include distance field and have a float value" do
88
+ Location.all(:address.near => {:origin => 'portland, or', :distance => 5.mi}).first.should respond_to(:address_distance)
89
+ Location.all(:address.near => {:origin => 'portland, or', :distance => 5.mi}).first.address_distance.should be_a(Float)
90
+ Location.all(:address.near => {:origin => 'portland, or', :distance => 5.mi}).first.address.should respond_to(:distance)
91
+ Location.all(:address.near => {:origin => 'portland, or', :distance => 5.mi}).first.address.distance.should be_a(Float)
92
+ end
93
+
94
+ it "should include distance field that changes with distance" do
95
+ 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
96
+ end
97
+
98
+ it "should order by distance desc" do
99
+ seattle = Location.create(:address => "Seattle, WA USA")
100
+ tacoma = Location.create(:address => "Tacoma, WA USA")
101
+ locations = Location.all(:address.near => {:origin => '97211', :distance => 500.mi}, :order => [:address_distance.desc])
102
+ locations.first.address_distance.should > locations.last.address_distance
103
+ end
104
+
105
+ it "should order by distance asc" do
106
+ locations = Location.all(:address.near => {:origin => '97211', :distance => 500.mi}, :order => [:address_distance.asc])
107
+ locations.first.address_distance.should < locations.last.address_distance
108
+ end
109
+
110
+ it "should filter on association search" do
111
+ Comment.create(:location_id => Location.first.id, :name => 'Example')
112
+ locations = Location.all(:address.near => {:origin => '97211', :distance => 500.mi}, :order => [:address_distance.asc], 'comments.name' => 'Example')
113
+ locations.size.should == 1
114
+ end
115
+
116
+ end
@@ -0,0 +1,31 @@
1
+ $TESTING=true
2
+ $:.push File.join(File.dirname(__FILE__), '..', 'lib')
3
+ %w(dm-geokit dm-aggregates).each{|l| require l}
4
+
5
+ DataMapper::Logger.new(STDOUT, :debug)
6
+ DataMapper.setup(:default, "mysql://root@localhost/dm_geokit_test")
7
+
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
+ DataMapper.auto_migrate!
31
+
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rpbertp13-dm-geokit
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.11
5
+ platform: ruby
6
+ authors:
7
+ - Foy Savas
8
+ - Daniel Higginbotham
9
+ - Matt King
10
+ - Roberto Thais
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+
15
+ date: 2009-10-24 00:00:00 -04:00
16
+ default_executable:
17
+ dependencies:
18
+ - !ruby/object:Gem::Dependency
19
+ name: dm-core
20
+ type: :runtime
21
+ version_requirement:
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: "0"
27
+ version:
28
+ - !ruby/object:Gem::Dependency
29
+ name: geokit
30
+ type: :runtime
31
+ version_requirement:
32
+ version_requirements: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: "0"
37
+ version:
38
+ description: Simple and opinionated helper for creating Rubygem projects on GitHub
39
+ email: roberto@robertothais.com
40
+ executables: []
41
+
42
+ extensions: []
43
+
44
+ extra_rdoc_files:
45
+ - LICENSE
46
+ - README
47
+ - TODO
48
+ files:
49
+ - LICENSE
50
+ - README
51
+ - Rakefile
52
+ - TODO
53
+ - VERSION.yml
54
+ - lib/dm-geokit.rb
55
+ - lib/dm-geokit/acts_as_mappable.rb
56
+ - lib/dm-geokit/ip_geocode_lookup.rb
57
+ - lib/dm-geokit/merbtasks.rb
58
+ - lib/dm-geokit/resource.rb
59
+ - lib/dm-geokit/support/distance_measurement.rb
60
+ - lib/dm-geokit/support/distance_support.rb
61
+ - lib/dm-geokit/support/float.rb
62
+ - lib/dm-geokit/support/integer.rb
63
+ - lib/dm-geokit/support/symbol.rb
64
+ - lib/jeweler/templates/.gitignore
65
+ - lib/skeleton/api_keys_template
66
+ has_rdoc: true
67
+ homepage: http://github.com/rpbertp13/dm-geokit/tree/master
68
+ licenses: []
69
+
70
+ post_install_message:
71
+ rdoc_options:
72
+ - --charset=UTF-8
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: "0"
80
+ version:
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: "0"
86
+ version:
87
+ requirements: []
88
+
89
+ rubyforge_project:
90
+ rubygems_version: 1.3.5
91
+ signing_key:
92
+ specification_version: 3
93
+ summary: DataMapper plugin for geokit stuff forked from Foy Savas's project. Now relies on the geokit gem rather than Foy's gem.
94
+ test_files:
95
+ - spec/dm_geokit_spec.rb
96
+ - spec/spec_helper.rb