#![expect(
clippy::cast_possible_truncation,
reason = "shape iteration (usize->i32) — count always in i32 range"
)]
#![expect(
clippy::cast_possible_wrap,
reason = "shape iteration (usize->i32) — count always in i32 range"
)]
use std::f64::consts::{FRAC_PI_2, PI};
use crate::s1::{self, ChordAngle};
use crate::s2::builder::layer::Layer;
use crate::s2::builder::snap::{IdentitySnapFunction, SnapFunction};
use crate::s2::builder::{Options as BuilderOptions, S2Error, S2ErrorCode};
use crate::s2::contains_point_query::{ContainsPointQuery, VertexModel};
use crate::s2::edge_crosser::EdgeCrosser;
use crate::s2::edge_crossings::angle_contains_vertex;
use crate::s2::edge_distances;
use crate::s2::lax_loop::LaxLoop;
use crate::s2::ortho;
use crate::s2::predicates;
use crate::s2::shape::{Dimension, Shape};
use crate::s2::shape_index::ShapeIndex;
use crate::s2::shape_util;
use crate::s2::winding_operation::{S2WindingOperation, WindingOptions, WindingRule};
use crate::s2::{Point, Polygon};
const MIN_REQUESTED_ERROR: f64 = 2.0 * predicates::DBL_ERROR;
const MAX_ABSOLUTE_INTERPOLATION_ERROR: f64 = {
let dbl_err = predicates::DBL_ERROR;
let robust_cross_prod_error = 6.0 * dbl_err;
let get_point_on_line_error =
(4.0 + 2.0 / 1.7320508075688772) * dbl_err + robust_cross_prod_error;
let get_point_on_ray_perp_error = 3.0 * dbl_err;
get_point_on_line_error + get_point_on_ray_perp_error
};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum EndCapStyle {
#[default]
Round,
Flat,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum PolylineSide {
Left,
Right,
#[default]
Both,
}
pub const MIN_ERROR_FRACTION: f64 = 1e-6;
pub const MAX_CIRCLE_SEGMENTS: f64 = 1570.7968503979573;
#[derive(Debug)]
pub struct BufferOptions {
buffer_radius: s1::Angle,
error_fraction: f64,
end_cap_style: EndCapStyle,
polyline_side: PolylineSide,
snap_function: Box<dyn SnapFunction>,
pub memory_tracker:
Option<std::sync::Arc<std::sync::Mutex<crate::s2::memory_tracker::S2MemoryTracker>>>,
}
impl Default for BufferOptions {
fn default() -> Self {
BufferOptions {
buffer_radius: s1::Angle::ZERO,
error_fraction: 0.02,
end_cap_style: EndCapStyle::Round,
polyline_side: PolylineSide::Both,
snap_function: Box::new(IdentitySnapFunction::new(s1::Angle::ZERO)),
memory_tracker: None,
}
}
}
impl BufferOptions {
pub fn new(buffer_radius: s1::Angle) -> Self {
BufferOptions {
buffer_radius,
..Default::default()
}
}
pub fn buffer_radius(&self) -> s1::Angle {
self.buffer_radius
}
pub fn set_buffer_radius(&mut self, r: s1::Angle) {
self.buffer_radius = r;
}
pub fn error_fraction(&self) -> f64 {
self.error_fraction
}
pub fn set_error_fraction(&mut self, f: f64) {
debug_assert!(f >= MIN_ERROR_FRACTION);
debug_assert!(f <= 1.0);
self.error_fraction = f.clamp(MIN_ERROR_FRACTION, 1.0);
}
pub fn max_error(&self) -> s1::Angle {
let abs_rad = self.buffer_radius.radians().abs();
let requested = self.error_fraction * abs_rad;
let requested = requested.max(MIN_REQUESTED_ERROR);
let mut builder_opts = BuilderOptions::new(self.snap_function.clone_snap());
builder_opts.split_crossing_edges = true;
let snap_error = builder_opts.max_edge_deviation();
s1::Angle::from_radians(requested + MAX_ABSOLUTE_INTERPOLATION_ERROR + snap_error.radians())
}
pub fn circle_segments(&self) -> f64 {
PI / (1.0 - self.error_fraction).acos()
}
pub fn set_circle_segments(&mut self, n: f64) {
debug_assert!(n >= 2.0);
debug_assert!(n <= MAX_CIRCLE_SEGMENTS);
let n = n.clamp(2.0, MAX_CIRCLE_SEGMENTS);
self.set_error_fraction(1.0 - (PI / n).cos() + 1e-15);
}
pub fn end_cap_style(&self) -> EndCapStyle {
self.end_cap_style
}
pub fn set_end_cap_style(&mut self, style: EndCapStyle) {
self.end_cap_style = style;
}
pub fn polyline_side(&self) -> PolylineSide {
self.polyline_side
}
pub fn set_polyline_side(&mut self, side: PolylineSide) {
self.polyline_side = side;
}
pub fn snap_function(&self) -> &dyn SnapFunction {
&*self.snap_function
}
pub fn set_snap_function(&mut self, f: Box<dyn SnapFunction>) {
self.snap_function = f;
}
}
#[derive(Debug)]
pub struct S2BufferOperation {
options: BufferOptions,
num_polygon_layers: i32,
buffer_sign: i32,
abs_radius: ChordAngle,
vertex_step: ChordAngle,
edge_step: ChordAngle,
point_step: ChordAngle,
op: S2WindingOperation,
path: Vec<Point>,
ref_point: Point,
ref_winding: i32,
sweep_a: Point,
sweep_b: Point,
input_start: Point,
offset_start: Point,
have_input_start: bool,
have_offset_start: bool,
}
impl S2BufferOperation {
pub fn new(result_layer: Box<dyn Layer>, options: BufferOptions) -> Self {
let buffer_sign = sgn(options.buffer_radius.radians());
let abs_radius_angle = s1::Angle::from_radians(options.buffer_radius.radians().abs());
let requested_error =
(options.error_fraction * abs_radius_angle.radians()).max(MIN_REQUESTED_ERROR);
let max_error = MAX_ABSOLUTE_INTERPOLATION_ERROR + requested_error;
let (buffer_sign, abs_radius, vertex_step, edge_step, point_step) =
if abs_radius_angle.radians() <= max_error {
(
0,
ChordAngle::ZERO,
ChordAngle::ZERO,
ChordAngle::ZERO,
ChordAngle::ZERO,
)
} else if abs_radius_angle.radians() + max_error >= PI {
(
buffer_sign,
ChordAngle::STRAIGHT,
ChordAngle::ZERO,
ChordAngle::ZERO,
ChordAngle::ZERO,
)
} else {
let abs_ca = ChordAngle::from_angle(abs_radius_angle);
let vs =
get_max_edge_span(abs_radius_angle, s1::Angle::from_radians(requested_error));
let vs_ca = ChordAngle::from_angle(vs);
let ps_rad = 2.0 * PI / (2.0 * PI / vs.radians()).ceil() + 1e-15;
let ps_ca = ChordAngle::from_radians(ps_rad);
let edge_radius = s1::Angle::from_radians(FRAC_PI_2 - abs_radius_angle.radians());
let es_ca = if edge_radius.radians() > max_error {
ChordAngle::from_angle(get_max_edge_span(
edge_radius,
s1::Angle::from_radians(requested_error),
))
} else {
ChordAngle::ZERO
};
(buffer_sign, abs_ca, vs_ca, es_ca, ps_ca)
};
let mut winding_options =
WindingOptions::with_snap_function(options.snap_function.clone_snap());
winding_options
.set_include_degeneracies(buffer_sign == 0 && options.buffer_radius >= s1::Angle::ZERO);
let op = S2WindingOperation::new(result_layer, winding_options);
S2BufferOperation {
options,
num_polygon_layers: 0,
buffer_sign,
abs_radius,
vertex_step,
edge_step,
point_step,
op,
path: Vec::new(),
ref_point: Point::origin(),
ref_winding: 0,
sweep_a: Point::origin(),
sweep_b: Point::origin(),
input_start: Point::origin(),
offset_start: Point::origin(),
have_input_start: false,
have_offset_start: false,
}
}
pub fn options(&self) -> &BufferOptions {
&self.options
}
fn set_input_vertex(&mut self, new_a: Point) {
if self.have_input_start {
debug_assert!(self.have_offset_start);
self.update_ref_winding(self.sweep_a, self.sweep_b, new_a);
} else {
self.input_start = new_a;
self.have_input_start = true;
}
self.sweep_a = new_a;
}
fn add_offset_vertex(&mut self, new_b: Point) {
self.path.push(new_b);
if self.have_offset_start {
debug_assert!(self.have_input_start);
self.update_ref_winding(self.sweep_a, self.sweep_b, new_b);
} else {
self.offset_start = new_b;
self.have_offset_start = true;
}
self.sweep_b = new_b;
}
fn close_buffer_region(&mut self) {
if self.have_offset_start && self.have_input_start {
self.update_ref_winding(self.sweep_a, self.sweep_b, self.input_start);
self.update_ref_winding(self.input_start, self.sweep_b, self.offset_start);
}
}
fn output_path(&mut self) {
let path = std::mem::take(&mut self.path);
self.op.add_loop(&path);
self.have_input_start = false;
self.have_offset_start = false;
}
fn update_ref_winding(&mut self, a: Point, b: Point, c: Point) {
let sign = predicates::robust_sign(a, b, c) as i32;
if sign == 0 {
return;
}
let mut inside = angle_contains_vertex(a, b, c) == (sign > 0);
let mut crosser = EdgeCrosser::new(b, self.ref_point);
inside ^= crosser.edge_or_vertex_crossing(a, b);
inside ^= crosser.edge_or_vertex_crossing(b, c);
inside ^= crosser.edge_or_vertex_crossing(c, a);
if inside {
self.ref_winding += sign;
}
}
fn add_full_polygon(&mut self) {
self.ref_winding += 1;
}
fn get_edge_axis(&self, a: Point, b: Point) -> Point {
debug_assert_ne!(self.buffer_sign, 0);
let robust_ba = b.point_cross(a);
Point(robust_ba.0.normalize() * f64::from(self.buffer_sign))
}
fn add_vertex_arc(&mut self, v: Point, start: Point, end: Point) {
let rotate_dir = Point(v.0.cross(start.0).normalize() * f64::from(self.buffer_sign));
let span = ChordAngle::from_points(start, end);
let mut angle = ChordAngle::ZERO;
loop {
let dir = edge_distances::point_on_ray_chord(start, rotate_dir, angle);
self.add_offset_vertex(edge_distances::point_on_ray_chord(v, dir, self.abs_radius));
angle = angle + self.vertex_step;
if angle >= span {
break;
}
}
}
fn close_vertex_arc(&mut self, v: Point, end: Point) {
self.add_offset_vertex(edge_distances::point_on_ray_chord(v, end, self.abs_radius));
}
fn add_edge_arc(&mut self, a: Point, b: Point) {
let ab_axis = self.get_edge_axis(a, b);
if self.edge_step == ChordAngle::ZERO {
self.add_offset_vertex(ab_axis);
} else {
let rotate_dir = Point(a.0.cross(ab_axis.0).normalize() * f64::from(self.buffer_sign));
let span = ChordAngle::from_points(a, b);
let mut angle = ChordAngle::ZERO;
loop {
let p = edge_distances::point_on_ray_chord(a, rotate_dir, angle);
self.add_offset_vertex(edge_distances::point_on_ray_chord(
p,
ab_axis,
self.abs_radius,
));
angle = angle + self.edge_step;
if angle >= span {
break;
}
}
}
self.set_input_vertex(b);
}
fn close_edge_arc(&mut self, a: Point, b: Point) {
if self.edge_step != ChordAngle::ZERO {
let axis = self.get_edge_axis(a, b);
self.add_offset_vertex(edge_distances::point_on_ray_chord(b, axis, self.abs_radius));
}
}
fn buffer_edge_and_vertex(&mut self, a: Point, b: Point, c: Point) {
debug_assert_ne!(a, b);
debug_assert_ne!(b, c);
debug_assert_ne!(self.buffer_sign, 0);
self.add_edge_arc(a, b);
let sign = predicates::robust_sign(a, b, c) as i32;
if self.buffer_sign * sign >= 0 {
let start = self.get_edge_axis(a, b);
let end = self.get_edge_axis(b, c);
self.add_vertex_arc(b, start, end);
if self.edge_step == ChordAngle::ZERO {
self.close_vertex_arc(b, end);
}
} else {
self.close_edge_arc(a, b);
self.add_offset_vertex(b);
}
}
fn add_start_cap(&mut self, a: Point, b: Point) {
let axis = self.get_edge_axis(a, b);
if self.options.end_cap_style == EndCapStyle::Flat {
if self.options.polyline_side == PolylineSide::Both {
self.add_offset_vertex(edge_distances::point_on_ray_chord(
a,
-axis,
self.abs_radius,
));
}
} else {
debug_assert!(self.options.end_cap_style == EndCapStyle::Round);
if self.options.polyline_side == PolylineSide::Both {
self.add_vertex_arc(a, -axis, axis);
} else {
let start = Point(axis.0.cross(a.0).normalize());
self.add_vertex_arc(a, start, axis);
}
}
}
fn add_end_cap(&mut self, a: Point, b: Point) {
let axis = self.get_edge_axis(a, b);
if self.options.end_cap_style == EndCapStyle::Flat {
self.close_edge_arc(a, b);
} else {
debug_assert!(self.options.end_cap_style == EndCapStyle::Round);
if self.options.polyline_side == PolylineSide::Both {
self.add_vertex_arc(b, axis, -axis);
} else {
let end = Point(b.0.cross(axis.0).normalize());
self.add_vertex_arc(b, axis, end);
self.close_vertex_arc(b, end);
}
}
}
fn buffer_loop(&mut self, loop_vertices: &[Point]) {
if loop_vertices.is_empty() {
return;
}
if loop_vertices.len() == 1 {
return self.add_point(loop_vertices[0]);
}
if self.abs_radius >= ChordAngle::STRAIGHT {
if self.buffer_sign > 0 {
self.add_full_polygon();
}
return;
}
if self.buffer_sign == 0 {
self.path.extend_from_slice(loop_vertices);
} else {
let n = loop_vertices.len();
self.set_input_vertex(loop_vertices[0]);
for i in 0..n {
let a = loop_vertices[i];
let b = loop_vertices[(i + 1) % n];
let c = loop_vertices[(i + 2) % n];
self.buffer_edge_and_vertex(a, b, c);
}
self.close_buffer_region();
}
self.output_path();
}
pub fn add_point(&mut self, point: Point) {
if self.buffer_sign < 0 {
return;
}
if self.abs_radius >= ChordAngle::STRAIGHT {
return self.add_full_polygon();
}
if self.buffer_sign == 0 {
self.path.push(point);
} else {
self.set_input_vertex(point);
let mut start = ortho(point);
let mut angle = ChordAngle::ZERO;
for _quadrant in 0..4 {
let rotate_dir = Point(point.0.cross(start.0).normalize());
while angle < ChordAngle::RIGHT {
let dir = edge_distances::point_on_ray_chord(start, rotate_dir, angle);
self.add_offset_vertex(edge_distances::point_on_ray_chord(
point,
dir,
self.abs_radius,
));
angle = angle + self.point_step;
}
angle = angle - ChordAngle::RIGHT;
start = rotate_dir;
}
self.close_buffer_region();
}
self.output_path();
}
pub fn add_polyline(&mut self, polyline: &[Point]) {
let reversed;
let polyline = if self.options.polyline_side == PolylineSide::Left {
reversed = polyline.iter().copied().rev().collect::<Vec<_>>();
&reversed
} else {
polyline
};
if self.buffer_sign < 0 {
return;
}
let n = polyline.len();
if n <= 1 {
return;
}
if n == 2 && polyline[0] == polyline[1] {
return self.add_point(polyline[0]);
}
if self.abs_radius >= ChordAngle::STRAIGHT {
return self.add_full_polygon();
}
if self.buffer_sign == 0 {
self.path.extend_from_slice(&polyline[..n - 1]);
self.path.extend(polyline.iter().rev().skip(1).copied());
} else {
self.set_input_vertex(polyline[0]);
self.add_start_cap(polyline[0], polyline[1]);
for i in 0..n - 2 {
self.buffer_edge_and_vertex(polyline[i], polyline[i + 1], polyline[i + 2]);
}
self.add_edge_arc(polyline[n - 2], polyline[n - 1]);
self.add_end_cap(polyline[n - 2], polyline[n - 1]);
if self.options.polyline_side == PolylineSide::Both {
for i in (0..n - 2).rev() {
self.buffer_edge_and_vertex(polyline[i + 2], polyline[i + 1], polyline[i]);
}
self.add_edge_arc(polyline[1], polyline[0]);
self.close_buffer_region();
} else {
self.path.extend(polyline.iter().rev().copied());
}
}
self.output_path();
}
pub fn add_loop(&mut self, loop_vertices: &[Point]) {
if loop_vertices.is_empty() {
return;
}
self.buffer_loop(loop_vertices);
let lax = LaxLoop::new(loop_vertices.to_vec());
self.ref_winding += i32::from(shape_util::contains_brute_force(&lax, self.ref_point));
self.num_polygon_layers += 1;
}
pub fn add_shape(&mut self, shape: &dyn Shape) {
self.buffer_shape(shape);
self.ref_winding += i32::from(shape_util::contains_brute_force(shape, self.ref_point));
self.num_polygon_layers += i32::from(shape.dimension() == Dimension::Polygon);
}
pub fn add_shape_index(&mut self, index: &ShapeIndex) {
let mut max_dimension: Option<Dimension> = None;
for shape_id in 0..index.num_shape_ids() {
if let Some(shape) = index.shape(shape_id as i32) {
let d = shape.dimension();
max_dimension = Some(match max_dimension {
Some(prev) if prev >= d => prev,
_ => d,
});
self.buffer_shape(shape);
}
}
let mut query = ContainsPointQuery::new(index, VertexModel::SemiOpen);
self.ref_winding += i32::from(query.contains(self.ref_point));
self.num_polygon_layers += i32::from(max_dimension == Some(Dimension::Polygon));
}
fn buffer_shape(&mut self, shape: &dyn Shape) {
let dimension = shape.dimension();
let num_chains = shape.num_chains();
for c in 0..num_chains {
let chain = shape.chain(c);
if chain.length == 0 {
continue;
}
match dimension {
Dimension::Point => {
let edge = shape.chain_edge(c, 0);
self.add_point(edge.v0);
}
Dimension::Polyline => {
let vertices = get_chain_vertices(shape, c);
self.add_polyline(&vertices);
}
Dimension::Polygon => {
let vertices = get_chain_vertices(shape, c);
self.buffer_loop(&vertices);
}
}
}
}
pub fn build(&mut self) -> Result<Box<dyn Layer>, S2Error> {
if self.buffer_sign < 0 && self.num_polygon_layers > 1 {
return Err(S2Error::new(
S2ErrorCode::FailedPrecondition,
"Negative buffer radius requires at most one polygon layer",
));
}
self.op
.build(self.ref_point, self.ref_winding, WindingRule::Positive)
}
}
fn sgn(x: f64) -> i32 {
if x > 0.0 {
1
} else if x < 0.0 {
-1
} else {
0
}
}
fn get_max_edge_span(radius: s1::Angle, requested_error: s1::Angle) -> s1::Angle {
let mut step = s1::Angle::from_radians(2.0 * PI / 3.0 + 1e-15);
let min_radius = radius - requested_error;
debug_assert!(min_radius.radians() >= 0.0);
if radius.radians() < FRAC_PI_2 {
let s = s1::Angle::from_radians(
2.0 * (min_radius.radians().tan() / radius.radians().tan()).acos(),
);
if s < step {
step = s;
}
} else if min_radius.radians() > FRAC_PI_2 {
let s = s1::Angle::from_radians(
2.0 * (radius.radians().tan() / min_radius.radians().tan()).acos(),
);
if s < step {
step = s;
}
}
step
}
fn get_chain_vertices(shape: &dyn Shape, chain_id: usize) -> Vec<Point> {
let chain = shape.chain(chain_id);
let dimension = shape.dimension();
let mut vertices = Vec::with_capacity(chain.length + 1);
if chain.length == 0 {
return vertices;
}
vertices.push(shape.chain_edge(chain_id, 0).v0);
for i in 0..chain.length {
vertices.push(shape.chain_edge(chain_id, i).v1);
}
if dimension == Dimension::Polygon {
if !vertices.is_empty() && vertices.last() == vertices.first() {
vertices.pop();
}
}
vertices
}
impl ChordAngle {
fn from_points(a: Point, b: Point) -> Self {
a.chord_angle(b)
}
}
fn run_buffer(options: BufferOptions, add: impl FnOnce(&mut S2BufferOperation)) -> Polygon {
use crate::s2::builder::polygon_layer::S2PolygonLayer;
use std::cell::RefCell;
use std::rc::Rc;
let output = Rc::new(RefCell::new(Polygon::empty()));
let layer = S2PolygonLayer::new_legacy(Rc::clone(&output));
let mut op = S2BufferOperation::new(Box::new(layer), options);
add(&mut op);
let _outcome = op.build();
output.borrow().clone()
}
pub fn buffer_polygon(polygon: &Polygon, options: BufferOptions) -> Polygon {
run_buffer(options, |op| op.add_shape(polygon))
}
pub fn buffer_polyline(vertices: &[Point], options: BufferOptions) -> Polygon {
run_buffer(options, |op| op.add_polyline(vertices))
}
pub fn buffer_loop(vertices: &[Point], options: BufferOptions) -> Polygon {
run_buffer(options, |op| op.add_loop(vertices))
}
pub fn buffer_point(point: Point, options: BufferOptions) -> Polygon {
run_buffer(options, |op| op.add_point(point))
}
#[cfg(test)]
#[path = "buffer_operation_tests.rs"]
mod buffer_operation_tests;
#[cfg(test)]
mod tests {
use super::*;
use crate::s2::builder::lax_polygon_layer::LaxPolygonLayer;
use crate::s2::lax_polygon::LaxPolygon;
use crate::s2::shape::Shape;
use crate::s2::text_format;
use crate::s2::{LatLng, Point};
use std::cell::RefCell;
use std::rc::Rc;
fn do_buffer(
input_fn: impl FnOnce(&mut S2BufferOperation),
options: BufferOptions,
) -> LaxPolygon {
let output = Rc::new(RefCell::new(LaxPolygon::empty()));
let layer = LaxPolygonLayer::new_legacy(Rc::clone(&output));
let mut op = S2BufferOperation::new(Box::new(layer), options);
input_fn(&mut op);
op.build().expect("Build failed");
output.borrow().clone()
}
fn do_buffer_radius(
input_fn: impl FnOnce(&mut S2BufferOperation),
buffer_radius: s1::Angle,
error_fraction: f64,
) -> LaxPolygon {
let mut options = BufferOptions::new(buffer_radius);
options.set_error_fraction(error_fraction);
do_buffer(input_fn, options)
}
fn test_buffer_empty(input_fn: impl Fn(&mut S2BufferOperation) + Clone) {
for &radius_deg in &[-200.0, -1.0, 0.0, 1.0, 200.0] {
let f = input_fn.clone();
let result =
do_buffer_radius(move |op| f(op), s1::Angle::from_degrees(radius_deg), 0.1);
assert!(
result.is_empty(),
"Expected empty output for radius={radius_deg}, got {} loops",
result.num_loops(),
);
}
}
fn test_buffer_full(input_fn: impl Fn(&mut S2BufferOperation) + Clone) {
for &radius_deg in &[-200.0, -1.0, 0.0, 1.0, 200.0] {
let f = input_fn.clone();
let result =
do_buffer_radius(move |op| f(op), s1::Angle::from_degrees(radius_deg), 0.1);
assert!(
result.is_full(),
"Expected full output for radius={radius_deg}",
);
}
}
#[test]
fn test_no_input() {
test_buffer_empty(|_op| {});
}
#[test]
fn test_empty_polyline() {
test_buffer_empty(|op| {
op.add_polyline(&[Point::from_coords(1.0, 0.0, 0.0)]);
});
}
#[test]
fn test_empty_loop() {
test_buffer_empty(|op| {
op.add_loop(&[]);
});
}
#[test]
fn test_full_polygon_shape() {
test_buffer_full(|op| {
let poly = text_format::make_lax_polygon("full");
op.add_shape(&poly);
});
}
#[test]
fn test_points_and_polylines_are_removed() {
let result = do_buffer_radius(
|op| {
op.add_point(LatLng::from_degrees(0.0, 0.0).to_point());
op.add_polyline(&[
LatLng::from_degrees(2.0, 2.0).to_point(),
LatLng::from_degrees(2.0, 3.0).to_point(),
]);
},
s1::Angle::from_degrees(-1.0),
0.1,
);
assert!(result.is_empty());
}
#[test]
fn test_buffered_points_are_symmetric() {
let result = do_buffer_radius(
|op| {
op.add_point(Point::from_coords(1.0, 0.0, 0.0));
},
s1::Angle::from_degrees(5.0),
0.001234567,
);
assert!(!result.is_empty(), "Expected non-empty output");
let n = result.num_loop_vertices(0);
assert!(n >= 3, "Expected at least 3 vertices, got {n}");
let v = |i: usize| result.loop_vertex(0, i);
let ref_len = v(0).distance(v(n - 1));
for i in 1..n {
let edge_len = v(i - 1).distance(v(i));
assert!(
(ref_len.radians() - edge_len.radians()).abs() < 1e-13,
"Edge {i} length {edge_len} differs from reference {ref_len}",
);
}
}
#[test]
fn test_set_circle_segments() {
let mut options = BufferOptions::new(s1::Angle::from_radians(1e-12));
for n in 3..=20 {
options.set_circle_segments(n as f64);
let result = do_buffer(
|op| {
op.add_point(Point::from_coords(1.0, 0.0, 0.0));
},
{
let mut o = BufferOptions::new(s1::Angle::from_radians(1e-12));
o.set_circle_segments(n as f64);
o
},
);
assert_eq!(
result.num_loop_vertices(0),
n,
"Expected {n} vertices for circle_segments={n}",
);
}
}
#[test]
fn test_negative_buffer_radius_multiple_layers() {
let layer = LaxPolygonLayer::new();
let mut op = S2BufferOperation::new(
Box::new(layer),
BufferOptions::new(s1::Angle::from_radians(-1.0)),
);
op.add_loop(&[
LatLng::from_degrees(0.0, 0.0).to_point(),
LatLng::from_degrees(0.0, 1.0).to_point(),
LatLng::from_degrees(1.0, 0.0).to_point(),
]);
op.add_loop(&[
LatLng::from_degrees(2.0, 2.0).to_point(),
LatLng::from_degrees(2.0, 3.0).to_point(),
LatLng::from_degrees(3.0, 2.0).to_point(),
]);
let err = op
.build()
.expect_err("Expected build to fail with multiple polygon layers and negative radius");
assert_eq!(err.code, S2ErrorCode::FailedPrecondition);
}
#[test]
fn test_point_buffer_basic() {
let center = LatLng::from_degrees(0.0, 0.0).to_point();
let result = do_buffer_radius(
|op| op.add_point(center),
s1::Angle::from_degrees(1.0),
0.01,
);
assert!(!result.is_empty(), "Point buffer should produce output");
let n = result.num_loop_vertices(0);
for i in 0..n {
let v = result.loop_vertex(0, i);
let dist = center.distance(v);
assert!(
(dist.degrees() - 1.0).abs() < 0.02,
"Vertex {i} at distance {:.4}° from center, expected ≈1°",
dist.degrees(),
);
}
}
#[test]
fn test_loop_buffer_expands() {
let vertices = vec![
LatLng::from_degrees(0.0, 0.0).to_point(),
LatLng::from_degrees(0.0, 2.0).to_point(),
LatLng::from_degrees(2.0, 1.0).to_point(),
];
let result = do_buffer_radius(
|op| op.add_loop(&vertices),
s1::Angle::from_degrees(1.0),
0.01,
);
assert!(!result.is_empty(), "Loop buffer should produce output");
assert!(
result.num_loop_vertices(0) > 3,
"Buffered loop should have more vertices than original",
);
}
#[test]
fn test_polyline_buffer_basic() {
let polyline = vec![
LatLng::from_degrees(0.0, 0.0).to_point(),
LatLng::from_degrees(0.0, 5.0).to_point(),
];
let result = do_buffer_radius(
|op| op.add_polyline(&polyline),
s1::Angle::from_degrees(1.0),
0.01,
);
assert!(!result.is_empty(), "Polyline buffer should produce output");
}
#[test]
fn test_loop_contract() {
let vertices = vec![
LatLng::from_degrees(-30.0, -30.0).to_point(),
LatLng::from_degrees(-30.0, 30.0).to_point(),
LatLng::from_degrees(30.0, 0.0).to_point(),
];
let result = do_buffer_radius(
|op| op.add_loop(&vertices),
s1::Angle::from_degrees(-1.0),
0.01,
);
assert!(!result.is_empty(), "Contracted loop should still exist");
}
#[test]
fn test_zero_radius_passthrough() {
let vertices = vec![
LatLng::from_degrees(0.0, 0.0).to_point(),
LatLng::from_degrees(0.0, 10.0).to_point(),
LatLng::from_degrees(10.0, 5.0).to_point(),
];
let result = do_buffer_radius(|op| op.add_loop(&vertices), s1::Angle::ZERO, 0.01);
assert!(!result.is_empty(), "Zero-radius should pass through");
}
#[test]
fn test_flat_end_cap() {
let polyline = vec![
LatLng::from_degrees(0.0, 0.0).to_point(),
LatLng::from_degrees(0.0, 5.0).to_point(),
];
let mut options = BufferOptions::new(s1::Angle::from_degrees(1.0));
options.set_end_cap_style(EndCapStyle::Flat);
let result = do_buffer(|op| op.add_polyline(&polyline), options);
assert!(!result.is_empty(), "Flat cap buffer should produce output");
}
#[test]
fn test_polyline_one_sided() {
let polyline = vec![
LatLng::from_degrees(0.0, 0.0).to_point(),
LatLng::from_degrees(0.0, 5.0).to_point(),
];
let mut options = BufferOptions::new(s1::Angle::from_degrees(1.0));
options.set_polyline_side(PolylineSide::Right);
let result = do_buffer(|op| op.add_polyline(&polyline), options);
assert!(!result.is_empty(), "One-sided buffer should produce output",);
}
fn take_lax_polygon(layer: Box<dyn Layer>) -> LaxPolygon {
layer
.into_any()
.downcast::<LaxPolygonLayer>()
.expect("expected a LaxPolygonLayer")
.take_output()
.unwrap_or_else(LaxPolygon::empty)
}
fn index_difference(a: &mut ShapeIndex, b: &mut ShapeIndex) -> LaxPolygon {
use crate::s2::boolean_operation::{OpType, Options as BooleanOptions, S2BooleanOperation};
let mut op = S2BooleanOperation::new(
OpType::Difference,
Box::new(LaxPolygonLayer::new()),
BooleanOptions::default(),
);
let layers = op.build(a, b).expect("Difference failed");
layers
.into_iter()
.next()
.map(take_lax_polygon)
.unwrap_or_else(LaxPolygon::empty)
}
fn test_buffer_containment(vertices: &[Point], buffer_radius: s1::Angle, error_fraction: f64) {
use crate::s2::lax_loop::LaxLoop;
use crate::s2::shape_index::ShapeIndex;
let mut options = BufferOptions::new(buffer_radius);
options.set_error_fraction(error_fraction);
let max_error = options.max_error();
let mut op = S2BufferOperation::new(Box::new(LaxPolygonLayer::new()), options);
op.add_loop(vertices);
let output = take_lax_polygon(op.build().expect("Build failed"));
let mut input_index = ShapeIndex::new();
input_index.add(Box::new(LaxLoop::new(vertices.to_vec())));
let mut output_index = ShapeIndex::new();
output_index.add(Box::new(output.clone()));
if buffer_radius.radians() > max_error.radians() {
assert!(
index_difference(&mut input_index, &mut output_index).is_empty(),
"Output should contain input (positive buffer radius)",
);
} else if buffer_radius.radians() < -max_error.radians() {
assert!(
index_difference(&mut output_index, &mut input_index).is_empty(),
"Input should contain output (negative buffer radius)",
);
}
}
#[test]
fn test_square_expand() {
let vertices: Vec<Point> = [(-3.0, -3.0), (-3.0, 3.0), (3.0, 3.0), (3.0, -3.0)]
.iter()
.map(|&(lat, lng)| LatLng::from_degrees(lat, lng).to_point())
.collect();
test_buffer_containment(&vertices, s1::Angle::from_degrees(1.0), 0.01);
}
#[test]
fn test_square_contract() {
let vertices: Vec<Point> = [(-3.0, -3.0), (-3.0, 3.0), (3.0, 3.0), (3.0, -3.0)]
.iter()
.map(|&(lat, lng)| LatLng::from_degrees(lat, lng).to_point())
.collect();
test_buffer_containment(&vertices, s1::Angle::from_degrees(-1.0), 0.01);
}
#[test]
fn test_hollow_square() {
let outer: Vec<Point> = [(-3.0, -3.0), (-3.0, 3.0), (3.0, 3.0), (3.0, -3.0)]
.iter()
.map(|&(lat, lng)| LatLng::from_degrees(lat, lng).to_point())
.collect();
let inner: Vec<Point> = [(2.0, 2.0), (-2.0, 2.0), (-2.0, -2.0), (2.0, -2.0)]
.iter()
.map(|&(lat, lng)| LatLng::from_degrees(lat, lng).to_point())
.collect();
let output = Rc::new(RefCell::new(LaxPolygon::empty()));
let layer = LaxPolygonLayer::new_legacy(Rc::clone(&output));
let mut options = BufferOptions::new(s1::Angle::from_degrees(1.0));
options.set_error_fraction(0.01);
let mut op = S2BufferOperation::new(Box::new(layer), options);
op.add_loop(&outer);
op.add_loop(&inner);
op.build().expect("Build failed");
assert!(
!output.borrow().is_empty(),
"Hollow square buffer should produce output"
);
}
#[test]
#[ignore = "graph_edge_clipper multiplicity assertion with near-degenerate zigzag"]
fn test_zigzag_loop() {
let vertices: Vec<Point> = [
(0.0, 0.0),
(0.0, 7.0),
(5.0, 3.0),
(5.0, 10.0),
(6.0, 10.0),
(6.0, 1.0),
(1.0, 5.0),
(1.0, 0.0),
]
.iter()
.map(|&(lat, lng)| LatLng::from_degrees(lat, lng).to_point())
.collect();
test_buffer_containment(&vertices, s1::Angle::from_degrees(0.2), 0.01);
}
#[test]
fn test_poorly_normalized_point() {
let p = Point::from_coords(1.0 - 2.0 * f64::EPSILON, 0.0, 0.0);
let result = do_buffer_radius(|op| op.add_point(p), s1::Angle::from_degrees(1.0), 0.01);
assert!(
!result.is_empty(),
"Poorly normalized point should buffer ok"
);
}
#[test]
fn test_options_roundtrip() {
let options = BufferOptions::new(s1::Angle::from_radians(1e-12));
let layer = LaxPolygonLayer::new();
let op = S2BufferOperation::new(Box::new(layer), options);
assert!(
(op.options().buffer_radius().radians() - 1e-12).abs() < 1e-20,
"Buffer radius should be preserved in options()",
);
}
#[test]
fn test_empty_lax_polyline_shape() {
use crate::s2::lax_polyline::LaxPolyline;
test_buffer_empty(|op| {
let shape = LaxPolyline::new(vec![]);
op.add_shape(&shape);
});
}
#[test]
fn test_empty_polygon_shape() {
test_buffer_empty(|op| {
let poly = text_format::make_lax_polygon("");
op.add_shape(&poly);
});
}
#[test]
fn test_full_shape() {
let poly = text_format::make_lax_polygon("full");
let result = do_buffer_radius(
move |op| op.add_shape(&poly),
s1::Angle::from_degrees(1.0),
0.1,
);
assert!(
result.is_full(),
"Full polygon buffered by 1° should be full"
);
}
#[test]
fn test_empty_shape_index() {
test_buffer_empty(|op| {
let index = ShapeIndex::new();
op.add_shape_index(&index);
});
}
#[test]
fn test_polyline_left_side() {
let polyline = vec![
LatLng::from_degrees(0.0, 0.0).to_point(),
LatLng::from_degrees(0.0, 5.0).to_point(),
];
let mut options = BufferOptions::new(s1::Angle::from_degrees(1.0));
options.set_polyline_side(PolylineSide::Left);
let result = do_buffer(|op| op.add_polyline(&polyline), options);
assert!(
!result.is_empty(),
"Left-sided buffer should produce output"
);
}
#[test]
fn test_polyline_zigzag() {
let polyline: Vec<Point> = [(0.0, 0.0), (0.0, 7.0), (5.0, 3.0), (5.0, 10.0)]
.iter()
.map(|&(lat, lng)| LatLng::from_degrees(lat, lng).to_point())
.collect();
let mut options = BufferOptions::new(s1::Angle::from_degrees(1.0));
options.set_polyline_side(PolylineSide::Both);
options.set_end_cap_style(EndCapStyle::Round);
let result = do_buffer(|op| op.add_polyline(&polyline), options);
assert!(
!result.is_empty(),
"Zigzag polyline buffer with Both+Round should produce output",
);
}
#[test]
fn test_large_buffer_becomes_full() {
let result = do_buffer_radius(
|op| op.add_point(Point::from_coords(1.0, 0.0, 0.0)),
s1::Angle::from_degrees(200.0),
0.1,
);
assert!(
result.is_full(),
"200° buffer of point should be full polygon"
);
}
#[test]
fn test_degenerate_polyline_as_point() {
let p = LatLng::from_degrees(10.0, 20.0).to_point();
let result = do_buffer_radius(
|op| op.add_polyline(&[p, p]),
s1::Angle::from_degrees(1.0),
0.01,
);
assert!(
!result.is_empty(),
"Degenerate polyline should buffer like a point"
);
}
#[cfg(feature = "serde")]
#[test]
fn test_serde_enums_roundtrip() {
for e in [EndCapStyle::Round, EndCapStyle::Flat] {
let json = serde_json::to_string(&e).unwrap();
let back: EndCapStyle = serde_json::from_str(&json).unwrap();
assert_eq!(e, back);
}
for s in [PolylineSide::Left, PolylineSide::Right, PolylineSide::Both] {
let json = serde_json::to_string(&s).unwrap();
let back: PolylineSide = serde_json::from_str(&json).unwrap();
assert_eq!(s, back);
}
}
}