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]
##
# Conversion factor: multiply by kilometers to get miles.
#
KM_IN_MI = 0.621371192
##
# Conversion factor: multiply by nautical miles to get miles.
#
KM_IN_NM = 0.539957
##
# Conversion factor: multiply by radians to get degrees.
#
DEGREES_PER_RADIAN = 57.2957795
##
# Radius of the Earth, in kilometers.
# Value taken from: http://en.wikipedia.org/wiki/Earth_radius
#
EARTH_RADII = {km: 6371.0}
EARTH_RADII[:mi] = EARTH_RADII[:km] * KM_IN_MI
EARTH_RADII[:nm] = EARTH_RADII[:km] * KM_IN_NM
EARTH_RADIUS = EARTH_RADII[:km] # TODO: deprecate this constant (use `EARTH_RADII[:km]`)
# Not a number constant
NAN = defined?(::Float::NAN) ? ::Float::NAN : 0 / 0.0
##
# Returns true if all given arguments are valid latitude/longitude values.
#
def coordinates_present?(*args)
args.each do |a|
# note that Float::NAN != Float::NAN
# still, this could probably be improved:
return false if (!a.is_a?(Numeric) or a.to_s == "NaN")
end
true
end
##
# Distance spanned by one degree of latitude in the given units.
#
def latitude_degree_distance(units = nil)
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 = nil)
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 or :km
# Use Geocoder.configure(:units => ...) to configure default units.
#
def distance_between(point1, point2, options = {})
# 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 or :spherical;
# the spherical method is "correct" in that it returns the shortest path
# (one along a great circle) but the linear method is less confusing
# (returns due east or west when given two points with the same latitude).
# Use Geocoder.configure(:distances => ...) to configure calculation method.
#
# Based on: http://www.movable-type.co.uk/scripts/latlong.html
#
def bearing_between(point1, point2, options = {})
# set default options
options[:method] ||= Geocoder.config.distances
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.0 / 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 southwest and northeast 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 or :km.
# Use Geocoder.configure(:units => ...) to configure default units.
#
def bounding_box(point, radius, options = {})
lat,lon = extract_coordinates(point)
radius = radius.to_f
[
lat - (radius / latitude_degree_distance(options[:units])),
lon - (radius / longitude_degree_distance(lat, options[:units])),
lat + (radius / latitude_degree_distance(options[:units])),
lon + (radius / longitude_degree_distance(lat, options[:units]))
]
end
##
# Random point within a circle of provided radius centered
# around the provided point
# Takes one point, one radius, 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 or :km
# Use Geocoder.configure(:units => ...) to configure default units.
# * :seed - The seed for the random number generator
def random_point_near(center, radius, options = {})
random = Random.new(options[:seed] || Random.new_seed)
# convert to coordinate arrays
center = extract_coordinates(center)
earth_circumference = 2 * Math::PI * earth_radius(options[:units])
max_degree_delta = 360.0 * (radius / earth_circumference)
# random bearing in radians
theta = 2 * Math::PI * random.rand
# random radius, use the square root to ensure a uniform
# distribution of points over the circle
r = Math.sqrt(random.rand) * max_degree_delta
delta_lat, delta_long = [r * Math.cos(theta), r * Math.sin(theta)]
[center[0] + delta_lat, center[1] + delta_long]
end
##
# Given a start point, heading (in degrees), and distance, provides
# an endpoint.
# The starting point is given in the same way that points are given to all
# Geocoder methods that accept points as arguments. It 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
#
def endpoint(start, heading, distance, options = {})
radius = earth_radius(options[:units])
start = extract_coordinates(start)
# convert degrees to radians
start = to_radians(start)
lat = start[0]
lon = start[1]
heading = to_radians(heading)
distance = distance.to_f
end_lat = Math.asin(Math.sin(lat)*Math.cos(distance/radius) +
Math.cos(lat)*Math.sin(distance/radius)*Math.cos(heading))
end_lon = lon+Math.atan2(Math.sin(heading)*Math.sin(distance/radius)*Math.cos(lat),
Math.cos(distance/radius)-Math.sin(lat)*Math.sin(end_lat))
to_degrees [end_lat, end_lon]
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 = nil)
distance.to_f / earth_radius(units)
end
def radians_to_distance(radians, units = nil)
radians * earth_radius(units)
end
##
# Convert miles to kilometers.
#
def to_kilometers(mi)
Geocoder.log(:warn, "DEPRECATION WARNING: Geocoder::Calculations.to_kilometers is deprecated and will be removed in Geocoder 1.5.0. Please multiply by MI_IN_KM instead.")
mi * mi_in_km
end
##
# Convert kilometers to miles.
#
def to_miles(km)
Geocoder.log(:warn, "DEPRECATION WARNING: Geocoder::Calculations.to_miles is deprecated and will be removed in Geocoder 1.5.0. Please multiply by KM_IN_MI instead.")
km * KM_IN_MI
end
##
# Convert kilometers to nautical miles.
#
def to_nautical_miles(km)
Geocoder.log(:warn, "DEPRECATION WARNING: Geocoder::Calculations.to_nautical_miles is deprecated and will be removed in Geocoder 1.5.0. Please multiply by KM_IN_NM instead.")
km * KM_IN_NM
end
##
# Radius of the Earth in the given units (:mi or :km).
# Use Geocoder.configure(:units => ...) to configure default units.
#
def earth_radius(units = nil)
EARTH_RADII[units || Geocoder.config.units]
end
##
# Conversion factor: km to mi.
#
def km_in_mi
Geocoder.log(:warn, "DEPRECATION WARNING: Geocoder::Calculations.km_in_mi is deprecated and will be removed in Geocoder 1.5.0. Please use the constant KM_IN_MI instead.")
KM_IN_MI
end
##
# Conversion factor: km to nm.
#
def km_in_nm
Geocoder.log(:warn, "DEPRECATION WARNING: Geocoder::Calculations.km_in_nm is deprecated and will be removed in Geocoder 1.5.0. Please use the constant KM_IN_NM instead.")
KM_IN_NM
end
##
# Conversion factor: mi to km.
#
def mi_in_km
Geocoder.log(:warn, "DEPRECATION WARNING: Geocoder::Calculations.mi_in_km is deprecated and will be removed in Geocoder 1.5.0. Please use 1.0 / KM_IN_MI instead.")
1.0 / KM_IN_MI
end
##
# Conversion factor: nm to km.
#
def nm_in_km
Geocoder.log(:warn, "DEPRECATION WARNING: Geocoder::Calculations.nm_in_km is deprecated and will be removed in Geocoder 1.5.0. Please use 1.0 / KM_IN_NM instead.")
1.0 / KM_IN_NM
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
if point.size == 2
lat, lon = point
if !lat.nil? && lat.respond_to?(:to_f) and
!lon.nil? && lon.respond_to?(:to_f)
then
return [ lat.to_f, lon.to_f ]
end
end
when String
point = Geocoder.coordinates(point) and return point
else
if point.respond_to?(:to_coordinates)
if Array === array = point.to_coordinates
return extract_coordinates(array)
end
end
end
[ NAN, NAN ]
end
end
end