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