1 require 'geo_kit/defaults'
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.
8 # At present, two forms of distance calculations are provided:
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.
13 # Distance units supported are :miles and :kms.
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
23 # Mix below class methods into the includer.
24 def self.included(receiver) # :nodoc:
25 receiver.extend ClassMethods
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
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)))
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)
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)
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))
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
77 end_lat=Math.asin(Math.sin(lat)*Math.cos(distance/radius) +
78 Math.cos(lat)*Math.sin(distance/radius)*Math.cos(heading))
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))
83 LatLng.new(rad2deg(end_lat),rad2deg(end_lng))
86 # Returns the midpoint, given two points. Returns a LatLng.
87 # Typically, the instance method will be used instead of this method.
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)
93 units = options[:units] || GeoKit::default_units
95 heading=from.heading_to(to)
96 distance=from.distance_to(to,options)
97 midpoint=from.endpoint(heading,distance/2,options)
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
110 degrees.to_f / 180.0 * Math::PI
114 rad.to_f * 180.0 / Math::PI
118 (rad2deg(rad)+360)%360
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
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
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
138 # -----------------------------------------------------------------------------------------------
139 # Instance methods below here
140 # -----------------------------------------------------------------------------------------------
142 # Extracts a LatLng instance. Use with models that are acts_as_mappable
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)
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)
156 alias distance_from distance_to
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)
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)
170 # Returns the endpoint, given a heading (in degrees) and distance.
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)
177 # Returns the midpoint, given another point on the map.
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)
189 attr_accessor :lat, :lng
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)
200 # Latitude attribute setter; stored as a float.
202 @lat = lat.to_f if lat
205 # Longitude attribute setter; stored as a float;
210 # Returns the lat and lng attributes as a comma-separated string.
215 #returns a string with comma-separated lat,lng values
220 #returns a two-element array
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.
227 other.is_a?(LatLng) ? self.lat == other.lat && self.lng == other.lng : false
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
243 if thing.is_a?(String)
245 if match=thing.match(/(\-?\d+\.?\d*)[, ] ?(\-?\d+\.?\d*)$/)
246 return GeoKit::LatLng.new(match[1],match[2])
248 res = GeoKit::Geocoders::MultiGeocoder.geocode(thing)
249 return res if res.success
250 raise GeoKit::Geocoders::GeocodeError
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
256 elsif thing.class.respond_to?(:acts_as_mappable) && thing.class.respond_to?(:distance_column_name)
257 return thing.to_lat_lng
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.")
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
281 # Constructor expects a hash of symbols to correspond with attributes.
283 @street_address=h[:street_address]
287 @country_code=h[:country_code]
290 super(h[:lat],h[:lng])
293 # Returns true if geocoded to the United States.
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.
302 @full_address ? @full_address : to_geocodeable_s
305 # Extracts the street number from the street address if the street address
308 street_address[/(\d*)/] if street_address
311 # Returns the street name portion of the street address.
313 street_address[street_number.length, street_address.length].strip if street_address
316 # gives you all the important fields as key-value pairs
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) }
324 # Sets the city after capitalizing each word within the city name.
326 @city = city.titleize if city
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
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.
337 a=[street_address, city, state, zip, country_code].compact
338 a.delete_if { |e| !e || e == '' }
342 # Returns a string representation of the instance.
344 "Provider: #{provider}\n Street: #{street_address}\nCity: #{city}\nState: #{state}\nZip: #{zip}\nLatitude: #{lat}\nLongitude: #{lng}\nCountry: #{country_code}\nSuccess: #{success}"
348 # Bounds represents a rectangular bounds, defined by the SW and NE corners
350 # sw and ne are LatLng objects
351 attr_accessor :sw, :ne
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))
359 #returns the a single point which is the center of the rectangular bounds
364 # a simple string representation:sw,ne
366 "#{@sw.to_s},#{@ne.to_s}"
369 # a two-element array of two-element arrays: sw,ne
374 # Returns true if the bounds contain the passed point.
375 # allows for bounds which cross the meridian
377 point=GeoKit::LatLng.normalize(point)
378 res = point.lat > @sw.lat && point.lat < @ne.lat
380 res &= point.lng < @ne.lng || point.lng > @sw.lng
382 res &= point.lng < @ne.lng && point.lng > @sw.lng
387 # returns true if the bounds crosses the international dateline
388 def crosses_meridian?
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.
395 other.is_a?(Bounds) ? self.sw == other.sw && self.ne == other.ne : false
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)
412 # Takes two main combinations of arguements to create a bounds:
413 # point,point (this is the only one which takes two arguments
415 # . . . where a point is anything LatLng#normalize can handle (which is quite a lot)
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
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
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))