use crate::error::Result;
use oxigdal_core::vector::{Coordinate, Geometry, LineString, Polygon};
#[cfg(feature = "std")]
use std::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
Error,
Warning,
Info,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IssueType {
UnclosedRing,
SelfIntersection,
DuplicateVertices,
Spike,
InsufficientPoints,
WrongOrientation,
HoleNotContained,
TooFewDistinctPoints,
CollinearVertices,
}
#[derive(Debug, Clone)]
pub struct ValidationIssue {
pub severity: Severity,
pub issue_type: IssueType,
pub description: String,
pub location: Option<Coordinate>,
pub repair_suggestion: Option<String>,
}
impl ValidationIssue {
pub fn new(
severity: Severity,
issue_type: IssueType,
description: String,
location: Option<Coordinate>,
repair_suggestion: Option<String>,
) -> Self {
Self {
severity,
issue_type,
description,
location,
repair_suggestion,
}
}
}
pub fn validate_geometry(geometry: &Geometry) -> Result<Vec<ValidationIssue>> {
match geometry {
Geometry::Point(_) => Ok(vec![]), Geometry::LineString(ls) => validate_linestring(ls),
Geometry::Polygon(p) => validate_polygon(p),
Geometry::MultiPoint(_) => Ok(vec![]), Geometry::MultiLineString(mls) => {
let mut issues = Vec::new();
for ls in &mls.line_strings {
issues.extend(validate_linestring(ls)?);
}
Ok(issues)
}
Geometry::MultiPolygon(mp) => {
let mut issues = Vec::new();
for p in &mp.polygons {
issues.extend(validate_polygon(p)?);
}
Ok(issues)
}
Geometry::GeometryCollection(gc) => {
let mut issues = Vec::new();
for geom in &gc.geometries {
issues.extend(validate_geometry(geom)?);
}
Ok(issues)
}
}
}
pub fn validate_linestring(linestring: &LineString) -> Result<Vec<ValidationIssue>> {
let mut issues = Vec::new();
if linestring.coords.len() < 2 {
issues.push(ValidationIssue::new(
Severity::Error,
IssueType::InsufficientPoints,
format!(
"LineString must have at least 2 points, got {}",
linestring.coords.len()
),
None,
Some("Add more points to the linestring".to_string()),
));
}
for i in 0..linestring.coords.len().saturating_sub(1) {
if coords_equal(&linestring.coords[i], &linestring.coords[i + 1]) {
issues.push(ValidationIssue::new(
Severity::Warning,
IssueType::DuplicateVertices,
format!("Duplicate consecutive vertices at index {}", i),
Some(linestring.coords[i]),
Some("Remove duplicate vertices".to_string()),
));
}
}
if linestring.coords.len() >= 3 {
for i in 0..linestring.coords.len() - 2 {
if are_collinear(
&linestring.coords[i],
&linestring.coords[i + 1],
&linestring.coords[i + 2],
) {
issues.push(ValidationIssue::new(
Severity::Info,
IssueType::CollinearVertices,
format!("Collinear vertices at indices {}-{}-{}", i, i + 1, i + 2),
Some(linestring.coords[i + 1]),
Some("Consider removing middle vertex for simplification".to_string()),
));
}
}
}
Ok(issues)
}
pub fn validate_polygon(polygon: &Polygon) -> Result<Vec<ValidationIssue>> {
let mut issues = Vec::new();
issues.extend(validate_ring(&polygon.exterior.coords, true)?);
for (i, hole) in polygon.interiors.iter().enumerate() {
let hole_issues = validate_ring(&hole.coords, false)?;
for mut issue in hole_issues {
issue.description = format!("Hole {}: {}", i, issue.description);
issues.push(issue);
}
if !hole_contained_in_exterior(&hole.coords, &polygon.exterior.coords) {
issues.push(ValidationIssue::new(
Severity::Error,
IssueType::HoleNotContained,
format!("Hole {} is not contained within exterior ring", i),
None,
Some("Move hole inside exterior ring or remove it".to_string()),
));
}
}
Ok(issues)
}
fn validate_ring(coords: &[Coordinate], is_exterior: bool) -> Result<Vec<ValidationIssue>> {
let mut issues = Vec::new();
let ring_type = if is_exterior { "Exterior" } else { "Interior" };
if coords.len() < 4 {
issues.push(ValidationIssue::new(
Severity::Error,
IssueType::InsufficientPoints,
format!(
"{} ring must have at least 4 points, got {}",
ring_type,
coords.len()
),
None,
Some("Add more points to form a closed ring".to_string()),
));
return Ok(issues);
}
if !coords_equal(&coords[0], &coords[coords.len() - 1]) {
issues.push(ValidationIssue::new(
Severity::Error,
IssueType::UnclosedRing,
format!("{} ring is not closed", ring_type),
Some(coords[coords.len() - 1]),
Some("Make last point equal to first point".to_string()),
));
}
for i in 0..coords.len() - 1 {
if coords_equal(&coords[i], &coords[i + 1]) {
issues.push(ValidationIssue::new(
Severity::Warning,
IssueType::DuplicateVertices,
format!(
"{} ring has duplicate consecutive vertices at index {}",
ring_type, i
),
Some(coords[i]),
Some("Remove duplicate vertices".to_string()),
));
}
}
let distinct_count = count_distinct_points(coords);
if distinct_count < 3 {
issues.push(ValidationIssue::new(
Severity::Error,
IssueType::TooFewDistinctPoints,
format!(
"{} ring has only {} distinct points (need at least 3)",
ring_type, distinct_count
),
None,
Some("Add more distinct points".to_string()),
));
}
let spikes = find_spikes(coords);
for spike_idx in spikes {
issues.push(ValidationIssue::new(
Severity::Warning,
IssueType::Spike,
format!("{} ring has spike at index {}", ring_type, spike_idx),
Some(coords[spike_idx]),
Some("Remove spike by deleting the vertex or adjusting coordinates".to_string()),
));
}
if has_self_intersection(coords) {
issues.push(ValidationIssue::new(
Severity::Error,
IssueType::SelfIntersection,
format!("{} ring is self-intersecting", ring_type),
None,
Some("Modify coordinates to eliminate self-intersection".to_string()),
));
}
let is_ccw = is_counter_clockwise(coords);
if is_exterior && !is_ccw {
issues.push(ValidationIssue::new(
Severity::Warning,
IssueType::WrongOrientation,
"Exterior ring should be counter-clockwise".to_string(),
None,
Some("Reverse vertex order".to_string()),
));
} else if !is_exterior && is_ccw {
issues.push(ValidationIssue::new(
Severity::Warning,
IssueType::WrongOrientation,
"Interior ring (hole) should be clockwise".to_string(),
None,
Some("Reverse vertex order".to_string()),
));
}
Ok(issues)
}
fn coords_equal(c1: &Coordinate, c2: &Coordinate) -> bool {
(c1.x - c2.x).abs() < f64::EPSILON && (c1.y - c2.y).abs() < f64::EPSILON
}
fn are_collinear(p1: &Coordinate, p2: &Coordinate, p3: &Coordinate) -> bool {
let cross = (p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y);
cross.abs() < f64::EPSILON
}
fn count_distinct_points(coords: &[Coordinate]) -> usize {
if coords.is_empty() {
return 0;
}
let mut distinct = 1; for i in 1..coords.len() {
let mut is_distinct = true;
for j in 0..i {
if coords_equal(&coords[i], &coords[j]) {
is_distinct = false;
break;
}
}
if is_distinct {
distinct += 1;
}
}
distinct
}
fn find_spikes(coords: &[Coordinate]) -> Vec<usize> {
let mut spikes = Vec::new();
if coords.len() < 3 {
return spikes;
}
for i in 1..coords.len() - 1 {
let prev = &coords[i - 1];
let curr = &coords[i];
let next = &coords[i + 1];
if is_spike(prev, curr, next) {
spikes.push(i);
}
}
spikes
}
fn is_spike(prev: &Coordinate, curr: &Coordinate, next: &Coordinate) -> 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 dot = dx1 * dx2 + dy1 * dy2;
let len1 = (dx1 * dx1 + dy1 * dy1).sqrt();
let len2 = (dx2 * dx2 + dy2 * dy2).sqrt();
if len1 < f64::EPSILON || len2 < f64::EPSILON {
return false;
}
let cos_angle = dot / (len1 * len2);
cos_angle < -0.99
}
fn has_self_intersection(coords: &[Coordinate]) -> bool {
let n = coords.len();
if n < 4 {
return false;
}
for i in 0..n - 1 {
for j in i + 2..n - 1 {
if j == i + 1 || (i == 0 && j == n - 2) {
continue;
}
if segments_intersect(&coords[i], &coords[i + 1], &coords[j], &coords[j + 1]) {
return true;
}
}
}
false
}
fn segments_intersect(p1: &Coordinate, p2: &Coordinate, p3: &Coordinate, p4: &Coordinate) -> bool {
let d1 = direction(p3, p4, p1);
let d2 = direction(p3, p4, p2);
let d3 = direction(p1, p2, p3);
let d4 = direction(p1, p2, p4);
if ((d1 > 0.0 && d2 < 0.0) || (d1 < 0.0 && d2 > 0.0))
&& ((d3 > 0.0 && d4 < 0.0) || (d3 < 0.0 && d4 > 0.0))
{
return true;
}
false
}
fn direction(a: &Coordinate, b: &Coordinate, p: &Coordinate) -> f64 {
(b.x - a.x) * (p.y - a.y) - (p.x - a.x) * (b.y - a.y)
}
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
}
fn hole_contained_in_exterior(hole: &[Coordinate], exterior: &[Coordinate]) -> bool {
if hole.is_empty() {
return false;
}
for point in hole {
if !point_in_ring(point, exterior) {
return false;
}
}
true
}
fn point_in_ring(point: &Coordinate, ring: &[Coordinate]) -> bool {
let mut inside = false;
let n = ring.len();
let mut j = n - 1;
for i in 0..n {
let xi = ring[i].x;
let yi = ring[i].y;
let xj = ring[j].x;
let yj = ring[j].y;
let intersect = ((yi > point.y) != (yj > point.y))
&& (point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi);
if intersect {
inside = !inside;
}
j = i;
}
inside
}
#[cfg(test)]
mod tests {
use super::*;
fn create_valid_square() -> Polygon {
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),
Coordinate::new_2d(0.0, 0.0),
];
let exterior = LineString::new(coords).expect("valid linestring");
Polygon::new(exterior, vec![]).expect("valid polygon")
}
#[test]
fn test_validate_valid_polygon() {
let poly = create_valid_square();
let result = validate_polygon(&poly);
assert!(result.is_ok());
if let Ok(issues) = result {
let errors = issues
.iter()
.filter(|i| i.severity == Severity::Error)
.count();
assert_eq!(errors, 0);
}
}
#[test]
fn test_validate_unclosed_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 ls = LineString::new(coords);
if let Ok(linestring) = ls {
let result = validate_linestring(&linestring);
assert!(result.is_ok());
}
}
#[test]
fn test_validate_duplicate_vertices() {
let coords = vec![
Coordinate::new_2d(0.0, 0.0),
Coordinate::new_2d(2.0, 0.0),
Coordinate::new_2d(2.0, 0.0), Coordinate::new_2d(4.0, 0.0),
];
let ls = LineString::new(coords);
assert!(ls.is_ok());
if let Ok(linestring) = ls {
let result = validate_linestring(&linestring);
assert!(result.is_ok());
if let Ok(issues) = result {
let duplicates = issues
.iter()
.filter(|i| i.issue_type == IssueType::DuplicateVertices)
.count();
assert!(duplicates > 0);
}
}
}
#[test]
fn test_coords_equal() {
let c1 = Coordinate::new_2d(1.0, 2.0);
let c2 = Coordinate::new_2d(1.0, 2.0);
let c3 = Coordinate::new_2d(1.1, 2.0);
assert!(coords_equal(&c1, &c2));
assert!(!coords_equal(&c1, &c3));
}
#[test]
fn test_are_collinear() {
let p1 = Coordinate::new_2d(0.0, 0.0);
let p2 = Coordinate::new_2d(1.0, 1.0);
let p3 = Coordinate::new_2d(2.0, 2.0);
assert!(are_collinear(&p1, &p2, &p3));
let p4 = Coordinate::new_2d(1.0, 2.0);
assert!(!are_collinear(&p1, &p2, &p4));
}
#[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_has_self_intersection() {
let self_intersecting = vec![
Coordinate::new_2d(0.0, 0.0),
Coordinate::new_2d(4.0, 4.0),
Coordinate::new_2d(4.0, 0.0),
Coordinate::new_2d(0.0, 4.0),
Coordinate::new_2d(0.0, 0.0),
];
assert!(has_self_intersection(&self_intersecting));
let valid = 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!(!has_self_intersection(&valid));
}
#[test]
fn test_count_distinct_points() {
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, 0.0), ];
assert_eq!(count_distinct_points(&coords), 3);
}
#[test]
fn test_is_spike() {
let prev = Coordinate::new_2d(0.0, 0.0);
let curr = Coordinate::new_2d(2.0, 0.0);
let next = Coordinate::new_2d(0.0, 0.0);
assert!(is_spike(&prev, &curr, &next));
let next2 = Coordinate::new_2d(2.0, 2.0);
assert!(!is_spike(&prev, &curr, &next2));
}
}