use rustial_math::{
tile_to_geo, tile_xy_to_geo, Ellipsoid, Equirectangular, GeoBounds, GeoCoord, Globe,
Projection, TileId, VerticalPerspective, WebMercator, WorldCoord,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProjectionSupport {
Stable,
Experimental,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum CameraProjection {
#[default]
WebMercator,
Equirectangular,
Globe,
VerticalPerspective {
center: GeoCoord,
camera_height: f64,
},
}
impl CameraProjection {
#[inline]
pub fn support_level(&self) -> ProjectionSupport {
match self {
Self::WebMercator | Self::Equirectangular => ProjectionSupport::Stable,
Self::Globe | Self::VerticalPerspective { .. } => ProjectionSupport::Experimental,
}
}
#[inline]
pub fn is_stable_for_v1(&self) -> bool {
matches!(self.support_level(), ProjectionSupport::Stable)
}
#[inline]
pub fn is_experimental_for_v1(&self) -> bool {
matches!(self.support_level(), ProjectionSupport::Experimental)
}
#[inline]
pub fn vertical_perspective(center: GeoCoord, camera_height: f64) -> Self {
Self::VerticalPerspective {
center: GeoCoord::from_lat_lon(center.lat, center.lon),
camera_height: if camera_height.is_finite() {
camera_height.max(1.0)
} else {
1.0
},
}
}
#[inline]
pub fn center(&self) -> Option<GeoCoord> {
match self {
Self::VerticalPerspective { center, .. } => Some(*center),
_ => None,
}
}
#[inline]
pub fn camera_height(&self) -> Option<f64> {
match self {
Self::VerticalPerspective { camera_height, .. } => Some(*camera_height),
_ => None,
}
}
#[inline]
pub fn is_tile_compatible(&self) -> bool {
matches!(self, Self::WebMercator | Self::Equirectangular)
}
#[inline]
pub fn project(&self, geo: &GeoCoord) -> WorldCoord {
match self {
Self::WebMercator => WebMercator::project(geo),
Self::Equirectangular => Equirectangular.project(geo),
Self::Globe => Globe::project(geo),
Self::VerticalPerspective {
center,
camera_height,
} => VerticalPerspective::new(*center, *camera_height).project(geo),
}
}
#[inline]
pub fn unproject(&self, world: &WorldCoord) -> GeoCoord {
match self {
Self::WebMercator => WebMercator::unproject(world),
Self::Equirectangular => Equirectangular.unproject(world),
Self::Globe => Globe::unproject(world),
Self::VerticalPerspective {
center,
camera_height,
} => VerticalPerspective::new(*center, *camera_height).unproject(world),
}
}
#[inline]
pub fn scale_factor(&self, geo: &GeoCoord) -> f64 {
match self {
Self::WebMercator => WebMercator.scale_factor(geo),
Self::Equirectangular => Equirectangular.scale_factor(geo),
Self::Globe => Globe.scale_factor(geo),
Self::VerticalPerspective {
center,
camera_height,
} => VerticalPerspective::new(*center, *camera_height).scale_factor(geo),
}
}
#[inline]
pub fn max_extent(&self) -> f64 {
match self {
Self::WebMercator => WebMercator::max_extent(),
Self::Equirectangular => std::f64::consts::PI * Ellipsoid::WGS84.a,
Self::Globe => Globe::radius(),
Self::VerticalPerspective {
center,
camera_height,
} => {
let projection = VerticalPerspective::new(*center, *camera_height);
let horizon = projection.horizon_central_angle();
projection.radius() * horizon.tan()
}
}
}
#[inline]
pub fn world_size(&self) -> f64 {
match self {
Self::Globe => Globe::radius() * 2.0,
_ => self.max_extent() * 2.0,
}
}
#[inline]
pub fn project_position(&self, geo: &GeoCoord) -> [f64; 3] {
let world = self.project(geo);
[world.position.x, world.position.y, world.position.z]
}
#[inline]
pub fn tile_geo_bounds(&self, tile: &TileId) -> GeoBounds {
match self {
Self::Equirectangular => {
let tiles_per_axis = (1u32 << tile.zoom) as f64;
let west = tile.x as f64 / tiles_per_axis * 360.0 - 180.0;
let east = (tile.x as f64 + 1.0) / tiles_per_axis * 360.0 - 180.0;
let north = 90.0 - tile.y as f64 / tiles_per_axis * 180.0;
let south = 90.0 - (tile.y as f64 + 1.0) / tiles_per_axis * 180.0;
GeoBounds::new(
GeoCoord::from_lat_lon(south, west),
GeoCoord::from_lat_lon(north, east),
)
}
_ => {
let nw = tile_to_geo(tile);
let se = tile_xy_to_geo(tile.zoom, tile.x as f64 + 1.0, tile.y as f64 + 1.0);
GeoBounds::new(
GeoCoord::from_lat_lon(se.lat.min(nw.lat), nw.lon.min(se.lon)),
GeoCoord::from_lat_lon(se.lat.max(nw.lat), nw.lon.max(se.lon)),
)
}
}
}
#[inline]
pub fn project_tile_corner(&self, tile: &TileId, u: f64, v: f64) -> [f64; 3] {
let bounds = self.tile_geo_bounds(tile);
let lat = bounds.north() + (bounds.south() - bounds.north()) * v.clamp(0.0, 1.0);
let lon = bounds.west() + (bounds.east() - bounds.west()) * u.clamp(0.0, 1.0);
self.project_position(&GeoCoord::from_lat_lon(lat, lon))
}
#[inline]
pub fn project_tile_center(&self, tile: &TileId) -> [f64; 3] {
let bounds = self.tile_geo_bounds(tile);
let mid_lat = (bounds.north() + bounds.south()) * 0.5;
let mid_lon = (bounds.east() + bounds.west()) * 0.5;
self.project_position(&GeoCoord::from_lat_lon(mid_lat, mid_lon))
}
}
impl Eq for CameraProjection {}
impl std::hash::Hash for CameraProjection {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
std::mem::discriminant(self).hash(state);
if let Self::VerticalPerspective {
center,
camera_height,
} = self
{
center.lat.to_bits().hash(state);
center.lon.to_bits().hash(state);
camera_height.to_bits().hash(state);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_web_mercator() {
assert_eq!(CameraProjection::default(), CameraProjection::WebMercator);
}
#[test]
fn support_levels_match_v1_contract() {
assert_eq!(
CameraProjection::WebMercator.support_level(),
ProjectionSupport::Stable
);
assert_eq!(
CameraProjection::Equirectangular.support_level(),
ProjectionSupport::Stable
);
assert_eq!(
CameraProjection::Globe.support_level(),
ProjectionSupport::Experimental
);
assert!(matches!(
CameraProjection::vertical_perspective(GeoCoord::from_lat_lon(0.0, 0.0), 1_000_000.0)
.support_level(),
ProjectionSupport::Experimental
));
}
#[test]
fn equirectangular_roundtrip() {
let projection = CameraProjection::Equirectangular;
let geo = GeoCoord::new(25.0, 30.0, 100.0);
let world = projection.project(&geo);
let back = projection.unproject(&world);
assert!((back.lat - geo.lat).abs() < 1e-8);
assert!((back.lon - geo.lon).abs() < 1e-8);
assert!((back.alt - geo.alt).abs() < 1e-8);
}
#[test]
fn globe_roundtrip() {
let projection = CameraProjection::Globe;
let geo = GeoCoord::new(51.1, 17.0, 1200.0);
let world = projection.project(&geo);
let back = projection.unproject(&world);
assert!((back.lat - geo.lat).abs() < 1e-6);
assert!((back.lon - geo.lon).abs() < 1e-6);
assert!((back.alt - geo.alt).abs() < 1e-3);
}
#[test]
fn vertical_perspective_roundtrip() {
let projection =
CameraProjection::vertical_perspective(GeoCoord::from_lat_lon(0.0, 0.0), 8_000_000.0);
let geo = GeoCoord::new(10.0, 15.0, 250.0);
let world = projection.project(&geo);
assert!(world.position.x.is_finite());
assert!(world.position.y.is_finite());
let back = projection.unproject(&world);
assert!((back.lat - geo.lat).abs() < 1e-6);
assert!((back.lon - geo.lon).abs() < 1e-6);
}
}