#![allow(clippy::expect_used, clippy::unwrap_used)]
use oxigdal_algorithms::{
CollapseOptions, Coordinate, DisplaceOptions, ExaggerateAnchor, ExaggerateOptions, LineString,
Polygon, collapse_linestring_to_point, collapse_polygon_to_point, displace_points,
displace_polygons_by_centroid, exaggerate_coord, exaggerate_polygon, should_collapse_polygon,
};
fn make_square(x0: f64, y0: f64, side: f64) -> Polygon {
let coords = vec![
Coordinate::new_2d(x0, y0),
Coordinate::new_2d(x0 + side, y0),
Coordinate::new_2d(x0 + side, y0 + side),
Coordinate::new_2d(x0, y0 + side),
Coordinate::new_2d(x0, y0),
];
let ext = LineString::new(coords).expect("valid exterior ring");
Polygon::new(ext, vec![]).expect("valid polygon")
}
fn make_hline(x0: f64, x1: f64) -> LineString {
let coords = vec![Coordinate::new_2d(x0, 0.0), Coordinate::new_2d(x1, 0.0)];
LineString::new(coords).expect("valid linestring")
}
fn make_coord(x: f64, y: f64) -> Coordinate {
Coordinate::new_2d(x, y)
}
#[test]
fn test_collapse_small_polygon_returns_centroid() {
let poly = make_square(0.0, 0.0, 1.0);
let opts = CollapseOptions::new(2.0);
let result = collapse_polygon_to_point(&poly, &opts);
assert!(
result.is_some(),
"unit square should collapse when min_area = 2.0"
);
let c = result.unwrap();
assert!(
(c.x - 0.5).abs() < 1e-10,
"centroid x should be 0.5, got {}",
c.x
);
assert!(
(c.y - 0.5).abs() < 1e-10,
"centroid y should be 0.5, got {}",
c.y
);
}
#[test]
fn test_collapse_large_polygon_returns_none() {
let poly = make_square(0.0, 0.0, 10.0);
let opts = CollapseOptions::new(1.0);
let result = collapse_polygon_to_point(&poly, &opts);
assert!(
result.is_none(),
"10×10 square should not collapse with min_area = 1.0"
);
}
#[test]
fn test_collapse_threshold_zero_keeps_all() {
let tiny = make_square(0.0, 0.0, 0.001);
let opts = CollapseOptions::new(0.0);
let result = collapse_polygon_to_point(&tiny, &opts);
assert!(
result.is_none(),
"no polygon should collapse when min_area = 0.0"
);
}
#[test]
fn test_should_collapse_polygon_below_threshold() {
let poly = make_square(0.0, 0.0, 0.5); let opts = CollapseOptions::new(1.0);
assert!(
should_collapse_polygon(&poly, &opts),
"polygon with area 0.25 should be flagged for collapse at threshold 1.0"
);
}
#[test]
fn test_collapse_linestring_short_returns_midpoint() {
let line = make_hline(0.0, 1.0);
let opts = CollapseOptions::default().with_min_linestring_length(2.0);
let result = collapse_linestring_to_point(&line, &opts);
assert!(
result.is_some(),
"line of length 1.0 should collapse when min_len = 2.0"
);
let mid = result.unwrap();
assert!((mid.x - 0.5).abs() < 1e-10, "midpoint x should be 0.5");
assert!((mid.y - 0.0).abs() < 1e-10, "midpoint y should be 0.0");
}
#[test]
fn test_collapse_linestring_long_returns_none() {
let line = make_hline(0.0, 10.0);
let opts = CollapseOptions::default().with_min_linestring_length(0.5);
let result = collapse_linestring_to_point(&line, &opts);
assert!(
result.is_none(),
"line of length 10.0 should not collapse with min_len = 0.5"
);
}
#[test]
fn test_exaggerate_coord_scale_2x_doubles_offset_from_anchor() {
let anchor = make_coord(0.0, 0.0);
let point = make_coord(1.0, 0.0);
let result = exaggerate_coord(&point, &anchor, 2.0);
assert!(
(result.x - 2.0).abs() < 1e-12,
"x should be 2.0, got {}",
result.x
);
assert!(
(result.y - 0.0).abs() < 1e-12,
"y should be 0.0, got {}",
result.y
);
}
#[test]
fn test_exaggerate_coord_scale_1_returns_input() {
let anchor = make_coord(3.0, 7.0);
let point = make_coord(5.0, 2.0);
let result = exaggerate_coord(&point, &anchor, 1.0);
assert!((result.x - point.x).abs() < 1e-12, "x unchanged at scale=1");
assert!((result.y - point.y).abs() < 1e-12, "y unchanged at scale=1");
}
#[test]
fn test_exaggerate_polygon_preserves_ring_count() {
let ext_coords = vec![
Coordinate::new_2d(0.0, 0.0),
Coordinate::new_2d(10.0, 0.0),
Coordinate::new_2d(10.0, 10.0),
Coordinate::new_2d(0.0, 10.0),
Coordinate::new_2d(0.0, 0.0),
];
let hole_coords = vec![
Coordinate::new_2d(2.0, 2.0),
Coordinate::new_2d(8.0, 2.0),
Coordinate::new_2d(8.0, 8.0),
Coordinate::new_2d(2.0, 8.0),
Coordinate::new_2d(2.0, 2.0),
];
let exterior = LineString::new(ext_coords).expect("valid exterior");
let hole = LineString::new(hole_coords).expect("valid hole");
let poly = Polygon::new(exterior, vec![hole]).expect("valid donut polygon");
let opts = ExaggerateOptions {
scale_factor: 1.5,
anchor: ExaggerateAnchor::Centroid,
};
let result = exaggerate_polygon(&poly, &opts);
assert_eq!(
result.interiors().len(),
1,
"exaggerated polygon must retain exactly one interior ring"
);
assert_eq!(
result.exterior().coords.len(),
poly.exterior().coords.len(),
"exterior ring coordinate count must be unchanged"
);
}
#[test]
fn test_exaggerate_polygon_centroid_anchor_doubles_extent() {
let poly = make_square(0.0, 0.0, 1.0);
let opts = ExaggerateOptions {
scale_factor: 2.0,
anchor: ExaggerateAnchor::Centroid,
};
let result = exaggerate_polygon(&poly, &opts);
let coords = result.exterior().coords();
let min_x = coords.iter().map(|c| c.x).fold(f64::INFINITY, f64::min);
let max_x = coords.iter().map(|c| c.x).fold(f64::NEG_INFINITY, f64::max);
let min_y = coords.iter().map(|c| c.y).fold(f64::INFINITY, f64::min);
let max_y = coords.iter().map(|c| c.y).fold(f64::NEG_INFINITY, f64::max);
assert!(
(min_x - (-0.5)).abs() < 1e-10,
"min_x should be -0.5, got {}",
min_x
);
assert!(
(max_x - 1.5).abs() < 1e-10,
"max_x should be 1.5, got {}",
max_x
);
assert!(
(min_y - (-0.5)).abs() < 1e-10,
"min_y should be -0.5, got {}",
min_y
);
assert!(
(max_y - 1.5).abs() < 1e-10,
"max_y should be 1.5, got {}",
max_y
);
}
#[test]
fn test_displace_two_overlapping_points_pushed_apart() {
let mut positions = vec![
make_coord(0.0, 0.0),
make_coord(0.1, 0.0), ];
let opts = DisplaceOptions {
min_distance: 1.0,
max_iterations: 200,
damping: 0.5,
convergence_tol: 1e-9,
};
displace_points(&mut positions, &opts);
let dx = positions[1].x - positions[0].x;
let dy = positions[1].y - positions[0].y;
let dist = (dx * dx + dy * dy).sqrt();
assert!(
dist >= 0.99, "after displacement, distance should be ≥ 1.0; got {dist:.6}"
);
}
#[test]
fn test_displace_already_separated_points_unchanged() {
let initial = vec![
make_coord(0.0, 0.0),
make_coord(10.0, 0.0), ];
let mut positions = initial.clone();
let opts = DisplaceOptions {
min_distance: 1.0,
..Default::default()
};
displace_points(&mut positions, &opts);
for (orig, moved) in initial.iter().zip(positions.iter()) {
let d = ((moved.x - orig.x).powi(2) + (moved.y - orig.y).powi(2)).sqrt();
assert!(
d < 1e-12,
"already-separated point should not move; displacement was {d:.2e}"
);
}
}
#[test]
fn test_displace_converges_within_max_iter() {
let mut positions = vec![
make_coord(0.0, 0.0),
make_coord(0.05, 0.0),
make_coord(0.0, 0.05),
];
let opts = DisplaceOptions {
min_distance: 1.0,
max_iterations: 500,
damping: 0.3,
convergence_tol: 1e-6,
};
let stats = displace_points(&mut positions, &opts);
assert!(
stats.iterations <= 500,
"solver must not exceed max_iterations; got {}",
stats.iterations
);
for i in 0..positions.len() {
for j in (i + 1)..positions.len() {
let dx = positions[j].x - positions[i].x;
let dy = positions[j].y - positions[i].y;
let dist = (dx * dx + dy * dy).sqrt();
assert!(
dist >= 0.95,
"after displacement, pair ({i},{j}) distance should be ≥ 1.0; got {dist:.4}"
);
}
}
}
#[test]
fn test_displace_polygons_translates_by_centroid_delta() {
let mut polys = vec![
make_square(0.0, 0.0, 1.0), make_square(0.1, 0.0, 1.0), ];
let opts = DisplaceOptions {
min_distance: 1.0,
max_iterations: 200,
damping: 0.5,
convergence_tol: 1e-9,
};
displace_polygons_by_centroid(&mut polys, &opts);
let c0 = {
let ext = polys[0].exterior().coords();
let n = ext.len() as f64;
let sx: f64 = ext.iter().map(|c| c.x).sum();
let sy: f64 = ext.iter().map(|c| c.y).sum();
(sx / n, sy / n)
};
let c1 = {
let ext = polys[1].exterior().coords();
let n = ext.len() as f64;
let sx: f64 = ext.iter().map(|c| c.x).sum();
let sy: f64 = ext.iter().map(|c| c.y).sum();
(sx / n, sy / n)
};
let dx = c1.0 - c0.0;
let dy = c1.1 - c0.1;
let centroid_dist = (dx * dx + dy * dy).sqrt();
assert!(
centroid_dist >= 0.95,
"centroid distance after displacement should be ≥ 1.0; got {centroid_dist:.4}"
);
}