Skip to content

Commit

Permalink
Add basic validation of polygons on creation (#25)
Browse files Browse the repository at this point in the history
This change adds basic validation of polygons on creation, validating:
1. Number of vertices (at least 3)
2. Valid cycle visiting every vertex once in chain
3. Edge intersection validation, that adjacent edges share a vertex and
non-adjacent edges do not intersect
  • Loading branch information
adamconkey authored Dec 29, 2024
1 parent 3f4786d commit 44e5875
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 12 deletions.
112 changes: 100 additions & 12 deletions computational_geometry/src/polygon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ pub struct Polygon {
impl Polygon {
pub fn new(points: Vec<Point>) -> Polygon {
let vertex_map = VertexMap::new(points);
Polygon { vertex_map }
let polygon = Polygon { vertex_map };
polygon.validate();
polygon
}

pub fn from_json<P: AsRef<Path>>(path: P) -> Polygon {
Expand All @@ -88,17 +90,7 @@ impl Polygon {
pub fn to_json<P: AsRef<Path>>(&self, path: P) {
// TODO return result

// Getting a vec of vertices ordered by vertex ID so that it
// matches the order of input points as closely as possible.
let mut vertices = self.vertex_map
.values()
.collect::<Vec<&Vertex>>();
vertices.sort_by(|a, b| a.id.cmp(&b.id));

let points = vertices
.iter()
.map(|v| v.coords.clone())
.collect::<Vec<Point>>();
let points = self.vertex_map.sorted_points();
let points_str = serde_json::to_string(&points).unwrap();
// TODO don't expect below or unwrap above, want to return result
// where it can possibly error on serialization or file write
Expand Down Expand Up @@ -221,6 +213,81 @@ impl Polygon {
}
true
}

pub fn validate(&self) {
self.validate_num_vertices();
self.validate_cycle();
self.validate_edge_intersections();
}

fn validate_num_vertices(&self) {
let num_vertices = self.num_vertices();
assert!(
num_vertices >= 3,
"Polygon must have at least 3 vertices, \
this one has {num_vertices}"
);
}

fn validate_cycle(&self) {
// Walk the chain and terminate once a loop closure is
// encountered, then validate every vertex was visited
// once. Note the loop must terminate since there are
// finite vertices and visited vertices are tracked.
let anchor = self.vertex_map.anchor();
let mut current = self.vertex_map.anchor();
let mut visited = HashSet::<VertexId>::new();

loop {
visited.insert(current.id);
current = self.vertex_map.get(&current.next);
if current.id == anchor.id || visited.contains(&current.id) {
break;
}
}

let mut not_visited = HashSet::<VertexId>::new();
for v in self.vertex_map.sorted_vertices() {
if !visited.contains(&v.id) {
not_visited.insert(v.id);
}
}
assert!(
not_visited.is_empty(),
"Expected vertex chain to form a cycle but these \
vertices were not visited: {not_visited:?}"
);
}

fn validate_edge_intersections(&self) {
let mut edges = Vec::new();
let anchor_id = self.vertex_map.anchor().id;
let mut current = self.get_vertex(&anchor_id);
loop {
let next = self.get_vertex(&current.next);
let ls = LineSegment::from_vertices(current, next);
edges.push(ls);
current = next;
if current.id == anchor_id {
break;
}
}

for i in 0..(edges.len() - 1) {
let e1 = &edges[i];
// Adjacent edges should share a common vertex
assert!(e1.incident_to(edges[i+1].p1));
for e2 in edges.iter().take(edges.len() -1).skip(i+2) {
// Non-adjacent edges should have no intersection
assert!(!e1.intersects(e2));
assert!(!e1.incident_to(e2.p1));
assert!(!e1.incident_to(e2.p2));
assert!(!e2.intersects(e1));
assert!(!e2.incident_to(e1.p1));
assert!(!e2.incident_to(e1.p2));
}
}
}
}


Expand Down Expand Up @@ -354,6 +421,27 @@ mod tests {
fn all_polygons(#[case] case: PolygonTestCase) {}


#[test]
#[should_panic]
fn test_invalid_polygon_not_enough_vertices() {
let p1 = Point::new(1, 2);
let p2 = Point::new(3, 4);
let points = vec![p1, p2];
let _polygon = Polygon::new(points);
}

#[test]
#[should_panic]
fn test_invalid_polygon_not_simple() {
let p1 = Point::new(0, 0);
let p2 = Point::new(2, 0);
let p3 = Point::new(2, 2);
let p4 = Point::new(0, 2);
let p5 = Point::new(4, 1); // This one should break it
let points = vec![p1, p2, p3, p4, p5];
let _polygon = Polygon::new(points);
}

#[apply(all_polygons)]
fn test_json(case: PolygonTestCase) {
let filename = NamedTempFile::new()
Expand Down
8 changes: 8 additions & 0 deletions computational_geometry/src/vertex.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::fmt;

use crate::{
line_segment::LineSegment,
point::Point,
Expand All @@ -19,6 +21,12 @@ impl From<usize> for VertexId {
}
}

impl fmt::Display for VertexId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}


#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Vertex {
Expand Down
14 changes: 14 additions & 0 deletions computational_geometry/src/vertex_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,20 @@ impl VertexMap {
self.map.values()
}

pub fn sorted_vertices(&self) -> Vec<&Vertex> {
let mut vertices = self.values()
.collect::<Vec<&Vertex>>();
vertices.sort_by(|a, b| a.id.cmp(&b.id));
vertices
}

pub fn sorted_points(&self) -> Vec<Point> {
self.sorted_vertices()
.iter()
.map(|v| v.coords.clone())
.collect::<Vec<Point>>()
}

pub fn anchor(&self) -> &Vertex {
// TODO I'm not yet convinced this is something I want, ultimately
// need something to initiate algorithms in the vertex chain.
Expand Down

0 comments on commit 44e5875

Please sign in to comment.