use super::*;
const EPSILON: f64 = 1e-6;
fn approx_eq(a: f64, b: f64) -> bool {
(a - b).abs() < EPSILON
}
#[test]
fn test_project_origin() {
let p = project(0.0, 0.0);
assert!(approx_eq(p.x, 0.5), "x={}, expected 0.5", p.x);
assert!(approx_eq(p.y, 0.5), "y={}, expected 0.5", p.y);
}
#[test]
fn test_project_copenhagen() {
let p = project(55.68, 12.57);
assert!(approx_eq(p.x, 0.534_917), "x={}", p.x);
assert!(
p.y < 0.5,
"y={} should be < 0.5 for northern hemisphere",
p.y
);
assert!(p.y > 0.0, "y={} should be > 0.0", p.y);
assert!(approx_eq(p.y, 0.312_976), "y={}", p.y);
}
#[test]
fn test_project_e7() {
let p = project_e7(556_800_000, 125_700_000);
let p2 = project(55.68, 12.57);
assert!(approx_eq(p.x, p2.x), "x: {} vs {}", p.x, p2.x);
assert!(approx_eq(p.y, p2.y), "y: {} vs {}", p.y, p2.y);
}
#[test]
fn test_project_e7_lut_accuracy() {
for lat_deg in -85..=85 {
#[allow(clippy::cast_possible_truncation)]
let lat_e7 = (lat_deg as f64 * 1e7) as i32;
let lut_p = project_e7(lat_e7, 0);
let exact_p = project(lat_deg as f64, 0.0);
assert!(
(lut_p.y - exact_p.y).abs() < EPSILON,
"LUT mismatch at lat={lat_deg}°: lut={}, exact={}, diff={}",
lut_p.y,
exact_p.y,
(lut_p.y - exact_p.y).abs(),
);
}
}
#[test]
fn test_project_extreme_latitude_clamped() {
let p_north = project(90.0, 0.0);
let p_max = project(MAX_LATITUDE, 0.0);
assert!(
approx_eq(p_north.y, p_max.y),
"90° should clamp to same as {MAX_LATITUDE}°: {} vs {}",
p_north.y,
p_max.y,
);
}
#[test]
fn test_merc_y_to_lat_roundtrip() {
let lat = 55.68;
let p = project(lat, 0.0);
let recovered_lat = merc_y_to_lat(p.y);
assert!(
approx_eq(recovered_lat, lat),
"roundtrip lat: {recovered_lat} vs {lat}",
);
}
#[test]
fn test_merc_to_tile_px_center() {
let (px, py) = merc_to_tile_px(&Point::new(0.5, 0.5), 0, 0, 0);
assert_eq!(px, 2048);
assert_eq!(py, 2048);
}
#[test]
fn test_merc_to_tile_px_origin() {
let (px, py) = merc_to_tile_px(&Point::new(0.0, 0.0), 0, 0, 0);
assert_eq!(px, 0);
assert_eq!(py, 0);
}
#[test]
fn test_simplify_triangle_preserved() {
let points = vec![
Point::new(0.0, 0.0),
Point::new(0.5, 1.0),
Point::new(1.0, 0.0),
];
let simplified = simplify(&points, 0.01);
assert_eq!(
simplified.len(),
3,
"triangle should be preserved with small tolerance"
);
}
#[test]
fn test_simplify_triangle_collapsed() {
let points = vec![
Point::new(0.0, 0.0),
Point::new(0.5, 0.001), Point::new(1.0, 0.0),
];
let simplified = simplify(&points, 0.01);
assert_eq!(
simplified.len(),
2,
"near-collinear point should be removed"
);
}
#[test]
fn test_simplify_two_points() {
let points = vec![Point::new(0.0, 0.0), Point::new(1.0, 1.0)];
let simplified = simplify(&points, 0.1);
assert_eq!(simplified.len(), 2, "two-point line always preserved");
}
#[test]
fn test_simplify_preserves_endpoints() {
let points = vec![
Point::new(0.0, 0.0),
Point::new(0.25, 0.0001),
Point::new(0.5, 0.0001),
Point::new(0.75, 0.0001),
Point::new(1.0, 0.0),
];
let simplified = simplify(&points, 0.01);
assert!(approx_eq(simplified[0].x, 0.0), "first point preserved");
assert!(
approx_eq(simplified[simplified.len() - 1].x, 1.0),
"last point preserved",
);
}
#[test]
fn test_simplify_with_required_preserves_pinned_vertex() {
let points = vec![
Point::new(0.0, 0.0),
Point::new(0.25, 0.0001),
Point::new(0.5, 0.0001),
Point::new(0.75, 0.0001),
Point::new(1.0, 0.0),
];
let mut keep = Vec::new();
let mut out = Vec::new();
let _ = simplify_into_with_required(&points, 0.01, &[2], &mut keep, &mut out);
assert!(
out.iter()
.any(|p| approx_eq(p.x, 0.5) && approx_eq(p.y, 0.0001)),
"required interior point should survive DP",
);
}
#[test]
fn test_simplify_with_required_ignores_out_of_range_indices() {
let points = vec![
Point::new(0.0, 0.0),
Point::new(0.5, 0.0),
Point::new(1.0, 0.0),
];
let mut keep = Vec::new();
let mut out = Vec::new();
let _ = simplify_into_with_required(&points, 0.01, &[999], &mut keep, &mut out);
assert_eq!(out.len(), 2, "invalid required index must be ignored");
}
#[test]
fn test_clip_line_crossing() {
let rect = ClipRect::new(0.0, 0.0, 1.0, 1.0);
let line = vec![Point::new(-0.5, 0.5), Point::new(1.5, 0.5)];
let clipped = clip_linestring(&line, &rect);
assert_eq!(clipped.len(), 1, "should produce one sub-line");
let seg = &clipped[0];
assert_eq!(seg.len(), 2);
assert!(
approx_eq(seg[0].x, 0.0),
"entry at left edge: x={}",
seg[0].x
);
assert!(
approx_eq(seg[1].x, 1.0),
"exit at right edge: x={}",
seg[1].x
);
}
#[test]
fn test_clip_line_fully_inside() {
let rect = ClipRect::new(0.0, 0.0, 1.0, 1.0);
let line = vec![Point::new(0.2, 0.2), Point::new(0.8, 0.8)];
let clipped = clip_linestring(&line, &rect);
assert_eq!(clipped.len(), 1);
assert_eq!(clipped[0].len(), 2);
}
#[test]
fn test_clip_line_fully_outside() {
let rect = ClipRect::new(0.0, 0.0, 1.0, 1.0);
let line = vec![Point::new(2.0, 2.0), Point::new(3.0, 3.0)];
let clipped = clip_linestring(&line, &rect);
assert!(
clipped.is_empty(),
"line fully outside should produce no output"
);
}
#[test]
fn test_clip_line_multiple_crossings() {
let rect = ClipRect::new(0.0, 0.0, 1.0, 1.0);
let line = vec![
Point::new(-0.5, 0.5),
Point::new(0.5, 0.5),
Point::new(1.5, 0.5),
Point::new(2.5, 0.5), ];
let clipped = clip_linestring(&line, &rect);
assert_eq!(clipped.len(), 1, "should produce one contiguous sub-line");
}
#[test]
fn test_clip_polygon_fully_inside() {
let rect = ClipRect::new(0.0, 0.0, 1.0, 1.0);
let ring = vec![
Point::new(0.2, 0.2),
Point::new(0.8, 0.2),
Point::new(0.8, 0.8),
Point::new(0.2, 0.8),
];
let clipped = clip_polygon(&ring, &rect);
assert_eq!(clipped.len(), 4, "fully inside polygon unchanged");
}
#[test]
fn test_clip_polygon_partially_outside() {
let rect = ClipRect::new(0.0, 0.0, 1.0, 1.0);
let ring = vec![
Point::new(0.5, 0.2),
Point::new(1.5, 0.2),
Point::new(1.5, 0.8),
Point::new(0.5, 0.8),
];
let clipped = clip_polygon(&ring, &rect);
assert!(
!clipped.is_empty(),
"partially overlapping polygon should produce output"
);
for p in &clipped {
assert!(p.x >= -EPSILON, "x={} should be >= 0", p.x);
assert!(p.x <= 1.0 + EPSILON, "x={} should be <= 1", p.x);
assert!(p.y >= -EPSILON, "y={} should be >= 0", p.y);
assert!(p.y <= 1.0 + EPSILON, "y={} should be <= 1", p.y);
}
}
#[test]
fn test_clip_polygon_fully_outside() {
let rect = ClipRect::new(0.0, 0.0, 1.0, 1.0);
let ring = vec![
Point::new(2.0, 2.0),
Point::new(3.0, 2.0),
Point::new(3.0, 3.0),
Point::new(2.0, 3.0),
];
let clipped = clip_polygon(&ring, &rect);
assert!(clipped.is_empty(), "fully outside polygon should be empty");
}
#[test]
fn test_ccw_ring() {
let ring = vec![
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
Point::new(1.0, 1.0),
Point::new(0.0, 1.0),
];
assert!(is_ccw(&ring), "CCW ring should be detected as CCW");
assert!(!is_cw(&ring), "CCW ring should not be detected as CW");
}
#[test]
fn test_cw_ring() {
let ring = vec![
Point::new(0.0, 1.0),
Point::new(1.0, 1.0),
Point::new(1.0, 0.0),
Point::new(0.0, 0.0),
];
assert!(is_cw(&ring), "CW ring should be detected as CW");
assert!(!is_ccw(&ring), "CW ring should not be detected as CCW");
}
#[test]
fn test_signed_area_unit_square() {
let ring = vec![
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
Point::new(1.0, 1.0),
Point::new(0.0, 1.0),
];
let area = signed_area(&ring);
assert!(
approx_eq(area, 1.0),
"unit square area: {area}, expected 1.0"
);
}
#[test]
fn test_reverse_ring() {
let mut ring = vec![
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
Point::new(1.0, 1.0),
];
assert!(is_ccw(&ring));
reverse_ring(&mut ring);
assert!(is_cw(&ring));
}
#[test]
fn test_tiles_for_bbox_zoom_0() {
let bbox = MercBbox {
min_x: 0.0,
min_y: 0.0,
max_x: 1.0,
max_y: 1.0,
};
let tiles = tiles_for_bbox(&bbox, 0);
assert_eq!(tiles.len(), 1);
assert_eq!(tiles[0], (0, 0));
}
#[test]
fn test_tiles_for_bbox_zoom_1() {
let bbox = MercBbox {
min_x: 0.0,
min_y: 0.0,
max_x: 0.999,
max_y: 0.999,
};
let tiles = tiles_for_bbox(&bbox, 1);
assert_eq!(tiles.len(), 4);
assert!(tiles.contains(&(0, 0)));
assert!(tiles.contains(&(1, 0)));
assert!(tiles.contains(&(0, 1)));
assert!(tiles.contains(&(1, 1)));
}
#[test]
fn test_tiles_for_bbox_single_tile() {
let bbox = MercBbox {
min_x: 0.1,
min_y: 0.1,
max_x: 0.4,
max_y: 0.4,
};
let tiles = tiles_for_bbox(&bbox, 1);
assert_eq!(tiles.len(), 1);
assert_eq!(tiles[0], (0, 0));
}
#[test]
fn test_tiles_for_bbox_copenhagen() {
let bbox = project_bbox(55.6, 12.5, 55.7, 12.6);
let tiles = tiles_for_bbox(&bbox, 10);
assert!(
!tiles.is_empty(),
"Copenhagen should intersect at least one tile"
);
assert!(
tiles.len() <= 4,
"should be a small number of tiles: {}",
tiles.len()
);
}
#[test]
fn test_area_sq_meters_equator() {
let sw = project(0.0, 0.0);
let se = project(0.0, 1.0);
let ne = project(1.0, 1.0);
let nw = project(1.0, 0.0);
let ring = vec![sw, se, ne, nw];
let area = area_sq_meters(&ring);
let area_km2 = area / 1e6;
assert!(
area_km2 > 10_000.0 && area_km2 < 15_000.0,
"1°×1° at equator ≈ 12,000 km², got {area_km2:.0} km²",
);
}
#[test]
fn test_area_sq_meters_high_latitude() {
let sw = project(70.0, 10.0);
let se = project(70.0, 11.0);
let ne = project(71.0, 11.0);
let nw = project(71.0, 10.0);
let ring = vec![sw, se, ne, nw];
let area_km2 = area_sq_meters(&ring) / 1e6;
assert!(
area_km2 > 3_500.0 && area_km2 < 5_000.0,
"1°×1° at 70°N ≈ 4,200 km², got {area_km2:.0} km²",
);
}
#[test]
fn test_area_sq_meters_wide_latitude_span() {
let mut ring = Vec::new();
ring.push(project(55.0, 20.0));
ring.push(project(55.0, 30.0));
for lat in 56..=80 {
ring.push(project(lat as f64, 30.0));
}
ring.push(project(80.0, 20.0));
for lat in (55..80).rev() {
ring.push(project(lat as f64, 20.0));
}
let area_km2 = area_sq_meters(&ring) / 1e6;
assert!(
area_km2 > 1_115_000.0 && area_km2 < 1_235_000.0,
"10°×25° at 55-80°N ≈ 1,175,000 km², got {area_km2:.0} km²",
);
}
#[test]
fn test_point_on_surface_square() {
let ring = vec![
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
Point::new(1.0, 1.0),
Point::new(0.0, 1.0),
];
let p = point_on_surface(&ring).expect("should find a point");
assert!(p.x > 0.0 && p.x < 1.0, "x={} should be inside", p.x);
assert!(p.y > 0.0 && p.y < 1.0, "y={} should be inside", p.y);
}
#[test]
fn test_point_on_surface_degenerate() {
let ring = vec![Point::new(0.0, 0.0), Point::new(1.0, 0.0)];
assert!(point_on_surface(&ring).is_none());
}
#[test]
fn test_point_on_surface_with_holes_avoids_hole() {
let outer = vec![
Point::new(0.0, 0.0),
Point::new(10.0, 0.0),
Point::new(10.0, 10.0),
Point::new(0.0, 10.0),
];
let hole = vec![
Point::new(2.0, 2.0),
Point::new(8.0, 2.0),
Point::new(8.0, 8.0),
Point::new(2.0, 8.0),
];
let p = point_on_surface_with_holes(&outer, std::slice::from_ref(&hole))
.expect("should find a point");
assert!(point_in_polygon(&p, &outer));
assert!(!point_in_polygon(&p, &hole));
}
#[test]
fn test_point_on_surface_with_holes_multiple_holes() {
let outer = vec![
Point::new(0.0, 0.0),
Point::new(10.0, 0.0),
Point::new(10.0, 10.0),
Point::new(0.0, 10.0),
];
let hole_a = vec![
Point::new(1.0, 1.0),
Point::new(4.5, 1.0),
Point::new(4.5, 6.0),
Point::new(1.0, 6.0),
];
let hole_b = vec![
Point::new(5.5, 4.0),
Point::new(9.0, 4.0),
Point::new(9.0, 9.0),
Point::new(5.5, 9.0),
];
let inners = vec![hole_a.clone(), hole_b.clone()];
let p = point_on_surface_with_holes(&outer, &inners).expect("should find a point");
assert!(point_in_polygon(&p, &outer));
assert!(!point_in_polygon(&p, &hole_a));
assert!(!point_in_polygon(&p, &hole_b));
}
#[test]
fn test_point_on_surface_with_holes_adjacent_holes() {
let outer = vec![
Point::new(0.0, 0.0),
Point::new(10.0, 0.0),
Point::new(10.0, 10.0),
Point::new(0.0, 10.0),
];
let left_hole = vec![
Point::new(2.0, 2.0),
Point::new(5.0, 2.0),
Point::new(5.0, 8.0),
Point::new(2.0, 8.0),
];
let right_hole = vec![
Point::new(5.0, 2.0),
Point::new(8.0, 2.0),
Point::new(8.0, 8.0),
Point::new(5.0, 8.0),
];
let inners = vec![left_hole.clone(), right_hole.clone()];
let p = point_on_surface_with_holes(&outer, &inners).expect("should find a point");
assert!(point_in_polygon(&p, &outer));
assert!(!point_in_polygon(&p, &left_hole));
assert!(!point_in_polygon(&p, &right_hole));
}
#[test]
fn test_point_on_surface_with_holes_fallback_inside_hole_returns_none() {
let outer = vec![
Point::new(0.0, 0.0),
Point::new(10.0, 0.0),
Point::new(10.0, 10.0),
Point::new(0.0, 10.0),
];
let hole = outer.clone();
let inners = vec![hole];
assert!(point_on_surface_with_holes(&outer, &inners).is_none());
}
#[test]
fn test_clip_rect_for_tile() {
let rect = ClipRect::for_tile(0, 0, 1, 0.0);
assert!(approx_eq(rect.min_x, 0.0), "min_x={}", rect.min_x);
assert!(approx_eq(rect.min_y, 0.0), "min_y={}", rect.min_y);
assert!(approx_eq(rect.max_x, 0.5), "max_x={}", rect.max_x);
assert!(approx_eq(rect.max_y, 0.5), "max_y={}", rect.max_y);
}
#[test]
fn test_clip_rect_for_tile_with_buffer() {
let rect = ClipRect::for_tile(0, 0, 1, 0.1);
assert!(
rect.min_x < 0.0,
"buffered min_x={} should be < 0",
rect.min_x
);
assert!(
rect.max_x > 0.5,
"buffered max_x={} should be > 0.5",
rect.max_x
);
}
#[test]
fn test_simplify_tolerance_decreases_with_zoom() {
let tol_0 = simplify_tolerance(0);
let tol_10 = simplify_tolerance(10);
assert!(
tol_0 > tol_10,
"tolerance at z0 ({tol_0}) should be > z10 ({tol_10})",
);
}
#[test]
fn test_merc_bbox_subpixel_tiny_feature() {
let pixel_z10 = 1.0 / (256.0 * 1024.0);
let tiny = vec![
Point::new(0.5, 0.5),
Point::new(0.5 + pixel_z10 * 0.1, 0.5 + pixel_z10 * 0.1),
];
assert!(merc_bbox_is_subpixel(&tiny, 10));
assert!(!merc_bbox_is_subpixel(&tiny, 14));
}
#[test]
fn test_merc_bbox_subpixel_large_feature() {
let large = vec![Point::new(0.5, 0.5), Point::new(0.51, 0.51)];
for z in 0..=14 {
assert!(!merc_bbox_is_subpixel(&large, z));
}
}
fn square_ring(cx: f64, cy: f64, hw: f64) -> Vec<Point> {
vec![
Point::new(cx - hw, cy - hw),
Point::new(cx + hw, cy - hw),
Point::new(cx + hw, cy + hw),
Point::new(cx - hw, cy + hw),
Point::new(cx - hw, cy - hw),
]
}
#[test]
fn multi_simplify_no_inners_all_zooms() {
let outer = square_ring(0.5, 0.5, 0.1);
let inners: Vec<Vec<Point>> = vec![];
let mut scratch = SimplifyMultiScratch::new();
let mut results: Vec<(u8, usize, usize)> = Vec::new();
for_each_zoom_simplified_multi(
&outer,
&inners,
14,
14,
&mut scratch,
|_| 1.0,
|z, o, i| {
results.push((z, o.len(), i.len()));
},
);
assert_eq!(results.len(), 1);
assert_eq!(results[0], (14, 5, 0)); }
#[test]
fn multi_simplify_callback_per_zoom() {
let outer = square_ring(0.5, 0.5, 0.1);
let inners: Vec<Vec<Point>> = vec![];
let mut scratch = SimplifyMultiScratch::new();
let mut zooms: Vec<u8> = Vec::new();
for_each_zoom_simplified_multi(
&outer,
&inners,
10,
14,
&mut scratch,
|_| 1.0,
|z, _o, _i| {
zooms.push(z);
},
);
assert_eq!(zooms, vec![14, 13, 12, 11, 10]);
}
#[test]
fn multi_simplify_inner_count_non_increasing() {
let outer = square_ring(0.5, 0.5, 0.2);
let inner = vec![
Point::new(0.49, 0.50),
Point::new(0.495, 0.500_001),
Point::new(0.50, 0.500_002),
Point::new(0.505, 0.500_001),
Point::new(0.51, 0.50),
Point::new(0.505, 0.499_999),
Point::new(0.50, 0.499_998),
Point::new(0.495, 0.499_999),
Point::new(0.49, 0.50),
];
let inners = vec![inner];
let mut scratch = SimplifyMultiScratch::new();
let mut inner_counts: Vec<(u8, usize)> = Vec::new();
for_each_zoom_simplified_multi(
&outer,
&inners,
4,
14,
&mut scratch,
|_| 1.0,
|z, _o, i| {
inner_counts.push((z, i.len()));
},
);
assert_eq!(inner_counts[0], (14, 1));
for w in inner_counts.windows(2) {
assert!(
w[0].1 >= w[1].1,
"inner count increased from z{} ({}) to z{} ({})",
w[0].0,
w[0].1,
w[1].0,
w[1].1
);
}
}
#[test]
fn multi_simplify_subpixel_outer_stops_early() {
let outer = square_ring(0.5, 0.5, 0.00001); let inners: Vec<Vec<Point>> = vec![];
let mut scratch = SimplifyMultiScratch::new();
let mut zoom_count = 0;
for_each_zoom_simplified_multi(
&outer,
&inners,
0,
14,
&mut scratch,
|_| 1.0,
|_z, _o, _i| {
zoom_count += 1;
},
);
assert!(
zoom_count < 15,
"subpixel outer should stop early, got {zoom_count} zooms"
);
}
#[test]
fn multi_simplify_z14_preserves_all_inners() {
let outer = square_ring(0.5, 0.5, 0.3);
let inner1 = square_ring(0.3, 0.5, 0.05);
let inner2 = square_ring(0.7, 0.5, 0.02);
let inners = vec![inner1.clone(), inner2.clone()];
let mut scratch = SimplifyMultiScratch::new();
let mut z14_data: Option<(Vec<Point>, Vec<Vec<Point>>)> = None;
for_each_zoom_simplified_multi(
&outer,
&inners,
14,
14,
&mut scratch,
|_| 1.0,
|_z, o, i| {
z14_data = Some((o.to_vec(), i.to_vec()));
},
);
let (out_outer, out_inners) = z14_data.expect("should have z14 callback");
assert_eq!(
out_outer.len(),
outer.len(),
"outer should be unchanged at z14"
);
assert_eq!(out_inners.len(), 2, "both inners should be present at z14");
assert_eq!(out_inners[0].len(), inner1.len(), "inner1 unchanged at z14");
assert_eq!(out_inners[1].len(), inner2.len(), "inner2 unchanged at z14");
}
#[test]
fn multi_simplify_outer_vertex_count_non_increasing() {
let outer = square_ring(0.5, 0.5, 0.1);
let inners: Vec<Vec<Point>> = vec![];
let mut scratch = SimplifyMultiScratch::new();
let mut vertex_counts: Vec<(u8, usize)> = Vec::new();
for_each_zoom_simplified_multi(
&outer,
&inners,
4,
14,
&mut scratch,
|_| 1.0,
|z, o, _i| {
vertex_counts.push((z, o.len()));
},
);
for w in vertex_counts.windows(2) {
assert!(
w[0].1 >= w[1].1,
"vertex count increased from z{} ({}) to z{} ({})",
w[0].0,
w[0].1,
w[1].0,
w[1].1
);
}
assert_eq!(vertex_counts[0], (14, 5));
}
#[test]
fn buffer_fraction_is_8_rendered_pixels() {
assert!((BUFFER_FRACTION - 8.0 / 256.0).abs() < f64::EPSILON);
assert!((BUFFER_FRACTION - 0.03125).abs() < f64::EPSILON);
}
#[test]
fn buffer_fraction_produces_128_extent_unit_buffer() {
let buffer_extent_units = BUFFER_FRACTION * EXTENT;
assert!((buffer_extent_units - 128.0).abs() < f64::EPSILON);
}
#[test]
fn clip_rect_for_tile_extends_by_buffer() {
let clip = ClipRect::for_tile(0, 0, 1, BUFFER_FRACTION);
let buf = BUFFER_FRACTION / 2.0;
let eps = 1e-12;
assert!((clip.min_x - (-buf)).abs() < eps);
assert!((clip.min_y - (-buf)).abs() < eps);
assert!((clip.max_x - (0.5 + buf)).abs() < eps);
assert!((clip.max_y - (0.5 + buf)).abs() < eps);
}
#[test]
fn shared_chain_two_adjacent_squares() {
let ring_a = vec![(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)];
let ring_b = vec![(10, 0), (20, 0), (20, 10), (10, 10), (10, 0)];
let chains = detect_shared_chains(&[ring_a, ring_b]);
assert_eq!(chains.len(), 1, "expected one shared chain");
let chain = &chains[0];
assert_eq!(chain.vertices.len(), 2, "single shared edge = 2 vertices");
assert_eq!(chain.incidents.len(), 2);
let ring_idxs: Vec<usize> = chain.incidents.iter().map(|c| c.ring_idx).collect();
assert!(ring_idxs.contains(&0));
assert!(ring_idxs.contains(&1));
}
#[test]
fn shared_chain_two_edges_form_one_chain() {
let ring_a = vec![(0, 0), (10, 0), (10, 5), (10, 10), (0, 10), (0, 0)];
let ring_b = vec![(10, 0), (20, 0), (20, 10), (10, 10), (10, 5), (10, 0)];
let chains = detect_shared_chains(&[ring_a, ring_b]);
assert_eq!(chains.len(), 1, "two consecutive shared edges = one chain");
assert_eq!(chains[0].vertices.len(), 3, "chain should have 3 vertices");
}
#[test]
fn shared_chain_no_shared_edges() {
let ring_a = vec![(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)];
let ring_b = vec![(20, 0), (30, 0), (30, 10), (20, 10), (20, 0)];
let chains = detect_shared_chains(&[ring_a, ring_b]);
assert!(chains.is_empty());
}
#[test]
fn shared_chain_shared_vertex_no_shared_edge() {
let ring_a = vec![(0, 0), (10, 10), (0, 10), (0, 0)];
let ring_b = vec![(10, 10), (20, 0), (20, 10), (10, 10)];
let chains = detect_shared_chains(&[ring_a, ring_b]);
assert!(
chains.is_empty(),
"shared vertex alone should not produce a chain"
);
}
#[test]
fn shared_chain_triple_junction() {
let ring_a = vec![(0, 0), (10, 0), (5, 5), (0, 0)];
let ring_b = vec![(10, 0), (10, 10), (5, 5), (10, 0)];
let ring_c = vec![(0, 0), (5, 5), (0, 10), (0, 0)];
let chains = detect_shared_chains(&[ring_a, ring_b, ring_c]);
assert_eq!(chains.len(), 2, "two pairs sharing one edge each");
for chain in &chains {
assert_eq!(chain.vertices.len(), 2);
assert_eq!(chain.incidents.len(), 2);
}
}
#[test]
fn shared_chain_single_ring() {
let ring = vec![(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)];
let chains = detect_shared_chains(&[ring]);
assert!(chains.is_empty());
}
#[test]
fn shared_chain_empty_input() {
let chains = detect_shared_chains(&[]);
assert!(chains.is_empty());
}
#[test]
fn shared_chain_degenerate_ring() {
let ring_a = vec![(0, 0)];
let ring_b = vec![(0, 0), (10, 0), (10, 10), (0, 0)];
let chains = detect_shared_chains(&[ring_a, ring_b]);
assert!(chains.is_empty());
}
#[test]
fn shared_chain_marks_reversed_incident() {
let ring_a = vec![(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)];
let ring_b = vec![(10, 0), (20, 0), (20, 10), (10, 10), (10, 0)];
let chains = detect_shared_chains(&[ring_a, ring_b]);
assert_eq!(chains.len(), 1);
let chain = &chains[0];
let reversed_count = chain.incidents.iter().filter(|c| c.reversed).count();
assert_eq!(reversed_count, 1, "one of two incidents should be reversed");
}
#[test]
fn shared_chain_three_rings_same_edge() {
let ring_a = vec![(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)];
let ring_b = vec![(10, 0), (20, 0), (20, 10), (10, 10), (10, 0)];
let ring_c = vec![(10, 0), (20, 0), (20, 10), (10, 10), (10, 0)];
let chains = detect_shared_chains(&[ring_a, ring_b, ring_c]);
assert!(
!chains.is_empty(),
"coincident geometry should produce chains"
);
}
#[test]
fn shared_chain_wrap_around_ring_seam() {
let ring_a = vec![(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)];
let ring_b = vec![(0, 0), (0, 10), (-10, 10), (-10, 0), (10, 0), (0, 0)];
let chains = detect_shared_chains(&[ring_a, ring_b]);
assert_eq!(
chains.len(),
1,
"seam fragments should be merged into one chain"
);
assert_eq!(chains[0].vertices.len(), 3, "two edges = three vertices");
assert_eq!(chains[0].incidents.len(), 2);
}
#[test]
fn shared_chain_consecutive_wrap_around() {
let ring_a = vec![(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)];
let ring_b = vec![(0, 0), (0, 10), (10, 10), (20, 10), (20, 0), (0, 0)];
let chains = detect_shared_chains(&[ring_a, ring_b]);
assert_eq!(
chains.len(),
1,
"consecutive shared edges in both rings should form one chain"
);
assert_eq!(chains[0].vertices.len(), 3, "two edges = three vertices");
}
#[test]
fn decode_mvt_polygon_single_ring_round_trip() {
let ring = vec![(100, 200), (300, 200), (300, 400), (100, 400), (100, 200)];
let mut buf = Vec::new();
crate::mvt::encode_polygon(&mut buf, &[&ring]);
let decoded = super::decode_mvt_polygon(&buf);
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0], ring);
}
#[test]
fn decode_mvt_polygon_multi_ring_round_trip() {
let outer = vec![(0, 0), (4096, 0), (4096, 4096), (0, 4096), (0, 0)];
let inner = vec![
(1000, 1000),
(1000, 3000),
(3000, 3000),
(3000, 1000),
(1000, 1000),
];
let mut buf = Vec::new();
crate::mvt::encode_polygon(&mut buf, &[&outer, &inner]);
let decoded = super::decode_mvt_polygon(&buf);
assert_eq!(decoded.len(), 2);
assert_eq!(decoded[0], outer);
assert_eq!(decoded[1], inner);
}
#[test]
fn decode_mvt_polygon_empty() {
let decoded = super::decode_mvt_polygon(&[]);
assert!(decoded.is_empty());
}
#[test]
fn decode_mvt_polygon_negative_coords() {
let ring = vec![
(-128, -128),
(4224, -128),
(4224, 4224),
(-128, 4224),
(-128, -128),
];
let mut buf = Vec::new();
crate::mvt::encode_polygon(&mut buf, &[&ring]);
let decoded = super::decode_mvt_polygon(&buf);
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0], ring);
}
#[test]
fn decode_mvt_polygon_two_outer_rings() {
let ring_a = vec![(0, 0), (100, 0), (100, 100), (0, 100), (0, 0)];
let ring_b = vec![(200, 200), (300, 200), (300, 300), (200, 300), (200, 200)];
let mut buf = Vec::new();
crate::mvt::encode_polygon(&mut buf, &[&ring_a, &ring_b]);
let decoded = super::decode_mvt_polygon(&buf);
assert_eq!(decoded.len(), 2);
assert_eq!(decoded[0], ring_a);
assert_eq!(decoded[1], ring_b);
}
#[test]
fn canonicalize_two_adjacent_squares() {
let ring_a = vec![(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)];
let ring_b = vec![(10, 0), (20, 0), (20, 10), (10, 10), (10, 0)];
let chains = super::detect_shared_chains(&[ring_a.clone(), ring_b.clone()]);
assert_eq!(chains.len(), 1);
let result = super::canonicalize_shared_chains(&mut [ring_a.clone(), ring_b.clone()], &chains);
assert_eq!(result.reconciled, 1);
assert_eq!(result.skipped, 0);
let mut ring_b = ring_b;
ring_b[3] = (10, 11); let mut rings = [ring_a, ring_b];
let result = super::canonicalize_shared_chains(&mut rings, &chains);
assert_eq!(result.reconciled, 1);
assert_eq!(rings[1][3], (10, 10), "shared vertex should be restored");
}
#[test]
fn canonicalize_skips_gt2_incidents() {
let chain = super::SharedChain {
vertices: vec![(0, 0), (10, 0)],
incidents: vec![
super::ChainRef {
ring_idx: 0,
start: 0,
len: 2,
reversed: false,
},
super::ChainRef {
ring_idx: 1,
start: 3,
len: 2,
reversed: true,
},
super::ChainRef {
ring_idx: 2,
start: 1,
len: 2,
reversed: false,
},
],
};
let mut rings = vec![
vec![(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)],
vec![(10, 0), (20, 0), (20, 10), (10, 10), (10, 0)],
vec![(0, 0), (10, 0), (10, -10), (0, -10), (0, 0)],
];
let result = super::canonicalize_shared_chains(&mut rings, &[chain]);
assert_eq!(result.reconciled, 0);
assert_eq!(result.skipped, 1);
}
#[test]
fn simplify_ring_tile_coords_no_pins() {
let ring = vec![(0, 0), (500, 0), (1000, 0), (1000, 1000), (0, 1000), (0, 0)];
let pinned = vec![false; ring.len()];
let simplified = super::simplify_ring_tile_coords(&ring, &pinned, 16.0);
assert_eq!(simplified.len(), 5, "collinear point should be removed");
assert!(!simplified.contains(&(500, 0)));
}
#[test]
fn simplify_ring_tile_coords_with_pins() {
let ring = vec![(0, 0), (500, 0), (1000, 0), (1000, 1000), (0, 1000), (0, 0)];
let pinned = vec![true, true, true, false, false, true]; let simplified = super::simplify_ring_tile_coords(&ring, &pinned, 16.0);
assert!(simplified.contains(&(500, 0)), "pinned vertex must survive");
}
#[test]
fn build_pinned_mask_basic() {
let chain = super::SharedChain {
vertices: vec![(10, 0), (10, 10)],
incidents: vec![
super::ChainRef {
ring_idx: 0,
start: 1,
len: 2,
reversed: false,
},
super::ChainRef {
ring_idx: 1,
start: 3,
len: 2,
reversed: true,
},
],
};
let mask = super::build_pinned_mask(5, 0, std::slice::from_ref(&chain));
assert_eq!(mask, vec![false, true, true, false, false]);
let mask = super::build_pinned_mask(5, 1, std::slice::from_ref(&chain));
assert_eq!(mask, vec![true, false, false, true, true]);
}
#[test]
fn shared_edge_pinning_produces_identical_simplification() {
let n_shared = 22; let mut shared_pts: Vec<Point> = Vec::with_capacity(n_shared);
for i in 0..n_shared {
let t = i as f64 / (n_shared - 1) as f64;
let y = 0.3 + 0.4 * t;
let x = 0.5 + 0.0003 * (i as f64 * 1.7).sin();
shared_pts.push(Point { x, y });
}
let mut ring_a: Vec<Point> = Vec::new();
ring_a.push(Point { x: 0.3, y: 0.3 });
ring_a.push(Point { x: 0.3, y: 0.7 });
ring_a.push(Point { x: 0.35, y: 0.71 });
for &p in &shared_pts {
ring_a.push(p);
}
let mut ring_b: Vec<Point> = Vec::new();
for &p in shared_pts.iter().rev() {
ring_b.push(p);
}
ring_b.push(Point { x: 0.7, y: 0.3 });
ring_b.push(Point { x: 0.72, y: 0.5 });
ring_b.push(Point { x: 0.7, y: 0.7 });
let shared_start_a = 3; let shared_end_a = shared_start_a + n_shared - 1; let shared_start_b = 0; let shared_end_b = n_shared - 1;
let tol = simplify_tolerance(6);
let mut keep_a = Vec::new();
let mut keep_b = Vec::new();
let mut out_a = Vec::new();
let mut out_b = Vec::new();
simplify_into(&ring_a, tol, &mut keep_a, &mut out_a);
simplify_into(&ring_b, tol, &mut keep_b, &mut out_b);
let required_a: Vec<usize> = vec![shared_start_a, shared_end_a];
let required_b: Vec<usize> = vec![shared_start_b, shared_end_b];
simplify_into_with_required(&ring_a, tol, &required_a, &mut keep_a, &mut out_a);
simplify_into_with_required(&ring_b, tol, &required_b, &mut keep_b, &mut out_b);
let pinned_shared_a: Vec<Point> = out_a
.iter()
.filter(|p| p.x > 0.49 && p.x < 0.51 && p.y >= 0.29 && p.y <= 0.71)
.copied()
.collect();
let pinned_shared_b: Vec<Point> = out_b
.iter()
.filter(|p| p.x > 0.49 && p.x < 0.51 && p.y >= 0.29 && p.y <= 0.71)
.rev()
.copied()
.collect();
assert!(
pinned_shared_a.len() >= 2,
"pinned shared A must have at least endpoints"
);
assert!(
pinned_shared_b.len() >= 2,
"pinned shared B must have at least endpoints"
);
let eps = 1e-10;
assert!(
(pinned_shared_a[0].y - 0.3).abs() < eps,
"A start endpoint preserved"
);
assert!(
(pinned_shared_a.last().unwrap().y - 0.7).abs() < eps,
"A end endpoint preserved"
);
assert!(
(pinned_shared_b[0].y - 0.3).abs() < eps,
"B start endpoint preserved (reversed)"
);
assert!(
(pinned_shared_b.last().unwrap().y - 0.7).abs() < eps,
"B end endpoint preserved (reversed)"
);
}
#[test]
fn isolated_shared_segment_simplification_is_identical() {
let n = 50;
let tol = simplify_tolerance(8);
let mut shared: Vec<Point> = Vec::with_capacity(n);
for i in 0..n {
let t = i as f64 / (n - 1) as f64;
let y = 0.3 + 0.4 * t;
let wiggle = tol * 0.3 * ((i as f64 * 2.3).sin() + (i as f64 * 0.7).cos() * 0.5);
let x = 0.5 + wiggle;
shared.push(Point { x, y });
}
let shared_rev: Vec<Point> = shared.iter().rev().copied().collect();
let mut keep = Vec::new();
let mut out_fwd = Vec::new();
let mut out_rev = Vec::new();
simplify_into(&shared, tol, &mut keep, &mut out_fwd);
simplify_into(&shared_rev, tol, &mut keep, &mut out_rev);
out_rev.reverse();
assert_eq!(
out_fwd.len(),
out_rev.len(),
"isolated shared segment simplified forward ({}) vs reversed ({}) must have same vertex count",
out_fwd.len(),
out_rev.len()
);
for (i, (a, b)) in out_fwd.iter().zip(out_rev.iter()).enumerate() {
assert!(
(a.x - b.x).abs() < 1e-15 && (a.y - b.y).abs() < 1e-15,
"vertex {i} diverged: fwd=({}, {}) rev=({}, {})",
a.x,
a.y,
b.x,
b.y
);
}
assert!(
out_fwd.len() < n,
"DP should have simplified: {} vertices in, {} out",
n,
out_fwd.len()
);
}