1use rustial_math::{
9 tile_to_geo, tile_xy_to_geo, Ellipsoid, Equirectangular, GeoBounds, GeoCoord, Globe,
10 Projection, TileId, VerticalPerspective, WebMercator, WorldCoord,
11};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ProjectionSupport {
16 Stable,
18 Experimental,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Default)]
25pub enum CameraProjection {
26 #[default]
28 WebMercator,
29 Equirectangular,
31 Globe,
33 VerticalPerspective {
35 center: GeoCoord,
37 camera_height: f64,
39 },
40}
41
42impl CameraProjection {
43 #[inline]
45 pub fn support_level(&self) -> ProjectionSupport {
46 match self {
47 Self::WebMercator | Self::Equirectangular => ProjectionSupport::Stable,
48 Self::Globe | Self::VerticalPerspective { .. } => ProjectionSupport::Experimental,
49 }
50 }
51
52 #[inline]
54 pub fn is_stable_for_v1(&self) -> bool {
55 matches!(self.support_level(), ProjectionSupport::Stable)
56 }
57
58 #[inline]
60 pub fn is_experimental_for_v1(&self) -> bool {
61 matches!(self.support_level(), ProjectionSupport::Experimental)
62 }
63
64 #[inline]
66 pub fn vertical_perspective(center: GeoCoord, camera_height: f64) -> Self {
67 Self::VerticalPerspective {
68 center: GeoCoord::from_lat_lon(center.lat, center.lon),
69 camera_height: if camera_height.is_finite() {
70 camera_height.max(1.0)
71 } else {
72 1.0
73 },
74 }
75 }
76
77 #[inline]
79 pub fn center(&self) -> Option<GeoCoord> {
80 match self {
81 Self::VerticalPerspective { center, .. } => Some(*center),
82 _ => None,
83 }
84 }
85
86 #[inline]
88 pub fn camera_height(&self) -> Option<f64> {
89 match self {
90 Self::VerticalPerspective { camera_height, .. } => Some(*camera_height),
91 _ => None,
92 }
93 }
94
95 #[inline]
98 pub fn is_tile_compatible(&self) -> bool {
99 matches!(self, Self::WebMercator | Self::Equirectangular)
100 }
101
102 #[inline]
104 pub fn project(&self, geo: &GeoCoord) -> WorldCoord {
105 match self {
106 Self::WebMercator => WebMercator::project(geo),
107 Self::Equirectangular => Equirectangular.project(geo),
108 Self::Globe => Globe::project(geo),
109 Self::VerticalPerspective {
110 center,
111 camera_height,
112 } => VerticalPerspective::new(*center, *camera_height).project(geo),
113 }
114 }
115
116 #[inline]
118 pub fn unproject(&self, world: &WorldCoord) -> GeoCoord {
119 match self {
120 Self::WebMercator => WebMercator::unproject(world),
121 Self::Equirectangular => Equirectangular.unproject(world),
122 Self::Globe => Globe::unproject(world),
123 Self::VerticalPerspective {
124 center,
125 camera_height,
126 } => VerticalPerspective::new(*center, *camera_height).unproject(world),
127 }
128 }
129
130 #[inline]
132 pub fn scale_factor(&self, geo: &GeoCoord) -> f64 {
133 match self {
134 Self::WebMercator => WebMercator.scale_factor(geo),
135 Self::Equirectangular => Equirectangular.scale_factor(geo),
136 Self::Globe => Globe.scale_factor(geo),
137 Self::VerticalPerspective {
138 center,
139 camera_height,
140 } => VerticalPerspective::new(*center, *camera_height).scale_factor(geo),
141 }
142 }
143
144 #[inline]
146 pub fn max_extent(&self) -> f64 {
147 match self {
148 Self::WebMercator => WebMercator::max_extent(),
149 Self::Equirectangular => std::f64::consts::PI * Ellipsoid::WGS84.a,
150 Self::Globe => Globe::radius(),
151 Self::VerticalPerspective {
152 center,
153 camera_height,
154 } => {
155 let projection = VerticalPerspective::new(*center, *camera_height);
156 let horizon = projection.horizon_central_angle();
157 projection.radius() * horizon.tan()
158 }
159 }
160 }
161
162 #[inline]
164 pub fn world_size(&self) -> f64 {
165 match self {
166 Self::Globe => Globe::radius() * 2.0,
167 _ => self.max_extent() * 2.0,
168 }
169 }
170
171 #[inline]
174 pub fn project_position(&self, geo: &GeoCoord) -> [f64; 3] {
175 let world = self.project(geo);
176 [world.position.x, world.position.y, world.position.z]
177 }
178
179 #[inline]
182 pub fn tile_geo_bounds(&self, tile: &TileId) -> GeoBounds {
183 match self {
184 Self::Equirectangular => {
185 let tiles_per_axis = (1u32 << tile.zoom) as f64;
186 let west = tile.x as f64 / tiles_per_axis * 360.0 - 180.0;
187 let east = (tile.x as f64 + 1.0) / tiles_per_axis * 360.0 - 180.0;
188 let north = 90.0 - tile.y as f64 / tiles_per_axis * 180.0;
189 let south = 90.0 - (tile.y as f64 + 1.0) / tiles_per_axis * 180.0;
190
191 GeoBounds::new(
192 GeoCoord::from_lat_lon(south, west),
193 GeoCoord::from_lat_lon(north, east),
194 )
195 }
196 _ => {
197 let nw = tile_to_geo(tile);
198 let se = tile_xy_to_geo(tile.zoom, tile.x as f64 + 1.0, tile.y as f64 + 1.0);
199
200 GeoBounds::new(
201 GeoCoord::from_lat_lon(se.lat.min(nw.lat), nw.lon.min(se.lon)),
202 GeoCoord::from_lat_lon(se.lat.max(nw.lat), nw.lon.max(se.lon)),
203 )
204 }
205 }
206 }
207
208 #[inline]
210 pub fn project_tile_corner(&self, tile: &TileId, u: f64, v: f64) -> [f64; 3] {
211 let bounds = self.tile_geo_bounds(tile);
212 let lat = bounds.north() + (bounds.south() - bounds.north()) * v.clamp(0.0, 1.0);
213 let lon = bounds.west() + (bounds.east() - bounds.west()) * u.clamp(0.0, 1.0);
214 self.project_position(&GeoCoord::from_lat_lon(lat, lon))
215 }
216
217 #[inline]
223 pub fn project_tile_center(&self, tile: &TileId) -> [f64; 3] {
224 let bounds = self.tile_geo_bounds(tile);
225 let mid_lat = (bounds.north() + bounds.south()) * 0.5;
226 let mid_lon = (bounds.east() + bounds.west()) * 0.5;
227 self.project_position(&GeoCoord::from_lat_lon(mid_lat, mid_lon))
228 }
229}
230
231impl Eq for CameraProjection {}
232
233impl std::hash::Hash for CameraProjection {
234 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
235 std::mem::discriminant(self).hash(state);
236 if let Self::VerticalPerspective {
237 center,
238 camera_height,
239 } = self
240 {
241 center.lat.to_bits().hash(state);
242 center.lon.to_bits().hash(state);
243 camera_height.to_bits().hash(state);
244 }
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn default_is_web_mercator() {
254 assert_eq!(CameraProjection::default(), CameraProjection::WebMercator);
255 }
256
257 #[test]
258 fn support_levels_match_v1_contract() {
259 assert_eq!(
260 CameraProjection::WebMercator.support_level(),
261 ProjectionSupport::Stable
262 );
263 assert_eq!(
264 CameraProjection::Equirectangular.support_level(),
265 ProjectionSupport::Stable
266 );
267 assert_eq!(
268 CameraProjection::Globe.support_level(),
269 ProjectionSupport::Experimental
270 );
271 assert!(matches!(
272 CameraProjection::vertical_perspective(GeoCoord::from_lat_lon(0.0, 0.0), 1_000_000.0)
273 .support_level(),
274 ProjectionSupport::Experimental
275 ));
276 }
277
278 #[test]
279 fn equirectangular_roundtrip() {
280 let projection = CameraProjection::Equirectangular;
281 let geo = GeoCoord::new(25.0, 30.0, 100.0);
282 let world = projection.project(&geo);
283 let back = projection.unproject(&world);
284 assert!((back.lat - geo.lat).abs() < 1e-8);
285 assert!((back.lon - geo.lon).abs() < 1e-8);
286 assert!((back.alt - geo.alt).abs() < 1e-8);
287 }
288
289 #[test]
290 fn globe_roundtrip() {
291 let projection = CameraProjection::Globe;
292 let geo = GeoCoord::new(51.1, 17.0, 1200.0);
293 let world = projection.project(&geo);
294 let back = projection.unproject(&world);
295 assert!((back.lat - geo.lat).abs() < 1e-6);
296 assert!((back.lon - geo.lon).abs() < 1e-6);
297 assert!((back.alt - geo.alt).abs() < 1e-3);
298 }
299
300 #[test]
301 fn vertical_perspective_roundtrip() {
302 let projection =
303 CameraProjection::vertical_perspective(GeoCoord::from_lat_lon(0.0, 0.0), 8_000_000.0);
304 let geo = GeoCoord::new(10.0, 15.0, 250.0);
305 let world = projection.project(&geo);
306 assert!(world.position.x.is_finite());
307 assert!(world.position.y.is_finite());
308 let back = projection.unproject(&world);
309 assert!((back.lat - geo.lat).abs() < 1e-6);
310 assert!((back.lon - geo.lon).abs() < 1e-6);
311 }
312}