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