///|
pub let mean_earth_radius_meters : Double = 6371008.8
///|
fn geographic_error(message : String) -> NmeaError {
NmeaError::from_diagnostic(
Diagnostic::new(GeographicInvalid, Error, message, SourceRef::sentence(0)),
)
}
///|
fn finite(value : Double) -> Bool {
!value.is_nan() && !value.is_inf()
}
///|
fn validate_position(
latitude : Double,
longitude : Double,
) -> Result[Unit, NmeaError] {
if !finite(latitude) || !finite(longitude) {
return Err(geographic_error("coordinate components must be finite"))
}
if latitude < -90.0 || latitude > 90.0 {
return Err(geographic_error("latitude must be between -90 and 90 degrees"))
}
if longitude < -180.0 || longitude > 180.0 {
return Err(
geographic_error("longitude must be between -180 and 180 degrees"),
)
}
Ok(())
}
///|
fn degrees_to_radians(value : Double) -> Double {
value * @math.PI / 180.0
}
///|
pub fn haversine_distance_meters(
latitude1 : Double,
longitude1 : Double,
latitude2 : Double,
longitude2 : Double,
) -> Result[Double, NmeaError] {
match validate_position(latitude1, longitude1) {
Err(error) => return Err(error)
Ok(_) => ()
}
match validate_position(latitude2, longitude2) {
Err(error) => return Err(error)
Ok(_) => ()
}
let phi1 = degrees_to_radians(latitude1)
let phi2 = degrees_to_radians(latitude2)
let delta_phi = degrees_to_radians(latitude2 - latitude1)
let delta_lambda = degrees_to_radians(longitude2 - longitude1)
let sin_phi = @math.sin(delta_phi / 2.0)
let sin_lambda = @math.sin(delta_lambda / 2.0)
let raw_a = sin_phi * sin_phi +
@math.cos(phi1) * @math.cos(phi2) * sin_lambda * sin_lambda
let a = if raw_a < 0.0 { 0.0 } else if raw_a > 1.0 { 1.0 } else { raw_a }
let central_angle = 2.0 * @math.atan2(a.sqrt(), (1.0 - a).sqrt())
Ok(mean_earth_radius_meters * central_angle)
}
///|
pub fn initial_bearing_degrees(
latitude1 : Double,
longitude1 : Double,
latitude2 : Double,
longitude2 : Double,
) -> Result[Double, NmeaError] {
match validate_position(latitude1, longitude1) {
Err(error) => return Err(error)
Ok(_) => ()
}
match validate_position(latitude2, longitude2) {
Err(error) => return Err(error)
Ok(_) => ()
}
let phi1 = degrees_to_radians(latitude1)
let phi2 = degrees_to_radians(latitude2)
let delta_lambda = degrees_to_radians(longitude2 - longitude1)
let y = @math.sin(delta_lambda) * @math.cos(phi2)
let x = @math.cos(phi1) * @math.sin(phi2) -
@math.sin(phi1) * @math.cos(phi2) * @math.cos(delta_lambda)
let raw = @math.atan2(y, x) * 180.0 / @math.PI
let normalized = raw % 360.0
Ok(if normalized < 0.0 { normalized + 360.0 } else { normalized })
}
///|
pub fn speed_meters_per_second(
distance_meters : Double,
elapsed_millis~ : Int64,
) -> Result[Double, NmeaError] {
if !finite(distance_meters) || distance_meters < 0.0 {
return Err(geographic_error("distance must be finite and non-negative"))
}
if elapsed_millis <= 0L {
return Err(geographic_error("elapsed duration must be positive"))
}
Ok(distance_meters * 1000.0 / elapsed_millis.to_double())
}