rustial_engine/math/
equirectangular.rs1use crate::coord::{GeoCoord, WorldCoord};
35use crate::ellipsoid::Ellipsoid;
36use crate::projection::Projection;
37
38pub struct Equirectangular;
44
45impl Projection for Equirectangular {
46 fn project(&self, geo: &GeoCoord) -> WorldCoord {
50 let a = Ellipsoid::WGS84.a;
51 WorldCoord::new(a * geo.lon.to_radians(), a * geo.lat.to_radians(), geo.alt)
52 }
53
54 fn unproject(&self, world: &WorldCoord) -> GeoCoord {
58 let a = Ellipsoid::WGS84.a;
59 let lat = (world.position.y / a).to_degrees().clamp(-90.0, 90.0);
60 let lon = ((world.position.x / a).to_degrees() + 180.0).rem_euclid(360.0) - 180.0;
61 GeoCoord::new(lat, lon, world.position.z)
62 }
63
64 fn scale_factor(&self, geo: &GeoCoord) -> f64 {
71 1.0 / geo.lat.to_radians().cos()
72 }
73
74 }
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
85 fn roundtrip_origin() {
86 let geo = GeoCoord::from_lat_lon(0.0, 0.0);
87 let world = Equirectangular.project(&geo);
88 let back = Equirectangular.unproject(&world);
89 assert!((back.lat - geo.lat).abs() < 1e-10);
90 assert!((back.lon - geo.lon).abs() < 1e-10);
91 }
92
93 #[test]
94 fn roundtrip_nonzero() {
95 let geo = GeoCoord::from_lat_lon(45.0, 90.0);
96 let world = Equirectangular.project(&geo);
97 let back = Equirectangular.unproject(&world);
98 assert!((back.lat - geo.lat).abs() < 1e-8);
99 assert!((back.lon - geo.lon).abs() < 1e-8);
100 }
101
102 #[test]
103 fn poles_roundtrip() {
104 let north = GeoCoord::from_lat_lon(90.0, 0.0);
105 let north_w = Equirectangular.project(&north);
106 let north_back = Equirectangular.unproject(&north_w);
107 assert!((north_back.lat - 90.0).abs() < 1e-9);
108
109 let south = GeoCoord::from_lat_lon(-90.0, 0.0);
110 let south_w = Equirectangular.project(&south);
111 let south_back = Equirectangular.unproject(&south_w);
112 assert!((south_back.lat + 90.0).abs() < 1e-9);
113 }
114
115 #[test]
116 fn anti_meridian_roundtrip() {
117 let east = GeoCoord::from_lat_lon(10.0, 180.0);
120 let east_back = Equirectangular.unproject(&Equirectangular.project(&east));
121 assert!((east_back.lon.abs() - 180.0).abs() < 1e-9);
122
123 let west = GeoCoord::from_lat_lon(10.0, -180.0);
124 let west_back = Equirectangular.unproject(&Equirectangular.project(&west));
125 assert!((west_back.lon.abs() - 180.0).abs() < 1e-9);
126 }
127
128 #[test]
129 fn extreme_latitudes_stable() {
130 for lat in [89.999, -89.999, 89.9, -89.9] {
131 let geo = GeoCoord::from_lat_lon(lat, 45.0);
132 let world = Equirectangular.project(&geo);
133 let back = Equirectangular.unproject(&world);
134 assert!(
135 (back.lat - lat).abs() < 1e-6,
136 "lat roundtrip failed for {lat}"
137 );
138 }
139 }
140
141 #[test]
144 fn equator_scale() {
145 let a = GeoCoord::from_lat_lon(0.0, 0.0);
146 let b = GeoCoord::from_lat_lon(0.0, 1.0);
147 let wa = Equirectangular.project(&a);
148 let wb = Equirectangular.project(&b);
149 let dx = wb.position.x - wa.position.x;
150 assert!((dx - 111_319.49).abs() < 1.0);
152 }
153
154 #[test]
157 fn scale_factor_equator() {
158 let sf = Equirectangular.scale_factor(&GeoCoord::from_lat_lon(0.0, 0.0));
159 assert!((sf - 1.0).abs() < 1e-10);
160 }
161
162 #[test]
163 fn scale_factor_45_degrees() {
164 let sf = Equirectangular.scale_factor(&GeoCoord::from_lat_lon(45.0, 0.0));
166 assert!((sf - std::f64::consts::SQRT_2).abs() < 1e-10);
167 }
168
169 #[test]
172 fn altitude_passthrough() {
173 let geo = GeoCoord::new(30.0, 60.0, 1234.5);
174 let world = Equirectangular.project(&geo);
175 let back = Equirectangular.unproject(&world);
176 assert!((world.position.z - 1234.5).abs() < 1e-12);
177 assert!((back.alt - 1234.5).abs() < 1e-12);
178 }
179
180 #[test]
183 fn projection_bounds_full_globe() {
184 let bounds = Equirectangular.projection_bounds();
185 assert!((bounds.sw().lat - (-90.0)).abs() < 1e-10);
186 assert!((bounds.ne().lat - 90.0).abs() < 1e-10);
187 assert!((bounds.sw().lon - (-180.0)).abs() < 1e-10);
188 assert!((bounds.ne().lon - 180.0).abs() < 1e-10);
189 }
190}