#[cfg(test)]
mod tests {
use crate::camera::CameraMode;
use crate::camera_projection::CameraProjection;
use crate::geometry::{Feature, FeatureCollection, Geometry, LineString, Point, PropertyValue};
use crate::models::{AltitudeMode, ModelInstance, ModelMesh};
use crate::style::{
BackgroundStyleLayer, CircleStyleLayer, FillExtrusionStyleLayer, FillStyleLayer,
GeoJsonSource, HeatmapStyleLayer, HillshadeStyleLayer, LineStyleLayer, ModelSource,
ModelStyleLayer, RasterStyleLayer, StyleDocument, StyleLayer, StyleProjection, StyleSource,
StyleSourceKind, SymbolStyleLayer, VectorStyleLayer, VectorTileSource,
};
use crate::terrain::FlatElevationSource;
use rustial_math::GeoCoord;
use rustial_math::{ElevationGrid, Equirectangular, Projection, TileId, WebMercator};
use std::collections::HashMap;
#[test]
fn source_kind_raster_exists() {
assert_eq!(StyleSourceKind::Raster.as_str(), "raster");
}
#[test]
fn source_kind_terrain_exists() {
assert_eq!(StyleSourceKind::Terrain.as_str(), "terrain");
}
#[test]
fn source_kind_geojson_exists() {
assert_eq!(StyleSourceKind::GeoJson.as_str(), "geojson");
}
#[test]
fn source_kind_vector_tile_exists() {
assert_eq!(StyleSourceKind::VectorTile.as_str(), "vector");
}
#[test]
fn source_kind_image_exists() {
assert_eq!(StyleSourceKind::Image.as_str(), "image");
}
#[test]
fn source_kind_video_exists() {
assert_eq!(StyleSourceKind::Video.as_str(), "video");
}
#[test]
fn source_kind_canvas_exists() {
assert_eq!(StyleSourceKind::Canvas.as_str(), "canvas");
}
#[test]
fn source_kind_model_exists() {
assert_eq!(StyleSourceKind::Model.as_str(), "model");
}
#[test]
fn geojson_source_round_trip() {
let fc = FeatureCollection {
features: vec![Feature {
geometry: Geometry::Point(Point {
coord: GeoCoord::from_lat_lon(51.1, 17.0),
}),
properties: {
let mut p = HashMap::new();
p.insert("name".into(), PropertyValue::String("test".into()));
p
},
}],
};
let src = GeoJsonSource::new(fc.clone());
assert_eq!(src.data.len(), 1);
let ss = StyleSource::GeoJson(src);
assert_eq!(ss.kind(), StyleSourceKind::GeoJson);
}
#[test]
fn vector_tile_source_round_trip() {
let fc = FeatureCollection {
features: vec![Feature {
geometry: Geometry::LineString(LineString {
coords: vec![
GeoCoord::from_lat_lon(0.0, 0.0),
GeoCoord::from_lat_lon(1.0, 1.0),
],
}),
properties: HashMap::new(),
}],
};
let src = VectorTileSource::new(fc);
let ss = StyleSource::VectorTile(src);
assert_eq!(ss.kind(), StyleSourceKind::VectorTile);
}
#[test]
fn model_source_round_trip() {
let src = ModelSource::new(vec![ModelInstance {
mesh: ModelMesh {
positions: vec![[0.0, 0.0, 0.0]],
normals: vec![[0.0, 0.0, 1.0]],
uvs: vec![[0.0, 0.0]],
indices: vec![0],
},
position: GeoCoord::from_lat_lon(0.0, 0.0),
altitude_mode: AltitudeMode::Absolute,
scale: 1.0,
heading: 0.0,
pitch: 0.0,
roll: 0.0,
}]);
let ss = StyleSource::Model(src);
assert_eq!(ss.kind(), StyleSourceKind::Model);
}
#[cfg(feature = "geojson")]
#[test]
fn geojson_parser_all_geometry_types() {
use crate::geojson::parse_geojson;
let fc = parse_geojson(
r#"{"type":"Feature","geometry":{"type":"Point","coordinates":[17,51]},"properties":{}}"#,
)
.unwrap();
assert_eq!(fc.len(), 1);
let fc = parse_geojson(
r#"{"type":"Feature","geometry":{"type":"LineString","coordinates":[[0,0],[1,1]]},"properties":{}}"#,
)
.unwrap();
assert_eq!(fc.features[0].geometry.type_name(), "LineString");
let fc = parse_geojson(
r#"{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[0,0],[1,0],[1,1],[0,0]]]},"properties":{}}"#,
)
.unwrap();
assert_eq!(fc.features[0].geometry.type_name(), "Polygon");
let fc = parse_geojson(
r#"{"type":"Feature","geometry":{"type":"MultiPoint","coordinates":[[0,0],[1,1]]},"properties":{}}"#,
)
.unwrap();
assert_eq!(fc.features[0].geometry.type_name(), "MultiPoint");
let fc = parse_geojson(
r#"{"type":"Feature","geometry":{"type":"MultiLineString","coordinates":[[[0,0],[1,1]],[[2,2],[3,3]]]},"properties":{}}"#,
)
.unwrap();
assert_eq!(fc.features[0].geometry.type_name(), "MultiLineString");
let fc = parse_geojson(
r#"{"type":"Feature","geometry":{"type":"MultiPolygon","coordinates":[[[[0,0],[1,0],[1,1],[0,0]]]]},"properties":{}}"#,
)
.unwrap();
assert_eq!(fc.features[0].geometry.type_name(), "MultiPolygon");
let fc = parse_geojson(
r#"{"type":"Feature","geometry":{"type":"GeometryCollection","geometries":[{"type":"Point","coordinates":[0,0]}]},"properties":{}}"#,
)
.unwrap();
assert_eq!(fc.features[0].geometry.type_name(), "GeometryCollection");
let fc = parse_geojson(
r#"{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[0,0]},"properties":{"a":1}}]}"#,
)
.unwrap();
assert_eq!(fc.len(), 1);
assert_eq!(
fc.features[0].property("a").and_then(|v| v.as_f64()),
Some(1.0)
);
}
#[cfg(feature = "shapefile")]
#[test]
fn shapefile_parser_rejects_invalid() {
use crate::shapefile_parser::parse_shapefile;
assert!(parse_shapefile(b"not a shapefile").is_err());
}
#[cfg(feature = "gltf")]
#[test]
fn gltf_loader_rejects_invalid() {
assert!(ModelMesh::from_gltf(b"not a gltf").is_err());
}
#[cfg(feature = "obj")]
#[test]
fn obj_loader_triangle() {
let obj = b"v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 3\n";
let mesh = ModelMesh::from_obj(obj).unwrap();
assert_eq!(mesh.positions.len(), 3);
assert_eq!(mesh.indices.len(), 3);
}
#[test]
fn style_layer_background() {
let layer = StyleLayer::Background(BackgroundStyleLayer::new("bg", [0.1, 0.2, 0.3, 1.0]));
assert_eq!(layer.id(), "bg");
}
#[test]
fn style_layer_hillshade() {
let layer = StyleLayer::Hillshade(HillshadeStyleLayer::new("hs"));
assert_eq!(layer.id(), "hs");
}
#[test]
fn style_layer_raster() {
let layer = StyleLayer::Raster(RasterStyleLayer::new("raster", "src"));
assert_eq!(layer.id(), "raster");
}
#[test]
fn style_layer_fill() {
let layer = StyleLayer::Fill(FillStyleLayer::new("fill", "src"));
assert_eq!(layer.id(), "fill");
}
#[test]
fn style_layer_line() {
let layer = StyleLayer::Line(LineStyleLayer::new("line", "src"));
assert_eq!(layer.id(), "line");
}
#[test]
fn style_layer_circle() {
let layer = StyleLayer::Circle(CircleStyleLayer::new("circle", "src"));
assert_eq!(layer.id(), "circle");
}
#[test]
fn style_layer_symbol() {
let layer = StyleLayer::Symbol(SymbolStyleLayer::new("sym", "src"));
assert_eq!(layer.id(), "sym");
}
#[test]
fn style_layer_fill_extrusion() {
let layer = StyleLayer::FillExtrusion(FillExtrusionStyleLayer::new("ext", "src"));
assert_eq!(layer.id(), "ext");
}
#[test]
fn style_layer_heatmap() {
let layer = StyleLayer::Heatmap(HeatmapStyleLayer::new("heat", "src"));
assert_eq!(layer.id(), "heat");
}
#[test]
fn style_layer_model() {
let layer = StyleLayer::Model(ModelStyleLayer::new("model", "src"));
assert_eq!(layer.id(), "model");
}
#[test]
fn style_layer_vector_generic() {
let layer = StyleLayer::Vector(VectorStyleLayer::new("vec", "src"));
assert_eq!(layer.id(), "vec");
}
#[test]
fn flat_elevation_source_exists() {
let src = FlatElevationSource::new(1, 1);
let _cfg = crate::terrain::TerrainConfig {
enabled: true,
vertical_exaggeration: 1.0,
mesh_resolution: 16,
skirt_depth: 10.0,
source_max_zoom: 15,
source: Box::new(src),
};
}
#[test]
fn projection_web_mercator() {
let p = CameraProjection::WebMercator;
assert_eq!(format!("{p:?}"), "WebMercator");
}
#[test]
fn projection_equirectangular() {
let p = CameraProjection::Equirectangular;
assert_eq!(format!("{p:?}"), "Equirectangular");
}
#[test]
fn projection_globe_experimental() {
let p = CameraProjection::Globe;
assert_eq!(format!("{p:?}"), "Globe");
}
#[test]
fn projection_vertical_perspective_experimental() {
let p = CameraProjection::vertical_perspective(GeoCoord::default(), 10_000_000.0);
let name = format!("{p:?}");
assert!(name.contains("VerticalPerspective"));
}
#[test]
fn style_projection_to_camera_projection() {
assert!(matches![
StyleProjection::Mercator.to_camera_projection(),
CameraProjection::WebMercator
]);
assert!(matches![
StyleProjection::Equirectangular.to_camera_projection(),
CameraProjection::Equirectangular
]);
assert!(matches![
StyleProjection::Globe.to_camera_projection(),
CameraProjection::Globe
]);
let _ = StyleProjection::VerticalPerspective.to_camera_projection();
}
#[test]
fn camera_mode_perspective() {
let m = CameraMode::Perspective;
assert_eq!(format!("{m:?}"), "Perspective");
}
#[test]
fn camera_mode_orthographic() {
let m = CameraMode::Orthographic;
assert_eq!(format!("{m:?}"), "Orthographic");
}
#[test]
fn style_document_assembles_with_all_stable_layer_types() {
let fc = FeatureCollection {
features: vec![Feature {
geometry: Geometry::Point(Point {
coord: GeoCoord::from_lat_lon(0.0, 0.0),
}),
properties: HashMap::new(),
}],
};
let mut doc = StyleDocument::new();
doc.add_source("geo", StyleSource::GeoJson(GeoJsonSource::new(fc.clone())))
.unwrap();
doc.add_source("models", StyleSource::Model(ModelSource::new(vec![])))
.unwrap();
doc.add_layer(StyleLayer::Background(BackgroundStyleLayer::new(
"bg",
[0.1, 0.2, 0.3, 1.0],
)))
.unwrap();
doc.add_layer(StyleLayer::Hillshade(HillshadeStyleLayer::new("hs")))
.unwrap();
doc.add_layer(StyleLayer::Fill(FillStyleLayer::new("fill", "geo")))
.unwrap();
doc.add_layer(StyleLayer::Line(LineStyleLayer::new("line", "geo")))
.unwrap();
doc.add_layer(StyleLayer::Circle(CircleStyleLayer::new("circle", "geo")))
.unwrap();
doc.add_layer(StyleLayer::Symbol(SymbolStyleLayer::new("sym", "geo")))
.unwrap();
doc.add_layer(StyleLayer::FillExtrusion(FillExtrusionStyleLayer::new(
"ext", "geo",
)))
.unwrap();
doc.add_layer(StyleLayer::Heatmap(HeatmapStyleLayer::new("heat", "geo")))
.unwrap();
doc.add_layer(StyleLayer::Model(ModelStyleLayer::new("model", "models")))
.unwrap();
doc.add_layer(StyleLayer::Vector(VectorStyleLayer::new("vec", "geo")))
.unwrap();
assert_eq!(doc.layers().len(), 10);
}
#[test]
fn all_source_kinds_have_stable_names() {
let expected = [
(StyleSourceKind::Raster, "raster"),
(StyleSourceKind::Terrain, "terrain"),
(StyleSourceKind::GeoJson, "geojson"),
(StyleSourceKind::VectorTile, "vector"),
(StyleSourceKind::Image, "image"),
(StyleSourceKind::Video, "video"),
(StyleSourceKind::Canvas, "canvas"),
(StyleSourceKind::Model, "model"),
];
for (kind, name) in &expected {
assert_eq!(kind.as_str(), *name, "mismatch for {kind:?}");
}
}
#[test]
fn mercator_projection_roundtrip_preserves_sub_meter_scale() {
let geo = GeoCoord::new(51.09916, 17.03664, 123.45);
let world = WebMercator::project(&geo);
let back = WebMercator::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-10);
}
#[test]
fn equirectangular_projection_roundtrip_preserves_sub_meter_scale() {
let geo = GeoCoord::new(-33.865143, 151.2099, 987.0);
let world = Equirectangular.project(&geo);
let back = Equirectangular.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-10);
}
#[test]
fn globe_projection_roundtrip_preserves_sub_meter_scale() {
use rustial_math::Globe;
let points = [
GeoCoord::new(0.0, 0.0, 0.0),
GeoCoord::new(51.5, -0.1, 100.0),
GeoCoord::new(-33.9, 151.2, 50.0),
GeoCoord::new(89.9, 45.0, 0.0),
GeoCoord::new(-89.9, -120.0, 0.0),
GeoCoord::new(0.0, 180.0, 0.0),
GeoCoord::new(0.0, -180.0, 0.0),
];
for geo in &points {
let world = Globe::project(geo);
let back = Globe::unproject(&world);
assert!(
(back.lat - geo.lat).abs() < 1e-6,
"Globe lat round-trip failed for {geo:?}: got {back:?}"
);
assert!(
(back.lon - geo.lon).abs() < 1e-6,
"Globe lon round-trip failed for {geo:?}: got {back:?}"
);
assert!(
(back.alt - geo.alt).abs() < 1.0,
"Globe alt round-trip failed for {geo:?}: got {back:?}"
);
}
}
#[test]
fn camera_projection_roundtrip_all_stable_variants() {
let projections = [
CameraProjection::WebMercator,
CameraProjection::Equirectangular,
];
let points = [
GeoCoord::new(0.0, 0.0, 0.0),
GeoCoord::new(51.5, -0.1, 250.0),
GeoCoord::new(-33.9, 151.2, 0.0),
GeoCoord::new(85.0, 179.0, 0.0),
GeoCoord::new(-85.0, -179.0, 0.0),
];
for proj in &projections {
for geo in &points {
let world = proj.project(geo);
let back = proj.unproject(&world);
assert!(
(back.lat - geo.lat).abs() < 1e-6,
"{proj:?} lat round-trip failed for {geo:?}: got {back:?}"
);
assert!(
(back.lon - geo.lon).abs() < 1e-6,
"{proj:?} lon round-trip failed for {geo:?}: got {back:?}"
);
}
}
}
#[test]
fn camera_projection_globe_roundtrip() {
let proj = CameraProjection::Globe;
let geo = GeoCoord::new(51.5, -0.1, 1200.0);
let world = proj.project(&geo);
let back = proj.unproject(&world);
assert!((back.lat - geo.lat).abs() < 1e-6);
assert!((back.lon - geo.lon).abs() < 1e-6);
assert!((back.alt - geo.alt).abs() < 1.0);
}
#[test]
fn camera_projection_vertical_perspective_roundtrip() {
let proj =
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 = proj.project(&geo);
assert!(world.position.x.is_finite());
assert!(world.position.y.is_finite());
let back = proj.unproject(&world);
assert!((back.lat - geo.lat).abs() < 1e-5);
assert!((back.lon - geo.lon).abs() < 1e-5);
}
#[test]
fn mercator_scale_factor_at_equator_is_unity() {
let sf = WebMercator.scale_factor(&GeoCoord::from_lat_lon(0.0, 0.0));
assert!((sf - 1.0).abs() < 1e-10);
}
#[test]
fn mercator_scale_factor_at_60_degrees_is_two() {
let sf = WebMercator.scale_factor(&GeoCoord::from_lat_lon(60.0, 0.0));
assert!((sf - 2.0).abs() < 1e-10);
}
#[test]
fn equirectangular_scale_factor_at_equator_is_unity() {
let sf = Equirectangular.scale_factor(&GeoCoord::from_lat_lon(0.0, 0.0));
assert!((sf - 1.0).abs() < 1e-10);
}
#[test]
fn camera_projection_scale_factor_delegates_correctly() {
let geo = GeoCoord::from_lat_lon(45.0, 10.0);
let merc_sf = CameraProjection::WebMercator.scale_factor(&geo);
let eq_sf = CameraProjection::Equirectangular.scale_factor(&geo);
let direct_merc = WebMercator.scale_factor(&geo);
let direct_eq = Equirectangular.scale_factor(&geo);
assert!((merc_sf - direct_merc).abs() < 1e-12);
assert!((eq_sf - direct_eq).abs() < 1e-12);
}
#[test]
fn anti_meridian_wrapping_matches_equivalent_longitudes_in_mercator() {
let a = GeoCoord::new(12.5, 179.9999, 0.0);
let b = GeoCoord {
lat: 12.5,
lon: -180.0001,
alt: 0.0,
}
.clamped_mercator();
let wa = WebMercator::project_clamped(&a);
let wb = WebMercator::project_clamped(&b);
assert!((wa.position.x - wb.position.x).abs() < 1e-6);
assert!((wa.position.y - wb.position.y).abs() < 1e-6);
}
#[test]
fn anti_meridian_longitude_wrapping_in_geo_coord() {
let a = GeoCoord {
lat: 0.0,
lon: 190.0,
alt: 0.0,
}
.clamped_mercator();
assert!((a.lon - (-170.0)).abs() < 1e-10);
let b = GeoCoord {
lat: 0.0,
lon: -190.0,
alt: 0.0,
}
.clamped_mercator();
assert!((b.lon - 170.0).abs() < 1e-10);
}
#[test]
fn tile_bounds_at_antimeridian_are_valid() {
let tile = TileId::new(1, 1, 0);
let bounds = CameraProjection::WebMercator.tile_geo_bounds(&tile);
assert!(
bounds.south() < bounds.north(),
"lat range must be positive"
);
assert!(
bounds.west() < bounds.east(),
"lon range must be positive for non-wrapping tile"
);
assert!(bounds.east() <= 180.0);
assert!(bounds.west() >= -180.0);
}
#[test]
fn tile_bounds_western_and_eastern_halves_cover_full_longitude() {
let west_bounds = CameraProjection::WebMercator.tile_geo_bounds(&TileId::new(1, 0, 0));
let east_bounds = CameraProjection::WebMercator.tile_geo_bounds(&TileId::new(1, 1, 0));
assert!((west_bounds.west() - (-180.0)).abs() < 1e-6);
assert!((east_bounds.east() - 180.0).abs() < 1e-6);
assert!((west_bounds.east() - east_bounds.west()).abs() < 1e-6);
}
#[test]
fn equirectangular_antimeridian_roundtrip() {
let east = GeoCoord::from_lat_lon(10.0, 180.0);
let west = GeoCoord::from_lat_lon(10.0, -180.0);
let east_back = Equirectangular.unproject(&Equirectangular.project(&east));
let west_back = Equirectangular.unproject(&Equirectangular.project(&west));
assert!((east_back.lon.abs() - 180.0).abs() < 1e-9);
assert!((west_back.lon.abs() - 180.0).abs() < 1e-9);
}
#[test]
fn geo_to_tile_wrapping_at_antimeridian() {
use rustial_math::geo_to_tile;
let tc = geo_to_tile(&GeoCoord::from_lat_lon(0.0, 179.999), 4);
let n = TileId::axis_tiles(4);
assert_eq!(tc.tile_id().x, n - 1);
let tc = geo_to_tile(&GeoCoord::from_lat_lon(0.0, -179.999), 4);
assert_eq!(tc.tile_id().x, 0);
}
#[test]
fn geodesic_distance_across_antimeridian_follows_short_path() {
use rustial_math::geodesic_distance;
let a = GeoCoord::from_lat_lon(10.0, 179.0);
let b = GeoCoord::from_lat_lon(10.0, -179.0);
let result = geodesic_distance(&a, &b).unwrap();
assert!(
result.distance < 500_000.0,
"Should follow short dateline crossing"
);
assert!(
result.distance > 100_000.0,
"Distance should be non-trivial"
);
}
#[test]
fn high_latitude_mercator_projection_remains_finite_near_limit() {
for lat in [85.0, 85.01, 85.051_129, -85.0, -85.01, -85.051_129] {
let world = WebMercator::project_clamped(&GeoCoord::from_lat_lon(lat, 45.0));
assert!(world.position.x.is_finite(), "x not finite for lat={lat}");
assert!(world.position.y.is_finite(), "y not finite for lat={lat}");
}
}
#[test]
fn high_latitude_mercator_checked_rejects_beyond_limit() {
assert!(WebMercator::project_checked(&GeoCoord::from_lat_lon(86.0, 0.0)).is_none());
assert!(WebMercator::project_checked(&GeoCoord::from_lat_lon(-86.0, 0.0)).is_none());
}
#[test]
fn high_latitude_scale_factor_diverges_appropriately() {
let sf = WebMercator.scale_factor(&GeoCoord::from_lat_lon(85.0, 0.0));
assert!(sf > 10.0, "Scale factor at 85° should be > 10, got {sf}");
assert!(sf < 15.0, "Scale factor at 85° should be < 15, got {sf}");
}
#[test]
fn equirectangular_handles_poles() {
let north = GeoCoord::from_lat_lon(90.0, 0.0);
let w = Equirectangular.project(&north);
let back = Equirectangular.unproject(&w);
assert!((back.lat - 90.0).abs() < 1e-9);
let south = GeoCoord::from_lat_lon(-90.0, 0.0);
let w = Equirectangular.project(&south);
let back = Equirectangular.unproject(&w);
assert!((back.lat + 90.0).abs() < 1e-9);
}
#[test]
fn tile_at_high_latitude_has_valid_bounds() {
let tile = TileId::new(4, 8, 0);
let bounds = CameraProjection::WebMercator.tile_geo_bounds(&tile);
assert!(bounds.north().is_finite());
assert!(
bounds.north() > 80.0,
"Northernmost tile should be above 80°"
);
assert!(bounds.south() < bounds.north());
}
#[test]
fn geo_to_tile_at_mercator_limit_is_valid() {
use rustial_math::geo_to_tile;
let coord = GeoCoord::from_lat_lon(85.05, 0.0);
let tc = geo_to_tile(&coord, 4);
assert!(tc.y >= 0.0);
let tid = tc.tile_id();
assert!(tid.y < TileId::axis_tiles(4));
}
#[test]
fn large_world_precision_preserves_meter_scale_deltas() {
let base = WebMercator::project(&GeoCoord::from_lat_lon(0.0, 179.999));
let shifted = rustial_math::WorldCoord::new(
base.position.x + 0.25,
base.position.y - 0.5,
base.position.z + 3.0,
);
assert!((shifted.position.x - base.position.x - 0.25).abs() < 1e-12);
assert!((shifted.position.y - base.position.y + 0.5).abs() < 1e-12);
assert!((shifted.position.z - base.position.z - 3.0).abs() < 1e-12);
}
#[test]
fn camera_relative_rendering_preserves_f32_precision() {
let target = WebMercator::project(&GeoCoord::from_lat_lon(51.5, -0.1));
let nearby = WebMercator::project(&GeoCoord::from_lat_lon(51.5001, -0.0999));
let dx = nearby.position.x - target.position.x;
let dy = nearby.position.y - target.position.y;
let dx_f32 = dx as f32;
let dy_f32 = dy as f32;
assert!(
(dx_f32 as f64 - dx).abs() < 0.01,
"Camera-relative X should survive f32 cast: dx={dx}, dx_f32={dx_f32}"
);
assert!(
(dy_f32 as f64 - dy).abs() < 0.01,
"Camera-relative Y should survive f32 cast: dy={dy}, dy_f32={dy_f32}"
);
}
#[test]
fn absolute_world_coords_do_not_survive_f32_at_extreme_longitude() {
let world = WebMercator::project(&GeoCoord::from_lat_lon(0.0, 179.999));
let abs_x = world.position.x;
let abs_x_f32 = abs_x as f32;
assert!(
(abs_x_f32 as f64 - abs_x).abs() > 0.1,
"Absolute coordinates should lose precision in f32 (proving camera-relative is needed)"
);
}
#[test]
fn geodesic_distance_london_paris_matches_known_value() {
use rustial_math::geodesic_distance;
let london = GeoCoord::from_lat_lon(51.5074, -0.1278);
let paris = GeoCoord::from_lat_lon(48.8566, 2.3522);
let result = geodesic_distance(&london, &paris).unwrap();
assert!(
(result.distance - 343_500.0).abs() < 1500.0,
"London-Paris distance should be ~343.5 km, got {:.0} m",
result.distance
);
}
#[test]
fn geodesic_distance_is_symmetric() {
use rustial_math::geodesic_distance;
let a = GeoCoord::from_lat_lon(40.0, -74.0);
let b = GeoCoord::from_lat_lon(51.5, -0.1);
let ab = geodesic_distance(&a, &b).unwrap();
let ba = geodesic_distance(&b, &a).unwrap();
assert!((ab.distance - ba.distance).abs() < 1e-6);
}
#[test]
fn geodesic_direct_inverse_roundtrip() {
use rustial_math::{geodesic_destination, geodesic_distance};
let start = GeoCoord::from_lat_lon(40.0, -74.0);
let azimuth = 0.8;
let dist = 500_000.0;
let dest = geodesic_destination(&start, azimuth, dist).unwrap();
let inv = geodesic_distance(&start, &dest).unwrap();
assert!(
(inv.distance - dist).abs() < 0.01,
"Round-trip distance should match"
);
}
#[test]
fn world_coord_arithmetic_stable_far_from_origin() {
let extent = WebMercator::max_extent();
let w1 = rustial_math::WorldCoord::new(extent - 1.0, extent - 1.0, 0.0);
let w2 = rustial_math::WorldCoord::new(extent - 0.99, extent - 0.99, 0.0);
let dx = w2.position.x - w1.position.x;
let dy = w2.position.y - w1.position.y;
assert!(
(dx - 0.01).abs() < 1e-8,
"f64 should preserve cm-scale delta at extent edge"
);
assert!(
(dy - 0.01).abs() < 1e-8,
"f64 should preserve cm-scale delta at extent edge"
);
}
#[test]
fn terrain_elevation_sampling_is_bilinear_and_meter_based() {
let grid =
ElevationGrid::from_data(TileId::new(0, 0, 0), 2, 2, vec![0.0, 100.0, 200.0, 300.0])
.unwrap();
let center = GeoCoord::from_lat_lon(0.0, 0.0);
let elev = grid.sample_geo(¢er).unwrap();
assert!((elev - 150.0).abs() < 1e-3);
}
#[test]
fn terrain_elevation_grid_corners_return_exact_values() {
let grid =
ElevationGrid::from_data(TileId::new(0, 0, 0), 2, 2, vec![10.0, 20.0, 30.0, 40.0])
.unwrap();
assert!((grid.sample(0.0, 0.0).unwrap() - 10.0).abs() < 1e-6);
assert!((grid.sample(1.0, 0.0).unwrap() - 20.0).abs() < 1e-6);
assert!((grid.sample(0.0, 1.0).unwrap() - 30.0).abs() < 1e-6);
assert!((grid.sample(1.0, 1.0).unwrap() - 40.0).abs() < 1e-6);
}
#[test]
fn terrain_elevation_grid_edge_clamping() {
let grid =
ElevationGrid::from_data(TileId::new(0, 0, 0), 2, 2, vec![100.0, 200.0, 300.0, 400.0])
.unwrap();
assert!((grid.sample(-1.0, -1.0).unwrap() - 100.0).abs() < 1e-6);
assert!((grid.sample(2.0, 2.0).unwrap() - 400.0).abs() < 1e-6);
}
#[test]
fn terrain_elevation_altitude_preserved_through_projection() {
let geo = GeoCoord::new(45.0, 12.0, 1234.5);
let world = WebMercator::project(&geo);
assert!((world.position.z - 1234.5).abs() < 1e-12);
let back = WebMercator::unproject(&world);
assert!((back.alt - 1234.5).abs() < 1e-12);
}
#[test]
fn terrain_flat_elevation_source_produces_zero_grid() {
use crate::terrain::ElevationSource;
let src = FlatElevationSource::new(4, 4);
src.request(TileId::new(5, 10, 10));
let results = src.poll();
assert_eq!(results.len(), 1);
let (id, grid_result) = &results[0];
assert_eq!(*id, TileId::new(5, 10, 10));
let grid = grid_result.as_ref().unwrap();
assert_eq!(grid.min_elev, 0.0);
assert_eq!(grid.max_elev, 0.0);
assert!((grid.sample(0.5, 0.5).unwrap() - 0.0).abs() < 1e-6);
}
#[test]
fn terrain_elevation_grid_min_max_are_correct() {
let data = vec![-50.0, 0.0, 100.0, 8848.0]; let grid = ElevationGrid::from_data(TileId::new(0, 0, 0), 2, 2, data).unwrap();
assert!((grid.min_elev - (-50.0)).abs() < 1e-6);
assert!((grid.max_elev - 8848.0).abs() < 1e-6);
assert!((grid.elevation_range() - 8898.0).abs() < 1e-6);
}
#[test]
fn terrain_elevation_neighboring_tile_edges_are_queryable() {
let tile_a = TileId::new(1, 0, 0);
let tile_b = TileId::new(1, 1, 0);
let grid_a =
ElevationGrid::from_data(tile_a, 2, 2, vec![100.0, 200.0, 300.0, 400.0]).unwrap();
let grid_b =
ElevationGrid::from_data(tile_b, 2, 2, vec![200.0, 500.0, 400.0, 600.0]).unwrap();
let east_edge_a = grid_a.sample(1.0, 0.5).unwrap();
let west_edge_b = grid_b.sample(0.0, 0.5).unwrap();
assert!(east_edge_a.is_finite());
assert!(west_edge_b.is_finite());
}
#[test]
fn mercator_world_size_is_double_extent() {
assert!((WebMercator::world_size() - 2.0 * WebMercator::max_extent()).abs() < 1e-6);
}
#[test]
fn one_degree_longitude_at_equator_is_approximately_111km() {
let a = WebMercator::project(&GeoCoord::from_lat_lon(0.0, 0.0));
let b = WebMercator::project(&GeoCoord::from_lat_lon(0.0, 1.0));
let dx = (b.position.x - a.position.x).abs();
assert!(
(dx - 111_319.49).abs() < 1.0,
"1° lon at equator should be ~111,319 m, got {dx}"
);
}
#[test]
fn one_degree_latitude_at_equator_is_approximately_111km_in_equirectangular() {
let a = Equirectangular.project(&GeoCoord::from_lat_lon(0.0, 0.0));
let b = Equirectangular.project(&GeoCoord::from_lat_lon(1.0, 0.0));
let dy = (b.position.y - a.position.y).abs();
assert!(
(dy - 111_319.49).abs() < 1.0,
"1° lat should be ~111,319 m in equirectangular, got {dy}"
);
}
#[test]
fn mercator_max_extent_matches_pi_times_earth_radius() {
use rustial_math::Ellipsoid;
let expected = Ellipsoid::WGS84.a * std::f64::consts::PI;
assert!((WebMercator::max_extent() - expected).abs() < 1e-6);
}
#[test]
fn projection_bounds_documented_and_correct() {
use rustial_math::Projection;
let merc_bounds = WebMercator.projection_bounds();
assert!((merc_bounds.south() - (-85.051_129)).abs() < 1e-3);
assert!((merc_bounds.north() - 85.051_129).abs() < 1e-3);
assert!((merc_bounds.west() - (-180.0)).abs() < 1e-10);
assert!((merc_bounds.east() - 180.0).abs() < 1e-10);
let eq_bounds = Equirectangular.projection_bounds();
assert!((eq_bounds.south() - (-90.0)).abs() < 1e-10);
assert!((eq_bounds.north() - 90.0).abs() < 1e-10);
}
#[test]
fn camera_projection_tile_bounds_cross_antimeridian_at_world_edge() {
let projection = CameraProjection::WebMercator;
let tile = TileId::new(1, 1, 0);
let bounds = projection.tile_geo_bounds(&tile);
assert!(bounds.west() >= 0.0);
assert!(bounds.east() <= 180.0);
assert!(bounds.south() < bounds.north());
}
#[test]
fn tile_world_bounds_are_meter_based() {
use rustial_math::tile_bounds_world;
let tile = TileId::new(0, 0, 0);
let bounds = tile_bounds_world(&tile);
let expected_size = WebMercator::world_size();
let actual_width = bounds.max.position.x - bounds.min.position.x;
assert!(
(actual_width - expected_size).abs() < 1.0,
"Tile 0/0/0 width should be ~{expected_size:.0} m, got {actual_width:.0} m"
);
}
#[test]
fn camera_meters_per_pixel_is_positive_for_all_modes() {
use crate::camera::Camera;
let mut cam = Camera::default();
cam.set_distance(100_000.0);
cam.set_viewport(800, 600);
cam.set_mode(CameraMode::Perspective);
let mpp_persp = cam.meters_per_pixel();
assert!(mpp_persp > 0.0 && mpp_persp.is_finite());
cam.set_mode(CameraMode::Orthographic);
let mpp_ortho = cam.meters_per_pixel();
assert!(mpp_ortho > 0.0 && mpp_ortho.is_finite());
}
#[test]
fn camera_meters_per_pixel_decreases_with_zoom_in() {
use crate::camera::Camera;
let mut cam = Camera::default();
cam.set_viewport(800, 600);
cam.set_distance(100_000.0);
let mpp_far = cam.meters_per_pixel();
cam.set_distance(1_000.0);
let mpp_close = cam.meters_per_pixel();
assert!(
mpp_close < mpp_far,
"Closer camera should have finer resolution"
);
}
#[test]
fn geo_coord_new_checked_rejects_out_of_range() {
assert!(GeoCoord::new_checked(91.0, 0.0, 0.0).is_none());
assert!(GeoCoord::new_checked(-91.0, 0.0, 0.0).is_none());
assert!(GeoCoord::new_checked(0.0, 181.0, 0.0).is_none());
assert!(GeoCoord::new_checked(0.0, -181.0, 0.0).is_none());
assert!(GeoCoord::new_checked(90.0, 180.0, 0.0).is_some());
assert!(GeoCoord::new_checked(-90.0, -180.0, 0.0).is_some());
}
#[test]
fn tile_id_quadkey_roundtrip() {
let tile = TileId::new(10, 500, 300);
let qk = tile.quadkey();
let back = TileId::from_quadkey(&qk).unwrap();
assert_eq!(back, tile);
}
#[test]
fn tile_parent_child_relationship_is_consistent() {
let parent = TileId::new(5, 10, 12);
let children = parent.children();
for child in &children {
assert_eq!(child.zoom, parent.zoom + 1);
assert_eq!(child.parent().unwrap(), parent);
}
}
#[test]
fn geo_to_tile_and_tile_to_geo_roundtrip_at_zoom_10() {
use rustial_math::{geo_to_tile, tile_to_geo};
let original = GeoCoord::from_lat_lon(51.5, 17.0);
let tc = geo_to_tile(&original, 10);
let tile = tc.tile_id();
let nw = tile_to_geo(&tile);
assert!((nw.lat - original.lat).abs() < 0.5);
assert!((nw.lon - original.lon).abs() < 0.5);
}
#[test]
fn globe_projection_equator_radius_matches_wgs84() {
use rustial_math::{Ellipsoid, Globe};
let geo = GeoCoord::from_lat_lon(0.0, 0.0);
let world = Globe::project(&geo);
let radius =
(world.position.x.powi(2) + world.position.y.powi(2) + world.position.z.powi(2)).sqrt();
assert!(
(radius - Ellipsoid::WGS84.a).abs() < 1.0,
"Globe radius at equator should be ~{:.0} m, got {radius:.0} m",
Ellipsoid::WGS84.a
);
}
#[test]
fn globe_projection_pole_height_matches_wgs84_semi_minor() {
use rustial_math::{Ellipsoid, Globe};
let north = GeoCoord::from_lat_lon(90.0, 0.0);
let world = Globe::project(&north);
assert!(
(world.position.z - Ellipsoid::WGS84.b).abs() < 1.0,
"Globe Z at north pole should be ~{:.0} m, got {:.0} m",
Ellipsoid::WGS84.b,
world.position.z
);
}
#[test]
fn visible_tiles_at_zoom_0_returns_single_tile() {
use rustial_math::{visible_tiles, WorldBounds, WorldCoord};
let extent = WebMercator::max_extent();
let bounds = WorldBounds::new(
WorldCoord::new(-extent, -extent, 0.0),
WorldCoord::new(extent, extent, 0.0),
);
let tiles = visible_tiles(&bounds, 0);
assert_eq!(tiles.len(), 1);
assert_eq!(tiles[0], TileId::new(0, 0, 0));
}
#[test]
fn visible_tiles_no_duplicates() {
use rustial_math::{visible_tiles, WorldBounds, WorldCoord};
let bounds = WorldBounds::new(
WorldCoord::new(-5_000_000.0, -5_000_000.0, 0.0),
WorldCoord::new(5_000_000.0, 5_000_000.0, 0.0),
);
let tiles = visible_tiles(&bounds, 8);
let unique: std::collections::HashSet<_> = tiles.iter().collect();
assert_eq!(
tiles.len(),
unique.len(),
"visible_tiles should produce no duplicates"
);
}
#[test]
fn tile_bounds_world_tiles_partition_without_overlap_at_zoom_2() {
use rustial_math::tile_bounds_world;
let n = TileId::axis_tiles(2);
for y in 0..n {
for x in 0..n - 1 {
let left = tile_bounds_world(&TileId::new(2, x, y));
let right = tile_bounds_world(&TileId::new(2, x + 1, y));
assert!(
(left.max.position.x - right.min.position.x).abs() < 1e-3,
"Tiles ({x},{y}) and ({},{y}) should share an edge",
x + 1
);
}
}
}
#[test]
fn camera_relative_origin_equals_target_world() {
use crate::camera::Camera;
let mut cam = Camera::default();
cam.set_target(GeoCoord::from_lat_lon(51.5, -0.1));
cam.set_distance(50_000.0);
let target_world = cam.target_world();
assert!(target_world.x.is_finite());
assert!(target_world.y.is_finite());
assert!(target_world.z.is_finite());
let projected = cam.projection().project(cam.target());
assert!((target_world.x - projected.position.x).abs() < 1e-6);
assert!((target_world.y - projected.position.y).abs() < 1e-6);
}
#[test]
fn camera_relative_origin_consistent_across_projections() {
use crate::camera::Camera;
let target = GeoCoord::from_lat_lon(35.0, 139.0);
for proj in [
CameraProjection::WebMercator,
CameraProjection::Equirectangular,
] {
let mut cam = Camera::default();
cam.set_projection(proj);
cam.set_target(target);
let origin = cam.target_world();
let expected = proj.project(&target);
assert!(
(origin.x - expected.position.x).abs() < 1e-6,
"{proj:?}: origin X mismatch"
);
assert!(
(origin.y - expected.position.y).abs() < 1e-6,
"{proj:?}: origin Y mismatch"
);
}
}
#[test]
fn view_projection_f32_cast_preserves_near_field_structure() {
use crate::camera::Camera;
let mut cam = Camera::default();
cam.set_target(GeoCoord::from_lat_lon(51.5, -0.1));
cam.set_distance(10_000.0);
cam.set_pitch(0.8);
cam.set_viewport(1920, 1080);
let vp_f64 = cam.view_projection_matrix();
let vp_f32 = vp_f64.as_mat4();
for col in 0..4 {
let c = vp_f32.col(col);
assert!(
c.x.is_finite() && c.y.is_finite() && c.z.is_finite() && c.w.is_finite(),
"VP f32 column {col} has non-finite elements"
);
}
for col in 0..4 {
let f64_col = vp_f64.col(col);
let f32_col = vp_f32.col(col);
for row in 0..4 {
let a = f64_col[row];
let b = f32_col[row] as f64;
if a.abs() > 1e-10 {
let rel = ((a - b) / a).abs();
assert!(
rel < 1e-5,
"VP element [{row},{col}] relative error {rel:.2e} exceeds threshold"
);
}
}
}
}
#[test]
fn view_projection_f32_finite_for_all_camera_modes_and_projections() {
use crate::camera::{Camera, CameraMode};
let targets = [
GeoCoord::from_lat_lon(0.0, 0.0),
GeoCoord::from_lat_lon(51.5, -0.1),
GeoCoord::from_lat_lon(-33.9, 151.2),
GeoCoord::from_lat_lon(0.0, 179.99),
];
let projections = [
CameraProjection::WebMercator,
CameraProjection::Equirectangular,
];
let modes = [CameraMode::Perspective, CameraMode::Orthographic];
for target in &targets {
for proj in &projections {
for mode in &modes {
let mut cam = Camera::default();
cam.set_projection(*proj);
cam.set_target(*target);
cam.set_distance(100_000.0);
cam.set_mode(*mode);
cam.set_viewport(800, 600);
let vp = cam.view_projection_matrix();
let vp_f32 = vp.as_mat4();
for col in 0..4 {
let c = vp_f32.col(col);
assert!(
c.x.is_finite()
&& c.y.is_finite()
&& c.z.is_finite()
&& c.w.is_finite(),
"Non-finite VP f32 for target={target:?}, proj={proj:?}, mode={mode:?}, col={col}"
);
}
}
}
}
}
#[test]
fn camera_relative_tile_vertex_survives_f32_at_extreme_longitude() {
let tile = TileId::new(4, 15, 8);
let cam_target = GeoCoord::from_lat_lon(0.0, 170.0);
let cam_origin = CameraProjection::WebMercator.project(&cam_target);
let tile_corner = CameraProjection::WebMercator.project_tile_corner(&tile, 0.0, 0.0);
let rel_x = tile_corner[0] - cam_origin.position.x;
let rel_y = tile_corner[1] - cam_origin.position.y;
let rel_x_f32 = rel_x as f32;
let rel_y_f32 = rel_y as f32;
assert!(
(rel_x_f32 as f64 - rel_x).abs() < 1.0,
"Camera-relative tile X should survive f32: rel_x={rel_x}, f32={rel_x_f32}"
);
assert!(
(rel_y_f32 as f64 - rel_y).abs() < 1.0,
"Camera-relative tile Y should survive f32: rel_y={rel_y}, f32={rel_y_f32}"
);
}
#[test]
fn camera_relative_positions_sub_meter_for_nearby_features() {
let cam_target = GeoCoord::from_lat_lon(51.5, -0.1);
let cam_origin = CameraProjection::WebMercator.project(&cam_target);
let feature_a = CameraProjection::WebMercator.project(&GeoCoord::from_lat_lon(51.5, -0.1));
let feature_b =
CameraProjection::WebMercator.project(&GeoCoord::from_lat_lon(51.50009, -0.1));
let rel_a_y = (feature_a.position.y - cam_origin.position.y) as f32;
let rel_b_y = (feature_b.position.y - cam_origin.position.y) as f32;
let delta = (rel_b_y - rel_a_y).abs();
assert!(
delta > 5.0,
"10m separation should be visible in f32: delta={delta}"
);
}
#[test]
fn model_instance_camera_relative_placement_is_sub_meter() {
let cam_target = GeoCoord::from_lat_lon(48.8566, 2.3522); let cam_origin = CameraProjection::WebMercator.project(&cam_target);
let model_pos = GeoCoord::new(48.8567, 2.3523, 50.0);
let model_world = CameraProjection::WebMercator.project(&model_pos);
let rel_x = (model_world.position.x - cam_origin.position.x) as f32;
let rel_y = (model_world.position.y - cam_origin.position.y) as f32;
let rel_z = (model_pos.alt - cam_origin.position.z) as f32;
assert!(rel_x.abs() < 500.0, "Model rel_x should be small: {rel_x}");
assert!(rel_y.abs() < 500.0, "Model rel_y should be small: {rel_y}");
assert!(
(rel_z - 50.0).abs() < 0.01,
"Model altitude offset: {rel_z}"
);
let err_x = (rel_x as f64 - (model_world.position.x - cam_origin.position.x)).abs();
let err_y = (rel_y as f64 - (model_world.position.y - cam_origin.position.y)).abs();
assert!(err_x < 0.001, "Model X f32 error should be < 1mm: {err_x}");
assert!(err_y < 0.001, "Model Y f32 error should be < 1mm: {err_y}");
}
#[test]
fn eye_offset_finite_for_all_pitch_yaw_combinations() {
use crate::camera::Camera;
let mut cam = Camera::default();
cam.set_distance(50_000.0);
for pitch_deg in (0..=85).step_by(5) {
for yaw_deg in (0..=350).step_by(10) {
cam.set_pitch((pitch_deg as f64).to_radians());
cam.set_yaw((yaw_deg as f64).to_radians());
let eye = cam.eye_offset();
assert!(
eye.x.is_finite() && eye.y.is_finite() && eye.z.is_finite(),
"Non-finite eye offset at pitch={pitch_deg}°, yaw={yaw_deg}°"
);
}
}
}
#[test]
fn meters_per_pixel_consistent_between_projections_at_equator() {
use crate::camera::Camera;
let mut cam_merc = Camera::default();
cam_merc.set_projection(CameraProjection::WebMercator);
cam_merc.set_target(GeoCoord::from_lat_lon(0.0, 0.0));
cam_merc.set_distance(100_000.0);
cam_merc.set_viewport(800, 600);
let mut cam_eq = Camera::default();
cam_eq.set_projection(CameraProjection::Equirectangular);
cam_eq.set_target(GeoCoord::from_lat_lon(0.0, 0.0));
cam_eq.set_distance(100_000.0);
cam_eq.set_viewport(800, 600);
let mpp_merc = cam_merc.meters_per_pixel();
let mpp_eq = cam_eq.meters_per_pixel();
assert!(
(mpp_merc - mpp_eq).abs() / mpp_merc < 0.01,
"MPP diverges at equator: merc={mpp_merc}, eq={mpp_eq}"
);
}
#[test]
fn terrain_tile_geo_bounds_are_ordered_and_within_valid_range() {
for zoom in 0..=5 {
let n = TileId::axis_tiles(zoom);
for y in 0..n {
for x in 0..n {
let tile = TileId::new(zoom, x, y);
let bounds = CameraProjection::WebMercator.tile_geo_bounds(&tile);
assert!(
bounds.south() < bounds.north(),
"Tile {tile:?}: lat range inverted"
);
assert!(
bounds.west() < bounds.east(),
"Tile {tile:?}: lon range inverted"
);
assert!(bounds.north() <= 90.0);
assert!(bounds.south() >= -90.0);
assert!(bounds.east() <= 180.0);
assert!(bounds.west() >= -180.0);
}
}
}
}
#[test]
fn view_matrix_eye_looks_toward_origin() {
use crate::camera::Camera;
use glam::DVec3;
let mut cam = Camera::default();
cam.set_target(GeoCoord::from_lat_lon(40.0, -74.0));
cam.set_distance(50_000.0);
cam.set_pitch(0.5);
cam.set_yaw(0.3);
let view = cam.view_matrix(DVec3::ZERO);
let target_in_view = view * glam::DVec4::new(0.0, 0.0, 0.0, 1.0);
assert!(
target_in_view.z < 0.0,
"Target should be in front of camera (negative Z in view space), got z={}",
target_in_view.z
);
}
#[test]
fn view_matrix_determinant_is_nonzero() {
use crate::camera::Camera;
use glam::DVec3;
let mut cam = Camera::default();
cam.set_distance(10_000.0);
cam.set_pitch(1.0);
cam.set_yaw(2.5);
let view = cam.view_matrix(DVec3::ZERO);
let det = view.determinant();
assert!(
det.abs() > 1e-10,
"View matrix should be invertible, got det={det}"
);
}
#[test]
fn projection_matrix_near_less_than_far_for_all_modes() {
use crate::camera::{Camera, CameraMode};
for mode in [CameraMode::Perspective, CameraMode::Orthographic] {
let mut cam = Camera::default();
cam.set_mode(mode);
cam.set_distance(10_000.0);
cam.set_viewport(800, 600);
let proj = cam.projection_matrix();
let det = proj.determinant();
assert!(
det.abs() > 1e-20,
"{mode:?}: projection matrix should be invertible, det={det}"
);
}
}
#[test]
fn high_zoom_tile_camera_relative_offset_is_f32_safe() {
let zoom = 14;
let cam_target = GeoCoord::from_lat_lon(51.5, -0.1);
let cam_origin = CameraProjection::WebMercator.project(&cam_target);
let tc = rustial_math::geo_to_tile(&cam_target, zoom);
let center_tile = tc.tile_id();
for dy in -1i32..=1 {
for dx in -1i32..=1 {
let x = (center_tile.x as i32 + dx).max(0) as u32;
let y = (center_tile.y as i32 + dy).max(0) as u32;
let tile = TileId::new(zoom, x, y);
for (u, v) in [(0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (1.0, 1.0)] {
let corner = CameraProjection::WebMercator.project_tile_corner(&tile, u, v);
let rel_x = corner[0] - cam_origin.position.x;
let rel_y = corner[1] - cam_origin.position.y;
let rel_x_f32 = rel_x as f32;
let rel_y_f32 = rel_y as f32;
let err_x = (rel_x_f32 as f64 - rel_x).abs();
let err_y = (rel_y_f32 as f64 - rel_y).abs();
assert!(
err_x < 0.01,
"Tile {tile:?} corner ({u},{v}) X f32 error = {err_x:.6}m > 1cm"
);
assert!(
err_y < 0.01,
"Tile {tile:?} corner ({u},{v}) Y f32 error = {err_y:.6}m > 1cm"
);
}
}
}
}
#[test]
fn scene_origin_quantisation_is_stable() {
let origin = WebMercator::project(&GeoCoord::from_lat_lon(51.5, -0.1));
let key_a = [
(origin.position.x * 100.0) as i64,
(origin.position.y * 100.0) as i64,
(origin.position.z * 100.0) as i64,
];
let key_b = [
(origin.position.x * 100.0) as i64,
(origin.position.y * 100.0) as i64,
(origin.position.z * 100.0) as i64,
];
assert_eq!(key_a, key_b);
}
#[test]
fn elevation_data_survives_f32_for_full_earth_range() {
let test_values: &[f64] = &[-11034.0, -430.0, 0.0, 100.0, 1234.5, 8848.0];
for &elev in test_values {
let elev_f32 = elev as f32;
let roundtrip = elev_f32 as f64;
let err = (roundtrip - elev).abs();
assert!(
err < 0.01,
"Elevation {elev}m ? f32 ? f64 error = {err:.6}m (> 1cm)"
);
}
}
}