Skip to main content

geo_core/
lib.rs

1#![doc = include_str!("../README.md")]
2
3pub mod surface;
4use std::collections::BTreeMap;
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use video_analysis_core::{DetectError, Result};
9
10/// JSON object used for feature properties.
11pub type Properties = BTreeMap<String, Value>;
12
13/// Two-dimensional coordinate position in `[longitude, latitude]` order.
14pub type Position = [f64; 2];
15
16const GEOMETRY_EPSILON: f64 = 1e-12;
17
18fn invalid_argument(message: impl Into<String>) -> DetectError {
19    DetectError::InvalidArgument(message.into())
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
23/// Data type for a 2D geographic coordinate.
24pub struct Coordinate {
25    /// Longitude or x coordinate.
26    pub lon: f64,
27    /// Latitude or y coordinate.
28    pub lat: f64,
29}
30
31impl Coordinate {
32    /// Creates a new value.
33    pub fn new(lon: f64, lat: f64) -> Result<Self> {
34        let coordinate = Self { lon, lat };
35        coordinate.validate()?;
36        Ok(coordinate)
37    }
38
39    /// Builds this value from a GeoJSON position.
40    pub fn from_position(position: Position) -> Result<Self> {
41        Self::new(position[0], position[1])
42    }
43
44    /// Returns this coordinate as a GeoJSON position.
45    pub fn as_position(self) -> Position {
46        [self.lon, self.lat]
47    }
48
49    /// Validates this value.
50    pub fn validate(self) -> Result<()> {
51        if !self.lon.is_finite() || !self.lat.is_finite() {
52            return Err(invalid_argument("coordinate values must be finite"));
53        }
54        Ok(())
55    }
56
57    /// Validates this value as a longitude/latitude coordinate.
58    pub fn validate_geographic(self) -> Result<()> {
59        self.validate()?;
60        if !(-180.0..=180.0).contains(&self.lon) {
61            return Err(invalid_argument(
62                "coordinate longitude must be between -180 and 180",
63            ));
64        }
65        if !(-90.0..=90.0).contains(&self.lat) {
66            return Err(invalid_argument(
67                "coordinate latitude must be between -90 and 90",
68            ));
69        }
70        Ok(())
71    }
72}
73
74impl From<Coordinate> for Position {
75    fn from(value: Coordinate) -> Self {
76        value.as_position()
77    }
78}
79
80impl TryFrom<Position> for Coordinate {
81    type Error = DetectError;
82
83    fn try_from(value: Position) -> Result<Self> {
84        Self::from_position(value)
85    }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
89/// Data type for a 2D bounding box.
90pub struct BBox {
91    /// Minimum longitude or x coordinate.
92    pub min_lon: f64,
93    /// Minimum latitude or y coordinate.
94    pub min_lat: f64,
95    /// Maximum longitude or x coordinate.
96    pub max_lon: f64,
97    /// Maximum latitude or y coordinate.
98    pub max_lat: f64,
99}
100
101impl BBox {
102    /// Creates a new value from `[min_lon, min_lat, max_lon, max_lat]`.
103    pub fn new(values: [f64; 4]) -> Result<Self> {
104        let bbox = Self {
105            min_lon: values[0],
106            min_lat: values[1],
107            max_lon: values[2],
108            max_lat: values[3],
109        };
110        bbox.validate()?;
111        Ok(bbox)
112    }
113
114    /// Returns this value as `[min_lon, min_lat, max_lon, max_lat]`.
115    pub fn as_array(self) -> [f64; 4] {
116        [self.min_lon, self.min_lat, self.max_lon, self.max_lat]
117    }
118
119    /// Validates this value.
120    pub fn validate(self) -> Result<()> {
121        if !self.min_lon.is_finite()
122            || !self.min_lat.is_finite()
123            || !self.max_lon.is_finite()
124            || !self.max_lat.is_finite()
125        {
126            return Err(invalid_argument("bbox values must be finite"));
127        }
128        if self.min_lon > self.max_lon {
129            return Err(invalid_argument("bbox min_lon must be <= max_lon"));
130        }
131        if self.min_lat > self.max_lat {
132            return Err(invalid_argument("bbox min_lat must be <= max_lat"));
133        }
134        Ok(())
135    }
136
137    /// Validates this value as a longitude/latitude bounding box.
138    pub fn validate_geographic(self) -> Result<()> {
139        self.validate()?;
140        Coordinate::new(self.min_lon, self.min_lat)?.validate_geographic()?;
141        Coordinate::new(self.max_lon, self.max_lat)?.validate_geographic()?;
142        Ok(())
143    }
144
145    /// Returns true when this bbox contains a coordinate.
146    pub fn contains(self, coordinate: Coordinate) -> bool {
147        coordinate.lon >= self.min_lon
148            && coordinate.lon <= self.max_lon
149            && coordinate.lat >= self.min_lat
150            && coordinate.lat <= self.max_lat
151    }
152
153    /// Returns true when this bbox contains at least one coordinate.
154    pub fn intersects_coordinates(self, coordinates: &[Coordinate]) -> bool {
155        coordinates
156            .iter()
157            .copied()
158            .any(|coordinate| self.contains(coordinate))
159    }
160
161    /// Returns true when this bbox intersects a geometry.
162    pub fn intersects_geometry(self, geometry: &Geometry) -> bool {
163        geometry_intersects_bbox(geometry, self)
164    }
165}
166
167#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
168#[serde(tag = "type")]
169/// GeoJSON-shaped geometry data.
170pub enum Geometry {
171    /// Point geometry.
172    Point {
173        /// Coordinate position.
174        coordinates: Position,
175    },
176    /// MultiPoint geometry.
177    MultiPoint {
178        /// Coordinate positions.
179        coordinates: Vec<Position>,
180    },
181    /// LineString geometry.
182    LineString {
183        /// Coordinate positions.
184        coordinates: Vec<Position>,
185    },
186    /// MultiLineString geometry.
187    MultiLineString {
188        /// Coordinate lines.
189        coordinates: Vec<Vec<Position>>,
190    },
191    /// Polygon geometry.
192    Polygon {
193        /// Linear rings. The first ring is the exterior ring.
194        coordinates: Vec<Vec<Position>>,
195    },
196    /// MultiPolygon geometry.
197    MultiPolygon {
198        /// Polygon rings grouped by polygon.
199        coordinates: Vec<Vec<Vec<Position>>>,
200    },
201    /// GeometryCollection geometry.
202    GeometryCollection {
203        /// Child geometries.
204        geometries: Vec<Geometry>,
205    },
206}
207
208impl Geometry {
209    /// Validates this geometry.
210    pub fn validate(&self) -> Result<()> {
211        match self {
212            Self::Point { coordinates } => {
213                Coordinate::from_position(*coordinates)?;
214            }
215            Self::MultiPoint { coordinates } => {
216                validate_positions(coordinates, "multipoint coordinates")?;
217            }
218            Self::LineString { coordinates } => {
219                validate_line_positions(coordinates, "linestring coordinates")?;
220            }
221            Self::MultiLineString { coordinates } => {
222                for line in coordinates {
223                    validate_line_positions(line, "multilinestring coordinates")?;
224                }
225            }
226            Self::Polygon { coordinates } => {
227                validate_polygon_positions(coordinates, "polygon coordinates")?;
228            }
229            Self::MultiPolygon { coordinates } => {
230                for polygon in coordinates {
231                    validate_polygon_positions(polygon, "multipolygon coordinates")?;
232                }
233            }
234            Self::GeometryCollection { geometries } => {
235                for geometry in geometries {
236                    geometry.validate()?;
237                }
238            }
239        }
240        Ok(())
241    }
242}
243
244#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
245/// GeoJSON-compatible feature data.
246pub struct GeoFeature {
247    /// Optional feature id.
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub id: Option<String>,
250    /// Optional feature bounding box.
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub bbox: Option<BBox>,
253    /// Optional feature geometry.
254    #[serde(skip_serializing_if = "Option::is_none")]
255    pub geometry: Option<Geometry>,
256    /// Feature properties.
257    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
258    pub properties: Properties,
259}
260
261impl GeoFeature {
262    /// Creates a new value.
263    pub fn new(geometry: Option<Geometry>) -> Self {
264        Self {
265            id: None,
266            bbox: None,
267            geometry,
268            properties: Properties::new(),
269        }
270    }
271
272    /// Returns this feature with an id.
273    pub fn with_id(mut self, id: impl Into<String>) -> Self {
274        self.id = Some(id.into());
275        self
276    }
277
278    /// Returns this feature with a bbox.
279    pub fn with_bbox(mut self, bbox: BBox) -> Result<Self> {
280        bbox.validate()?;
281        self.bbox = Some(bbox);
282        Ok(self)
283    }
284
285    /// Inserts a property value.
286    pub fn insert_property(&mut self, key: impl Into<String>, value: impl Into<Value>) {
287        self.properties.insert(key.into(), value.into());
288    }
289
290    /// Validates this feature.
291    pub fn validate(&self) -> Result<()> {
292        if self.id.as_ref().is_some_and(String::is_empty) {
293            return Err(invalid_argument("feature id must not be empty"));
294        }
295        if let Some(bbox) = self.bbox {
296            bbox.validate()?;
297        }
298        if let Some(geometry) = &self.geometry {
299            geometry.validate()?;
300        }
301        Ok(())
302    }
303}
304
305#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
306/// GeoJSON-compatible feature collection data.
307pub struct GeoFeatureCollection {
308    /// Optional collection bounding box.
309    #[serde(skip_serializing_if = "Option::is_none")]
310    pub bbox: Option<BBox>,
311    /// Collection features.
312    pub features: Vec<GeoFeature>,
313}
314
315impl GeoFeatureCollection {
316    /// Creates a new collection.
317    pub fn new(features: impl Into<Vec<GeoFeature>>) -> Self {
318        Self {
319            bbox: None,
320            features: features.into(),
321        }
322    }
323
324    /// Adds a feature to this collection.
325    pub fn push(&mut self, feature: GeoFeature) {
326        self.features.push(feature);
327    }
328
329    /// Filters features whose geometry intersects a bbox.
330    pub fn filter_intersecting_bbox(&self, bbox: BBox) -> Self {
331        Self {
332            bbox: self.bbox,
333            features: self
334                .features
335                .iter()
336                .filter(|feature| {
337                    feature
338                        .geometry
339                        .as_ref()
340                        .is_some_and(|geometry| bbox.intersects_geometry(geometry))
341                })
342                .cloned()
343                .collect(),
344        }
345    }
346
347    /// Validates this collection.
348    pub fn validate(&self) -> Result<()> {
349        if let Some(bbox) = self.bbox {
350            bbox.validate()?;
351        }
352        for feature in &self.features {
353            feature.validate()?;
354        }
355        Ok(())
356    }
357}
358
359/// Creates point geometry.
360pub fn point(coordinate: Coordinate) -> Geometry {
361    Geometry::Point {
362        coordinates: coordinate.as_position(),
363    }
364}
365
366/// Creates linestring geometry from coordinates.
367pub fn line_string(coordinates: &[Coordinate]) -> Result<Geometry> {
368    let coordinates = coordinates_to_positions(coordinates)?;
369    validate_line_positions(&coordinates, "linestring coordinates")?;
370    Ok(Geometry::LineString { coordinates })
371}
372
373/// Creates polygon or multipolygon geometry from polygon rings.
374pub fn polygon_or_multipolygon(polygons: Vec<Vec<Vec<Coordinate>>>) -> Option<Geometry> {
375    let mut output: Vec<Vec<Vec<Position>>> = polygons
376        .into_iter()
377        .map(|polygon| {
378            polygon
379                .into_iter()
380                .map(|ring| ring.into_iter().map(Coordinate::as_position).collect())
381                .collect()
382        })
383        .collect();
384
385    match output.len() {
386        0 => None,
387        1 => Some(Geometry::Polygon {
388            coordinates: output.remove(0),
389        }),
390        _ => Some(Geometry::MultiPolygon {
391            coordinates: output,
392        }),
393    }
394}
395
396/// Assembles polygon or multipolygon geometry from outer and inner ring segments.
397pub fn assemble_multipolygon(
398    outer_segments: Vec<Vec<Coordinate>>,
399    inner_segments: Vec<Vec<Coordinate>>,
400) -> Result<Geometry> {
401    let mut outer_rings = stitch_rings(outer_segments)
402        .ok_or_else(|| invalid_argument("outer ring segments could not be stitched"))?;
403    let mut inner_rings = stitch_rings(inner_segments)
404        .ok_or_else(|| invalid_argument("inner ring segments could not be stitched"))?;
405
406    if outer_rings.is_empty() {
407        return Err(invalid_argument(
408            "multipolygon requires at least one outer ring",
409        ));
410    }
411
412    for ring in &mut outer_rings {
413        normalize_ring_orientation(ring, true);
414    }
415    for ring in &mut inner_rings {
416        normalize_ring_orientation(ring, false);
417    }
418
419    let mut polygons: Vec<Vec<Vec<Coordinate>>> =
420        outer_rings.into_iter().map(|outer| vec![outer]).collect();
421
422    for inner in inner_rings {
423        let Some(point) = inner.first().copied() else {
424            return Err(invalid_argument("inner ring must not be empty"));
425        };
426        let Some((target_index, _)) = polygons
427            .iter()
428            .enumerate()
429            .filter_map(|(index, polygon)| {
430                let outer = &polygon[0];
431                if point_in_ring(point, outer) {
432                    Some((index, ring_area(outer).abs()))
433                } else {
434                    None
435                }
436            })
437            .min_by(|(_, left), (_, right)| left.total_cmp(right))
438        else {
439            return Err(invalid_argument(
440                "inner ring is not contained by an outer ring",
441            ));
442        };
443        polygons[target_index].push(inner);
444    }
445
446    polygon_or_multipolygon(polygons)
447        .ok_or_else(|| invalid_argument("multipolygon requires at least one polygon"))
448}
449
450/// Stitches line segments into closed rings.
451pub fn stitch_rings(mut segments: Vec<Vec<Coordinate>>) -> Option<Vec<Vec<Coordinate>>> {
452    let mut rings = Vec::new();
453
454    while !segments.is_empty() {
455        let mut ring = segments.remove(0);
456        if ring.len() < 2 {
457            return None;
458        }
459
460        loop {
461            if is_valid_closed_ring(&ring) {
462                rings.push(ring);
463                break;
464            }
465
466            let (index, action) = find_connecting_segment(&ring, &segments)?;
467            let segment = segments.remove(index);
468            apply_segment(&mut ring, segment, action);
469        }
470    }
471
472    Some(rings)
473}
474
475/// Returns true when a ring has at least four positions and matching first/last points.
476pub fn is_valid_closed_ring(ring: &[Coordinate]) -> bool {
477    ring.len() >= 4 && ring.first() == ring.last()
478}
479
480/// Returns signed planar area for a closed ring.
481pub fn ring_area(ring: &[Coordinate]) -> f64 {
482    if ring.len() < 4 {
483        return 0.0;
484    }
485    ring.windows(2)
486        .map(|window| {
487            let a = window[0];
488            let b = window[1];
489            (a.lon * b.lat) - (b.lon * a.lat)
490        })
491        .sum::<f64>()
492        / 2.0
493}
494
495/// Reverses a ring when needed to match the requested orientation.
496pub fn normalize_ring_orientation(ring: &mut [Coordinate], counter_clockwise: bool) {
497    let is_counter_clockwise = ring_area(ring) > 0.0;
498    if is_counter_clockwise != counter_clockwise {
499        ring.reverse();
500    }
501}
502
503/// Returns true when a point lies inside a closed ring.
504pub fn point_in_ring(point: Coordinate, ring: &[Coordinate]) -> bool {
505    if ring.len() < 4 {
506        return false;
507    }
508
509    let mut inside = false;
510    let mut previous = ring[ring.len() - 1];
511    for current in ring.iter().copied() {
512        let intersects = ((current.lat > point.lat) != (previous.lat > point.lat))
513            && (point.lon
514                < (previous.lon - current.lon) * (point.lat - current.lat)
515                    / (previous.lat - current.lat)
516                    + current.lon);
517        if intersects {
518            inside = !inside;
519        }
520        previous = current;
521    }
522    inside
523}
524
525/// Returns true when a geometry intersects a bbox.
526pub fn geometry_intersects_bbox(geometry: &Geometry, bbox: BBox) -> bool {
527    match geometry {
528        Geometry::Point { coordinates } => Coordinate::from_position(*coordinates)
529            .map(|coordinate| bbox.contains(coordinate))
530            .unwrap_or(false),
531        Geometry::MultiPoint { coordinates } => point_positions_intersect_bbox(coordinates, bbox),
532        Geometry::LineString { coordinates } => line_positions_intersect_bbox(coordinates, bbox),
533        Geometry::MultiLineString { coordinates } => coordinates
534            .iter()
535            .any(|line| line_positions_intersect_bbox(line, bbox)),
536        Geometry::Polygon { coordinates } => polygon_positions_intersect_bbox(coordinates, bbox),
537        Geometry::MultiPolygon { coordinates } => coordinates
538            .iter()
539            .any(|polygon| polygon_positions_intersect_bbox(polygon, bbox)),
540        Geometry::GeometryCollection { geometries } => geometries
541            .iter()
542            .any(|geometry| geometry_intersects_bbox(geometry, bbox)),
543    }
544}
545
546fn line_positions_intersect_bbox(coordinates: &[Position], bbox: BBox) -> bool {
547    if positions_intersect_bbox(coordinates, bbox) {
548        return true;
549    }
550    positions_to_coordinates(coordinates)
551        .map(|coordinates| line_segments_intersect_bbox(&coordinates, bbox))
552        .unwrap_or(false)
553}
554
555fn polygon_positions_intersect_bbox(polygon: &[Vec<Position>], bbox: BBox) -> bool {
556    let rings = polygon
557        .iter()
558        .map(|ring| positions_to_coordinates(ring))
559        .collect::<Result<Vec<_>>>();
560    let Ok(rings) = rings else {
561        return false;
562    };
563    if rings.iter().any(|ring| {
564        ring.iter()
565            .copied()
566            .any(|coordinate| bbox.contains(coordinate))
567            || line_segments_intersect_bbox(ring, bbox)
568    }) {
569        return true;
570    }
571    let Some(exterior) = rings.first() else {
572        return false;
573    };
574    bbox_corners(bbox).iter().copied().any(|corner| {
575        point_in_ring(corner, exterior)
576            && !rings
577                .iter()
578                .skip(1)
579                .any(|interior| point_in_ring(corner, interior))
580    })
581}
582
583fn line_segments_intersect_bbox(coordinates: &[Coordinate], bbox: BBox) -> bool {
584    coordinates
585        .windows(2)
586        .any(|segment| segment_intersects_bbox(segment[0], segment[1], bbox))
587}
588
589fn segment_intersects_bbox(start: Coordinate, end: Coordinate, bbox: BBox) -> bool {
590    if bbox.contains(start) || bbox.contains(end) {
591        return true;
592    }
593    if start.lon.max(end.lon) < bbox.min_lon
594        || start.lon.min(end.lon) > bbox.max_lon
595        || start.lat.max(end.lat) < bbox.min_lat
596        || start.lat.min(end.lat) > bbox.max_lat
597    {
598        return false;
599    }
600    let corners = bbox_corners(bbox);
601    let edges = [
602        (corners[0], corners[1]),
603        (corners[1], corners[2]),
604        (corners[2], corners[3]),
605        (corners[3], corners[0]),
606    ];
607    edges
608        .iter()
609        .any(|(edge_start, edge_end)| segments_intersect(start, end, *edge_start, *edge_end))
610}
611
612fn bbox_corners(bbox: BBox) -> [Coordinate; 4] {
613    [
614        Coordinate {
615            lon: bbox.min_lon,
616            lat: bbox.min_lat,
617        },
618        Coordinate {
619            lon: bbox.max_lon,
620            lat: bbox.min_lat,
621        },
622        Coordinate {
623            lon: bbox.max_lon,
624            lat: bbox.max_lat,
625        },
626        Coordinate {
627            lon: bbox.min_lon,
628            lat: bbox.max_lat,
629        },
630    ]
631}
632
633fn segments_intersect(
634    first_start: Coordinate,
635    first_end: Coordinate,
636    second_start: Coordinate,
637    second_end: Coordinate,
638) -> bool {
639    let o1 = orientation_sign(first_start, first_end, second_start);
640    let o2 = orientation_sign(first_start, first_end, second_end);
641    let o3 = orientation_sign(second_start, second_end, first_start);
642    let o4 = orientation_sign(second_start, second_end, first_end);
643
644    if o1 == 0 && coordinate_on_segment(second_start, first_start, first_end) {
645        return true;
646    }
647    if o2 == 0 && coordinate_on_segment(second_end, first_start, first_end) {
648        return true;
649    }
650    if o3 == 0 && coordinate_on_segment(first_start, second_start, second_end) {
651        return true;
652    }
653    if o4 == 0 && coordinate_on_segment(first_end, second_start, second_end) {
654        return true;
655    }
656
657    o1 != o2 && o3 != o4
658}
659
660fn orientation_sign(a: Coordinate, b: Coordinate, c: Coordinate) -> i8 {
661    let orientation = (b.lon - a.lon) * (c.lat - a.lat) - (b.lat - a.lat) * (c.lon - a.lon);
662    if orientation.abs() <= GEOMETRY_EPSILON {
663        0
664    } else if orientation > 0.0 {
665        1
666    } else {
667        -1
668    }
669}
670
671fn coordinate_on_segment(point: Coordinate, start: Coordinate, end: Coordinate) -> bool {
672    orientation_sign(start, end, point) == 0
673        && point.lon >= start.lon.min(end.lon) - GEOMETRY_EPSILON
674        && point.lon <= start.lon.max(end.lon) + GEOMETRY_EPSILON
675        && point.lat >= start.lat.min(end.lat) - GEOMETRY_EPSILON
676        && point.lat <= start.lat.max(end.lat) + GEOMETRY_EPSILON
677}
678
679/// Applies a coordinate transform to every coordinate in a geometry.
680pub fn map_geometry_coordinates<F>(geometry: &Geometry, mut transform: F) -> Result<Geometry>
681where
682    F: FnMut(Coordinate) -> Result<Coordinate>,
683{
684    map_geometry_coordinates_inner(geometry, &mut transform)
685}
686
687/// Applies a coordinate transform to every coordinate in a feature.
688pub fn map_feature_coordinates<F>(feature: &GeoFeature, transform: F) -> Result<GeoFeature>
689where
690    F: FnMut(Coordinate) -> Result<Coordinate>,
691{
692    let geometry = match &feature.geometry {
693        Some(geometry) => Some(map_geometry_coordinates(geometry, transform)?),
694        None => None,
695    };
696
697    Ok(GeoFeature {
698        id: feature.id.clone(),
699        bbox: feature.bbox,
700        geometry,
701        properties: feature.properties.clone(),
702    })
703}
704
705/// Translates every coordinate in a geometry by a longitude/x and latitude/y delta.
706pub fn translate_geometry(geometry: &Geometry, delta_lon: f64, delta_lat: f64) -> Result<Geometry> {
707    if !delta_lon.is_finite() || !delta_lat.is_finite() {
708        return Err(invalid_argument("translation delta values must be finite"));
709    }
710    map_geometry_coordinates(geometry, |coordinate| {
711        Coordinate::new(coordinate.lon + delta_lon, coordinate.lat + delta_lat)
712    })
713}
714
715/// Simplifies lines and rings in a geometry using Douglas-Peucker simplification.
716pub fn simplify_geometry(geometry: &Geometry, tolerance: f64) -> Result<Geometry> {
717    validate_tolerance(tolerance)?;
718    match geometry {
719        Geometry::Point { .. } | Geometry::MultiPoint { .. } => Ok(geometry.clone()),
720        Geometry::LineString { coordinates } => Ok(Geometry::LineString {
721            coordinates: coordinates_to_positions(&simplify_line(
722                &positions_to_coordinates(coordinates)?,
723                tolerance,
724            )?)?,
725        }),
726        Geometry::MultiLineString { coordinates } => Ok(Geometry::MultiLineString {
727            coordinates: coordinates
728                .iter()
729                .map(|line| {
730                    let simplified = simplify_line(&positions_to_coordinates(line)?, tolerance)?;
731                    coordinates_to_positions(&simplified)
732                })
733                .collect::<Result<Vec<_>>>()?,
734        }),
735        Geometry::Polygon { coordinates } => Ok(Geometry::Polygon {
736            coordinates: simplify_polygon_positions(coordinates, tolerance)?,
737        }),
738        Geometry::MultiPolygon { coordinates } => Ok(Geometry::MultiPolygon {
739            coordinates: coordinates
740                .iter()
741                .map(|polygon| simplify_polygon_positions(polygon, tolerance))
742                .collect::<Result<Vec<_>>>()?,
743        }),
744        Geometry::GeometryCollection { geometries } => Ok(Geometry::GeometryCollection {
745            geometries: geometries
746                .iter()
747                .map(|geometry| simplify_geometry(geometry, tolerance))
748                .collect::<Result<Vec<_>>>()?,
749        }),
750    }
751}
752
753/// Simplifies an open line using Douglas-Peucker simplification.
754pub fn simplify_line(coordinates: &[Coordinate], tolerance: f64) -> Result<Vec<Coordinate>> {
755    validate_tolerance(tolerance)?;
756    validate_coordinate_slice(coordinates, 2, "line coordinates")?;
757
758    if coordinates.len() <= 2 || tolerance == 0.0 {
759        return Ok(coordinates.to_vec());
760    }
761
762    let mut keep = vec![false; coordinates.len()];
763    keep[0] = true;
764    keep[coordinates.len() - 1] = true;
765    simplify_line_range(coordinates, 0, coordinates.len() - 1, tolerance, &mut keep);
766
767    Ok(coordinates
768        .iter()
769        .copied()
770        .zip(keep)
771        .filter_map(|(coordinate, keep)| keep.then_some(coordinate))
772        .collect())
773}
774
775/// Simplifies a closed ring and preserves ring closure.
776pub fn simplify_ring(ring: &[Coordinate], tolerance: f64) -> Result<Vec<Coordinate>> {
777    validate_tolerance(tolerance)?;
778    validate_ring(ring, "ring coordinates")?;
779
780    if ring.len() <= 5 || tolerance == 0.0 {
781        return Ok(ring.to_vec());
782    }
783
784    let mut open_ring = ring[..ring.len() - 1].to_vec();
785    let first = open_ring[0];
786    open_ring.push(first);
787    let mut simplified = simplify_line(&open_ring, tolerance)?;
788    simplified.pop();
789
790    if simplified.len() < 3 {
791        return Ok(ring.to_vec());
792    }
793
794    if simplified.last().copied() != Some(first) {
795        simplified.push(first);
796    }
797    Ok(simplified)
798}
799
800fn validate_positions(coordinates: &[Position], label: &str) -> Result<()> {
801    for coordinate in coordinates {
802        Coordinate::from_position(*coordinate)
803            .map_err(|_| invalid_argument(format!("{label} must be finite")))?;
804    }
805    Ok(())
806}
807
808fn validate_line_positions(coordinates: &[Position], label: &str) -> Result<()> {
809    if coordinates.len() < 2 {
810        return Err(invalid_argument(format!(
811            "{label} must contain at least two positions"
812        )));
813    }
814    validate_positions(coordinates, label)
815}
816
817fn validate_polygon_positions(coordinates: &[Vec<Position>], label: &str) -> Result<()> {
818    if coordinates.is_empty() {
819        return Err(invalid_argument(format!(
820            "{label} must contain at least one ring"
821        )));
822    }
823    for ring in coordinates {
824        let ring = positions_to_coordinates(ring)?;
825        validate_ring(&ring, label)?;
826    }
827    Ok(())
828}
829
830fn validate_coordinate_slice(
831    coordinates: &[Coordinate],
832    minimum: usize,
833    label: &str,
834) -> Result<()> {
835    if coordinates.len() < minimum {
836        return Err(invalid_argument(format!(
837            "{label} must contain at least {minimum} positions"
838        )));
839    }
840    for coordinate in coordinates {
841        coordinate.validate()?;
842    }
843    Ok(())
844}
845
846fn validate_ring(ring: &[Coordinate], label: &str) -> Result<()> {
847    validate_coordinate_slice(ring, 4, label)?;
848    if ring.first() != ring.last() {
849        return Err(invalid_argument(format!("{label} must be closed")));
850    }
851    Ok(())
852}
853
854fn validate_tolerance(tolerance: f64) -> Result<()> {
855    if tolerance < 0.0 || !tolerance.is_finite() {
856        return Err(invalid_argument(
857            "simplification tolerance must be finite and non-negative",
858        ));
859    }
860    Ok(())
861}
862
863fn positions_to_coordinates(positions: &[Position]) -> Result<Vec<Coordinate>> {
864    positions
865        .iter()
866        .copied()
867        .map(Coordinate::from_position)
868        .collect()
869}
870
871fn coordinates_to_positions(coordinates: &[Coordinate]) -> Result<Vec<Position>> {
872    coordinates
873        .iter()
874        .map(|coordinate| {
875            coordinate.validate()?;
876            Ok(coordinate.as_position())
877        })
878        .collect()
879}
880
881fn point_positions_intersect_bbox(coordinates: &[Position], bbox: BBox) -> bool {
882    coordinates
883        .iter()
884        .copied()
885        .filter_map(|position| Coordinate::from_position(position).ok())
886        .any(|coordinate| bbox.contains(coordinate))
887}
888
889fn positions_intersect_bbox(coordinates: &[Position], bbox: BBox) -> bool {
890    let coordinates = coordinates
891        .iter()
892        .copied()
893        .filter_map(|position| Coordinate::from_position(position).ok())
894        .collect::<Vec<_>>();
895    coordinates
896        .iter()
897        .copied()
898        .any(|coordinate| bbox.contains(coordinate))
899        || line_segments_intersect_bbox(&coordinates, bbox)
900}
901
902fn map_geometry_coordinates_inner(
903    geometry: &Geometry,
904    transform: &mut dyn FnMut(Coordinate) -> Result<Coordinate>,
905) -> Result<Geometry> {
906    match geometry {
907        Geometry::Point { coordinates } => Ok(Geometry::Point {
908            coordinates: transform(Coordinate::from_position(*coordinates)?)?.as_position(),
909        }),
910        Geometry::MultiPoint { coordinates } => Ok(Geometry::MultiPoint {
911            coordinates: map_positions(coordinates, transform)?,
912        }),
913        Geometry::LineString { coordinates } => Ok(Geometry::LineString {
914            coordinates: map_positions(coordinates, transform)?,
915        }),
916        Geometry::MultiLineString { coordinates } => Ok(Geometry::MultiLineString {
917            coordinates: coordinates
918                .iter()
919                .map(|line| map_positions(line, transform))
920                .collect::<Result<Vec<_>>>()?,
921        }),
922        Geometry::Polygon { coordinates } => Ok(Geometry::Polygon {
923            coordinates: coordinates
924                .iter()
925                .map(|ring| map_positions(ring, transform))
926                .collect::<Result<Vec<_>>>()?,
927        }),
928        Geometry::MultiPolygon { coordinates } => Ok(Geometry::MultiPolygon {
929            coordinates: coordinates
930                .iter()
931                .map(|polygon| {
932                    polygon
933                        .iter()
934                        .map(|ring| map_positions(ring, transform))
935                        .collect::<Result<Vec<_>>>()
936                })
937                .collect::<Result<Vec<_>>>()?,
938        }),
939        Geometry::GeometryCollection { geometries } => Ok(Geometry::GeometryCollection {
940            geometries: geometries
941                .iter()
942                .map(|geometry| map_geometry_coordinates_inner(geometry, transform))
943                .collect::<Result<Vec<_>>>()?,
944        }),
945    }
946}
947
948fn map_positions(
949    positions: &[Position],
950    transform: &mut dyn FnMut(Coordinate) -> Result<Coordinate>,
951) -> Result<Vec<Position>> {
952    positions
953        .iter()
954        .copied()
955        .map(|position| {
956            transform(Coordinate::from_position(position)?).map(Coordinate::as_position)
957        })
958        .collect()
959}
960
961fn simplify_polygon_positions(
962    polygon: &[Vec<Position>],
963    tolerance: f64,
964) -> Result<Vec<Vec<Position>>> {
965    polygon
966        .iter()
967        .map(|ring| {
968            let simplified = simplify_ring(&positions_to_coordinates(ring)?, tolerance)?;
969            coordinates_to_positions(&simplified)
970        })
971        .collect()
972}
973
974fn simplify_line_range(
975    coordinates: &[Coordinate],
976    start: usize,
977    end: usize,
978    tolerance: f64,
979    keep: &mut [bool],
980) {
981    if end <= start + 1 {
982        return;
983    }
984
985    let mut farthest_index = start + 1;
986    let mut farthest_distance = 0.0;
987    for index in start + 1..end {
988        let distance =
989            perpendicular_distance(coordinates[index], coordinates[start], coordinates[end]);
990        if distance > farthest_distance {
991            farthest_distance = distance;
992            farthest_index = index;
993        }
994    }
995
996    if farthest_distance > tolerance {
997        keep[farthest_index] = true;
998        simplify_line_range(coordinates, start, farthest_index, tolerance, keep);
999        simplify_line_range(coordinates, farthest_index, end, tolerance, keep);
1000    }
1001}
1002
1003fn perpendicular_distance(point: Coordinate, start: Coordinate, end: Coordinate) -> f64 {
1004    let dx = end.lon - start.lon;
1005    let dy = end.lat - start.lat;
1006    if dx == 0.0 && dy == 0.0 {
1007        return (point.lon - start.lon).hypot(point.lat - start.lat);
1008    }
1009    ((dy * point.lon - dx * point.lat + end.lon * start.lat - end.lat * start.lon).abs())
1010        / dx.hypot(dy)
1011}
1012
1013#[derive(Debug, Clone, Copy)]
1014enum StitchAction {
1015    AppendForward,
1016    AppendReverse,
1017    PrependForward,
1018    PrependReverse,
1019}
1020
1021fn find_connecting_segment(
1022    ring: &[Coordinate],
1023    segments: &[Vec<Coordinate>],
1024) -> Option<(usize, StitchAction)> {
1025    let first = *ring.first()?;
1026    let last = *ring.last()?;
1027    segments.iter().enumerate().find_map(|(index, segment)| {
1028        let segment_first = *segment.first()?;
1029        let segment_last = *segment.last()?;
1030        if last == segment_first {
1031            Some((index, StitchAction::AppendForward))
1032        } else if last == segment_last {
1033            Some((index, StitchAction::AppendReverse))
1034        } else if first == segment_last {
1035            Some((index, StitchAction::PrependForward))
1036        } else if first == segment_first {
1037            Some((index, StitchAction::PrependReverse))
1038        } else {
1039            None
1040        }
1041    })
1042}
1043
1044fn apply_segment(ring: &mut Vec<Coordinate>, mut segment: Vec<Coordinate>, action: StitchAction) {
1045    match action {
1046        StitchAction::AppendForward => ring.extend(segment.into_iter().skip(1)),
1047        StitchAction::AppendReverse => {
1048            segment.reverse();
1049            ring.extend(segment.into_iter().skip(1));
1050        }
1051        StitchAction::PrependForward => {
1052            segment.pop();
1053            segment.extend(ring.iter().copied());
1054            *ring = segment;
1055        }
1056        StitchAction::PrependReverse => {
1057            segment.reverse();
1058            segment.pop();
1059            segment.extend(ring.iter().copied());
1060            *ring = segment;
1061        }
1062    }
1063}
1064
1065#[cfg(test)]
1066mod tests {
1067    use super::*;
1068
1069    fn coord(lon: f64, lat: f64) -> Coordinate {
1070        Coordinate::new(lon, lat).unwrap()
1071    }
1072
1073    #[test]
1074    fn bbox_contains_coordinate() {
1075        let bbox = BBox::new([8.5, 48.8, 9.3, 49.2]).unwrap();
1076
1077        assert!(bbox.contains(coord(8.7, 48.9)));
1078        assert!(!bbox.contains(coord(10.0, 48.9)));
1079    }
1080
1081    #[test]
1082    fn bbox_intersects_crossing_line_segments() {
1083        let bbox = BBox::new([0.0, 0.0, 1.0, 1.0]).unwrap();
1084        let geometry = Geometry::LineString {
1085            coordinates: vec![[-1.0, 0.5], [2.0, 0.5]],
1086        };
1087
1088        assert!(bbox.intersects_geometry(&geometry));
1089    }
1090
1091    #[test]
1092    fn segment_intersection_handles_crossing_touching_overlap_and_separated() {
1093        assert!(segments_intersect(
1094            coord(0.0, 0.0),
1095            coord(2.0, 2.0),
1096            coord(0.0, 2.0),
1097            coord(2.0, 0.0),
1098        ));
1099        assert!(segments_intersect(
1100            coord(0.0, 0.0),
1101            coord(1.0, 1.0),
1102            coord(1.0, 1.0),
1103            coord(2.0, 0.0),
1104        ));
1105        assert!(segments_intersect(
1106            coord(0.0, 0.0),
1107            coord(2.0, 0.0),
1108            coord(1.0, 0.0),
1109            coord(3.0, 0.0),
1110        ));
1111        assert!(!segments_intersect(
1112            coord(0.0, 0.0),
1113            coord(1.0, 0.0),
1114            coord(2.0, 0.0),
1115            coord(3.0, 0.0),
1116        ));
1117    }
1118
1119    #[test]
1120    fn coordinate_on_segment_handles_endpoint_interior_and_off_segment() {
1121        let start = coord(0.0, 0.0);
1122        let end = coord(2.0, 2.0);
1123
1124        assert!(coordinate_on_segment(start, start, end));
1125        assert!(coordinate_on_segment(coord(1.0, 1.0), start, end));
1126        assert!(!coordinate_on_segment(coord(1.0, 1.1), start, end));
1127        assert!(!coordinate_on_segment(coord(3.0, 3.0), start, end));
1128    }
1129
1130    #[test]
1131    fn positions_intersect_bbox_detects_crossing_without_inside_vertices() {
1132        let bbox = BBox::new([0.0, 0.0, 1.0, 1.0]).unwrap();
1133
1134        assert!(positions_intersect_bbox(&[[-1.0, 0.5], [2.0, 0.5]], bbox));
1135    }
1136
1137    #[test]
1138    fn bbox_intersects_polygon_that_contains_viewport() {
1139        let bbox = BBox::new([0.0, 0.0, 1.0, 1.0]).unwrap();
1140        let geometry = Geometry::Polygon {
1141            coordinates: vec![vec![
1142                [-1.0, -1.0],
1143                [2.0, -1.0],
1144                [2.0, 2.0],
1145                [-1.0, 2.0],
1146                [-1.0, -1.0],
1147            ]],
1148        };
1149
1150        assert!(bbox.intersects_geometry(&geometry));
1151    }
1152
1153    #[test]
1154    fn bbox_inside_polygon_hole_does_not_intersect() {
1155        let bbox = BBox::new([0.25, 0.25, 0.75, 0.75]).unwrap();
1156        let geometry = Geometry::Polygon {
1157            coordinates: vec![
1158                vec![
1159                    [-1.0, -1.0],
1160                    [2.0, -1.0],
1161                    [2.0, 2.0],
1162                    [-1.0, 2.0],
1163                    [-1.0, -1.0],
1164                ],
1165                vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]],
1166            ],
1167        };
1168
1169        assert!(!bbox.intersects_geometry(&geometry));
1170    }
1171
1172    #[test]
1173    fn normalizes_ring_orientation() {
1174        let mut ring = vec![
1175            coord(0.0, 0.0),
1176            coord(0.0, 1.0),
1177            coord(1.0, 1.0),
1178            coord(1.0, 0.0),
1179            coord(0.0, 0.0),
1180        ];
1181
1182        normalize_ring_orientation(&mut ring, true);
1183        assert!(ring_area(&ring) > 0.0);
1184        normalize_ring_orientation(&mut ring, false);
1185        assert!(ring_area(&ring) < 0.0);
1186    }
1187
1188    #[test]
1189    fn point_in_ring_detects_inside_and_outside() {
1190        let ring = vec![
1191            coord(0.0, 0.0),
1192            coord(1.0, 0.0),
1193            coord(1.0, 1.0),
1194            coord(0.0, 1.0),
1195            coord(0.0, 0.0),
1196        ];
1197
1198        assert!(point_in_ring(coord(0.5, 0.5), &ring));
1199        assert!(!point_in_ring(coord(2.0, 0.5), &ring));
1200    }
1201
1202    #[test]
1203    fn stitches_reversed_segments_into_ring() {
1204        let segments = vec![
1205            vec![coord(0.0, 0.0), coord(1.0, 0.0), coord(1.0, 1.0)],
1206            vec![coord(0.0, 0.0), coord(0.0, 1.0), coord(1.0, 1.0)],
1207        ];
1208
1209        let rings = stitch_rings(segments).unwrap();
1210
1211        assert_eq!(rings.len(), 1);
1212        assert!(is_valid_closed_ring(&rings[0]));
1213    }
1214
1215    #[test]
1216    fn assemble_multipolygon_assigns_inner_ring() {
1217        let outer = vec![
1218            coord(0.0, 0.0),
1219            coord(4.0, 0.0),
1220            coord(4.0, 4.0),
1221            coord(0.0, 4.0),
1222            coord(0.0, 0.0),
1223        ];
1224        let inner = vec![
1225            coord(1.0, 1.0),
1226            coord(2.0, 1.0),
1227            coord(2.0, 2.0),
1228            coord(1.0, 2.0),
1229            coord(1.0, 1.0),
1230        ];
1231
1232        let geometry = assemble_multipolygon(vec![outer], vec![inner]).unwrap();
1233
1234        let Geometry::Polygon { coordinates } = geometry else {
1235            panic!("expected polygon");
1236        };
1237        assert_eq!(coordinates.len(), 2);
1238    }
1239
1240    #[test]
1241    fn maps_geometry_coordinates() {
1242        let geometry = point(coord(1.0, 2.0));
1243
1244        let translated = translate_geometry(&geometry, 3.0, 4.0).unwrap();
1245
1246        assert_eq!(
1247            translated,
1248            Geometry::Point {
1249                coordinates: [4.0, 6.0]
1250            }
1251        );
1252    }
1253
1254    #[test]
1255    fn simplifies_line_coordinates() {
1256        let line = vec![
1257            coord(0.0, 0.0),
1258            coord(1.0, 0.01),
1259            coord(2.0, -0.01),
1260            coord(3.0, 0.0),
1261        ];
1262
1263        let simplified = simplify_line(&line, 0.1).unwrap();
1264
1265        assert_eq!(simplified, vec![coord(0.0, 0.0), coord(3.0, 0.0)]);
1266    }
1267
1268    #[test]
1269    fn simplify_ring_preserves_valid_closure() {
1270        let ring = vec![
1271            coord(0.0, 0.0),
1272            coord(1.0, 0.01),
1273            coord(2.0, 0.0),
1274            coord(2.0, 2.0),
1275            coord(0.0, 2.0),
1276            coord(0.0, 0.0),
1277        ];
1278
1279        let simplified = simplify_ring(&ring, 0.1).unwrap();
1280
1281        assert!(is_valid_closed_ring(&simplified));
1282        assert_eq!(simplified.first(), simplified.last());
1283    }
1284
1285    #[test]
1286    fn feature_properties_are_generic_json_values() {
1287        let mut feature = GeoFeature::new(Some(point(coord(8.7, 48.9)))).with_id("node/123");
1288        feature.insert_property("name", "Test");
1289
1290        assert_eq!(feature.id.as_deref(), Some("node/123"));
1291        assert_eq!(feature.properties["name"], Value::from("Test"));
1292    }
1293}