glebm-geokit 1.5.2

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