scirs2_spatial/
alphashapes.rs

1//! Alpha shapes implementation for spatial analysis
2//!
3//! Alpha shapes are a generalization of convex hulls that allow for
4//! non-convex boundaries by controlling the "tightness" of the shape
5//! around a set of points through the alpha parameter.
6
7use crate::error::{SpatialError, SpatialResult};
8use scirs2_core::ndarray::{Array2, ArrayView1, ArrayView2};
9
10/// Alpha shape representation for point cloud analysis
11///
12/// Alpha shapes provide a way to compute non-convex boundaries around
13/// a set of points, controlled by the alpha parameter.
14#[derive(Debug, Clone)]
15pub struct AlphaShape {
16    /// Points defining the alpha shape
17    pub points: Array2<f64>,
18    /// Alpha parameter controlling shape tightness
19    pub alpha: f64,
20    /// Boundary edges of the alpha shape
21    pub edges: Vec<(usize, usize)>,
22    /// Triangles in the alpha shape (if applicable)
23    pub triangles: Vec<(usize, usize, usize)>,
24}
25
26impl AlphaShape {
27    /// Create a new alpha shape from a set of points
28    ///
29    /// # Arguments
30    ///
31    /// * `points` - Input points as a 2D array
32    /// * `alpha` - Alpha parameter controlling shape tightness
33    ///
34    /// # Returns
35    ///
36    /// * Result containing the computed alpha shape
37    pub fn new(points: &ArrayView2<'_, f64>, alpha: f64) -> SpatialResult<Self> {
38        if points.is_empty() {
39            return Err(SpatialError::ValueError(
40                "Points array cannot be empty".to_string(),
41            ));
42        }
43
44        if alpha <= 0.0 {
45            return Err(SpatialError::ValueError(
46                "Alpha parameter must be positive".to_string(),
47            ));
48        }
49
50        // Basic implementation - create a minimal alpha shape
51        // In a full implementation, this would use Delaunay triangulation
52        // and alpha-complex computation
53        let points_owned = points.to_owned();
54        let n_points = points_owned.nrows();
55
56        // For now, create a simple convex hull-like boundary
57        let mut edges = Vec::new();
58        let mut triangles = Vec::new();
59
60        // Simple boundary for demonstration
61        if n_points >= 3 {
62            for i in 0..n_points {
63                edges.push((i, (i + 1) % n_points));
64            }
65
66            // Add a single triangle if we have exactly 3 points
67            if n_points == 3 {
68                triangles.push((0, 1, 2));
69            }
70        }
71
72        Ok(AlphaShape {
73            points: points_owned,
74            alpha,
75            edges,
76            triangles,
77        })
78    }
79
80    /// Get the number of points in the alpha shape
81    pub fn num_points(&self) -> usize {
82        self.points.nrows()
83    }
84
85    /// Get the number of edges in the alpha shape boundary
86    pub fn num_edges(&self) -> usize {
87        self.edges.len()
88    }
89
90    /// Get the number of triangles in the alpha shape
91    pub fn num_triangles(&self) -> usize {
92        self.triangles.len()
93    }
94
95    /// Check if a point is inside the alpha shape
96    ///
97    /// # Arguments
98    ///
99    /// * `point` - Point to test
100    ///
101    /// # Returns
102    ///
103    /// * True if the point is inside the alpha shape
104    pub fn contains_point(&self, point: &ArrayView1<f64>) -> bool {
105        // Simplified implementation - just check if point is close to any input point
106        if point.len() != self.points.ncols() {
107            return false;
108        }
109
110        let tolerance = self.alpha;
111        for i in 0..self.points.nrows() {
112            let dist: f64 = point
113                .iter()
114                .zip(self.points.row(i).iter())
115                .map(|(&a, &b)| (a - b).powi(2))
116                .sum::<f64>()
117                .sqrt();
118
119            if dist <= tolerance {
120                return true;
121            }
122        }
123
124        false
125    }
126
127    /// Compute the area/volume of the alpha shape
128    ///
129    /// # Returns
130    ///
131    /// * Area (2D) or volume (3D) of the alpha shape
132    pub fn area(&self) -> f64 {
133        // Simplified implementation
134        // In a full implementation, this would compute the actual area/volume
135        // based on the triangulation
136
137        if self.triangles.is_empty() {
138            return 0.0;
139        }
140
141        // Simple area calculation for triangles in 2D
142        if self.points.ncols() == 2 {
143            let mut total_area = 0.0;
144            for &(i, j, k) in &self.triangles {
145                let p1 = self.points.row(i);
146                let p2 = self.points.row(j);
147                let p3 = self.points.row(k);
148
149                // Triangle area using cross product
150                let area = 0.5
151                    * ((p2[0] - p1[0]) * (p3[1] - p1[1]) - (p3[0] - p1[0]) * (p2[1] - p1[1])).abs();
152                total_area += area;
153            }
154            return total_area;
155        }
156
157        // For 3D and higher dimensions, return a placeholder
158        1.0
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use scirs2_core::ndarray::array;
166
167    #[test]
168    fn test_alpha_shape_creation() {
169        let points = array![[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]];
170        let alpha_shape = AlphaShape::new(&points.view(), 1.0);
171        assert!(alpha_shape.is_ok());
172
173        let shape = alpha_shape.unwrap();
174        assert_eq!(shape.num_points(), 3);
175        assert_eq!(shape.alpha, 1.0);
176    }
177
178    #[test]
179    fn test_alpha_shape_contains() {
180        let points = array![[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]];
181        let alpha_shape = AlphaShape::new(&points.view(), 0.5).unwrap();
182
183        let test_point = array![0.1, 0.1];
184        let contains = alpha_shape.contains_point(&test_point.view());
185        assert!(contains);
186
187        let far_point = array![10.0, 10.0];
188        let contains_far = alpha_shape.contains_point(&far_point.view());
189        assert!(!contains_far);
190    }
191
192    #[test]
193    fn test_alpha_shape_area() {
194        let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
195        let alpha_shape = AlphaShape::new(&points.view(), 1.0).unwrap();
196
197        let area = alpha_shape.area();
198        assert!(area >= 0.0);
199    }
200
201    #[test]
202    fn test_invalid_alpha() {
203        let points = array![[0.0, 0.0], [1.0, 0.0]];
204        let result = AlphaShape::new(&points.view(), -1.0);
205        assert!(result.is_err());
206    }
207
208    #[test]
209    fn test_empty_points() {
210        let points = Array2::<f64>::zeros((0, 2));
211        let result = AlphaShape::new(&points.view(), 1.0);
212        assert!(result.is_err());
213    }
214}