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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
102pub enum GeometryType {
103    Point,
104    LineString,
105    Polygon,
106    MultiPoint,
107    MultiLineString,
108    MultiPolygon,
109    GeometryCollection,
110}
111
112#[derive(Debug, Clone, PartialEq)]
113pub enum Geometry {
114    Point(Coord),
115    LineString(LineString),
116    Polygon(Polygon),
117    MultiPoint(Vec<Coord>),
118    MultiLineString(Vec<LineString>),
119    MultiPolygon(Vec<Polygon>),
120    GeometryCollection(Vec<Geometry>),
121    /// A typed-empty geometry (e.g. `POINT EMPTY`, `POLYGON EMPTY`). The tag
122    /// is preserved so WKB serialization round-trips correctly.
123    Empty(GeometryType),
124}
125
126impl Geometry {
127    pub fn type_of(&self) -> GeometryType {
128        match self {
129            Geometry::Point(_) => GeometryType::Point,
130            Geometry::LineString(_) => GeometryType::LineString,
131            Geometry::Polygon(_) => GeometryType::Polygon,
132            Geometry::MultiPoint(_) => GeometryType::MultiPoint,
133            Geometry::MultiLineString(_) => GeometryType::MultiLineString,
134            Geometry::MultiPolygon(_) => GeometryType::MultiPolygon,
135            Geometry::GeometryCollection(_) => GeometryType::GeometryCollection,
136            Geometry::Empty(t) => *t,
137        }
138    }
139
140    pub fn is_empty(&self) -> bool {
141        match self {
142            Geometry::Empty(_) => true,
143            Geometry::Point(_) => false,
144            Geometry::LineString(ls) => ls.is_empty(),
145            Geometry::Polygon(p) => p.is_empty(),
146            Geometry::MultiPoint(v) => v.is_empty(),
147            Geometry::MultiLineString(v) => v.iter().all(LineString::is_empty),
148            Geometry::MultiPolygon(v) => v.iter().all(Polygon::is_empty),
149            Geometry::GeometryCollection(v) => v.iter().all(Geometry::is_empty),
150        }
151    }
152
153    /// True if any coordinate in the tree carries a Z ordinate.
154    pub fn has_z(&self) -> bool {
155        match self {
156            Geometry::Empty(_) => false,
157            Geometry::Point(c) => c.has_z(),
158            Geometry::LineString(ls) => ls.coords.iter().any(Coord::has_z),
159            Geometry::Polygon(p) => {
160                p.exterior.coords.iter().any(Coord::has_z)
161                    || p.holes.iter().any(|h| h.coords.iter().any(Coord::has_z))
162            }
163            Geometry::MultiPoint(v) => v.iter().any(Coord::has_z),
164            Geometry::MultiLineString(v) => v.iter().any(|ls| ls.coords.iter().any(Coord::has_z)),
165            Geometry::MultiPolygon(v) => v.iter().any(|p| {
166                p.exterior.coords.iter().any(Coord::has_z)
167                    || p.holes.iter().any(|h| h.coords.iter().any(Coord::has_z))
168            }),
169            Geometry::GeometryCollection(v) => v.iter().any(Geometry::has_z),
170        }
171    }
172
173    /// Compute the 2D bounding box `[xmin, ymin, xmax, ymax]`.
174    ///
175    /// Returns `None` for empty geometries (no coordinates to bound). NaN
176    /// coordinates are skipped, so `POINT EMPTY` (stored as NaN/NaN) also
177    /// yields `None`.
178    pub fn bbox(&self) -> Option<[f64; 4]> {
179        let mut acc: Option<[f64; 4]> = None;
180        for_each_xy(self, &mut |x, y| {
181            if !x.is_finite() || !y.is_finite() {
182                return;
183            }
184            match &mut acc {
185                None => acc = Some([x, y, x, y]),
186                Some(b) => {
187                    if x < b[0] {
188                        b[0] = x;
189                    }
190                    if y < b[1] {
191                        b[1] = y;
192                    }
193                    if x > b[2] {
194                        b[2] = x;
195                    }
196                    if y > b[3] {
197                        b[3] = y;
198                    }
199                }
200            }
201        });
202        acc
203    }
204
205    /// True if any coordinate in the tree carries an M ordinate.
206    pub fn has_m(&self) -> bool {
207        match self {
208            Geometry::Empty(_) => false,
209            Geometry::Point(c) => c.has_m(),
210            Geometry::LineString(ls) => ls.coords.iter().any(Coord::has_m),
211            Geometry::Polygon(p) => {
212                p.exterior.coords.iter().any(Coord::has_m)
213                    || p.holes.iter().any(|h| h.coords.iter().any(Coord::has_m))
214            }
215            Geometry::MultiPoint(v) => v.iter().any(Coord::has_m),
216            Geometry::MultiLineString(v) => v.iter().any(|ls| ls.coords.iter().any(Coord::has_m)),
217            Geometry::MultiPolygon(v) => v.iter().any(|p| {
218                p.exterior.coords.iter().any(Coord::has_m)
219                    || p.holes.iter().any(|h| h.coords.iter().any(Coord::has_m))
220            }),
221            Geometry::GeometryCollection(v) => v.iter().any(Geometry::has_m),
222        }
223    }
224}
225
226/// Walk every (x, y) coordinate in the tree and invoke `f`. Used by `bbox`
227/// and (later) by visitor-style writers.
228fn for_each_xy(g: &Geometry, f: &mut dyn FnMut(f64, f64)) {
229    match g {
230        Geometry::Empty(_) => {}
231        Geometry::Point(c) => f(c.x, c.y),
232        Geometry::LineString(ls) => {
233            for c in &ls.coords {
234                f(c.x, c.y);
235            }
236        }
237        Geometry::Polygon(p) => {
238            for c in &p.exterior.coords {
239                f(c.x, c.y);
240            }
241            for h in &p.holes {
242                for c in &h.coords {
243                    f(c.x, c.y);
244                }
245            }
246        }
247        Geometry::MultiPoint(v) => {
248            for c in v {
249                f(c.x, c.y);
250            }
251        }
252        Geometry::MultiLineString(v) => {
253            for ls in v {
254                for c in &ls.coords {
255                    f(c.x, c.y);
256                }
257            }
258        }
259        Geometry::MultiPolygon(v) => {
260            for p in v {
261                for c in &p.exterior.coords {
262                    f(c.x, c.y);
263                }
264                for h in &p.holes {
265                    for c in &h.coords {
266                        f(c.x, c.y);
267                    }
268                }
269            }
270        }
271        Geometry::GeometryCollection(v) => {
272            for inner in v {
273                for_each_xy(inner, f);
274            }
275        }
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    #[test]
284    fn coord_constructors_track_dimensions() {
285        assert!(!Coord::xy(1.0, 2.0).has_z());
286        assert!(!Coord::xy(1.0, 2.0).has_m());
287
288        let c = Coord::xyz(1.0, 2.0, 3.0);
289        assert!(c.has_z() && !c.has_m());
290        assert_eq!(c.z, Some(3.0));
291
292        let c = Coord::xym(1.0, 2.0, 99.0);
293        assert!(!c.has_z() && c.has_m());
294        assert_eq!(c.m, Some(99.0));
295
296        let c = Coord::xyzm(1.0, 2.0, 3.0, 99.0);
297        assert!(c.has_z() && c.has_m());
298    }
299
300    #[test]
301    fn geometry_type_of_matches_variant() {
302        assert_eq!(
303            Geometry::Point(Coord::xy(0.0, 0.0)).type_of(),
304            GeometryType::Point
305        );
306        assert_eq!(
307            Geometry::Polygon(Polygon::default()).type_of(),
308            GeometryType::Polygon
309        );
310        assert_eq!(
311            Geometry::Empty(GeometryType::MultiPolygon).type_of(),
312            GeometryType::MultiPolygon
313        );
314    }
315
316    #[test]
317    fn empty_detection() {
318        assert!(Geometry::Empty(GeometryType::Point).is_empty());
319        assert!(Geometry::LineString(LineString::default()).is_empty());
320        assert!(!Geometry::Point(Coord::xy(0.0, 0.0)).is_empty());
321
322        let ls = LineString::new(vec![Coord::xy(0.0, 0.0), Coord::xy(1.0, 1.0)]);
323        assert!(!Geometry::LineString(ls).is_empty());
324    }
325
326    #[test]
327    fn bbox_of_polygon_with_hole() {
328        let p = Polygon::new(
329            LineString::new(vec![
330                Coord::xy(0.0, 0.0),
331                Coord::xy(10.0, 0.0),
332                Coord::xy(10.0, 10.0),
333                Coord::xy(0.0, 10.0),
334                Coord::xy(0.0, 0.0),
335            ]),
336            vec![LineString::new(vec![
337                Coord::xy(2.0, 2.0),
338                Coord::xy(4.0, 2.0),
339                Coord::xy(4.0, 4.0),
340                Coord::xy(2.0, 4.0),
341                Coord::xy(2.0, 2.0),
342            ])],
343        );
344        let bbox = Geometry::Polygon(p).bbox().unwrap();
345        assert_eq!(bbox, [0.0, 0.0, 10.0, 10.0]);
346    }
347
348    #[test]
349    fn bbox_of_empty_geometry_is_none() {
350        assert!(Geometry::Empty(GeometryType::Polygon).bbox().is_none());
351        assert!(Geometry::LineString(LineString::default()).bbox().is_none());
352    }
353
354    #[test]
355    fn bbox_skips_nan() {
356        let g = Geometry::MultiPoint(vec![
357            Coord::xy(1.0, 2.0),
358            Coord::xy(f64::NAN, f64::NAN),
359            Coord::xy(5.0, 6.0),
360        ]);
361        assert_eq!(g.bbox(), Some([1.0, 2.0, 5.0, 6.0]));
362    }
363
364    #[test]
365    fn bbox_of_geometry_collection() {
366        let g = Geometry::GeometryCollection(vec![
367            Geometry::Point(Coord::xy(-1.0, -1.0)),
368            Geometry::Point(Coord::xy(5.0, 5.0)),
369        ]);
370        assert_eq!(g.bbox(), Some([-1.0, -1.0, 5.0, 5.0]));
371    }
372
373    #[test]
374    fn has_z_m_recursion() {
375        let ls = LineString::new(vec![Coord::xy(0.0, 0.0), Coord::xyz(1.0, 1.0, 5.0)]);
376        let g = Geometry::LineString(ls);
377        assert!(g.has_z());
378        assert!(!g.has_m());
379
380        let mls = Geometry::MultiLineString(vec![LineString::new(vec![Coord::xym(0.0, 0.0, 7.0)])]);
381        assert!(!mls.has_z());
382        assert!(mls.has_m());
383
384        let collection =
385            Geometry::GeometryCollection(vec![Geometry::Point(Coord::xyzm(0.0, 0.0, 1.0, 2.0))]);
386        assert!(collection.has_z());
387        assert!(collection.has_m());
388    }
389}