scirs2_spatial/convex_hull/algorithms/
graham_scan.rs1use crate::convex_hull::core::ConvexHull;
8use crate::convex_hull::geometry::calculations_2d::{compute_2d_hull_equations, cross_product_2d};
9use crate::error::{SpatialError, SpatialResult};
10use qhull::Qh;
11use scirs2_core::ndarray::ArrayView2;
12
13pub fn compute_graham_scan(points: &ArrayView2<'_, f64>) -> SpatialResult<ConvexHull> {
45 let npoints = points.nrows();
46
47 if points.ncols() != 2 {
48 return Err(SpatialError::ValueError(
49 "Graham scan algorithm only supports 2D points".to_string(),
50 ));
51 }
52
53 if npoints < 3 {
54 return Err(SpatialError::ValueError(
55 "Need at least 3 points for 2D convex hull".to_string(),
56 ));
57 }
58
59 let mut indexed_points: Vec<(usize, [f64; 2])> = (0..npoints)
61 .map(|i| (i, [points[[i, 0]], points[[i, 1]]]))
62 .collect();
63
64 let start_idx = indexed_points
66 .iter()
67 .min_by(|a, b| {
68 let cmp = a.1[1].partial_cmp(&b.1[1]).unwrap();
69 if cmp == std::cmp::Ordering::Equal {
70 a.1[0].partial_cmp(&b.1[0]).unwrap()
71 } else {
72 cmp
73 }
74 })
75 .unwrap()
76 .0;
77
78 let start_point = indexed_points
79 .iter()
80 .find(|(idx, _)| *idx == start_idx)
81 .unwrap()
82 .1;
83
84 indexed_points.sort_by(|a, b| {
86 if a.0 == start_idx {
87 return std::cmp::Ordering::Less;
88 }
89 if b.0 == start_idx {
90 return std::cmp::Ordering::Greater;
91 }
92
93 let angle_a = (a.1[1] - start_point[1]).atan2(a.1[0] - start_point[0]);
94 let angle_b = (b.1[1] - start_point[1]).atan2(b.1[0] - start_point[0]);
95
96 let angle_cmp = angle_a.partial_cmp(&angle_b).unwrap();
97 if angle_cmp == std::cmp::Ordering::Equal {
98 let dist_a = (a.1[0] - start_point[0]).powi(2) + (a.1[1] - start_point[1]).powi(2);
100 let dist_b = (b.1[0] - start_point[0]).powi(2) + (b.1[1] - start_point[1]).powi(2);
101 dist_a.partial_cmp(&dist_b).unwrap()
102 } else {
103 angle_cmp
104 }
105 });
106
107 let mut stack: Vec<usize> = Vec::new();
109
110 for (point_idx, point) in indexed_points {
111 while stack.len() >= 2 {
113 let top = stack[stack.len() - 1];
114 let second = stack[stack.len() - 2];
115
116 let p1 = [points[[second, 0]], points[[second, 1]]];
117 let p2 = [points[[top, 0]], points[[top, 1]]];
118 let p3 = point;
119
120 if cross_product_2d(p1, p2, p3) <= 0.0 {
121 stack.pop();
122 } else {
123 break;
124 }
125 }
126 stack.push(point_idx);
127 }
128
129 let vertex_indices = stack;
130
131 let n = vertex_indices.len();
133 let mut simplices = Vec::new();
134 for i in 0..n {
135 let j = (i + 1) % n;
136 simplices.push(vec![vertex_indices[i], vertex_indices[j]]);
137 }
138
139 let points_vec: Vec<Vec<f64>> = (0..npoints).map(|i| points.row(i).to_vec()).collect();
141 let qh = Qh::builder()
142 .compute(false)
143 .build_from_iter(points_vec)
144 .map_err(|e| SpatialError::ComputationError(format!("Qhull error: {e}")))?;
145
146 let equations = compute_2d_hull_equations(points, &vertex_indices);
148
149 Ok(ConvexHull {
150 points: points.to_owned(),
151 qh,
152 vertex_indices,
153 simplices,
154 equations: Some(equations),
155 })
156}
157
158pub fn find_start_point(points: &ArrayView2<'_, f64>) -> usize {
182 let npoints = points.nrows();
183 let mut start_idx = 0;
184
185 for i in 1..npoints {
186 let current_y = points[[i, 1]];
187 let start_y = points[[start_idx, 1]];
188
189 if current_y < start_y || (current_y == start_y && points[[i, 0]] < points[[start_idx, 0]])
190 {
191 start_idx = i;
192 }
193 }
194
195 start_idx
196}
197
198pub fn sort_by_polar_angle(points: &ArrayView2<'_, f64>, reference_point: [f64; 2]) -> Vec<usize> {
221 let npoints = points.nrows();
222 let mut indexed_points: Vec<(usize, f64, f64)> = Vec::new();
223
224 for i in 0..npoints {
225 let point = [points[[i, 0]], points[[i, 1]]];
226 let angle = (point[1] - reference_point[1]).atan2(point[0] - reference_point[0]);
227 let distance_sq =
228 (point[0] - reference_point[0]).powi(2) + (point[1] - reference_point[1]).powi(2);
229 indexed_points.push((i, angle, distance_sq));
230 }
231
232 indexed_points.sort_by(|a, b| {
234 let angle_cmp = a.1.partial_cmp(&b.1).unwrap();
235 if angle_cmp == std::cmp::Ordering::Equal {
236 a.2.partial_cmp(&b.2).unwrap()
237 } else {
238 angle_cmp
239 }
240 });
241
242 indexed_points.into_iter().map(|(idx, _, _)| idx).collect()
243}
244
245pub fn is_ccw_turn(p1: [f64; 2], p2: [f64; 2], p3: [f64; 2]) -> bool {
269 cross_product_2d(p1, p2, p3) > 0.0
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275 use scirs2_core::ndarray::arr2;
276
277 #[test]
278 fn test_graham_scan_basic() {
279 let points = arr2(&[
280 [0.0, 0.0],
281 [1.0, 0.0],
282 [0.0, 1.0],
283 [0.5, 0.5], ]);
285
286 let hull = compute_graham_scan(&points.view()).unwrap();
287
288 assert_eq!(hull.ndim(), 2);
290
291 assert_eq!(hull.vertex_indices().len(), 3);
293
294 assert!(!hull.vertex_indices().contains(&3));
296 }
297
298 #[test]
299 fn test_graham_scan_square() {
300 let points = arr2(&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]);
301
302 let hull = compute_graham_scan(&points.view()).unwrap();
303
304 assert_eq!(hull.ndim(), 2);
305 assert_eq!(hull.vertex_indices().len(), 4); }
307
308 #[test]
309 fn test_find_start_point() {
310 let points = arr2(&[[1.0, 1.0], [0.0, 0.0], [2.0, 0.0], [0.0, 1.0]]);
311 let start_idx = find_start_point(&points.view());
312 assert_eq!(start_idx, 1); }
314
315 #[test]
316 fn test_sort_by_polar_angle() {
317 let points = arr2(&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]);
318 let reference = [0.0, 0.0];
319 let sorted_indices = sort_by_polar_angle(&points.view(), reference);
320
321 assert_eq!(sorted_indices.len(), 4);
322 assert_eq!(sorted_indices[0], 0); }
324
325 #[test]
326 fn test_is_ccw_turn() {
327 let p1 = [0.0, 0.0];
328 let p2 = [1.0, 0.0];
329 let p3 = [0.0, 1.0];
330
331 assert!(is_ccw_turn(p1, p2, p3)); assert!(!is_ccw_turn(p1, p3, p2)); }
334
335 #[test]
336 fn test_error_cases() {
337 let too_few = arr2(&[[0.0, 0.0], [1.0, 0.0]]);
339 let result = compute_graham_scan(&too_few.view());
340 assert!(result.is_err());
341
342 let points_3d = arr2(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]);
344 let result = compute_graham_scan(&points_3d.view());
345 assert!(result.is_err());
346 }
347
348 #[test]
349 fn test_collinear_points() {
350 let points = arr2(&[
352 [0.0, 0.0],
353 [1.0, 0.0],
354 [2.0, 0.0],
355 [0.5, 1.0], ]);
357
358 let hull = compute_graham_scan(&points.view()).unwrap();
359
360 assert_eq!(hull.vertex_indices().len(), 3);
362 }
363}