Skip to content

Commit

Permalink
Merge pull request #6 from geocrystal/destination
Browse files Browse the repository at this point in the history
calculates the location of a destination point
  • Loading branch information
mamantoha committed Apr 11, 2024
2 parents 160c7f5 + 39e8719 commit 8593842
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 36 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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 (<https://github.com/geocrystal/haversine/fork>)
Expand Down
8 changes: 8 additions & 0 deletions spec/haversine_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
90 changes: 79 additions & 11 deletions src/haversine.cr
Original file line number Diff line number Diff line change
@@ -1,22 +1,46 @@
require "./haversine/*"

# The haversine formula determines the great-circle distance between two points on a sphere
# given their latitudes and longitudes.
#
# https://en.wikipedia.org/wiki/Haversine_formula
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)
Expand All @@ -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/*"
27 changes: 2 additions & 25 deletions src/haversine/distance.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}

Expand Down

0 comments on commit 8593842

Please sign in to comment.