RedEnginePress logo
RedEnginePress
AlgorithmsLanguagesPlaygroundAbout

Rhumbline

P
use std::f64::consts::PI;

const EARTH_RADIUS: f64 = 6371000.0;

pub fn rhumb_dist(lat1: f64, long1: f64, lat2: f64, long2: f64) -> f64 {
    let phi1 = lat1 * PI / 180.00;
    let phi2 = lat2 * PI / 180.00;
    let del_phi = phi2 - phi1;
    let mut del_lambda = (long2 - long1) * PI / 180.00;

    if del_lambda > PI {
        del_lambda -= 2.00 * PI;
    } else if del_lambda < -PI {
        del_lambda += 2.00 * PI;
    }

    let del_psi = ((phi2 / 2.00 + PI / 4.00).tan() / (phi1 / 2.00 + PI / 4.00).tan()).ln();
    let q = if del_psi.abs() > 1e-12 {
        del_phi / del_psi
    } else {
        phi1.cos()
    };

    (del_phi.powf(2.00) + (q * del_lambda).powf(2.00)).sqrt() * EARTH_RADIUS
}

pub fn rhumb_bearing(lat1: f64, long1: f64, lat2: f64, long2: f64) -> f64 {
    let phi1 = lat1 * PI / 180.00;
    let phi2 = lat2 * PI / 180.00;
    let mut del_lambda = (long2 - long1) * PI / 180.00;

    if del_lambda > PI {
        del_lambda -= 2.0 * PI;
    } else if del_lambda < -PI {
        del_lambda += 2.0 * PI;
    }

    let del_psi = ((phi2 / 2.00 + PI / 4.00).tan() / (phi1 / 2.00 + PI / 4.00).tan()).ln();
    let bearing = del_lambda.atan2(del_psi) * 180.0 / PI;
    (bearing + 360.00) % 360.00
}
pub fn rhumb_destination(lat: f64, long: f64, distance: f64, bearing: f64) -> (f64, f64) {
    let del = distance / EARTH_RADIUS;
    let phi1 = lat * PI / 180.00;
    let lambda1 = long * PI / 180.00;
    let theta = bearing * PI / 180.00;

    let del_phi = del * theta.cos();
    let phi2 = (phi1 + del_phi).clamp(-PI / 2.0, PI / 2.0);

    let del_psi = ((phi2 / 2.00 + PI / 4.00).tan() / (phi1 / 2.0 + PI / 4.0).tan()).ln();
    let q = if del_psi.abs() > 1e-12 {
        del_phi / del_psi
    } else {
        phi1.cos()
    };

    let del_lambda = del * theta.sin() / q;
    let lambda2 = lambda1 + del_lambda;

    (phi2 * 180.00 / PI, lambda2 * 180.00 / PI)
}

// TESTS
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_rhumb_distance() {
        let distance = rhumb_dist(28.5416, 77.2006, 28.5457, 77.1928);
        assert!(distance > 700.00 && distance < 1000.0);
    }

    #[test]
    fn test_rhumb_bearing() {
        let bearing = rhumb_bearing(28.5416, 77.2006, 28.5457, 77.1928);
        assert!((bearing - 300.0).abs() < 5.0);
    }

    #[test]
    fn test_rhumb_destination_point() {
        let (lat, lng) = rhumb_destination(28.5457, 77.1928, 1000.00, 305.0);
        assert!((lat - 28.550).abs() < 0.010);
        assert!((lng - 77.1851).abs() < 0.010);
    }
    // edge cases

    #[test]
    fn test_rhumb_distance_cross_antimeridian() {
        // Test when del_lambda > PI (line 12)
        let distance = rhumb_dist(0.0, 170.0, 0.0, -170.0);
        assert!(distance > 0.0);
    }

    #[test]
    fn test_rhumb_distance_cross_antimeridian_negative() {
        // Test when del_lambda < -PI (line 14)
        let distance = rhumb_dist(0.0, -170.0, 0.0, 170.0);
        assert!(distance > 0.0);
    }

    #[test]
    fn test_rhumb_distance_to_equator() {
        // Test when del_psi is near zero (line 21 - the else branch)
        let distance = rhumb_dist(0.0, 0.0, 0.0, 1.0);
        assert!(distance > 0.0);
    }
}