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}