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    /// Total coordinate count across the geometry tree. O(N) walk.
163    ///
164    /// This is the right number to use when budgeting memory for an
165    /// operation that scales with vertex density (Hilbert sort, geometry
166    /// simplification, WKB encode). A single polygon with 100 000
167    /// vertices charges 100 000; a `MultiPolygon` of 50 such parts
168    /// charges 5 000 000 — feature *count* alone hides this asymmetry.
169    pub fn coord_count(&self) -> usize {
170        match self {
171            Geometry::Empty(_) => 0,
172            Geometry::Point(_) => 1,
173            Geometry::LineString(ls) => ls.coords.len(),
174            Geometry::Polygon(p) => {
175                p.exterior.coords.len()
176                    + p.holes.iter().map(|h| h.coords.len()).sum::<usize>()
177            }
178            Geometry::MultiPoint(v) => v.len(),
179            Geometry::MultiLineString(v) => v.iter().map(|ls| ls.coords.len()).sum(),
180            Geometry::MultiPolygon(v) => v
181                .iter()
182                .map(|p| {
183                    p.exterior.coords.len()
184                        + p.holes.iter().map(|h| h.coords.len()).sum::<usize>()
185                })
186                .sum(),
187            Geometry::GeometryCollection(v) => v.iter().map(Geometry::coord_count).sum(),
188        }
189    }
190
191    /// True if any coordinate in the tree carries a Z ordinate.
192    pub fn has_z(&self) -> bool {
193        match self {
194            Geometry::Empty(_) => false,
195            Geometry::Point(c) => c.has_z(),
196            Geometry::LineString(ls) => ls.coords.iter().any(Coord::has_z),
197            Geometry::Polygon(p) => {
198                p.exterior.coords.iter().any(Coord::has_z)
199                    || p.holes.iter().any(|h| h.coords.iter().any(Coord::has_z))
200            }
201            Geometry::MultiPoint(v) => v.iter().any(Coord::has_z),
202            Geometry::MultiLineString(v) => v.iter().any(|ls| ls.coords.iter().any(Coord::has_z)),
203            Geometry::MultiPolygon(v) => v.iter().any(|p| {
204                p.exterior.coords.iter().any(Coord::has_z)
205                    || p.holes.iter().any(|h| h.coords.iter().any(Coord::has_z))
206            }),
207            Geometry::GeometryCollection(v) => v.iter().any(Geometry::has_z),
208        }
209    }
210
211    /// Compute the 2D bounding box `[xmin, ymin, xmax, ymax]`.
212    ///
213    /// Returns `None` for empty geometries (no coordinates to bound). NaN
214    /// coordinates are skipped, so `POINT EMPTY` (stored as NaN/NaN) also
215    /// yields `None`.
216    pub fn bbox(&self) -> Option<[f64; 4]> {
217        let mut acc: Option<[f64; 4]> = None;
218        for_each_xy(self, &mut |x, y| {
219            if !x.is_finite() || !y.is_finite() {
220                return;
221            }
222            match &mut acc {
223                None => acc = Some([x, y, x, y]),
224                Some(b) => {
225                    if x < b[0] {
226                        b[0] = x;
227                    }
228                    if y < b[1] {
229                        b[1] = y;
230                    }
231                    if x > b[2] {
232                        b[2] = x;
233                    }
234                    if y > b[3] {
235                        b[3] = y;
236                    }
237                }
238            }
239        });
240        acc
241    }
242
243    /// True if any coordinate in the tree carries an M ordinate.
244    pub fn has_m(&self) -> bool {
245        match self {
246            Geometry::Empty(_) => false,
247            Geometry::Point(c) => c.has_m(),
248            Geometry::LineString(ls) => ls.coords.iter().any(Coord::has_m),
249            Geometry::Polygon(p) => {
250                p.exterior.coords.iter().any(Coord::has_m)
251                    || p.holes.iter().any(|h| h.coords.iter().any(Coord::has_m))
252            }
253            Geometry::MultiPoint(v) => v.iter().any(Coord::has_m),
254            Geometry::MultiLineString(v) => v.iter().any(|ls| ls.coords.iter().any(Coord::has_m)),
255            Geometry::MultiPolygon(v) => v.iter().any(|p| {
256                p.exterior.coords.iter().any(Coord::has_m)
257                    || p.holes.iter().any(|h| h.coords.iter().any(Coord::has_m))
258            }),
259            Geometry::GeometryCollection(v) => v.iter().any(Geometry::has_m),
260        }
261    }
262}
263
264/// Walk every (x, y) coordinate in the tree and invoke `f`. Used by `bbox`
265/// and (later) by visitor-style writers.
266fn for_each_xy(g: &Geometry, f: &mut dyn FnMut(f64, f64)) {
267    match g {
268        Geometry::Empty(_) => {}
269        Geometry::Point(c) => f(c.x, c.y),
270        Geometry::LineString(ls) => {
271            for c in &ls.coords {
272                f(c.x, c.y);
273            }
274        }
275        Geometry::Polygon(p) => {
276            for c in &p.exterior.coords {
277                f(c.x, c.y);
278            }
279            for h in &p.holes {
280                for c in &h.coords {
281                    f(c.x, c.y);
282                }
283            }
284        }
285        Geometry::MultiPoint(v) => {
286            for c in v {
287                f(c.x, c.y);
288            }
289        }
290        Geometry::MultiLineString(v) => {
291            for ls in v {
292                for c in &ls.coords {
293                    f(c.x, c.y);
294                }
295            }
296        }
297        Geometry::MultiPolygon(v) => {
298            for p in v {
299                for c in &p.exterior.coords {
300                    f(c.x, c.y);
301                }
302                for h in &p.holes {
303                    for c in &h.coords {
304                        f(c.x, c.y);
305                    }
306                }
307            }
308        }
309        Geometry::GeometryCollection(v) => {
310            for inner in v {
311                for_each_xy(inner, f);
312            }
313        }
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    #[test]
322    fn coord_constructors_track_dimensions() {
323        assert!(!Coord::xy(1.0, 2.0).has_z());
324        assert!(!Coord::xy(1.0, 2.0).has_m());
325
326        let c = Coord::xyz(1.0, 2.0, 3.0);
327        assert!(c.has_z() && !c.has_m());
328        assert_eq!(c.z, Some(3.0));
329
330        let c = Coord::xym(1.0, 2.0, 99.0);
331        assert!(!c.has_z() && c.has_m());
332        assert_eq!(c.m, Some(99.0));
333
334        let c = Coord::xyzm(1.0, 2.0, 3.0, 99.0);
335        assert!(c.has_z() && c.has_m());
336    }
337
338    #[test]
339    fn geometry_type_of_matches_variant() {
340        assert_eq!(
341            Geometry::Point(Coord::xy(0.0, 0.0)).type_of(),
342            GeometryType::Point
343        );
344        assert_eq!(
345            Geometry::Polygon(Polygon::default()).type_of(),
346            GeometryType::Polygon
347        );
348        assert_eq!(
349            Geometry::Empty(GeometryType::MultiPolygon).type_of(),
350            GeometryType::MultiPolygon
351        );
352    }
353
354    #[test]
355    fn empty_detection() {
356        assert!(Geometry::Empty(GeometryType::Point).is_empty());
357        assert!(Geometry::LineString(LineString::default()).is_empty());
358        assert!(!Geometry::Point(Coord::xy(0.0, 0.0)).is_empty());
359
360        let ls = LineString::new(vec![Coord::xy(0.0, 0.0), Coord::xy(1.0, 1.0)]);
361        assert!(!Geometry::LineString(ls).is_empty());
362    }
363
364    #[test]
365    fn bbox_of_polygon_with_hole() {
366        let p = Polygon::new(
367            LineString::new(vec![
368                Coord::xy(0.0, 0.0),
369                Coord::xy(10.0, 0.0),
370                Coord::xy(10.0, 10.0),
371                Coord::xy(0.0, 10.0),
372                Coord::xy(0.0, 0.0),
373            ]),
374            vec![LineString::new(vec![
375                Coord::xy(2.0, 2.0),
376                Coord::xy(4.0, 2.0),
377                Coord::xy(4.0, 4.0),
378                Coord::xy(2.0, 4.0),
379                Coord::xy(2.0, 2.0),
380            ])],
381        );
382        let bbox = Geometry::Polygon(p).bbox().unwrap();
383        assert_eq!(bbox, [0.0, 0.0, 10.0, 10.0]);
384    }
385
386    #[test]
387    fn bbox_of_empty_geometry_is_none() {
388        assert!(Geometry::Empty(GeometryType::Polygon).bbox().is_none());
389        assert!(Geometry::LineString(LineString::default()).bbox().is_none());
390    }
391
392    #[test]
393    fn bbox_skips_nan() {
394        let g = Geometry::MultiPoint(vec![
395            Coord::xy(1.0, 2.0),
396            Coord::xy(f64::NAN, f64::NAN),
397            Coord::xy(5.0, 6.0),
398        ]);
399        assert_eq!(g.bbox(), Some([1.0, 2.0, 5.0, 6.0]));
400    }
401
402    #[test]
403    fn bbox_of_geometry_collection() {
404        let g = Geometry::GeometryCollection(vec![
405            Geometry::Point(Coord::xy(-1.0, -1.0)),
406            Geometry::Point(Coord::xy(5.0, 5.0)),
407        ]);
408        assert_eq!(g.bbox(), Some([-1.0, -1.0, 5.0, 5.0]));
409    }
410
411    #[test]
412    fn has_z_m_recursion() {
413        let ls = LineString::new(vec![Coord::xy(0.0, 0.0), Coord::xyz(1.0, 1.0, 5.0)]);
414        let g = Geometry::LineString(ls);
415        assert!(g.has_z());
416        assert!(!g.has_m());
417
418        let mls = Geometry::MultiLineString(vec![LineString::new(vec![Coord::xym(0.0, 0.0, 7.0)])]);
419        assert!(!mls.has_z());
420        assert!(mls.has_m());
421
422        let collection =
423            Geometry::GeometryCollection(vec![Geometry::Point(Coord::xyzm(0.0, 0.0, 1.0, 2.0))]);
424        assert!(collection.has_z());
425        assert!(collection.has_m());
426    }
427}