rustial-engine 0.0.1

Framework-agnostic 2.5D map engine for rustial
Documentation
//! Map projection trait.

use crate::bounds::GeoBounds;
use crate::coord::{GeoCoord, WorldCoord};

/// A map projection that converts between geographic and projected coordinates.
///
/// Implementations must be thread-safe (`Send + Sync`) so they can be
/// shared across rendering and engine threads.
pub trait Projection: Send + Sync {
    /// Project a geographic coordinate to world space (meters).
    fn project(&self, geo: &GeoCoord) -> WorldCoord;

    /// Inverse-project world coordinates back to geographic.
    fn unproject(&self, world: &WorldCoord) -> GeoCoord;

    /// Local linear scale factor at the given geographic coordinate.
    ///
    /// Returns the ratio of projected distance to true geodesic distance
    /// at `geo`.  For a conformal projection like Web Mercator this is
    /// `1 / cos(lat)` (i.e. `sec(lat)`); for Equirectangular at the
    /// equator it is `1.0`.
    ///
    /// Useful for computing meters-per-pixel, line-width scaling, and
    /// LOD thresholds.
    ///
    /// The default implementation returns `1.0` (no distortion),
    /// which is correct only at the standard parallel.
    fn scale_factor(&self, _geo: &GeoCoord) -> f64 {
        1.0
    }

    /// The geographic bounding box of valid input for this projection.
    ///
    /// Coordinates outside this range may produce `NaN` or `Infinity`
    /// when projected.  For example, Web Mercator is valid only within
    /// approximately 85.06 degrees latitude.
    ///
    /// The default implementation returns the full geographic range
    /// (90 degrees lat, 180 degrees lon).
    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::*;

    /// Verify the trait is object-safe (can be used as `dyn Projection`).
    #[test]
    fn object_safety() {
        fn _accept(_p: &dyn Projection) {}
    }

    /// Verify a boxed trait object is Send + Sync.
    #[test]
    fn boxed_send_sync() {
        fn _assert_send_sync<T: Send + Sync>() {}
        _assert_send_sync::<Box<dyn Projection>>();
    }

    /// Default `scale_factor` returns 1.0.
    #[test]
    fn default_scale_factor() {
        struct Dummy;
        impl Projection for Dummy {
            fn project(&self, _geo: &GeoCoord) -> WorldCoord {
                WorldCoord::default()
            }
            fn unproject(&self, _world: &WorldCoord) -> GeoCoord {
                GeoCoord::default()
            }
        }
        let d = Dummy;
        assert!((d.scale_factor(&GeoCoord::from_lat_lon(45.0, 10.0)) - 1.0).abs() < f64::EPSILON);
    }

    /// Default `projection_bounds` covers the full globe.
    #[test]
    fn default_projection_bounds() {
        struct Dummy;
        impl Projection for Dummy {
            fn project(&self, _geo: &GeoCoord) -> WorldCoord {
                WorldCoord::default()
            }
            fn unproject(&self, _world: &WorldCoord) -> GeoCoord {
                GeoCoord::default()
            }
        }
        let bounds = Dummy.projection_bounds();
        assert!((bounds.sw().lat - (-90.0)).abs() < f64::EPSILON);
        assert!((bounds.ne().lat - 90.0).abs() < f64::EPSILON);
        assert!((bounds.sw().lon - (-180.0)).abs() < f64::EPSILON);
        assert!((bounds.ne().lon - 180.0).abs() < f64::EPSILON);
    }
}