1use std::f64::consts::PI;
39
40use crate::error::{AlgorithmError, Result};
41use crate::vector::simplify_linestring_dp;
42use oxigdal_core::vector::{Coordinate, LineString};
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
50pub enum JoinStyle {
51 #[default]
53 Miter,
54 Bevel,
56 Round,
58}
59
60#[derive(Debug, Clone)]
62pub struct OffsetOptions {
63 pub miter_limit: f64,
69 pub join_style: JoinStyle,
71 pub simplify_tolerance: Option<f64>,
76}
77
78impl Default for OffsetOptions {
79 fn default() -> Self {
80 Self {
81 miter_limit: 10.0,
82 join_style: JoinStyle::Miter,
83 simplify_tolerance: None,
84 }
85 }
86}
87
88#[derive(Debug, Clone)]
90pub struct OffsetResult {
91 pub coords: Vec<(f64, f64)>,
93 pub was_simplified: bool,
95}
96
97pub fn offset_linestring(
119 coords: &[(f64, f64)],
120 distance: f64,
121 options: &OffsetOptions,
122) -> Result<OffsetResult> {
123 let n = coords.len();
124 if n < 2 {
125 return Err(AlgorithmError::InsufficientData {
126 operation: "offset_linestring",
127 message: format!("need at least 2 vertices, got {n}"),
128 });
129 }
130
131 if distance == 0.0 {
133 return Ok(OffsetResult {
134 coords: coords.to_vec(),
135 was_simplified: false,
136 });
137 }
138
139 let normals = compute_segment_normals(coords);
141
142 let mut out: Vec<(f64, f64)> = Vec::with_capacity(n + 8);
144
145 let n0 = first_valid_normal(&normals);
147 let (px, py) = coords[0];
148 out.push((px + distance * n0.0, py + distance * n0.1));
149
150 for i in 1..n - 1 {
152 let na = prev_valid_normal(&normals, i);
153 let nb = next_valid_normal(&normals, i);
154 let (vx, vy) = coords[i];
155 emit_join_points(&mut out, (vx, vy), na, nb, distance, options);
156 }
157
158 let n_last = last_valid_normal(&normals);
160 let (ex, ey) = coords[n - 1];
161 out.push((ex + distance * n_last.0, ey + distance * n_last.1));
162
163 let was_simplified;
165 if let Some(tol) = options.simplify_tolerance {
166 let ls_coords: Vec<Coordinate> =
167 out.iter().map(|&(x, y)| Coordinate::new_2d(x, y)).collect();
168 match LineString::new(ls_coords) {
169 Ok(ls) => match simplify_linestring_dp(&ls, tol) {
170 Ok(simplified) => {
171 out = simplified.coords.iter().map(|c| (c.x, c.y)).collect();
172 was_simplified = true;
173 }
174 Err(_) => {
175 was_simplified = false;
177 }
178 },
179 Err(_) => {
180 was_simplified = false;
181 }
182 }
183 } else {
184 was_simplified = false;
185 }
186
187 Ok(OffsetResult {
188 coords: out,
189 was_simplified,
190 })
191}
192
193pub fn offset_polygon_rings(
207 rings: &[Vec<(f64, f64)>],
208 distance: f64,
209 options: &OffsetOptions,
210) -> Result<Vec<Vec<(f64, f64)>>> {
211 rings
212 .iter()
213 .map(|ring| offset_closed_ring(ring, distance, options))
214 .collect()
215}
216
217fn offset_closed_ring(
230 ring: &[(f64, f64)],
231 distance: f64,
232 options: &OffsetOptions,
233) -> Result<Vec<(f64, f64)>> {
234 let effective_ring = strip_closing_vertex(ring);
237 let n = effective_ring.len();
238 if n < 3 {
239 return Err(AlgorithmError::InsufficientData {
240 operation: "offset_polygon_rings",
241 message: format!("ring must have at least 3 distinct vertices, got {n}"),
242 });
243 }
244
245 let signed_area = ring_signed_area(effective_ring);
248 let effective_distance = if signed_area > 0.0 {
252 -distance
253 } else {
254 distance
255 };
256
257 let normals = compute_cyclic_normals(effective_ring);
259
260 let mut out: Vec<(f64, f64)> = Vec::with_capacity(n + 4);
261
262 for i in 0..n {
263 let prev_seg = if i == 0 { n - 1 } else { i - 1 };
265 let curr_seg = i;
266
267 let na = normals[prev_seg];
268 let nb = normals[curr_seg];
269 let (vx, vy) = effective_ring[i];
270 emit_join_points(&mut out, (vx, vy), na, nb, effective_distance, options);
271 }
272
273 if let Some(&first) = out.first() {
275 out.push(first);
276 }
277
278 Ok(out)
279}
280
281fn compute_segment_normals(coords: &[(f64, f64)]) -> Vec<(f64, f64)> {
290 coords.windows(2).map(|w| left_normal(w[0], w[1])).collect()
291}
292
293fn compute_cyclic_normals(ring: &[(f64, f64)]) -> Vec<(f64, f64)> {
296 let n = ring.len();
297 (0..n)
298 .map(|i| left_normal(ring[i], ring[(i + 1) % n]))
299 .collect()
300}
301
302fn left_normal(a: (f64, f64), b: (f64, f64)) -> (f64, f64) {
306 let dx = b.0 - a.0;
307 let dy = b.1 - a.1;
308 let len = dx.hypot(dy);
309 if len < f64::EPSILON {
310 return (0.0, 0.0);
311 }
312 (-dy / len, dx / len)
314}
315
316fn first_valid_normal(normals: &[(f64, f64)]) -> (f64, f64) {
318 normals
319 .iter()
320 .find(|&&n| normal_is_valid(n))
321 .copied()
322 .unwrap_or((0.0, 0.0))
323}
324
325fn last_valid_normal(normals: &[(f64, f64)]) -> (f64, f64) {
327 normals
328 .iter()
329 .rev()
330 .find(|&&n| normal_is_valid(n))
331 .copied()
332 .unwrap_or((0.0, 0.0))
333}
334
335fn prev_valid_normal(normals: &[(f64, f64)], vertex_idx: usize) -> (f64, f64) {
338 let seg_idx = vertex_idx.saturating_sub(1);
340 (0..=seg_idx)
341 .rev()
342 .map(|j| normals[j])
343 .find(|&n| normal_is_valid(n))
344 .unwrap_or((0.0, 0.0))
345}
346
347fn next_valid_normal(normals: &[(f64, f64)], vertex_idx: usize) -> (f64, f64) {
350 (vertex_idx..normals.len())
352 .map(|j| normals[j])
353 .find(|&n| normal_is_valid(n))
354 .unwrap_or((0.0, 0.0))
355}
356
357#[inline]
359fn normal_is_valid(n: (f64, f64)) -> bool {
360 n.0 != 0.0 || n.1 != 0.0
361}
362
363fn emit_join_points(
371 out: &mut Vec<(f64, f64)>,
372 v: (f64, f64),
373 na: (f64, f64),
374 nb: (f64, f64),
375 distance: f64,
376 options: &OffsetOptions,
377) {
378 let pa = (v.0 + distance * na.0, v.1 + distance * na.1);
380 let pb = (v.0 + distance * nb.0, v.1 + distance * nb.1);
381
382 if !normal_is_valid(na) && !normal_is_valid(nb) {
384 out.push(pa);
385 return;
386 }
387 if !normal_is_valid(na) {
388 out.push(pb);
389 return;
390 }
391 if !normal_is_valid(nb) {
392 out.push(pa);
393 return;
394 }
395
396 match options.join_style {
397 JoinStyle::Bevel => {
398 emit_bevel(out, pa, pb);
399 }
400 JoinStyle::Round => {
401 emit_round(out, v, pa, pb, distance);
402 }
403 JoinStyle::Miter => {
404 emit_miter(out, v, na, nb, pa, pb, distance, options.miter_limit);
405 }
406 }
407}
408
409#[inline]
411fn emit_bevel(out: &mut Vec<(f64, f64)>, pa: (f64, f64), pb: (f64, f64)) {
412 out.push(pa);
413 out.push(pb);
414}
415
416fn emit_miter(
419 out: &mut Vec<(f64, f64)>,
420 v: (f64, f64),
421 na: (f64, f64),
422 nb: (f64, f64),
423 pa: (f64, f64),
424 pb: (f64, f64),
425 distance: f64,
426 miter_limit: f64,
427) {
428 let bx = na.0 + nb.0;
430 let by = na.1 + nb.1;
431 let blen = bx.hypot(by);
432
433 if blen < f64::EPSILON {
435 out.push(pa);
436 return;
437 }
438
439 let (bux, buy) = (bx / blen, by / blen);
440
441 let dot = bux * na.0 + buy * na.1;
444
445 if dot.abs() < f64::EPSILON {
447 emit_bevel(out, pa, pb);
448 return;
449 }
450
451 let miter_length = distance / dot;
452 let ratio = (miter_length / distance).abs();
453
454 if ratio > miter_limit.abs() || !miter_length.is_finite() {
455 emit_bevel(out, pa, pb);
457 } else {
458 out.push((v.0 + miter_length * bux, v.1 + miter_length * buy));
460 }
461}
462
463fn emit_round(
467 out: &mut Vec<(f64, f64)>,
468 v: (f64, f64),
469 pa: (f64, f64),
470 pb: (f64, f64),
471 distance: f64,
472) {
473 out.push(pa);
474
475 let radius = distance.abs();
476 if radius < f64::EPSILON {
477 out.push(pb);
478 return;
479 }
480
481 let angle_a = (pa.1 - v.1).atan2(pa.0 - v.0);
482 let angle_b = (pb.1 - v.1).atan2(pb.0 - v.0);
483
484 let mut delta = angle_b - angle_a;
486
487 while delta > PI {
489 delta -= 2.0 * PI;
490 }
491 while delta < -PI {
492 delta += 2.0 * PI;
493 }
494
495 let segments = (((delta.abs() / PI) * 8.0).ceil() as usize).max(1);
497
498 for k in 1..segments {
499 let t = (k as f64) / (segments as f64);
500 let angle = angle_a + t * delta;
501 out.push((v.0 + radius * angle.cos(), v.1 + radius * angle.sin()));
502 }
503
504 out.push(pb);
505}
506
507fn ring_signed_area(ring: &[(f64, f64)]) -> f64 {
516 let n = ring.len();
517 let mut sum = 0.0_f64;
518 for i in 0..n {
519 let j = (i + 1) % n;
520 sum += ring[i].0 * ring[j].1;
521 sum -= ring[j].0 * ring[i].1;
522 }
523 sum / 2.0
524}
525
526fn strip_closing_vertex(ring: &[(f64, f64)]) -> &[(f64, f64)] {
529 if ring.len() >= 2 {
530 let first = ring[0];
531 let last = ring[ring.len() - 1];
532 if (first.0 - last.0).abs() < f64::EPSILON && (first.1 - last.1).abs() < f64::EPSILON {
533 return &ring[..ring.len() - 1];
534 }
535 }
536 ring
537}
538
539#[cfg(test)]
544mod tests {
545 use super::*;
546
547 fn opts_miter() -> OffsetOptions {
548 OffsetOptions {
549 join_style: JoinStyle::Miter,
550 ..Default::default()
551 }
552 }
553
554 fn opts_bevel() -> OffsetOptions {
555 OffsetOptions {
556 join_style: JoinStyle::Bevel,
557 ..Default::default()
558 }
559 }
560
561 fn opts_round() -> OffsetOptions {
562 OffsetOptions {
563 join_style: JoinStyle::Round,
564 ..Default::default()
565 }
566 }
567
568 #[test]
571 fn test_left_normal_east() {
572 let n = left_normal((0.0, 0.0), (1.0, 0.0));
574 assert!((n.0 - 0.0).abs() < 1e-12, "nx should be 0, got {}", n.0);
575 assert!((n.1 - 1.0).abs() < 1e-12, "ny should be 1, got {}", n.1);
576 }
577
578 #[test]
579 fn test_left_normal_north() {
580 let n = left_normal((0.0, 0.0), (0.0, 1.0));
582 assert!((n.0 - (-1.0)).abs() < 1e-12);
583 assert!((n.1 - 0.0).abs() < 1e-12);
584 }
585
586 #[test]
589 fn test_offset_horizontal_line_left_positive() {
590 let coords = vec![(0.0, 0.0), (10.0, 0.0)];
591 let result = offset_linestring(&coords, 1.0, &opts_miter());
592 assert!(result.is_ok(), "expected Ok, got {result:?}");
593 let r = result.expect("checked above");
594 assert_eq!(r.coords.len(), 2);
595 for &(_, y) in &r.coords {
596 assert!((y - 1.0).abs() < 1e-10, "expected y=1, got {y}");
597 }
598 }
599
600 #[test]
601 fn test_offset_horizontal_line_right_negative() {
602 let coords = vec![(0.0, 0.0), (10.0, 0.0)];
603 let result = offset_linestring(&coords, -1.0, &opts_miter());
604 assert!(result.is_ok());
605 let r = result.expect("checked above");
606 assert_eq!(r.coords.len(), 2);
607 for &(_, y) in &r.coords {
608 assert!((y - (-1.0)).abs() < 1e-10, "expected y=-1, got {y}");
609 }
610 }
611
612 #[test]
613 fn test_offset_zero_distance_returns_input() {
614 let coords = vec![(0.0, 0.0), (5.0, 5.0), (10.0, 0.0)];
615 let result = offset_linestring(&coords, 0.0, &opts_miter());
616 assert!(result.is_ok());
617 let r = result.expect("checked above");
618 assert_eq!(r.coords, coords);
619 }
620
621 #[test]
622 fn test_offset_right_angle_miter_within_limit() {
623 let coords = vec![(0.0, 10.0), (0.0, 0.0), (10.0, 0.0)];
625 let opts = opts_miter();
626 let result = offset_linestring(&coords, 1.0, &opts);
627 assert!(result.is_ok());
628 let r = result.expect("checked above");
629 for &(x, y) in &r.coords {
631 assert!(x.is_finite(), "x must be finite, got {x}");
632 assert!(y.is_finite(), "y must be finite, got {y}");
633 }
634 assert!(r.coords.len() >= 3, "got {} points", r.coords.len());
636 }
637
638 #[test]
639 fn test_offset_right_angle_bevel_style() {
640 let coords = vec![(0.0, 10.0), (0.0, 0.0), (10.0, 0.0)];
642 let result = offset_linestring(&coords, 1.0, &opts_bevel());
643 assert!(result.is_ok());
644 let r = result.expect("checked above");
645 assert_eq!(
646 r.coords.len(),
647 4,
648 "bevel should produce 4 points, got {}",
649 r.coords.len()
650 );
651 }
652
653 #[test]
654 fn test_offset_miter_limit_clamps_sharp_angle() {
655 let coords = vec![(0.0, 0.0), (10.0, 0.0), (10.05, 0.01)];
660 let opts = OffsetOptions {
661 join_style: JoinStyle::Miter,
662 miter_limit: 2.0,
663 ..Default::default()
664 };
665 let result = offset_linestring(&coords, 1.0, &opts);
666 assert!(result.is_ok());
667 let r = result.expect("checked above");
668 for &(x, y) in &r.coords {
669 assert!(x.is_finite() && y.is_finite(), "coords must be finite");
670 }
671 }
672
673 #[test]
674 fn test_offset_insufficient_vertices_errors() {
675 let coords = vec![(0.0, 0.0)]; let result = offset_linestring(&coords, 1.0, &opts_miter());
677 assert!(result.is_err(), "expected Err for 1-point input");
678 }
679
680 #[test]
681 fn test_offset_round_style_no_nan() {
682 let coords = vec![(0.0, 10.0), (0.0, 0.0), (10.0, 0.0)];
683 let result = offset_linestring(&coords, 1.0, &opts_round());
684 assert!(result.is_ok());
685 let r = result.expect("checked above");
686 for &(x, y) in &r.coords {
687 assert!(
688 x.is_finite() && y.is_finite(),
689 "round join produced non-finite coord"
690 );
691 }
692 assert!(
694 r.coords.len() > 4,
695 "expected >4 coords for round join, got {}",
696 r.coords.len()
697 );
698 }
699
700 #[test]
701 fn test_offset_polygon_ring_square() {
702 let ring = vec![
704 (0.0_f64, 0.0_f64),
705 (10.0, 0.0),
706 (10.0, 10.0),
707 (0.0, 10.0),
708 (0.0, 0.0), ];
710 let opts = OffsetOptions {
711 join_style: JoinStyle::Miter,
712 ..Default::default()
713 };
714 let result = offset_polygon_rings(&[ring], 1.0, &opts);
715 assert!(result.is_ok());
716 let rings = result.expect("checked above");
717 assert_eq!(rings.len(), 1);
718 let out_ring = &rings[0];
719
720 for &(x, y) in out_ring {
722 assert!(x.is_finite() && y.is_finite());
723 }
724
725 let area = shoelace_area(out_ring);
728 assert!(
729 area > 100.0,
730 "expanded area should exceed original; got {area}"
731 );
732 }
733
734 fn shoelace_area(ring: &[(f64, f64)]) -> f64 {
736 let n = ring.len();
737 let mut sum = 0.0;
738 for i in 0..n - 1 {
739 sum += ring[i].0 * ring[i + 1].1;
740 sum -= ring[i + 1].0 * ring[i].1;
741 }
742 sum.abs() / 2.0
743 }
744}