module Geocoder module Calculations extend self ## # Compass point names, listed clockwise starting at North. # # If you want bearings named using more, fewer, or different points # override Geocoder::Calculations.COMPASS_POINTS with your own array. # COMPASS_POINTS = %w[N NE E SE S SW W NW] ## # Radius of the Earth, in kilometers. # Value taken from: http://en.wikipedia.org/wiki/Earth_radius # EARTH_RADIUS = 6371.0 ## # Conversion factor: multiply by kilometers to get miles. # KM_IN_MI = 0.621371192 ## # Distance spanned by one degree of latitude in the given units. # def latitude_degree_distance(units = :mi) 2 * Math::PI * earth_radius(units) / 360 end ## # Distance spanned by one degree of longitude at the given latitude. # This ranges from around 69 miles at the equator to zero at the poles. # def longitude_degree_distance(latitude, units = :mi) latitude_degree_distance(units) * Math.cos(to_radians(latitude)) end ## # Distance between two points on Earth (Haversine formula). # Takes two points and an options hash. # The points are given in the same way that points are given to all # Geocoder methods that accept points as arguments. They can be: # # * an array of coordinates ([lat,lon]) # * a geocodable address (string) # * a geocoded object (one which implements a +to_coordinates+ method # which returns a [lat,lon] array # # The options hash supports: # # * :units - :mi (default) or :km # def distance_between(point1, point2, options = {}, *args) if args.size > 0 warn "DEPRECATION WARNING: Instead of passing lat1/lon1/lat2/lon2 as separate arguments to the distance_between method, please pass two two-element arrays: [#{point1},#{point2}], [#{options}, #{args.first}]. The old argument format will not be supported in Geocoder v.1.0." point1 = [point1, point2] point2 = [options, args.shift] options = args.shift || {} end # set default options options[:units] ||= :mi # convert to coordinate arrays point1 = extract_coordinates(point1) point2 = extract_coordinates(point2) # convert degrees to radians point1 = to_radians(point1) point2 = to_radians(point2) # compute deltas dlat = point2[0] - point1[0] dlon = point2[1] - point1[1] a = (Math.sin(dlat / 2))**2 + Math.cos(point1[0]) * (Math.sin(dlon / 2))**2 * Math.cos(point2[0]) c = 2 * Math.atan2( Math.sqrt(a), Math.sqrt(1-a)) c * earth_radius(options[:units]) end ## # Bearing between two points on Earth. # Returns a number of degrees from due north (clockwise). # # See Geocoder::Calculations.distance_between for # ways of specifying the points. Also accepts an options hash: # # * :method - :linear (default) or :spherical; # the spherical method is "correct" in that it returns the shortest path # (one along a great circle) but the linear method is the default as it # is less confusing (returns due east or west when given two points with # the same latitude) # # Based on: http://www.movable-type.co.uk/scripts/latlong.html # def bearing_between(point1, point2, options = {}, *args) if args.size > 0 warn "DEPRECATION WARNING: Instead of passing lat1/lon1/lat2/lon2 as separate arguments to the bearing_between method, please pass two two-element arrays: [#{point1},#{point2}], [#{options}, #{args.first}]. The old argument format will not be supported in Geocoder v.1.0." point1 = [point1, point2] point2 = [options, args.shift] options = args.shift || {} end options[:method] = :linear unless options[:method] == :spherical # convert to coordinate arrays point1 = extract_coordinates(point1) point2 = extract_coordinates(point2) # convert degrees to radians point1 = to_radians(point1) point2 = to_radians(point2) # compute deltas dlat = point2[0] - point1[0] dlon = point2[1] - point1[1] case options[:method] when :linear y = dlon x = dlat when :spherical y = Math.sin(dlon) * Math.cos(point2[0]) x = Math.cos(point1[0]) * Math.sin(point2[0]) - Math.sin(point1[0]) * Math.cos(point2[0]) * Math.cos(dlon) end bearing = Math.atan2(x,y) # Answer is in radians counterclockwise from due east. # Convert to degrees clockwise from due north: (90 - to_degrees(bearing) + 360) % 360 end ## # Translate a bearing (float) into a compass direction (string, eg "North"). # def compass_point(bearing, points = COMPASS_POINTS) seg_size = 360 / points.size points[((bearing + (seg_size / 2)) % 360) / seg_size] end ## # Compute the geographic center (aka geographic midpoint, center of # gravity) for an array of geocoded objects and/or [lat,lon] arrays # (can be mixed). Any objects missing coordinates are ignored. Follows # the procedure documented at http://www.geomidpoint.com/calculation.html. # def geographic_center(points) # convert objects to [lat,lon] arrays and convert degrees to radians coords = points.map{ |p| to_radians(extract_coordinates(p)) } # convert to Cartesian coordinates x = []; y = []; z = [] coords.each do |p| x << Math.cos(p[0]) * Math.cos(p[1]) y << Math.cos(p[0]) * Math.sin(p[1]) z << Math.sin(p[0]) end # compute average coordinate values xa, ya, za = [x,y,z].map do |c| c.inject(0){ |tot,i| tot += i } / c.size.to_f end # convert back to latitude/longitude lon = Math.atan2(ya, xa) hyp = Math.sqrt(xa**2 + ya**2) lat = Math.atan2(za, hyp) # return answer in degrees to_degrees [lat, lon] end ## # Returns coordinates of the lower-left and upper-right corners of a box # with the given point at its center. The radius is the shortest distance # from the center point to any side of the box (the length of each side # is twice the radius). # # This is useful for finding corner points of a map viewport, or for # roughly limiting the possible solutions in a geo-spatial search # (ActiveRecord queries use it thusly). # # See Geocoder::Calculations.distance_between for # ways of specifying the point. Also accepts an options hash: # # * :units - :mi (default) or :km # def bounding_box(point, radius, options = {}, *args) if point.is_a?(Numeric) warn "DEPRECATION WARNING: Instead of passing latitude/longitude as separate arguments to the bounding_box method, please pass an array [#{point},#{radius}], a geocoded object, or a geocodable address (string). The old argument format will not be supported in Geocoder v.1.0." point = [point, radius] radius = options options = args.first || {} end lat,lon = extract_coordinates(point) radius = radius.to_f units = options[:units] || :mi [ lat - (radius / latitude_degree_distance(units)), lon - (radius / longitude_degree_distance(lat, units)), lat + (radius / latitude_degree_distance(units)), lon + (radius / longitude_degree_distance(lat, units)) ] end ## # Convert degrees to radians. # If an array (or multiple arguments) is passed, # converts each value and returns array. # def to_radians(*args) args = args.first if args.first.is_a?(Array) if args.size == 1 args.first * (Math::PI / 180) else args.map{ |i| to_radians(i) } end end ## # Convert radians to degrees. # If an array (or multiple arguments) is passed, # converts each value and returns array. # def to_degrees(*args) args = args.first if args.first.is_a?(Array) if args.size == 1 (args.first * 180.0) / Math::PI else args.map{ |i| to_degrees(i) } end end def distance_to_radians(distance, units = :mi) distance.to_f / earth_radius(units) end def radians_to_distance(radians, units = :mi) radians * earth_radius(units) end ## # Convert miles to kilometers. # def to_kilometers(mi) mi * mi_in_km end ## # Convert kilometers to miles. # def to_miles(km) km * km_in_mi end ## # Radius of the Earth in the given units (:mi or :km). Default is :mi. # def earth_radius(units = :mi) units == :km ? EARTH_RADIUS : to_miles(EARTH_RADIUS) end ## # Conversion factor: km to mi. # def km_in_mi KM_IN_MI end ## # Conversion factor: mi to km. # def mi_in_km 1.0 / KM_IN_MI end ## # Takes an object which is a [lat,lon] array, a geocodable string, # or an object that implements +to_coordinates+ and returns a # [lat,lon] array. Note that if a string is passed this may be a slow- # running method and may return nil. # def extract_coordinates(point) case point when Array; point when String; Geocoder.coordinates(point) else point.to_coordinates end end end end