1use anyhow::{Result, anyhow, bail};
10use tracing::debug;
11
12pub struct PolygonHandler;
17
18impl PolygonHandler {
19 pub fn validate_and_canonicalize(value: &str, field_name: &str) -> Result<String> {
20 debug!(
21 "Validating polygon field '{}' with value: {}",
22 field_name, value
23 );
24
25 let coordinates = Self::parse_polygon_coordinates(value).map_err(|e| {
26 anyhow!("field '{}' must be a valid polygon: {}", field_name, e)
27 })?;
28 debug!(
29 "Parsed {} coordinate pairs for field '{}'",
30 coordinates.len(),
31 field_name
32 );
33
34 Self::validate_polygon_geometry(&coordinates).map_err(|e| {
35 anyhow!("field '{}' must be a valid polygon: {}", field_name, e)
36 })?;
37 debug!(
38 "Polygon geometry validation passed for field '{}'",
39 field_name
40 );
41
42 Ok(value.to_string())
43 }
44
45 pub fn parse_polygon_coordinates(coord_string: &str) -> Result<Vec<(f64, f64)>> {
61 let raw = coord_string.trim();
62 if raw.is_empty() {
63 bail!("polygon coordinate string is empty");
64 }
65
66 let inner = match (raw.starts_with('('), raw.ends_with(')')) {
67 (true, true) => &raw[1..raw.len() - 1],
68 (false, false) => raw,
69 (true, false) => bail!(
70 "polygon coordinate string has opening '(' but is missing the closing ')'"
71 ),
72 (false, true) => bail!(
73 "polygon coordinate string has closing ')' but is missing the opening '('"
74 ),
75 };
76
77 if inner.contains('(') || inner.contains(')') {
78 bail!(
79 "polygon coordinate string must have at most one outer pair of parentheses; \
80 nested '(' or ')' are not allowed"
81 );
82 }
83
84 let inner = inner.trim();
85 if inner.is_empty() {
86 bail!("polygon coordinate string is empty between parentheses");
87 }
88
89 let coord_parts: Vec<&str> = inner.split(',').collect();
90
91 if !coord_parts.len().is_multiple_of(2) {
92 bail!("polygon coordinates must be in lat,lon pairs (got an odd number of values)");
93 }
94
95 let mut coordinates = Vec::new();
96 let mut iter = coord_parts.iter();
97
98 while let Some(lat_str) = iter.next() {
99 let lon_str = iter.next().unwrap(); let lat: f64 = lat_str
102 .trim()
103 .parse()
104 .map_err(|_| anyhow!("could not parse latitude '{}' as a number", lat_str.trim()))?;
105
106 let lon: f64 = lon_str
107 .trim()
108 .parse()
109 .map_err(|_| anyhow!("could not parse longitude '{}' as a number", lon_str.trim()))?;
110
111 coordinates.push((lat, lon));
112 }
113
114 Ok(coordinates)
115 }
116
117 fn validate_polygon_geometry(coordinates: &[(f64, f64)]) -> Result<()> {
119 if coordinates.len() < 4 {
120 bail!(
121 "polygon must have at least 4 coordinate pairs (3 unique vertices plus a \
122 closing repeat of the first vertex)"
123 );
124 }
125
126 let first = coordinates.first().unwrap();
127 let last = coordinates.last().unwrap();
128
129 if first != last {
130 bail!("polygon must be closed (first and last coordinates must be identical)");
131 }
132
133 Ok(())
134 }
135
136 pub fn calculate_bounding_box(coordinates: &[(f64, f64)]) -> String {
139 let mut min_lat = f64::INFINITY;
140 let mut min_lon = f64::INFINITY;
141 let mut max_lat = f64::NEG_INFINITY;
142 let mut max_lon = f64::NEG_INFINITY;
143
144 for &(lat, lon) in coordinates {
145 min_lat = min_lat.min(lat);
146 min_lon = min_lon.min(lon);
147 max_lat = max_lat.max(lat);
148 max_lon = max_lon.max(lon);
149 }
150
151 format!("{},{},{},{}", min_lat, min_lon, max_lat, max_lon)
152 }
153
154 pub fn parse_bbox_coordinates(s: &str) -> Result<(f64, f64, f64, f64)> {
155 let s = s.trim_matches(|c| c == '(' || c == ')');
157 let coords: Vec<f64> = s
158 .split(',')
159 .map(|part| part.trim().parse())
160 .collect::<Result<_, _>>()?;
161 if coords.len() != 4 {
162 anyhow::bail!("BBox must have 4 numbers");
163 }
164 Ok((coords[0], coords[1], coords[2], coords[3]))
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn test_validate_and_canonicalize_valid_polygon() {
174 let polygon_str = "(52.5,13.4,52.6,13.5,52.5,13.6,52.4,13.5,52.5,13.4)";
175 let result = PolygonHandler::validate_and_canonicalize(polygon_str, "polygon");
176
177 assert!(result.is_ok());
178 assert_eq!(result.unwrap(), polygon_str);
179 }
180
181 #[test]
182 fn test_validate_and_canonicalize_with_spaces() {
183 let polygon_str = "( 52.5 , 13.4 , 52.6 , 13.5 , 52.5 , 13.6 , 52.4 , 13.5 , 52.5 , 13.4 )";
184 let result = PolygonHandler::validate_and_canonicalize(polygon_str, "polygon");
185
186 assert!(result.is_ok());
187 assert_eq!(result.unwrap(), polygon_str);
188 }
189
190 #[test]
191 fn test_validate_and_canonicalize_not_closed() {
192 let polygon_str = "(52.5,13.4,52.6,13.5,52.5,13.6,52.4,13.5)"; let result = PolygonHandler::validate_and_canonicalize(polygon_str, "polygon");
194
195 assert!(result.is_err());
196 }
197
198 #[test]
199 fn test_validate_and_canonicalize_too_few_points() {
200 let polygon_str = "(52.5,13.4,52.6,13.5)"; let result = PolygonHandler::validate_and_canonicalize(polygon_str, "polygon");
202
203 assert!(result.is_err());
204 }
205
206 #[test]
207 fn test_validate_and_canonicalize_empty_string() {
208 let polygon_str = "";
209 let result = PolygonHandler::validate_and_canonicalize(polygon_str, "polygon");
210
211 assert!(result.is_err());
212 }
213
214 #[test]
215 fn test_validate_and_canonicalize_empty_parentheses() {
216 let polygon_str = "()";
217 let result = PolygonHandler::validate_and_canonicalize(polygon_str, "polygon");
218
219 assert!(result.is_err());
220 }
221
222 #[test]
223 fn test_parse_polygon_coordinates_valid() {
224 let coord_string = "(52.5,13.4,52.6,13.5,52.5,13.6,52.4,13.5,52.5,13.4)";
225 let result = PolygonHandler::parse_polygon_coordinates(coord_string);
226
227 assert!(result.is_ok());
228 let coordinates = result.unwrap();
229 assert_eq!(coordinates.len(), 5);
230 assert_eq!(coordinates[0], (52.5, 13.4));
231 assert_eq!(coordinates[1], (52.6, 13.5));
232 assert_eq!(coordinates[4], (52.5, 13.4)); }
234
235 #[test]
236 fn test_parse_polygon_coordinates_without_parentheses() {
237 let coord_string = "52.5,13.4,52.6,13.5,52.5,13.6,52.4,13.5,52.5,13.4";
238 let result = PolygonHandler::parse_polygon_coordinates(coord_string);
239
240 assert!(result.is_ok());
241 let coordinates = result.unwrap();
242 assert_eq!(coordinates.len(), 5);
243 assert_eq!(coordinates[0], (52.5, 13.4));
244 }
245
246 #[test]
247 fn test_parse_polygon_coordinates_with_spaces() {
248 let coord_string = "( 52.5 , 13.4 , 52.6 , 13.5 , 52.5 , 13.4 )";
249 let result = PolygonHandler::parse_polygon_coordinates(coord_string);
250
251 assert!(result.is_ok());
252 let coordinates = result.unwrap();
253 assert_eq!(coordinates.len(), 3);
254 assert_eq!(coordinates[0], (52.5, 13.4));
255 assert_eq!(coordinates[1], (52.6, 13.5));
256 assert_eq!(coordinates[2], (52.5, 13.4));
257 }
258
259 #[test]
260 fn test_parse_polygon_coordinates_odd_number() {
261 let coord_string = "(52.5,13.4,52.6)"; let result = PolygonHandler::parse_polygon_coordinates(coord_string);
263
264 assert!(result.is_err());
265 }
266
267 #[test]
268 fn test_parse_polygon_coordinates_invalid_latitude() {
269 let coord_string = "(invalid,13.4,52.6,13.5,52.5,13.4)";
270 let result = PolygonHandler::parse_polygon_coordinates(coord_string);
271
272 assert!(result.is_err());
273 }
274
275 #[test]
276 fn test_parse_polygon_coordinates_invalid_longitude() {
277 let coord_string = "(52.5,invalid,52.6,13.5,52.5,13.4)";
278 let result = PolygonHandler::parse_polygon_coordinates(coord_string);
279
280 assert!(result.is_err());
281 }
282
283 #[test]
284 fn test_parse_polygon_coordinates_empty() {
285 let coord_string = "()";
286 let result = PolygonHandler::parse_polygon_coordinates(coord_string);
287
288 assert!(result.is_err());
289 }
290
291 #[test]
292 fn rejects_polygon_with_opening_paren_but_no_closing_paren() {
293 let coord_string = "(50.0,10.0,52.0,10.0,52.0,12.0,50.0,12.0,50.0,10.0";
294 let err = PolygonHandler::parse_polygon_coordinates(coord_string)
295 .expect_err("unbalanced parens must be rejected");
296 let msg = err.to_string();
297 assert!(
298 msg.contains("opening") && msg.contains("missing the closing"),
299 "error should pinpoint the missing closing paren; got: {msg}"
300 );
301 }
302
303 #[test]
304 fn rejects_polygon_with_closing_paren_but_no_opening_paren() {
305 let coord_string = "50.0,10.0,52.0,10.0,52.0,12.0,50.0,12.0,50.0,10.0)";
306 let err = PolygonHandler::parse_polygon_coordinates(coord_string)
307 .expect_err("unbalanced parens must be rejected");
308 let msg = err.to_string();
309 assert!(
310 msg.contains("closing") && msg.contains("missing the opening"),
311 "error should pinpoint the missing opening paren; got: {msg}"
312 );
313 }
314
315 #[test]
316 fn rejects_polygon_with_extra_nested_parens() {
317 let coord_string = "(50.0,10.0),52.0,10.0,52.0,12.0,50.0,12.0,50.0,10.0)";
318 let err = PolygonHandler::parse_polygon_coordinates(coord_string)
319 .expect_err("nested parens must be rejected, not produce a confusing parse error");
320 let msg = err.to_string();
321 assert!(
322 msg.contains("nested") || msg.contains("outer pair"),
323 "error should mention parentheses placement, not e.g. a number-parse failure; got: {msg}"
324 );
325 }
326
327 #[test]
328 fn validate_and_canonicalize_wraps_errors_with_field_name_and_validation_marker() {
329 let bad = "(50.0,10.0,52.0,10.0,52.0,12.0,50.0,12.0,50.0,10.0";
334 let err = PolygonHandler::validate_and_canonicalize(bad, "polygon")
335 .expect_err("unbalanced polygon must error");
336 let msg = err.to_string();
337 assert!(
338 msg.contains("field 'polygon'") && msg.contains("must be a valid"),
339 "error must carry the validation-classifier markers; got: {msg}"
340 );
341 }
342
343 #[test]
344 fn test_validate_polygon_geometry_valid_triangle() {
345 let coordinates = vec![(0.0, 0.0), (1.0, 0.0), (0.5, 1.0), (0.0, 0.0)];
346 let result = PolygonHandler::validate_polygon_geometry(&coordinates);
347
348 assert!(result.is_ok());
349 }
350
351 #[test]
352 fn test_validate_polygon_geometry_valid_rectangle() {
353 let coordinates = vec![
354 (52.5, 13.4),
355 (52.6, 13.4),
356 (52.6, 13.5),
357 (52.5, 13.5),
358 (52.5, 13.4),
359 ];
360 let result = PolygonHandler::validate_polygon_geometry(&coordinates);
361
362 assert!(result.is_ok());
363 }
364
365 #[test]
366 fn test_validate_polygon_geometry_too_few_points() {
367 let coordinates = vec![(0.0, 0.0), (1.0, 0.0)]; let result = PolygonHandler::validate_polygon_geometry(&coordinates);
369
370 assert!(result.is_err());
371 }
372
373 #[test]
374 fn rejects_three_pair_closed_line_segment_as_degenerate_polygon() {
375 let coordinates = vec![(0.0, 0.0), (1.0, 0.0), (0.0, 0.0)];
380 let err = PolygonHandler::validate_polygon_geometry(&coordinates)
381 .expect_err("3-pair closed line segment must be rejected as a polygon");
382 let msg = err.to_string();
383 assert!(
384 msg.contains("at least 4 coordinate pairs"),
385 "error must specify the new minimum; got: {msg}"
386 );
387 }
388
389 #[test]
390 fn test_validate_polygon_geometry_not_closed() {
391 let coordinates = vec![(0.0, 0.0), (1.0, 0.0), (0.5, 1.0), (0.1, 0.1)]; let result = PolygonHandler::validate_polygon_geometry(&coordinates);
393
394 assert!(result.is_err());
395 }
396
397 #[test]
398 fn test_validate_polygon_geometry_minimum_valid() {
399 let coordinates = vec![(0.0, 0.0), (1.0, 0.0), (0.5, 1.0), (0.0, 0.0)]; let result = PolygonHandler::validate_polygon_geometry(&coordinates);
401
402 assert!(result.is_ok());
403 }
404
405 #[test]
406 fn test_calculate_bounding_box_rectangle() {
407 let coordinates = vec![
408 (52.5, 13.4),
409 (52.6, 13.4),
410 (52.6, 13.5),
411 (52.5, 13.5),
412 (52.5, 13.4),
413 ];
414 let bbox = PolygonHandler::calculate_bounding_box(&coordinates);
415
416 assert_eq!(bbox, "52.5,13.4,52.6,13.5");
417 }
418
419 #[test]
420 fn test_calculate_bounding_box_triangle() {
421 let coordinates = vec![(0.0, 0.0), (1.0, 0.0), (0.5, 1.0), (0.0, 0.0)];
422 let bbox = PolygonHandler::calculate_bounding_box(&coordinates);
423
424 assert_eq!(bbox, "0,0,1,1");
425 }
426
427 #[test]
428 fn test_calculate_bounding_box_single_point() {
429 let coordinates = vec![(52.5, 13.4), (52.5, 13.4), (52.5, 13.4), (52.5, 13.4)];
430 let bbox = PolygonHandler::calculate_bounding_box(&coordinates);
431
432 assert_eq!(bbox, "52.5,13.4,52.5,13.4");
433 }
434
435 #[test]
436 fn test_calculate_bounding_box_negative_coordinates() {
437 let coordinates = vec![
438 (-1.0, -1.0),
439 (1.0, -1.0),
440 (1.0, 1.0),
441 (-1.0, 1.0),
442 (-1.0, -1.0),
443 ];
444 let bbox = PolygonHandler::calculate_bounding_box(&coordinates);
445
446 assert_eq!(bbox, "-1,-1,1,1");
447 }
448
449 #[test]
450 fn test_integration_parse_and_validate() {
451 let polygon_str = "(52.5,13.4,52.6,13.5,52.5,13.6,52.4,13.5,52.5,13.4)";
452
453 let coordinates = PolygonHandler::parse_polygon_coordinates(polygon_str).unwrap();
455 let validation_result = PolygonHandler::validate_polygon_geometry(&coordinates);
456 assert!(validation_result.is_ok());
457
458 let bbox = PolygonHandler::calculate_bounding_box(&coordinates);
459 assert_eq!(bbox, "52.4,13.4,52.6,13.6");
460 }
461
462 #[test]
463 fn test_real_world_berlin_polygon() {
464 let polygon_str =
466 "(52.5200,13.4050,52.5200,13.4500,52.4800,13.4500,52.4800,13.4050,52.5200,13.4050)";
467 let result = PolygonHandler::validate_and_canonicalize(polygon_str, "berlin_area");
468
469 assert!(result.is_ok());
470
471 let coordinates = PolygonHandler::parse_polygon_coordinates(polygon_str).unwrap();
472 let bbox = PolygonHandler::calculate_bounding_box(&coordinates);
473 assert_eq!(bbox, "52.48,13.405,52.52,13.45");
474 }
475
476 #[test]
477 fn test_precision_handling() {
478 let polygon_str = "(52.123456789,13.987654321,52.234567890,13.876543210,52.345678901,13.765432109,52.123456789,13.987654321)";
480 let result = PolygonHandler::validate_and_canonicalize(polygon_str, "precision_test");
481
482 assert!(result.is_ok());
483
484 let coordinates = PolygonHandler::parse_polygon_coordinates(polygon_str).unwrap();
485 assert_eq!(coordinates[0].0, 52.123456789);
486 assert_eq!(coordinates[0].1, 13.987654321);
487 }
488}