andre-geokit 1.1.0

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.
@@ -0,0 +1,30 @@
1
+ module Geokit
2
+ VERSION = '1.0.0'
3
+ # These defaults are used in Geokit::Mappable.distance_to and in acts_as_mappable
4
+ @@default_units = :miles
5
+ @@default_formula = :sphere
6
+
7
+ [:default_units, :default_formula].each do |sym|
8
+ class_eval <<-EOS, __FILE__, __LINE__
9
+ def self.#{sym}
10
+ if defined?(#{sym.to_s.upcase})
11
+ #{sym.to_s.upcase}
12
+ else
13
+ @@#{sym}
14
+ end
15
+ end
16
+
17
+ def self.#{sym}=(obj)
18
+ @@#{sym} = obj
19
+ end
20
+ EOS
21
+ end
22
+ end
23
+
24
+ require 'geocoders'
25
+ require 'mappable'
26
+
27
+ # make old-style module name "GeoKit" equivilent to new-style "Geokit"
28
+ module GeoKit
29
+ include Geokit
30
+ end
@@ -0,0 +1,432 @@
1
+
2
+ module Geokit
3
+ # Contains class and instance methods providing distance calcuation services. This
4
+ # module is meant to be mixed into classes containing lat and lng attributes where
5
+ # distance calculation is desired.
6
+ #
7
+ # At present, two forms of distance calculations are provided:
8
+ #
9
+ # * Pythagorean Theory (flat Earth) - which assumes the world is flat and loses accuracy over long distances.
10
+ # * Haversine (sphere) - which is fairly accurate, but at a performance cost.
11
+ #
12
+ # Distance units supported are :miles and :kms.
13
+ module Mappable
14
+ PI_DIV_RAD = 0.0174
15
+ KMS_PER_MILE = 1.609
16
+ EARTH_RADIUS_IN_MILES = 3963.19
17
+ EARTH_RADIUS_IN_KMS = EARTH_RADIUS_IN_MILES * KMS_PER_MILE
18
+ MILES_PER_LATITUDE_DEGREE = 69.1
19
+ KMS_PER_LATITUDE_DEGREE = MILES_PER_LATITUDE_DEGREE * KMS_PER_MILE
20
+ LATITUDE_DEGREES = EARTH_RADIUS_IN_MILES / MILES_PER_LATITUDE_DEGREE
21
+
22
+ # Mix below class methods into the includer.
23
+ def self.included(receiver) # :nodoc:
24
+ receiver.extend ClassMethods
25
+ end
26
+
27
+ module ClassMethods #:nodoc:
28
+ # Returns the distance between two points. The from and to parameters are
29
+ # required to have lat and lng attributes. Valid options are:
30
+ # :units - valid values are :miles or :kms (Geokit::default_units is the default)
31
+ # :formula - valid values are :flat or :sphere (Geokit::default_formula is the default)
32
+ def distance_between(from, to, options={})
33
+ from=Geokit::LatLng.normalize(from)
34
+ to=Geokit::LatLng.normalize(to)
35
+ return 0.0 if from == to # fixes a "zero-distance" bug
36
+ units = options[:units] || Geokit::default_units
37
+ formula = options[:formula] || Geokit::default_formula
38
+ case formula
39
+ when :sphere
40
+ units_sphere_multiplier(units) *
41
+ Math.acos( Math.sin(deg2rad(from.lat)) * Math.sin(deg2rad(to.lat)) +
42
+ Math.cos(deg2rad(from.lat)) * Math.cos(deg2rad(to.lat)) *
43
+ Math.cos(deg2rad(to.lng) - deg2rad(from.lng)))
44
+ when :flat
45
+ Math.sqrt((units_per_latitude_degree(units)*(from.lat-to.lat))**2 +
46
+ (units_per_longitude_degree(from.lat, units)*(from.lng-to.lng))**2)
47
+ end
48
+ end
49
+
50
+ # Returns heading in degrees (0 is north, 90 is east, 180 is south, etc)
51
+ # from the first point to the second point. Typicaly, the instance methods will be used
52
+ # instead of this method.
53
+ def heading_between(from,to)
54
+ from=Geokit::LatLng.normalize(from)
55
+ to=Geokit::LatLng.normalize(to)
56
+
57
+ d_lng=deg2rad(to.lng-from.lng)
58
+ from_lat=deg2rad(from.lat)
59
+ to_lat=deg2rad(to.lat)
60
+ y=Math.sin(d_lng) * Math.cos(to_lat)
61
+ x=Math.cos(from_lat)*Math.sin(to_lat)-Math.sin(from_lat)*Math.cos(to_lat)*Math.cos(d_lng)
62
+ heading=to_heading(Math.atan2(y,x))
63
+ end
64
+
65
+ # Given a start point, distance, and heading (in degrees), provides
66
+ # an endpoint. Returns a LatLng instance. Typically, the instance method
67
+ # will be used instead of this method.
68
+ def endpoint(start,heading, distance, options={})
69
+ units = options[:units] || Geokit::default_units
70
+ radius = units == :miles ? EARTH_RADIUS_IN_MILES : EARTH_RADIUS_IN_KMS
71
+ start=Geokit::LatLng.normalize(start)
72
+ lat=deg2rad(start.lat)
73
+ lng=deg2rad(start.lng)
74
+ heading=deg2rad(heading)
75
+ distance=distance.to_f
76
+
77
+ end_lat=Math.asin(Math.sin(lat)*Math.cos(distance/radius) +
78
+ Math.cos(lat)*Math.sin(distance/radius)*Math.cos(heading))
79
+
80
+ end_lng=lng+Math.atan2(Math.sin(heading)*Math.sin(distance/radius)*Math.cos(lat),
81
+ Math.cos(distance/radius)-Math.sin(lat)*Math.sin(end_lat))
82
+
83
+ LatLng.new(rad2deg(end_lat),rad2deg(end_lng))
84
+ end
85
+
86
+ # Returns the midpoint, given two points. Returns a LatLng.
87
+ # Typically, the instance method will be used instead of this method.
88
+ # Valid option:
89
+ # :units - valid values are :miles or :kms (:miles is the default)
90
+ def midpoint_between(from,to,options={})
91
+ from=Geokit::LatLng.normalize(from)
92
+
93
+ units = options[:units] || Geokit::default_units
94
+
95
+ heading=from.heading_to(to)
96
+ distance=from.distance_to(to,options)
97
+ midpoint=from.endpoint(heading,distance/2,options)
98
+ end
99
+
100
+ # Geocodes a location using the multi geocoder.
101
+ def geocode(location)
102
+ res = Geocoders::MultiGeocoder.geocode(location)
103
+ return res if res.success
104
+ raise Geokit::Geocoders::GeocodeError
105
+ end
106
+
107
+ protected
108
+
109
+ def deg2rad(degrees)
110
+ degrees.to_f / 180.0 * Math::PI
111
+ end
112
+
113
+ def rad2deg(rad)
114
+ rad.to_f * 180.0 / Math::PI
115
+ end
116
+
117
+ def to_heading(rad)
118
+ (rad2deg(rad)+360)%360
119
+ end
120
+
121
+ # Returns the multiplier used to obtain the correct distance units.
122
+ def units_sphere_multiplier(units)
123
+ units == :miles ? EARTH_RADIUS_IN_MILES : EARTH_RADIUS_IN_KMS
124
+ end
125
+
126
+ # Returns the number of units per latitude degree.
127
+ def units_per_latitude_degree(units)
128
+ units == :miles ? MILES_PER_LATITUDE_DEGREE : KMS_PER_LATITUDE_DEGREE
129
+ end
130
+
131
+ # Returns the number units per longitude degree.
132
+ def units_per_longitude_degree(lat, units)
133
+ miles_per_longitude_degree = (LATITUDE_DEGREES * Math.cos(lat * PI_DIV_RAD)).abs
134
+ units == :miles ? miles_per_longitude_degree : miles_per_longitude_degree * KMS_PER_MILE
135
+ end
136
+ end
137
+
138
+ # -----------------------------------------------------------------------------------------------
139
+ # Instance methods below here
140
+ # -----------------------------------------------------------------------------------------------
141
+
142
+ # Extracts a LatLng instance. Use with models that are acts_as_mappable
143
+ def to_lat_lng
144
+ return self if instance_of?(Geokit::LatLng) || instance_of?(Geokit::GeoLoc)
145
+ return LatLng.new(send(self.class.lat_column_name),send(self.class.lng_column_name)) if self.class.respond_to?(:acts_as_mappable)
146
+ return nil
147
+ end
148
+
149
+ # Returns the distance from another point. The other point parameter is
150
+ # required to have lat and lng attributes. Valid options are:
151
+ # :units - valid values are :miles or :kms (:miles is the default)
152
+ # :formula - valid values are :flat or :sphere (:sphere is the default)
153
+ def distance_to(other, options={})
154
+ self.class.distance_between(self, other, options)
155
+ end
156
+ alias distance_from distance_to
157
+
158
+ # Returns heading in degrees (0 is north, 90 is east, 180 is south, etc)
159
+ # to the given point. The given point can be a LatLng or a string to be Geocoded
160
+ def heading_to(other)
161
+ self.class.heading_between(self,other)
162
+ end
163
+
164
+ # Returns heading in degrees (0 is north, 90 is east, 180 is south, etc)
165
+ # FROM the given point. The given point can be a LatLng or a string to be Geocoded
166
+ def heading_from(other)
167
+ self.class.heading_between(other,self)
168
+ end
169
+
170
+ # Returns the endpoint, given a heading (in degrees) and distance.
171
+ # Valid option:
172
+ # :units - valid values are :miles or :kms (:miles is the default)
173
+ def endpoint(heading,distance,options={})
174
+ self.class.endpoint(self,heading,distance,options)
175
+ end
176
+
177
+ # Returns the midpoint, given another point on the map.
178
+ # Valid option:
179
+ # :units - valid values are :miles or :kms (:miles is the default)
180
+ def midpoint_to(other, options={})
181
+ self.class.midpoint_between(self,other,options)
182
+ end
183
+
184
+ end
185
+
186
+ class LatLng
187
+ include Mappable
188
+
189
+ attr_accessor :lat, :lng
190
+
191
+ # Accepts latitude and longitude or instantiates an empty instance
192
+ # if lat and lng are not provided. Converted to floats if provided
193
+ def initialize(lat=nil, lng=nil)
194
+ lat = lat.to_f if lat && !lat.is_a?(Numeric)
195
+ lng = lng.to_f if lng && !lng.is_a?(Numeric)
196
+ @lat = lat
197
+ @lng = lng
198
+ end
199
+
200
+ # Latitude attribute setter; stored as a float.
201
+ def lat=(lat)
202
+ @lat = lat.to_f if lat
203
+ end
204
+
205
+ # Longitude attribute setter; stored as a float;
206
+ def lng=(lng)
207
+ @lng=lng.to_f if lng
208
+ end
209
+
210
+ # Returns the lat and lng attributes as a comma-separated string.
211
+ def ll
212
+ "#{lat},#{lng}"
213
+ end
214
+
215
+ #returns a string with comma-separated lat,lng values
216
+ def to_s
217
+ ll
218
+ end
219
+
220
+ #returns a two-element array
221
+ def to_a
222
+ [lat,lng]
223
+ end
224
+ # Returns true if the candidate object is logically equal. Logical equivalence
225
+ # is true if the lat and lng attributes are the same for both objects.
226
+ def ==(other)
227
+ other.is_a?(LatLng) ? self.lat == other.lat && self.lng == other.lng : false
228
+ end
229
+
230
+ # A *class* method to take anything which can be inferred as a point and generate
231
+ # a LatLng from it. You should use this anything you're not sure what the input is,
232
+ # and want to deal with it as a LatLng if at all possible. Can take:
233
+ # 1) two arguments (lat,lng)
234
+ # 2) a string in the format "37.1234,-129.1234" or "37.1234 -129.1234"
235
+ # 3) a string which can be geocoded on the fly
236
+ # 4) an array in the format [37.1234,-129.1234]
237
+ # 5) a LatLng or GeoLoc (which is just passed through as-is)
238
+ # 6) anything which acts_as_mappable -- a LatLng will be extracted from it
239
+ def self.normalize(thing,other=nil)
240
+ # if an 'other' thing is supplied, normalize the input by creating an array of two elements
241
+ thing=[thing,other] if other
242
+
243
+ if thing.is_a?(String)
244
+ thing.strip!
245
+ if match=thing.match(/(\-?\d+\.?\d*)[, ] ?(\-?\d+\.?\d*)$/)
246
+ return Geokit::LatLng.new(match[1],match[2])
247
+ else
248
+ res = Geokit::Geocoders::MultiGeocoder.geocode(thing)
249
+ return res if res.success
250
+ raise Geokit::Geocoders::GeocodeError
251
+ end
252
+ elsif thing.is_a?(Array) && thing.size==2
253
+ return Geokit::LatLng.new(thing[0],thing[1])
254
+ elsif thing.is_a?(LatLng) # will also be true for GeoLocs
255
+ return thing
256
+ elsif thing.class.respond_to?(:acts_as_mappable) && thing.class.respond_to?(:distance_column_name)
257
+ return thing.to_lat_lng
258
+ end
259
+
260
+ raise ArgumentError.new("#{thing} (#{thing.class}) cannot be normalized to a LatLng. We tried interpreting it as an array, string, Mappable, etc., but no dice.")
261
+ end
262
+
263
+ end
264
+
265
+ # This class encapsulates the result of a geocoding call
266
+ # It's primary purpose is to homogenize the results of multiple
267
+ # geocoding providers. It also provides some additional functionality, such as
268
+ # the "full address" method for geocoders that do not provide a
269
+ # full address in their results (for example, Yahoo), and the "is_us" method.
270
+ class GeoLoc < LatLng
271
+ # Location attributes. Full address is a concatenation of all values. For example:
272
+ # 100 Spear St, San Francisco, CA, 94101, US
273
+ attr_accessor :street_address, :city, :state, :zip, :country_code, :full_address
274
+ # Attributes set upon return from geocoding. Success will be true for successful
275
+ # geocode lookups. The provider will be set to the name of the providing geocoder.
276
+ # Finally, precision is an indicator of the accuracy of the geocoding.
277
+ attr_accessor :success, :provider, :precision
278
+ # Street number and street name are extracted from the street address attribute.
279
+ attr_reader :street_number, :street_name
280
+
281
+ # Constructor expects a hash of symbols to correspond with attributes.
282
+ def initialize(h={})
283
+ @street_address=h[:street_address]
284
+ @city=h[:city]
285
+ @state=h[:state]
286
+ @zip=h[:zip]
287
+ @country_code=h[:country_code]
288
+ @success=false
289
+ @precision='unknown'
290
+ @full_address=nil
291
+ super(h[:lat],h[:lng])
292
+ end
293
+
294
+ # Returns true if geocoded to the United States.
295
+ def is_us?
296
+ country_code == 'US'
297
+ end
298
+
299
+ # full_address is provided by google but not by yahoo. It is intended that the google
300
+ # geocoding method will provide the full address, whereas for yahoo it will be derived
301
+ # from the parts of the address we do have.
302
+ def full_address
303
+ @full_address ? @full_address : to_geocodeable_s
304
+ end
305
+
306
+ # Extracts the street number from the street address if the street address
307
+ # has a value.
308
+ def street_number
309
+ street_address[/(\d*)/] if street_address
310
+ end
311
+
312
+ # Returns the street name portion of the street address.
313
+ def street_name
314
+ street_address[street_number.length, street_address.length].strip if street_address
315
+ end
316
+
317
+ # gives you all the important fields as key-value pairs
318
+ def hash
319
+ res={}
320
+ [:success,:lat,:lng,:country_code,:city,:state,:zip,:street_address,:provider,:full_address,:is_us?,:ll,:precision].each { |s| res[s] = self.send(s.to_s) }
321
+ res
322
+ end
323
+ alias to_hash hash
324
+
325
+ # Sets the city after capitalizing each word within the city name.
326
+ def city=(city)
327
+ @city = Geokit::Inflector::titleize(city) if city
328
+ end
329
+
330
+ # Sets the street address after capitalizing each word within the street address.
331
+ def street_address=(address)
332
+ @street_address = Geokit::Inflector::titleize(address) if address
333
+ end
334
+
335
+ # Returns a comma-delimited string consisting of the street address, city, state,
336
+ # zip, and country code. Only includes those attributes that are non-blank.
337
+ def to_geocodeable_s
338
+ a=[street_address, city, state, zip, country_code].compact
339
+ a.delete_if { |e| !e || e == '' }
340
+ a.join(', ')
341
+ end
342
+
343
+ # Returns a string representation of the instance.
344
+ def to_s
345
+ "Provider: #{provider}\n Street: #{street_address}\nCity: #{city}\nState: #{state}\nZip: #{zip}\nLatitude: #{lat}\nLongitude: #{lng}\nCountry: #{country_code}\nSuccess: #{success}"
346
+ end
347
+ end
348
+
349
+ # Bounds represents a rectangular bounds, defined by the SW and NE corners
350
+ class Bounds
351
+ # sw and ne are LatLng objects
352
+ attr_accessor :sw, :ne
353
+
354
+ # provide sw and ne to instantiate a new Bounds instance
355
+ def initialize(sw,ne)
356
+ raise ArguementError if !(sw.is_a?(Geokit::LatLng) && ne.is_a?(Geokit::LatLng))
357
+ @sw,@ne=sw,ne
358
+ end
359
+
360
+ #returns the a single point which is the center of the rectangular bounds
361
+ def center
362
+ @sw.midpoint_to(@ne)
363
+ end
364
+
365
+ # a simple string representation:sw,ne
366
+ def to_s
367
+ "#{@sw.to_s},#{@ne.to_s}"
368
+ end
369
+
370
+ # a two-element array of two-element arrays: sw,ne
371
+ def to_a
372
+ [@sw.to_a, @ne.to_a]
373
+ end
374
+
375
+ # Returns true if the bounds contain the passed point.
376
+ # allows for bounds which cross the meridian
377
+ def contains?(point)
378
+ point=Geokit::LatLng.normalize(point)
379
+ res = point.lat > @sw.lat && point.lat < @ne.lat
380
+ if crosses_meridian?
381
+ res &= point.lng < @ne.lng || point.lng > @sw.lng
382
+ else
383
+ res &= point.lng < @ne.lng && point.lng > @sw.lng
384
+ end
385
+ res
386
+ end
387
+
388
+ # returns true if the bounds crosses the international dateline
389
+ def crosses_meridian?
390
+ @sw.lng > @ne.lng
391
+ end
392
+
393
+ # Returns true if the candidate object is logically equal. Logical equivalence
394
+ # is true if the lat and lng attributes are the same for both objects.
395
+ def ==(other)
396
+ other.is_a?(Bounds) ? self.sw == other.sw && self.ne == other.ne : false
397
+ end
398
+
399
+ class <<self
400
+
401
+ # returns an instance of bounds which completely encompases the given circle
402
+ def from_point_and_radius(point,radius,options={})
403
+ point=LatLng.normalize(point)
404
+ p0=point.endpoint(0,radius,options)
405
+ p90=point.endpoint(90,radius,options)
406
+ p180=point.endpoint(180,radius,options)
407
+ p270=point.endpoint(270,radius,options)
408
+ sw=Geokit::LatLng.new(p180.lat,p270.lng)
409
+ ne=Geokit::LatLng.new(p0.lat,p90.lng)
410
+ Geokit::Bounds.new(sw,ne)
411
+ end
412
+
413
+ # Takes two main combinations of arguements to create a bounds:
414
+ # point,point (this is the only one which takes two arguments
415
+ # [point,point]
416
+ # . . . where a point is anything LatLng#normalize can handle (which is quite a lot)
417
+ #
418
+ # NOTE: everything combination is assumed to pass points in the order sw, ne
419
+ def normalize (thing,other=nil)
420
+ # maybe this will be simple -- an actual bounds object is passed, and we can all go home
421
+ return thing if thing.is_a? Bounds
422
+
423
+ # no? OK, if there's no "other," the thing better be a two-element array
424
+ thing,other=thing if !other && thing.is_a?(Array) && thing.size==2
425
+
426
+ # Now that we're set with a thing and another thing, let LatLng do the heavy lifting.
427
+ # Exceptions may be thrown
428
+ Bounds.new(Geokit::LatLng.normalize(thing),Geokit::LatLng.normalize(other))
429
+ end
430
+ end
431
+ end
432
+ end