andre-geokit 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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