1use crate::coordinate::{Coordinate, coordinates_to_json, parse_coordinate};
10use anyhow::{Result, anyhow, bail};
11use serde_json::Value;
12use tracing::debug;
13
14pub struct PolygonHandler;
19
20impl PolygonHandler {
21 pub fn validate_and_canonicalize(value: &str, field_name: &str) -> Result<String> {
22 debug!(
23 "Validating polygon field '{}' with value: {}",
24 field_name, value
25 );
26
27 let coordinates = Self::parse_polygon_coordinates(value)
28 .map_err(|e| anyhow!("field '{}' must be a valid polygon: {}", field_name, e))?;
29 debug!(
30 "Parsed {} coordinate pairs for field '{}'",
31 coordinates.len(),
32 field_name
33 );
34
35 Self::validate_polygon_geometry(&coordinates)
36 .map_err(|e| anyhow!("field '{}' must be a valid polygon: {}", field_name, e))?;
37 debug!(
38 "Polygon geometry validation passed for field '{}'",
39 field_name
40 );
41
42 Ok(value.to_string())
43 }
44
45 pub fn validate_json_and_canonicalize(value: &Value, field_name: &str) -> Result<Value> {
47 let coordinates = Self::parse_polygon_value(value).map_err(|error| {
48 anyhow!("field '{}' must be a valid polygon: {}", field_name, error)
49 })?;
50 Self::validate_polygon_geometry(&coordinates).map_err(|error| {
51 anyhow!("field '{}' must be a valid polygon: {}", field_name, error)
52 })?;
53 coordinates_to_json(&coordinates).map_err(Into::into)
54 }
55
56 pub fn parse_polygon_value(value: &Value) -> Result<Vec<Coordinate>> {
58 match value {
59 Value::Array(points) => points
60 .iter()
61 .enumerate()
62 .map(|(index, point)| {
63 parse_coordinate(point)
64 .map_err(|error| anyhow!("polygon point {} is invalid: {}", index, error))
65 })
66 .collect(),
67 Value::String(value) => Self::parse_polygon_coordinates(value),
68 _ => bail!("polygon must be an array of [lat,lon] points or a compatible string"),
69 }
70 }
71
72 pub fn parse_polygon_coordinates(coord_string: &str) -> Result<Vec<(f64, f64)>> {
88 let raw = coord_string.trim();
89 if raw.is_empty() {
90 bail!("polygon coordinate string is empty");
91 }
92
93 if raw.starts_with('[') {
94 let value: Value = serde_json::from_str(raw)
95 .map_err(|error| anyhow!("invalid polygon JSON array: {}", error))?;
96 return Self::parse_polygon_value(&value);
97 }
98
99 let inner = match (raw.starts_with('('), raw.ends_with(')')) {
100 (true, true) => &raw[1..raw.len() - 1],
101 (false, false) => raw,
102 (true, false) => {
103 bail!("polygon coordinate string has opening '(' but is missing the closing ')'")
104 }
105 (false, true) => {
106 bail!("polygon coordinate string has closing ')' but is missing the opening '('")
107 }
108 };
109
110 if inner.contains('(') || inner.contains(')') {
111 bail!(
112 "polygon coordinate string must have at most one outer pair of parentheses; \
113 nested '(' or ')' are not allowed"
114 );
115 }
116
117 let inner = inner.trim();
118 if inner.is_empty() {
119 bail!("polygon coordinate string is empty between parentheses");
120 }
121
122 let coord_parts: Vec<&str> = inner.split(',').collect();
123
124 if !coord_parts.len().is_multiple_of(2) {
125 bail!("polygon coordinates must be in lat,lon pairs (got an odd number of values)");
126 }
127
128 let mut coordinates = Vec::new();
129 let mut iter = coord_parts.iter();
130
131 while let Some(lat_str) = iter.next() {
132 let Some(lon_str) = iter.next() else {
133 bail!("polygon coordinates must be in lat,lon pairs")
134 };
135
136 let lat: f64 = lat_str.trim().parse().map_err(|_| {
137 anyhow!("could not parse latitude '{}' as a number", lat_str.trim())
138 })?;
139
140 let lon: f64 = lon_str.trim().parse().map_err(|_| {
141 anyhow!("could not parse longitude '{}' as a number", lon_str.trim())
142 })?;
143
144 if !(-90.0..=90.0).contains(&lat) {
149 bail!("latitude {} is outside the valid range [-90, 90]", lat);
150 }
151 if !(-180.0..=180.0).contains(&lon) {
152 bail!("longitude {} is outside the valid range [-180, 180]", lon);
153 }
154
155 coordinates.push((lat, lon));
156 }
157
158 Ok(coordinates)
159 }
160
161 fn validate_polygon_geometry(coordinates: &[(f64, f64)]) -> Result<()> {
163 if coordinates.len() < 4 {
164 bail!(
165 "polygon must have at least 4 coordinate pairs (3 unique vertices plus a \
166 closing repeat of the first vertex)"
167 );
168 }
169
170 let first = coordinates
171 .first()
172 .ok_or_else(|| anyhow!("polygon has no first coordinate"))?;
173 let last = coordinates
174 .last()
175 .ok_or_else(|| anyhow!("polygon has no last coordinate"))?;
176
177 if first != last {
178 bail!("polygon must be closed (first and last coordinates must be identical)");
179 }
180
181 Ok(())
182 }
183
184 pub fn calculate_bounding_box(coordinates: &[(f64, f64)]) -> String {
187 let mut min_lat = f64::INFINITY;
188 let mut min_lon = f64::INFINITY;
189 let mut max_lat = f64::NEG_INFINITY;
190 let mut max_lon = f64::NEG_INFINITY;
191
192 for &(lat, lon) in coordinates {
193 min_lat = min_lat.min(lat);
194 min_lon = min_lon.min(lon);
195 max_lat = max_lat.max(lat);
196 max_lon = max_lon.max(lon);
197 }
198
199 format!("{},{},{},{}", min_lat, min_lon, max_lat, max_lon)
200 }
201
202 pub fn parse_bbox_coordinates(s: &str) -> Result<(f64, f64, f64, f64)> {
203 let s = s.trim_matches(|c| c == '(' || c == ')');
205 let coords: Vec<f64> = s
206 .split(',')
207 .map(|part| part.trim().parse())
208 .collect::<Result<_, _>>()?;
209 if coords.len() != 4 {
210 anyhow::bail!("BBox must have 4 numbers");
211 }
212 Ok((coords[0], coords[1], coords[2], coords[3]))
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn test_validate_and_canonicalize_valid_polygon() {
222 let polygon_str = "(52.5,13.4,52.6,13.5,52.5,13.6,52.4,13.5,52.5,13.4)";
223 let result = PolygonHandler::validate_and_canonicalize(polygon_str, "polygon");
224
225 assert!(result.is_ok());
226 assert_eq!(result.unwrap(), polygon_str);
227 }
228
229 #[test]
230 fn test_validate_and_canonicalize_with_spaces() {
231 let polygon_str = "( 52.5 , 13.4 , 52.6 , 13.5 , 52.5 , 13.6 , 52.4 , 13.5 , 52.5 , 13.4 )";
232 let result = PolygonHandler::validate_and_canonicalize(polygon_str, "polygon");
233
234 assert!(result.is_ok());
235 assert_eq!(result.unwrap(), polygon_str);
236 }
237
238 #[test]
239 fn test_validate_and_canonicalize_not_closed() {
240 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");
242
243 assert!(result.is_err());
244 }
245
246 #[test]
247 fn test_validate_and_canonicalize_too_few_points() {
248 let polygon_str = "(52.5,13.4,52.6,13.5)"; let result = PolygonHandler::validate_and_canonicalize(polygon_str, "polygon");
250
251 assert!(result.is_err());
252 }
253
254 #[test]
255 fn test_validate_and_canonicalize_empty_string() {
256 let polygon_str = "";
257 let result = PolygonHandler::validate_and_canonicalize(polygon_str, "polygon");
258
259 assert!(result.is_err());
260 }
261
262 #[test]
263 fn test_validate_and_canonicalize_empty_parentheses() {
264 let polygon_str = "()";
265 let result = PolygonHandler::validate_and_canonicalize(polygon_str, "polygon");
266
267 assert!(result.is_err());
268 }
269
270 #[test]
271 fn test_parse_polygon_coordinates_valid() {
272 let coord_string = "(52.5,13.4,52.6,13.5,52.5,13.6,52.4,13.5,52.5,13.4)";
273 let result = PolygonHandler::parse_polygon_coordinates(coord_string);
274
275 assert!(result.is_ok());
276 let coordinates = result.unwrap();
277 assert_eq!(coordinates.len(), 5);
278 assert_eq!(coordinates[0], (52.5, 13.4));
279 assert_eq!(coordinates[1], (52.6, 13.5));
280 assert_eq!(coordinates[4], (52.5, 13.4)); }
282
283 #[test]
284 fn test_parse_polygon_coordinates_without_parentheses() {
285 let coord_string = "52.5,13.4,52.6,13.5,52.5,13.6,52.4,13.5,52.5,13.4";
286 let result = PolygonHandler::parse_polygon_coordinates(coord_string);
287
288 assert!(result.is_ok());
289 let coordinates = result.unwrap();
290 assert_eq!(coordinates.len(), 5);
291 assert_eq!(coordinates[0], (52.5, 13.4));
292 }
293
294 #[test]
295 fn test_parse_polygon_coordinates_with_spaces() {
296 let coord_string = "( 52.5 , 13.4 , 52.6 , 13.5 , 52.5 , 13.4 )";
297 let result = PolygonHandler::parse_polygon_coordinates(coord_string);
298
299 assert!(result.is_ok());
300 let coordinates = result.unwrap();
301 assert_eq!(coordinates.len(), 3);
302 assert_eq!(coordinates[0], (52.5, 13.4));
303 assert_eq!(coordinates[1], (52.6, 13.5));
304 assert_eq!(coordinates[2], (52.5, 13.4));
305 }
306
307 #[test]
308 fn test_parse_polygon_coordinates_odd_number() {
309 let coord_string = "(52.5,13.4,52.6)"; let result = PolygonHandler::parse_polygon_coordinates(coord_string);
311
312 assert!(result.is_err());
313 }
314
315 #[test]
316 fn test_parse_polygon_coordinates_invalid_latitude() {
317 let coord_string = "(invalid,13.4,52.6,13.5,52.5,13.4)";
318 let result = PolygonHandler::parse_polygon_coordinates(coord_string);
319
320 assert!(result.is_err());
321 }
322
323 #[test]
324 fn test_parse_polygon_coordinates_invalid_longitude() {
325 let coord_string = "(52.5,invalid,52.6,13.5,52.5,13.4)";
326 let result = PolygonHandler::parse_polygon_coordinates(coord_string);
327
328 assert!(result.is_err());
329 }
330
331 #[test]
332 fn test_parse_polygon_coordinates_empty() {
333 let coord_string = "()";
334 let result = PolygonHandler::parse_polygon_coordinates(coord_string);
335
336 assert!(result.is_err());
337 }
338
339 #[test]
340 fn rejects_polygon_with_opening_paren_but_no_closing_paren() {
341 let coord_string = "(50.0,10.0,52.0,10.0,52.0,12.0,50.0,12.0,50.0,10.0";
342 let err = PolygonHandler::parse_polygon_coordinates(coord_string)
343 .expect_err("unbalanced parens must be rejected");
344 let msg = err.to_string();
345 assert!(
346 msg.contains("opening") && msg.contains("missing the closing"),
347 "error should pinpoint the missing closing paren; got: {msg}"
348 );
349 }
350
351 #[test]
352 fn rejects_polygon_with_closing_paren_but_no_opening_paren() {
353 let coord_string = "50.0,10.0,52.0,10.0,52.0,12.0,50.0,12.0,50.0,10.0)";
354 let err = PolygonHandler::parse_polygon_coordinates(coord_string)
355 .expect_err("unbalanced parens must be rejected");
356 let msg = err.to_string();
357 assert!(
358 msg.contains("closing") && msg.contains("missing the opening"),
359 "error should pinpoint the missing opening paren; got: {msg}"
360 );
361 }
362
363 #[test]
364 fn rejects_polygon_with_extra_nested_parens() {
365 let coord_string = "(50.0,10.0),52.0,10.0,52.0,12.0,50.0,12.0,50.0,10.0)";
366 let err = PolygonHandler::parse_polygon_coordinates(coord_string)
367 .expect_err("nested parens must be rejected, not produce a confusing parse error");
368 let msg = err.to_string();
369 assert!(
370 msg.contains("nested") || msg.contains("outer pair"),
371 "error should mention parentheses placement, not e.g. a number-parse failure; got: {msg}"
372 );
373 }
374
375 #[test]
376 fn rejects_latitude_above_90() {
377 let coord_string = "(91.0,10.0,50.0,10.0,50.0,11.0,91.0,10.0)";
378 let err = PolygonHandler::parse_polygon_coordinates(coord_string)
379 .expect_err("latitude > 90 must be rejected");
380 let msg = err.to_string();
381 assert!(
382 msg.contains("latitude") && msg.contains("-90") && msg.contains("90"),
383 "error should pinpoint the latitude range; got: {msg}"
384 );
385 }
386
387 #[test]
388 fn rejects_latitude_below_minus_90() {
389 let coord_string = "(-90.5,10.0,50.0,10.0,50.0,11.0,-90.5,10.0)";
390 let err = PolygonHandler::parse_polygon_coordinates(coord_string)
391 .expect_err("latitude < -90 must be rejected");
392 let msg = err.to_string();
393 assert!(msg.contains("latitude") && msg.contains("-90.5"));
394 }
395
396 #[test]
397 fn rejects_longitude_above_180() {
398 let coord_string = "(50.0,181.0,50.0,10.0,50.0,11.0,50.0,181.0)";
399 let err = PolygonHandler::parse_polygon_coordinates(coord_string)
400 .expect_err("longitude > 180 must be rejected");
401 let msg = err.to_string();
402 assert!(
403 msg.contains("longitude") && msg.contains("-180") && msg.contains("180"),
404 "error should pinpoint the longitude range; got: {msg}"
405 );
406 }
407
408 #[test]
409 fn rejects_longitude_below_minus_180() {
410 let coord_string = "(50.0,-180.5,50.0,10.0,50.0,11.0,50.0,-180.5)";
411 let err = PolygonHandler::parse_polygon_coordinates(coord_string)
412 .expect_err("longitude < -180 must be rejected");
413 let msg = err.to_string();
414 assert!(msg.contains("longitude") && msg.contains("-180.5"));
415 }
416
417 #[test]
418 fn rejects_non_finite_coordinates_via_range_check() {
419 for bad_value in &["NaN", "inf", "-inf"] {
422 let coord_string = format!("({bad_value},10.0,50.0,10.0,50.0,11.0,{bad_value},10.0)");
423 assert!(
424 PolygonHandler::parse_polygon_coordinates(&coord_string).is_err(),
425 "non-finite coordinate `{bad_value}` must be rejected"
426 );
427 }
428 }
429
430 #[test]
431 fn accepts_exact_boundary_coordinates() {
432 let polygon = "(90.0,-180.0,89.0,-180.0,89.0,-179.0,90.0,-180.0)";
433 let result = PolygonHandler::parse_polygon_coordinates(polygon);
434 assert!(
435 result.is_ok(),
436 "lat=90 and lon=-180 are valid (inclusive) boundary values: {:?}",
437 result.err()
438 );
439 }
440
441 #[test]
442 fn validate_and_canonicalize_wraps_errors_with_field_name_and_validation_marker() {
443 let bad = "(50.0,10.0,52.0,10.0,52.0,12.0,50.0,12.0,50.0,10.0";
448 let err = PolygonHandler::validate_and_canonicalize(bad, "polygon")
449 .expect_err("unbalanced polygon must error");
450 let msg = err.to_string();
451 assert!(
452 msg.contains("field 'polygon'") && msg.contains("must be a valid"),
453 "error must carry the validation-classifier markers; got: {msg}"
454 );
455 }
456
457 #[test]
458 fn test_validate_polygon_geometry_valid_triangle() {
459 let coordinates = vec![(0.0, 0.0), (1.0, 0.0), (0.5, 1.0), (0.0, 0.0)];
460 let result = PolygonHandler::validate_polygon_geometry(&coordinates);
461
462 assert!(result.is_ok());
463 }
464
465 #[test]
466 fn test_validate_polygon_geometry_valid_rectangle() {
467 let coordinates = vec![
468 (52.5, 13.4),
469 (52.6, 13.4),
470 (52.6, 13.5),
471 (52.5, 13.5),
472 (52.5, 13.4),
473 ];
474 let result = PolygonHandler::validate_polygon_geometry(&coordinates);
475
476 assert!(result.is_ok());
477 }
478
479 #[test]
480 fn test_validate_polygon_geometry_too_few_points() {
481 let coordinates = vec![(0.0, 0.0), (1.0, 0.0)]; let result = PolygonHandler::validate_polygon_geometry(&coordinates);
483
484 assert!(result.is_err());
485 }
486
487 #[test]
488 fn rejects_three_pair_closed_line_segment_as_degenerate_polygon() {
489 let coordinates = vec![(0.0, 0.0), (1.0, 0.0), (0.0, 0.0)];
494 let err = PolygonHandler::validate_polygon_geometry(&coordinates)
495 .expect_err("3-pair closed line segment must be rejected as a polygon");
496 let msg = err.to_string();
497 assert!(
498 msg.contains("at least 4 coordinate pairs"),
499 "error must specify the new minimum; got: {msg}"
500 );
501 }
502
503 #[test]
504 fn test_validate_polygon_geometry_not_closed() {
505 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);
507
508 assert!(result.is_err());
509 }
510
511 #[test]
512 fn test_validate_polygon_geometry_minimum_valid() {
513 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);
515
516 assert!(result.is_ok());
517 }
518
519 #[test]
520 fn test_calculate_bounding_box_rectangle() {
521 let coordinates = vec![
522 (52.5, 13.4),
523 (52.6, 13.4),
524 (52.6, 13.5),
525 (52.5, 13.5),
526 (52.5, 13.4),
527 ];
528 let bbox = PolygonHandler::calculate_bounding_box(&coordinates);
529
530 assert_eq!(bbox, "52.5,13.4,52.6,13.5");
531 }
532
533 #[test]
534 fn test_calculate_bounding_box_triangle() {
535 let coordinates = vec![(0.0, 0.0), (1.0, 0.0), (0.5, 1.0), (0.0, 0.0)];
536 let bbox = PolygonHandler::calculate_bounding_box(&coordinates);
537
538 assert_eq!(bbox, "0,0,1,1");
539 }
540
541 #[test]
542 fn test_calculate_bounding_box_single_point() {
543 let coordinates = vec![(52.5, 13.4), (52.5, 13.4), (52.5, 13.4), (52.5, 13.4)];
544 let bbox = PolygonHandler::calculate_bounding_box(&coordinates);
545
546 assert_eq!(bbox, "52.5,13.4,52.5,13.4");
547 }
548
549 #[test]
550 fn test_calculate_bounding_box_negative_coordinates() {
551 let coordinates = vec![
552 (-1.0, -1.0),
553 (1.0, -1.0),
554 (1.0, 1.0),
555 (-1.0, 1.0),
556 (-1.0, -1.0),
557 ];
558 let bbox = PolygonHandler::calculate_bounding_box(&coordinates);
559
560 assert_eq!(bbox, "-1,-1,1,1");
561 }
562
563 #[test]
564 fn test_integration_parse_and_validate() {
565 let polygon_str = "(52.5,13.4,52.6,13.5,52.5,13.6,52.4,13.5,52.5,13.4)";
566
567 let coordinates = PolygonHandler::parse_polygon_coordinates(polygon_str).unwrap();
569 let validation_result = PolygonHandler::validate_polygon_geometry(&coordinates);
570 assert!(validation_result.is_ok());
571
572 let bbox = PolygonHandler::calculate_bounding_box(&coordinates);
573 assert_eq!(bbox, "52.4,13.4,52.6,13.6");
574 }
575
576 #[test]
577 fn test_real_world_berlin_polygon() {
578 let polygon_str =
580 "(52.5200,13.4050,52.5200,13.4500,52.4800,13.4500,52.4800,13.4050,52.5200,13.4050)";
581 let result = PolygonHandler::validate_and_canonicalize(polygon_str, "berlin_area");
582
583 assert!(result.is_ok());
584
585 let coordinates = PolygonHandler::parse_polygon_coordinates(polygon_str).unwrap();
586 let bbox = PolygonHandler::calculate_bounding_box(&coordinates);
587 assert_eq!(bbox, "52.48,13.405,52.52,13.45");
588 }
589
590 #[test]
591 fn test_precision_handling() {
592 let polygon_str = "(52.123456789,13.987654321,52.234567890,13.876543210,52.345678901,13.765432109,52.123456789,13.987654321)";
594 let result = PolygonHandler::validate_and_canonicalize(polygon_str, "precision_test");
595
596 assert!(result.is_ok());
597
598 let coordinates = PolygonHandler::parse_polygon_coordinates(polygon_str).unwrap();
599 assert_eq!(coordinates[0].0, 52.123456789);
600 assert_eq!(coordinates[0].1, 13.987654321);
601 }
602}