Skip to main content

typed_geojson/
geo_types_bridge.rs

1//! Conversions between our typed geometries and [`geo_types`] (the georust
2//! primitive layer that `geo`'s algorithms operate on), behind the `geo-types`
3//! feature — mirroring the same feature-gated bridge on the underlying
4//! `geojson` crate, and matching its semantics:
5//!
6//! - **`geo_types` → ours is infallible** ([`From`]). A `geo_types` coordinate
7//!   is `(x, y)`, so it becomes a 2-element GeoJSON position; a higher
8//!   dimension `geo_types` does not carry is simply not present (exactly as
9//!   upstream `geojson` does). `Line` → `LineString`, and `Rect` / `Triangle`
10//!   → `Polygon`, since GeoJSON has no native form for those.
11//! - **ours → `geo_types` is fallible** ([`TryFrom`]). The geometry *kind*
12//!   always lines up (our types are concrete), but a position could be missing
13//!   its `x`/`y`, so a position with fewer than two numbers is an error. The
14//!   error type is [`serde_json::Error`], the one every bridge in this crate
15//!   already reports, so callers see a single error type across all of them.
16//!
17//! All conversions are on `f64` coordinates (GeoJSON is `f64` per RFC 7946).
18//! They operate on the geometry types; a `Feature`'s geometry converts by
19//! reaching through its `geometry` field.
20
21use serde::de;
22
23use crate::{
24    Geometry, GeometryCollection, LineString, MultiLineString, MultiPoint, MultiPolygon, Point,
25    Polygon, Position,
26};
27
28// --- geo_types -> ours (infallible) -------------------------------------------
29
30/// A `geo_types` coordinate as a 2D GeoJSON position `[x, y]`.
31fn position(coord: geo_types::Coord<f64>) -> Position {
32    vec![coord.x, coord.y]
33}
34
35/// The vertices of a `geo_types` line string as GeoJSON positions.
36fn line_string_positions(ls: geo_types::LineString<f64>) -> Vec<Position> {
37    ls.0.into_iter().map(position).collect()
38}
39
40/// A `geo_types` polygon as GeoJSON rings (exterior first, then interiors).
41/// An empty exterior yields no rings — matching upstream, which avoids `[[]]`.
42fn polygon_rings(poly: geo_types::Polygon<f64>) -> Vec<Vec<Position>> {
43    let (exterior, interiors) = poly.into_inner();
44    let exterior = line_string_positions(exterior);
45    if exterior.is_empty() {
46        return vec![];
47    }
48    let mut rings = vec![exterior];
49    rings.extend(interiors.into_iter().map(line_string_positions));
50    rings
51}
52
53impl From<geo_types::Point<f64>> for Point {
54    fn from(p: geo_types::Point<f64>) -> Self {
55        Point::new(position(p.0))
56    }
57}
58
59impl From<geo_types::MultiPoint<f64>> for MultiPoint {
60    fn from(mp: geo_types::MultiPoint<f64>) -> Self {
61        MultiPoint::new(mp.0.into_iter().map(|p| position(p.0)).collect())
62    }
63}
64
65impl From<geo_types::LineString<f64>> for LineString {
66    fn from(ls: geo_types::LineString<f64>) -> Self {
67        LineString::new(line_string_positions(ls))
68    }
69}
70
71impl From<geo_types::MultiLineString<f64>> for MultiLineString {
72    fn from(mls: geo_types::MultiLineString<f64>) -> Self {
73        MultiLineString::new(mls.0.into_iter().map(line_string_positions).collect())
74    }
75}
76
77impl From<geo_types::Polygon<f64>> for Polygon {
78    fn from(poly: geo_types::Polygon<f64>) -> Self {
79        Polygon::new(polygon_rings(poly))
80    }
81}
82
83impl From<geo_types::MultiPolygon<f64>> for MultiPolygon {
84    fn from(mp: geo_types::MultiPolygon<f64>) -> Self {
85        MultiPolygon::new(mp.0.into_iter().map(polygon_rings).collect())
86    }
87}
88
89impl From<geo_types::GeometryCollection<f64>> for GeometryCollection {
90    fn from(gc: geo_types::GeometryCollection<f64>) -> Self {
91        GeometryCollection::new(gc.0.into_iter().map(Geometry::from).collect())
92    }
93}
94
95impl From<geo_types::Geometry<f64>> for Geometry {
96    fn from(g: geo_types::Geometry<f64>) -> Self {
97        use geo_types::Geometry as G;
98        match g {
99            G::Point(p) => Geometry::Point(p.into()),
100            G::MultiPoint(mp) => Geometry::MultiPoint(mp.into()),
101            G::LineString(ls) => Geometry::LineString(ls.into()),
102            G::MultiLineString(mls) => Geometry::MultiLineString(mls.into()),
103            G::Polygon(p) => Geometry::Polygon(p.into()),
104            G::MultiPolygon(mp) => Geometry::MultiPolygon(mp.into()),
105            G::GeometryCollection(gc) => Geometry::GeometryCollection(gc.into()),
106            // GeoJSON has no Line / Rect / Triangle; degrade as upstream does.
107            G::Line(l) => {
108                Geometry::LineString(LineString::new(vec![position(l.start), position(l.end)]))
109            }
110            G::Rect(r) => Geometry::Polygon(r.to_polygon().into()),
111            G::Triangle(t) => Geometry::Polygon(t.to_polygon().into()),
112        }
113    }
114}
115
116// --- ours -> geo_types (fallible on coordinate arity) -------------------------
117
118fn arity_error(len: usize) -> serde_json::Error {
119    <serde_json::Error as de::Error>::custom(format!(
120        "geo_types needs at least 2 coordinates (x, y) per position, found {len}"
121    ))
122}
123
124/// A GeoJSON position as a `geo_types` coordinate, taking the first two numbers
125/// (`x`, `y`) and ignoring any third (elevation), like upstream `geojson`.
126fn coord(pos: &Position) -> Result<geo_types::Coord<f64>, serde_json::Error> {
127    match pos.as_slice() {
128        [x, y, ..] => Ok(geo_types::Coord { x: *x, y: *y }),
129        _ => Err(arity_error(pos.len())),
130    }
131}
132
133fn geo_line_string(ring: &[Position]) -> Result<geo_types::LineString<f64>, serde_json::Error> {
134    ring.iter()
135        .map(coord)
136        .collect::<Result<Vec<_>, _>>()
137        .map(geo_types::LineString)
138}
139
140fn geo_polygon(rings: &[Vec<Position>]) -> Result<geo_types::Polygon<f64>, serde_json::Error> {
141    let exterior = match rings.first() {
142        Some(ring) => geo_line_string(ring)?,
143        None => geo_types::LineString(vec![]),
144    };
145    let interiors = rings
146        .iter()
147        .skip(1)
148        .map(|ring| geo_line_string(ring))
149        .collect::<Result<Vec<_>, _>>()?;
150    Ok(geo_types::Polygon::new(exterior, interiors))
151}
152
153impl TryFrom<Point> for geo_types::Point<f64> {
154    type Error = serde_json::Error;
155    fn try_from(p: Point) -> Result<Self, Self::Error> {
156        Ok(geo_types::Point(coord(&p.coordinates)?))
157    }
158}
159
160impl TryFrom<MultiPoint> for geo_types::MultiPoint<f64> {
161    type Error = serde_json::Error;
162    fn try_from(mp: MultiPoint) -> Result<Self, Self::Error> {
163        mp.coordinates
164            .iter()
165            .map(|c| coord(c).map(geo_types::Point))
166            .collect::<Result<Vec<_>, _>>()
167            .map(geo_types::MultiPoint)
168    }
169}
170
171impl TryFrom<LineString> for geo_types::LineString<f64> {
172    type Error = serde_json::Error;
173    fn try_from(ls: LineString) -> Result<Self, Self::Error> {
174        geo_line_string(&ls.coordinates)
175    }
176}
177
178impl TryFrom<MultiLineString> for geo_types::MultiLineString<f64> {
179    type Error = serde_json::Error;
180    fn try_from(mls: MultiLineString) -> Result<Self, Self::Error> {
181        mls.coordinates
182            .iter()
183            .map(|ring| geo_line_string(ring))
184            .collect::<Result<Vec<_>, _>>()
185            .map(geo_types::MultiLineString)
186    }
187}
188
189impl TryFrom<Polygon> for geo_types::Polygon<f64> {
190    type Error = serde_json::Error;
191    fn try_from(p: Polygon) -> Result<Self, Self::Error> {
192        geo_polygon(&p.coordinates)
193    }
194}
195
196impl TryFrom<MultiPolygon> for geo_types::MultiPolygon<f64> {
197    type Error = serde_json::Error;
198    fn try_from(mp: MultiPolygon) -> Result<Self, Self::Error> {
199        mp.coordinates
200            .iter()
201            .map(|poly| geo_polygon(poly))
202            .collect::<Result<Vec<_>, _>>()
203            .map(geo_types::MultiPolygon)
204    }
205}
206
207impl TryFrom<GeometryCollection> for geo_types::GeometryCollection<f64> {
208    type Error = serde_json::Error;
209    fn try_from(gc: GeometryCollection) -> Result<Self, Self::Error> {
210        gc.geometries
211            .into_iter()
212            .map(geo_types::Geometry::try_from)
213            .collect::<Result<Vec<_>, _>>()
214            .map(geo_types::GeometryCollection)
215    }
216}
217
218impl TryFrom<Geometry> for geo_types::Geometry<f64> {
219    type Error = serde_json::Error;
220    fn try_from(g: Geometry) -> Result<Self, Self::Error> {
221        Ok(match g {
222            Geometry::Point(p) => geo_types::Geometry::Point(p.try_into()?),
223            Geometry::MultiPoint(mp) => geo_types::Geometry::MultiPoint(mp.try_into()?),
224            Geometry::LineString(ls) => geo_types::Geometry::LineString(ls.try_into()?),
225            Geometry::MultiLineString(mls) => geo_types::Geometry::MultiLineString(mls.try_into()?),
226            Geometry::Polygon(p) => geo_types::Geometry::Polygon(p.try_into()?),
227            Geometry::MultiPolygon(mp) => geo_types::Geometry::MultiPolygon(mp.try_into()?),
228            Geometry::GeometryCollection(gc) => {
229                geo_types::Geometry::GeometryCollection(gc.try_into()?)
230            }
231        })
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[test]
240    fn point_round_trips_through_geo_types() {
241        let ours = Point::new(vec![-96.8, 32.8]);
242        let geo: geo_types::Point<f64> = ours.clone().try_into().unwrap();
243        assert_eq!(geo, geo_types::Point::new(-96.8, 32.8));
244        // geo -> ours reproduces the 2D position.
245        assert_eq!(Point::from(geo), ours);
246    }
247
248    #[test]
249    fn elevation_is_dropped_like_upstream() {
250        // A 3D position converts (x, y kept; z dropped), matching `geojson`.
251        let p3d = Point::new(vec![1.0, 2.0, 3.0]);
252        let geo: geo_types::Point<f64> = p3d.try_into().unwrap();
253        assert_eq!(geo, geo_types::Point::new(1.0, 2.0));
254    }
255
256    #[test]
257    fn short_position_is_an_error() {
258        let bad = Point::new(vec![1.0]);
259        let err = geo_types::Point::<f64>::try_from(bad).unwrap_err();
260        assert!(err.to_string().contains("at least 2"));
261    }
262
263    #[test]
264    fn polygon_rings_survive_both_directions() {
265        // A square with a triangular hole: exterior ring + one interior ring.
266        let ours = Polygon::new(vec![
267            vec![
268                vec![0.0, 0.0],
269                vec![4.0, 0.0],
270                vec![4.0, 4.0],
271                vec![0.0, 4.0],
272                vec![0.0, 0.0],
273            ],
274            vec![
275                vec![1.0, 1.0],
276                vec![2.0, 1.0],
277                vec![1.0, 2.0],
278                vec![1.0, 1.0],
279            ],
280        ]);
281        let geo: geo_types::Polygon<f64> = ours.clone().try_into().unwrap();
282        assert_eq!(geo.interiors().len(), 1);
283        assert_eq!(Polygon::from(geo), ours);
284    }
285
286    #[test]
287    fn geo_line_and_triangle_degrade_to_geojson_shapes() {
288        let line = geo_types::Line::new(
289            geo_types::Coord { x: 0.0, y: 0.0 },
290            geo_types::Coord { x: 1.0, y: 1.0 },
291        );
292        assert!(matches!(
293            Geometry::from(geo_types::Geometry::Line(line)),
294            Geometry::LineString(_)
295        ));
296
297        let tri = geo_types::Triangle::new(
298            geo_types::Coord { x: 0.0, y: 0.0 },
299            geo_types::Coord { x: 2.0, y: 0.0 },
300            geo_types::Coord { x: 1.0, y: 1.0 },
301        );
302        assert!(matches!(
303            Geometry::from(geo_types::Geometry::Triangle(tri)),
304            Geometry::Polygon(_)
305        ));
306    }
307
308    #[test]
309    fn geometry_union_round_trips() {
310        let ours = Geometry::MultiPoint(MultiPoint::new(vec![vec![0.0, 0.0], vec![1.0, 1.0]]));
311        let geo: geo_types::Geometry<f64> = ours.clone().try_into().unwrap();
312        assert_eq!(Geometry::from(geo), ours);
313    }
314}