Skip to main content

aviso_validators/
polygon.rs

1// (C) Copyright 2024- ECMWF and individual contributors.
2//
3// This software is licensed under the terms of the Apache Licence Version 2.0
4// which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
5// In applying this licence, ECMWF does not waive the privileges and immunities
6// granted to it by virtue of its status as an intergovernmental organisation nor
7// does it submit to any jurisdiction.
8
9use crate::coordinate::{Coordinate, coordinates_to_json, parse_coordinate};
10use anyhow::{Result, anyhow, bail};
11use serde_json::Value;
12use tracing::debug;
13
14/// Polygon coordinate validator
15///
16/// Validates polygon coordinate strings in the format "(lat1,lon1,lat2,lon2,lat1,lon1)"
17/// and ensures proper polygon geometry (closed, minimum vertices, valid coordinates).
18pub 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    /// Validate a JSON polygon array or a compatible legacy polygon string.
46    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    /// Parse a JSON polygon coordinate array or a compatible legacy string.
57    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    /// Parse a polygon coordinate string into a vector of `(lat, lon)` tuples.
73    ///
74    /// Accepted forms (whitespace tolerated everywhere):
75    ///   * `"(lat1,lon1,...,lat1,lon1)"`, parenthesised and balanced
76    ///   * `"lat1,lon1,...,lat1,lon1"`, without parentheses
77    ///
78    /// Rejected forms (each with a specific error message):
79    ///   * Opening `(` without a matching closing `)` (or vice versa)
80    ///   * Embedded `(` or `)` anywhere except as the single outer pair
81    ///   * Empty string or `()`
82    ///   * Odd number of comma-separated values
83    ///   * Any value that does not parse as `f64`
84    ///
85    /// This function ALWAYS returns `(lat, lon)` pairs. DO NOT swap here; only
86    /// swap to `(lon, lat)` when passing to the `geo` crate.
87    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            // Range check after parse. `RangeInclusive::contains` uses f64's
145            // PartialOrd comparisons, so this single guard rejects NaN (every
146            // ordering comparison against NaN is false) and rejects ±inf (out
147            // of any finite range) without a separate is_finite() check.
148            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    /// Validates polygon geometry requirements
162    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    /// Calculates bounding box for spatial filtering optimization
185    /// This will be used later when we handle the payload and headers
186    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        // expects (lat_min,lon_min,lat_max,lon_max)
204        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)"; // Missing closing point
241        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)"; // Only 2 points
249        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)); // Should be closed
281    }
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)"; // Odd number of coordinates
310        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        // NaN and ±inf parse as valid f64 but fall outside any finite range, so
420        // the existing range check rejects them without a separate is_finite() guard.
421        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        // The classifier in handlers::notification_processor matches "field '" and
444        // "must be a valid" to route polygon errors to a 400 response. This test
445        // pins both substrings so the public error-classification contract does
446        // not silently drift.
447        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)]; // Only 2 points
482        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        // Three pairs is two unique vertices closed back on the first, i.e. a line
490        // segment, not a polygon. The downstream geo conversion in
491        // src/notification/spatial.rs requires 4+ pairs; without rejecting here
492        // the request silently degraded to a 500 NOTIFICATION_PROCESSING_FAILED.
493        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)]; // Not closed
506        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)]; // Minimum valid triangle
514        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        // Test the full pipeline: parse -> validate -> calculate bbox
568        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        // Real-world coordinates around Berlin
579        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        // Test with high precision coordinates
593        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}