use crate::error::{AlgorithmError, Result};
use oxigdal_core::vector::{Coordinate, LineString, Polygon};
#[cfg(feature = "std")]
use std::vec::Vec;
#[derive(Debug, Clone)]
pub struct RepairOptions {
pub remove_duplicates: bool,
pub fix_orientation: bool,
pub close_rings: bool,
pub remove_spikes: bool,
pub remove_collinear: bool,
pub tolerance: f64,
}
impl Default for RepairOptions {
fn default() -> Self {
Self {
remove_duplicates: true,
fix_orientation: true,
close_rings: true,
remove_spikes: true,
remove_collinear: false,
tolerance: f64::EPSILON,
}
}
}
impl RepairOptions {
pub fn all() -> Self {
Self {
remove_duplicates: true,
fix_orientation: true,
close_rings: true,
remove_spikes: true,
remove_collinear: true,
tolerance: f64::EPSILON,
}
}
pub fn basic() -> Self {
Self {
remove_duplicates: true,
fix_orientation: false,
close_rings: true,
remove_spikes: false,
remove_collinear: false,
tolerance: f64::EPSILON,
}
}
pub fn with_tolerance(mut self, tolerance: f64) -> Self {
self.tolerance = tolerance;
self
}
}
pub fn repair_polygon(polygon: &Polygon) -> Result<Polygon> {
repair_polygon_with_options(polygon, &RepairOptions::default())
}
pub fn repair_polygon_with_options(polygon: &Polygon, options: &RepairOptions) -> Result<Polygon> {
let exterior_coords = repair_ring(&polygon.exterior.coords, true, options)?;
let exterior = LineString::new(exterior_coords).map_err(|e| AlgorithmError::GeometryError {
message: format!("Failed to create exterior ring: {}", e),
})?;
let mut interiors = Vec::new();
for hole in &polygon.interiors {
let hole_coords = repair_ring(&hole.coords, false, options)?;
let hole_ring =
LineString::new(hole_coords).map_err(|e| AlgorithmError::GeometryError {
message: format!("Failed to create interior ring: {}", e),
})?;
interiors.push(hole_ring);
}
Polygon::new(exterior, interiors).map_err(|e| AlgorithmError::GeometryError {
message: format!("Failed to create repaired polygon: {}", e),
})
}
pub fn repair_linestring(linestring: &LineString) -> Result<LineString> {
repair_linestring_with_options(linestring, &RepairOptions::default())
}
pub fn repair_linestring_with_options(
linestring: &LineString,
options: &RepairOptions,
) -> Result<LineString> {
let mut coords = linestring.coords.clone();
if options.remove_duplicates {
coords = remove_duplicate_vertices(&coords, options.tolerance);
}
if options.remove_collinear {
coords = remove_collinear_vertices(&coords, options.tolerance);
}
if options.remove_spikes {
coords = remove_spikes(&coords, options.tolerance);
}
LineString::new(coords).map_err(|e| AlgorithmError::GeometryError {
message: format!("Failed to create repaired linestring: {}", e),
})
}
fn repair_ring(
coords: &[Coordinate],
is_exterior: bool,
options: &RepairOptions,
) -> Result<Vec<Coordinate>> {
if coords.is_empty() {
return Err(AlgorithmError::EmptyInput {
operation: "repair_ring",
});
}
let mut result = coords.to_vec();
if options.close_rings
&& !coords_equal(
&result[0],
result
.last()
.ok_or_else(|| AlgorithmError::InsufficientData {
operation: "repair_ring",
message: "Ring has no points".to_string(),
})?,
options.tolerance,
)
{
result.push(result[0]);
}
if options.remove_duplicates {
result = remove_duplicate_vertices(&result, options.tolerance);
}
if options.remove_spikes {
result = remove_spikes(&result, options.tolerance);
}
if options.remove_collinear {
result = remove_collinear_vertices(&result, options.tolerance);
}
if options.fix_orientation {
let is_ccw = is_counter_clockwise(&result);
if is_exterior && !is_ccw {
result.reverse();
} else if !is_exterior && is_ccw {
result.reverse();
}
}
if result.len() < 4 {
return Err(AlgorithmError::InsufficientData {
operation: "repair_ring",
message: format!(
"Ring must have at least 4 points after repair, got {}",
result.len()
),
});
}
Ok(result)
}
pub fn remove_duplicate_vertices(coords: &[Coordinate], tolerance: f64) -> Vec<Coordinate> {
if coords.is_empty() {
return Vec::new();
}
let mut result = Vec::with_capacity(coords.len());
result.push(coords[0]);
for i in 1..coords.len() {
if !coords_equal_with_tolerance(&coords[i], &coords[i - 1], tolerance) {
result.push(coords[i]);
}
}
result
}
pub fn remove_collinear_vertices(coords: &[Coordinate], tolerance: f64) -> Vec<Coordinate> {
if coords.len() < 3 {
return coords.to_vec();
}
let mut result = Vec::with_capacity(coords.len());
result.push(coords[0]);
for i in 1..coords.len() - 1 {
if !are_collinear_with_tolerance(&coords[i - 1], &coords[i], &coords[i + 1], tolerance) {
result.push(coords[i]);
}
}
result.push(coords[coords.len() - 1]);
result
}
pub fn remove_spikes(coords: &[Coordinate], tolerance: f64) -> Vec<Coordinate> {
if coords.len() < 3 {
return coords.to_vec();
}
let mut result = Vec::with_capacity(coords.len());
result.push(coords[0]);
for i in 1..coords.len() - 1 {
if !is_spike(&coords[i - 1], &coords[i], &coords[i + 1], tolerance) {
result.push(coords[i]);
}
}
result.push(coords[coords.len() - 1]);
result
}
pub fn reverse_ring(coords: &[Coordinate]) -> Vec<Coordinate> {
let mut result = coords.to_vec();
result.reverse();
result
}
pub fn close_ring(coords: &[Coordinate]) -> Result<Vec<Coordinate>> {
if coords.is_empty() {
return Err(AlgorithmError::EmptyInput {
operation: "close_ring",
});
}
let mut result = coords.to_vec();
if !coords_equal(
&result[0],
result
.last()
.ok_or_else(|| AlgorithmError::InsufficientData {
operation: "close_ring",
message: "Ring has no points".to_string(),
})?,
f64::EPSILON,
) {
result.push(result[0]);
}
Ok(result)
}
fn coords_equal_with_tolerance(c1: &Coordinate, c2: &Coordinate, tolerance: f64) -> bool {
(c1.x - c2.x).abs() < tolerance && (c1.y - c2.y).abs() < tolerance
}
fn coords_equal(c1: &Coordinate, c2: &Coordinate, tolerance: f64) -> bool {
coords_equal_with_tolerance(c1, c2, tolerance)
}
fn are_collinear_with_tolerance(
p1: &Coordinate,
p2: &Coordinate,
p3: &Coordinate,
tolerance: f64,
) -> bool {
let cross = (p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y);
cross.abs() < tolerance.max(f64::EPSILON)
}
fn is_spike(prev: &Coordinate, curr: &Coordinate, next: &Coordinate, tolerance: f64) -> bool {
let dx1 = curr.x - prev.x;
let dy1 = curr.y - prev.y;
let dx2 = next.x - curr.x;
let dy2 = next.y - curr.y;
let len1_sq = dx1 * dx1 + dy1 * dy1;
let len2_sq = dx2 * dx2 + dy2 * dy2;
if len1_sq < tolerance || len2_sq < tolerance {
return false;
}
let dot = dx1 * dx2 + dy1 * dy2;
let len1 = len1_sq.sqrt();
let len2 = len2_sq.sqrt();
let cos_angle = dot / (len1 * len2);
cos_angle < -0.99
}
fn is_counter_clockwise(coords: &[Coordinate]) -> bool {
let mut area = 0.0;
let n = coords.len();
for i in 0..n {
let j = (i + 1) % n;
area += coords[i].x * coords[j].y;
area -= coords[j].x * coords[i].y;
}
area > 0.0
}
pub fn fix_self_intersection(polygon: &Polygon) -> Result<Polygon> {
repair_polygon(polygon)
}
#[cfg(test)]
mod tests {
use super::*;
fn create_square_coords() -> Vec<Coordinate> {
vec![
Coordinate::new_2d(0.0, 0.0),
Coordinate::new_2d(4.0, 0.0),
Coordinate::new_2d(4.0, 4.0),
Coordinate::new_2d(0.0, 4.0),
Coordinate::new_2d(0.0, 0.0),
]
}
#[test]
fn test_remove_duplicate_vertices() {
let coords = vec![
Coordinate::new_2d(0.0, 0.0),
Coordinate::new_2d(1.0, 0.0),
Coordinate::new_2d(1.0, 0.0), Coordinate::new_2d(2.0, 0.0),
];
let result = remove_duplicate_vertices(&coords, f64::EPSILON);
assert_eq!(result.len(), 3);
}
#[test]
fn test_remove_collinear_vertices() {
let coords = vec![
Coordinate::new_2d(0.0, 0.0),
Coordinate::new_2d(1.0, 1.0),
Coordinate::new_2d(2.0, 2.0), Coordinate::new_2d(3.0, 0.0),
];
let result = remove_collinear_vertices(&coords, f64::EPSILON);
assert_eq!(result.len(), 3);
}
#[test]
fn test_remove_spikes() {
let coords = vec![
Coordinate::new_2d(0.0, 0.0),
Coordinate::new_2d(2.0, 0.0),
Coordinate::new_2d(2.001, 0.0), Coordinate::new_2d(0.5, 0.0), Coordinate::new_2d(4.0, 0.0),
];
let result = remove_spikes(&coords, 0.01);
assert!(result.len() >= 2);
assert!(result.len() <= coords.len());
}
#[test]
fn test_close_ring() {
let coords = vec![
Coordinate::new_2d(0.0, 0.0),
Coordinate::new_2d(4.0, 0.0),
Coordinate::new_2d(4.0, 4.0),
Coordinate::new_2d(0.0, 4.0),
];
let result = close_ring(&coords);
assert!(result.is_ok());
if let Ok(closed) = result {
assert_eq!(closed.len(), 5);
assert!(coords_equal(&closed[0], &closed[4], f64::EPSILON));
}
}
#[test]
fn test_reverse_ring() {
let coords = create_square_coords();
let reversed = reverse_ring(&coords);
assert_eq!(coords.len(), reversed.len());
assert!(coords_equal(
&coords[0],
&reversed[reversed.len() - 1],
f64::EPSILON
));
}
#[test]
fn test_repair_polygon() {
let coords = vec![
Coordinate::new_2d(0.0, 0.0),
Coordinate::new_2d(4.0, 0.0),
Coordinate::new_2d(4.0, 0.0), Coordinate::new_2d(4.0, 4.0),
Coordinate::new_2d(0.0, 4.0),
Coordinate::new_2d(0.0, 0.0), ];
let exterior = LineString::new(coords);
assert!(exterior.is_ok());
if let Ok(ext) = exterior {
let polygon = Polygon::new(ext, vec![]);
if let Ok(poly) = polygon {
let result = repair_polygon(&poly);
assert!(result.is_ok());
if let Ok(repaired) = result {
assert!(repaired.exterior.coords.len() >= 4);
assert!(coords_equal(
&repaired.exterior.coords[0],
repaired
.exterior
.coords
.last()
.ok_or(())
.map_err(|_| ())
.expect("has last"),
f64::EPSILON
));
}
}
}
}
#[test]
fn test_repair_linestring() {
let coords = vec![
Coordinate::new_2d(0.0, 0.0),
Coordinate::new_2d(1.0, 0.0),
Coordinate::new_2d(1.0, 0.0), Coordinate::new_2d(2.0, 0.0),
];
let linestring = LineString::new(coords);
assert!(linestring.is_ok());
if let Ok(ls) = linestring {
let result = repair_linestring(&ls);
assert!(result.is_ok());
if let Ok(repaired) = result {
assert_eq!(repaired.coords.len(), 3);
}
}
}
#[test]
fn test_repair_options() {
let options = RepairOptions::default();
assert!(options.remove_duplicates);
assert!(options.close_rings);
let all_options = RepairOptions::all();
assert!(all_options.remove_collinear);
let basic_options = RepairOptions::basic();
assert!(!basic_options.remove_spikes);
}
#[test]
fn test_is_counter_clockwise() {
let ccw = vec![
Coordinate::new_2d(0.0, 0.0),
Coordinate::new_2d(4.0, 0.0),
Coordinate::new_2d(4.0, 4.0),
Coordinate::new_2d(0.0, 4.0),
Coordinate::new_2d(0.0, 0.0),
];
assert!(is_counter_clockwise(&ccw));
let cw = vec![
Coordinate::new_2d(0.0, 0.0),
Coordinate::new_2d(0.0, 4.0),
Coordinate::new_2d(4.0, 4.0),
Coordinate::new_2d(4.0, 0.0),
Coordinate::new_2d(0.0, 0.0),
];
assert!(!is_counter_clockwise(&cw));
}
#[test]
fn test_repair_with_custom_options() {
let coords = vec![
Coordinate::new_2d(0.0, 0.0),
Coordinate::new_2d(1.0, 1.0),
Coordinate::new_2d(2.0, 2.0), Coordinate::new_2d(3.0, 0.0),
];
let linestring = LineString::new(coords);
assert!(linestring.is_ok());
if let Ok(ls) = linestring {
let options = RepairOptions::default().with_tolerance(1e-6);
let result = repair_linestring_with_options(&ls, &options);
assert!(result.is_ok());
}
}
}