Skip to main content

geo_core/
lib.rs

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