1use rustial_math::{
9 tile_to_geo, tile_xy_to_geo, Equirectangular, Ellipsoid, 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 { center, camera_height } => {
110 VerticalPerspective::new(*center, *camera_height).project(geo)
111 }
112 }
113 }
114
115 #[inline]
117 pub fn unproject(&self, world: &WorldCoord) -> GeoCoord {
118 match self {
119 Self::WebMercator => WebMercator::unproject(world),
120 Self::Equirectangular => Equirectangular.unproject(world),
121 Self::Globe => Globe::unproject(world),
122 Self::VerticalPerspective { center, camera_height } => {
123 VerticalPerspective::new(*center, *camera_height).unproject(world)
124 }
125 }
126 }
127
128 #[inline]
130 pub fn scale_factor(&self, geo: &GeoCoord) -> f64 {
131 match self {
132 Self::WebMercator => WebMercator.scale_factor(geo),
133 Self::Equirectangular => Equirectangular.scale_factor(geo),
134 Self::Globe => Globe.scale_factor(geo),
135 Self::VerticalPerspective { center, camera_height } => {
136 VerticalPerspective::new(*center, *camera_height).scale_factor(geo)
137 }
138 }
139 }
140
141 #[inline]
143 pub fn max_extent(&self) -> f64 {
144 match self {
145 Self::WebMercator => WebMercator::max_extent(),
146 Self::Equirectangular => std::f64::consts::PI * Ellipsoid::WGS84.a,
147 Self::Globe => Globe::radius(),
148 Self::VerticalPerspective { center, camera_height } => {
149 let projection = VerticalPerspective::new(*center, *camera_height);
150 let horizon = projection.horizon_central_angle();
151 projection.radius() * horizon.tan()
152 }
153 }
154 }
155
156 #[inline]
158 pub fn world_size(&self) -> f64 {
159 match self {
160 Self::Globe => Globe::radius() * 2.0,
161 _ => self.max_extent() * 2.0,
162 }
163 }
164
165 #[inline]
168 pub fn project_position(&self, geo: &GeoCoord) -> [f64; 3] {
169 let world = self.project(geo);
170 [world.position.x, world.position.y, world.position.z]
171 }
172
173 #[inline]
176 pub fn tile_geo_bounds(&self, tile: &TileId) -> GeoBounds {
177 match self {
178 Self::Equirectangular => {
179 let tiles_per_axis = (1u32 << tile.zoom) as f64;
180 let west = tile.x as f64 / tiles_per_axis * 360.0 - 180.0;
181 let east = (tile.x as f64 + 1.0) / tiles_per_axis * 360.0 - 180.0;
182 let north = 90.0 - tile.y as f64 / tiles_per_axis * 180.0;
183 let south = 90.0 - (tile.y as f64 + 1.0) / tiles_per_axis * 180.0;
184
185 GeoBounds::new(
186 GeoCoord::from_lat_lon(south, west),
187 GeoCoord::from_lat_lon(north, east),
188 )
189 }
190 _ => {
191 let nw = tile_to_geo(tile);
192 let se = tile_xy_to_geo(tile.zoom, tile.x as f64 + 1.0, tile.y as f64 + 1.0);
193
194 GeoBounds::new(
195 GeoCoord::from_lat_lon(se.lat.min(nw.lat), nw.lon.min(se.lon)),
196 GeoCoord::from_lat_lon(se.lat.max(nw.lat), nw.lon.max(se.lon)),
197 )
198 }
199 }
200 }
201
202 #[inline]
204 pub fn project_tile_corner(&self, tile: &TileId, u: f64, v: f64) -> [f64; 3] {
205 let bounds = self.tile_geo_bounds(tile);
206 let lat = bounds.north() + (bounds.south() - bounds.north()) * v.clamp(0.0, 1.0);
207 let lon = bounds.west() + (bounds.east() - bounds.west()) * u.clamp(0.0, 1.0);
208 self.project_position(&GeoCoord::from_lat_lon(lat, lon))
209 }
210
211 #[inline]
217 pub fn project_tile_center(&self, tile: &TileId) -> [f64; 3] {
218 let bounds = self.tile_geo_bounds(tile);
219 let mid_lat = (bounds.north() + bounds.south()) * 0.5;
220 let mid_lon = (bounds.east() + bounds.west()) * 0.5;
221 self.project_position(&GeoCoord::from_lat_lon(mid_lat, mid_lon))
222 }
223}
224
225impl Eq for CameraProjection {}
226
227impl std::hash::Hash for CameraProjection {
228 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
229 std::mem::discriminant(self).hash(state);
230 if let Self::VerticalPerspective { center, camera_height } = self {
231 center.lat.to_bits().hash(state);
232 center.lon.to_bits().hash(state);
233 camera_height.to_bits().hash(state);
234 }
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn default_is_web_mercator() {
244 assert_eq!(CameraProjection::default(), CameraProjection::WebMercator);
245 }
246
247 #[test]
248 fn support_levels_match_v1_contract() {
249 assert_eq!(CameraProjection::WebMercator.support_level(), ProjectionSupport::Stable);
250 assert_eq!(CameraProjection::Equirectangular.support_level(), ProjectionSupport::Stable);
251 assert_eq!(CameraProjection::Globe.support_level(), ProjectionSupport::Experimental);
252 assert!(matches!(
253 CameraProjection::vertical_perspective(GeoCoord::from_lat_lon(0.0, 0.0), 1_000_000.0)
254 .support_level(),
255 ProjectionSupport::Experimental
256 ));
257 }
258
259 #[test]
260 fn equirectangular_roundtrip() {
261 let projection = CameraProjection::Equirectangular;
262 let geo = GeoCoord::new(25.0, 30.0, 100.0);
263 let world = projection.project(&geo);
264 let back = projection.unproject(&world);
265 assert!((back.lat - geo.lat).abs() < 1e-8);
266 assert!((back.lon - geo.lon).abs() < 1e-8);
267 assert!((back.alt - geo.alt).abs() < 1e-8);
268 }
269
270 #[test]
271 fn globe_roundtrip() {
272 let projection = CameraProjection::Globe;
273 let geo = GeoCoord::new(51.1, 17.0, 1200.0);
274 let world = projection.project(&geo);
275 let back = projection.unproject(&world);
276 assert!((back.lat - geo.lat).abs() < 1e-6);
277 assert!((back.lon - geo.lon).abs() < 1e-6);
278 assert!((back.alt - geo.alt).abs() < 1e-3);
279 }
280
281 #[test]
282 fn vertical_perspective_roundtrip() {
283 let projection = CameraProjection::vertical_perspective(
284 GeoCoord::from_lat_lon(0.0, 0.0),
285 8_000_000.0,
286 );
287 let geo = GeoCoord::new(10.0, 15.0, 250.0);
288 let world = projection.project(&geo);
289 assert!(world.position.x.is_finite());
290 assert!(world.position.y.is_finite());
291 let back = projection.unproject(&world);
292 assert!((back.lat - geo.lat).abs() < 1e-6);
293 assert!((back.lon - geo.lon).abs() < 1e-6);
294 }
295}