Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

calculates the location of a destination point #6

Merged
merged 5 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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