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(
62 lat,
63 lon,
64 world.position.z,
65 )
66 }
67
68 fn scale_factor(&self, geo: &GeoCoord) -> f64 {
75 1.0 / geo.lat.to_radians().cos()
76 }
77
78 }
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
89 fn roundtrip_origin() {
90 let geo = GeoCoord::from_lat_lon(0.0, 0.0);
91 let world = Equirectangular.project(&geo);
92 let back = Equirectangular.unproject(&world);
93 assert!((back.lat - geo.lat).abs() < 1e-10);
94 assert!((back.lon - geo.lon).abs() < 1e-10);
95 }
96
97 #[test]
98 fn roundtrip_nonzero() {
99 let geo = GeoCoord::from_lat_lon(45.0, 90.0);
100 let world = Equirectangular.project(&geo);
101 let back = Equirectangular.unproject(&world);
102 assert!((back.lat - geo.lat).abs() < 1e-8);
103 assert!((back.lon - geo.lon).abs() < 1e-8);
104 }
105
106 #[test]
107 fn poles_roundtrip() {
108 let north = GeoCoord::from_lat_lon(90.0, 0.0);
109 let north_w = Equirectangular.project(&north);
110 let north_back = Equirectangular.unproject(&north_w);
111 assert!((north_back.lat - 90.0).abs() < 1e-9);
112
113 let south = GeoCoord::from_lat_lon(-90.0, 0.0);
114 let south_w = Equirectangular.project(&south);
115 let south_back = Equirectangular.unproject(&south_w);
116 assert!((south_back.lat + 90.0).abs() < 1e-9);
117 }
118
119 #[test]
120 fn anti_meridian_roundtrip() {
121 let east = GeoCoord::from_lat_lon(10.0, 180.0);
124 let east_back = Equirectangular.unproject(&Equirectangular.project(&east));
125 assert!((east_back.lon.abs() - 180.0).abs() < 1e-9);
126
127 let west = GeoCoord::from_lat_lon(10.0, -180.0);
128 let west_back = Equirectangular.unproject(&Equirectangular.project(&west));
129 assert!((west_back.lon.abs() - 180.0).abs() < 1e-9);
130 }
131
132 #[test]
133 fn extreme_latitudes_stable() {
134 for lat in [89.999, -89.999, 89.9, -89.9] {
135 let geo = GeoCoord::from_lat_lon(lat, 45.0);
136 let world = Equirectangular.project(&geo);
137 let back = Equirectangular.unproject(&world);
138 assert!((back.lat - lat).abs() < 1e-6, "lat roundtrip failed for {lat}");
139 }
140 }
141
142 #[test]
145 fn equator_scale() {
146 let a = GeoCoord::from_lat_lon(0.0, 0.0);
147 let b = GeoCoord::from_lat_lon(0.0, 1.0);
148 let wa = Equirectangular.project(&a);
149 let wb = Equirectangular.project(&b);
150 let dx = wb.position.x - wa.position.x;
151 assert!((dx - 111_319.49).abs() < 1.0);
153 }
154
155 #[test]
158 fn scale_factor_equator() {
159 let sf = Equirectangular.scale_factor(&GeoCoord::from_lat_lon(0.0, 0.0));
160 assert!((sf - 1.0).abs() < 1e-10);
161 }
162
163 #[test]
164 fn scale_factor_45_degrees() {
165 let sf = Equirectangular.scale_factor(&GeoCoord::from_lat_lon(45.0, 0.0));
167 assert!((sf - std::f64::consts::SQRT_2).abs() < 1e-10);
168 }
169
170 #[test]
173 fn altitude_passthrough() {
174 let geo = GeoCoord::new(30.0, 60.0, 1234.5);
175 let world = Equirectangular.project(&geo);
176 let back = Equirectangular.unproject(&world);
177 assert!((world.position.z - 1234.5).abs() < 1e-12);
178 assert!((back.alt - 1234.5).abs() < 1e-12);
179 }
180
181 #[test]
184 fn projection_bounds_full_globe() {
185 let bounds = Equirectangular.projection_bounds();
186 assert!((bounds.sw().lat - (-90.0)).abs() < 1e-10);
187 assert!((bounds.ne().lat - 90.0).abs() < 1e-10);
188 assert!((bounds.sw().lon - (-180.0)).abs() < 1e-10);
189 assert!((bounds.ne().lon - 180.0).abs() < 1e-10);
190 }
191}