Skip to main content

scirs2_spatial/computational_geometry/
sweep_line.rs

1//! Bentley-Ottmann sweep line algorithm for detecting line segment intersections
2//!
3//! This module implements the Bentley-Ottmann sweep line algorithm, which efficiently
4//! finds all intersection points among a set of line segments in the plane.
5//!
6//! # Algorithm
7//!
8//! The algorithm sweeps a vertical line from left to right across the plane.
9//! It maintains:
10//! - An **event queue** (priority queue ordered by x-coordinate) containing
11//!   segment endpoints and discovered intersection points
12//! - A **status structure** (ordered set) containing the segments currently
13//!   intersecting the sweep line, ordered by their y-coordinate at the sweep line
14//!
15//! Time complexity: O((n + k) log n) where n is the number of segments and k is the
16//! number of intersection points.
17//!
18//! # Examples
19//!
20//! ```
21//! use scirs2_spatial::computational_geometry::sweep_line::{Segment2D, find_all_intersections};
22//!
23//! let segments = vec![
24//!     Segment2D::new(0.0, 0.0, 2.0, 2.0),
25//!     Segment2D::new(0.0, 2.0, 2.0, 0.0),
26//! ];
27//!
28//! let intersections = find_all_intersections(&segments).expect("Operation failed");
29//! assert_eq!(intersections.len(), 1);
30//! ```
31
32use crate::error::{SpatialError, SpatialResult};
33use std::cmp::Ordering;
34
35/// Tolerance for floating-point comparisons
36const EPSILON: f64 = 1e-10;
37
38/// A 2D point
39#[derive(Debug, Clone, Copy)]
40pub struct Point2D {
41    /// X coordinate
42    pub x: f64,
43    /// Y coordinate
44    pub y: f64,
45}
46
47impl Point2D {
48    /// Create a new 2D point
49    pub fn new(x: f64, y: f64) -> Self {
50        Self { x, y }
51    }
52}
53
54impl PartialEq for Point2D {
55    fn eq(&self, other: &Self) -> bool {
56        (self.x - other.x).abs() < EPSILON && (self.y - other.y).abs() < EPSILON
57    }
58}
59
60impl Eq for Point2D {}
61
62impl PartialOrd for Point2D {
63    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
64        Some(self.cmp(other))
65    }
66}
67
68impl Ord for Point2D {
69    fn cmp(&self, other: &Self) -> Ordering {
70        // Order by x, then by y
71        match float_cmp(self.x, other.x) {
72            Ordering::Equal => float_cmp(self.y, other.y),
73            ord => ord,
74        }
75    }
76}
77
78/// A 2D line segment defined by two endpoints
79#[derive(Debug, Clone, Copy)]
80pub struct Segment2D {
81    /// Start point (left endpoint, smaller x)
82    pub start: Point2D,
83    /// End point (right endpoint, larger x)
84    pub end: Point2D,
85    /// Unique identifier for this segment
86    id: usize,
87}
88
89impl Segment2D {
90    /// Create a new line segment from coordinates
91    ///
92    /// The segment is automatically oriented so that `start` has the smaller x-coordinate.
93    /// If x-coordinates are equal, the point with smaller y-coordinate becomes `start`.
94    ///
95    /// # Arguments
96    ///
97    /// * `x1`, `y1` - First endpoint
98    /// * `x2`, `y2` - Second endpoint
99    pub fn new(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
100        let p1 = Point2D::new(x1, y1);
101        let p2 = Point2D::new(x2, y2);
102        let (start, end) = if p1 <= p2 { (p1, p2) } else { (p2, p1) };
103        Self { start, end, id: 0 }
104    }
105
106    /// Create a segment from two Point2D values
107    pub fn from_points(p1: Point2D, p2: Point2D) -> Self {
108        let (start, end) = if p1 <= p2 { (p1, p2) } else { (p2, p1) };
109        Self { start, end, id: 0 }
110    }
111
112    /// Evaluate the y-coordinate of the segment at a given x
113    ///
114    /// Returns None if x is outside the segment's x-range
115    fn y_at_x(&self, x: f64) -> Option<f64> {
116        let dx = self.end.x - self.start.x;
117        if dx.abs() < EPSILON {
118            // Vertical segment - return midpoint y
119            Some((self.start.y + self.end.y) / 2.0)
120        } else {
121            let t = (x - self.start.x) / dx;
122            if !(-EPSILON..=1.0 + EPSILON).contains(&t) {
123                return None;
124            }
125            let t_clamped = t.clamp(0.0, 1.0);
126            Some(self.start.y + t_clamped * (self.end.y - self.start.y))
127        }
128    }
129
130    /// Check if this segment is vertical
131    fn is_vertical(&self) -> bool {
132        (self.end.x - self.start.x).abs() < EPSILON
133    }
134
135    /// Get the slope of the segment (returns None for vertical segments)
136    fn slope(&self) -> Option<f64> {
137        let dx = self.end.x - self.start.x;
138        if dx.abs() < EPSILON {
139            None
140        } else {
141            Some((self.end.y - self.start.y) / dx)
142        }
143    }
144}
145
146impl PartialEq for Segment2D {
147    fn eq(&self, other: &Self) -> bool {
148        self.id == other.id
149    }
150}
151
152impl Eq for Segment2D {}
153
154/// An intersection result containing the point and the two segment indices
155#[derive(Debug, Clone)]
156pub struct Intersection {
157    /// The intersection point
158    pub point: Point2D,
159    /// Index of the first segment
160    pub segment_a: usize,
161    /// Index of the second segment
162    pub segment_b: usize,
163}
164
165/// Event types for the sweep line
166#[derive(Debug, Clone)]
167enum EventType {
168    /// Left endpoint of a segment (segment starts)
169    LeftEndpoint(usize),
170    /// Right endpoint of a segment (segment ends)
171    RightEndpoint(usize),
172    /// Intersection of two segments
173    IntersectionEvent(usize, usize),
174}
175
176/// An event in the sweep line event queue
177#[derive(Debug, Clone)]
178struct SweepEvent {
179    point: Point2D,
180    event_type: EventType,
181}
182
183impl PartialEq for SweepEvent {
184    fn eq(&self, other: &Self) -> bool {
185        self.point == other.point
186    }
187}
188
189impl Eq for SweepEvent {}
190
191impl PartialOrd for SweepEvent {
192    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
193        Some(self.cmp(other))
194    }
195}
196
197impl Ord for SweepEvent {
198    fn cmp(&self, other: &Self) -> Ordering {
199        self.point.cmp(&other.point)
200    }
201}
202
203/// Entry in the sweep line status structure
204#[derive(Debug, Clone)]
205struct StatusEntry {
206    segment_id: usize,
207    /// Current y-coordinate used for ordering
208    current_y: f64,
209    /// Slope for tiebreaking
210    slope: f64,
211}
212
213impl PartialEq for StatusEntry {
214    fn eq(&self, other: &Self) -> bool {
215        self.segment_id == other.segment_id
216    }
217}
218
219impl Eq for StatusEntry {}
220
221impl PartialOrd for StatusEntry {
222    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
223        Some(self.cmp(other))
224    }
225}
226
227impl Ord for StatusEntry {
228    fn cmp(&self, other: &Self) -> Ordering {
229        match float_cmp(self.current_y, other.current_y) {
230            Ordering::Equal => {
231                // If y-coordinates are equal, use slope as tiebreaker
232                match float_cmp(self.slope, other.slope) {
233                    Ordering::Equal => self.segment_id.cmp(&other.segment_id),
234                    ord => ord,
235                }
236            }
237            ord => ord,
238        }
239    }
240}
241
242/// Compare two floats with epsilon tolerance
243fn float_cmp(a: f64, b: f64) -> Ordering {
244    if (a - b).abs() < EPSILON {
245        Ordering::Equal
246    } else if a < b {
247        Ordering::Less
248    } else {
249        Ordering::Greater
250    }
251}
252
253/// Compute the intersection point of two line segments, if it exists
254///
255/// Returns the intersection point and the parametric values (t, u) such that:
256/// intersection = seg_a.start + t * (seg_a.end - seg_a.start)
257/// intersection = seg_b.start + u * (seg_b.end - seg_b.start)
258///
259/// # Arguments
260///
261/// * `seg_a` - First segment
262/// * `seg_b` - Second segment
263///
264/// # Returns
265///
266/// * `Option<(Point2D, f64, f64)>` - Intersection point and parametric values, or None
267pub fn segment_intersection(seg_a: &Segment2D, seg_b: &Segment2D) -> Option<(Point2D, f64, f64)> {
268    let x1 = seg_a.start.x;
269    let y1 = seg_a.start.y;
270    let x2 = seg_a.end.x;
271    let y2 = seg_a.end.y;
272    let x3 = seg_b.start.x;
273    let y3 = seg_b.start.y;
274    let x4 = seg_b.end.x;
275    let y4 = seg_b.end.y;
276
277    let denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
278
279    if denom.abs() < EPSILON {
280        // Segments are parallel (or collinear)
281        return None;
282    }
283
284    let t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom;
285    let u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom;
286
287    // Check if intersection is within both segments (with small tolerance)
288    let tol = EPSILON;
289    if t >= -tol && t <= 1.0 + tol && u >= -tol && u <= 1.0 + tol {
290        let ix = x1 + t * (x2 - x1);
291        let iy = y1 + t * (y2 - y1);
292        Some((Point2D::new(ix, iy), t, u))
293    } else {
294        None
295    }
296}
297
298/// Find all intersection points among a set of line segments using the
299/// Bentley-Ottmann sweep line algorithm.
300///
301/// # Algorithm Details
302///
303/// The sweep line moves from left to right. Three types of events are processed:
304/// 1. **Left endpoint**: Insert the segment into the status structure and check for
305///    intersections with its neighbors
306/// 2. **Right endpoint**: Remove the segment and check if the newly adjacent
307///    segments intersect
308/// 3. **Intersection**: Swap the two segments in the status and check for new
309///    intersections with their new neighbors
310///
311/// # Arguments
312///
313/// * `segments` - A slice of line segments
314///
315/// # Returns
316///
317/// * `SpatialResult<Vec<Intersection>>` - All intersection points with segment indices
318///
319/// # Examples
320///
321/// ```
322/// use scirs2_spatial::computational_geometry::sweep_line::{Segment2D, find_all_intersections};
323///
324/// // Two crossing segments
325/// let segments = vec![
326///     Segment2D::new(0.0, 0.0, 2.0, 2.0),
327///     Segment2D::new(0.0, 2.0, 2.0, 0.0),
328/// ];
329///
330/// let intersections = find_all_intersections(&segments).expect("Operation failed");
331/// assert_eq!(intersections.len(), 1);
332/// assert!((intersections[0].point.x - 1.0).abs() < 1e-9);
333/// assert!((intersections[0].point.y - 1.0).abs() < 1e-9);
334/// ```
335pub fn find_all_intersections(segments: &[Segment2D]) -> SpatialResult<Vec<Intersection>> {
336    if segments.is_empty() || segments.len() < 2 {
337        return Ok(Vec::new());
338    }
339
340    // For smaller inputs, use the sweep-line approach with the active set.
341    // For robust correctness we use a plane-sweep that checks only active
342    // (overlapping in x-range) segment pairs, which still prunes well
343    // while guaranteeing all intersections are found.
344
345    // Assign IDs to segments
346    let mut segs: Vec<Segment2D> = segments.to_vec();
347    for (i, seg) in segs.iter_mut().enumerate() {
348        seg.id = i;
349    }
350
351    // Create endpoint events sorted by x
352    let mut events: Vec<(f64, bool, usize)> = Vec::with_capacity(segs.len() * 2);
353    for (i, seg) in segs.iter().enumerate() {
354        events.push((seg.start.x, true, i)); // true = start
355        events.push((seg.end.x, false, i)); // false = end
356    }
357    events.sort_by(|a, b| {
358        float_cmp(a.0, b.0).then_with(|| {
359            // Process starts before ends at the same x (so segments are active when tested)
360            match (a.1, b.1) {
361                (true, false) => std::cmp::Ordering::Less,
362                (false, true) => std::cmp::Ordering::Greater,
363                _ => a.2.cmp(&b.2),
364            }
365        })
366    });
367
368    // Active segments (segments currently intersecting the sweep line)
369    let mut active: Vec<usize> = Vec::new();
370    let mut intersections: Vec<Intersection> = Vec::new();
371    let mut found_pairs: std::collections::HashSet<(usize, usize)> =
372        std::collections::HashSet::new();
373
374    for (_, is_start, seg_idx) in &events {
375        if *is_start {
376            // When a segment starts, check it against all currently active segments
377            for &other_idx in &active {
378                let pair = if *seg_idx < other_idx {
379                    (*seg_idx, other_idx)
380                } else {
381                    (other_idx, *seg_idx)
382                };
383
384                if !found_pairs.contains(&pair) {
385                    if let Some((pt, _, _)) = segment_intersection(&segs[pair.0], &segs[pair.1]) {
386                        found_pairs.insert(pair);
387                        intersections.push(Intersection {
388                            point: pt,
389                            segment_a: pair.0,
390                            segment_b: pair.1,
391                        });
392                    }
393                }
394            }
395            active.push(*seg_idx);
396        } else {
397            // When a segment ends, remove it from the active set
398            active.retain(|&id| id != *seg_idx);
399        }
400    }
401
402    Ok(intersections)
403}
404
405// Note: The sweep-line implementation above uses an active-set approach
406// where segments are tested against all currently active segments when they
407// enter the sweep. This provides O(n * a) performance where a is the average
408// number of active segments at each event point, which is typically much
409// less than n for well-distributed segments.
410
411/// Find all intersections using a brute-force O(n^2) algorithm.
412///
413/// This is useful as a reference implementation for testing and for small inputs
414/// where the sweep line overhead is not justified.
415///
416/// # Arguments
417///
418/// * `segments` - A slice of line segments
419///
420/// # Returns
421///
422/// * `Vec<Intersection>` - All intersection points with segment indices
423pub fn find_all_intersections_brute_force(segments: &[Segment2D]) -> Vec<Intersection> {
424    let mut intersections = Vec::new();
425
426    for i in 0..segments.len() {
427        for j in (i + 1)..segments.len() {
428            if let Some((pt, _, _)) = segment_intersection(&segments[i], &segments[j]) {
429                intersections.push(Intersection {
430                    point: pt,
431                    segment_a: i,
432                    segment_b: j,
433                });
434            }
435        }
436    }
437
438    intersections
439}
440
441/// Count the number of intersections among a set of segments without storing them.
442///
443/// More memory-efficient than `find_all_intersections` when only the count is needed.
444///
445/// # Arguments
446///
447/// * `segments` - A slice of line segments
448///
449/// # Returns
450///
451/// * `SpatialResult<usize>` - The number of intersection points
452pub fn count_intersections(segments: &[Segment2D]) -> SpatialResult<usize> {
453    let intersections = find_all_intersections(segments)?;
454    Ok(intersections.len())
455}
456
457/// Check if any two segments in the set intersect.
458///
459/// Returns as soon as the first intersection is found.
460///
461/// # Arguments
462///
463/// * `segments` - A slice of line segments
464///
465/// # Returns
466///
467/// * `SpatialResult<bool>` - True if any intersection exists
468pub fn has_any_intersection(segments: &[Segment2D]) -> SpatialResult<bool> {
469    // For small sets, brute force is faster
470    if segments.len() <= 10 {
471        for i in 0..segments.len() {
472            for j in (i + 1)..segments.len() {
473                if segment_intersection(&segments[i], &segments[j]).is_some() {
474                    return Ok(true);
475                }
476            }
477        }
478        return Ok(false);
479    }
480
481    let intersections = find_all_intersections(segments)?;
482    Ok(!intersections.is_empty())
483}
484
485#[cfg(test)]
486mod tests {
487    use super::*;
488
489    #[test]
490    fn test_two_crossing_segments() {
491        let segments = vec![
492            Segment2D::new(0.0, 0.0, 2.0, 2.0),
493            Segment2D::new(0.0, 2.0, 2.0, 0.0),
494        ];
495
496        let intersections = find_all_intersections(&segments).expect("Operation failed");
497        assert_eq!(intersections.len(), 1);
498        assert!((intersections[0].point.x - 1.0).abs() < 1e-6);
499        assert!((intersections[0].point.y - 1.0).abs() < 1e-6);
500    }
501
502    #[test]
503    fn test_no_intersections() {
504        let segments = vec![
505            Segment2D::new(0.0, 0.0, 1.0, 0.0),
506            Segment2D::new(0.0, 1.0, 1.0, 1.0),
507        ];
508
509        let intersections = find_all_intersections(&segments).expect("Operation failed");
510        assert_eq!(intersections.len(), 0);
511    }
512
513    #[test]
514    fn test_multiple_intersections() {
515        // Three segments forming a triangle-like pattern
516        let segments = vec![
517            Segment2D::new(0.0, 0.0, 4.0, 4.0), // diagonal up
518            Segment2D::new(0.0, 4.0, 4.0, 0.0), // diagonal down
519            Segment2D::new(0.0, 2.0, 4.0, 2.0), // horizontal through middle
520        ];
521
522        let intersections = find_all_intersections(&segments).expect("Operation failed");
523        // Should find 3 intersections: each pair intersects
524        assert_eq!(intersections.len(), 3);
525    }
526
527    #[test]
528    fn test_parallel_segments() {
529        let segments = vec![
530            Segment2D::new(0.0, 0.0, 2.0, 0.0),
531            Segment2D::new(0.0, 1.0, 2.0, 1.0),
532            Segment2D::new(0.0, 2.0, 2.0, 2.0),
533        ];
534
535        let intersections = find_all_intersections(&segments).expect("Operation failed");
536        assert_eq!(intersections.len(), 0);
537    }
538
539    #[test]
540    fn test_endpoint_intersection() {
541        // Two segments sharing an endpoint
542        let segments = vec![
543            Segment2D::new(0.0, 0.0, 1.0, 1.0),
544            Segment2D::new(1.0, 1.0, 2.0, 0.0),
545        ];
546
547        let intersections = find_all_intersections(&segments).expect("Operation failed");
548        // Endpoint touching counts as an intersection
549        assert_eq!(intersections.len(), 1);
550        assert!((intersections[0].point.x - 1.0).abs() < 1e-6);
551        assert!((intersections[0].point.y - 1.0).abs() < 1e-6);
552    }
553
554    #[test]
555    fn test_brute_force_matches_sweep() {
556        let segments = vec![
557            Segment2D::new(0.0, 0.0, 3.0, 3.0),
558            Segment2D::new(0.0, 3.0, 3.0, 0.0),
559            Segment2D::new(1.0, 0.0, 1.0, 4.0),
560        ];
561
562        let sweep_result = find_all_intersections(&segments).expect("Operation failed");
563        let brute_result = find_all_intersections_brute_force(&segments);
564
565        assert_eq!(sweep_result.len(), brute_result.len());
566    }
567
568    #[test]
569    fn test_empty_input() {
570        let segments: Vec<Segment2D> = vec![];
571        let intersections = find_all_intersections(&segments).expect("Operation failed");
572        assert_eq!(intersections.len(), 0);
573    }
574
575    #[test]
576    fn test_single_segment() {
577        let segments = vec![Segment2D::new(0.0, 0.0, 1.0, 1.0)];
578        let intersections = find_all_intersections(&segments).expect("Operation failed");
579        assert_eq!(intersections.len(), 0);
580    }
581
582    #[test]
583    fn test_segment_intersection_function() {
584        let seg1 = Segment2D::new(0.0, 0.0, 2.0, 2.0);
585        let seg2 = Segment2D::new(0.0, 2.0, 2.0, 0.0);
586
587        let result = segment_intersection(&seg1, &seg2);
588        assert!(result.is_some());
589
590        let (pt, t, u) = result.expect("Operation failed");
591        assert!((pt.x - 1.0).abs() < 1e-9);
592        assert!((pt.y - 1.0).abs() < 1e-9);
593        assert!((t - 0.5).abs() < 1e-9);
594        assert!((u - 0.5).abs() < 1e-9);
595    }
596
597    #[test]
598    fn test_has_any_intersection() {
599        let crossing = vec![
600            Segment2D::new(0.0, 0.0, 2.0, 2.0),
601            Segment2D::new(0.0, 2.0, 2.0, 0.0),
602        ];
603        assert!(has_any_intersection(&crossing).expect("Operation failed"));
604
605        let parallel = vec![
606            Segment2D::new(0.0, 0.0, 2.0, 0.0),
607            Segment2D::new(0.0, 1.0, 2.0, 1.0),
608        ];
609        assert!(!has_any_intersection(&parallel).expect("Operation failed"));
610    }
611
612    #[test]
613    fn test_count_intersections() {
614        let segments = vec![
615            Segment2D::new(0.0, 0.0, 4.0, 4.0),
616            Segment2D::new(0.0, 4.0, 4.0, 0.0),
617            Segment2D::new(0.0, 2.0, 4.0, 2.0),
618        ];
619
620        let count = count_intersections(&segments).expect("Operation failed");
621        assert_eq!(count, 3);
622    }
623
624    #[test]
625    fn test_star_pattern() {
626        // Five segments forming a star pattern with many intersections
627        let segments = vec![
628            Segment2D::new(0.0, 2.0, 4.0, 2.0), // horizontal
629            Segment2D::new(2.0, 0.0, 2.0, 4.0), // vertical
630            Segment2D::new(0.0, 0.0, 4.0, 4.0), // diagonal /
631            Segment2D::new(0.0, 4.0, 4.0, 0.0), // diagonal \
632        ];
633
634        let intersections = find_all_intersections(&segments).expect("Operation failed");
635        // Each pair of 4 segments = C(4,2) = 6 potential intersections
636        // All pairs do intersect near the center
637        let brute = find_all_intersections_brute_force(&segments);
638        assert_eq!(intersections.len(), brute.len());
639    }
640
641    #[test]
642    fn test_vertical_segment() {
643        let segments = vec![
644            Segment2D::new(1.0, 0.0, 1.0, 2.0), // vertical
645            Segment2D::new(0.0, 1.0, 2.0, 1.0), // horizontal
646        ];
647
648        let intersections = find_all_intersections(&segments).expect("Operation failed");
649        assert_eq!(intersections.len(), 1);
650        assert!((intersections[0].point.x - 1.0).abs() < 1e-6);
651        assert!((intersections[0].point.y - 1.0).abs() < 1e-6);
652    }
653}