use crate::error::{AlgorithmError, Result};
use oxigdal_core::vector::Coordinate;
#[derive(Debug, Clone)]
pub struct SnapRoundingOptions {
pub precision: f64,
pub max_iterations: usize,
}
impl Default for SnapRoundingOptions {
fn default() -> Self {
Self {
precision: 1e-6,
max_iterations: 8,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SnappedSegment {
pub start: Coordinate,
pub end: Coordinate,
pub source_line: usize,
}
#[derive(Debug, Clone)]
pub struct SnapRoundingResult {
pub segments: Vec<SnappedSegment>,
pub intersections_added: usize,
pub iterations: usize,
}
#[derive(Debug, Clone)]
struct WorkSegment {
start: Coordinate,
end: Coordinate,
source_line: usize,
}
#[inline]
fn snap_to_grid(x: f64, y: f64, prec: f64) -> (f64, f64) {
((x / prec).round() * prec, (y / prec).round() * prec)
}
#[inline]
fn coords_equal_grid(a: &Coordinate, b: &Coordinate, prec: f64) -> bool {
let half = prec * 0.5;
(a.x - b.x).abs() < half && (a.y - b.y).abs() < half
}
#[inline]
fn point_on_segment(p: &Coordinate, a: &Coordinate, b: &Coordinate, prec: f64) -> bool {
let cross = (b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x);
if cross.abs() > prec {
return false;
}
let dot = (p.x - a.x) * (b.x - a.x) + (p.y - a.y) * (b.y - a.y);
let len2 = (b.x - a.x).powi(2) + (b.y - a.y).powi(2);
dot >= -prec && dot <= len2 + prec
}
#[inline]
fn split_segment_at(seg: &WorkSegment, p: &Coordinate) -> (WorkSegment, WorkSegment) {
(
WorkSegment {
start: seg.start,
end: *p,
source_line: seg.source_line,
},
WorkSegment {
start: *p,
end: seg.end,
source_line: seg.source_line,
},
)
}
#[must_use]
pub fn snap_coordinate(c: &Coordinate, precision: f64) -> Coordinate {
let (sx, sy) = snap_to_grid(c.x, c.y, precision);
Coordinate::new_2d(sx, sy)
}
#[must_use]
pub fn snap_linestring(coords: &[Coordinate], precision: f64) -> Vec<Coordinate> {
let mut result: Vec<Coordinate> = Vec::with_capacity(coords.len());
for c in coords {
let s = snap_coordinate(c, precision);
if result.last().is_none_or(|prev| {
(prev.x - s.x).abs() > f64::EPSILON || (prev.y - s.y).abs() > f64::EPSILON
}) {
result.push(s);
}
}
result
}
pub fn snap_round(
lines: &[Vec<Coordinate>],
options: &SnapRoundingOptions,
) -> Result<SnapRoundingResult> {
if options.precision <= 0.0 || options.precision.is_nan() {
return Err(AlgorithmError::InvalidParameter {
parameter: "precision",
message: format!(
"precision must be strictly positive, got {}",
options.precision
),
});
}
let prec = options.precision;
let mut segments: Vec<WorkSegment> = Vec::new();
for (line_idx, line) in lines.iter().enumerate() {
let snapped = snap_linestring(line, prec);
for pair in snapped.windows(2) {
let start = pair[0];
let end = pair[1];
if coords_equal_grid(&start, &end, prec) {
continue;
}
segments.push(WorkSegment {
start,
end,
source_line: line_idx,
});
}
}
let mut total_intersections_added: usize = 0;
let mut iterations_done: usize = 0;
for _iter in 0..options.max_iterations {
let new_pts = collect_cross_intersections(&segments, prec);
if new_pts.is_empty() {
break;
}
total_intersections_added += new_pts.len();
segments = insert_intersection_points(segments, &new_pts, prec);
iterations_done += 1;
}
let output: Vec<SnappedSegment> = segments
.into_iter()
.filter(|seg| !coords_equal_grid(&seg.start, &seg.end, prec))
.map(|seg| SnappedSegment {
start: seg.start,
end: seg.end,
source_line: seg.source_line,
})
.collect();
Ok(SnapRoundingResult {
segments: output,
intersections_added: total_intersections_added,
iterations: iterations_done,
})
}
fn collect_cross_intersections(segments: &[WorkSegment], prec: f64) -> Vec<Coordinate> {
use crate::vector::intersection::{SegmentIntersection, intersect_segment_segment};
let mut found: Vec<Coordinate> = Vec::new();
let n = segments.len();
for i in 0..n {
for j in (i + 1)..n {
let a = &segments[i];
let b = &segments[j];
if a.source_line == b.source_line {
continue;
}
match intersect_segment_segment(&a.start, &a.end, &b.start, &b.end) {
SegmentIntersection::Point(pt) => {
let (sx, sy) = snap_to_grid(pt.x, pt.y, prec);
let snapped = Coordinate::new_2d(sx, sy);
if !found.iter().any(|p| coords_equal_grid(p, &snapped, prec)) {
found.push(snapped);
}
}
SegmentIntersection::Overlap(c1, c2) => {
for &raw in &[c1, c2] {
let (sx, sy) = snap_to_grid(raw.x, raw.y, prec);
let snapped = Coordinate::new_2d(sx, sy);
if !found.iter().any(|p| coords_equal_grid(p, &snapped, prec)) {
found.push(snapped);
}
}
}
SegmentIntersection::None => {}
}
}
}
found
}
fn insert_intersection_points(
segments: Vec<WorkSegment>,
new_pts: &[Coordinate],
prec: f64,
) -> Vec<WorkSegment> {
let mut current = segments;
for pt in new_pts {
let mut next: Vec<WorkSegment> = Vec::with_capacity(current.len() + 4);
for seg in ¤t {
let at_start = coords_equal_grid(&seg.start, pt, prec);
let at_end = coords_equal_grid(&seg.end, pt, prec);
if at_start || at_end {
next.push(seg.clone());
continue;
}
if point_on_segment(pt, &seg.start, &seg.end, prec) {
let (left, right) = split_segment_at(seg, pt);
if !coords_equal_grid(&left.start, &left.end, prec) {
next.push(left);
}
if !coords_equal_grid(&right.start, &right.end, prec) {
next.push(right);
}
} else {
next.push(seg.clone());
}
}
current = next;
}
current
}
#[cfg(test)]
mod tests {
use super::*;
fn c(x: f64, y: f64) -> Coordinate {
Coordinate::new_2d(x, y)
}
#[test]
fn test_snap_coordinate_basic() {
let coord = c(1.2345678, -9.8765432);
let snapped = snap_coordinate(&coord, 1e-6);
assert!((snapped.x - 1.234568_f64).abs() < 1e-12);
assert!((snapped.y - -9.876543_f64).abs() < 1e-12);
}
#[test]
fn test_snap_linestring_no_duplicates() {
let coords = vec![c(0.0, 0.0), c(1e-9, 1e-9), c(1.0, 0.0)];
let result = snap_linestring(&coords, 1e-6);
assert_eq!(result.len(), 2);
assert!((result[0].x).abs() < 1e-12);
assert!((result[1].x - 1.0).abs() < 1e-12);
}
#[test]
fn test_snap_round_invalid_precision() {
let lines: Vec<Vec<Coordinate>> = vec![vec![c(0.0, 0.0), c(1.0, 1.0)]];
let opts = SnapRoundingOptions {
precision: -1.0,
max_iterations: 4,
};
assert!(snap_round(&lines, &opts).is_err());
let opts_zero = SnapRoundingOptions {
precision: 0.0,
max_iterations: 4,
};
assert!(snap_round(&lines, &opts_zero).is_err());
}
#[test]
fn test_point_on_segment_midpoint() {
let a = c(0.0, 0.0);
let b = c(10.0, 0.0);
let p = c(5.0, 0.0);
assert!(point_on_segment(&p, &a, &b, 1e-6));
}
#[test]
fn test_point_on_segment_off_line() {
let a = c(0.0, 0.0);
let b = c(10.0, 0.0);
let p = c(5.0, 1.0); assert!(!point_on_segment(&p, &a, &b, 1e-6));
}
}