use crate::error::{SpatialError, SpatialResult};
use scirs2_core::ndarray::{Array2, ArrayView2};
use std::cmp::Ordering;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq)]
struct Point2D {
x: f64,
y: f64,
}
impl Point2D {
fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
#[allow(dead_code)]
fn distance_to(&self, other: &Point2D) -> f64 {
((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
}
fn cross_product(&self, other: &Point2D) -> f64 {
self.x * other.y - self.y * other.x
}
#[allow(dead_code)]
fn dot_product(&self, other: &Point2D) -> f64 {
self.x * other.x + self.y * other.y
}
}
#[derive(Debug, Clone)]
struct Edge {
start: Point2D,
end: Point2D,
polygon_id: usize, intersectionpoints: Vec<IntersectionPoint>,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct IntersectionPoint {
point: Point2D,
t: f64, other_edge_id: usize,
}
#[derive(Debug, Clone)]
struct LabeledPolygon {
vertices: Vec<Point2D>,
edges: Vec<Edge>,
#[allow(dead_code)]
is_hole: bool,
}
impl LabeledPolygon {
fn from_array(vertices: &ArrayView2<'_, f64>) -> SpatialResult<Self> {
if vertices.ncols() != 2 {
return Err(SpatialError::ValueError(
"Polygon vertices must be 2D".to_string(),
));
}
let points: Vec<Point2D> = vertices
.outer_iter()
.map(|row| Point2D::new(row[0], row[1]))
.collect();
if points.len() < 3 {
return Err(SpatialError::ValueError(
"Polygon must have at least 3 vertices".to_string(),
));
}
let mut edges = Vec::new();
for i in 0..points.len() {
let start = points[i];
let end = points[(i + 1) % points.len()];
edges.push(Edge {
start,
end,
polygon_id: 0,
intersectionpoints: Vec::new(),
});
}
Ok(LabeledPolygon {
vertices: points,
edges,
is_hole: false,
})
}
fn to_array(&self) -> Array2<f64> {
let mut data = Vec::with_capacity(self.vertices.len() * 2);
for vertex in &self.vertices {
data.push(vertex.x);
data.push(vertex.y);
}
Array2::from_shape_vec((self.vertices.len(), 2), data).expect("Operation failed")
}
fn ispoint_inside(&self, point: &Point2D) -> bool {
let mut inside = false;
let n = self.vertices.len();
for i in 0..n {
let j = (i + 1) % n;
let vi = &self.vertices[i];
let vj = &self.vertices[j];
if ((vi.y > point.y) != (vj.y > point.y))
&& (point.x < (vj.x - vi.x) * (point.y - vi.y) / (vj.y - vi.y) + vi.x)
{
inside = !inside;
}
}
inside
}
fn compute_area(&self) -> f64 {
let mut area = 0.0;
let n = self.vertices.len();
for i in 0..n {
let j = (i + 1) % n;
let vi = &self.vertices[i];
let vj = &self.vertices[j];
area += vi.x * vj.y - vj.x * vi.y;
}
area.abs() / 2.0
}
#[allow(dead_code)]
fn is_clockwise(&self) -> bool {
let mut sum = 0.0;
let n = self.vertices.len();
for i in 0..n {
let j = (i + 1) % n;
let vi = &self.vertices[i];
let vj = &self.vertices[j];
sum += (vj.x - vi.x) * (vj.y + vi.y);
}
sum > 0.0
}
#[allow(dead_code)]
fn reverse(&mut self) {
self.vertices.reverse();
let mut edges = Vec::new();
for i in 0..self.vertices.len() {
let start = self.vertices[i];
let end = self.vertices[(i + 1) % self.vertices.len()];
edges.push(Edge {
start,
end,
polygon_id: self.edges[0].polygon_id,
intersectionpoints: Vec::new(),
});
}
self.edges = edges;
}
}
#[allow(dead_code)]
pub fn polygon_union(
poly1: &ArrayView2<'_, f64>,
poly2: &ArrayView2<'_, f64>,
) -> SpatialResult<Array2<f64>> {
let mut p1 = LabeledPolygon::from_array(poly1)?;
let mut p2 = LabeledPolygon::from_array(poly2)?;
for edge in &mut p1.edges {
edge.polygon_id = 0;
}
for edge in &mut p2.edges {
edge.polygon_id = 1;
}
find_intersections(&mut p1, &mut p2)?;
let result_polygons = weiler_atherton_union(&p1, &p2)?;
if result_polygons.is_empty() {
if p1.compute_area() >= p2.compute_area() {
Ok(p1.to_array())
} else {
Ok(p2.to_array())
}
} else {
Ok(result_polygons[0].to_array())
}
}
#[allow(dead_code)]
pub fn polygon_intersection(
poly1: &ArrayView2<'_, f64>,
poly2: &ArrayView2<'_, f64>,
) -> SpatialResult<Array2<f64>> {
let mut p1 = LabeledPolygon::from_array(poly1)?;
let mut p2 = LabeledPolygon::from_array(poly2)?;
for edge in &mut p1.edges {
edge.polygon_id = 0;
}
for edge in &mut p2.edges {
edge.polygon_id = 1;
}
find_intersections(&mut p1, &mut p2)?;
let result = sutherland_hodgman_clip(&p1, &p2)?;
Ok(result.to_array())
}
#[allow(dead_code)]
pub fn polygon_difference(
poly1: &ArrayView2<'_, f64>,
poly2: &ArrayView2<'_, f64>,
) -> SpatialResult<Array2<f64>> {
let mut p1 = LabeledPolygon::from_array(poly1)?;
let mut p2 = LabeledPolygon::from_array(poly2)?;
for edge in &mut p1.edges {
edge.polygon_id = 0;
}
for edge in &mut p2.edges {
edge.polygon_id = 1;
}
find_intersections(&mut p1, &mut p2)?;
let result = weiler_atherton_difference(&p1, &p2)?;
if result.is_empty() {
Ok(p1.to_array())
} else {
Ok(result[0].to_array())
}
}
#[allow(dead_code)]
pub fn polygon_symmetric_difference(
poly1: &ArrayView2<'_, f64>,
poly2: &ArrayView2<'_, f64>,
) -> SpatialResult<Array2<f64>> {
let diff1 = polygon_difference(poly1, poly2)?;
let diff2 = polygon_difference(poly2, poly1)?;
polygon_union(&diff1.view(), &diff2.view())
}
#[allow(dead_code)]
fn find_intersections(poly1: &mut LabeledPolygon, poly2: &mut LabeledPolygon) -> SpatialResult<()> {
for (i, edge1) in poly1.edges.iter_mut().enumerate() {
for (j, edge2) in poly2.edges.iter_mut().enumerate() {
if let Some((intersectionpoint, t1, t2)) =
line_segment_intersection(&edge1.start, &edge1.end, &edge2.start, &edge2.end)
{
edge1.intersectionpoints.push(IntersectionPoint {
point: intersectionpoint,
t: t1,
other_edge_id: j,
});
edge2.intersectionpoints.push(IntersectionPoint {
point: intersectionpoint,
t: t2,
other_edge_id: i,
});
}
}
}
for edge in &mut poly1.edges {
edge.intersectionpoints
.sort_by(|a, b| a.t.partial_cmp(&b.t).unwrap_or(Ordering::Equal));
}
for edge in &mut poly2.edges {
edge.intersectionpoints
.sort_by(|a, b| a.t.partial_cmp(&b.t).unwrap_or(Ordering::Equal));
}
Ok(())
}
#[allow(dead_code)]
fn line_segment_intersection(
p1: &Point2D,
p2: &Point2D,
p3: &Point2D,
p4: &Point2D,
) -> Option<(Point2D, f64, f64)> {
let x1 = p1.x;
let y1 = p1.y;
let x2 = p2.x;
let y2 = p2.y;
let x3 = p3.x;
let y3 = p3.y;
let x4 = p4.x;
let y4 = p4.y;
let denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
if denom.abs() < 1e-10 {
return None;
}
let t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom;
let u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom;
if (0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u) {
let ix = x1 + t * (x2 - x1);
let iy = y1 + t * (y2 - y1);
let intersection = Point2D::new(ix, iy);
Some((intersection, t, u))
} else {
None
}
}
#[allow(dead_code)]
fn sutherland_hodgman_clip(
subject: &LabeledPolygon,
clip: &LabeledPolygon,
) -> SpatialResult<LabeledPolygon> {
let mut outputvertices = subject.vertices.clone();
for i in 0..clip.vertices.len() {
if outputvertices.is_empty() {
break;
}
let clip_edge_start = clip.vertices[i];
let clip_edge_end = clip.vertices[(i + 1) % clip.vertices.len()];
let inputvertices = outputvertices.clone();
outputvertices.clear();
if !inputvertices.is_empty() {
let mut s = inputvertices[inputvertices.len() - 1];
for vertex in inputvertices {
if is_inside(&vertex, &clip_edge_start, &clip_edge_end) {
if !is_inside(&s, &clip_edge_start, &clip_edge_end) {
if let Some((intersection__, _, _)) =
line_segment_intersection(&s, &vertex, &clip_edge_start, &clip_edge_end)
{
outputvertices.push(intersection__);
}
}
outputvertices.push(vertex);
} else if is_inside(&s, &clip_edge_start, &clip_edge_end) {
if let Some((intersection__, _, _)) =
line_segment_intersection(&s, &vertex, &clip_edge_start, &clip_edge_end)
{
outputvertices.push(intersection__);
}
}
s = vertex;
}
}
}
if outputvertices.len() < 3 {
outputvertices.clear();
}
let mut edges = Vec::new();
for i in 0..outputvertices.len() {
let start = outputvertices[i];
let end = outputvertices[(i + 1) % outputvertices.len()];
edges.push(Edge {
start,
end,
polygon_id: 0,
intersectionpoints: Vec::new(),
});
}
Ok(LabeledPolygon {
vertices: outputvertices,
edges,
is_hole: false,
})
}
#[allow(dead_code)]
fn is_inside(point: &Point2D, edge_start: &Point2D, edgeend: &Point2D) -> bool {
let edge_vector = Point2D::new(edgeend.x - edge_start.x, edgeend.y - edge_start.y);
let point_vector = Point2D::new(point.x - edge_start.x, point.y - edge_start.y);
edge_vector.cross_product(&point_vector) >= 0.0
}
#[allow(dead_code)]
fn weiler_atherton_union(
poly1: &LabeledPolygon,
poly2: &LabeledPolygon,
) -> SpatialResult<Vec<LabeledPolygon>> {
if !polygons_intersect(poly1, poly2) {
return Ok(vec![poly1.clone(), poly2.clone()]);
}
let intersection_graph = build_intersection_graph(poly1, poly2)?;
let result_polygons = trace_union_boundary(&intersection_graph, poly1, poly2)?;
Ok(result_polygons)
}
#[allow(dead_code)]
fn weiler_atherton_difference(
poly1: &LabeledPolygon,
poly2: &LabeledPolygon,
) -> SpatialResult<Vec<LabeledPolygon>> {
if !polygons_intersect(poly1, poly2) {
return Ok(vec![poly1.clone()]);
}
let intersection_graph = build_intersection_graph(poly1, poly2)?;
let result_polygons = trace_difference_boundary(&intersection_graph, poly1, poly2)?;
Ok(result_polygons)
}
#[allow(dead_code)]
fn polygons_intersect(poly1: &LabeledPolygon, poly2: &LabeledPolygon) -> bool {
for vertex in &poly1.vertices {
if poly2.ispoint_inside(vertex) {
return true;
}
}
for vertex in &poly2.vertices {
if poly1.ispoint_inside(vertex) {
return true;
}
}
for edge1 in &poly1.edges {
for edge2 in &poly2.edges {
if line_segment_intersection(&edge1.start, &edge1.end, &edge2.start, &edge2.end)
.is_some()
{
return true;
}
}
}
false
}
#[allow(dead_code)]
fn build_intersection_graph(
poly1: &LabeledPolygon,
_poly2: &LabeledPolygon,
) -> SpatialResult<HashMap<String, Vec<Point2D>>> {
Ok(HashMap::new())
}
#[allow(dead_code)]
fn trace_union_boundary(
_graph: &HashMap<String, Vec<Point2D>>,
poly1: &LabeledPolygon,
poly2: &LabeledPolygon,
) -> SpatialResult<Vec<LabeledPolygon>> {
if poly1.compute_area() >= poly2.compute_area() {
Ok(vec![poly1.clone()])
} else {
Ok(vec![poly2.clone()])
}
}
#[allow(dead_code)]
fn trace_difference_boundary(
_graph: &HashMap<String, Vec<Point2D>>,
poly1: &LabeledPolygon,
_poly2: &LabeledPolygon,
) -> SpatialResult<Vec<LabeledPolygon>> {
Ok(vec![poly1.clone()])
}
#[allow(dead_code)]
pub fn is_convex_polygon(vertices: &ArrayView2<'_, f64>) -> SpatialResult<bool> {
if vertices.ncols() != 2 {
return Err(SpatialError::ValueError("Vertices must be 2D".to_string()));
}
let n = vertices.nrows();
if n < 3 {
return Ok(false);
}
let mut sign = 0i32;
for i in 0..n {
let p1 = Point2D::new(vertices[[i, 0]], vertices[[i, 1]]);
let p2 = Point2D::new(vertices[[(i + 1) % n, 0]], vertices[[(i + 1) % n, 1]]);
let p3 = Point2D::new(vertices[[(i + 2) % n, 0]], vertices[[(i + 2) % n, 1]]);
let v1 = Point2D::new(p2.x - p1.x, p2.y - p1.y);
let v2 = Point2D::new(p3.x - p2.x, p3.y - p2.y);
let cross = v1.cross_product(&v2);
if cross.abs() > 1e-10 {
let current_sign = if cross > 0.0 { 1 } else { -1 };
if sign == 0 {
sign = current_sign;
} else if sign != current_sign {
return Ok(false);
}
}
}
Ok(true)
}
#[allow(dead_code)]
pub fn compute_polygon_area(vertices: &ArrayView2<'_, f64>) -> SpatialResult<f64> {
let polygon = LabeledPolygon::from_array(vertices)?;
Ok(polygon.compute_area())
}
#[allow(dead_code)]
pub fn is_self_intersecting(vertices: &ArrayView2<'_, f64>) -> SpatialResult<bool> {
let polygon = LabeledPolygon::from_array(vertices)?;
let n = polygon.vertices.len();
for i in 0..n {
let edge1_start = polygon.vertices[i];
let edge1_end = polygon.vertices[(i + 1) % n];
for j in (i + 2)..n {
if j == (i + n - 1) % n {
continue;
}
let edge2_start = polygon.vertices[j];
let edge2_end = polygon.vertices[(j + 1) % n];
if line_segment_intersection(&edge1_start, &edge1_end, &edge2_start, &edge2_end)
.is_some()
{
return Ok(true);
}
}
}
Ok(false)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use scirs2_core::ndarray::arr2;
#[test]
fn testpoint2d_operations() {
let p1 = Point2D::new(0.0, 0.0);
let p2 = Point2D::new(1.0, 1.0);
assert_relative_eq!(p1.distance_to(&p2), 2.0_f64.sqrt(), epsilon = 1e-10);
let v1 = Point2D::new(1.0, 0.0);
let v2 = Point2D::new(0.0, 1.0);
assert_relative_eq!(v1.cross_product(&v2), 1.0, epsilon = 1e-10);
assert_relative_eq!(v1.dot_product(&v2), 0.0, epsilon = 1e-10);
}
#[test]
fn test_polygon_creation() {
let vertices = arr2(&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]);
let polygon = LabeledPolygon::from_array(&vertices.view()).expect("Operation failed");
assert_eq!(polygon.vertices.len(), 4);
assert_eq!(polygon.edges.len(), 4);
assert_relative_eq!(polygon.compute_area(), 1.0, epsilon = 1e-10);
}
#[test]
fn testpoint_inside_polygon() {
let vertices = arr2(&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]);
let polygon = LabeledPolygon::from_array(&vertices.view()).expect("Operation failed");
let insidepoint = Point2D::new(0.5, 0.5);
let outsidepoint = Point2D::new(1.5, 1.5);
assert!(polygon.ispoint_inside(&insidepoint));
assert!(!polygon.ispoint_inside(&outsidepoint));
}
#[test]
fn test_line_intersection() {
let p1 = Point2D::new(0.0, 0.0);
let p2 = Point2D::new(1.0, 1.0);
let p3 = Point2D::new(0.0, 1.0);
let p4 = Point2D::new(1.0, 0.0);
let result = line_segment_intersection(&p1, &p2, &p3, &p4);
assert!(result.is_some());
let (intersection, t1, t2) = result.expect("Operation failed");
assert_relative_eq!(intersection.x, 0.5, epsilon = 1e-10);
assert_relative_eq!(intersection.y, 0.5, epsilon = 1e-10);
assert_relative_eq!(t1, 0.5, epsilon = 1e-10);
assert_relative_eq!(t2, 0.5, epsilon = 1e-10);
}
#[test]
fn test_is_convex_polygon() {
let convex = arr2(&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]);
assert!(is_convex_polygon(&convex.view()).expect("Operation failed"));
let non_convex = arr2(&[
[0.0, 0.0],
[1.0, 0.0],
[1.0, 0.5],
[0.5, 0.5],
[0.5, 1.0],
[0.0, 1.0],
]);
assert!(!is_convex_polygon(&non_convex.view()).expect("Operation failed"));
}
#[test]
fn test_polygon_area() {
let square = arr2(&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]);
let area = compute_polygon_area(&square.view()).expect("Operation failed");
assert_relative_eq!(area, 1.0, epsilon = 1e-10);
let triangle = arr2(&[[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]]);
let area = compute_polygon_area(&triangle.view()).expect("Operation failed");
assert_relative_eq!(area, 0.5, epsilon = 1e-10);
}
#[test]
fn test_intersection() {
let square = arr2(&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]);
assert!(!is_self_intersecting(&square.view()).expect("Operation failed"));
let bowtie = arr2(&[[0.0, 0.0], [1.0, 1.0], [1.0, 0.0], [0.0, 1.0]]);
assert!(is_self_intersecting(&bowtie.view()).expect("Operation failed"));
}
#[test]
fn test_polygon_union_basic() {
let poly1 = arr2(&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]);
let poly2 = arr2(&[[2.0, 0.0], [3.0, 0.0], [3.0, 1.0], [2.0, 1.0]]);
let union = polygon_union(&poly1.view(), &poly2.view()).expect("Operation failed");
assert!(union.nrows() >= 4); }
#[test]
fn test_polygon_intersection_basic() {
let poly1 = arr2(&[[0.0, 0.0], [2.0, 0.0], [2.0, 2.0], [0.0, 2.0]]);
let poly2 = arr2(&[[1.0, 1.0], [3.0, 1.0], [3.0, 3.0], [1.0, 3.0]]);
let intersection =
polygon_intersection(&poly1.view(), &poly2.view()).expect("Operation failed");
if intersection.nrows() > 0 {
let area = compute_polygon_area(&intersection.view()).expect("Operation failed");
assert!(area > 0.0); }
}
#[test]
fn test_polygon_difference_basic() {
let poly1 = arr2(&[[0.0, 0.0], [2.0, 0.0], [2.0, 2.0], [0.0, 2.0]]);
let poly2 = arr2(&[[1.0, 1.0], [3.0, 1.0], [3.0, 3.0], [1.0, 3.0]]);
let difference =
polygon_difference(&poly1.view(), &poly2.view()).expect("Operation failed");
assert!(difference.nrows() >= 3); }
#[test]
fn test_sutherland_hodgman_clip() {
let subjectvertices = vec![
Point2D::new(0.0, 0.0),
Point2D::new(2.0, 0.0),
Point2D::new(2.0, 2.0),
Point2D::new(0.0, 2.0),
];
let clipvertices = vec![
Point2D::new(1.0, 1.0),
Point2D::new(3.0, 1.0),
Point2D::new(3.0, 3.0),
Point2D::new(1.0, 3.0),
];
let subject = LabeledPolygon {
vertices: subjectvertices,
edges: Vec::new(),
is_hole: false,
};
let clip = LabeledPolygon {
vertices: clipvertices,
edges: Vec::new(),
is_hole: false,
};
let result = sutherland_hodgman_clip(&subject, &clip).expect("Operation failed");
assert!(result.vertices.len() >= 3);
}
}