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::offset::{JoinStyle, OffsetOptions, offset_polygon_rings};
30use crate::vector::pool::{PoolGuard, get_pooled_polygon};
31use oxigdal_core::vector::{Coordinate, LineString, Point, Polygon};
32
33#[cfg(not(feature = "std"))]
34use core::f64::consts::PI;
35#[cfg(feature = "std")]
36use std::f64::consts::PI;
37
38/// End cap style for line buffers
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
40pub enum BufferCapStyle {
41    /// Round caps (semi-circles at endpoints)
42    #[default]
43    Round,
44    /// Flat caps (perpendicular to line direction)
45    Flat,
46    /// Square caps (extended by buffer distance)
47    Square,
48}
49
50/// Join style for line buffers at vertices
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
52pub enum BufferJoinStyle {
53    /// Round joins (circular arcs)
54    #[default]
55    Round,
56    /// Miter joins (sharp points, with miter limit)
57    Miter,
58    /// Bevel joins (cut off at buffer distance)
59    Bevel,
60}
61
62/// Options for buffer operations
63#[derive(Debug, Clone)]
64pub struct BufferOptions {
65    /// Number of segments per quadrant for round caps/joins
66    pub quadrant_segments: usize,
67    /// Cap style for line endpoints
68    pub cap_style: BufferCapStyle,
69    /// Join style for line vertices
70    pub join_style: BufferJoinStyle,
71    /// Miter limit (ratio) for miter joins
72    pub miter_limit: f64,
73    /// Simplification tolerance (0.0 = no simplification)
74    pub simplify_tolerance: f64,
75}
76
77impl Default for BufferOptions {
78    fn default() -> Self {
79        Self {
80            quadrant_segments: 8,
81            cap_style: BufferCapStyle::Round,
82            join_style: BufferJoinStyle::Round,
83            miter_limit: 5.0,
84            simplify_tolerance: 0.0,
85        }
86    }
87}
88
89/// Generates a circular buffer around a point
90///
91/// # Arguments
92///
93/// * `center` - The center point
94/// * `radius` - Buffer radius (must be positive)
95/// * `options` - Buffer options controlling segment count and other parameters
96///
97/// # Errors
98///
99/// Returns error if radius is negative or non-finite
100pub fn buffer_point(center: &Point, radius: f64, options: &BufferOptions) -> Result<Polygon> {
101    if radius < 0.0 {
102        return Err(AlgorithmError::InvalidParameter {
103            parameter: "radius",
104            message: "radius must be non-negative".to_string(),
105        });
106    }
107
108    if !radius.is_finite() {
109        return Err(AlgorithmError::InvalidParameter {
110            parameter: "radius",
111            message: "radius must be finite".to_string(),
112        });
113    }
114
115    if radius == 0.0 {
116        // Degenerate case: return point as tiny polygon
117        return create_degenerate_polygon(&center.coord);
118    }
119
120    let segments = options.quadrant_segments * 4;
121    let mut coords = Vec::with_capacity(segments + 1);
122
123    for i in 0..segments {
124        let angle = 2.0 * PI * (i as f64) / (segments as f64);
125        let x = center.coord.x + radius * angle.cos();
126        let y = center.coord.y + radius * angle.sin();
127        coords.push(Coordinate::new_2d(x, y));
128    }
129
130    // Close the ring
131    coords.push(coords[0]);
132
133    let exterior = LineString::new(coords).map_err(AlgorithmError::Core)?;
134    Polygon::new(exterior, vec![]).map_err(AlgorithmError::Core)
135}
136
137/// Generates a buffer around a linestring
138///
139/// Creates a polygon buffer around a linestring using parallel offset curves
140/// on both sides, with configurable cap and join styles.
141///
142/// # Arguments
143///
144/// * `line` - The linestring to buffer
145/// * `distance` - Buffer distance (positive for expansion, negative for contraction)
146/// * `options` - Buffer options
147///
148/// # Errors
149///
150/// Returns error if linestring is invalid or has insufficient points
151pub fn buffer_linestring(
152    line: &LineString,
153    distance: f64,
154    options: &BufferOptions,
155) -> Result<Polygon> {
156    if line.coords.len() < 2 {
157        return Err(AlgorithmError::InsufficientData {
158            operation: "buffer_linestring",
159            message: "linestring must have at least 2 coordinates".to_string(),
160        });
161    }
162
163    if !distance.is_finite() {
164        return Err(AlgorithmError::InvalidParameter {
165            parameter: "distance",
166            message: "distance must be finite".to_string(),
167        });
168    }
169
170    if distance == 0.0 {
171        // Degenerate case: return line as thin polygon
172        return create_degenerate_linestring_polygon(line);
173    }
174
175    let abs_distance = distance.abs();
176    let mut left_coords = Vec::new();
177    let mut right_coords = Vec::new();
178
179    // Generate parallel offset curves
180    for i in 0..(line.coords.len() - 1) {
181        let p1 = &line.coords[i];
182        let p2 = &line.coords[i + 1];
183
184        let (left, right) = offset_segment(p1, p2, abs_distance)?;
185
186        if i == 0 {
187            // Start cap
188            add_start_cap(&mut left_coords, p1, &left, abs_distance, options);
189        }
190
191        left_coords.push(left);
192
193        if i == line.coords.len() - 2 {
194            // Last segment
195            let (left2, right2) = offset_segment(p1, p2, abs_distance)?;
196            left_coords.push(left2);
197
198            // End cap
199            add_end_cap(&mut left_coords, p2, &left2, abs_distance, options);
200
201            // Add right side in reverse
202            right_coords.insert(0, right2);
203            right_coords.insert(0, right);
204        } else {
205            // Add join. `left` is the offset of the segment start point `p1`;
206            // for a correct corner join we need the offset of the *vertex* `p2`
207            // along this segment's normal. That is `p2 + (left - p1)`, since
208            // `(left - p1)` is exactly the perpendicular offset vector of the
209            // segment `(p1, p2)`. `left3` is already the vertex offset of `p2`
210            // along the next segment's normal.
211            let p3 = &line.coords[i + 2];
212            let (left3, _) = offset_segment(p2, p3, abs_distance)?;
213            let off1_at_vertex = Coordinate::new_2d(p2.x + (left.x - p1.x), p2.y + (left.y - p1.y));
214
215            add_join(
216                &mut left_coords,
217                &off1_at_vertex,
218                &left3,
219                p2,
220                abs_distance,
221                options,
222            )?;
223
224            right_coords.insert(0, right);
225        }
226    }
227
228    // Combine left and right sides
229    left_coords.extend(right_coords);
230    left_coords.push(left_coords[0]); // Close ring
231
232    let exterior = LineString::new(left_coords).map_err(AlgorithmError::Core)?;
233    Polygon::new(exterior, vec![]).map_err(AlgorithmError::Core)
234}
235
236/// Generates a buffer around a polygon
237///
238/// For positive distances, expands the polygon. For negative distances,
239/// performs erosion (inward buffer).
240///
241/// # Arguments
242///
243/// * `polygon` - The polygon to buffer
244/// * `distance` - Buffer distance (positive expands, negative erodes)
245/// * `options` - Buffer options
246///
247/// # Errors
248///
249/// Returns error if polygon is invalid
250pub fn buffer_polygon(
251    polygon: &Polygon,
252    distance: f64,
253    options: &BufferOptions,
254) -> Result<Polygon> {
255    if !distance.is_finite() {
256        return Err(AlgorithmError::InvalidParameter {
257            parameter: "distance",
258            message: "distance must be finite".to_string(),
259        });
260    }
261
262    if distance == 0.0 {
263        // No change
264        return Ok(polygon.clone());
265    }
266
267    // For polygon buffering, we buffer the exterior ring outward
268    // and interior rings inward (to expand holes)
269    let exterior_buffer = buffer_ring(&polygon.exterior, distance, options, false)?;
270
271    // Handle interior rings (holes)
272    let mut interior_buffers = Vec::new();
273    for interior in &polygon.interiors {
274        // Invert distance for holes
275        let hole_buffer = buffer_ring(interior, -distance, options, true)?;
276        interior_buffers.push(hole_buffer);
277    }
278
279    Polygon::new(exterior_buffer, interior_buffers).map_err(AlgorithmError::Core)
280}
281
282// ============================================================================
283// Helper Functions
284// ============================================================================
285
286/// Creates a degenerate polygon from a single point
287fn create_degenerate_polygon(coord: &Coordinate) -> Result<Polygon> {
288    let coords = vec![*coord, *coord, *coord, *coord];
289    let exterior = LineString::new(coords).map_err(AlgorithmError::Core)?;
290    Polygon::new(exterior, vec![]).map_err(AlgorithmError::Core)
291}
292
293/// Creates a degenerate polygon from a linestring (collapsed)
294fn create_degenerate_linestring_polygon(line: &LineString) -> Result<Polygon> {
295    let mut coords = line.coords.clone();
296    coords.reverse();
297    coords.extend_from_slice(&line.coords);
298    coords.push(coords[0]);
299
300    let exterior = LineString::new(coords).map_err(AlgorithmError::Core)?;
301    Polygon::new(exterior, vec![]).map_err(AlgorithmError::Core)
302}
303
304/// Computes offset points for a line segment
305///
306/// Returns (left_offset, right_offset) perpendicular to the segment direction
307fn offset_segment(
308    p1: &Coordinate,
309    p2: &Coordinate,
310    distance: f64,
311) -> Result<(Coordinate, Coordinate)> {
312    let dx = p2.x - p1.x;
313    let dy = p2.y - p1.y;
314    let length = (dx * dx + dy * dy).sqrt();
315
316    if length < f64::EPSILON {
317        return Err(AlgorithmError::GeometryError {
318            message: "degenerate segment (zero length)".to_string(),
319        });
320    }
321
322    // Perpendicular vector (rotated 90 degrees)
323    let perp_x = -dy / length;
324    let perp_y = dx / length;
325
326    let left = Coordinate::new_2d(p1.x + perp_x * distance, p1.y + perp_y * distance);
327
328    let right = Coordinate::new_2d(p1.x - perp_x * distance, p1.y - perp_y * distance);
329
330    Ok((left, right))
331}
332
333/// Adds a start cap to the buffer
334fn add_start_cap(
335    coords: &mut Vec<Coordinate>,
336    point: &Coordinate,
337    offset: &Coordinate,
338    distance: f64,
339    options: &BufferOptions,
340) {
341    match options.cap_style {
342        BufferCapStyle::Round => {
343            add_round_cap(coords, point, offset, distance, options, true);
344        }
345        BufferCapStyle::Flat => {
346            coords.push(*offset);
347        }
348        BufferCapStyle::Square => {
349            // Extend by distance in direction perpendicular to offset
350            let dx = offset.x - point.x;
351            let dy = offset.y - point.y;
352            let len = (dx * dx + dy * dy).sqrt();
353            if len > f64::EPSILON {
354                let nx = -dy / len;
355                let ny = dx / len;
356                let extended =
357                    Coordinate::new_2d(offset.x + nx * distance, offset.y + ny * distance);
358                coords.push(extended);
359            }
360            coords.push(*offset);
361        }
362    }
363}
364
365/// Adds an end cap to the buffer
366fn add_end_cap(
367    coords: &mut Vec<Coordinate>,
368    point: &Coordinate,
369    offset: &Coordinate,
370    distance: f64,
371    options: &BufferOptions,
372) {
373    match options.cap_style {
374        BufferCapStyle::Round => {
375            add_round_cap(coords, point, offset, distance, options, false);
376        }
377        BufferCapStyle::Flat => {
378            coords.push(*offset);
379        }
380        BufferCapStyle::Square => {
381            let dx = offset.x - point.x;
382            let dy = offset.y - point.y;
383            let len = (dx * dx + dy * dy).sqrt();
384            if len > f64::EPSILON {
385                let nx = dy / len;
386                let ny = -dx / len;
387                let extended =
388                    Coordinate::new_2d(offset.x + nx * distance, offset.y + ny * distance);
389                coords.push(*offset);
390                coords.push(extended);
391            }
392        }
393    }
394}
395
396/// Adds a round cap (semi-circle)
397fn add_round_cap(
398    coords: &mut Vec<Coordinate>,
399    center: &Coordinate,
400    start_offset: &Coordinate,
401    radius: f64,
402    options: &BufferOptions,
403    is_start: bool,
404) {
405    let segments = options.quadrant_segments * 2; // Half circle
406    let start_angle = (start_offset.y - center.y).atan2(start_offset.x - center.x);
407
408    for i in 0..=segments {
409        let t = if is_start {
410            (i as f64) / (segments as f64)
411        } else {
412            (i as f64) / (segments as f64)
413        };
414        let angle = start_angle + t * PI * if is_start { 1.0 } else { -1.0 };
415        let x = center.x + radius * angle.cos();
416        let y = center.y + radius * angle.sin();
417        coords.push(Coordinate::new_2d(x, y));
418    }
419}
420
421/// Adds a join between two offset segments
422fn add_join(
423    coords: &mut Vec<Coordinate>,
424    offset1: &Coordinate,
425    offset2: &Coordinate,
426    vertex: &Coordinate,
427    distance: f64,
428    options: &BufferOptions,
429) -> Result<()> {
430    match options.join_style {
431        BufferJoinStyle::Round => {
432            add_round_join(coords, offset1, offset2, vertex, distance, options);
433        }
434        BufferJoinStyle::Miter => {
435            add_miter_join(coords, offset1, offset2, vertex, distance, options)?;
436        }
437        BufferJoinStyle::Bevel => {
438            coords.push(*offset1);
439            coords.push(*offset2);
440        }
441    }
442    Ok(())
443}
444
445/// Adds a round join (circular arc)
446fn add_round_join(
447    coords: &mut Vec<Coordinate>,
448    offset1: &Coordinate,
449    offset2: &Coordinate,
450    center: &Coordinate,
451    radius: f64,
452    options: &BufferOptions,
453) {
454    coords.push(*offset1);
455
456    let angle1 = (offset1.y - center.y).atan2(offset1.x - center.x);
457    let angle2 = (offset2.y - center.y).atan2(offset2.x - center.x);
458
459    let mut angle_diff = angle2 - angle1;
460    // Normalize to [-PI, PI]
461    while angle_diff > PI {
462        angle_diff -= 2.0 * PI;
463    }
464    while angle_diff < -PI {
465        angle_diff += 2.0 * PI;
466    }
467
468    let segments = ((angle_diff.abs() / (PI / 2.0)) * (options.quadrant_segments as f64)) as usize;
469
470    for i in 1..segments {
471        let t = (i as f64) / (segments as f64);
472        let angle = angle1 + t * angle_diff;
473        let x = center.x + radius * angle.cos();
474        let y = center.y + radius * angle.sin();
475        coords.push(Coordinate::new_2d(x, y));
476    }
477}
478
479/// Adds a miter join (sharp corner with limit)
480fn add_miter_join(
481    coords: &mut Vec<Coordinate>,
482    offset1: &Coordinate,
483    offset2: &Coordinate,
484    vertex: &Coordinate,
485    distance: f64,
486    options: &BufferOptions,
487) -> Result<()> {
488    coords.push(*offset1);
489
490    // Compute miter point (intersection of two offset lines)
491    // If miter is too sharp, fall back to bevel
492    let miter_result = compute_miter_point(offset1, offset2, vertex, distance, options.miter_limit);
493
494    if let Some(miter) = miter_result {
495        coords.push(miter);
496    }
497
498    coords.push(*offset2);
499    Ok(())
500}
501
502/// Computes the true miter join point.
503///
504/// The miter point is the intersection of the two offset lines, which lies on
505/// the corner bisector at distance `distance / cos(θ/2)` from the vertex, where
506/// θ is the turn angle. It is generally *beyond* both offset points (farther
507/// from the vertex), so that the sharp outer corner is fully covered — this is
508/// what distinguishes a miter join from a bevel join.
509///
510/// `offset1` and `offset2` must be the offset points *at the vertex* (each at
511/// distance `distance` from `vertex`), so that `(offset - vertex) / distance`
512/// recovers the corresponding segment's unit normal. This mirrors the correct
513/// `emit_miter` implementation in [`crate::vector::offset`].
514///
515/// Returns `None` (caller falls back to bevel) when the corner is (near)
516/// straight/anti-parallel, or when the miter extension ratio exceeds
517/// `miter_limit`.
518fn compute_miter_point(
519    offset1: &Coordinate,
520    offset2: &Coordinate,
521    vertex: &Coordinate,
522    distance: f64,
523    miter_limit: f64,
524) -> Option<Coordinate> {
525    if distance.abs() < f64::EPSILON {
526        return None;
527    }
528
529    // Arm vectors from the vertex to each offset point (length ≈ `distance`).
530    let ax = offset1.x - vertex.x;
531    let ay = offset1.y - vertex.y;
532    let bx = offset2.x - vertex.x;
533    let by = offset2.y - vertex.y;
534
535    // Bisector direction = normalize(na + nb) where na = a/distance, nb = b/distance.
536    let sx = ax + bx;
537    let sy = ay + by;
538    let blen = (sx * sx + sy * sy).sqrt();
539
540    // Collinear / anti-parallel normals → no distinct miter point; use bevel.
541    if blen < f64::EPSILON {
542        return None;
543    }
544
545    let bux = sx / blen;
546    let buy = sy / blen;
547
548    // cos(θ/2) = bisector_unit · na, where na = a / distance.
549    let cos_half = (bux * ax + buy * ay) / distance;
550    if cos_half.abs() < f64::EPSILON {
551        // Nearly anti-parallel → miter length blows up; fall back to bevel.
552        return None;
553    }
554
555    // Signed miter length along the bisector.
556    let miter_length = distance / cos_half;
557    let ratio = (miter_length / distance).abs();
558
559    if ratio > miter_limit.abs() || !miter_length.is_finite() {
560        // Too sharp: fall back to bevel.
561        None
562    } else {
563        Some(Coordinate::new_2d(
564            vertex.x + miter_length * bux,
565            vertex.y + miter_length * buy,
566        ))
567    }
568}
569
570/// Buffers a closed ring (for polygon buffering).
571///
572/// This offsets the ring outward for a positive `distance` and inward for a
573/// negative `distance`, applying the configured [`BufferJoinStyle`] at every
574/// vertex. Orientation (CW/CCW) is detected internally via the shoelace
575/// formula so a positive `distance` always expands an exterior (CCW) ring
576/// *outward* regardless of the input winding. The heavy lifting is delegated to
577/// the correct closed-ring offset machinery in [`crate::vector::offset`], which
578/// inserts proper miter/bevel/round join geometry at each corner rather than
579/// dropping the corners entirely.
580///
581/// `is_hole` is retained for API symmetry; the outward/inward direction for
582/// holes is already encoded by the caller negating `distance`.
583fn buffer_ring(
584    ring: &LineString,
585    distance: f64,
586    options: &BufferOptions,
587    _is_hole: bool,
588) -> Result<LineString> {
589    if ring.coords.len() < 4 {
590        return Err(AlgorithmError::InsufficientData {
591            operation: "buffer_ring",
592            message: "ring must have at least 4 coordinates".to_string(),
593        });
594    }
595
596    // Convert the ring to the tuple representation used by the offset engine.
597    let ring_tuples: Vec<(f64, f64)> = ring.coords.iter().map(|c| (c.x, c.y)).collect();
598
599    // Translate the buffer options to offset options so the requested join
600    // style (and miter limit / simplification) is honoured for polygons.
601    let offset_options = OffsetOptions {
602        miter_limit: options.miter_limit,
603        join_style: match options.join_style {
604            BufferJoinStyle::Round => JoinStyle::Round,
605            BufferJoinStyle::Miter => JoinStyle::Miter,
606            BufferJoinStyle::Bevel => JoinStyle::Bevel,
607        },
608        simplify_tolerance: if options.simplify_tolerance > 0.0 {
609            Some(options.simplify_tolerance)
610        } else {
611            None
612        },
613    };
614
615    let mut offset_rings = offset_polygon_rings(&[ring_tuples], distance, &offset_options)?;
616
617    let out_ring = offset_rings
618        .pop()
619        .ok_or_else(|| AlgorithmError::GeometryError {
620            message: "offset_polygon_rings returned no ring".to_string(),
621        })?;
622
623    let offset_coords: Vec<Coordinate> = out_ring
624        .iter()
625        .map(|&(x, y)| Coordinate::new_2d(x, y))
626        .collect();
627
628    LineString::new(offset_coords).map_err(AlgorithmError::Core)
629}
630
631//
632// Pooled buffer operations for reduced allocations
633//
634
635/// Generates a circular buffer around a point using object pooling
636///
637/// This is the pooled version of `buffer_point` that reuses allocated
638/// polygons from a thread-local pool, reducing allocation overhead for
639/// batch operations.
640///
641/// # Arguments
642///
643/// * `center` - The center point
644/// * `radius` - Buffer radius (must be positive)
645/// * `options` - Buffer options controlling segment count and other parameters
646///
647/// # Returns
648///
649/// A `PoolGuard<Polygon>` that automatically returns the polygon to the pool
650/// when dropped. Use `.into_inner()` to take ownership without returning to pool.
651///
652/// # Errors
653///
654/// Returns error if radius is negative or non-finite
655///
656/// # Performance
657///
658/// For batch operations, this can reduce allocations by 2-3x compared to
659/// the non-pooled version.
660///
661/// # Example
662///
663/// ```
664/// use oxigdal_algorithms::vector::{buffer_point_pooled, Point, BufferOptions};
665///
666/// let point = Point::new(0.0, 0.0);
667/// let options = BufferOptions::default();
668/// let buffered = buffer_point_pooled(&point, 10.0, &options)?;
669/// // Use buffered polygon...
670/// // Automatically returned to pool when buffered drops
671/// # Ok::<(), oxigdal_algorithms::error::AlgorithmError>(())
672/// ```
673pub fn buffer_point_pooled(
674    center: &Point,
675    radius: f64,
676    options: &BufferOptions,
677) -> Result<PoolGuard<'static, Polygon>> {
678    if radius < 0.0 {
679        return Err(AlgorithmError::InvalidParameter {
680            parameter: "radius",
681            message: "radius must be non-negative".to_string(),
682        });
683    }
684
685    if !radius.is_finite() {
686        return Err(AlgorithmError::InvalidParameter {
687            parameter: "radius",
688            message: "radius must be finite".to_string(),
689        });
690    }
691
692    let mut poly = get_pooled_polygon();
693
694    if radius == 0.0 {
695        // Degenerate case: return point as tiny polygon
696        let degenerate = create_degenerate_polygon(&center.coord)?;
697        poly.exterior = degenerate.exterior;
698        poly.interiors = degenerate.interiors;
699        return Ok(poly);
700    }
701
702    let segments = options.quadrant_segments * 4;
703    poly.exterior.coords.clear();
704    poly.exterior.coords.reserve(segments + 1);
705
706    for i in 0..segments {
707        let angle = 2.0 * PI * (i as f64) / (segments as f64);
708        let x = center.coord.x + radius * angle.cos();
709        let y = center.coord.y + radius * angle.sin();
710        poly.exterior.coords.push(Coordinate::new_2d(x, y));
711    }
712
713    // Close the ring
714    if let Some(&first) = poly.exterior.coords.first() {
715        poly.exterior.coords.push(first);
716    }
717
718    Ok(poly)
719}
720
721/// Generates a buffer around a linestring using object pooling
722///
723/// This is the pooled version of `buffer_linestring` that reuses allocated
724/// polygons from a thread-local pool.
725///
726/// # Arguments
727///
728/// * `line` - The linestring to buffer
729/// * `distance` - Buffer distance (positive for expansion)
730/// * `options` - Buffer options
731///
732/// # Returns
733///
734/// A `PoolGuard<Polygon>` that automatically returns the polygon to the pool
735/// when dropped.
736///
737/// # Errors
738///
739/// Returns error if linestring is invalid or has insufficient points
740///
741/// # Example
742///
743/// ```
744/// use oxigdal_algorithms::vector::{buffer_linestring_pooled, LineString, Coordinate, BufferOptions};
745///
746/// let coords = vec![Coordinate::new_2d(0.0, 0.0), Coordinate::new_2d(10.0, 0.0)];
747/// let line = LineString::new(coords)?;
748/// let options = BufferOptions::default();
749/// let buffered = buffer_linestring_pooled(&line, 5.0, &options)?;
750/// # Ok::<(), oxigdal_algorithms::error::AlgorithmError>(())
751/// ```
752pub fn buffer_linestring_pooled(
753    line: &LineString,
754    distance: f64,
755    options: &BufferOptions,
756) -> Result<PoolGuard<'static, Polygon>> {
757    // Compute the buffer using the non-pooled version
758    let result = buffer_linestring(line, distance, options)?;
759
760    // Get a pooled polygon and copy the result into it
761    let mut poly = get_pooled_polygon();
762    poly.exterior = result.exterior;
763    poly.interiors = result.interiors;
764
765    Ok(poly)
766}
767
768/// Generates a buffer around a polygon using object pooling
769///
770/// This is the pooled version of `buffer_polygon` that reuses allocated
771/// polygons from a thread-local pool.
772///
773/// # Arguments
774///
775/// * `polygon` - The polygon to buffer
776/// * `distance` - Buffer distance (positive for expansion, negative for erosion)
777/// * `options` - Buffer options
778///
779/// # Returns
780///
781/// A `PoolGuard<Polygon>` that automatically returns the polygon to the pool
782/// when dropped.
783///
784/// # Errors
785///
786/// Returns error if polygon is invalid
787///
788/// # Example
789///
790/// ```
791/// use oxigdal_algorithms::vector::{buffer_polygon_pooled, Polygon, LineString, Coordinate, BufferOptions};
792///
793/// let exterior = LineString::new(vec![
794///     Coordinate::new_2d(0.0, 0.0),
795///     Coordinate::new_2d(10.0, 0.0),
796///     Coordinate::new_2d(10.0, 10.0),
797///     Coordinate::new_2d(0.0, 10.0),
798///     Coordinate::new_2d(0.0, 0.0),
799/// ])?;
800/// let polygon = Polygon::new(exterior, vec![])?;
801/// let options = BufferOptions::default();
802/// let buffered = buffer_polygon_pooled(&polygon, 2.0, &options)?;
803/// # Ok::<(), oxigdal_algorithms::error::AlgorithmError>(())
804/// ```
805pub fn buffer_polygon_pooled(
806    polygon: &Polygon,
807    distance: f64,
808    options: &BufferOptions,
809) -> Result<PoolGuard<'static, Polygon>> {
810    // Compute the buffer using the non-pooled version
811    let result = buffer_polygon(polygon, distance, options)?;
812
813    // Get a pooled polygon and copy the result into it
814    let mut poly = get_pooled_polygon();
815    poly.exterior = result.exterior;
816    poly.interiors = result.interiors;
817
818    Ok(poly)
819}
820
821#[cfg(test)]
822mod tests {
823    use super::*;
824    use approx::assert_relative_eq;
825
826    #[test]
827    fn test_buffer_point_basic() {
828        let point = Point::new(0.0, 0.0);
829        let options = BufferOptions::default();
830        let result = buffer_point(&point, 10.0, &options);
831        assert!(result.is_ok());
832
833        let polygon = result.ok();
834        assert!(polygon.is_some());
835        if let Some(poly) = polygon {
836            // Check that all points are approximately at distance 10 from center
837            for coord in &poly.exterior.coords {
838                let dist = (coord.x * coord.x + coord.y * coord.y).sqrt();
839                assert_relative_eq!(dist, 10.0, epsilon = 1e-10);
840            }
841        }
842    }
843
844    #[test]
845    fn test_buffer_point_zero_radius() {
846        let point = Point::new(5.0, 5.0);
847        let options = BufferOptions::default();
848        let result = buffer_point(&point, 0.0, &options);
849        assert!(result.is_ok());
850    }
851
852    #[test]
853    fn test_buffer_point_negative_radius() {
854        let point = Point::new(0.0, 0.0);
855        let options = BufferOptions::default();
856        let result = buffer_point(&point, -10.0, &options);
857        assert!(result.is_err());
858    }
859
860    #[test]
861    fn test_buffer_linestring_basic() {
862        let coords = vec![Coordinate::new_2d(0.0, 0.0), Coordinate::new_2d(10.0, 0.0)];
863        let line = LineString::new(coords);
864        assert!(line.is_ok());
865
866        if let Ok(ls) = line {
867            let options = BufferOptions::default();
868            let result = buffer_linestring(&ls, 5.0, &options);
869            assert!(result.is_ok());
870
871            if let Ok(poly) = result {
872                // Buffer should create a polygon
873                assert!(poly.exterior.coords.len() > 4);
874            }
875        }
876    }
877
878    #[test]
879    fn test_buffer_linestring_empty() {
880        let coords = vec![Coordinate::new_2d(0.0, 0.0)];
881        let line = LineString::new(coords);
882        assert!(line.is_err()); // Should fail in LineString::new
883    }
884
885    #[test]
886    fn test_buffer_polygon_basic() {
887        let exterior_coords = vec![
888            Coordinate::new_2d(0.0, 0.0),
889            Coordinate::new_2d(10.0, 0.0),
890            Coordinate::new_2d(10.0, 10.0),
891            Coordinate::new_2d(0.0, 10.0),
892            Coordinate::new_2d(0.0, 0.0),
893        ];
894        let exterior = LineString::new(exterior_coords);
895        assert!(exterior.is_ok());
896
897        if let Ok(ext) = exterior {
898            let polygon = Polygon::new(ext, vec![]);
899            assert!(polygon.is_ok());
900
901            if let Ok(poly) = polygon {
902                let options = BufferOptions::default();
903                let result = buffer_polygon(&poly, 2.0, &options);
904                assert!(result.is_ok());
905
906                // The buffered exterior must EXPAND outward, not shrink inward:
907                // a unit-square [0,10]×[0,10] buffered by +2 must reach roughly
908                // [-2,12]×[-2,12]. The old (buggy) implementation produced points
909                // strictly inside the original bounding box.
910                if let Ok(buffered) = result {
911                    let mut min_x = f64::INFINITY;
912                    let mut min_y = f64::INFINITY;
913                    let mut max_x = f64::NEG_INFINITY;
914                    let mut max_y = f64::NEG_INFINITY;
915                    for c in &buffered.exterior.coords {
916                        min_x = min_x.min(c.x);
917                        min_y = min_y.min(c.y);
918                        max_x = max_x.max(c.x);
919                        max_y = max_y.max(c.y);
920                    }
921                    // Strictly grows beyond the original [0,10]×[0,10] box.
922                    assert!(min_x < 0.0, "min_x should extend below 0, got {min_x}");
923                    assert!(min_y < 0.0, "min_y should extend below 0, got {min_y}");
924                    assert!(max_x > 10.0, "max_x should extend beyond 10, got {max_x}");
925                    assert!(max_y > 10.0, "max_y should extend beyond 10, got {max_y}");
926                    // Should be close to the expected [-2, 12] extent.
927                    assert!((-2.5..=-1.5).contains(&min_x), "min_x ~ -2, got {min_x}");
928                    assert!((11.5..=12.5).contains(&max_x), "max_x ~ 12, got {max_x}");
929                }
930            }
931        }
932    }
933
934    #[test]
935    fn test_buffer_polygon_honors_join_style() {
936        // A right-angle square exercises corner join geometry. Each join style
937        // must be honoured (previously `_options` was ignored for polygons).
938        let exterior_coords = vec![
939            Coordinate::new_2d(0.0, 0.0),
940            Coordinate::new_2d(10.0, 0.0),
941            Coordinate::new_2d(10.0, 10.0),
942            Coordinate::new_2d(0.0, 10.0),
943            Coordinate::new_2d(0.0, 0.0),
944        ];
945        let exterior = LineString::new(exterior_coords).expect("valid ring");
946        let poly = Polygon::new(exterior, vec![]).expect("valid polygon");
947
948        let mut round_pts = 0usize;
949        let mut bevel_pts = 0usize;
950        let mut miter_pts = 0usize;
951
952        for (style, out) in [
953            (BufferJoinStyle::Round, &mut round_pts),
954            (BufferJoinStyle::Bevel, &mut bevel_pts),
955            (BufferJoinStyle::Miter, &mut miter_pts),
956        ] {
957            let options = BufferOptions {
958                join_style: style,
959                ..BufferOptions::default()
960            };
961            let buffered = buffer_polygon(&poly, 2.0, &options).expect("buffer ok");
962            // All coordinates must be finite and the ring closed.
963            for c in &buffered.exterior.coords {
964                assert!(c.x.is_finite() && c.y.is_finite());
965            }
966            *out = buffered.exterior.coords.len();
967        }
968
969        // Round joins insert arc points at each corner, so they must produce
970        // strictly more vertices than the sharp miter join.
971        assert!(
972            round_pts > miter_pts,
973            "round joins ({round_pts}) should add more points than miter ({miter_pts})"
974        );
975        // Bevel inserts two points per corner; miter a single point per corner,
976        // so bevel must have at least as many points as miter.
977        assert!(
978            bevel_pts >= miter_pts,
979            "bevel ({bevel_pts}) should have >= miter ({miter_pts}) points"
980        );
981    }
982
983    #[test]
984    fn test_compute_miter_point_extends_beyond_offsets() {
985        // Right-angle corner at the vertex (10, 0). The two vertex offsets are
986        // on the offset lines y = 1 (through (10,1)) and x = 9 (through (9,0)),
987        // so the true miter (their intersection) is (9, 1) — NOT the midpoint
988        // (9.5, 0.5) that the old implementation returned.
989        let vertex = Coordinate::new_2d(10.0, 0.0);
990        let offset1 = Coordinate::new_2d(10.0, 1.0); // seg1 (east) left-normal
991        let offset2 = Coordinate::new_2d(9.0, 0.0); // seg2 (north) left-normal
992        let miter =
993            compute_miter_point(&offset1, &offset2, &vertex, 1.0, 5.0).expect("within miter limit");
994        assert_relative_eq!(miter.x, 9.0, epsilon = 1e-9);
995        assert_relative_eq!(miter.y, 1.0, epsilon = 1e-9);
996
997        // Explicitly reject the degenerate midpoint answer.
998        let midpoint_x = (offset1.x + offset2.x) / 2.0;
999        let midpoint_y = (offset1.y + offset2.y) / 2.0;
1000        assert!(
1001            (miter.x - midpoint_x).abs() > 0.1 || (miter.y - midpoint_y).abs() > 0.1,
1002            "miter point must differ from the offset midpoint"
1003        );
1004    }
1005
1006    #[test]
1007    fn test_compute_miter_point_exceeds_limit_falls_back() {
1008        // Nearly anti-parallel arms → miter blows up → None (bevel fallback).
1009        let vertex = Coordinate::new_2d(0.0, 0.0);
1010        let offset1 = Coordinate::new_2d(0.0, 1.0);
1011        // A tiny tilt away from straight-down: cos(θ/2) ≈ 0.005 → ratio ≈ 200.
1012        let offset2 = Coordinate::new_2d(0.01, -0.99995);
1013        let miter = compute_miter_point(&offset1, &offset2, &vertex, 1.0, 5.0);
1014        assert!(
1015            miter.is_none(),
1016            "sharp corner must fall back to bevel (None), got {miter:?}"
1017        );
1018    }
1019
1020    #[test]
1021    fn test_offset_segment() {
1022        let p1 = Coordinate::new_2d(0.0, 0.0);
1023        let p2 = Coordinate::new_2d(10.0, 0.0);
1024        let result = offset_segment(&p1, &p2, 5.0);
1025
1026        assert!(result.is_ok());
1027        if let Ok((left, right)) = result {
1028            assert_relative_eq!(left.x, 0.0, epsilon = 1e-10);
1029            assert_relative_eq!(left.y, 5.0, epsilon = 1e-10);
1030            assert_relative_eq!(right.x, 0.0, epsilon = 1e-10);
1031            assert_relative_eq!(right.y, -5.0, epsilon = 1e-10);
1032        }
1033    }
1034
1035    #[test]
1036    fn test_buffer_cap_styles() {
1037        let coords = vec![Coordinate::new_2d(0.0, 0.0), Coordinate::new_2d(10.0, 0.0)];
1038        let line = LineString::new(coords);
1039        assert!(line.is_ok());
1040
1041        if let Ok(ls) = line {
1042            // Test round caps
1043            let mut options = BufferOptions::default();
1044            options.cap_style = BufferCapStyle::Round;
1045            let result = buffer_linestring(&ls, 5.0, &options);
1046            assert!(result.is_ok());
1047
1048            // Test flat caps
1049            options.cap_style = BufferCapStyle::Flat;
1050            let result = buffer_linestring(&ls, 5.0, &options);
1051            assert!(result.is_ok());
1052
1053            // Test square caps
1054            options.cap_style = BufferCapStyle::Square;
1055            let result = buffer_linestring(&ls, 5.0, &options);
1056            assert!(result.is_ok());
1057        }
1058    }
1059
1060    #[test]
1061    fn test_buffer_join_styles() {
1062        let coords = vec![
1063            Coordinate::new_2d(0.0, 0.0),
1064            Coordinate::new_2d(10.0, 0.0),
1065            Coordinate::new_2d(10.0, 10.0),
1066        ];
1067        let line = LineString::new(coords);
1068        assert!(line.is_ok());
1069
1070        if let Ok(ls) = line {
1071            // Test round joins
1072            let mut options = BufferOptions::default();
1073            options.join_style = BufferJoinStyle::Round;
1074            let result = buffer_linestring(&ls, 5.0, &options);
1075            assert!(result.is_ok());
1076
1077            // Test miter joins
1078            options.join_style = BufferJoinStyle::Miter;
1079            let result = buffer_linestring(&ls, 5.0, &options);
1080            assert!(result.is_ok());
1081
1082            // Test bevel joins
1083            options.join_style = BufferJoinStyle::Bevel;
1084            let result = buffer_linestring(&ls, 5.0, &options);
1085            assert!(result.is_ok());
1086        }
1087    }
1088}