rustial_engine/math/projection.rs
1//! Map projection trait.
2
3use crate::bounds::GeoBounds;
4use crate::coord::{GeoCoord, WorldCoord};
5
6/// A map projection that converts between geographic and projected coordinates.
7///
8/// Implementations must be thread-safe (`Send + Sync`) so they can be
9/// shared across rendering and engine threads.
10pub trait Projection: Send + Sync {
11 /// Project a geographic coordinate to world space (meters).
12 fn project(&self, geo: &GeoCoord) -> WorldCoord;
13
14 /// Inverse-project world coordinates back to geographic.
15 fn unproject(&self, world: &WorldCoord) -> GeoCoord;
16
17 /// Local linear scale factor at the given geographic coordinate.
18 ///
19 /// Returns the ratio of projected distance to true geodesic distance
20 /// at `geo`. For a conformal projection like Web Mercator this is
21 /// `1 / cos(lat)` (i.e. `sec(lat)`); for Equirectangular at the
22 /// equator it is `1.0`.
23 ///
24 /// Useful for computing meters-per-pixel, line-width scaling, and
25 /// LOD thresholds.
26 ///
27 /// The default implementation returns `1.0` (no distortion),
28 /// which is correct only at the standard parallel.
29 fn scale_factor(&self, _geo: &GeoCoord) -> f64 {
30 1.0
31 }
32
33 /// The geographic bounding box of valid input for this projection.
34 ///
35 /// Coordinates outside this range may produce `NaN` or `Infinity`
36 /// when projected. For example, Web Mercator is valid only within
37 /// approximately 85.06 degrees latitude.
38 ///
39 /// The default implementation returns the full geographic range
40 /// (90 degrees lat, 180 degrees lon).
41 fn projection_bounds(&self) -> GeoBounds {
42 GeoBounds::new(
43 GeoCoord::from_lat_lon(-90.0, -180.0),
44 GeoCoord::from_lat_lon(90.0, 180.0),
45 )
46 }
47}
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52
53 /// Verify the trait is object-safe (can be used as `dyn Projection`).
54 #[test]
55 fn object_safety() {
56 fn _accept(_p: &dyn Projection) {}
57 }
58
59 /// Verify a boxed trait object is Send + Sync.
60 #[test]
61 fn boxed_send_sync() {
62 fn _assert_send_sync<T: Send + Sync>() {}
63 _assert_send_sync::<Box<dyn Projection>>();
64 }
65
66 /// Default `scale_factor` returns 1.0.
67 #[test]
68 fn default_scale_factor() {
69 struct Dummy;
70 impl Projection for Dummy {
71 fn project(&self, _geo: &GeoCoord) -> WorldCoord {
72 WorldCoord::default()
73 }
74 fn unproject(&self, _world: &WorldCoord) -> GeoCoord {
75 GeoCoord::default()
76 }
77 }
78 let d = Dummy;
79 assert!((d.scale_factor(&GeoCoord::from_lat_lon(45.0, 10.0)) - 1.0).abs() < f64::EPSILON);
80 }
81
82 /// Default `projection_bounds` covers the full globe.
83 #[test]
84 fn default_projection_bounds() {
85 struct Dummy;
86 impl Projection for Dummy {
87 fn project(&self, _geo: &GeoCoord) -> WorldCoord {
88 WorldCoord::default()
89 }
90 fn unproject(&self, _world: &WorldCoord) -> GeoCoord {
91 GeoCoord::default()
92 }
93 }
94 let bounds = Dummy.projection_bounds();
95 assert!((bounds.sw().lat - (-90.0)).abs() < f64::EPSILON);
96 assert!((bounds.ne().lat - 90.0).abs() < f64::EPSILON);
97 assert!((bounds.sw().lon - (-180.0)).abs() < f64::EPSILON);
98 assert!((bounds.ne().lon - 180.0).abs() < f64::EPSILON);
99 }
100}