1struct FloatGridMap {
23 width: usize,
24 height: usize,
25 resolution: f64,
26 _center_x: f64,
27 _center_y: f64,
28 left_lower_x: f64,
29 left_lower_y: f64,
30 data: Vec<f64>,
31}
32
33impl FloatGridMap {
34 fn new(width: usize, height: usize, resolution: f64, center_x: f64, center_y: f64) -> Self {
35 let left_lower_x = center_x - (width as f64) / 2.0 * resolution;
36 let left_lower_y = center_y - (height as f64) / 2.0 * resolution;
37 let n = width * height;
38 Self {
39 width,
40 height,
41 resolution,
42 _center_x: center_x,
43 _center_y: center_y,
44 left_lower_x,
45 left_lower_y,
46 data: vec![0.0; n],
47 }
48 }
49
50 fn grid_index(&self, xi: i32, yi: i32) -> Option<usize> {
51 if xi < 0 || yi < 0 || (xi as usize) >= self.width || (yi as usize) >= self.height {
52 return None;
53 }
54 Some(yi as usize * self.width + xi as usize)
55 }
56
57 fn get_value(&self, xi: i32, yi: i32) -> Option<f64> {
58 self.grid_index(xi, yi).map(|i| self.data[i])
59 }
60
61 fn set_value(&mut self, xi: i32, yi: i32, val: f64) -> bool {
62 if let Some(i) = self.grid_index(xi, yi) {
63 self.data[i] = val;
64 true
65 } else {
66 false
67 }
68 }
69
70 fn cell_center(&self, xi: i32, yi: i32) -> (f64, f64) {
72 let x = self.left_lower_x + (xi as f64) * self.resolution + self.resolution / 2.0;
73 let y = self.left_lower_y + (yi as f64) * self.resolution + self.resolution / 2.0;
74 (x, y)
75 }
76
77 fn is_occupied(&self, xi: i32, yi: i32, threshold: f64) -> bool {
79 match self.get_value(xi, yi) {
80 Some(v) => v >= threshold,
81 None => true,
82 }
83 }
84
85 fn set_value_from_polygon(&mut self, pol_x: &[f64], pol_y: &[f64], val: f64, inside: bool) {
89 for yi in 0..self.height as i32 {
90 for xi in 0..self.width as i32 {
91 let (px, py) = self.cell_center(xi, yi);
92 let is_inside = point_in_polygon(px, py, pol_x, pol_y);
93 if is_inside == inside {
94 self.set_value(xi, yi, val);
95 }
96 }
97 }
98 }
99
100 fn expand_grid(&mut self) {
102 let mut marks: Vec<(i32, i32, f64)> = Vec::new();
103 for yi in 0..self.height as i32 {
104 for xi in 0..self.width as i32 {
105 if self.is_occupied(xi, yi, 1.0) {
106 if let Some(v) = self.get_value(xi, yi) {
107 for &(dx, dy) in &[
108 (1, 0),
109 (0, 1),
110 (1, 1),
111 (-1, 0),
112 (0, -1),
113 (-1, -1),
114 (1, -1),
115 (-1, 1),
116 ] {
117 marks.push((xi + dx, yi + dy, v));
118 }
119 }
120 }
121 }
122 }
123 for (x, y, v) in marks {
124 self.set_value(x, y, v);
125 }
126 }
127}
128
129fn point_in_polygon(px: f64, py: f64, poly_x: &[f64], poly_y: &[f64]) -> bool {
131 let n = poly_x.len() - 1; let mut inside = false;
133 for i1 in 0..n {
134 let i2 = (i1 + 1) % (n + 1);
135 let (min_x, max_x) = if poly_x[i1] >= poly_x[i2] {
136 (poly_x[i2], poly_x[i1])
137 } else {
138 (poly_x[i1], poly_x[i2])
139 };
140 if !(min_x <= px && px < max_x) {
141 continue;
142 }
143 let slope = (poly_y[i2] - poly_y[i1]) / (poly_x[i2] - poly_x[i1]);
144 if (poly_y[i1] + slope * (px - poly_x[i1]) - py) > 0.0 {
145 inside = !inside;
146 }
147 }
148 inside
149}
150
151fn rotate_points(x: &[f64], y: &[f64], theta: f64) -> (Vec<f64>, Vec<f64>) {
157 let c = theta.cos();
158 let s = theta.sin();
159 let rx: Vec<f64> = x
160 .iter()
161 .zip(y.iter())
162 .map(|(&xi, &yi)| c * xi + s * yi)
163 .collect();
164 let ry: Vec<f64> = x
165 .iter()
166 .zip(y.iter())
167 .map(|(&xi, &yi)| -s * xi + c * yi)
168 .collect();
169 (rx, ry)
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178pub enum SweepDirection {
179 Up = 1,
180 Down = -1,
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185pub enum MovingDirection {
186 Right = 1,
187 Left = -1,
188}
189
190struct SweepSearcher {
191 moving_dir: i32,
192 sweep_dir: i32,
193 turning_window: Vec<(i32, i32)>,
194 x_indexes_goal_y: Vec<i32>,
195 goal_y: i32,
196}
197
198impl SweepSearcher {
199 fn new(
200 moving: MovingDirection,
201 sweep: SweepDirection,
202 x_inds_goal_y: Vec<i32>,
203 goal_y: i32,
204 ) -> Self {
205 let moving_dir = moving as i32;
206 let sweep_dir = sweep as i32;
207 let turning_window = Self::build_turning_window(moving_dir, sweep_dir);
208 Self {
209 moving_dir,
210 sweep_dir,
211 turning_window,
212 x_indexes_goal_y: x_inds_goal_y,
213 goal_y,
214 }
215 }
216
217 fn build_turning_window(md: i32, sd: i32) -> Vec<(i32, i32)> {
218 vec![(md, 0), (md, sd), (0, sd), (-md, sd)]
219 }
220
221 fn swap_moving_direction(&mut self) {
222 self.moving_dir = -self.moving_dir;
223 self.turning_window = Self::build_turning_window(self.moving_dir, self.sweep_dir);
224 }
225
226 fn move_target_grid(&mut self, cx: i32, cy: i32, grid: &FloatGridMap) -> Option<(i32, i32)> {
228 let nx = cx + self.moving_dir;
229 let ny = cy;
230
231 if !grid.is_occupied(nx, ny, 0.5) {
233 return Some((nx, ny));
234 }
235
236 if let Some((mut tnx, tny)) = self.find_safe_turning_grid(cx, cy, grid) {
238 while !grid.is_occupied(tnx + self.moving_dir, tny, 0.5) {
240 tnx += self.moving_dir;
241 }
242 self.swap_moving_direction();
243 Some((tnx, tny))
244 } else {
245 let bx = cx - self.moving_dir;
247 let by = cy;
248 if grid.is_occupied(bx, by, 1.0) {
249 None } else {
251 Some((bx, by))
252 }
253 }
254 }
255
256 fn find_safe_turning_grid(&self, cx: i32, cy: i32, grid: &FloatGridMap) -> Option<(i32, i32)> {
257 for &(dx, dy) in &self.turning_window {
258 let nx = cx + dx;
259 let ny = cy + dy;
260 if !grid.is_occupied(nx, ny, 0.5) {
261 return Some((nx, ny));
262 }
263 }
264 None
265 }
266
267 fn is_search_done(&self, grid: &FloatGridMap) -> bool {
268 for &ix in &self.x_indexes_goal_y {
269 if !grid.is_occupied(ix, self.goal_y, 0.5) {
270 return false;
271 }
272 }
273 true
274 }
275
276 fn search_start_grid(&self, grid: &FloatGridMap) -> Option<(i32, i32)> {
277 let (x_inds, y_ind) = match self.sweep_dir.cmp(&0) {
278 std::cmp::Ordering::Less => search_free_grid_index_at_edge_y(grid, true),
279 _ => search_free_grid_index_at_edge_y(grid, false),
280 };
281 let y_ind = y_ind?;
282 if x_inds.is_empty() {
283 return None;
284 }
285 let x_ind = if self.moving_dir > 0 {
286 *x_inds.iter().min().unwrap()
287 } else {
288 *x_inds.iter().max().unwrap()
289 };
290 Some((x_ind, y_ind))
291 }
292}
293
294fn search_free_grid_index_at_edge_y(
297 grid: &FloatGridMap,
298 from_upper: bool,
299) -> (Vec<i32>, Option<i32>) {
300 let y_range: Box<dyn Iterator<Item = i32>> = if from_upper {
301 Box::new((0..grid.height as i32).rev())
302 } else {
303 Box::new(0..grid.height as i32)
304 };
305
306 for iy in y_range {
307 let mut x_inds = Vec::new();
308 let x_iter: Box<dyn Iterator<Item = i32>> = if from_upper {
309 Box::new((0..grid.width as i32).rev())
310 } else {
311 Box::new(0..grid.width as i32)
312 };
313 for ix in x_iter {
314 if !grid.is_occupied(ix, iy, 0.5) {
315 x_inds.push(ix);
316 }
317 }
318 if !x_inds.is_empty() {
319 return (x_inds, Some(iy));
320 }
321 }
322 (Vec::new(), None)
323}
324
325fn setup_grid_map(
330 ox: &[f64],
331 oy: &[f64],
332 resolution: f64,
333 sweep_dir: SweepDirection,
334) -> (FloatGridMap, Vec<i32>, i32) {
335 let offset_grid: usize = 10;
336 let (min_x, max_x) = min_max(ox);
337 let (min_y, max_y) = min_max(oy);
338 let width = ((max_x - min_x) / resolution).ceil() as usize + offset_grid;
339 let height = ((max_y - min_y) / resolution).ceil() as usize + offset_grid;
340 let center_x = (max_x + min_x) / 2.0;
341 let center_y = (max_y + min_y) / 2.0;
342
343 let mut grid = FloatGridMap::new(width, height, resolution, center_x, center_y);
344 grid.set_value_from_polygon(ox, oy, 1.0, false); grid.expand_grid();
346
347 let (x_inds_goal_y, goal_y) = match sweep_dir {
348 SweepDirection::Up => search_free_grid_index_at_edge_y(&grid, true),
349 SweepDirection::Down => search_free_grid_index_at_edge_y(&grid, false),
350 };
351 let goal_y = goal_y.unwrap_or(0);
352
353 (grid, x_inds_goal_y, goal_y)
354}
355
356fn min_max(v: &[f64]) -> (f64, f64) {
357 let mut lo = f64::INFINITY;
358 let mut hi = f64::NEG_INFINITY;
359 for &x in v {
360 if x < lo {
361 lo = x;
362 }
363 if x > hi {
364 hi = x;
365 }
366 }
367 (lo, hi)
368}
369
370fn sweep_path_search(
375 searcher: &mut SweepSearcher,
376 grid: &mut FloatGridMap,
377) -> (Vec<f64>, Vec<f64>) {
378 let (mut cx, mut cy) = match searcher.search_start_grid(grid) {
379 Some(p) => p,
380 None => return (Vec::new(), Vec::new()),
381 };
382
383 if !grid.set_value(cx, cy, 0.5) {
384 return (Vec::new(), Vec::new());
385 }
386
387 let (x, y) = grid.cell_center(cx, cy);
388 let mut px = vec![x];
389 let mut py = vec![y];
390
391 while let Some((nx, ny)) = searcher.move_target_grid(cx, cy, grid) {
392 cx = nx;
393 cy = ny;
394
395 if searcher.is_search_done(grid) {
396 break;
397 }
398
399 let (x, y) = grid.cell_center(cx, cy);
400 px.push(x);
401 py.push(y);
402
403 grid.set_value(cx, cy, 0.5);
404 }
405
406 (px, py)
407}
408
409fn find_sweep_direction_and_start(ox: &[f64], oy: &[f64]) -> ([f64; 2], [f64; 2]) {
416 let mut max_dist = 0.0_f64;
417 let mut vec = [0.0, 0.0];
418 let mut start = [0.0, 0.0];
419 for i in 0..ox.len() - 1 {
420 let dx = ox[i + 1] - ox[i];
421 let dy = oy[i + 1] - oy[i];
422 let d = (dx * dx + dy * dy).sqrt();
423 if d > max_dist {
424 max_dist = d;
425 vec = [dx, dy];
426 start = [ox[i], oy[i]];
427 }
428 }
429 (vec, start)
430}
431
432fn convert_grid_coordinate(
433 ox: &[f64],
434 oy: &[f64],
435 sweep_vec: [f64; 2],
436 sweep_start: [f64; 2],
437) -> (Vec<f64>, Vec<f64>) {
438 let tx: Vec<f64> = ox.iter().map(|x| x - sweep_start[0]).collect();
439 let ty: Vec<f64> = oy.iter().map(|y| y - sweep_start[1]).collect();
440 let theta = sweep_vec[1].atan2(sweep_vec[0]);
441 rotate_points(&tx, &ty, theta)
442}
443
444fn convert_global_coordinate(
445 px: &[f64],
446 py: &[f64],
447 sweep_vec: [f64; 2],
448 sweep_start: [f64; 2],
449) -> (Vec<f64>, Vec<f64>) {
450 let theta = sweep_vec[1].atan2(sweep_vec[0]);
451 let (rx, ry) = rotate_points(px, py, -theta);
452 let gx: Vec<f64> = rx.iter().map(|x| x + sweep_start[0]).collect();
453 let gy: Vec<f64> = ry.iter().map(|y| y + sweep_start[1]).collect();
454 (gx, gy)
455}
456
457#[derive(Debug, Clone)]
463pub struct GridBasedSweepConfig {
464 pub resolution: f64,
466 pub moving_direction: MovingDirection,
468 pub sweep_direction: SweepDirection,
470}
471
472impl Default for GridBasedSweepConfig {
473 fn default() -> Self {
474 Self {
475 resolution: 5.0,
476 moving_direction: MovingDirection::Right,
477 sweep_direction: SweepDirection::Up,
478 }
479 }
480}
481
482#[derive(Debug, Clone)]
484pub struct SweepCoveragePath {
485 pub x: Vec<f64>,
487 pub y: Vec<f64>,
489}
490
491pub fn plan_sweep_coverage(
522 ox: &[f64],
523 oy: &[f64],
524 config: &GridBasedSweepConfig,
525) -> SweepCoveragePath {
526 assert!(
527 ox.len() >= 4 && ox.len() == oy.len(),
528 "polygon must have at least 3 distinct vertices (4 points with closing vertex)"
529 );
530
531 let (sweep_vec, sweep_start) = find_sweep_direction_and_start(ox, oy);
532 let (rox, roy) = convert_grid_coordinate(ox, oy, sweep_vec, sweep_start);
533
534 let (mut grid, x_inds_goal_y, goal_y) =
535 setup_grid_map(&rox, &roy, config.resolution, config.sweep_direction);
536
537 let mut searcher = SweepSearcher::new(
538 config.moving_direction,
539 config.sweep_direction,
540 x_inds_goal_y,
541 goal_y,
542 );
543
544 let (px, py) = sweep_path_search(&mut searcher, &mut grid);
545 let (gx, gy) = convert_global_coordinate(&px, &py, sweep_vec, sweep_start);
546
547 SweepCoveragePath { x: gx, y: gy }
548}
549
550#[cfg(test)]
555mod tests {
556 use super::*;
557 use std::f64::consts::PI;
558
559 fn dist(x1: f64, y1: f64, x2: f64, y2: f64) -> f64 {
561 ((x2 - x1).powi(2) + (y2 - y1).powi(2)).sqrt()
562 }
563
564 #[test]
567 fn test_point_in_polygon_square() {
568 let px = vec![0.0, 10.0, 10.0, 0.0, 0.0];
570 let py = vec![0.0, 0.0, 10.0, 10.0, 0.0];
571 assert!(point_in_polygon(5.0, 5.0, &px, &py));
572 assert!(!point_in_polygon(15.0, 5.0, &px, &py));
573 assert!(!point_in_polygon(-1.0, 5.0, &px, &py));
574 }
575
576 #[test]
577 fn test_rotate_points_90_degrees() {
578 let x = vec![1.0, 0.0];
579 let y = vec![0.0, 1.0];
580 let (rx, ry) = rotate_points(&x, &y, PI / 2.0);
581 assert!((rx[0] - 0.0).abs() < 1e-10);
583 assert!((ry[0] - (-1.0)).abs() < 1e-10);
584 assert!((rx[1] - 1.0).abs() < 1e-10);
585 assert!((ry[1] - 0.0).abs() < 1e-10);
586 }
587
588 #[test]
589 fn test_find_sweep_direction_longest_edge() {
590 let ox = vec![0.0, 100.0, 100.0, 0.0, 0.0];
591 let oy = vec![0.0, 0.0, 10.0, 10.0, 0.0];
592 let (vec, start) = find_sweep_direction_and_start(&ox, &oy);
593 assert!((vec[0] - 100.0).abs() < 1e-10);
595 assert!((vec[1] - 0.0).abs() < 1e-10);
596 assert!((start[0] - 0.0).abs() < 1e-10);
597 assert!((start[1] - 0.0).abs() < 1e-10);
598 }
599
600 #[test]
601 fn test_grid_map_set_get() {
602 let mut gm = FloatGridMap::new(10, 10, 1.0, 5.0, 5.0);
603 assert!(!gm.is_occupied(5, 5, 0.5));
604 gm.set_value(5, 5, 1.0);
605 assert!(gm.is_occupied(5, 5, 0.5));
606 }
607
608 #[test]
609 fn test_grid_map_out_of_bounds() {
610 let gm = FloatGridMap::new(5, 5, 1.0, 2.5, 2.5);
611 assert!(gm.is_occupied(-1, 0, 0.5));
612 assert!(gm.is_occupied(0, -1, 0.5));
613 assert!(gm.is_occupied(5, 0, 0.5));
614 assert!(gm.is_occupied(0, 5, 0.5));
615 }
616
617 #[test]
620 fn test_rectangle_coverage() {
621 let ox = vec![0.0, 50.0, 50.0, 0.0, 0.0];
622 let oy = vec![0.0, 0.0, 30.0, 30.0, 0.0];
623 let config = GridBasedSweepConfig {
624 resolution: 5.0,
625 ..Default::default()
626 };
627 let path = plan_sweep_coverage(&ox, &oy, &config);
628
629 assert!(
630 !path.x.is_empty(),
631 "coverage path should not be empty for a rectangle"
632 );
633 assert!(
635 path.x.len() > 5,
636 "expected more than 5 waypoints, got {}",
637 path.x.len()
638 );
639 }
640
641 #[test]
642 fn test_polygon_coverage() {
643 let ox = vec![0.0, 20.0, 50.0, 100.0, 130.0, 40.0, 0.0];
644 let oy = vec![0.0, -20.0, 0.0, 30.0, 60.0, 80.0, 0.0];
645 let config = GridBasedSweepConfig {
646 resolution: 5.0,
647 ..Default::default()
648 };
649 let path = plan_sweep_coverage(&ox, &oy, &config);
650
651 assert!(!path.x.is_empty());
652 assert!(path.x.len() > 10);
653 }
654
655 #[test]
656 fn test_path_lengths_match() {
657 let ox = vec![0.0, 50.0, 50.0, 0.0, 0.0];
658 let oy = vec![0.0, 0.0, 30.0, 30.0, 0.0];
659 let config = GridBasedSweepConfig::default();
660 let path = plan_sweep_coverage(&ox, &oy, &config);
661
662 assert_eq!(
663 path.x.len(),
664 path.y.len(),
665 "x and y paths must have the same length"
666 );
667 }
668
669 #[test]
670 fn test_waypoints_inside_polygon_neighbourhood() {
671 let ox = vec![0.0, 50.0, 50.0, 0.0, 0.0];
673 let oy = vec![0.0, 0.0, 30.0, 30.0, 0.0];
674 let config = GridBasedSweepConfig {
675 resolution: 1.3,
676 ..Default::default()
677 };
678 let path = plan_sweep_coverage(&ox, &oy, &config);
679
680 let margin = config.resolution * 3.0;
681 for (&x, &y) in path.x.iter().zip(path.y.iter()) {
682 assert!(
683 x > -margin && x < 50.0 + margin && y > -margin && y < 30.0 + margin,
684 "waypoint ({}, {}) is too far from the polygon",
685 x,
686 y
687 );
688 }
689 }
690
691 #[test]
692 fn test_consecutive_waypoints_are_close() {
693 let ox = vec![0.0, 50.0, 50.0, 0.0, 0.0];
694 let oy = vec![0.0, 0.0, 30.0, 30.0, 0.0];
695 let config = GridBasedSweepConfig {
696 resolution: 2.0,
697 ..Default::default()
698 };
699 let path = plan_sweep_coverage(&ox, &oy, &config);
700
701 let max_step = config.resolution * 3.0; for i in 1..path.x.len() {
703 let d = dist(path.x[i - 1], path.y[i - 1], path.x[i], path.y[i]);
704 assert!(
705 d < max_step,
706 "step {} -> {} is too large: {:.2} (max {:.2})",
707 i - 1,
708 i,
709 d,
710 max_step,
711 );
712 }
713 }
714
715 #[test]
716 fn test_different_sweep_directions() {
717 let ox = vec![0.0, 50.0, 50.0, 0.0, 0.0];
718 let oy = vec![0.0, 0.0, 30.0, 30.0, 0.0];
719
720 let cfg_up_right = GridBasedSweepConfig {
721 resolution: 5.0,
722 moving_direction: MovingDirection::Right,
723 sweep_direction: SweepDirection::Up,
724 };
725 let cfg_down_left = GridBasedSweepConfig {
726 resolution: 5.0,
727 moving_direction: MovingDirection::Left,
728 sweep_direction: SweepDirection::Down,
729 };
730
731 let path1 = plan_sweep_coverage(&ox, &oy, &cfg_up_right);
732 let path2 = plan_sweep_coverage(&ox, &oy, &cfg_down_left);
733
734 assert!(!path1.x.is_empty());
735 assert!(!path2.x.is_empty());
736 let same_start =
738 (path1.x[0] - path2.x[0]).abs() < 1e-6 && (path1.y[0] - path2.y[0]).abs() < 1e-6;
739 assert!(
740 !same_start,
741 "different sweep configs should produce different starting points"
742 );
743 }
744
745 #[test]
746 fn test_fine_resolution_rectangle() {
747 let ox = vec![0.0, 50.0, 50.0, 0.0, 0.0];
748 let oy = vec![0.0, 0.0, 30.0, 30.0, 0.0];
749 let config = GridBasedSweepConfig {
750 resolution: 1.3,
751 ..Default::default()
752 };
753 let path = plan_sweep_coverage(&ox, &oy, &config);
754 assert!(
756 path.x.len() > 50,
757 "fine resolution should yield many waypoints, got {}",
758 path.x.len()
759 );
760 }
761
762 #[test]
763 #[should_panic(expected = "polygon must have at least 3 distinct vertices")]
764 fn test_too_few_vertices_panics() {
765 let ox = vec![0.0, 1.0];
766 let oy = vec![0.0, 1.0];
767 plan_sweep_coverage(&ox, &oy, &GridBasedSweepConfig::default());
768 }
769
770 #[test]
771 fn test_large_irregular_polygon() {
772 let ox = vec![0.0, 20.0, 50.0, 200.0, 130.0, 40.0, 0.0];
773 let oy = vec![0.0, -80.0, 0.0, 30.0, 60.0, 80.0, 0.0];
774 let config = GridBasedSweepConfig {
775 resolution: 5.0,
776 ..Default::default()
777 };
778 let path = plan_sweep_coverage(&ox, &oy, &config);
779 assert!(!path.x.is_empty());
780 assert!(path.x.len() > 20);
781 }
782}