Skip to main content

oxigdal_algorithms/vector/
valid.rs

1//! Geometry validation and repair
2//!
3//! This module provides functions for validating geometries according to
4//! OGC Simple Features specification and identifying common geometric issues.
5//!
6//! # Validation Checks
7//!
8//! - **Ring Closure**: Ensures polygon rings are properly closed
9//! - **Self-Intersection**: Detects self-intersecting polygons
10//! - **Duplicate Vertices**: Identifies consecutive duplicate points
11//! - **Spike Detection**: Finds degenerate spikes in polygon boundaries
12//! - **Minimum Points**: Verifies geometries have required number of points
13//! - **Ring Orientation**: Checks proper orientation (exterior CCW, holes CW)
14//! - **Hole Containment**: Verifies holes are inside exterior ring
15//!
16//! # Examples
17//!
18//! ```
19//! use oxigdal_algorithms::vector::{Polygon, LineString, Coordinate, validate_polygon};
20//! # use oxigdal_algorithms::error::Result;
21//!
22//! # fn main() -> Result<()> {
23//! let coords = vec![
24//!     Coordinate::new_2d(0.0, 0.0),
25//!     Coordinate::new_2d(4.0, 0.0),
26//!     Coordinate::new_2d(4.0, 4.0),
27//!     Coordinate::new_2d(0.0, 4.0),
28//!     Coordinate::new_2d(0.0, 0.0),
29//! ];
30//! let exterior = LineString::new(coords)?;
31//! let polygon = Polygon::new(exterior, vec![])?;
32//! let issues = validate_polygon(&polygon)?;
33//! // issues should be empty for a valid square
34//! # Ok(())
35//! # }
36//! ```
37
38use crate::error::Result;
39use oxigdal_core::vector::{Coordinate, Geometry, LineString, Polygon};
40
41#[cfg(feature = "std")]
42use std::vec::Vec;
43
44/// Validation issue severity
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum Severity {
47    /// Error: geometry is invalid
48    Error,
49    /// Warning: geometry may cause issues
50    Warning,
51    /// Info: best practice violation
52    Info,
53}
54
55/// Validation issue type
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum IssueType {
58    /// Ring is not closed
59    UnclosedRing,
60    /// Self-intersecting polygon
61    SelfIntersection,
62    /// Consecutive duplicate vertices
63    DuplicateVertices,
64    /// Degenerate spike in boundary
65    Spike,
66    /// Insufficient number of points
67    InsufficientPoints,
68    /// Wrong ring orientation
69    WrongOrientation,
70    /// Hole not contained in exterior
71    HoleNotContained,
72    /// Ring has too few distinct points
73    TooFewDistinctPoints,
74    /// Collinear vertices
75    CollinearVertices,
76}
77
78/// A validation issue found in a geometry
79#[derive(Debug, Clone)]
80pub struct ValidationIssue {
81    /// Severity level
82    pub severity: Severity,
83    /// Type of issue
84    pub issue_type: IssueType,
85    /// Human-readable description
86    pub description: String,
87    /// Location of the issue (if applicable)
88    pub location: Option<Coordinate>,
89    /// Suggested repair action
90    pub repair_suggestion: Option<String>,
91}
92
93impl ValidationIssue {
94    /// Creates a new validation issue
95    pub fn new(
96        severity: Severity,
97        issue_type: IssueType,
98        description: String,
99        location: Option<Coordinate>,
100        repair_suggestion: Option<String>,
101    ) -> Self {
102        Self {
103            severity,
104            issue_type,
105            description,
106            location,
107            repair_suggestion,
108        }
109    }
110}
111
112/// Validates a geometry and returns list of issues
113///
114/// # Arguments
115///
116/// * `geometry` - Geometry to validate
117///
118/// # Returns
119///
120/// Vector of validation issues (empty if geometry is valid)
121///
122/// # Errors
123///
124/// Returns error if validation process fails
125pub fn validate_geometry(geometry: &Geometry) -> Result<Vec<ValidationIssue>> {
126    match geometry {
127        Geometry::Point(_) => Ok(vec![]), // Points are always valid
128        Geometry::LineString(ls) => validate_linestring(ls),
129        Geometry::Polygon(p) => validate_polygon(p),
130        Geometry::MultiPoint(_) => Ok(vec![]), // MultiPoints are always valid
131        Geometry::MultiLineString(mls) => {
132            let mut issues = Vec::new();
133            for ls in &mls.line_strings {
134                issues.extend(validate_linestring(ls)?);
135            }
136            Ok(issues)
137        }
138        Geometry::MultiPolygon(mp) => {
139            let mut issues = Vec::new();
140            for p in &mp.polygons {
141                issues.extend(validate_polygon(p)?);
142            }
143            Ok(issues)
144        }
145        Geometry::GeometryCollection(gc) => {
146            let mut issues = Vec::new();
147            for geom in &gc.geometries {
148                issues.extend(validate_geometry(geom)?);
149            }
150            Ok(issues)
151        }
152    }
153}
154
155/// Validates a linestring
156///
157/// # Arguments
158///
159/// * `linestring` - LineString to validate
160///
161/// # Returns
162///
163/// Vector of validation issues
164///
165/// # Errors
166///
167/// Returns error if validation fails
168pub fn validate_linestring(linestring: &LineString) -> Result<Vec<ValidationIssue>> {
169    let mut issues = Vec::new();
170
171    // Check minimum points
172    if linestring.coords.len() < 2 {
173        issues.push(ValidationIssue::new(
174            Severity::Error,
175            IssueType::InsufficientPoints,
176            format!(
177                "LineString must have at least 2 points, got {}",
178                linestring.coords.len()
179            ),
180            None,
181            Some("Add more points to the linestring".to_string()),
182        ));
183    }
184
185    // Check for duplicate consecutive vertices
186    for i in 0..linestring.coords.len().saturating_sub(1) {
187        if coords_equal(&linestring.coords[i], &linestring.coords[i + 1]) {
188            issues.push(ValidationIssue::new(
189                Severity::Warning,
190                IssueType::DuplicateVertices,
191                format!("Duplicate consecutive vertices at index {}", i),
192                Some(linestring.coords[i]),
193                Some("Remove duplicate vertices".to_string()),
194            ));
195        }
196    }
197
198    // Check for collinear vertices
199    if linestring.coords.len() >= 3 {
200        for i in 0..linestring.coords.len() - 2 {
201            if are_collinear(
202                &linestring.coords[i],
203                &linestring.coords[i + 1],
204                &linestring.coords[i + 2],
205            ) {
206                issues.push(ValidationIssue::new(
207                    Severity::Info,
208                    IssueType::CollinearVertices,
209                    format!("Collinear vertices at indices {}-{}-{}", i, i + 1, i + 2),
210                    Some(linestring.coords[i + 1]),
211                    Some("Consider removing middle vertex for simplification".to_string()),
212                ));
213            }
214        }
215    }
216
217    Ok(issues)
218}
219
220/// Validates a polygon
221///
222/// # Arguments
223///
224/// * `polygon` - Polygon to validate
225///
226/// # Returns
227///
228/// Vector of validation issues (empty if valid)
229///
230/// # Errors
231///
232/// Returns error if validation fails
233pub fn validate_polygon(polygon: &Polygon) -> Result<Vec<ValidationIssue>> {
234    let mut issues = Vec::new();
235
236    // Validate exterior ring
237    issues.extend(validate_ring(&polygon.exterior.coords, true)?);
238
239    // Validate interior rings (holes)
240    for (i, hole) in polygon.interiors.iter().enumerate() {
241        let hole_issues = validate_ring(&hole.coords, false)?;
242        for mut issue in hole_issues {
243            issue.description = format!("Hole {}: {}", i, issue.description);
244            issues.push(issue);
245        }
246
247        // Check if hole is contained in exterior
248        if !hole_contained_in_exterior(&hole.coords, &polygon.exterior.coords) {
249            issues.push(ValidationIssue::new(
250                Severity::Error,
251                IssueType::HoleNotContained,
252                format!("Hole {} is not contained within exterior ring", i),
253                None,
254                Some("Move hole inside exterior ring or remove it".to_string()),
255            ));
256        }
257    }
258
259    Ok(issues)
260}
261
262/// Validates a polygon ring (exterior or interior)
263fn validate_ring(coords: &[Coordinate], is_exterior: bool) -> Result<Vec<ValidationIssue>> {
264    let mut issues = Vec::new();
265    let ring_type = if is_exterior { "Exterior" } else { "Interior" };
266
267    // Check minimum points
268    if coords.len() < 4 {
269        issues.push(ValidationIssue::new(
270            Severity::Error,
271            IssueType::InsufficientPoints,
272            format!(
273                "{} ring must have at least 4 points, got {}",
274                ring_type,
275                coords.len()
276            ),
277            None,
278            Some("Add more points to form a closed ring".to_string()),
279        ));
280        return Ok(issues);
281    }
282
283    // Check if ring is closed
284    if !coords_equal(&coords[0], &coords[coords.len() - 1]) {
285        issues.push(ValidationIssue::new(
286            Severity::Error,
287            IssueType::UnclosedRing,
288            format!("{} ring is not closed", ring_type),
289            Some(coords[coords.len() - 1]),
290            Some("Make last point equal to first point".to_string()),
291        ));
292    }
293
294    // Check for duplicate consecutive vertices
295    for i in 0..coords.len() - 1 {
296        if coords_equal(&coords[i], &coords[i + 1]) {
297            issues.push(ValidationIssue::new(
298                Severity::Warning,
299                IssueType::DuplicateVertices,
300                format!(
301                    "{} ring has duplicate consecutive vertices at index {}",
302                    ring_type, i
303                ),
304                Some(coords[i]),
305                Some("Remove duplicate vertices".to_string()),
306            ));
307        }
308    }
309
310    // Check for too few distinct points
311    let distinct_count = count_distinct_points(coords);
312    if distinct_count < 3 {
313        issues.push(ValidationIssue::new(
314            Severity::Error,
315            IssueType::TooFewDistinctPoints,
316            format!(
317                "{} ring has only {} distinct points (need at least 3)",
318                ring_type, distinct_count
319            ),
320            None,
321            Some("Add more distinct points".to_string()),
322        ));
323    }
324
325    // Check for spikes
326    let spikes = find_spikes(coords);
327    for spike_idx in spikes {
328        issues.push(ValidationIssue::new(
329            Severity::Warning,
330            IssueType::Spike,
331            format!("{} ring has spike at index {}", ring_type, spike_idx),
332            Some(coords[spike_idx]),
333            Some("Remove spike by deleting the vertex or adjusting coordinates".to_string()),
334        ));
335    }
336
337    // Check for self-intersection
338    if has_self_intersection(coords) {
339        issues.push(ValidationIssue::new(
340            Severity::Error,
341            IssueType::SelfIntersection,
342            format!("{} ring is self-intersecting", ring_type),
343            None,
344            Some("Modify coordinates to eliminate self-intersection".to_string()),
345        ));
346    }
347
348    // Check orientation
349    let is_ccw = is_counter_clockwise(coords);
350    if is_exterior && !is_ccw {
351        issues.push(ValidationIssue::new(
352            Severity::Warning,
353            IssueType::WrongOrientation,
354            "Exterior ring should be counter-clockwise".to_string(),
355            None,
356            Some("Reverse vertex order".to_string()),
357        ));
358    } else if !is_exterior && is_ccw {
359        issues.push(ValidationIssue::new(
360            Severity::Warning,
361            IssueType::WrongOrientation,
362            "Interior ring (hole) should be clockwise".to_string(),
363            None,
364            Some("Reverse vertex order".to_string()),
365        ));
366    }
367
368    Ok(issues)
369}
370
371/// Checks if two coordinates are equal (within epsilon)
372fn coords_equal(c1: &Coordinate, c2: &Coordinate) -> bool {
373    (c1.x - c2.x).abs() < f64::EPSILON && (c1.y - c2.y).abs() < f64::EPSILON
374}
375
376/// Checks if three points are collinear
377fn are_collinear(p1: &Coordinate, p2: &Coordinate, p3: &Coordinate) -> bool {
378    let cross = (p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y);
379    cross.abs() < f64::EPSILON
380}
381
382/// Counts distinct points in a ring
383fn count_distinct_points(coords: &[Coordinate]) -> usize {
384    if coords.is_empty() {
385        return 0;
386    }
387
388    let mut distinct = 1; // First point is always distinct
389    for i in 1..coords.len() {
390        let mut is_distinct = true;
391        for j in 0..i {
392            if coords_equal(&coords[i], &coords[j]) {
393                is_distinct = false;
394                break;
395            }
396        }
397        if is_distinct {
398            distinct += 1;
399        }
400    }
401
402    distinct
403}
404
405/// Finds spikes (consecutive vertices that form a very sharp angle)
406fn find_spikes(coords: &[Coordinate]) -> Vec<usize> {
407    let mut spikes = Vec::new();
408
409    if coords.len() < 3 {
410        return spikes;
411    }
412
413    for i in 1..coords.len() - 1 {
414        let prev = &coords[i - 1];
415        let curr = &coords[i];
416        let next = &coords[i + 1];
417
418        // Check if current point is between prev and next (forms a spike)
419        if is_spike(prev, curr, next) {
420            spikes.push(i);
421        }
422    }
423
424    spikes
425}
426
427/// Checks if three points form a spike
428fn is_spike(prev: &Coordinate, curr: &Coordinate, next: &Coordinate) -> bool {
429    // A spike occurs when the current point is very close to the line from prev to next
430    let dx1 = curr.x - prev.x;
431    let dy1 = curr.y - prev.y;
432    let dx2 = next.x - curr.x;
433    let dy2 = next.y - curr.y;
434
435    // Check if vectors are opposite (angle close to 180 degrees)
436    let dot = dx1 * dx2 + dy1 * dy2;
437    let len1 = (dx1 * dx1 + dy1 * dy1).sqrt();
438    let len2 = (dx2 * dx2 + dy2 * dy2).sqrt();
439
440    if len1 < f64::EPSILON || len2 < f64::EPSILON {
441        return false;
442    }
443
444    let cos_angle = dot / (len1 * len2);
445
446    // If angle is close to 180 degrees (cos ~ -1), it's a spike
447    cos_angle < -0.99
448}
449
450/// Checks if a ring has self-intersections
451fn has_self_intersection(coords: &[Coordinate]) -> bool {
452    let n = coords.len();
453    if n < 4 {
454        return false;
455    }
456
457    for i in 0..n - 1 {
458        for j in i + 2..n - 1 {
459            // Skip adjacent segments
460            if j == i + 1 || (i == 0 && j == n - 2) {
461                continue;
462            }
463
464            if segments_intersect(&coords[i], &coords[i + 1], &coords[j], &coords[j + 1]) {
465                return true;
466            }
467        }
468    }
469
470    false
471}
472
473/// Checks if two segments intersect
474fn segments_intersect(p1: &Coordinate, p2: &Coordinate, p3: &Coordinate, p4: &Coordinate) -> bool {
475    let d1 = direction(p3, p4, p1);
476    let d2 = direction(p3, p4, p2);
477    let d3 = direction(p1, p2, p3);
478    let d4 = direction(p1, p2, p4);
479
480    if ((d1 > 0.0 && d2 < 0.0) || (d1 < 0.0 && d2 > 0.0))
481        && ((d3 > 0.0 && d4 < 0.0) || (d3 < 0.0 && d4 > 0.0))
482    {
483        return true;
484    }
485
486    false
487}
488
489/// Computes direction for orientation test
490fn direction(a: &Coordinate, b: &Coordinate, p: &Coordinate) -> f64 {
491    (b.x - a.x) * (p.y - a.y) - (p.x - a.x) * (b.y - a.y)
492}
493
494/// Checks if a ring is counter-clockwise using signed area
495fn is_counter_clockwise(coords: &[Coordinate]) -> bool {
496    let mut area = 0.0;
497    let n = coords.len();
498
499    for i in 0..n {
500        let j = (i + 1) % n;
501        area += coords[i].x * coords[j].y;
502        area -= coords[j].x * coords[i].y;
503    }
504
505    area > 0.0
506}
507
508/// Checks if a hole is contained within the exterior ring
509fn hole_contained_in_exterior(hole: &[Coordinate], exterior: &[Coordinate]) -> bool {
510    if hole.is_empty() {
511        return false;
512    }
513
514    // Check if at least one point of the hole is inside the exterior
515    for point in hole {
516        if !point_in_ring(point, exterior) {
517            return false;
518        }
519    }
520
521    true
522}
523
524/// Ray casting test for point in ring
525fn point_in_ring(point: &Coordinate, ring: &[Coordinate]) -> bool {
526    let mut inside = false;
527    let n = ring.len();
528
529    let mut j = n - 1;
530    for i in 0..n {
531        let xi = ring[i].x;
532        let yi = ring[i].y;
533        let xj = ring[j].x;
534        let yj = ring[j].y;
535
536        let intersect = ((yi > point.y) != (yj > point.y))
537            && (point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi);
538
539        if intersect {
540            inside = !inside;
541        }
542
543        j = i;
544    }
545
546    inside
547}
548
549#[cfg(test)]
550mod tests {
551    use super::*;
552
553    fn create_valid_square() -> Polygon {
554        let coords = vec![
555            Coordinate::new_2d(0.0, 0.0),
556            Coordinate::new_2d(4.0, 0.0),
557            Coordinate::new_2d(4.0, 4.0),
558            Coordinate::new_2d(0.0, 4.0),
559            Coordinate::new_2d(0.0, 0.0),
560        ];
561        let exterior = LineString::new(coords).expect("valid linestring");
562        Polygon::new(exterior, vec![]).expect("valid polygon")
563    }
564
565    #[test]
566    fn test_validate_valid_polygon() {
567        let poly = create_valid_square();
568        let result = validate_polygon(&poly);
569        assert!(result.is_ok());
570
571        if let Ok(issues) = result {
572            // Should have no errors or warnings, might have info about optimization
573            let errors = issues
574                .iter()
575                .filter(|i| i.severity == Severity::Error)
576                .count();
577            assert_eq!(errors, 0);
578        }
579    }
580
581    #[test]
582    fn test_validate_unclosed_ring() {
583        let coords = vec![
584            Coordinate::new_2d(0.0, 0.0),
585            Coordinate::new_2d(4.0, 0.0),
586            Coordinate::new_2d(4.0, 4.0),
587            Coordinate::new_2d(0.0, 4.0),
588            // Missing closing point
589        ];
590        let ls = LineString::new(coords);
591
592        if let Ok(linestring) = ls {
593            let result = validate_linestring(&linestring);
594            assert!(result.is_ok());
595            // LineString doesn't require closure, so this is valid
596        }
597    }
598
599    #[test]
600    fn test_validate_duplicate_vertices() {
601        let coords = vec![
602            Coordinate::new_2d(0.0, 0.0),
603            Coordinate::new_2d(2.0, 0.0),
604            Coordinate::new_2d(2.0, 0.0), // Duplicate
605            Coordinate::new_2d(4.0, 0.0),
606        ];
607        let ls = LineString::new(coords);
608        assert!(ls.is_ok());
609
610        if let Ok(linestring) = ls {
611            let result = validate_linestring(&linestring);
612            assert!(result.is_ok());
613
614            if let Ok(issues) = result {
615                let duplicates = issues
616                    .iter()
617                    .filter(|i| i.issue_type == IssueType::DuplicateVertices)
618                    .count();
619                assert!(duplicates > 0);
620            }
621        }
622    }
623
624    #[test]
625    fn test_coords_equal() {
626        let c1 = Coordinate::new_2d(1.0, 2.0);
627        let c2 = Coordinate::new_2d(1.0, 2.0);
628        let c3 = Coordinate::new_2d(1.1, 2.0);
629
630        assert!(coords_equal(&c1, &c2));
631        assert!(!coords_equal(&c1, &c3));
632    }
633
634    #[test]
635    fn test_are_collinear() {
636        let p1 = Coordinate::new_2d(0.0, 0.0);
637        let p2 = Coordinate::new_2d(1.0, 1.0);
638        let p3 = Coordinate::new_2d(2.0, 2.0);
639
640        assert!(are_collinear(&p1, &p2, &p3));
641
642        let p4 = Coordinate::new_2d(1.0, 2.0);
643        assert!(!are_collinear(&p1, &p2, &p4));
644    }
645
646    #[test]
647    fn test_is_counter_clockwise() {
648        // Counter-clockwise square
649        let ccw = vec![
650            Coordinate::new_2d(0.0, 0.0),
651            Coordinate::new_2d(4.0, 0.0),
652            Coordinate::new_2d(4.0, 4.0),
653            Coordinate::new_2d(0.0, 4.0),
654            Coordinate::new_2d(0.0, 0.0),
655        ];
656
657        assert!(is_counter_clockwise(&ccw));
658
659        // Clockwise square
660        let cw = vec![
661            Coordinate::new_2d(0.0, 0.0),
662            Coordinate::new_2d(0.0, 4.0),
663            Coordinate::new_2d(4.0, 4.0),
664            Coordinate::new_2d(4.0, 0.0),
665            Coordinate::new_2d(0.0, 0.0),
666        ];
667
668        assert!(!is_counter_clockwise(&cw));
669    }
670
671    #[test]
672    fn test_has_self_intersection() {
673        // Self-intersecting (bow-tie)
674        let self_intersecting = vec![
675            Coordinate::new_2d(0.0, 0.0),
676            Coordinate::new_2d(4.0, 4.0),
677            Coordinate::new_2d(4.0, 0.0),
678            Coordinate::new_2d(0.0, 4.0),
679            Coordinate::new_2d(0.0, 0.0),
680        ];
681
682        assert!(has_self_intersection(&self_intersecting));
683
684        // Non-self-intersecting
685        let valid = vec![
686            Coordinate::new_2d(0.0, 0.0),
687            Coordinate::new_2d(4.0, 0.0),
688            Coordinate::new_2d(4.0, 4.0),
689            Coordinate::new_2d(0.0, 4.0),
690            Coordinate::new_2d(0.0, 0.0),
691        ];
692
693        assert!(!has_self_intersection(&valid));
694    }
695
696    #[test]
697    fn test_count_distinct_points() {
698        let coords = vec![
699            Coordinate::new_2d(0.0, 0.0),
700            Coordinate::new_2d(4.0, 0.0),
701            Coordinate::new_2d(4.0, 4.0),
702            Coordinate::new_2d(0.0, 0.0), // Duplicate of first
703        ];
704
705        assert_eq!(count_distinct_points(&coords), 3);
706    }
707
708    #[test]
709    fn test_is_spike() {
710        // Spike: points go forward then immediately back
711        let prev = Coordinate::new_2d(0.0, 0.0);
712        let curr = Coordinate::new_2d(2.0, 0.0);
713        let next = Coordinate::new_2d(0.0, 0.0);
714
715        assert!(is_spike(&prev, &curr, &next));
716
717        // Not a spike: normal angle
718        let next2 = Coordinate::new_2d(2.0, 2.0);
719        assert!(!is_spike(&prev, &curr, &next2));
720    }
721}