rustial-engine 0.0.1

Framework-agnostic 2.5D map engine for rustial
Documentation
//! Globe / geocentric projection.
//!
//! This projection maps WGS-84 geographic coordinates directly onto a 3D globe
//! centered at the origin. It is useful as the geometric foundation for globe
//! rendering and globe-aware camera helpers.

use crate::bounds::GeoBounds;
use crate::coord::{GeoCoord, WorldCoord};
use crate::ellipsoid::Ellipsoid;
use crate::projection::Projection;

/// Geocentric globe projection using the WGS-84 ellipsoid.
///
/// Output coordinates are Earth-centered Earth-fixed style Cartesian meters:
///
/// - `x`: intersects the equator at longitude 0
/// - `y`: intersects the equator at longitude 90E
/// - `z`: north pole axis
pub struct Globe;

impl Globe {
    /// Project geographic coordinates to globe-centered Cartesian meters.
    #[inline]
    pub fn project(geo: &GeoCoord) -> WorldCoord {
        <Self as Projection>::project(&Self, geo)
    }

    /// Inverse-project globe-centered Cartesian meters to geographic coordinates.
    #[inline]
    pub fn unproject(world: &WorldCoord) -> GeoCoord {
        <Self as Projection>::unproject(&Self, world)
    }

    /// Mean Earth radius used for simple scale queries.
    #[inline]
    pub fn radius() -> f64 {
        Ellipsoid::WGS84.a
    }
}

impl Projection for Globe {
    fn project(&self, geo: &GeoCoord) -> WorldCoord {
        let ellipsoid = Ellipsoid::WGS84;
        let lat = geo.lat.to_radians();
        let lon = geo.lon.to_radians();
        let (sin_lat, cos_lat) = lat.sin_cos();
        let (sin_lon, cos_lon) = lon.sin_cos();

        let a = ellipsoid.a;
        let e2 = ellipsoid.e2;
        let n = a / (1.0_f64 - e2 * sin_lat * sin_lat).sqrt();
        let alt = geo.alt;

        let x = (n + alt) * cos_lat * cos_lon;
        let y = (n + alt) * cos_lat * sin_lon;
        let z = (n * (1.0 - e2) + alt) * sin_lat;
        WorldCoord::new(x, y, z)
    }

    fn unproject(&self, world: &WorldCoord) -> GeoCoord {
        let ellipsoid = Ellipsoid::WGS84;
        let a = ellipsoid.a;
        let b = ellipsoid.b;
        let e2 = ellipsoid.e2;
        let ep2 = (a * a - b * b) / (b * b);

        let x = world.position.x;
        let y = world.position.y;
        let z = world.position.z;

        let lon = y.atan2(x);
        let p = (x * x + y * y).sqrt();

        if p <= 1e-12 {
            let lat = if z >= 0.0 {
                std::f64::consts::FRAC_PI_2
            } else {
                -std::f64::consts::FRAC_PI_2
            };
            let alt = z.abs() - b;
            return GeoCoord::new(lat.to_degrees(), 0.0, alt);
        }

        let theta = (z * a).atan2(p * b);
        let (sin_theta, cos_theta) = theta.sin_cos();
        let lat = (z + ep2 * b * sin_theta.powi(3)).atan2(p - e2 * a * cos_theta.powi(3));
        let sin_lat = lat.sin();
        let n = a / (1.0_f64 - e2 * sin_lat * sin_lat).sqrt();
        let alt = p / lat.cos() - n;

        GeoCoord::new(lat.to_degrees(), lon.to_degrees(), alt)
    }

    fn scale_factor(&self, _geo: &GeoCoord) -> f64 {
        1.0
    }

    fn projection_bounds(&self) -> GeoBounds {
        GeoBounds::new(
            GeoCoord::from_lat_lon(-90.0, -180.0),
            GeoCoord::from_lat_lon(90.0, 180.0),
        )
    }
}

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

    #[test]
    fn roundtrip_origin() {
        let geo = GeoCoord::from_lat_lon(0.0, 0.0);
        let world = Globe::project(&geo);
        let back = Globe::unproject(&world);
        assert!((back.lat - geo.lat).abs() < 1e-8);
        assert!((back.lon - geo.lon).abs() < 1e-8);
    }

    #[test]
    fn roundtrip_nonzero() {
        let geo = GeoCoord::new(51.1, 17.0, 1200.0);
        let world = Globe::project(&geo);
        let back = Globe::unproject(&world);
        assert!((back.lat - geo.lat).abs() < 1e-6);
        assert!((back.lon - geo.lon).abs() < 1e-6);
        assert!((back.alt - geo.alt).abs() < 1e-3);
    }

    #[test]
    fn equator_axes_match_expected_orientation() {
        let zero = Globe::project(&GeoCoord::from_lat_lon(0.0, 0.0));
        let east = Globe::project(&GeoCoord::from_lat_lon(0.0, 90.0));
        let north = Globe::project(&GeoCoord::from_lat_lon(90.0, 0.0));

        assert!(zero.position.x > 6_000_000.0);
        assert!(east.position.y > 6_000_000.0);
        assert!(north.position.z > 6_000_000.0);
    }
}