Skip to main content

geonative_core/
geometry.rs

1//! Simple-Features geometry tree with optional Z/M ordinates.
2//!
3//! The shape is deliberately close to OGC Simple Features + WKB so writers
4//! (WKB, GeoJSON, Shapefile, GeoParquet) reduce to direct tree walks.
5
6/// A single coordinate. `z` and `m` are `Option`s so we can distinguish
7/// "this geometry has no Z dimension" from "Z is invalid". We do **not** use
8/// NaN as a sentinel — that's a GDB-specific quirk handled inside its reader.
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct Coord {
11    pub x: f64,
12    pub y: f64,
13    pub z: Option<f64>,
14    pub m: Option<f64>,
15}
16
17impl Coord {
18    pub const fn xy(x: f64, y: f64) -> Self {
19        Self {
20            x,
21            y,
22            z: None,
23            m: None,
24        }
25    }
26
27    pub const fn xyz(x: f64, y: f64, z: f64) -> Self {
28        Self {
29            x,
30            y,
31            z: Some(z),
32            m: None,
33        }
34    }
35
36    pub const fn xym(x: f64, y: f64, m: f64) -> Self {
37        Self {
38            x,
39            y,
40            z: None,
41            m: Some(m),
42        }
43    }
44
45    pub const fn xyzm(x: f64, y: f64, z: f64, m: f64) -> Self {
46        Self {
47            x,
48            y,
49            z: Some(z),
50            m: Some(m),
51        }
52    }
53
54    pub const fn has_z(&self) -> bool {
55        self.z.is_some()
56    }
57
58    pub const fn has_m(&self) -> bool {
59        self.m.is_some()
60    }
61}
62
63#[derive(Debug, Clone, PartialEq, Default)]
64pub struct LineString {
65    pub coords: Vec<Coord>,
66}
67
68impl LineString {
69    pub fn new(coords: Vec<Coord>) -> Self {
70        Self { coords }
71    }
72
73    pub fn is_empty(&self) -> bool {
74        self.coords.is_empty()
75    }
76}
77
78/// A polygon: one exterior ring plus zero or more interior rings (holes).
79///
80/// Ring orientation in this IR is **OGC-standard**: exterior counter-clockwise,
81/// interior clockwise. Drivers that read formats with a different convention
82/// (Esri Shapefile/GDB: exterior CW, hole CCW) re-orient at read time.
83#[derive(Debug, Clone, PartialEq, Default)]
84pub struct Polygon {
85    pub exterior: LineString,
86    pub holes: Vec<LineString>,
87}
88
89impl Polygon {
90    pub fn new(exterior: LineString, holes: Vec<LineString>) -> Self {
91        Self { exterior, holes }
92    }
93
94    pub fn is_empty(&self) -> bool {
95        self.exterior.is_empty()
96    }
97}
98
99/// Discriminant of [`Geometry`]. Used in `Geometry::Empty(_)` to preserve
100/// the type of a typed-empty geometry (WKB requires this).
101///
102/// Marked `#[non_exhaustive]` because future versions may add `Triangle`,
103/// `TIN`, `PolyhedralSurface`, or richer multi-* variants without that
104/// counting as a SemVer-breaking change.
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
106#[non_exhaustive]
107pub enum GeometryType {
108    Point,
109    LineString,
110    Polygon,
111    MultiPoint,
112    MultiLineString,
113    MultiPolygon,
114    GeometryCollection,
115}
116
117/// The geometry tree. Marked `#[non_exhaustive]` so future SemVer-minor
118/// releases can add variants (curves, surfaces, Z/M-bearing variants once
119/// the IR grows past 2D).
120#[derive(Debug, Clone, PartialEq)]
121#[non_exhaustive]
122pub enum Geometry {
123    Point(Coord),
124    LineString(LineString),
125    Polygon(Polygon),
126    MultiPoint(Vec<Coord>),
127    MultiLineString(Vec<LineString>),
128    MultiPolygon(Vec<Polygon>),
129    GeometryCollection(Vec<Geometry>),
130    /// A typed-empty geometry (e.g. `POINT EMPTY`, `POLYGON EMPTY`). The tag
131    /// is preserved so WKB serialization round-trips correctly.
132    Empty(GeometryType),
133}
134
135impl Geometry {
136    pub fn type_of(&self) -> GeometryType {
137        match self {
138            Geometry::Point(_) => GeometryType::Point,
139            Geometry::LineString(_) => GeometryType::LineString,
140            Geometry::Polygon(_) => GeometryType::Polygon,
141            Geometry::MultiPoint(_) => GeometryType::MultiPoint,
142            Geometry::MultiLineString(_) => GeometryType::MultiLineString,
143            Geometry::MultiPolygon(_) => GeometryType::MultiPolygon,
144            Geometry::GeometryCollection(_) => GeometryType::GeometryCollection,
145            Geometry::Empty(t) => *t,
146        }
147    }
148
149    pub fn is_empty(&self) -> bool {
150        match self {
151            Geometry::Empty(_) => true,
152            Geometry::Point(_) => false,
153            Geometry::LineString(ls) => ls.is_empty(),
154            Geometry::Polygon(p) => p.is_empty(),
155            Geometry::MultiPoint(v) => v.is_empty(),
156            Geometry::MultiLineString(v) => v.iter().all(LineString::is_empty),
157            Geometry::MultiPolygon(v) => v.iter().all(Polygon::is_empty),
158            Geometry::GeometryCollection(v) => v.iter().all(Geometry::is_empty),
159        }
160    }
161
162    /// True if any coordinate in the tree carries a Z ordinate.
163    pub fn has_z(&self) -> bool {
164        match self {
165            Geometry::Empty(_) => false,
166            Geometry::Point(c) => c.has_z(),
167            Geometry::LineString(ls) => ls.coords.iter().any(Coord::has_z),
168            Geometry::Polygon(p) => {
169                p.exterior.coords.iter().any(Coord::has_z)
170                    || p.holes.iter().any(|h| h.coords.iter().any(Coord::has_z))
171            }
172            Geometry::MultiPoint(v) => v.iter().any(Coord::has_z),
173            Geometry::MultiLineString(v) => v.iter().any(|ls| ls.coords.iter().any(Coord::has_z)),
174            Geometry::MultiPolygon(v) => v.iter().any(|p| {
175                p.exterior.coords.iter().any(Coord::has_z)
176                    || p.holes.iter().any(|h| h.coords.iter().any(Coord::has_z))
177            }),
178            Geometry::GeometryCollection(v) => v.iter().any(Geometry::has_z),
179        }
180    }
181
182    /// Compute the 2D bounding box `[xmin, ymin, xmax, ymax]`.
183    ///
184    /// Returns `None` for empty geometries (no coordinates to bound). NaN
185    /// coordinates are skipped, so `POINT EMPTY` (stored as NaN/NaN) also
186    /// yields `None`.
187    pub fn bbox(&self) -> Option<[f64; 4]> {
188        let mut acc: Option<[f64; 4]> = None;
189        for_each_xy(self, &mut |x, y| {
190            if !x.is_finite() || !y.is_finite() {
191                return;
192            }
193            match &mut acc {
194                None => acc = Some([x, y, x, y]),
195                Some(b) => {
196                    if x < b[0] {
197                        b[0] = x;
198                    }
199                    if y < b[1] {
200                        b[1] = y;
201                    }
202                    if x > b[2] {
203                        b[2] = x;
204                    }
205                    if y > b[3] {
206                        b[3] = y;
207                    }
208                }
209            }
210        });
211        acc
212    }
213
214    /// True if any coordinate in the tree carries an M ordinate.
215    pub fn has_m(&self) -> bool {
216        match self {
217            Geometry::Empty(_) => false,
218            Geometry::Point(c) => c.has_m(),
219            Geometry::LineString(ls) => ls.coords.iter().any(Coord::has_m),
220            Geometry::Polygon(p) => {
221                p.exterior.coords.iter().any(Coord::has_m)
222                    || p.holes.iter().any(|h| h.coords.iter().any(Coord::has_m))
223            }
224            Geometry::MultiPoint(v) => v.iter().any(Coord::has_m),
225            Geometry::MultiLineString(v) => v.iter().any(|ls| ls.coords.iter().any(Coord::has_m)),
226            Geometry::MultiPolygon(v) => v.iter().any(|p| {
227                p.exterior.coords.iter().any(Coord::has_m)
228                    || p.holes.iter().any(|h| h.coords.iter().any(Coord::has_m))
229            }),
230            Geometry::GeometryCollection(v) => v.iter().any(Geometry::has_m),
231        }
232    }
233}
234
235/// Walk every (x, y) coordinate in the tree and invoke `f`. Used by `bbox`
236/// and (later) by visitor-style writers.
237fn for_each_xy(g: &Geometry, f: &mut dyn FnMut(f64, f64)) {
238    match g {
239        Geometry::Empty(_) => {}
240        Geometry::Point(c) => f(c.x, c.y),
241        Geometry::LineString(ls) => {
242            for c in &ls.coords {
243                f(c.x, c.y);
244            }
245        }
246        Geometry::Polygon(p) => {
247            for c in &p.exterior.coords {
248                f(c.x, c.y);
249            }
250            for h in &p.holes {
251                for c in &h.coords {
252                    f(c.x, c.y);
253                }
254            }
255        }
256        Geometry::MultiPoint(v) => {
257            for c in v {
258                f(c.x, c.y);
259            }
260        }
261        Geometry::MultiLineString(v) => {
262            for ls in v {
263                for c in &ls.coords {
264                    f(c.x, c.y);
265                }
266            }
267        }
268        Geometry::MultiPolygon(v) => {
269            for p in v {
270                for c in &p.exterior.coords {
271                    f(c.x, c.y);
272                }
273                for h in &p.holes {
274                    for c in &h.coords {
275                        f(c.x, c.y);
276                    }
277                }
278            }
279        }
280        Geometry::GeometryCollection(v) => {
281            for inner in v {
282                for_each_xy(inner, f);
283            }
284        }
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    #[test]
293    fn coord_constructors_track_dimensions() {
294        assert!(!Coord::xy(1.0, 2.0).has_z());
295        assert!(!Coord::xy(1.0, 2.0).has_m());
296
297        let c = Coord::xyz(1.0, 2.0, 3.0);
298        assert!(c.has_z() && !c.has_m());
299        assert_eq!(c.z, Some(3.0));
300
301        let c = Coord::xym(1.0, 2.0, 99.0);
302        assert!(!c.has_z() && c.has_m());
303        assert_eq!(c.m, Some(99.0));
304
305        let c = Coord::xyzm(1.0, 2.0, 3.0, 99.0);
306        assert!(c.has_z() && c.has_m());
307    }
308
309    #[test]
310    fn geometry_type_of_matches_variant() {
311        assert_eq!(
312            Geometry::Point(Coord::xy(0.0, 0.0)).type_of(),
313            GeometryType::Point
314        );
315        assert_eq!(
316            Geometry::Polygon(Polygon::default()).type_of(),
317            GeometryType::Polygon
318        );
319        assert_eq!(
320            Geometry::Empty(GeometryType::MultiPolygon).type_of(),
321            GeometryType::MultiPolygon
322        );
323    }
324
325    #[test]
326    fn empty_detection() {
327        assert!(Geometry::Empty(GeometryType::Point).is_empty());
328        assert!(Geometry::LineString(LineString::default()).is_empty());
329        assert!(!Geometry::Point(Coord::xy(0.0, 0.0)).is_empty());
330
331        let ls = LineString::new(vec![Coord::xy(0.0, 0.0), Coord::xy(1.0, 1.0)]);
332        assert!(!Geometry::LineString(ls).is_empty());
333    }
334
335    #[test]
336    fn bbox_of_polygon_with_hole() {
337        let p = Polygon::new(
338            LineString::new(vec![
339                Coord::xy(0.0, 0.0),
340                Coord::xy(10.0, 0.0),
341                Coord::xy(10.0, 10.0),
342                Coord::xy(0.0, 10.0),
343                Coord::xy(0.0, 0.0),
344            ]),
345            vec![LineString::new(vec![
346                Coord::xy(2.0, 2.0),
347                Coord::xy(4.0, 2.0),
348                Coord::xy(4.0, 4.0),
349                Coord::xy(2.0, 4.0),
350                Coord::xy(2.0, 2.0),
351            ])],
352        );
353        let bbox = Geometry::Polygon(p).bbox().unwrap();
354        assert_eq!(bbox, [0.0, 0.0, 10.0, 10.0]);
355    }
356
357    #[test]
358    fn bbox_of_empty_geometry_is_none() {
359        assert!(Geometry::Empty(GeometryType::Polygon).bbox().is_none());
360        assert!(Geometry::LineString(LineString::default()).bbox().is_none());
361    }
362
363    #[test]
364    fn bbox_skips_nan() {
365        let g = Geometry::MultiPoint(vec![
366            Coord::xy(1.0, 2.0),
367            Coord::xy(f64::NAN, f64::NAN),
368            Coord::xy(5.0, 6.0),
369        ]);
370        assert_eq!(g.bbox(), Some([1.0, 2.0, 5.0, 6.0]));
371    }
372
373    #[test]
374    fn bbox_of_geometry_collection() {
375        let g = Geometry::GeometryCollection(vec![
376            Geometry::Point(Coord::xy(-1.0, -1.0)),
377            Geometry::Point(Coord::xy(5.0, 5.0)),
378        ]);
379        assert_eq!(g.bbox(), Some([-1.0, -1.0, 5.0, 5.0]));
380    }
381
382    #[test]
383    fn has_z_m_recursion() {
384        let ls = LineString::new(vec![Coord::xy(0.0, 0.0), Coord::xyz(1.0, 1.0, 5.0)]);
385        let g = Geometry::LineString(ls);
386        assert!(g.has_z());
387        assert!(!g.has_m());
388
389        let mls = Geometry::MultiLineString(vec![LineString::new(vec![Coord::xym(0.0, 0.0, 7.0)])]);
390        assert!(!mls.has_z());
391        assert!(mls.has_m());
392
393        let collection =
394            Geometry::GeometryCollection(vec![Geometry::Point(Coord::xyzm(0.0, 0.0, 1.0, 2.0))]);
395        assert!(collection.has_z());
396        assert!(collection.has_m());
397    }
398}