Skip to main content

oxigdal_algorithms/vector/
buffer.rs

1//! Buffer generation for geometries
2//!
3//! This module implements robust geometric buffering operations that create
4//! offset geometries around input features. Buffer operations are fundamental
5//! in spatial analysis for proximity analysis, safety zones, and cartographic
6//! generalization.
7//!
8//! # Implementation Notes
9//!
10//! The buffer algorithm uses parallel offset curves for linear geometries and
11//! Minkowski sum principles for polygons. The implementation handles:
12//!
13//! - Different cap styles (round, flat, square) for line endpoints
14//! - Different join styles (round, miter, bevel) for line vertices
15//! - Negative buffers (erosion) for polygons
16//! - Self-intersection resolution
17//!
18//! # Examples
19//!
20//! ```
21//! use oxigdal_algorithms::vector::{buffer_point, Point, BufferOptions};
22//!
23//! let point = Point::new(0.0, 0.0);
24//! let options = BufferOptions::default();
25//! let result = buffer_point(&point, 10.0, &options);
26//! ```
27
28use crate::error::{AlgorithmError, Result};
29use crate::vector::pool::{PoolGuard, get_pooled_polygon};
30use oxigdal_core::vector::{Coordinate, LineString, Point, Polygon};
31
32#[cfg(not(feature = "std"))]
33use core::f64::consts::PI;
34#[cfg(feature = "std")]
35use std::f64::consts::PI;
36
37/// End cap style for line buffers
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39pub enum BufferCapStyle {
40    /// Round caps (semi-circles at endpoints)
41    #[default]
42    Round,
43    /// Flat caps (perpendicular to line direction)
44    Flat,
45    /// Square caps (extended by buffer distance)
46    Square,
47}
48
49/// Join style for line buffers at vertices
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
51pub enum BufferJoinStyle {
52    /// Round joins (circular arcs)
53    #[default]
54    Round,
55    /// Miter joins (sharp points, with miter limit)
56    Miter,
57    /// Bevel joins (cut off at buffer distance)
58    Bevel,
59}
60
61/// Options for buffer operations
62#[derive(Debug, Clone)]
63pub struct BufferOptions {
64    /// Number of segments per quadrant for round caps/joins
65    pub quadrant_segments: usize,
66    /// Cap style for line endpoints
67    pub cap_style: BufferCapStyle,
68    /// Join style for line vertices
69    pub join_style: BufferJoinStyle,
70    /// Miter limit (ratio) for miter joins
71    pub miter_limit: f64,
72    /// Simplification tolerance (0.0 = no simplification)
73    pub simplify_tolerance: f64,
74}
75
76impl Default for BufferOptions {
77    fn default() -> Self {
78        Self {
79            quadrant_segments: 8,
80            cap_style: BufferCapStyle::Round,
81            join_style: BufferJoinStyle::Round,
82            miter_limit: 5.0,
83            simplify_tolerance: 0.0,
84        }
85    }
86}
87
88/// Generates a circular buffer around a point
89///
90/// # Arguments
91///
92/// * `center` - The center point
93/// * `radius` - Buffer radius (must be positive)
94/// * `options` - Buffer options controlling segment count and other parameters
95///
96/// # Errors
97///
98/// Returns error if radius is negative or non-finite
99pub fn buffer_point(center: &Point, radius: f64, options: &BufferOptions) -> Result<Polygon> {
100    if radius < 0.0 {
101        return Err(AlgorithmError::InvalidParameter {
102            parameter: "radius",
103            message: "radius must be non-negative".to_string(),
104        });
105    }
106
107    if !radius.is_finite() {
108        return Err(AlgorithmError::InvalidParameter {
109            parameter: "radius",
110            message: "radius must be finite".to_string(),
111        });
112    }
113
114    if radius == 0.0 {
115        // Degenerate case: return point as tiny polygon
116        return create_degenerate_polygon(&center.coord);
117    }
118
119    let segments = options.quadrant_segments * 4;
120    let mut coords = Vec::with_capacity(segments + 1);
121
122    for i in 0..segments {
123        let angle = 2.0 * PI * (i as f64) / (segments as f64);
124        let x = center.coord.x + radius * angle.cos();
125        let y = center.coord.y + radius * angle.sin();
126        coords.push(Coordinate::new_2d(x, y));
127    }
128
129    // Close the ring
130    coords.push(coords[0]);
131
132    let exterior = LineString::new(coords).map_err(AlgorithmError::Core)?;
133    Polygon::new(exterior, vec![]).map_err(AlgorithmError::Core)
134}
135
136/// Generates a buffer around a linestring
137///
138/// Creates a polygon buffer around a linestring using parallel offset curves
139/// on both sides, with configurable cap and join styles.
140///
141/// # Arguments
142///
143/// * `line` - The linestring to buffer
144/// * `distance` - Buffer distance (positive for expansion, negative for contraction)
145/// * `options` - Buffer options
146///
147/// # Errors
148///
149/// Returns error if linestring is invalid or has insufficient points
150pub fn buffer_linestring(
151    line: &LineString,
152    distance: f64,
153    options: &BufferOptions,
154) -> Result<Polygon> {
155    if line.coords.len() < 2 {
156        return Err(AlgorithmError::InsufficientData {
157            operation: "buffer_linestring",
158            message: "linestring must have at least 2 coordinates".to_string(),
159        });
160    }
161
162    if !distance.is_finite() {
163        return Err(AlgorithmError::InvalidParameter {
164            parameter: "distance",
165            message: "distance must be finite".to_string(),
166        });
167    }
168
169    if distance == 0.0 {
170        // Degenerate case: return line as thin polygon
171        return create_degenerate_linestring_polygon(line);
172    }
173
174    let abs_distance = distance.abs();
175    let mut left_coords = Vec::new();
176    let mut right_coords = Vec::new();
177
178    // Generate parallel offset curves
179    for i in 0..(line.coords.len() - 1) {
180        let p1 = &line.coords[i];
181        let p2 = &line.coords[i + 1];
182
183        let (left, right) = offset_segment(p1, p2, abs_distance)?;
184
185        if i == 0 {
186            // Start cap
187            add_start_cap(&mut left_coords, p1, &left, abs_distance, options);
188        }
189
190        left_coords.push(left);
191
192        if i == line.coords.len() - 2 {
193            // Last segment
194            let (left2, right2) = offset_segment(p1, p2, abs_distance)?;
195            left_coords.push(left2);
196
197            // End cap
198            add_end_cap(&mut left_coords, p2, &left2, abs_distance, options);
199
200            // Add right side in reverse
201            right_coords.insert(0, right2);
202            right_coords.insert(0, right);
203        } else {
204            // Add join
205            let p3 = &line.coords[i + 2];
206            let (left3, _) = offset_segment(p2, p3, abs_distance)?;
207
208            add_join(&mut left_coords, &left, &left3, p2, abs_distance, options)?;
209
210            right_coords.insert(0, right);
211        }
212    }
213
214    // Combine left and right sides
215    left_coords.extend(right_coords);
216    left_coords.push(left_coords[0]); // Close ring
217
218    let exterior = LineString::new(left_coords).map_err(AlgorithmError::Core)?;
219    Polygon::new(exterior, vec![]).map_err(AlgorithmError::Core)
220}
221
222/// Generates a buffer around a polygon
223///
224/// For positive distances, expands the polygon. For negative distances,
225/// performs erosion (inward buffer).
226///
227/// # Arguments
228///
229/// * `polygon` - The polygon to buffer
230/// * `distance` - Buffer distance (positive expands, negative erodes)
231/// * `options` - Buffer options
232///
233/// # Errors
234///
235/// Returns error if polygon is invalid
236pub fn buffer_polygon(
237    polygon: &Polygon,
238    distance: f64,
239    options: &BufferOptions,
240) -> Result<Polygon> {
241    if !distance.is_finite() {
242        return Err(AlgorithmError::InvalidParameter {
243            parameter: "distance",
244            message: "distance must be finite".to_string(),
245        });
246    }
247
248    if distance == 0.0 {
249        // No change
250        return Ok(polygon.clone());
251    }
252
253    // For polygon buffering, we buffer the exterior ring outward
254    // and interior rings inward (to expand holes)
255    let exterior_buffer = buffer_ring(&polygon.exterior, distance, options, false)?;
256
257    // Handle interior rings (holes)
258    let mut interior_buffers = Vec::new();
259    for interior in &polygon.interiors {
260        // Invert distance for holes
261        let hole_buffer = buffer_ring(interior, -distance, options, true)?;
262        interior_buffers.push(hole_buffer);
263    }
264
265    Polygon::new(exterior_buffer, interior_buffers).map_err(AlgorithmError::Core)
266}
267
268// ============================================================================
269// Helper Functions
270// ============================================================================
271
272/// Creates a degenerate polygon from a single point
273fn create_degenerate_polygon(coord: &Coordinate) -> Result<Polygon> {
274    let coords = vec![*coord, *coord, *coord, *coord];
275    let exterior = LineString::new(coords).map_err(AlgorithmError::Core)?;
276    Polygon::new(exterior, vec![]).map_err(AlgorithmError::Core)
277}
278
279/// Creates a degenerate polygon from a linestring (collapsed)
280fn create_degenerate_linestring_polygon(line: &LineString) -> Result<Polygon> {
281    let mut coords = line.coords.clone();
282    coords.reverse();
283    coords.extend_from_slice(&line.coords);
284    coords.push(coords[0]);
285
286    let exterior = LineString::new(coords).map_err(AlgorithmError::Core)?;
287    Polygon::new(exterior, vec![]).map_err(AlgorithmError::Core)
288}
289
290/// Computes offset points for a line segment
291///
292/// Returns (left_offset, right_offset) perpendicular to the segment direction
293fn offset_segment(
294    p1: &Coordinate,
295    p2: &Coordinate,
296    distance: f64,
297) -> Result<(Coordinate, Coordinate)> {
298    let dx = p2.x - p1.x;
299    let dy = p2.y - p1.y;
300    let length = (dx * dx + dy * dy).sqrt();
301
302    if length < f64::EPSILON {
303        return Err(AlgorithmError::GeometryError {
304            message: "degenerate segment (zero length)".to_string(),
305        });
306    }
307
308    // Perpendicular vector (rotated 90 degrees)
309    let perp_x = -dy / length;
310    let perp_y = dx / length;
311
312    let left = Coordinate::new_2d(p1.x + perp_x * distance, p1.y + perp_y * distance);
313
314    let right = Coordinate::new_2d(p1.x - perp_x * distance, p1.y - perp_y * distance);
315
316    Ok((left, right))
317}
318
319/// Adds a start cap to the buffer
320fn add_start_cap(
321    coords: &mut Vec<Coordinate>,
322    point: &Coordinate,
323    offset: &Coordinate,
324    distance: f64,
325    options: &BufferOptions,
326) {
327    match options.cap_style {
328        BufferCapStyle::Round => {
329            add_round_cap(coords, point, offset, distance, options, true);
330        }
331        BufferCapStyle::Flat => {
332            coords.push(*offset);
333        }
334        BufferCapStyle::Square => {
335            // Extend by distance in direction perpendicular to offset
336            let dx = offset.x - point.x;
337            let dy = offset.y - point.y;
338            let len = (dx * dx + dy * dy).sqrt();
339            if len > f64::EPSILON {
340                let nx = -dy / len;
341                let ny = dx / len;
342                let extended =
343                    Coordinate::new_2d(offset.x + nx * distance, offset.y + ny * distance);
344                coords.push(extended);
345            }
346            coords.push(*offset);
347        }
348    }
349}
350
351/// Adds an end cap to the buffer
352fn add_end_cap(
353    coords: &mut Vec<Coordinate>,
354    point: &Coordinate,
355    offset: &Coordinate,
356    distance: f64,
357    options: &BufferOptions,
358) {
359    match options.cap_style {
360        BufferCapStyle::Round => {
361            add_round_cap(coords, point, offset, distance, options, false);
362        }
363        BufferCapStyle::Flat => {
364            coords.push(*offset);
365        }
366        BufferCapStyle::Square => {
367            let dx = offset.x - point.x;
368            let dy = offset.y - point.y;
369            let len = (dx * dx + dy * dy).sqrt();
370            if len > f64::EPSILON {
371                let nx = dy / len;
372                let ny = -dx / len;
373                let extended =
374                    Coordinate::new_2d(offset.x + nx * distance, offset.y + ny * distance);
375                coords.push(*offset);
376                coords.push(extended);
377            }
378        }
379    }
380}
381
382/// Adds a round cap (semi-circle)
383fn add_round_cap(
384    coords: &mut Vec<Coordinate>,
385    center: &Coordinate,
386    start_offset: &Coordinate,
387    radius: f64,
388    options: &BufferOptions,
389    is_start: bool,
390) {
391    let segments = options.quadrant_segments * 2; // Half circle
392    let start_angle = (start_offset.y - center.y).atan2(start_offset.x - center.x);
393
394    for i in 0..=segments {
395        let t = if is_start {
396            (i as f64) / (segments as f64)
397        } else {
398            (i as f64) / (segments as f64)
399        };
400        let angle = start_angle + t * PI * if is_start { 1.0 } else { -1.0 };
401        let x = center.x + radius * angle.cos();
402        let y = center.y + radius * angle.sin();
403        coords.push(Coordinate::new_2d(x, y));
404    }
405}
406
407/// Adds a join between two offset segments
408fn add_join(
409    coords: &mut Vec<Coordinate>,
410    offset1: &Coordinate,
411    offset2: &Coordinate,
412    vertex: &Coordinate,
413    distance: f64,
414    options: &BufferOptions,
415) -> Result<()> {
416    match options.join_style {
417        BufferJoinStyle::Round => {
418            add_round_join(coords, offset1, offset2, vertex, distance, options);
419        }
420        BufferJoinStyle::Miter => {
421            add_miter_join(coords, offset1, offset2, vertex, distance, options)?;
422        }
423        BufferJoinStyle::Bevel => {
424            coords.push(*offset1);
425            coords.push(*offset2);
426        }
427    }
428    Ok(())
429}
430
431/// Adds a round join (circular arc)
432fn add_round_join(
433    coords: &mut Vec<Coordinate>,
434    offset1: &Coordinate,
435    offset2: &Coordinate,
436    center: &Coordinate,
437    radius: f64,
438    options: &BufferOptions,
439) {
440    coords.push(*offset1);
441
442    let angle1 = (offset1.y - center.y).atan2(offset1.x - center.x);
443    let angle2 = (offset2.y - center.y).atan2(offset2.x - center.x);
444
445    let mut angle_diff = angle2 - angle1;
446    // Normalize to [-PI, PI]
447    while angle_diff > PI {
448        angle_diff -= 2.0 * PI;
449    }
450    while angle_diff < -PI {
451        angle_diff += 2.0 * PI;
452    }
453
454    let segments = ((angle_diff.abs() / (PI / 2.0)) * (options.quadrant_segments as f64)) as usize;
455
456    for i in 1..segments {
457        let t = (i as f64) / (segments as f64);
458        let angle = angle1 + t * angle_diff;
459        let x = center.x + radius * angle.cos();
460        let y = center.y + radius * angle.sin();
461        coords.push(Coordinate::new_2d(x, y));
462    }
463}
464
465/// Adds a miter join (sharp corner with limit)
466fn add_miter_join(
467    coords: &mut Vec<Coordinate>,
468    offset1: &Coordinate,
469    offset2: &Coordinate,
470    vertex: &Coordinate,
471    distance: f64,
472    options: &BufferOptions,
473) -> Result<()> {
474    coords.push(*offset1);
475
476    // Compute miter point (intersection of two offset lines)
477    // If miter is too sharp, fall back to bevel
478    let miter_result = compute_miter_point(offset1, offset2, vertex, distance, options.miter_limit);
479
480    if let Some(miter) = miter_result {
481        coords.push(miter);
482    }
483
484    coords.push(*offset2);
485    Ok(())
486}
487
488/// Computes the miter join point
489fn compute_miter_point(
490    offset1: &Coordinate,
491    offset2: &Coordinate,
492    _vertex: &Coordinate,
493    distance: f64,
494    miter_limit: f64,
495) -> Option<Coordinate> {
496    // Simple implementation: just check if offsets are too far apart
497    let dx = offset2.x - offset1.x;
498    let dy = offset2.y - offset1.y;
499    let miter_distance = (dx * dx + dy * dy).sqrt();
500
501    if miter_distance > distance * miter_limit {
502        // Too sharp, return None to use bevel instead
503        None
504    } else {
505        // Use midpoint as approximation
506        Some(Coordinate::new_2d(
507            (offset1.x + offset2.x) / 2.0,
508            (offset1.y + offset2.y) / 2.0,
509        ))
510    }
511}
512
513/// Buffers a ring (for polygon buffering)
514fn buffer_ring(
515    ring: &LineString,
516    distance: f64,
517    _options: &BufferOptions,
518    _is_hole: bool,
519) -> Result<LineString> {
520    if ring.coords.len() < 4 {
521        return Err(AlgorithmError::InsufficientData {
522            operation: "buffer_ring",
523            message: "ring must have at least 4 coordinates".to_string(),
524        });
525    }
526
527    let abs_distance = distance.abs();
528    let mut offset_coords = Vec::new();
529
530    // Process each edge of the ring
531    for i in 0..(ring.coords.len() - 1) {
532        let p1 = &ring.coords[i];
533        let p2 = &ring.coords[i + 1];
534
535        let (left, _right) = offset_segment(p1, p2, abs_distance)?;
536
537        if distance > 0.0 {
538            offset_coords.push(left);
539        } else {
540            // For negative buffer, use right side
541            let (_left, right) = offset_segment(p1, p2, abs_distance)?;
542            offset_coords.push(right);
543        }
544    }
545
546    // Close the ring
547    if let Some(first) = offset_coords.first() {
548        offset_coords.push(*first);
549    }
550
551    LineString::new(offset_coords).map_err(AlgorithmError::Core)
552}
553
554//
555// Pooled buffer operations for reduced allocations
556//
557
558/// Generates a circular buffer around a point using object pooling
559///
560/// This is the pooled version of `buffer_point` that reuses allocated
561/// polygons from a thread-local pool, reducing allocation overhead for
562/// batch operations.
563///
564/// # Arguments
565///
566/// * `center` - The center point
567/// * `radius` - Buffer radius (must be positive)
568/// * `options` - Buffer options controlling segment count and other parameters
569///
570/// # Returns
571///
572/// A `PoolGuard<Polygon>` that automatically returns the polygon to the pool
573/// when dropped. Use `.into_inner()` to take ownership without returning to pool.
574///
575/// # Errors
576///
577/// Returns error if radius is negative or non-finite
578///
579/// # Performance
580///
581/// For batch operations, this can reduce allocations by 2-3x compared to
582/// the non-pooled version.
583///
584/// # Example
585///
586/// ```
587/// use oxigdal_algorithms::vector::{buffer_point_pooled, Point, BufferOptions};
588///
589/// let point = Point::new(0.0, 0.0);
590/// let options = BufferOptions::default();
591/// let buffered = buffer_point_pooled(&point, 10.0, &options)?;
592/// // Use buffered polygon...
593/// // Automatically returned to pool when buffered drops
594/// # Ok::<(), oxigdal_algorithms::error::AlgorithmError>(())
595/// ```
596pub fn buffer_point_pooled(
597    center: &Point,
598    radius: f64,
599    options: &BufferOptions,
600) -> Result<PoolGuard<'static, Polygon>> {
601    if radius < 0.0 {
602        return Err(AlgorithmError::InvalidParameter {
603            parameter: "radius",
604            message: "radius must be non-negative".to_string(),
605        });
606    }
607
608    if !radius.is_finite() {
609        return Err(AlgorithmError::InvalidParameter {
610            parameter: "radius",
611            message: "radius must be finite".to_string(),
612        });
613    }
614
615    let mut poly = get_pooled_polygon();
616
617    if radius == 0.0 {
618        // Degenerate case: return point as tiny polygon
619        let degenerate = create_degenerate_polygon(&center.coord)?;
620        poly.exterior = degenerate.exterior;
621        poly.interiors = degenerate.interiors;
622        return Ok(poly);
623    }
624
625    let segments = options.quadrant_segments * 4;
626    poly.exterior.coords.clear();
627    poly.exterior.coords.reserve(segments + 1);
628
629    for i in 0..segments {
630        let angle = 2.0 * PI * (i as f64) / (segments as f64);
631        let x = center.coord.x + radius * angle.cos();
632        let y = center.coord.y + radius * angle.sin();
633        poly.exterior.coords.push(Coordinate::new_2d(x, y));
634    }
635
636    // Close the ring
637    if let Some(&first) = poly.exterior.coords.first() {
638        poly.exterior.coords.push(first);
639    }
640
641    Ok(poly)
642}
643
644/// Generates a buffer around a linestring using object pooling
645///
646/// This is the pooled version of `buffer_linestring` that reuses allocated
647/// polygons from a thread-local pool.
648///
649/// # Arguments
650///
651/// * `line` - The linestring to buffer
652/// * `distance` - Buffer distance (positive for expansion)
653/// * `options` - Buffer options
654///
655/// # Returns
656///
657/// A `PoolGuard<Polygon>` that automatically returns the polygon to the pool
658/// when dropped.
659///
660/// # Errors
661///
662/// Returns error if linestring is invalid or has insufficient points
663///
664/// # Example
665///
666/// ```
667/// use oxigdal_algorithms::vector::{buffer_linestring_pooled, LineString, Coordinate, BufferOptions};
668///
669/// let coords = vec![Coordinate::new_2d(0.0, 0.0), Coordinate::new_2d(10.0, 0.0)];
670/// let line = LineString::new(coords)?;
671/// let options = BufferOptions::default();
672/// let buffered = buffer_linestring_pooled(&line, 5.0, &options)?;
673/// # Ok::<(), oxigdal_algorithms::error::AlgorithmError>(())
674/// ```
675pub fn buffer_linestring_pooled(
676    line: &LineString,
677    distance: f64,
678    options: &BufferOptions,
679) -> Result<PoolGuard<'static, Polygon>> {
680    // Compute the buffer using the non-pooled version
681    let result = buffer_linestring(line, distance, options)?;
682
683    // Get a pooled polygon and copy the result into it
684    let mut poly = get_pooled_polygon();
685    poly.exterior = result.exterior;
686    poly.interiors = result.interiors;
687
688    Ok(poly)
689}
690
691/// Generates a buffer around a polygon using object pooling
692///
693/// This is the pooled version of `buffer_polygon` that reuses allocated
694/// polygons from a thread-local pool.
695///
696/// # Arguments
697///
698/// * `polygon` - The polygon to buffer
699/// * `distance` - Buffer distance (positive for expansion, negative for erosion)
700/// * `options` - Buffer options
701///
702/// # Returns
703///
704/// A `PoolGuard<Polygon>` that automatically returns the polygon to the pool
705/// when dropped.
706///
707/// # Errors
708///
709/// Returns error if polygon is invalid
710///
711/// # Example
712///
713/// ```
714/// use oxigdal_algorithms::vector::{buffer_polygon_pooled, Polygon, LineString, Coordinate, BufferOptions};
715///
716/// let exterior = LineString::new(vec![
717///     Coordinate::new_2d(0.0, 0.0),
718///     Coordinate::new_2d(10.0, 0.0),
719///     Coordinate::new_2d(10.0, 10.0),
720///     Coordinate::new_2d(0.0, 10.0),
721///     Coordinate::new_2d(0.0, 0.0),
722/// ])?;
723/// let polygon = Polygon::new(exterior, vec![])?;
724/// let options = BufferOptions::default();
725/// let buffered = buffer_polygon_pooled(&polygon, 2.0, &options)?;
726/// # Ok::<(), oxigdal_algorithms::error::AlgorithmError>(())
727/// ```
728pub fn buffer_polygon_pooled(
729    polygon: &Polygon,
730    distance: f64,
731    options: &BufferOptions,
732) -> Result<PoolGuard<'static, Polygon>> {
733    // Compute the buffer using the non-pooled version
734    let result = buffer_polygon(polygon, distance, options)?;
735
736    // Get a pooled polygon and copy the result into it
737    let mut poly = get_pooled_polygon();
738    poly.exterior = result.exterior;
739    poly.interiors = result.interiors;
740
741    Ok(poly)
742}
743
744#[cfg(test)]
745mod tests {
746    use super::*;
747    use approx::assert_relative_eq;
748
749    #[test]
750    fn test_buffer_point_basic() {
751        let point = Point::new(0.0, 0.0);
752        let options = BufferOptions::default();
753        let result = buffer_point(&point, 10.0, &options);
754        assert!(result.is_ok());
755
756        let polygon = result.ok();
757        assert!(polygon.is_some());
758        if let Some(poly) = polygon {
759            // Check that all points are approximately at distance 10 from center
760            for coord in &poly.exterior.coords {
761                let dist = (coord.x * coord.x + coord.y * coord.y).sqrt();
762                assert_relative_eq!(dist, 10.0, epsilon = 1e-10);
763            }
764        }
765    }
766
767    #[test]
768    fn test_buffer_point_zero_radius() {
769        let point = Point::new(5.0, 5.0);
770        let options = BufferOptions::default();
771        let result = buffer_point(&point, 0.0, &options);
772        assert!(result.is_ok());
773    }
774
775    #[test]
776    fn test_buffer_point_negative_radius() {
777        let point = Point::new(0.0, 0.0);
778        let options = BufferOptions::default();
779        let result = buffer_point(&point, -10.0, &options);
780        assert!(result.is_err());
781    }
782
783    #[test]
784    fn test_buffer_linestring_basic() {
785        let coords = vec![Coordinate::new_2d(0.0, 0.0), Coordinate::new_2d(10.0, 0.0)];
786        let line = LineString::new(coords);
787        assert!(line.is_ok());
788
789        if let Ok(ls) = line {
790            let options = BufferOptions::default();
791            let result = buffer_linestring(&ls, 5.0, &options);
792            assert!(result.is_ok());
793
794            if let Ok(poly) = result {
795                // Buffer should create a polygon
796                assert!(poly.exterior.coords.len() > 4);
797            }
798        }
799    }
800
801    #[test]
802    fn test_buffer_linestring_empty() {
803        let coords = vec![Coordinate::new_2d(0.0, 0.0)];
804        let line = LineString::new(coords);
805        assert!(line.is_err()); // Should fail in LineString::new
806    }
807
808    #[test]
809    fn test_buffer_polygon_basic() {
810        let exterior_coords = vec![
811            Coordinate::new_2d(0.0, 0.0),
812            Coordinate::new_2d(10.0, 0.0),
813            Coordinate::new_2d(10.0, 10.0),
814            Coordinate::new_2d(0.0, 10.0),
815            Coordinate::new_2d(0.0, 0.0),
816        ];
817        let exterior = LineString::new(exterior_coords);
818        assert!(exterior.is_ok());
819
820        if let Ok(ext) = exterior {
821            let polygon = Polygon::new(ext, vec![]);
822            assert!(polygon.is_ok());
823
824            if let Ok(poly) = polygon {
825                let options = BufferOptions::default();
826                let result = buffer_polygon(&poly, 2.0, &options);
827                assert!(result.is_ok());
828            }
829        }
830    }
831
832    #[test]
833    fn test_offset_segment() {
834        let p1 = Coordinate::new_2d(0.0, 0.0);
835        let p2 = Coordinate::new_2d(10.0, 0.0);
836        let result = offset_segment(&p1, &p2, 5.0);
837
838        assert!(result.is_ok());
839        if let Ok((left, right)) = result {
840            assert_relative_eq!(left.x, 0.0, epsilon = 1e-10);
841            assert_relative_eq!(left.y, 5.0, epsilon = 1e-10);
842            assert_relative_eq!(right.x, 0.0, epsilon = 1e-10);
843            assert_relative_eq!(right.y, -5.0, epsilon = 1e-10);
844        }
845    }
846
847    #[test]
848    fn test_buffer_cap_styles() {
849        let coords = vec![Coordinate::new_2d(0.0, 0.0), Coordinate::new_2d(10.0, 0.0)];
850        let line = LineString::new(coords);
851        assert!(line.is_ok());
852
853        if let Ok(ls) = line {
854            // Test round caps
855            let mut options = BufferOptions::default();
856            options.cap_style = BufferCapStyle::Round;
857            let result = buffer_linestring(&ls, 5.0, &options);
858            assert!(result.is_ok());
859
860            // Test flat caps
861            options.cap_style = BufferCapStyle::Flat;
862            let result = buffer_linestring(&ls, 5.0, &options);
863            assert!(result.is_ok());
864
865            // Test square caps
866            options.cap_style = BufferCapStyle::Square;
867            let result = buffer_linestring(&ls, 5.0, &options);
868            assert!(result.is_ok());
869        }
870    }
871
872    #[test]
873    fn test_buffer_join_styles() {
874        let coords = vec![
875            Coordinate::new_2d(0.0, 0.0),
876            Coordinate::new_2d(10.0, 0.0),
877            Coordinate::new_2d(10.0, 10.0),
878        ];
879        let line = LineString::new(coords);
880        assert!(line.is_ok());
881
882        if let Ok(ls) = line {
883            // Test round joins
884            let mut options = BufferOptions::default();
885            options.join_style = BufferJoinStyle::Round;
886            let result = buffer_linestring(&ls, 5.0, &options);
887            assert!(result.is_ok());
888
889            // Test miter joins
890            options.join_style = BufferJoinStyle::Miter;
891            let result = buffer_linestring(&ls, 5.0, &options);
892            assert!(result.is_ok());
893
894            // Test bevel joins
895            options.join_style = BufferJoinStyle::Bevel;
896            let result = buffer_linestring(&ls, 5.0, &options);
897            assert!(result.is_ok());
898        }
899    }
900}