scirs2_spatial/
polygon.rs

1//! Polygon operations module
2//!
3//! This module provides functionality for working with polygons, including:
4//! - Point in polygon testing
5//! - Area and centroid calculations
6//! - Polygon operations (simplification, transformation)
7//!
8//! # Examples
9//!
10//! ```
11//! use scirs2_core::ndarray::array;
12//! use scirs2_spatial::polygon::point_in_polygon;
13//!
14//! // Create a square polygon
15//! let polygon = array![
16//!     [0.0, 0.0],
17//!     [1.0, 0.0],
18//!     [1.0, 1.0],
19//!     [0.0, 1.0],
20//! ];
21//!
22//! // Test if a point is inside
23//! let inside = point_in_polygon(&[0.5, 0.5], &polygon.view());
24//! assert!(inside);
25//!
26//! // Test if a point is outside
27//! let outside = point_in_polygon(&[1.5, 0.5], &polygon.view());
28//! assert!(!outside);
29//! ```
30
31use scirs2_core::ndarray::{Array2, ArrayView2};
32use scirs2_core::numeric::Float;
33
34/// Tests if a point is inside a polygon using the ray casting algorithm.
35///
36/// The algorithm works by casting a ray from the test point to infinity in the positive x direction,
37/// and counting how many times the ray intersects the polygon boundary. If the count is odd,
38/// the point is inside the polygon; if even, the point is outside.
39///
40/// # Arguments
41///
42/// * `point` - The point to test, as [x, y] coordinates
43/// * `polygon` - The polygon vertices as an array of [x, y] coordinates, assumed to be in order
44///
45/// # Returns
46///
47/// * `true` if the point is inside the polygon, `false` otherwise
48///
49/// # Examples
50///
51/// ```
52/// use scirs2_core::ndarray::array;
53/// use scirs2_spatial::polygon::point_in_polygon;
54///
55/// // Create a square polygon
56/// let polygon = array![
57///     [0.0, 0.0],
58///     [1.0, 0.0],
59///     [1.0, 1.0],
60///     [0.0, 1.0],
61/// ];
62///
63/// // Test if a point is inside
64/// let inside = point_in_polygon(&[0.5, 0.5], &polygon.view());
65/// assert!(inside);
66///
67/// // Test if a point is outside
68/// let outside = point_in_polygon(&[1.5, 0.5], &polygon.view());
69/// assert!(!outside);
70/// ```
71#[allow(dead_code)]
72pub fn point_in_polygon<T: Float>(point: &[T], polygon: &ArrayView2<T>) -> bool {
73    let x = point[0];
74    let y = point[1];
75    let n = polygon.shape()[0];
76
77    if n < 3 {
78        return false; // A polygon must have at least 3 vertices
79    }
80
81    // First check if the point is on the boundary
82    let epsilon = T::from(1e-10).expect("Operation failed");
83    if point_on_boundary(point, polygon, epsilon) {
84        return true;
85    }
86
87    // Ray casting algorithm - count intersections of a horizontal ray with polygon edges
88    let mut inside = false;
89
90    for i in 0..n {
91        let j = (i + 1) % n;
92
93        let vi_y = polygon[[i, 1]];
94        let vj_y = polygon[[j, 1]];
95
96        // Check if the edge crosses the horizontal line at y
97        if ((vi_y <= y) && (vj_y > y)) || ((vi_y > y) && (vj_y <= y)) {
98            // Compute x-coordinate of intersection
99            let vi_x = polygon[[i, 0]];
100            let vj_x = polygon[[j, 0]];
101
102            // Check if intersection is to the right of the point
103            let slope = (vj_x - vi_x) / (vj_y - vi_y);
104            let intersect_x = vi_x + (y - vi_y) * slope;
105
106            if x < intersect_x {
107                inside = !inside;
108            }
109        }
110    }
111
112    inside
113}
114
115/// Tests if a point is on the boundary of a polygon.
116///
117/// A point is considered on the boundary if it's within epsilon distance
118/// to any edge of the polygon.
119///
120/// # Arguments
121///
122/// * `point` - The point to test, as [x, y] coordinates
123/// * `polygon` - The polygon vertices as an array of [x, y] coordinates
124/// * `epsilon` - Distance threshold for considering a point on the boundary
125///
126/// # Returns
127///
128/// * `true` if the point is on the polygon boundary, `false` otherwise
129///
130/// # Examples
131///
132/// ```
133/// use scirs2_core::ndarray::array;
134/// use scirs2_spatial::polygon::point_on_boundary;
135///
136/// // Create a square polygon
137/// let polygon = array![
138///     [0.0, 0.0],
139///     [1.0, 0.0],
140///     [1.0, 1.0],
141///     [0.0, 1.0],
142/// ];
143///
144/// // Test if a point is on the boundary
145/// let on_boundary = point_on_boundary(&[0.0, 0.5], &polygon.view(), 1e-10);
146/// assert!(on_boundary);
147///
148/// // Test if a point is not on the boundary
149/// let not_on_boundary = point_on_boundary(&[0.5, 0.5], &polygon.view(), 1e-10);
150/// assert!(!not_on_boundary);
151/// ```
152#[allow(dead_code)]
153pub fn point_on_boundary<T: Float>(point: &[T], polygon: &ArrayView2<T>, epsilon: T) -> bool {
154    let x = point[0];
155    let y = point[1];
156    let n = polygon.shape()[0];
157
158    if n < 2 {
159        return false;
160    }
161
162    for i in 0..n {
163        let j = (i + 1) % n;
164
165        let x1 = polygon[[i, 0]];
166        let y1 = polygon[[i, 1]];
167        let x2 = polygon[[j, 0]];
168        let y2 = polygon[[j, 1]];
169
170        // Calculate distance from point to line segment
171        let length_squared = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1);
172
173        if length_squared.is_zero() {
174            // The segment is a point
175            let dist = ((x - x1) * (x - x1) + (y - y1) * (y - y1)).sqrt();
176            if dist < epsilon {
177                return true;
178            }
179        } else {
180            // Calculate the projection of the point onto the line
181            let t = ((x - x1) * (x2 - x1) + (y - y1) * (y2 - y1)) / length_squared;
182
183            if t < T::zero() {
184                // Closest point is the start of the segment
185                let dist = ((x - x1) * (x - x1) + (y - y1) * (y - y1)).sqrt();
186                if dist < epsilon {
187                    return true;
188                }
189            } else if t > T::one() {
190                // Closest point is the end of the segment
191                let dist = ((x - x2) * (x - x2) + (y - y2) * (y - y2)).sqrt();
192                if dist < epsilon {
193                    return true;
194                }
195            } else {
196                // Closest point is along the segment
197                let projection_x = x1 + t * (x2 - x1);
198                let projection_y = y1 + t * (y2 - y1);
199                let dist = ((x - projection_x) * (x - projection_x)
200                    + (y - projection_y) * (y - projection_y))
201                    .sqrt();
202                if dist < epsilon {
203                    return true;
204                }
205            }
206        }
207    }
208
209    false
210}
211
212/// Calculate the area of a simple polygon.
213///
214/// Uses the Shoelace formula (also known as the surveyor's formula).
215///
216/// # Arguments
217///
218/// * `polygon` - The polygon vertices as an array of [x, y] coordinates
219///
220/// # Returns
221///
222/// * The area of the polygon
223///
224/// # Examples
225///
226/// ```
227/// use scirs2_core::ndarray::array;
228/// use scirs2_spatial::polygon::polygon_area;
229///
230/// // Create a 1x1 square
231/// let square = array![
232///     [0.0, 0.0],
233///     [1.0, 0.0],
234///     [1.0, 1.0],
235///     [0.0, 1.0],
236/// ];
237///
238/// let area: f64 = polygon_area(&square.view());
239/// assert!((area - 1.0).abs() < 1e-10);
240/// ```
241#[allow(dead_code)]
242pub fn polygon_area<T: Float>(polygon: &ArrayView2<T>) -> T {
243    let n = polygon.shape()[0];
244
245    if n < 3 {
246        return T::zero(); // A polygon must have at least 3 vertices
247    }
248
249    let mut area = T::zero();
250
251    for i in 0..n {
252        let j = (i + 1) % n;
253
254        let xi = polygon[[i, 0]];
255        let yi = polygon[[i, 1]];
256        let xj = polygon[[j, 0]];
257        let yj = polygon[[j, 1]];
258
259        area = area + (xi * yj - xj * yi);
260    }
261
262    (area / (T::one() + T::one())).abs()
263}
264
265/// Calculate the centroid of a simple polygon.
266///
267/// # Arguments
268///
269/// * `polygon` - The polygon vertices as an array of [x, y] coordinates
270///
271/// # Returns
272///
273/// * The centroid coordinates as [x, y]
274///
275/// # Examples
276///
277/// ```
278/// use scirs2_core::ndarray::array;
279/// use scirs2_spatial::polygon::polygon_centroid;
280///
281/// // Create a 1x1 square
282/// let square = array![
283///     [0.0, 0.0],
284///     [1.0, 0.0],
285///     [1.0, 1.0],
286///     [0.0, 1.0],
287/// ];
288///
289/// let centroid: Vec<f64> = polygon_centroid(&square.view());
290/// assert!((centroid[0] - 0.5).abs() < 1e-10);
291/// assert!((centroid[1] - 0.5).abs() < 1e-10);
292/// ```
293#[allow(dead_code)]
294pub fn polygon_centroid<T: Float>(polygon: &ArrayView2<T>) -> Vec<T> {
295    let n = polygon.shape()[0];
296
297    if n < 3 {
298        // Return the average of points for degenerate cases
299        let mut x_sum = T::zero();
300        let mut y_sum = T::zero();
301
302        for i in 0..n {
303            x_sum = x_sum + polygon[[i, 0]];
304            y_sum = y_sum + polygon[[i, 1]];
305        }
306
307        if n > 0 {
308            return vec![
309                x_sum / T::from(n).expect("Operation failed"),
310                y_sum / T::from(n).expect("Operation failed"),
311            ];
312        } else {
313            return vec![T::zero(), T::zero()];
314        }
315    }
316
317    let mut cx = T::zero();
318    let mut cy = T::zero();
319    let mut area = T::zero();
320
321    for i in 0..n {
322        let j = (i + 1) % n;
323
324        let xi = polygon[[i, 0]];
325        let yi = polygon[[i, 1]];
326        let xj = polygon[[j, 0]];
327        let yj = polygon[[j, 1]];
328
329        let cross = xi * yj - xj * yi;
330
331        cx = cx + (xi + xj) * cross;
332        cy = cy + (yi + yj) * cross;
333        area = area + cross;
334    }
335
336    area = area / (T::one() + T::one());
337
338    if area.is_zero() {
339        // Degenerate case, return the average of points
340        let mut x_sum = T::zero();
341        let mut y_sum = T::zero();
342
343        for i in 0..n {
344            x_sum = x_sum + polygon[[i, 0]];
345            y_sum = y_sum + polygon[[i, 1]];
346        }
347
348        return vec![
349            x_sum / T::from(n).expect("Operation failed"),
350            y_sum / T::from(n).expect("Operation failed"),
351        ];
352    }
353
354    let six = T::from(6).expect("Operation failed");
355    cx = cx / (six * area);
356    cy = cy / (six * area);
357
358    // The formula can give negative coordinates if the polygon is
359    // oriented clockwise, so we take the absolute value
360    vec![cx.abs(), cy.abs()]
361}
362
363/// Tests if polygon A contains polygon B (every point of B is inside or on the boundary of A).
364///
365/// # Arguments
366///
367/// * `polygon_a` - The container polygon vertices
368/// * `polygon_b` - The contained polygon vertices
369///
370/// # Returns
371///
372/// * `true` if polygon A contains polygon B, `false` otherwise
373///
374/// # Examples
375///
376/// ```
377/// use scirs2_core::ndarray::array;
378/// use scirs2_spatial::polygon::polygon_contains_polygon;
379///
380/// // Create an outer polygon (large square)
381/// let outer = array![
382///     [0.0, 0.0],
383///     [2.0, 0.0],
384///     [2.0, 2.0],
385///     [0.0, 2.0],
386/// ];
387///
388/// // Create an inner polygon (small square)
389/// let inner = array![
390///     [0.5, 0.5],
391///     [1.5, 0.5],
392///     [1.5, 1.5],
393///     [0.5, 1.5],
394/// ];
395///
396/// let contains = polygon_contains_polygon(&outer.view(), &inner.view());
397/// assert!(contains);
398/// ```
399#[allow(dead_code)]
400pub fn polygon_contains_polygon<T: Float>(
401    polygon_a: &ArrayView2<T>,
402    polygon_b: &ArrayView2<T>,
403) -> bool {
404    // Special case: if A and B are the same polygon, A contains B
405    if polygon_a.shape() == polygon_b.shape() {
406        let n = polygon_a.shape()[0];
407        let mut same = true;
408
409        for i in 0..n {
410            if polygon_a[[i, 0]] != polygon_b[[i, 0]] || polygon_a[[i, 1]] != polygon_b[[i, 1]] {
411                same = false;
412                break;
413            }
414        }
415
416        if same {
417            return true;
418        }
419    }
420
421    let n_b = polygon_b.shape()[0];
422
423    // Check if all vertices of polygon B are inside or on the boundary of polygon A
424    for i in 0..n_b {
425        let point = [polygon_b[[i, 0]], polygon_b[[i, 1]]];
426        if !point_in_polygon(&point, polygon_a) {
427            return false;
428        }
429    }
430
431    // Check for non-coincident edge intersections only if polygons aren't identical
432    if polygon_a.shape() != polygon_b.shape() {
433        let n_a = polygon_a.shape()[0];
434
435        // Check if any edges of B cross any edges of A (that would mean B is not contained)
436        for i in 0..n_a {
437            let j = (i + 1) % n_a;
438            let a1 = [polygon_a[[i, 0]], polygon_a[[i, 1]]];
439            let a2 = [polygon_a[[j, 0]], polygon_a[[j, 1]]];
440
441            for k in 0..n_b {
442                let l = (k + 1) % n_b;
443                let b1 = [polygon_b[[k, 0]], polygon_b[[k, 1]]];
444                let b2 = [polygon_b[[l, 0]], polygon_b[[l, 1]]];
445
446                // Only consider proper intersections (not edge overlaps)
447                if segments_intersect(&a1, &a2, &b1, &b2)
448                    && !segments_overlap(&a1, &a2, &b1, &b2, T::epsilon())
449                    && !point_on_boundary(&a1, polygon_b, T::epsilon())
450                    && !point_on_boundary(&a2, polygon_b, T::epsilon())
451                    && !point_on_boundary(&b1, polygon_a, T::epsilon())
452                    && !point_on_boundary(&b2, polygon_a, T::epsilon())
453                {
454                    return false;
455                }
456            }
457        }
458    }
459
460    true
461}
462
463/// Check if two line segments intersect.
464///
465/// # Arguments
466///
467/// * `a1`, `a2` - The endpoints of the first segment
468/// * `b1`, `b2` - The endpoints of the second segment
469///
470/// # Returns
471///
472/// * `true` if the segments intersect, `false` otherwise
473#[allow(dead_code)]
474fn segments_intersect<T: Float>(a1: &[T], a2: &[T], b1: &[T], b2: &[T]) -> bool {
475    // Function to compute orientation of triplet (p, q, r)
476    // Returns:
477    // 0 -> collinear
478    // 1 -> clockwise
479    // 2 -> counterclockwise
480    let orientation = |p: &[T], q: &[T], r: &[T]| -> i32 {
481        let val = (q[1] - p[1]) * (r[0] - q[0]) - (q[0] - p[0]) * (r[1] - q[1]);
482
483        if val < T::zero() {
484            return 1; // clockwise
485        } else if val > T::zero() {
486            return 2; // counterclockwise
487        }
488
489        0 // collinear
490    };
491
492    // Function to check if point q is on segment pr
493    let on_segment = |p: &[T], q: &[T], r: &[T]| -> bool {
494        q[0] <= p[0].max(r[0])
495            && q[0] >= p[0].min(r[0])
496            && q[1] <= p[1].max(r[1])
497            && q[1] >= p[1].min(r[1])
498    };
499
500    let o1 = orientation(a1, a2, b1);
501    let o2 = orientation(a1, a2, b2);
502    let o3 = orientation(b1, b2, a1);
503    let o4 = orientation(b1, b2, a2);
504
505    // General case
506    if o1 != o2 && o3 != o4 {
507        return true;
508    }
509
510    // Special cases
511    if o1 == 0 && on_segment(a1, b1, a2) {
512        return true;
513    }
514
515    if o2 == 0 && on_segment(a1, b2, a2) {
516        return true;
517    }
518
519    if o3 == 0 && on_segment(b1, a1, b2) {
520        return true;
521    }
522
523    if o4 == 0 && on_segment(b1, a2, b2) {
524        return true;
525    }
526
527    false
528}
529
530/// Check if two line segments overlap (share multiple points).
531///
532/// # Arguments
533///
534/// * `a1`, `a2` - The endpoints of the first segment
535/// * `b1`, `b2` - The endpoints of the second segment
536/// * `epsilon` - Tolerance for floating-point comparisons
537///
538/// # Returns
539///
540/// * `true` if the segments overlap, `false` otherwise
541#[allow(dead_code)]
542fn segments_overlap<T: Float>(a1: &[T], a2: &[T], b1: &[T], b2: &[T], epsilon: T) -> bool {
543    // Check if the segments are collinear
544    let cross = (a2[0] - a1[0]) * (b2[1] - b1[1]) - (a2[1] - a1[1]) * (b2[0] - b1[0]);
545
546    if cross.abs() > epsilon {
547        return false; // Not collinear
548    }
549
550    // Check if the segments overlap on the x-axis
551    let overlap_x = !(a2[0] < b1[0].min(b2[0]) - epsilon || a1[0] > b1[0].max(b2[0]) + epsilon);
552
553    // Check if the segments overlap on the y-axis
554    let overlap_y = !(a2[1] < b1[1].min(b2[1]) - epsilon || a1[1] > b1[1].max(b2[1]) + epsilon);
555
556    overlap_x && overlap_y
557}
558
559/// Check if a polygon is simple (non-self-intersecting).
560///
561/// # Arguments
562///
563/// * `polygon` - The polygon vertices
564///
565/// # Returns
566///
567/// * `true` if the polygon is simple, `false` otherwise
568///
569/// # Examples
570///
571/// ```
572/// use scirs2_core::ndarray::array;
573/// use scirs2_spatial::polygon::is_simple_polygon;
574///
575/// // A simple square
576/// let simple = array![
577///     [0.0, 0.0],
578///     [1.0, 0.0],
579///     [1.0, 1.0],
580///     [0.0, 1.0],
581/// ];
582///
583/// // A self-intersecting "bow tie"
584/// let complex = array![
585///     [0.0, 0.0],
586///     [1.0, 1.0],
587///     [0.0, 1.0],
588///     [1.0, 0.0],
589/// ];
590///
591/// assert!(is_simple_polygon(&simple.view()));
592/// assert!(!is_simple_polygon(&complex.view()));
593/// ```
594#[allow(dead_code)]
595pub fn is_simple_polygon<T: Float>(polygon: &ArrayView2<T>) -> bool {
596    let n = polygon.shape()[0];
597
598    if n < 3 {
599        return true; // Degenerate cases are considered simple
600    }
601
602    // Check each pair of non-adjacent edges for intersection
603    for i in 0..n {
604        let i1 = (i + 1) % n;
605
606        let a1 = [polygon[[i, 0]], polygon[[i, 1]]];
607        let a2 = [polygon[[i1, 0]], polygon[[i1, 1]]];
608
609        for j in i + 2..i + n - 1 {
610            let j_mod = j % n;
611            let j1 = (j_mod + 1) % n;
612
613            // Skip edges that share a vertex
614            if j1 == i || j_mod == i1 {
615                continue;
616            }
617
618            let b1 = [polygon[[j_mod, 0]], polygon[[j_mod, 1]]];
619            let b2 = [polygon[[j1, 0]], polygon[[j1, 1]]];
620
621            if segments_intersect(&a1, &a2, &b1, &b2) {
622                return false; // Self-intersection found
623            }
624        }
625    }
626
627    true
628}
629
630/// Compute the convex hull of a polygon using the Graham scan algorithm.
631///
632/// # Arguments
633///
634/// * `points` - The input points
635///
636/// # Returns
637///
638/// * A new array containing the convex hull vertices
639///
640/// # Examples
641///
642/// ```
643/// use scirs2_core::ndarray::array;
644/// use scirs2_spatial::polygon::convex_hull_graham;
645///
646/// // A set of points
647/// let points = array![
648///     [0.0, 0.0],
649///     [1.0, 0.0],
650///     [0.5, 0.5],  // Inside point
651///     [1.0, 1.0],
652///     [0.0, 1.0],
653/// ];
654///
655/// let hull = convex_hull_graham(&points.view());
656///
657/// // The hull should have only 4 points (the corners)
658/// assert_eq!(hull.shape()[0], 4);
659/// ```
660#[allow(dead_code)]
661pub fn convex_hull_graham<T: Float + std::fmt::Debug>(points: &ArrayView2<T>) -> Array2<T> {
662    let n = points.shape()[0];
663
664    if n <= 3 {
665        // For 3 or fewer points, all points are on the convex hull
666        return points.to_owned();
667    }
668
669    // Find the point with the lowest y-coordinate (and leftmost if tied)
670    let mut lowest = 0;
671    for i in 1..n {
672        if points[[i, 1]] < points[[lowest, 1]]
673            || (points[[i, 1]] == points[[lowest, 1]] && points[[i, 0]] < points[[lowest, 0]])
674        {
675            lowest = i;
676        }
677    }
678
679    // Pivot point
680    let pivot_x = points[[lowest, 0]];
681    let pivot_y = points[[lowest, 1]];
682
683    // Function to compute polar angle of a point relative to the pivot
684    let polar_angle = |x: T, y: T| -> T {
685        let dx = x - pivot_x;
686        let dy = y - pivot_y;
687
688        // Handle special case where points are the same
689        if dx.is_zero() && dy.is_zero() {
690            return T::neg_infinity();
691        }
692
693        // Use atan2 for the polar angle
694        dy.atan2(dx)
695    };
696
697    // Sort points by polar angle
698    let mut indexedpoints: Vec<(usize, T)> = (0..n)
699        .map(|i| (i, polar_angle(points[[i, 0]], points[[i, 1]])))
700        .collect();
701
702    indexedpoints.sort_by(|a, b| {
703        // First by polar angle
704        let angle_cmp = a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal);
705
706        if angle_cmp == std::cmp::Ordering::Equal {
707            // Break ties by distance from pivot
708            let dist_a =
709                (points[[a.0, 0]] - pivot_x).powi(2) + (points[[a.0, 1]] - pivot_y).powi(2);
710            let dist_b =
711                (points[[b.0, 0]] - pivot_x).powi(2) + (points[[b.0, 1]] - pivot_y).powi(2);
712            dist_a
713                .partial_cmp(&dist_b)
714                .unwrap_or(std::cmp::Ordering::Equal)
715        } else {
716            angle_cmp
717        }
718    });
719
720    // Function to check if three points make a right turn
721    let ccw = |i1: usize, i2: usize, i3: usize| -> bool {
722        let p1_x = points[[i1, 0]];
723        let p1_y = points[[i1, 1]];
724        let p2_x = points[[i2, 0]];
725        let p2_y = points[[i2, 1]];
726        let p3_x = points[[i3, 0]];
727        let p3_y = points[[i3, 1]];
728
729        let val = (p2_x - p1_x) * (p3_y - p1_y) - (p2_y - p1_y) * (p3_x - p1_x);
730        val > T::zero()
731    };
732
733    // Graham scan algorithm
734    let mut hull_indices = Vec::new();
735
736    // Add first three points
737    hull_indices.push(lowest);
738
739    for &(index_, _) in &indexedpoints {
740        // Skip pivot point
741        if index_ == lowest {
742            continue;
743        }
744
745        while hull_indices.len() >= 2 {
746            let top = hull_indices.len() - 1;
747            if ccw(hull_indices[top - 1], hull_indices[top], index_) {
748                break;
749            }
750            hull_indices.pop();
751        }
752
753        hull_indices.push(index_);
754    }
755
756    // Create the hull array
757    let mut hull = Array2::zeros((hull_indices.len(), 2));
758    for (i, &idx) in hull_indices.iter().enumerate() {
759        hull[[i, 0]] = points[[idx, 0]];
760        hull[[i, 1]] = points[[idx, 1]];
761    }
762
763    hull
764}
765
766/// Simplify a polygon using the Douglas-Peucker algorithm.
767///
768/// The Douglas-Peucker algorithm recursively removes vertices that contribute less
769/// than a specified tolerance to the shape of the polygon. This is useful for
770/// reducing the complexity of polygons while preserving their essential characteristics.
771///
772/// # Arguments
773///
774/// * `polygon` - The polygon vertices to simplify
775/// * `tolerance` - The perpendicular distance tolerance. Vertices that are closer
776///   than this distance to the line connecting their neighbors may be removed.
777///
778/// # Returns
779///
780/// * A simplified polygon with fewer vertices
781///
782/// # Examples
783///
784/// ```
785/// use scirs2_core::ndarray::array;
786/// use scirs2_spatial::polygon::douglas_peucker_simplify;
787///
788/// // A polygon with many points
789/// let complexpolygon = array![
790///     [0.0, 0.0],
791///     [1.0, 0.1],  // Close to the line from (0,0) to (2,0)
792///     [2.0, 0.0],
793///     [2.0, 2.0],
794///     [0.0, 2.0],
795/// ];
796///
797/// let simplified = douglas_peucker_simplify(&complexpolygon.view(), 0.2);
798///
799/// // Should remove the intermediate point at (1.0, 0.1) since it's close to the line
800/// assert!(simplified.shape()[0] < complexpolygon.shape()[0]);
801/// ```
802#[allow(dead_code)]
803pub fn douglas_peucker_simplify<T: Float + std::fmt::Debug>(
804    polygon: &ArrayView2<T>,
805    tolerance: T,
806) -> Array2<T> {
807    let n = polygon.shape()[0];
808
809    if n <= 2 {
810        return polygon.to_owned();
811    }
812
813    // Create a boolean array to mark which points to keep
814    let mut keep = vec![false; n];
815
816    // Always keep the first and last points
817    keep[0] = true;
818    keep[n - 1] = true;
819
820    // Apply Douglas-Peucker recursively
821    douglas_peucker_recursive(polygon, 0, n - 1, tolerance, &mut keep);
822
823    // Count the points to keep
824    let num_keep = keep.iter().filter(|&&x| x).count();
825
826    // Create the simplified polygon
827    let mut simplified = Array2::zeros((num_keep, 2));
828    let mut simplified_idx = 0;
829
830    for i in 0..n {
831        if keep[i] {
832            simplified[[simplified_idx, 0]] = polygon[[i, 0]];
833            simplified[[simplified_idx, 1]] = polygon[[i, 1]];
834            simplified_idx += 1;
835        }
836    }
837
838    simplified
839}
840
841/// Recursive helper function for Douglas-Peucker algorithm
842#[allow(dead_code)]
843fn douglas_peucker_recursive<T: Float>(
844    polygon: &ArrayView2<T>,
845    start: usize,
846    end: usize,
847    tolerance: T,
848    keep: &mut [bool],
849) {
850    if end <= start + 1 {
851        return;
852    }
853
854    // Find the point with the maximum perpendicular distance from the line
855    let mut max_dist = T::zero();
856    let mut max_idx = start;
857
858    let startpoint = [polygon[[start, 0]], polygon[[start, 1]]];
859    let endpoint = [polygon[[end, 0]], polygon[[end, 1]]];
860
861    for i in start + 1..end {
862        let point = [polygon[[i, 0]], polygon[[i, 1]]];
863        let dist = perpendicular_distance(&point, &startpoint, &endpoint);
864
865        if dist > max_dist {
866            max_dist = dist;
867            max_idx = i;
868        }
869    }
870
871    // If the maximum distance is greater than tolerance, keep the point and recurse
872    if max_dist > tolerance {
873        keep[max_idx] = true;
874        douglas_peucker_recursive(polygon, start, max_idx, tolerance, keep);
875        douglas_peucker_recursive(polygon, max_idx, end, tolerance, keep);
876    }
877}
878
879/// Calculate the perpendicular distance from a point to a line segment
880#[allow(dead_code)]
881fn perpendicular_distance<T: Float>(point: &[T; 2], line_start: &[T; 2], lineend: &[T; 2]) -> T {
882    let dx = lineend[0] - line_start[0];
883    let dy = lineend[1] - line_start[1];
884
885    // If the line segment is actually a point, return distance to that point
886    if dx.is_zero() && dy.is_zero() {
887        let px = point[0] - line_start[0];
888        let py = point[1] - line_start[1];
889        return (px * px + py * py).sqrt();
890    }
891
892    // Calculate the perpendicular distance using the cross product formula
893    let numerator = ((dy * (point[0] - line_start[0])) - (dx * (point[1] - line_start[1]))).abs();
894    let denominator = (dx * dx + dy * dy).sqrt();
895
896    numerator / denominator
897}
898
899/// Simplify a polygon using the Visvalingam-Whyatt algorithm.
900///
901/// The Visvalingam-Whyatt algorithm removes vertices by calculating the area of
902/// triangles formed by consecutive triplets of vertices and removing vertices
903/// that form triangles with areas smaller than a threshold.
904///
905/// # Arguments
906///
907/// * `polygon` - The polygon vertices to simplify
908/// * `min_area` - The minimum triangle area threshold. Vertices forming triangles
909///   with areas smaller than this will be candidates for removal.
910///
911/// # Returns
912///
913/// * A simplified polygon with fewer vertices
914///
915/// # Examples
916///
917/// ```
918/// use scirs2_core::ndarray::array;
919/// use scirs2_spatial::polygon::visvalingam_whyatt_simplify;
920///
921/// // A polygon with some redundant vertices
922/// let polygon = array![
923///     [0.0, 0.0],
924///     [1.0, 0.0],
925///     [1.01, 0.01],  // Forms a very small triangle
926///     [2.0, 0.0],
927///     [2.0, 2.0],
928///     [0.0, 2.0],
929/// ];
930///
931/// let simplified = visvalingam_whyatt_simplify(&polygon.view(), 0.1);
932///
933/// // Should remove vertices that form very small triangles
934/// assert!(simplified.shape()[0] <= polygon.shape()[0]);
935/// ```
936#[allow(dead_code)]
937pub fn visvalingam_whyatt_simplify<T: Float + std::fmt::Debug>(
938    polygon: &ArrayView2<T>,
939    min_area: T,
940) -> Array2<T> {
941    let n = polygon.shape()[0];
942
943    if n <= 3 {
944        return polygon.to_owned();
945    }
946
947    // Create a list of vertices with their effective areas
948    let mut vertices: Vec<(usize, T)> = Vec::new();
949    let mut active = vec![true; n];
950
951    // Calculate initial areas for all vertices
952    for i in 0..n {
953        let area = calculate_triangle_area(polygon, i, &active);
954        vertices.push((i, area));
955    }
956
957    // Sort by _area (smallest first)
958    vertices.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
959
960    // Remove vertices with areas smaller than threshold
961    let removal_candidates: Vec<usize> = vertices
962        .iter()
963        .filter(|(_, area)| *area < min_area)
964        .map(|(idx_, _)| *idx_)
965        .collect();
966
967    for vertex_idx in removal_candidates {
968        if count_active(&active) > 3 && active[vertex_idx] {
969            active[vertex_idx] = false;
970
971            // Recalculate areas for neighboring vertices
972            let prev = find_previous_active(vertex_idx, &active, n);
973            let next = find_next_active(vertex_idx, &active, n);
974
975            if let (Some(prev_idx), Some(next_idx)) = (prev, next) {
976                // Update the _area for the previous vertex
977                update_vertex_area(polygon, prev_idx, &active, &mut vertices);
978                // Update the _area for the next vertex
979                update_vertex_area(polygon, next_idx, &active, &mut vertices);
980            }
981        }
982    }
983
984    // Create the simplified polygon
985    let num_active = count_active(&active);
986    let mut simplified = Array2::zeros((num_active, 2));
987    let mut simplified_idx = 0;
988
989    for i in 0..n {
990        if active[i] {
991            simplified[[simplified_idx, 0]] = polygon[[i, 0]];
992            simplified[[simplified_idx, 1]] = polygon[[i, 1]];
993            simplified_idx += 1;
994        }
995    }
996
997    simplified
998}
999
1000/// Calculate the area of triangle formed by vertex i and its active neighbors
1001#[allow(dead_code)]
1002fn calculate_triangle_area<T: Float>(
1003    polygon: &ArrayView2<T>,
1004    vertex_idx: usize,
1005    active: &[bool],
1006) -> T {
1007    let n = polygon.shape()[0];
1008
1009    let prev = find_previous_active(vertex_idx, active, n);
1010    let next = find_next_active(vertex_idx, active, n);
1011
1012    match (prev, next) {
1013        (Some(prev_idx), Some(next_idx)) => {
1014            let p1 = [polygon[[prev_idx, 0]], polygon[[prev_idx, 1]]];
1015            let p2 = [polygon[[vertex_idx, 0]], polygon[[vertex_idx, 1]]];
1016            let p3 = [polygon[[next_idx, 0]], polygon[[next_idx, 1]]];
1017
1018            triangle_area(&p1, &p2, &p3)
1019        }
1020        _ => T::infinity(), // End vertices have infinite area (never removed)
1021    }
1022}
1023
1024/// Calculate the area of a triangle given three points
1025#[allow(dead_code)]
1026fn triangle_area<T: Float>(p1: &[T; 2], p2: &[T; 2], p3: &[T; 2]) -> T {
1027    ((p1[0] * (p2[1] - p3[1]) + p2[0] * (p3[1] - p1[1]) + p3[0] * (p1[1] - p2[1]))
1028        / (T::one() + T::one()))
1029    .abs()
1030}
1031
1032/// Find the previous active vertex (wrapping around)
1033#[allow(dead_code)]
1034fn find_previous_active(current: usize, active: &[bool], n: usize) -> Option<usize> {
1035    for i in 1..n {
1036        let idx = (current + n - i) % n;
1037        if active[idx] && idx != current {
1038            return Some(idx);
1039        }
1040    }
1041    None
1042}
1043
1044/// Find the next active vertex (wrapping around)
1045#[allow(dead_code)]
1046fn find_next_active(current: usize, active: &[bool], n: usize) -> Option<usize> {
1047    for i in 1..n {
1048        let idx = (current + i) % n;
1049        if active[idx] && idx != current {
1050            return Some(idx);
1051        }
1052    }
1053    None
1054}
1055
1056/// Count the number of active vertices
1057#[allow(dead_code)]
1058fn count_active(active: &[bool]) -> usize {
1059    active.iter().filter(|&&x| x).count()
1060}
1061
1062/// Update the area for a specific vertex in the vertices list
1063#[allow(dead_code)]
1064fn update_vertex_area<T: Float + std::fmt::Debug>(
1065    polygon: &ArrayView2<T>,
1066    vertex_idx: usize,
1067    active: &[bool],
1068    vertices: &mut [(usize, T)],
1069) {
1070    let new_area = calculate_triangle_area(polygon, vertex_idx, active);
1071
1072    // Find and update the vertex in the list
1073    for (_idx, area) in vertices.iter_mut() {
1074        if *_idx == vertex_idx {
1075            *area = new_area;
1076            break;
1077        }
1078    }
1079}
1080
1081#[cfg(test)]
1082mod tests {
1083    use super::*;
1084    use approx::assert_relative_eq;
1085    use scirs2_core::ndarray::array;
1086
1087    #[test]
1088    fn testpoint_in_polygon() {
1089        // Simple square
1090        let square = array![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0],];
1091
1092        // Points inside
1093        assert!(point_in_polygon(&[0.5, 0.5], &square.view()));
1094        assert!(point_in_polygon(&[0.1, 0.1], &square.view()));
1095        assert!(point_in_polygon(&[0.9, 0.9], &square.view()));
1096
1097        // Points outside
1098        assert!(!point_in_polygon(&[1.5, 0.5], &square.view()));
1099        assert!(!point_in_polygon(&[-0.5, 0.5], &square.view()));
1100        assert!(!point_in_polygon(&[0.5, 1.5], &square.view()));
1101        assert!(!point_in_polygon(&[0.5, -0.5], &square.view()));
1102
1103        // Points on boundary (considered inside)
1104        assert!(point_in_polygon(&[0.0, 0.5], &square.view()));
1105        assert!(point_in_polygon(&[1.0, 0.5], &square.view()));
1106        assert!(point_in_polygon(&[0.5, 0.0], &square.view()));
1107        assert!(point_in_polygon(&[0.5, 1.0], &square.view()));
1108
1109        // More complex polygon (concave)
1110        let concave = array![[0.0, 0.0], [2.0, 0.0], [2.0, 2.0], [1.0, 1.0], [0.0, 2.0],];
1111
1112        // Inside the concave part - this point is actually outside the polygon
1113        // Modified test to assert the correct result
1114        assert!(!point_in_polygon(&[1.0, 1.5], &concave.view()));
1115
1116        // Inside the convex part
1117        assert!(point_in_polygon(&[0.5, 0.5], &concave.view()));
1118
1119        // Outside (but inside the bounding box)
1120        // For the given concave shape, point (1.5, 0.5) is actually inside
1121        assert!(point_in_polygon(&[1.5, 0.5], &concave.view()));
1122    }
1123
1124    #[test]
1125    fn testpoint_on_boundary() {
1126        // Simple square
1127        let square = array![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0],];
1128
1129        let epsilon = 1e-10;
1130
1131        // Points on boundary
1132        assert!(point_on_boundary(&[0.0, 0.5], &square.view(), epsilon));
1133        assert!(point_on_boundary(&[1.0, 0.5], &square.view(), epsilon));
1134        assert!(point_on_boundary(&[0.5, 0.0], &square.view(), epsilon));
1135        assert!(point_on_boundary(&[0.5, 1.0], &square.view(), epsilon));
1136
1137        // Corner points
1138        assert!(point_on_boundary(&[0.0, 0.0], &square.view(), epsilon));
1139        assert!(point_on_boundary(&[1.0, 0.0], &square.view(), epsilon));
1140        assert!(point_on_boundary(&[1.0, 1.0], &square.view(), epsilon));
1141        assert!(point_on_boundary(&[0.0, 1.0], &square.view(), epsilon));
1142
1143        // Points not on boundary
1144        assert!(!point_on_boundary(&[0.5, 0.5], &square.view(), epsilon));
1145        assert!(!point_on_boundary(&[1.5, 0.5], &square.view(), epsilon));
1146        assert!(!point_on_boundary(&[0.0, 2.0], &square.view(), epsilon));
1147
1148        // Points near but not exactly on boundary should still be detected
1149        // with a larger epsilon
1150        assert!(point_on_boundary(&[0.0, 0.5 + 1e-5], &square.view(), 1e-4));
1151    }
1152
1153    #[test]
1154    fn testpolygon_area() {
1155        // Square with area 1
1156        let square = array![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0],];
1157
1158        assert_relative_eq!(polygon_area(&square.view()), 1.0, epsilon = 1e-10);
1159
1160        // Rectangle with area 6
1161        let rectangle = array![[0.0, 0.0], [3.0, 0.0], [3.0, 2.0], [0.0, 2.0],];
1162
1163        assert_relative_eq!(polygon_area(&rectangle.view()), 6.0, epsilon = 1e-10);
1164
1165        // Triangle with area 0.5
1166        let triangle = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0],];
1167
1168        assert_relative_eq!(polygon_area(&triangle.view()), 0.5, epsilon = 1e-10);
1169
1170        // L-shaped polygon
1171        let lshape = array![
1172            [0.0, 0.0],
1173            [2.0, 0.0],
1174            [2.0, 1.0],
1175            [1.0, 1.0],
1176            [1.0, 2.0],
1177            [0.0, 2.0],
1178        ];
1179
1180        assert_relative_eq!(polygon_area(&lshape.view()), 3.0, epsilon = 1e-10);
1181    }
1182
1183    #[test]
1184    fn testpolygon_centroid() {
1185        // Square with centroid at (0.5, 0.5)
1186        let square = array![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0],];
1187
1188        let centroid = polygon_centroid(&square.view());
1189        assert_relative_eq!(centroid[0], 0.5, epsilon = 1e-10);
1190        assert_relative_eq!(centroid[1], 0.5, epsilon = 1e-10);
1191
1192        // Rectangle with centroid at (1.5, 1.0)
1193        let rectangle = array![[0.0, 0.0], [3.0, 0.0], [3.0, 2.0], [0.0, 2.0],];
1194
1195        let centroid = polygon_centroid(&rectangle.view());
1196        assert_relative_eq!(centroid[0], 1.5, epsilon = 1e-10);
1197        assert_relative_eq!(centroid[1], 1.0, epsilon = 1e-10);
1198
1199        // Triangle with centroid at (1/3, 1/3)
1200        let triangle = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0],];
1201
1202        let centroid = polygon_centroid(&triangle.view());
1203        assert_relative_eq!(centroid[0], 1.0 / 3.0, epsilon = 1e-10);
1204        assert_relative_eq!(centroid[1], 1.0 / 3.0, epsilon = 1e-10);
1205    }
1206
1207    #[test]
1208    fn testpolygon_contains_polygon() {
1209        // Outer square
1210        let outer = array![[0.0, 0.0], [3.0, 0.0], [3.0, 3.0], [0.0, 3.0],];
1211
1212        // Inner square
1213        let inner = array![[1.0, 1.0], [2.0, 1.0], [2.0, 2.0], [1.0, 2.0],];
1214
1215        // Outer should contain inner
1216        assert!(polygon_contains_polygon(&outer.view(), &inner.view()));
1217
1218        // Inner should not contain outer
1219        assert!(!polygon_contains_polygon(&inner.view(), &outer.view()));
1220
1221        // Overlapping squares
1222        let overlap = array![[2.0, 2.0], [4.0, 2.0], [4.0, 4.0], [2.0, 4.0],];
1223
1224        // Neither should fully contain the other
1225        assert!(!polygon_contains_polygon(&outer.view(), &overlap.view()));
1226        assert!(!polygon_contains_polygon(&overlap.view(), &outer.view()));
1227
1228        // A polygon contains itself
1229        assert!(polygon_contains_polygon(&outer.view(), &outer.view()));
1230    }
1231
1232    #[test]
1233    fn test_is_simple_polygon() {
1234        // Simple square (non-self-intersecting)
1235        let square = array![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0],];
1236
1237        assert!(is_simple_polygon(&square.view()));
1238
1239        // Self-intersecting bow tie
1240        let bowtie = array![[0.0, 0.0], [1.0, 1.0], [0.0, 1.0], [1.0, 0.0],];
1241
1242        assert!(!is_simple_polygon(&bowtie.view()));
1243
1244        // More complex non-self-intersecting polygon
1245        let complex = array![[0.0, 0.0], [2.0, 0.0], [2.0, 2.0], [1.0, 1.0], [0.0, 2.0],];
1246
1247        assert!(is_simple_polygon(&complex.view()));
1248    }
1249
1250    #[test]
1251    fn test_convex_hull_graham() {
1252        // Simple test with a square and a point inside
1253        let points = array![
1254            [0.0, 0.0],
1255            [1.0, 0.0],
1256            [0.5, 0.5], // This point should not be in the hull
1257            [1.0, 1.0],
1258            [0.0, 1.0],
1259        ];
1260
1261        let hull = convex_hull_graham(&points.view());
1262
1263        // The hull should have only 4 points (the corners)
1264        assert_eq!(hull.shape()[0], 4);
1265
1266        // Check that the inside point is not in the hull
1267        let mut found_inside = false;
1268        for i in 0..hull.shape()[0] {
1269            if (hull[[i, 0]] - 0.5).abs() < 1e-10 && (hull[[i, 1]] - 0.5).abs() < 1e-10 {
1270                found_inside = true;
1271                break;
1272            }
1273        }
1274        assert!(!found_inside);
1275
1276        // Test with points in a circle-like arrangement
1277        let mut circlepoints = Array2::zeros((8, 2));
1278        for i in 0..8 {
1279            let angle = 2.0 * std::f64::consts::PI * (i as f64) / 8.0;
1280            circlepoints[[i, 0]] = angle.cos();
1281            circlepoints[[i, 1]] = angle.sin();
1282        }
1283
1284        // Add some interior points
1285        let allpoints = array![
1286            [0.0, 0.0],  // Center point
1287            [0.5, 0.0],  // Interior point
1288            [0.0, 0.5],  // Interior point
1289            [-0.5, 0.0], // Interior point
1290            [0.0, -0.5], // Interior point
1291            // Add the circle points
1292            [1.0, 0.0],
1293            [
1294                std::f64::consts::FRAC_1_SQRT_2,
1295                std::f64::consts::FRAC_1_SQRT_2
1296            ],
1297            [0.0, 1.0],
1298            [
1299                -std::f64::consts::FRAC_1_SQRT_2,
1300                std::f64::consts::FRAC_1_SQRT_2
1301            ],
1302            [-1.0, 0.0],
1303            [
1304                -std::f64::consts::FRAC_1_SQRT_2,
1305                -std::f64::consts::FRAC_1_SQRT_2
1306            ],
1307            [0.0, -1.0],
1308            [
1309                std::f64::consts::FRAC_1_SQRT_2,
1310                -std::f64::consts::FRAC_1_SQRT_2
1311            ],
1312        ];
1313
1314        let hull = convex_hull_graham(&allpoints.view());
1315
1316        // The hull should have 8 points (the circle points)
1317        assert_eq!(hull.shape()[0], 8);
1318    }
1319}