rustial_engine/math/
mercator.rs1use crate::bounds::GeoBounds;
19use crate::coord::{GeoCoord, WorldCoord, MAX_MERCATOR_LAT};
20use crate::ellipsoid::Ellipsoid;
21use crate::projection::Projection;
22use std::f64::consts::PI;
23
24const EARTH_RADIUS: f64 = Ellipsoid::WGS84.a;
28
29pub struct WebMercator;
34
35impl WebMercator {
36 #[inline]
40 pub fn project(geo: &GeoCoord) -> WorldCoord {
41 <Self as Projection>::project(&Self, geo)
42 }
43
44 #[inline]
48 pub fn project_checked(geo: &GeoCoord) -> Option<WorldCoord> {
49 if !geo.is_web_mercator_valid() {
50 return None;
51 }
52 Some(Self::project(geo))
53 }
54
55 #[inline]
60 pub fn project_clamped(geo: &GeoCoord) -> WorldCoord {
61 Self::project(&geo.clamped_mercator())
62 }
63
64 #[inline]
66 pub fn unproject(world: &WorldCoord) -> GeoCoord {
67 <Self as Projection>::unproject(&Self, world)
68 }
69
70 #[inline]
74 pub fn max_extent() -> f64 {
75 EARTH_RADIUS * PI
76 }
77
78 #[inline]
80 pub fn world_size() -> f64 {
81 2.0 * Self::max_extent()
82 }
83}
84
85impl Projection for WebMercator {
86 fn project(&self, geo: &GeoCoord) -> WorldCoord {
87 let x = EARTH_RADIUS * geo.lon.to_radians();
91 let lat_rad = geo.lat.to_radians();
92 let y = EARTH_RADIUS * ((PI / 4.0 + lat_rad / 2.0).tan()).ln();
93 WorldCoord::new(x, y, geo.alt)
94 }
95
96 fn unproject(&self, world: &WorldCoord) -> GeoCoord {
97 let mut lon = (world.position.x / EARTH_RADIUS).to_degrees();
101 lon = ((lon + 180.0).rem_euclid(360.0)) - 180.0;
102 let lat = (2.0 * (world.position.y / EARTH_RADIUS).exp().atan() - PI / 2.0).to_degrees();
103 GeoCoord::new(lat, lon, world.position.z)
104 }
105
106 fn scale_factor(&self, geo: &GeoCoord) -> f64 {
111 1.0 / geo.lat.to_radians().cos()
112 }
113
114 fn projection_bounds(&self) -> GeoBounds {
117 GeoBounds::new(
118 GeoCoord::from_lat_lon(-MAX_MERCATOR_LAT, -180.0),
119 GeoCoord::from_lat_lon(MAX_MERCATOR_LAT, 180.0),
120 )
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn roundtrip_origin() {
130 let geo = GeoCoord::from_lat_lon(0.0, 0.0);
131 let world = WebMercator::project(&geo);
132 let back = WebMercator::unproject(&world);
133 assert!((back.lat - geo.lat).abs() < 1e-10);
134 assert!((back.lon - geo.lon).abs() < 1e-10);
135 }
136
137 #[test]
138 fn roundtrip_nonzero() {
139 let geo = GeoCoord::from_lat_lon(51.09916, 17.03664);
140 let world = WebMercator::project(&geo);
141 let back = WebMercator::unproject(&world);
142 assert!((back.lat - geo.lat).abs() < 1e-8);
143 assert!((back.lon - geo.lon).abs() < 1e-8);
144 }
145
146 #[test]
147 fn project_checked_rejects_invalid_lat() {
148 let geo = GeoCoord::from_lat_lon(89.0, 0.0);
149 assert!(WebMercator::project_checked(&geo).is_none());
150 }
151
152 #[test]
153 fn project_clamped_accepts_invalid_lat() {
154 let geo = GeoCoord::from_lat_lon(89.0, 0.0);
155 let world = WebMercator::project_clamped(&geo);
156 assert!(world.position.y.is_finite());
157 }
158
159 #[test]
160 fn world_size_is_double_extent() {
161 assert!((WebMercator::world_size() - 2.0 * WebMercator::max_extent()).abs() < 1e-10);
162 }
163
164 #[test]
165 fn scale_factor_equator() {
166 let sf = WebMercator.scale_factor(&GeoCoord::from_lat_lon(0.0, 0.0));
167 assert!((sf - 1.0).abs() < 1e-10);
168 }
169
170 #[test]
171 fn scale_factor_60_degrees() {
172 let sf = WebMercator.scale_factor(&GeoCoord::from_lat_lon(60.0, 0.0));
174 assert!((sf - 2.0).abs() < 1e-10);
175 }
176
177 #[test]
178 fn projection_bounds_mercator() {
179 let bounds = WebMercator.projection_bounds();
180 assert!((bounds.sw().lat - (-MAX_MERCATOR_LAT)).abs() < 1e-10);
181 assert!((bounds.ne().lat - MAX_MERCATOR_LAT).abs() < 1e-10);
182 }
183
184 #[test]
185 fn project_clamped_wraps_longitude() {
186 let a = GeoCoord {
187 lat: 0.0,
188 lon: 190.0,
189 alt: 0.0,
190 };
191 let b = GeoCoord::from_lat_lon(0.0, -170.0);
192 let wa = WebMercator::project_clamped(&a);
193 let wb = WebMercator::project_clamped(&b);
194 assert!((wa.position.x - wb.position.x).abs() < 1e-10);
195 }
196
197 #[test]
198 fn altitude_passthrough() {
199 let geo = GeoCoord::new(45.0, 12.0, 1234.5);
200 let world = WebMercator::project(&geo);
201 let back = WebMercator::unproject(&world);
202 assert!((world.position.z - 1234.5).abs() < 1e-12);
203 assert!((back.alt - 1234.5).abs() < 1e-12);
204 }
205
206 #[test]
207 fn unproject_wraps_longitude_back_into_valid_range() {
208 let extent = WebMercator::max_extent();
209 let world = WorldCoord::new(extent + 1000.0, 0.0, 0.0);
210 let geo = WebMercator::unproject(&world);
211 assert!(geo.lon >= -180.0 && geo.lon <= 180.0);
212 }
213}