1use crate::error::Result;
39use oxigdal_core::vector::{Coordinate, Geometry, LineString, Polygon};
40
41#[cfg(feature = "std")]
42use std::vec::Vec;
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum Severity {
47 Error,
49 Warning,
51 Info,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum IssueType {
58 UnclosedRing,
60 SelfIntersection,
62 DuplicateVertices,
64 Spike,
66 InsufficientPoints,
68 WrongOrientation,
70 HoleNotContained,
72 TooFewDistinctPoints,
74 CollinearVertices,
76}
77
78#[derive(Debug, Clone)]
80pub struct ValidationIssue {
81 pub severity: Severity,
83 pub issue_type: IssueType,
85 pub description: String,
87 pub location: Option<Coordinate>,
89 pub repair_suggestion: Option<String>,
91}
92
93impl ValidationIssue {
94 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
112pub fn validate_geometry(geometry: &Geometry) -> Result<Vec<ValidationIssue>> {
126 match geometry {
127 Geometry::Point(_) => Ok(vec![]), Geometry::LineString(ls) => validate_linestring(ls),
129 Geometry::Polygon(p) => validate_polygon(p),
130 Geometry::MultiPoint(_) => Ok(vec![]), 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
155pub fn validate_linestring(linestring: &LineString) -> Result<Vec<ValidationIssue>> {
169 let mut issues = Vec::new();
170
171 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 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 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
220pub fn validate_polygon(polygon: &Polygon) -> Result<Vec<ValidationIssue>> {
234 let mut issues = Vec::new();
235
236 issues.extend(validate_ring(&polygon.exterior.coords, true)?);
238
239 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 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
262fn 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 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 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 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 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 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 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 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
371fn coords_equal(c1: &Coordinate, c2: &Coordinate) -> bool {
373 (c1.x - c2.x).abs() < f64::EPSILON && (c1.y - c2.y).abs() < f64::EPSILON
374}
375
376fn 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
382fn count_distinct_points(coords: &[Coordinate]) -> usize {
384 if coords.is_empty() {
385 return 0;
386 }
387
388 let mut distinct = 1; 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
405fn 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 if is_spike(prev, curr, next) {
420 spikes.push(i);
421 }
422 }
423
424 spikes
425}
426
427fn is_spike(prev: &Coordinate, curr: &Coordinate, next: &Coordinate) -> bool {
429 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 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 cos_angle < -0.99
448}
449
450fn 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 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
473fn 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
489fn 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
494fn 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
508fn hole_contained_in_exterior(hole: &[Coordinate], exterior: &[Coordinate]) -> bool {
510 if hole.is_empty() {
511 return false;
512 }
513
514 for point in hole {
516 if !point_in_ring(point, exterior) {
517 return false;
518 }
519 }
520
521 true
522}
523
524fn 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 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 ];
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 }
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), 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 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 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 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 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), ];
704
705 assert_eq!(count_distinct_points(&coords), 3);
706 }
707
708 #[test]
709 fn test_is_spike() {
710 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 let next2 = Coordinate::new_2d(2.0, 2.0);
719 assert!(!is_spike(&prev, &curr, &next2));
720 }
721}