]> projects.mako.cc - selectricity-live/blob - vendor/plugins/geokit/lib/geo_kit/mappable.rb
Close tables
[selectricity-live] / vendor / plugins / geokit / lib / geo_kit / mappable.rb
1 require 'geo_kit/defaults'
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 and :kms.
14   module Mappable
15     PI_DIV_RAD = 0.0174
16     KMS_PER_MILE = 1.609
17     EARTH_RADIUS_IN_MILES = 3963.19
18     EARTH_RADIUS_IN_KMS = EARTH_RADIUS_IN_MILES * KMS_PER_MILE
19     MILES_PER_LATITUDE_DEGREE = 69.1
20     KMS_PER_LATITUDE_DEGREE = MILES_PER_LATITUDE_DEGREE * KMS_PER_MILE
21     LATITUDE_DEGREES = EARTH_RADIUS_IN_MILES / MILES_PER_LATITUDE_DEGREE  
22     
23     # Mix below class methods into the includer.
24     def self.included(receiver) # :nodoc:
25       receiver.extend ClassMethods
26     end   
27     
28     module ClassMethods #:nodoc:
29       # Returns the distance between two points.  The from and to parameters are
30       # required to have lat and lng attributes.  Valid options are:
31       # :units - valid values are :miles or :kms (GeoKit::default_units is the default)
32       # :formula - valid values are :flat or :sphere (GeoKit::default_formula is the default)
33       def distance_between(from, to, options={})
34         from=GeoKit::LatLng.normalize(from)
35         to=GeoKit::LatLng.normalize(to)
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       throw 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       super(h[:lat],h[:lng])
291     end
292
293     # Returns true if geocoded to the United States.
294     def is_us?
295       country_code == 'US'
296     end
297
298     # full_address is provided by google but not by yahoo. It is intended that the google
299     # geocoding method will provide the full address, whereas for yahoo it will be derived
300     # from the parts of the address we do have.
301     def full_address
302       @full_address ? @full_address : to_geocodeable_s
303     end
304
305     # Extracts the street number from the street address if the street address
306     # has a value.
307     def street_number
308       street_address[/(\d*)/] if street_address
309     end
310
311     # Returns the street name portion of the street address.
312     def street_name
313        street_address[street_number.length, street_address.length].strip if street_address
314     end
315
316     # gives you all the important fields as key-value pairs
317     def hash
318       res={}
319       [: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) }
320       res
321     end
322     alias to_hash hash
323
324     # Sets the city after capitalizing each word within the city name.
325     def city=(city)
326       @city = city.titleize if city
327     end
328
329     # Sets the street address after capitalizing each word within the street address.
330     def street_address=(address)
331       @street_address = address.titleize if address
332     end  
333
334     # Returns a comma-delimited string consisting of the street address, city, state,
335     # zip, and country code.  Only includes those attributes that are non-blank.
336     def to_geocodeable_s
337       a=[street_address, city, state, zip, country_code].compact
338       a.delete_if { |e| !e || e == '' }
339       a.join(', ')      
340     end
341
342     # Returns a string representation of the instance.
343     def to_s
344       "Provider: #{provider}\n Street: #{street_address}\nCity: #{city}\nState: #{state}\nZip: #{zip}\nLatitude: #{lat}\nLongitude: #{lng}\nCountry: #{country_code}\nSuccess: #{success}"
345     end
346   end
347   
348   # Bounds represents a rectangular bounds, defined by the SW and NE corners
349   class Bounds
350     # sw and ne are LatLng objects
351     attr_accessor :sw, :ne
352     
353     # provide sw and ne to instantiate a new Bounds instance
354     def initialize(sw,ne)
355       raise ArguementError if !(sw.is_a?(GeoKit::LatLng) && ne.is_a?(GeoKit::LatLng))
356       @sw,@ne=sw,ne
357     end
358     
359     #returns the a single point which is the center of the rectangular bounds
360     def center
361       @sw.midpoint_to(@ne)
362     end
363   
364     # a simple string representation:sw,ne
365     def to_s
366       "#{@sw.to_s},#{@ne.to_s}"   
367     end
368     
369     # a two-element array of two-element arrays: sw,ne
370     def to_a
371       [@sw.to_a, @ne.to_a]
372     end
373     
374     # Returns true if the bounds contain the passed point.
375     # allows for bounds which cross the meridian
376     def contains?(point)
377       point=GeoKit::LatLng.normalize(point)
378       res = point.lat > @sw.lat && point.lat < @ne.lat
379       if crosses_meridian?
380         res &= point.lng < @ne.lng || point.lng > @sw.lng
381       else
382         res &= point.lng < @ne.lng && point.lng > @sw.lng
383       end
384       res
385     end
386     
387     # returns true if the bounds crosses the international dateline
388     def crosses_meridian?
389       @sw.lng > @ne.lng 
390     end
391
392     # Returns true if the candidate object is logically equal.  Logical equivalence
393     # is true if the lat and lng attributes are the same for both objects.
394     def ==(other)
395       other.is_a?(Bounds) ? self.sw == other.sw && self.ne == other.ne : false
396     end
397     
398     class <<self
399       
400       # returns an instance of bounds which completely encompases the given circle
401       def from_point_and_radius(point,radius,options={})
402         point=LatLng.normalize(point)
403         p0=point.endpoint(0,radius,options)
404         p90=point.endpoint(90,radius,options)
405         p180=point.endpoint(180,radius,options)
406         p270=point.endpoint(270,radius,options)
407         sw=GeoKit::LatLng.new(p180.lat,p270.lng)
408         ne=GeoKit::LatLng.new(p0.lat,p90.lng)
409         GeoKit::Bounds.new(sw,ne)
410       end
411       
412       # Takes two main combinations of arguements to create a bounds:
413       # point,point   (this is the only one which takes two arguments
414       # [point,point]
415       # . . . where a point is anything LatLng#normalize can handle (which is quite a lot)
416       #
417       # NOTE: everything combination is assumed to pass points in the order sw, ne
418       def normalize (thing,other=nil)   
419         # maybe this will be simple -- an actual bounds object is passed, and we can all go home
420         return thing if thing.is_a? Bounds
421         
422         # no? OK, if there's no "other," the thing better be a two-element array        
423         thing,other=thing if !other && thing.is_a?(Array) && thing.size==2
424
425         # Now that we're set with a thing and another thing, let LatLng do the heavy lifting.
426         # Exceptions may be thrown
427         Bounds.new(GeoKit::LatLng.normalize(thing),GeoKit::LatLng.normalize(other))
428       end
429     end
430   end
431 end

Benjamin Mako Hill || Want to submit a patch?