use wbtopology::{
buffer_linestring,
buffer_linestring_with_precision,
buffer_point,
buffer_point_with_precision,
buffer_polygon,
buffer_polygon_multi,
buffer_polygon_with_precision,
contains,
is_valid_polygon,
make_valid_polygon,
offset_linestring,
polygonize_closed_linestrings,
BufferCapStyle,
BufferJoinStyle,
BufferOptions,
Coord,
Geometry,
LineString,
LinearRing,
OffsetCurveOptions,
OffsetSide,
PrecisionModel,
Polygon,
};
fn ring_area_abs(coords: &[Coord]) -> f64 {
if coords.len() < 4 {
return 0.0;
}
let mut s = 0.0;
for i in 0..(coords.len() - 1) {
s += coords[i].x * coords[i + 1].y - coords[i + 1].x * coords[i].y;
}
(0.5 * s).abs()
}
#[test]
fn make_valid_polygon_drops_hole_outside_shell() {
let poly = Polygon::new(
LinearRing::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(10.0, 0.0),
Coord::xy(10.0, 10.0),
Coord::xy(0.0, 10.0),
]),
vec![LinearRing::new(vec![
Coord::xy(20.0, 20.0),
Coord::xy(21.0, 20.0),
Coord::xy(21.0, 21.0),
Coord::xy(20.0, 21.0),
])],
);
let out = make_valid_polygon(&poly, 1.0e-9);
assert_eq!(out.len(), 1);
assert_eq!(out[0].holes.len(), 0);
}
#[test]
fn polygonize_closed_linestrings_builds_shell_and_hole() {
let shell = LineString::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(10.0, 0.0),
Coord::xy(10.0, 10.0),
Coord::xy(0.0, 10.0),
Coord::xy(0.0, 0.0),
]);
let hole = LineString::new(vec![
Coord::xy(3.0, 3.0),
Coord::xy(7.0, 3.0),
Coord::xy(7.0, 7.0),
Coord::xy(3.0, 7.0),
Coord::xy(3.0, 3.0),
]);
let polys = polygonize_closed_linestrings(&[shell, hole], 1.0e-9);
assert_eq!(polys.len(), 1);
assert_eq!(polys[0].holes.len(), 1);
}
#[test]
fn point_buffer_has_reasonable_area() {
let p = Coord::xy(0.0, 0.0);
let r = 2.0;
let poly = buffer_point(
p,
r,
BufferOptions {
quadrant_segments: 16,
..Default::default()
},
);
let area = ring_area_abs(&poly.exterior.coords);
let exact = std::f64::consts::PI * r * r;
let rel_err = (area - exact).abs() / exact;
assert!(rel_err < 0.03, "relative error too high: {}", rel_err);
}
#[test]
fn make_valid_polygon_splits_simple_bow_tie() {
let bow_tie = Polygon::new(
LinearRing::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(2.0, 2.0),
Coord::xy(0.0, 2.0),
Coord::xy(2.0, 0.0),
]),
vec![],
);
let out = make_valid_polygon(&bow_tie, 1.0e-9);
assert_eq!(out.len(), 2);
}
#[test]
fn make_valid_polygon_intersection_point_interpolates_z() {
let bow_tie = Polygon::new(
LinearRing::new(vec![
Coord::xyz(0.0, 0.0, 0.0),
Coord::xyz(2.0, 2.0, 20.0),
Coord::xyz(0.0, 2.0, 40.0),
Coord::xyz(2.0, 0.0, 60.0),
]),
vec![],
);
let out = make_valid_polygon(&bow_tie, 1.0e-9);
assert_eq!(out.len(), 2);
let mut found = false;
for poly in &out {
for c in &poly.exterior.coords {
if (c.x - 1.0).abs() <= 1.0e-9 && (c.y - 1.0).abs() <= 1.0e-9 {
assert_eq!(c.z, Some(10.0));
found = true;
}
}
}
assert!(found, "expected split intersection vertex at (1, 1)");
}
#[test]
fn linestring_buffer_with_square_caps_has_area() {
let ls = LineString::new(vec![Coord::xy(0.0, 0.0), Coord::xy(10.0, 0.0)]);
let poly = buffer_linestring(
&ls,
1.0,
BufferOptions {
quadrant_segments: 12,
cap_style: BufferCapStyle::Square,
..Default::default()
},
);
assert!(ring_area_abs(&poly.exterior.coords) > 0.0);
}
#[test]
fn polygon_buffer_grows_area_for_positive_distance() {
let p = Polygon::new(
LinearRing::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(4.0, 0.0),
Coord::xy(4.0, 4.0),
Coord::xy(0.0, 4.0),
]),
vec![],
);
let base = ring_area_abs(&p.exterior.coords);
let buffered = buffer_polygon(&p, 0.5, BufferOptions::default());
let grew = ring_area_abs(&buffered.exterior.coords);
assert!(grew > base);
}
#[test]
fn polygon_buffer_zero_distance_preserves_simple_polygon() {
let src = Polygon::new(
LinearRing::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(4.0, 0.0),
Coord::xy(4.0, 4.0),
Coord::xy(0.0, 4.0),
]),
vec![],
);
let buf = buffer_polygon(&src, 0.0, BufferOptions::default());
let gpoly = Geometry::Polygon(buf.clone());
assert!(contains(&gpoly, &Geometry::Point(Coord::xy(2.0, 2.0))));
assert!((ring_area_abs(&buf.exterior.coords) - ring_area_abs(&src.exterior.coords)).abs() < 1.0e-9);
}
#[test]
fn polygon_buffer_negative_distance_shrinks_square() {
let src = Polygon::new(
LinearRing::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(4.0, 0.0),
Coord::xy(4.0, 4.0),
Coord::xy(0.0, 4.0),
]),
vec![],
);
let buf = buffer_polygon(&src, -0.5, BufferOptions::default());
let gpoly = Geometry::Polygon(buf.clone());
assert!(ring_area_abs(&buf.exterior.coords) < ring_area_abs(&src.exterior.coords));
assert!(contains(&gpoly, &Geometry::Point(Coord::xy(2.0, 2.0))));
assert!(!contains(&gpoly, &Geometry::Point(Coord::xy(0.2, 0.2))));
}
#[test]
fn make_valid_polygon_splits_triple_crossing_ring() {
let star = Polygon::new(
LinearRing::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(4.0, 4.0),
Coord::xy(0.0, 4.0),
Coord::xy(4.0, 0.0),
Coord::xy(2.0, 5.0),
Coord::xy(2.0, -1.0),
]),
vec![],
);
let out = make_valid_polygon(&star, 1.0e-9);
assert!(!out.is_empty(), "expected at least one valid polygon from split");
}
#[test]
fn linestring_buffer_contains_all_source_vertices() {
let ls = LineString::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(5.0, 0.0),
Coord::xy(5.0, 5.0),
]);
let buf = buffer_linestring(&ls, 1.5, BufferOptions::default());
let gpoly = Geometry::Polygon(buf);
for v in &ls.coords {
assert!(
contains(&gpoly, &Geometry::Point(*v)),
"vertex {:?} not inside buffer",
v
);
}
}
#[test]
fn polygon_buffer_contains_source_ring_vertices() {
let src = Polygon::new(
LinearRing::new(vec![
Coord::xy(1.0, 1.0),
Coord::xy(5.0, 1.0),
Coord::xy(5.0, 5.0),
Coord::xy(1.0, 5.0),
]),
vec![],
);
let buf = buffer_polygon(&src, 1.0, BufferOptions::default());
let gpoly = Geometry::Polygon(buf);
for v in &src.exterior.coords {
assert!(
contains(&gpoly, &Geometry::Point(*v)),
"source vertex {:?} not inside polygon buffer",
v
);
}
}
#[test]
fn linestring_buffer_l_shape_does_not_fill_convex_hull_corner() {
let ls = LineString::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(5.0, 0.0),
Coord::xy(5.0, 5.0),
]);
let buf = buffer_linestring(&ls, 1.0, BufferOptions::default());
let gpoly = Geometry::Polygon(buf);
assert!(!contains(&gpoly, &Geometry::Point(Coord::xy(3.0, 3.0))));
}
#[test]
fn closed_linestring_buffer_preserves_interior_hole() {
let ls = LineString::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(10.0, 0.0),
Coord::xy(10.0, 6.0),
Coord::xy(0.0, 6.0),
Coord::xy(0.0, 0.0),
]);
let buf = buffer_linestring(
&ls,
1.0,
BufferOptions {
quadrant_segments: 12,
join_style: BufferJoinStyle::Round,
..Default::default()
},
);
let gpoly = Geometry::Polygon(buf.clone());
assert!(is_valid_polygon(&buf));
assert_eq!(buf.holes.len(), 1, "closed loop line buffer should retain one interior hole");
assert!(
!contains(&gpoly, &Geometry::Point(Coord::xy(5.0, 3.0))),
"centre of buffered loop should remain outside due to interior hole"
);
assert!(
contains(&gpoly, &Geometry::Point(Coord::xy(-0.5, 3.0))),
"outer corridor should still exist outside the loop"
);
}
#[test]
fn near_closed_linestring_buffer_preserves_interior_hole() {
let ls = LineString::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(10.0, 0.0),
Coord::xy(10.0, 6.0),
Coord::xy(0.0, 6.0),
Coord::xy(0.0, 0.00000001),
]);
let buf = buffer_linestring(
&ls,
1.0,
BufferOptions {
quadrant_segments: 12,
join_style: BufferJoinStyle::Round,
..Default::default()
},
);
let gpoly = Geometry::Polygon(buf.clone());
assert!(is_valid_polygon(&buf));
assert_eq!(buf.holes.len(), 1, "near-closed loop line buffer should retain one interior hole");
assert!(
!contains(&gpoly, &Geometry::Point(Coord::xy(5.0, 3.0))),
"centre of near-closed buffered loop should remain outside due to interior hole"
);
}
#[test]
fn exact_streetcentreline_loop_buffer_preserves_interior_hole() {
let ls = LineString::new(vec![
Coord::xy(565230.8119999999, 4817282.9957),
Coord::xy(565228.9401000002, 4817302.0011),
Coord::xy(565212.8797000004, 4817353.000399999),
Coord::xy(565199.5022, 4817403.0002999995),
Coord::xy(565195.7501999997, 4817409.500600001),
Coord::xy(565183.3761999998, 4817410.5001),
Coord::xy(565172.7553000003, 4817403.9997000005),
Coord::xy(565127.1873000003, 4817371.4979),
Coord::xy(565126.8718999997, 4817360.9999),
Coord::xy(565129.9397999998, 4817347.5035999995),
Coord::xy(565153.9382999996, 4817271.498199999),
Coord::xy(565158.3740999997, 4817260.000700001),
Coord::xy(565164.8787000002, 4817260.500399999),
Coord::xy(565172.0630999999, 4817261.4999),
Coord::xy(565230.8119999999, 4817282.9957),
]);
let buf = buffer_linestring(
&ls,
15.0,
BufferOptions {
quadrant_segments: 12,
join_style: BufferJoinStyle::Round,
..Default::default()
},
);
assert!(is_valid_polygon(&buf));
assert_eq!(
buf.holes.len(),
1,
"street centreline closed loop buffer should retain one interior hole"
);
}
#[test]
fn linestring_buffer_mitre_join_extends_farther_than_bevel() {
let ls = LineString::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(5.0, 0.0),
Coord::xy(5.0, 5.0),
]);
let mitre = buffer_linestring(
&ls,
1.0,
BufferOptions {
join_style: BufferJoinStyle::Mitre,
..Default::default()
},
);
let bevel = buffer_linestring(
&ls,
1.0,
BufferOptions {
join_style: BufferJoinStyle::Bevel,
..Default::default()
},
);
let mitre_max_x = mitre
.exterior
.coords
.iter()
.map(|c| c.x)
.fold(f64::NEG_INFINITY, f64::max);
let bevel_max_x = bevel
.exterior
.coords
.iter()
.map(|c| c.x)
.fold(f64::NEG_INFINITY, f64::max);
assert!(mitre_max_x >= bevel_max_x);
}
#[test]
fn polygon_buffer_preserves_concavity_better_than_hull() {
let src = Polygon::new(
LinearRing::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(6.0, 0.0),
Coord::xy(6.0, 2.0),
Coord::xy(2.0, 2.0),
Coord::xy(2.0, 6.0),
Coord::xy(0.0, 6.0),
]),
vec![],
);
let buf = buffer_polygon(&src, 0.5, BufferOptions::default());
let gpoly = Geometry::Polygon(buf);
assert!(!contains(&gpoly, &Geometry::Point(Coord::xy(3.0, 3.0))));
}
#[test]
fn polygon_buffer_shrinks_hole_for_small_distance() {
let src = Polygon::new(
LinearRing::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(10.0, 0.0),
Coord::xy(10.0, 10.0),
Coord::xy(0.0, 10.0),
]),
vec![LinearRing::new(vec![
Coord::xy(3.0, 3.0),
Coord::xy(7.0, 3.0),
Coord::xy(7.0, 7.0),
Coord::xy(3.0, 7.0),
])],
);
let buf = buffer_polygon(&src, 0.5, BufferOptions::default());
let gpoly = Geometry::Polygon(buf.clone());
assert!(contains(&gpoly, &Geometry::Point(Coord::xy(3.2, 5.0))));
assert!(!contains(&gpoly, &Geometry::Point(Coord::xy(5.0, 5.0))));
assert_eq!(buf.holes.len(), 1);
}
#[test]
fn polygon_buffer_drops_hole_when_it_collapses() {
let src = Polygon::new(
LinearRing::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(10.0, 0.0),
Coord::xy(10.0, 10.0),
Coord::xy(0.0, 10.0),
]),
vec![LinearRing::new(vec![
Coord::xy(3.0, 3.0),
Coord::xy(7.0, 3.0),
Coord::xy(7.0, 7.0),
Coord::xy(3.0, 7.0),
])],
);
let buf = buffer_polygon(&src, 2.5, BufferOptions::default());
let gpoly = Geometry::Polygon(buf.clone());
assert!(contains(&gpoly, &Geometry::Point(Coord::xy(5.0, 5.0))));
assert!(buf.holes.is_empty());
}
#[test]
fn polygon_buffer_negative_distance_expands_hole() {
let src = Polygon::new(
LinearRing::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(10.0, 0.0),
Coord::xy(10.0, 10.0),
Coord::xy(0.0, 10.0),
]),
vec![LinearRing::new(vec![
Coord::xy(4.0, 4.0),
Coord::xy(6.0, 4.0),
Coord::xy(6.0, 6.0),
Coord::xy(4.0, 6.0),
])],
);
let buf = buffer_polygon(&src, -0.5, BufferOptions::default());
let gpoly = Geometry::Polygon(buf.clone());
assert_eq!(buf.holes.len(), 1);
assert!(!contains(&gpoly, &Geometry::Point(Coord::xy(3.75, 5.0))));
assert!(!contains(&gpoly, &Geometry::Point(Coord::xy(5.0, 5.0))));
assert!(contains(&gpoly, &Geometry::Point(Coord::xy(2.0, 2.0))));
}
#[test]
fn polygon_buffer_negative_distance_returns_empty_when_hole_consumes_shell() {
let src = Polygon::new(
LinearRing::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(10.0, 0.0),
Coord::xy(10.0, 10.0),
Coord::xy(0.0, 10.0),
]),
vec![LinearRing::new(vec![
Coord::xy(4.0, 4.0),
Coord::xy(6.0, 4.0),
Coord::xy(6.0, 6.0),
Coord::xy(4.0, 6.0),
])],
);
let buf = buffer_polygon(&src, -2.0, BufferOptions::default());
assert!(buf.exterior.coords.is_empty());
}
#[test]
fn linestring_buffer_sharp_angle_is_valid_polygon() {
let ls = LineString::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(4.0, 0.0),
Coord::xy(4.2, 5.0),
]);
let buf = buffer_linestring(
&ls,
1.0,
BufferOptions {
quadrant_segments: 12,
join_style: BufferJoinStyle::Mitre,
..Default::default()
},
);
assert!(is_valid_polygon(&buf));
}
#[test]
fn linestring_buffer_round_sharp_angle_keeps_outer_arc_and_inner_corridor() {
let ls = LineString::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(4.0, 0.0),
Coord::xy(4.2, 5.0),
]);
let buf = buffer_linestring(
&ls,
1.0,
BufferOptions {
quadrant_segments: 12,
join_style: BufferJoinStyle::Round,
..Default::default()
},
);
let gpoly = Geometry::Polygon(buf.clone());
assert!(is_valid_polygon(&buf));
assert!(
contains(&gpoly, &Geometry::Point(Coord::xy(3.28, 0.69))),
"outer round-join lobe near the sharp bend should be preserved"
);
assert!(
contains(&gpoly, &Geometry::Point(Coord::xy(4.80, -0.60))),
"inner corridor near the sharp bend should not be clipped away"
);
}
#[test]
fn polygon_buffer_sharp_concavity_is_valid_polygon() {
let src = Polygon::new(
LinearRing::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(8.0, 0.0),
Coord::xy(8.0, 1.0),
Coord::xy(1.2, 1.0),
Coord::xy(1.0, 8.0),
Coord::xy(0.0, 8.0),
]),
vec![],
);
let buf = buffer_polygon(
&src,
0.8,
BufferOptions {
quadrant_segments: 12,
join_style: BufferJoinStyle::Round,
..Default::default()
},
);
assert!(is_valid_polygon(&buf));
}
#[test]
fn linestring_buffer_mitre_limit_clamps_spike_extent() {
let ls = LineString::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(5.0, 0.0),
Coord::xy(5.5, 5.0),
]);
let high = buffer_linestring(
&ls,
1.0,
BufferOptions {
join_style: BufferJoinStyle::Mitre,
mitre_limit: 10.0,
..Default::default()
},
);
let low = buffer_linestring(
&ls,
1.0,
BufferOptions {
join_style: BufferJoinStyle::Mitre,
mitre_limit: 1.2,
..Default::default()
},
);
let high_max_x = high
.exterior
.coords
.iter()
.map(|c| c.x)
.fold(f64::NEG_INFINITY, f64::max);
let low_max_x = low
.exterior
.coords
.iter()
.map(|c| c.x)
.fold(f64::NEG_INFINITY, f64::max);
assert!(high_max_x >= low_max_x);
}
#[test]
fn precision_aware_buffer_snaps_output_grid() {
let pm = PrecisionModel::Fixed { scale: 10.0 };
let point_buf = buffer_point_with_precision(
Coord::xy(0.03, 0.07),
1.0,
BufferOptions::default(),
pm,
);
for c in &point_buf.exterior.coords {
let sx = c.x * 10.0;
let sy = c.y * 10.0;
assert!((sx - sx.round()).abs() < 1.0e-9);
assert!((sy - sy.round()).abs() < 1.0e-9);
}
let ls = LineString::new(vec![Coord::xy(0.02, 0.03), Coord::xy(5.07, 0.04)]);
let _ = buffer_linestring_with_precision(&ls, 0.5, BufferOptions::default(), pm);
let poly = Polygon::new(
LinearRing::new(vec![
Coord::xy(0.01, 0.01),
Coord::xy(2.09, 0.01),
Coord::xy(2.09, 2.09),
Coord::xy(0.01, 2.09),
]),
vec![],
);
let _ = buffer_polygon_with_precision(&poly, 0.3, BufferOptions::default(), pm);
}
#[test]
fn buffer_polygon_multi_positive_distance_returns_single_polygon() {
let src = Polygon::new(
LinearRing::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(4.0, 0.0),
Coord::xy(4.0, 4.0),
Coord::xy(0.0, 4.0),
]),
vec![],
);
let result = buffer_polygon_multi(&src, 1.0, BufferOptions::default());
assert_eq!(result.len(), 1);
assert!(ring_area_abs(&result[0].exterior.coords) > 16.0);
}
#[test]
fn buffer_polygon_multi_zero_distance_returns_repaired_copy() {
let src = Polygon::new(
LinearRing::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(6.0, 0.0),
Coord::xy(6.0, 6.0),
Coord::xy(0.0, 6.0),
]),
vec![],
);
let result = buffer_polygon_multi(&src, 0.0, BufferOptions::default());
assert_eq!(result.len(), 1);
let area = ring_area_abs(&result[0].exterior.coords);
assert!((area - 36.0).abs() < 0.1);
}
#[test]
fn buffer_polygon_multi_negative_distance_shrinks_square() {
let src = Polygon::new(
LinearRing::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(10.0, 0.0),
Coord::xy(10.0, 10.0),
Coord::xy(0.0, 10.0),
]),
vec![],
);
let result = buffer_polygon_multi(&src, -1.0, BufferOptions::default());
assert_eq!(result.len(), 1);
let area = ring_area_abs(&result[0].exterior.coords);
assert!(area > 60.0 && area < 68.0);
}
#[test]
fn buffer_polygon_multi_negative_fully_eroded_returns_empty() {
let src = Polygon::new(
LinearRing::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(2.0, 0.0),
Coord::xy(2.0, 2.0),
Coord::xy(0.0, 2.0),
]),
vec![],
);
let result = buffer_polygon_multi(&src, -2.0, BufferOptions::default());
assert!(result.is_empty());
}
#[test]
fn buffer_polygon_multi_simple_concave_erosion_stays_single_component() {
let src = Polygon::new(
LinearRing::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(10.0, 0.0),
Coord::xy(10.0, 6.0),
Coord::xy(6.0, 6.0),
Coord::xy(6.0, 10.0),
Coord::xy(0.0, 10.0),
]),
vec![],
);
let result = buffer_polygon_multi(&src, -0.5, BufferOptions::default());
assert_eq!(result.len(), 1);
assert!(ring_area_abs(&result[0].exterior.coords) > 0.0);
}
#[test]
fn buffer_polygon_multi_returns_components_for_deeply_eroded_h_shape() {
let outer = LinearRing::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(30.0, 0.0),
Coord::xy(30.0, 20.0),
Coord::xy(0.0, 20.0),
]);
let left_notch = LinearRing::new(vec![
Coord::xy(0.0, 6.0),
Coord::xy(13.0, 6.0),
Coord::xy(13.0, 14.0),
Coord::xy(0.0, 14.0),
]);
let right_notch = LinearRing::new(vec![
Coord::xy(17.0, 6.0),
Coord::xy(30.0, 6.0),
Coord::xy(30.0, 14.0),
Coord::xy(17.0, 14.0),
]);
let src = Polygon::new(outer, vec![left_notch, right_notch]);
let result = buffer_polygon_multi(&src, -3.0, BufferOptions::default());
assert!(!result.is_empty());
for comp in &result {
assert!(ring_area_abs(&comp.exterior.coords) > 1.0);
}
}
#[test]
fn buffer_problem_building_cw_ring() {
let coords = vec![
Coord::xy(562637.537417164, 4818648.0372987),
Coord::xy(562644.815914335, 4818647.60529134),
Coord::xy(562644.928259569, 4818649.42786198),
Coord::xy(562654.188202102, 4818648.86993476),
Coord::xy(562653.642237858, 4818639.80165489),
Coord::xy(562650.180841506, 4818640.01375644),
Coord::xy(562649.83002479, 4818634.29045826),
Coord::xy(562638.621044375, 4818634.96351884),
Coord::xy(562638.991254563, 4818641.20901904),
Coord::xy(562637.131244156, 4818641.31386885),
Coord::xy(562637.537417164, 4818648.0372987), ];
let poly = Polygon::new(LinearRing::new(coords), vec![]);
let distance = 5.0;
let options = BufferOptions {
quadrant_segments: 8,
cap_style: BufferCapStyle::Round,
join_style: BufferJoinStyle::Round,
mitre_limit: 5.0,
};
let result = buffer_polygon_multi(&poly, distance, options);
assert_eq!(result.len(), 1, "Expected 1 buffer component, got {}", result.len());
let buf = &result[0];
assert!(is_valid_polygon(buf), "Buffer polygon is invalid");
let min_x = buf.exterior.coords.iter().map(|c| c.x).fold(f64::INFINITY, f64::min);
let max_x = buf.exterior.coords.iter().map(|c| c.x).fold(f64::NEG_INFINITY, f64::max);
let min_y = buf.exterior.coords.iter().map(|c| c.y).fold(f64::INFINITY, f64::min);
let max_y = buf.exterior.coords.iter().map(|c| c.y).fold(f64::NEG_INFINITY, f64::max);
assert!(min_x < 562637.131 - 4.0, "Buffer too narrow on left: min_x={}", min_x);
assert!(max_x > 562654.188 + 4.0, "Buffer too narrow on right: max_x={}", max_x);
assert!(min_y < 4818634.290 - 4.0, "Buffer too narrow on bottom: min_y={}", min_y);
assert!(max_y > 4818649.428 + 4.0, "Buffer too narrow on top: max_y={}", max_y);
let buf_area = ring_area_abs(&buf.exterior.coords);
assert!(buf_area > 400.0, "Buffer area {} too small (source ~201 m²)", buf_area);
}
#[test]
fn offset_linestring_left_horizontal() {
let ls = LineString::new(vec![Coord::xy(0.0, 0.0), Coord::xy(10.0, 0.0)]);
let opts = OffsetCurveOptions::default();
let result = offset_linestring(&ls, 3.0, OffsetSide::Left, opts);
assert_eq!(result.coords.len(), 2, "Expected 2 coords for simple horizontal line");
for c in &result.coords {
assert!((c.y - 3.0).abs() < 1.0e-9, "Left offset of horizontal line should have y≈3, got {}", c.y);
}
assert!((result.coords[0].x - 0.0).abs() < 1.0e-9);
assert!((result.coords[1].x - 10.0).abs() < 1.0e-9);
}
#[test]
fn offset_linestring_right_horizontal() {
let ls = LineString::new(vec![Coord::xy(0.0, 0.0), Coord::xy(10.0, 0.0)]);
let opts = OffsetCurveOptions::default();
let result = offset_linestring(&ls, 3.0, OffsetSide::Right, opts);
assert_eq!(result.coords.len(), 2);
for c in &result.coords {
assert!((c.y - (-3.0)).abs() < 1.0e-9, "Right offset should have y≈-3, got {}", c.y);
}
}
#[test]
fn offset_linestring_returns_open_not_closed() {
let ls = LineString::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(10.0, 0.0),
Coord::xy(10.0, 10.0),
]);
let result = offset_linestring(&ls, 2.0, OffsetSide::Left, OffsetCurveOptions::default());
assert!(result.coords.len() >= 2, "Result should have at least 2 coords");
let first = result.coords.first().unwrap();
let last = result.coords.last().unwrap();
let dist2 = (first.x - last.x).powi(2) + (first.y - last.y).powi(2);
assert!(dist2 > 1.0e-6, "Offset linestring should be open (first ≠last)");
}
#[test]
fn offset_linestring_preserves_direction() {
let ls = LineString::new(vec![Coord::xy(0.0, 0.0), Coord::xy(0.0, 10.0)]);
let result = offset_linestring(&ls, 2.0, OffsetSide::Left, OffsetCurveOptions::default());
assert_eq!(result.coords.len(), 2);
for c in &result.coords {
assert!(c.x < -1.9, "Left of north-going line should be west (x < 0), got {}", c.x);
}
}
#[test]
fn offset_linestring_right_preserves_direction() {
let ls = LineString::new(vec![Coord::xy(0.0, 0.0), Coord::xy(0.0, 10.0)]);
let result = offset_linestring(&ls, 2.0, OffsetSide::Right, OffsetCurveOptions::default());
assert_eq!(result.coords.len(), 2);
for c in &result.coords {
assert!(c.x > 1.9, "Right of north-going line should be east (x > 0), got {}", c.x);
}
}
#[test]
fn offset_linestring_mitre_join_style() {
let ls = LineString::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(10.0, 0.0),
Coord::xy(10.0, 10.0),
]);
let opts = OffsetCurveOptions {
join_style: BufferJoinStyle::Mitre,
..Default::default()
};
let result = offset_linestring(&ls, 2.0, OffsetSide::Left, opts);
assert_eq!(result.coords.len(), 3, "Mitre join should produce 3 coords, got {}", result.coords.len());
}
#[test]
fn offset_linestring_round_join_adds_arc_vertices() {
let ls = LineString::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(10.0, 0.0),
Coord::xy(10.0, 10.0),
]);
let opts = OffsetCurveOptions {
join_style: BufferJoinStyle::Round,
quadrant_segments: 8,
..Default::default()
};
let result = offset_linestring(&ls, 2.0, OffsetSide::Left, opts);
assert!(result.coords.len() > 3, "Round join should insert arc vertices (got {})", result.coords.len());
}
#[test]
fn offset_linestring_zero_distance_returns_empty() {
let ls = LineString::new(vec![Coord::xy(0.0, 0.0), Coord::xy(10.0, 0.0)]);
let result = offset_linestring(&ls, 0.0, OffsetSide::Left, OffsetCurveOptions::default());
assert!(result.coords.is_empty(), "Zero distance should return empty linestring");
}
#[test]
fn offset_linestring_negative_distance_returns_empty() {
let ls = LineString::new(vec![Coord::xy(0.0, 0.0), Coord::xy(10.0, 0.0)]);
let result = offset_linestring(&ls, -1.0, OffsetSide::Left, OffsetCurveOptions::default());
assert!(result.coords.is_empty(), "Negative distance should return empty linestring");
}
#[test]
fn offset_linestring_single_point_returns_empty() {
let ls = LineString::new(vec![Coord::xy(5.0, 5.0)]);
let result = offset_linestring(&ls, 1.0, OffsetSide::Left, OffsetCurveOptions::default());
assert!(result.coords.is_empty(), "Single-point input should return empty linestring");
}
#[test]
fn offset_linestring_multipoint_preserves_vertex_count_mitre() {
let ls = LineString::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(10.0, 5.0),
Coord::xy(20.0, 0.0),
Coord::xy(30.0, 5.0),
Coord::xy(40.0, 0.0),
]);
let opts = OffsetCurveOptions {
join_style: BufferJoinStyle::Mitre,
..Default::default()
};
let result = offset_linestring(&ls, 2.0, OffsetSide::Left, opts);
assert_eq!(result.coords.len(), 5, "5-point path with Mitre should have 5 output coords, got {}", result.coords.len());
}
#[test]
fn offset_linestring_left_right_symmetric_about_centreline() {
let ls = LineString::new(vec![
Coord::xy(0.0, 0.0),
Coord::xy(10.0, 0.0),
Coord::xy(20.0, 0.0),
]);
let opts = OffsetCurveOptions { join_style: BufferJoinStyle::Mitre, ..Default::default() };
let left = offset_linestring(&ls, 3.0, OffsetSide::Left, opts);
let right = offset_linestring(&ls, 3.0, OffsetSide::Right, opts);
assert_eq!(left.coords.len(), right.coords.len(), "Left and right should have same vertex count");
for (l, r) in left.coords.iter().zip(right.coords.iter()) {
assert!((l.x - r.x).abs() < 1.0e-9, "x-coords should match: {} vs {}", l.x, r.x);
assert!((l.y + r.y).abs() < 1.0e-9, "left.y + right.y should be 0: {} + {}", l.y, r.y);
}
}