nodedb_spatial/
validate.rs1use nodedb_types::geometry::Geometry;
14
15use crate::predicates::edge::segments_intersect;
16
17pub fn validate_geometry(geom: &Geometry) -> Vec<String> {
19 let mut issues = Vec::new();
20 validate_recursive(geom, &mut issues);
21 issues
22}
23
24pub fn is_valid(geom: &Geometry) -> bool {
26 validate_geometry(geom).is_empty()
27}
28
29fn validate_recursive(geom: &Geometry, issues: &mut Vec<String>) {
30 match geom {
31 Geometry::Point { coordinates }
32 if coordinates[0].is_nan()
33 || coordinates[1].is_nan()
34 || coordinates[0].is_infinite()
35 || coordinates[1].is_infinite() =>
36 {
37 issues.push("Point has NaN or Infinite coordinate".to_string());
38 }
39
40 Geometry::LineString { coordinates } if coordinates.len() < 2 => {
41 issues.push(format!(
42 "LineString has {} points, minimum is 2",
43 coordinates.len()
44 ));
45 }
46
47 Geometry::Polygon { coordinates } => {
48 if coordinates.is_empty() {
49 issues.push("Polygon has no rings".to_string());
50 return;
51 }
52
53 for (ring_idx, ring) in coordinates.iter().enumerate() {
54 let label = if ring_idx == 0 {
55 "Exterior ring".to_string()
56 } else {
57 format!("Hole ring {ring_idx}")
58 };
59
60 if ring.len() < 4 {
61 issues.push(format!(
62 "{label} has {} points, minimum is 4 (triangle + close)",
63 ring.len()
64 ));
65 continue;
66 }
67
68 if let (Some(first), Some(last)) = (ring.first(), ring.last())
70 && ((first[0] - last[0]).abs() > 1e-10 || (first[1] - last[1]).abs() > 1e-10)
71 {
72 issues.push(format!("{label} is not closed (first != last)"));
73 }
74
75 let area = signed_area(ring);
77 if ring_idx == 0 && area < 0.0 {
78 issues.push(
79 "Exterior ring has clockwise winding (should be counter-clockwise)"
80 .to_string(),
81 );
82 } else if ring_idx > 0 && area > 0.0 {
83 issues.push(format!(
84 "{label} has counter-clockwise winding (holes should be clockwise)"
85 ));
86 }
87
88 check_ring_self_intersection(ring, &label, issues);
90 }
91 }
92
93 Geometry::MultiPoint { coordinates } => {
94 for (i, c) in coordinates.iter().enumerate() {
95 if c[0].is_nan() || c[1].is_nan() || c[0].is_infinite() || c[1].is_infinite() {
96 issues.push(format!("MultiPoint[{i}] has NaN or Infinite coordinate"));
97 }
98 }
99 }
100
101 Geometry::MultiLineString { coordinates } => {
102 for (i, ls) in coordinates.iter().enumerate() {
103 if ls.len() < 2 {
104 issues.push(format!(
105 "MultiLineString[{i}] has {} points, minimum is 2",
106 ls.len()
107 ));
108 }
109 }
110 }
111
112 Geometry::MultiPolygon { coordinates } => {
113 for (i, poly) in coordinates.iter().enumerate() {
114 validate_recursive(
115 &Geometry::Polygon {
116 coordinates: poly.clone(),
117 },
118 issues,
119 );
120 let _ = i; }
124 }
125
126 Geometry::GeometryCollection { geometries } => {
127 for geom in geometries {
128 validate_recursive(geom, issues);
129 }
130 }
131
132 _ => {}
134 }
135}
136
137fn signed_area(ring: &[[f64; 2]]) -> f64 {
139 let n = ring.len();
140 if n < 3 {
141 return 0.0;
142 }
143 let mut sum = 0.0;
144 for i in 0..n {
145 let j = (i + 1) % n;
146 sum += ring[i][0] * ring[j][1];
147 sum -= ring[j][0] * ring[i][1];
148 }
149 sum / 2.0
150}
151
152fn check_ring_self_intersection(ring: &[[f64; 2]], label: &str, issues: &mut Vec<String>) {
154 let n = if ring.first() == ring.last() && ring.len() > 1 {
155 ring.len() - 1
156 } else {
157 ring.len()
158 };
159
160 if n < 4 {
161 return; }
163
164 for i in 0..n {
165 let i_next = (i + 1) % n;
166 for j in (i + 2)..n {
168 let j_next = (j + 1) % n;
169 if j_next == i {
171 continue;
172 }
173 if segments_intersect(ring[i], ring[i_next], ring[j], ring[j_next]) {
174 let shared_endpoint = ring[i] == ring[j]
176 || ring[i] == ring[j_next]
177 || ring[i_next] == ring[j]
178 || ring[i_next] == ring[j_next];
179 if !shared_endpoint {
180 issues.push(format!(
181 "{label} has self-intersection at edges {i}-{i_next} and {j}-{j_next}"
182 ));
183 return; }
185 }
186 }
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 #[test]
195 fn valid_polygon() {
196 let geom = Geometry::polygon(vec![vec![
197 [0.0, 0.0],
198 [10.0, 0.0],
199 [10.0, 10.0],
200 [0.0, 10.0],
201 [0.0, 0.0],
202 ]]);
203 assert!(is_valid(&geom));
204 }
205
206 #[test]
207 fn unclosed_ring() {
208 let geom = Geometry::polygon(vec![vec![
209 [0.0, 0.0],
210 [10.0, 0.0],
211 [10.0, 10.0],
212 [0.0, 10.0],
213 ]]);
214 let issues = validate_geometry(&geom);
215 assert!(issues.iter().any(|i| i.contains("not closed")));
216 }
217
218 #[test]
219 fn too_few_points() {
220 let geom = Geometry::polygon(vec![vec![[0.0, 0.0], [10.0, 0.0], [10.0, 10.0]]]);
221 let issues = validate_geometry(&geom);
222 assert!(issues.iter().any(|i| i.contains("minimum is 4")));
223 }
224
225 #[test]
226 fn self_intersecting_ring() {
227 let geom = Geometry::polygon(vec![vec![
229 [0.0, 0.0],
230 [10.0, 10.0],
231 [10.0, 0.0],
232 [0.0, 10.0],
233 [0.0, 0.0],
234 ]]);
235 let issues = validate_geometry(&geom);
236 assert!(issues.iter().any(|i| i.contains("self-intersection")));
237 }
238
239 #[test]
240 fn valid_linestring() {
241 let geom = Geometry::line_string(vec![[0.0, 0.0], [1.0, 1.0]]);
242 assert!(is_valid(&geom));
243 }
244
245 #[test]
246 fn linestring_too_short() {
247 let geom = Geometry::LineString {
248 coordinates: vec![[0.0, 0.0]],
249 };
250 assert!(!is_valid(&geom));
251 }
252
253 #[test]
254 fn valid_point() {
255 assert!(is_valid(&Geometry::point(0.0, 0.0)));
256 }
257
258 #[test]
259 fn nan_point_invalid() {
260 let geom = Geometry::Point {
261 coordinates: [f64::NAN, 0.0],
262 };
263 assert!(!is_valid(&geom));
264 }
265
266 #[test]
267 fn clockwise_exterior_warned() {
268 let geom = Geometry::polygon(vec![vec![
270 [0.0, 0.0],
271 [0.0, 10.0],
272 [10.0, 10.0],
273 [10.0, 0.0],
274 [0.0, 0.0],
275 ]]);
276 let issues = validate_geometry(&geom);
277 assert!(issues.iter().any(|i| i.contains("clockwise")));
278 }
279
280 #[test]
281 fn geometry_collection_validates_all() {
282 let gc = Geometry::GeometryCollection {
283 geometries: vec![
284 Geometry::point(0.0, 0.0),
285 Geometry::LineString {
286 coordinates: vec![[0.0, 0.0]],
287 }, ],
289 };
290 let issues = validate_geometry(&gc);
291 assert!(!issues.is_empty());
292 }
293}