scirs2_spatial/convex_hull/algorithms/jarvis_march.rs
1//! Jarvis march (Gift Wrapping) algorithm implementation for 2D convex hull computation
2//!
3//! The Jarvis march algorithm, also known as the Gift Wrapping algorithm, computes
4//! the convex hull by starting from the leftmost point and "wrapping" around the
5//! point set by repeatedly finding the most counterclockwise point.
6
7use crate::convex_hull::core::ConvexHull;
8use crate::convex_hull::geometry::calculations_2d::{
9 compute_2d_hull_equations, cross_product_2d, distance_squared_2d,
10};
11use crate::error::{SpatialError, SpatialResult};
12use qhull::Qh;
13use scirs2_core::ndarray::ArrayView2;
14
15/// Compute convex hull using Jarvis march (Gift Wrapping) algorithm (2D only)
16///
17/// The Jarvis march algorithm works by:
18/// 1. Finding the leftmost point
19/// 2. From each hull point, finding the most counterclockwise point
20/// 3. Continuing until we wrap back to the starting point
21///
22/// Time complexity: O(nh) where n is the number of points and h is the number of hull points.
23/// This makes it optimal for small hull sizes.
24///
25/// # Arguments
26///
27/// * `points` - Input points (shape: npoints x 2)
28///
29/// # Returns
30///
31/// * Result containing a ConvexHull instance or an error
32///
33/// # Errors
34///
35/// * Returns error if fewer than 3 points are provided
36/// * Only works for 2D points
37///
38/// # Examples
39///
40/// ```rust
41/// use scirs2_spatial::convex_hull::algorithms::jarvis_march::compute_jarvis_march;
42/// use scirs2_core::ndarray::array;
43///
44/// let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [0.5, 0.5]];
45/// let hull = compute_jarvis_march(&points.view()).unwrap();
46/// assert_eq!(hull.ndim(), 2);
47/// assert_eq!(hull.vertex_indices().len(), 3); // Excludes interior point
48/// ```
49pub fn compute_jarvis_march(points: &ArrayView2<'_, f64>) -> SpatialResult<ConvexHull> {
50 let npoints = points.nrows();
51
52 if points.ncols() != 2 {
53 return Err(SpatialError::ValueError(
54 "Jarvis march algorithm only supports 2D points".to_string(),
55 ));
56 }
57
58 if npoints < 3 {
59 return Err(SpatialError::ValueError(
60 "Need at least 3 points for 2D convex hull".to_string(),
61 ));
62 }
63
64 // Find the leftmost point
65 let mut leftmost = 0;
66 for i in 1..npoints {
67 if points[[i, 0]] < points[[leftmost, 0]] {
68 leftmost = i;
69 }
70 }
71
72 let mut hull_vertices = Vec::new();
73 let mut current = leftmost;
74
75 loop {
76 hull_vertices.push(current);
77
78 // Find the most counterclockwise point from current
79 let mut next = (current + 1) % npoints;
80
81 for i in 0..npoints {
82 if i == current {
83 continue;
84 }
85
86 let p1 = [points[[current, 0]], points[[current, 1]]];
87 let p2 = [points[[next, 0]], points[[next, 1]]];
88 let p3 = [points[[i, 0]], points[[i, 1]]];
89
90 let cross = cross_product_2d(p1, p2, p3);
91
92 // If cross product is positive, i is more counterclockwise than next
93 if cross > 0.0
94 || (cross == 0.0 && distance_squared_2d(p1, p3) > distance_squared_2d(p1, p2))
95 {
96 next = i;
97 }
98 }
99
100 current = next;
101 if current == leftmost {
102 break; // We've wrapped around to the start
103 }
104 }
105
106 let vertex_indices = hull_vertices;
107
108 // Create simplices (edges for 2D hull)
109 let n = vertex_indices.len();
110 let mut simplices = Vec::new();
111 for i in 0..n {
112 let j = (i + 1) % n;
113 simplices.push(vec![vertex_indices[i], vertex_indices[j]]);
114 }
115
116 // Create a dummy QHull instance for compatibility
117 let points_vec: Vec<Vec<f64>> = (0..npoints).map(|i| points.row(i).to_vec()).collect();
118 let qh = Qh::builder()
119 .compute(false)
120 .build_from_iter(points_vec)
121 .map_err(|e| SpatialError::ComputationError(format!("Qhull error: {e}")))?;
122
123 // Compute facet equations for 2D hull
124 let equations = compute_2d_hull_equations(points, &vertex_indices);
125
126 Ok(ConvexHull {
127 points: points.to_owned(),
128 qh,
129 vertex_indices,
130 simplices,
131 equations: Some(equations),
132 })
133}
134
135/// Find the leftmost point in the point set
136///
137/// In case of ties (multiple points with the same x-coordinate),
138/// returns the first one encountered.
139///
140/// # Arguments
141///
142/// * `points` - Input points array
143///
144/// # Returns
145///
146/// * Index of the leftmost point
147///
148/// # Examples
149///
150/// ```rust
151/// use scirs2_spatial::convex_hull::algorithms::jarvis_march::find_leftmost_point;
152/// use scirs2_core::ndarray::array;
153///
154/// let points = array![[1.0, 0.0], [0.0, 1.0], [2.0, 0.0], [0.0, 0.0]];
155/// let leftmost = find_leftmost_point(&points.view());
156/// assert!(leftmost == 1 || leftmost == 3); // Either [0.0, 1.0] or [0.0, 0.0]
157/// ```
158pub fn find_leftmost_point(points: &ArrayView2<'_, f64>) -> usize {
159 let npoints = points.nrows();
160 let mut leftmost = 0;
161
162 for i in 1..npoints {
163 if points[[i, 0]] < points[[leftmost, 0]] {
164 leftmost = i;
165 }
166 }
167
168 leftmost
169}
170
171/// Find the most counterclockwise point from a given point
172///
173/// Given a current point and a candidate next point, find the point in the set
174/// that is most counterclockwise relative to the line from current to candidate.
175///
176/// # Arguments
177///
178/// * `points` - Input points array
179/// * `current` - Index of the current point
180/// * `candidate` - Index of the candidate next point
181///
182/// # Returns
183///
184/// * Index of the most counterclockwise point
185///
186/// # Examples
187///
188/// ```rust
189/// use scirs2_spatial::convex_hull::algorithms::jarvis_march::find_most_counterclockwise;
190/// use scirs2_core::ndarray::array;
191///
192/// let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
193/// let current = 0; // [0.0, 0.0]
194/// let candidate = 1; // [1.0, 0.0]
195///
196/// let most_ccw = find_most_counterclockwise(&points.view(), current, candidate);
197/// assert_eq!(most_ccw, 2); // [0.0, 1.0] is most counterclockwise
198/// ```
199pub fn find_most_counterclockwise(
200 points: &ArrayView2<'_, f64>,
201 current: usize,
202 candidate: usize,
203) -> usize {
204 let npoints = points.nrows();
205 let mut best = candidate;
206
207 for i in 0..npoints {
208 if i == current {
209 continue;
210 }
211
212 let p1 = [points[[current, 0]], points[[current, 1]]];
213 let p2 = [points[[best, 0]], points[[best, 1]]];
214 let p3 = [points[[i, 0]], points[[i, 1]]];
215
216 let cross = cross_product_2d(p1, p2, p3);
217
218 // If cross product is positive, i is more counterclockwise than best
219 if cross > 0.0
220 || (cross == 0.0 && distance_squared_2d(p1, p3) > distance_squared_2d(p1, p2))
221 {
222 best = i;
223 }
224 }
225
226 best
227}
228
229/// Check if a point is more counterclockwise than another
230///
231/// # Arguments
232///
233/// * `reference` - Reference point [x, y]
234/// * `current_best` - Current best point [x, y]
235/// * `candidate` - Candidate point [x, y]
236///
237/// # Returns
238///
239/// * true if candidate is more counterclockwise than current_best
240///
241/// # Examples
242///
243/// ```rust
244/// use scirs2_spatial::convex_hull::algorithms::jarvis_march::is_more_counterclockwise;
245///
246/// let reference = [0.0, 0.0];
247/// let current_best = [1.0, 0.0];
248/// let candidate = [0.0, 1.0];
249///
250/// assert!(is_more_counterclockwise(reference, current_best, candidate));
251/// ```
252pub fn is_more_counterclockwise(
253 reference: [f64; 2],
254 current_best: [f64; 2],
255 candidate: [f64; 2],
256) -> bool {
257 let cross = cross_product_2d(reference, current_best, candidate);
258
259 if cross > 0.0 {
260 true // Candidate is more counterclockwise
261 } else if cross == 0.0 {
262 // If collinear, prefer the farther point
263 distance_squared_2d(reference, candidate) > distance_squared_2d(reference, current_best)
264 } else {
265 false
266 }
267}
268
269/// Perform one step of the Jarvis march
270///
271/// Given a current hull point, find the next point in the hull by selecting
272/// the most counterclockwise point.
273///
274/// # Arguments
275///
276/// * `points` - Input points array
277/// * `current` - Index of the current hull point
278///
279/// # Returns
280///
281/// * Index of the next hull point
282///
283/// # Examples
284///
285/// ```rust
286/// use scirs2_spatial::convex_hull::algorithms::jarvis_march::jarvis_step;
287/// use scirs2_core::ndarray::array;
288///
289/// let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
290/// let current = 0; // Start from [0.0, 0.0]
291///
292/// let next = jarvis_step(&points.view(), current);
293/// // Should find either [1.0, 0.0] or [0.0, 1.0] depending on the order
294/// assert!(next == 1 || next == 2);
295/// ```
296pub fn jarvis_step(points: &ArrayView2<'_, f64>, current: usize) -> usize {
297 let npoints = points.nrows();
298
299 // Find the first point that's not the current point
300 let mut next = if current == 0 { 1 } else { 0 };
301
302 // Find the most counterclockwise point
303 for i in 0..npoints {
304 if i == current {
305 continue;
306 }
307
308 let p_current = [points[[current, 0]], points[[current, 1]]];
309 let p_next = [points[[next, 0]], points[[next, 1]]];
310 let p_candidate = [points[[i, 0]], points[[i, 1]]];
311
312 if is_more_counterclockwise(p_current, p_next, p_candidate) {
313 next = i;
314 }
315 }
316
317 next
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323 use scirs2_core::ndarray::arr2;
324
325 #[test]
326 fn test_jarvis_march_basic() {
327 let points = arr2(&[
328 [0.0, 0.0],
329 [1.0, 0.0],
330 [0.0, 1.0],
331 [0.5, 0.5], // Interior point
332 ]);
333
334 let hull = compute_jarvis_march(&points.view()).unwrap();
335
336 // Check dimensions
337 assert_eq!(hull.ndim(), 2);
338
339 // The interior point should not be part of the convex hull
340 assert_eq!(hull.vertex_indices().len(), 3);
341
342 // Verify that the interior point is not in the hull
343 assert!(!hull.vertex_indices().contains(&3));
344 }
345
346 #[test]
347 fn test_jarvis_march_square() {
348 let points = arr2(&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]);
349
350 let hull = compute_jarvis_march(&points.view()).unwrap();
351
352 assert_eq!(hull.ndim(), 2);
353 assert_eq!(hull.vertex_indices().len(), 4); // All points should be vertices
354 }
355
356 #[test]
357 fn test_find_leftmost_point() {
358 let points = arr2(&[[1.0, 0.0], [0.0, 1.0], [2.0, 0.0], [0.0, 0.0]]);
359 let leftmost = find_leftmost_point(&points.view());
360
361 // Should be either index 1 or 3 (both have x = 0.0)
362 assert!(leftmost == 1 || leftmost == 3);
363 }
364
365 #[test]
366 fn test_find_most_counterclockwise() {
367 let points = arr2(&[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]]);
368 let current = 0; // [0.0, 0.0]
369 let candidate = 1; // [1.0, 0.0]
370
371 let most_ccw = find_most_counterclockwise(&points.view(), current, candidate);
372 assert_eq!(most_ccw, 2); // [0.0, 1.0] is most counterclockwise
373 }
374
375 #[test]
376 fn test_is_more_counterclockwise() {
377 let reference = [0.0, 0.0];
378 let current_best = [1.0, 0.0];
379 let candidate = [0.0, 1.0];
380
381 assert!(is_more_counterclockwise(reference, current_best, candidate));
382 assert!(!is_more_counterclockwise(
383 reference,
384 candidate,
385 current_best
386 ));
387 }
388
389 #[test]
390 fn test_jarvis_step() {
391 let points = arr2(&[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]);
392 let current = 0; // Start from [0.0, 0.0]
393
394 let next = jarvis_step(&points.view(), current);
395 // Should find either [1.0, 0.0] or [0.0, 1.0] depending on the implementation
396 assert!(next == 1 || next == 2);
397 }
398
399 #[test]
400 fn test_error_cases() {
401 // Test with too few points
402 let too_few = arr2(&[[0.0, 0.0], [1.0, 0.0]]);
403 let result = compute_jarvis_march(&too_few.view());
404 assert!(result.is_err());
405
406 // Test with 3D points (should fail)
407 let points_3d = arr2(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]);
408 let result = compute_jarvis_march(&points_3d.view());
409 assert!(result.is_err());
410 }
411
412 #[test]
413 fn test_collinear_points() {
414 // Test with collinear points
415 let points = arr2(&[
416 [0.0, 0.0],
417 [1.0, 0.0],
418 [2.0, 0.0],
419 [0.5, 1.0], // Point above the line
420 ]);
421
422 let hull = compute_jarvis_march(&points.view()).unwrap();
423
424 // Should form a triangle with the non-collinear points
425 assert_eq!(hull.vertex_indices().len(), 3);
426 // The point above the line should be included
427 assert!(hull.vertex_indices().contains(&3));
428 }
429
430 #[test]
431 fn test_identical_points() {
432 // Test with some identical points
433 let points = arr2(&[
434 [0.0, 0.0],
435 [1.0, 0.0],
436 [0.0, 1.0],
437 [0.0, 0.0], // Duplicate point
438 ]);
439
440 let hull = compute_jarvis_march(&points.view()).unwrap();
441
442 // Should still form a valid triangle
443 assert_eq!(hull.vertex_indices().len(), 3);
444 }
445}