diff --git a/README.md b/README.md index bbc44dc..96a8829 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ Crystal implementation of the [Haversine formula](https://en.wikipedia.org/wiki/ require "haversine" ``` +### Distance + Calling `Haversine.distance` with four latitude/longitude coordinates returns a `Haversine::Distance` object which can provide output in kilometers, meters, miles, feet, or nautical miles. Each "coordinates" member **must** be a pair of coordinates - `latitude` and `longitude`. @@ -79,6 +81,16 @@ distance2 = Haversine.distance(london, shanghai) distance1 < distance2 # => true ``` +### Destination + +Takes the starting point by `latitude`, `longitude` and calculates the location of a destination point +given a `distance` factor in degrees, radians, miles, or kilometers; and `bearing` in degrees. + +```crystal +Haversine.destination(39, -75, 5000, 90, :kilometers) +# => {26.440010707631124, -22.885355549364313} +``` + ## Contributing 1. Fork it () diff --git a/spec/haversine_spec.cr b/spec/haversine_spec.cr index 8313b02..29c13cf 100644 --- a/spec/haversine_spec.cr +++ b/spec/haversine_spec.cr @@ -31,4 +31,12 @@ describe Haversine do it { dist.to_feet.should eq(18275860.669896744) } end end + + describe ".destination" do + describe "calculates the location of a destination point" do + it { Haversine.destination(39, -75, 5000, 90, :kilometers).should eq({26.440010707631124, -22.885355549364313}) } + it { Haversine.destination([39, -75], 5000, 90, :kilometers).should eq({26.440010707631124, -22.885355549364313}) } + it { Haversine.destination({39, -75}, 5000, 90, :kilometers).should eq({26.440010707631124, -22.885355549364313}) } + end + end end diff --git a/src/haversine.cr b/src/haversine.cr index 9d3b4b1..15dd97b 100644 --- a/src/haversine.cr +++ b/src/haversine.cr @@ -1,5 +1,3 @@ -require "./haversine/*" - # The haversine formula determines the great-circle distance between two points on a sphere # given their latitudes and longitudes. # @@ -7,16 +5,42 @@ require "./haversine/*" module Haversine extend self - alias Number = Int32 | Float32 | Float64 + EARTH_RADIUS = 6371008.8 - RAD_PER_DEG = Math::PI / 180 + # Unit of measurement factors using a spherical (non-ellipsoid) earth radius. + # + # Keys are the name of the unit, values are the number of that unit in a single radians + FACTORS = { + centimeters: EARTH_RADIUS * 100, + centimetres: EARTH_RADIUS * 100, + degrees: 360 / (2 * Math::PI), + feet: EARTH_RADIUS * 3.28084, + inches: EARTH_RADIUS * 39.37, + kilometers: EARTH_RADIUS / 1000, + kilometres: EARTH_RADIUS / 1000, + meters: EARTH_RADIUS, + metres: EARTH_RADIUS, + miles: EARTH_RADIUS / 1609.344, + millimeters: EARTH_RADIUS * 1000, + millimetres: EARTH_RADIUS * 1000, + nautical_miles: EARTH_RADIUS / 1852, + radians: 1, + yards: EARTH_RADIUS * 1.0936, + } + + alias Number = Int32 | Float32 | Float64 # Calculates the haversine distance between two locations using latitude and longitude. def distance(lat1 : Number, lon1 : Number, lat2 : Number, lon2 : Number) : Haversine::Distance - dlon = lon2 - lon1 - dlat = lat2 - lat1 + dlon = to_radians(lon2 - lon1) + dlat = to_radians(lat2 - lat1) + lat1 = to_radians(lat1) + lat2 = to_radians(lat2) + + a = + Math.sin(dlat / 2) ** 2 + + (Math.sin(dlon / 2) ** 2) * Math.cos(lat1) * Math.cos(lat2) - a = calc(dlat, lat1, lat2, dlon) c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) Haversine::Distance.new(c) @@ -38,11 +62,55 @@ module Haversine distance(lat1, lon1, lat2, lon2) end - private def calc(dlat : Number, lat1 : Number, lat2 : Number, dlon : Number) : Number - (Math.sin(rpd(dlat) / 2)) ** 2 + Math.cos(rpd(lat1)) * Math.cos((rpd(lat2))) * (Math.sin(rpd(dlon) / 2)) ** 2 + # Takes the staring point by `latitude`, `longitude` and calculates the location of a destination point + # given a `distance` factor in `Haversine::FACTORS`; and `bearing` in degrees(ranging from -180 to 180). + # + # https://github.com/Turfjs/turf/blob/master/packages/turf-destination/index.ts + def destination(latitude : Number, longitude : Number, distance : Number, bearing : Number, unit : Symbol = :kilometers) : Tuple(Float64, Float64) + factor = FACTORS[unit] + + radians = distance / factor + bearing_rad = to_radians(bearing) + + latitude1 = to_radians(latitude) + longitude1 = to_radians(longitude) + + latitude2 = Math.asin( + Math.sin(latitude1) * Math.cos(radians) + + Math.cos(latitude1) * Math.sin(radians) * Math.cos(bearing_rad) + ) + + longitude2 = + longitude1 + + Math.atan2( + Math.sin(bearing_rad) * Math.sin(radians) * Math.cos(latitude1), + Math.cos(radians) - Math.sin(latitude1) * Math.sin(latitude2) + ) + + {to_degrees(latitude2), to_degrees(longitude2)} end - private def rpd(num : Number) : Number - num * RAD_PER_DEG + # :ditto: + def destination(coord : Array(Number), distance : Number, bearing : Number, unit : Symbol = :kilometers) : Tuple(Float64, Float64) + latitude, longitude = coord + + destination(latitude, longitude, distance, bearing, unit) + end + + # :ditto: + def destination(coord : Tuple(Number, Number), distance : Number, bearing : Number, unit : Symbol = :kilometers) : Tuple(Float64, Float64) + latitude, longitude = coord + + destination(latitude, longitude, distance, bearing, unit) + end + + private def to_radians(degrees : Number) : Number + degrees * Math::PI / 180.0 + end + + private def to_degrees(radians : Number) : Number + radians * 180.0 / Math::PI end end + +require "./haversine/*" diff --git a/src/haversine/distance.cr b/src/haversine/distance.cr index 27f868c..c01d168 100644 --- a/src/haversine/distance.cr +++ b/src/haversine/distance.cr @@ -2,37 +2,14 @@ module Haversine class Distance include Comparable(self) - EARTH_RADIUS = 6371008.8 - - # Unit of measurement factors using a spherical (non-ellipsoid) earth radius. - # - # Keys are the name of the unit, values are the number of that unit in a single radians - FACTORS = { - centimeters: EARTH_RADIUS * 100, - centimetres: EARTH_RADIUS * 100, - degrees: 360 / (2 * Math::PI), - feet: EARTH_RADIUS * 3.28084, - inches: EARTH_RADIUS * 39.37, - kilometers: EARTH_RADIUS / 1000, - kilometres: EARTH_RADIUS / 1000, - meters: EARTH_RADIUS, - metres: EARTH_RADIUS, - miles: EARTH_RADIUS / 1609.344, - millimeters: EARTH_RADIUS * 1000, - millimetres: EARTH_RADIUS * 1000, - nautical_miles: EARTH_RADIUS / 1852, - radians: 1, - yards: EARTH_RADIUS * 1.0936, - } - property distance def initialize(@distance : Number) end - {% for factor in FACTORS.keys %} + {% for factor in Haversine::FACTORS.keys %} def to_{{factor.id}} : Number - @distance * FACTORS[:{{factor.id}}] + @distance * Haversine::FACTORS[:{{factor.id}}] end {% end %}