Skip to main content

oxigdal_core/vector/
geometry.rs

1//! Geometry types for vector data
2//!
3//! This module provides geometry types compatible with Simple Features specification.
4
5use crate::error::{OxiGdalError, Result};
6use serde::{Deserialize, Serialize};
7
8#[cfg(feature = "std")]
9use std::vec::Vec;
10
11#[cfg(all(not(feature = "std"), feature = "alloc"))]
12use alloc::vec::Vec;
13
14/// Coordinate in 2D, 3D, or 4D space
15#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
16pub struct Coordinate {
17    /// X coordinate (longitude)
18    pub x: f64,
19    /// Y coordinate (latitude)
20    pub y: f64,
21    /// Z coordinate (elevation) - optional
22    pub z: Option<f64>,
23    /// M coordinate (measure) - optional
24    pub m: Option<f64>,
25}
26
27impl Coordinate {
28    /// Creates a new 2D coordinate
29    #[must_use]
30    pub const fn new_2d(x: f64, y: f64) -> Self {
31        Self {
32            x,
33            y,
34            z: None,
35            m: None,
36        }
37    }
38
39    /// Creates a new 3D coordinate
40    #[must_use]
41    pub const fn new_3d(x: f64, y: f64, z: f64) -> Self {
42        Self {
43            x,
44            y,
45            z: Some(z),
46            m: None,
47        }
48    }
49
50    /// Creates a new coordinate with measure
51    #[must_use]
52    pub const fn new_2dm(x: f64, y: f64, m: f64) -> Self {
53        Self {
54            x,
55            y,
56            z: None,
57            m: Some(m),
58        }
59    }
60
61    /// Creates a new 3D coordinate with measure
62    #[must_use]
63    pub const fn new_3dm(x: f64, y: f64, z: f64, m: f64) -> Self {
64        Self {
65            x,
66            y,
67            z: Some(z),
68            m: Some(m),
69        }
70    }
71
72    /// Returns true if this coordinate has Z dimension
73    #[must_use]
74    pub const fn has_z(&self) -> bool {
75        self.z.is_some()
76    }
77
78    /// Returns true if this coordinate has M dimension
79    #[must_use]
80    pub const fn has_m(&self) -> bool {
81        self.m.is_some()
82    }
83
84    /// Returns the number of dimensions (2, 3, or 4)
85    #[must_use]
86    pub const fn dimensions(&self) -> u8 {
87        let mut dims = 2;
88        if self.z.is_some() {
89            dims += 1;
90        }
91        if self.m.is_some() {
92            dims += 1;
93        }
94        dims
95    }
96
97    /// Returns the X coordinate
98    #[must_use]
99    pub const fn x(&self) -> f64 {
100        self.x
101    }
102
103    /// Returns the Y coordinate
104    #[must_use]
105    pub const fn y(&self) -> f64 {
106        self.y
107    }
108
109    /// Returns the Z coordinate if present
110    #[must_use]
111    pub const fn z(&self) -> Option<f64> {
112        self.z
113    }
114
115    /// Returns the M coordinate if present
116    #[must_use]
117    pub const fn m(&self) -> Option<f64> {
118        self.m
119    }
120}
121
122/// Geometry type enumeration
123#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
124pub enum Geometry {
125    /// Point geometry
126    Point(Point),
127    /// `LineString` geometry
128    LineString(LineString),
129    /// Polygon geometry
130    Polygon(Polygon),
131    /// `MultiPoint` geometry
132    MultiPoint(MultiPoint),
133    /// `MultiLineString` geometry
134    MultiLineString(MultiLineString),
135    /// `MultiPolygon` geometry
136    MultiPolygon(MultiPolygon),
137    /// `GeometryCollection`
138    GeometryCollection(GeometryCollection),
139}
140
141impl Geometry {
142    /// Returns the geometry type as a string
143    #[must_use]
144    pub const fn geometry_type(&self) -> &'static str {
145        match self {
146            Self::Point(_) => "Point",
147            Self::LineString(_) => "LineString",
148            Self::Polygon(_) => "Polygon",
149            Self::MultiPoint(_) => "MultiPoint",
150            Self::MultiLineString(_) => "MultiLineString",
151            Self::MultiPolygon(_) => "MultiPolygon",
152            Self::GeometryCollection(_) => "GeometryCollection",
153        }
154    }
155
156    /// Returns true if the geometry is empty
157    #[must_use]
158    pub fn is_empty(&self) -> bool {
159        match self {
160            Self::Point(p) => p.coord.x.is_nan() || p.coord.y.is_nan(),
161            Self::LineString(ls) => ls.coords.is_empty(),
162            Self::Polygon(p) => p.exterior.coords.is_empty(),
163            Self::MultiPoint(mp) => mp.points.is_empty(),
164            Self::MultiLineString(mls) => mls.line_strings.is_empty(),
165            Self::MultiPolygon(mp) => mp.polygons.is_empty(),
166            Self::GeometryCollection(gc) => gc.geometries.is_empty(),
167        }
168    }
169
170    /// Computes the bounding box of the geometry
171    #[must_use]
172    pub fn bounds(&self) -> Option<(f64, f64, f64, f64)> {
173        match self {
174            Self::Point(p) => {
175                if p.coord.x.is_nan() || p.coord.y.is_nan() {
176                    None
177                } else {
178                    Some((p.coord.x, p.coord.y, p.coord.x, p.coord.y))
179                }
180            }
181            Self::LineString(ls) => ls.bounds(),
182            Self::Polygon(p) => p.bounds(),
183            Self::MultiPoint(mp) => mp.bounds(),
184            Self::MultiLineString(mls) => mls.bounds(),
185            Self::MultiPolygon(mp) => mp.bounds(),
186            Self::GeometryCollection(gc) => gc.bounds(),
187        }
188    }
189}
190
191/// Point geometry
192#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
193pub struct Point {
194    /// Coordinate of the point
195    pub coord: Coordinate,
196}
197
198impl Point {
199    /// Creates a new 2D point
200    #[must_use]
201    pub const fn new(x: f64, y: f64) -> Self {
202        Self {
203            coord: Coordinate::new_2d(x, y),
204        }
205    }
206
207    /// Creates a new 3D point
208    #[must_use]
209    pub const fn new_3d(x: f64, y: f64, z: f64) -> Self {
210        Self {
211            coord: Coordinate::new_3d(x, y, z),
212        }
213    }
214
215    /// Creates a point from a coordinate
216    #[must_use]
217    pub const fn from_coord(coord: Coordinate) -> Self {
218        Self { coord }
219    }
220
221    /// Returns the X coordinate
222    #[must_use]
223    pub const fn x(&self) -> f64 {
224        self.coord.x
225    }
226
227    /// Returns the Y coordinate
228    #[must_use]
229    pub const fn y(&self) -> f64 {
230        self.coord.y
231    }
232
233    /// Returns the Z coordinate if present
234    #[must_use]
235    pub const fn z(&self) -> Option<f64> {
236        self.coord.z
237    }
238}
239
240/// `LineString` geometry
241#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
242pub struct LineString {
243    /// Coordinates of the line string
244    pub coords: Vec<Coordinate>,
245}
246
247impl LineString {
248    /// Creates a new line string
249    pub fn new(coords: Vec<Coordinate>) -> Result<Self> {
250        if coords.len() < 2 {
251            return Err(OxiGdalError::InvalidParameter {
252                parameter: "coords",
253                message: "LineString must have at least 2 coordinates".to_string(),
254            });
255        }
256        Ok(Self { coords })
257    }
258
259    /// Creates a new empty line string (for building)
260    #[must_use]
261    pub const fn empty() -> Self {
262        Self { coords: Vec::new() }
263    }
264
265    /// Adds a coordinate to the line string
266    pub fn push(&mut self, coord: Coordinate) {
267        self.coords.push(coord);
268    }
269
270    /// Returns the number of coordinates
271    #[must_use]
272    pub fn len(&self) -> usize {
273        self.coords.len()
274    }
275
276    /// Returns true if the line string is empty
277    #[must_use]
278    pub fn is_empty(&self) -> bool {
279        self.coords.is_empty()
280    }
281
282    /// Computes the bounding box
283    #[must_use]
284    pub fn bounds(&self) -> Option<(f64, f64, f64, f64)> {
285        if self.coords.is_empty() {
286            return None;
287        }
288
289        let mut min_x = f64::INFINITY;
290        let mut min_y = f64::INFINITY;
291        let mut max_x = f64::NEG_INFINITY;
292        let mut max_y = f64::NEG_INFINITY;
293
294        for coord in &self.coords {
295            min_x = min_x.min(coord.x);
296            min_y = min_y.min(coord.y);
297            max_x = max_x.max(coord.x);
298            max_y = max_y.max(coord.y);
299        }
300
301        Some((min_x, min_y, max_x, max_y))
302    }
303
304    /// Returns a reference to the coordinates as a slice
305    #[must_use]
306    pub fn coords(&self) -> &[Coordinate] {
307        &self.coords
308    }
309
310    /// Returns an iterator over the coordinates
311    pub fn points(&self) -> impl Iterator<Item = &Coordinate> {
312        self.coords.iter()
313    }
314}
315
316/// Polygon geometry
317#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
318pub struct Polygon {
319    /// Exterior ring
320    pub exterior: LineString,
321    /// Interior rings (holes)
322    pub interiors: Vec<LineString>,
323}
324
325impl Polygon {
326    /// Creates a new polygon
327    pub fn new(exterior: LineString, interiors: Vec<LineString>) -> Result<Self> {
328        if exterior.coords.len() < 4 {
329            return Err(OxiGdalError::InvalidParameter {
330                parameter: "exterior",
331                message: "Polygon exterior ring must have at least 4 coordinates".to_string(),
332            });
333        }
334
335        // Check if ring is closed
336        let first = &exterior.coords[0];
337        let last = &exterior.coords[exterior.coords.len() - 1];
338        if (first.x - last.x).abs() > f64::EPSILON || (first.y - last.y).abs() > f64::EPSILON {
339            return Err(OxiGdalError::InvalidParameter {
340                parameter: "exterior",
341                message: "Polygon exterior ring must be closed".to_string(),
342            });
343        }
344
345        // Validate interior rings
346        for interior in &interiors {
347            if interior.coords.len() < 4 {
348                return Err(OxiGdalError::InvalidParameter {
349                    parameter: "interiors",
350                    message: "Polygon interior ring must have at least 4 coordinates".to_string(),
351                });
352            }
353
354            let first = &interior.coords[0];
355            let last = &interior.coords[interior.coords.len() - 1];
356            if (first.x - last.x).abs() > f64::EPSILON || (first.y - last.y).abs() > f64::EPSILON {
357                return Err(OxiGdalError::InvalidParameter {
358                    parameter: "interiors",
359                    message: "Polygon interior ring must be closed".to_string(),
360                });
361            }
362        }
363
364        Ok(Self {
365            exterior,
366            interiors,
367        })
368    }
369
370    /// Creates a new empty polygon
371    #[must_use]
372    pub const fn empty() -> Self {
373        Self {
374            exterior: LineString::empty(),
375            interiors: Vec::new(),
376        }
377    }
378
379    /// Computes the bounding box
380    #[must_use]
381    pub fn bounds(&self) -> Option<(f64, f64, f64, f64)> {
382        self.exterior.bounds()
383    }
384
385    /// Returns a reference to the exterior ring
386    #[must_use]
387    pub fn exterior(&self) -> &LineString {
388        &self.exterior
389    }
390
391    /// Returns a reference to the interior rings (holes)
392    #[must_use]
393    pub fn interiors(&self) -> &[LineString] {
394        &self.interiors
395    }
396}
397
398/// `MultiPoint` geometry
399#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
400pub struct MultiPoint {
401    /// Points in the collection
402    pub points: Vec<Point>,
403}
404
405impl MultiPoint {
406    /// Creates a new multi-point
407    #[must_use]
408    pub const fn new(points: Vec<Point>) -> Self {
409        Self { points }
410    }
411
412    /// Creates an empty multi-point
413    #[must_use]
414    pub const fn empty() -> Self {
415        Self { points: Vec::new() }
416    }
417
418    /// Adds a point
419    pub fn push(&mut self, point: Point) {
420        self.points.push(point);
421    }
422
423    /// Computes the bounding box
424    #[must_use]
425    pub fn bounds(&self) -> Option<(f64, f64, f64, f64)> {
426        if self.points.is_empty() {
427            return None;
428        }
429
430        let mut min_x = f64::INFINITY;
431        let mut min_y = f64::INFINITY;
432        let mut max_x = f64::NEG_INFINITY;
433        let mut max_y = f64::NEG_INFINITY;
434
435        for point in &self.points {
436            min_x = min_x.min(point.coord.x);
437            min_y = min_y.min(point.coord.y);
438            max_x = max_x.max(point.coord.x);
439            max_y = max_y.max(point.coord.y);
440        }
441
442        Some((min_x, min_y, max_x, max_y))
443    }
444}
445
446/// `MultiLineString` geometry
447#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
448pub struct MultiLineString {
449    /// Line strings in the collection
450    pub line_strings: Vec<LineString>,
451}
452
453impl MultiLineString {
454    /// Creates a new multi-line-string
455    #[must_use]
456    pub const fn new(line_strings: Vec<LineString>) -> Self {
457        Self { line_strings }
458    }
459
460    /// Creates an empty multi-line-string
461    #[must_use]
462    pub const fn empty() -> Self {
463        Self {
464            line_strings: Vec::new(),
465        }
466    }
467
468    /// Adds a line string
469    pub fn push(&mut self, line_string: LineString) {
470        self.line_strings.push(line_string);
471    }
472
473    /// Computes the bounding box
474    #[must_use]
475    pub fn bounds(&self) -> Option<(f64, f64, f64, f64)> {
476        if self.line_strings.is_empty() {
477            return None;
478        }
479
480        let mut min_x = f64::INFINITY;
481        let mut min_y = f64::INFINITY;
482        let mut max_x = f64::NEG_INFINITY;
483        let mut max_y = f64::NEG_INFINITY;
484
485        for ls in &self.line_strings {
486            if let Some((x_min, y_min, x_max, y_max)) = ls.bounds() {
487                min_x = min_x.min(x_min);
488                min_y = min_y.min(y_min);
489                max_x = max_x.max(x_max);
490                max_y = max_y.max(y_max);
491            }
492        }
493
494        if min_x.is_infinite() {
495            None
496        } else {
497            Some((min_x, min_y, max_x, max_y))
498        }
499    }
500}
501
502/// `MultiPolygon` geometry
503#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
504pub struct MultiPolygon {
505    /// Polygons in the collection
506    pub polygons: Vec<Polygon>,
507}
508
509impl MultiPolygon {
510    /// Creates a new multi-polygon
511    #[must_use]
512    pub const fn new(polygons: Vec<Polygon>) -> Self {
513        Self { polygons }
514    }
515
516    /// Creates an empty multi-polygon
517    #[must_use]
518    pub const fn empty() -> Self {
519        Self {
520            polygons: Vec::new(),
521        }
522    }
523
524    /// Adds a polygon
525    pub fn push(&mut self, polygon: Polygon) {
526        self.polygons.push(polygon);
527    }
528
529    /// Computes the bounding box
530    #[must_use]
531    pub fn bounds(&self) -> Option<(f64, f64, f64, f64)> {
532        if self.polygons.is_empty() {
533            return None;
534        }
535
536        let mut min_x = f64::INFINITY;
537        let mut min_y = f64::INFINITY;
538        let mut max_x = f64::NEG_INFINITY;
539        let mut max_y = f64::NEG_INFINITY;
540
541        for poly in &self.polygons {
542            if let Some((x_min, y_min, x_max, y_max)) = poly.bounds() {
543                min_x = min_x.min(x_min);
544                min_y = min_y.min(y_min);
545                max_x = max_x.max(x_max);
546                max_y = max_y.max(y_max);
547            }
548        }
549
550        if min_x.is_infinite() {
551            None
552        } else {
553            Some((min_x, min_y, max_x, max_y))
554        }
555    }
556}
557
558/// `GeometryCollection`
559#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
560pub struct GeometryCollection {
561    /// Geometries in the collection
562    pub geometries: Vec<Geometry>,
563}
564
565impl GeometryCollection {
566    /// Creates a new geometry collection
567    #[must_use]
568    pub const fn new(geometries: Vec<Geometry>) -> Self {
569        Self { geometries }
570    }
571
572    /// Creates an empty geometry collection
573    #[must_use]
574    pub const fn empty() -> Self {
575        Self {
576            geometries: Vec::new(),
577        }
578    }
579
580    /// Adds a geometry
581    pub fn push(&mut self, geometry: Geometry) {
582        self.geometries.push(geometry);
583    }
584
585    /// Computes the bounding box
586    #[must_use]
587    pub fn bounds(&self) -> Option<(f64, f64, f64, f64)> {
588        if self.geometries.is_empty() {
589            return None;
590        }
591
592        let mut min_x = f64::INFINITY;
593        let mut min_y = f64::INFINITY;
594        let mut max_x = f64::NEG_INFINITY;
595        let mut max_y = f64::NEG_INFINITY;
596
597        for geom in &self.geometries {
598            if let Some((x_min, y_min, x_max, y_max)) = geom.bounds() {
599                min_x = min_x.min(x_min);
600                min_y = min_y.min(y_min);
601                max_x = max_x.max(x_max);
602                max_y = max_y.max(y_max);
603            }
604        }
605
606        if min_x.is_infinite() {
607            None
608        } else {
609            Some((min_x, min_y, max_x, max_y))
610        }
611    }
612}
613
614#[cfg(test)]
615mod tests {
616    use super::*;
617
618    #[test]
619    fn test_coordinate_2d() {
620        let coord = Coordinate::new_2d(1.0, 2.0);
621        assert_eq!(coord.x, 1.0);
622        assert_eq!(coord.y, 2.0);
623        assert!(!coord.has_z());
624        assert!(!coord.has_m());
625        assert_eq!(coord.dimensions(), 2);
626    }
627
628    #[test]
629    fn test_coordinate_3d() {
630        let coord = Coordinate::new_3d(1.0, 2.0, 3.0);
631        assert_eq!(coord.x, 1.0);
632        assert_eq!(coord.y, 2.0);
633        assert_eq!(coord.z, Some(3.0));
634        assert!(coord.has_z());
635        assert!(!coord.has_m());
636        assert_eq!(coord.dimensions(), 3);
637    }
638
639    #[test]
640    fn test_point() {
641        let point = Point::new(1.0, 2.0);
642        assert_eq!(point.coord.x, 1.0);
643        assert_eq!(point.coord.y, 2.0);
644    }
645
646    #[test]
647    fn test_linestring() {
648        let coords = vec![
649            Coordinate::new_2d(0.0, 0.0),
650            Coordinate::new_2d(1.0, 1.0),
651            Coordinate::new_2d(2.0, 0.0),
652        ];
653        let ls = LineString::new(coords).ok();
654        assert!(ls.is_some());
655        let ls = ls.expect("linestring creation failed");
656        assert_eq!(ls.len(), 3);
657        assert!(!ls.is_empty());
658
659        let bounds = ls.bounds();
660        assert!(bounds.is_some());
661        let (min_x, min_y, max_x, max_y) = bounds.expect("bounds calculation failed");
662        assert_eq!(min_x, 0.0);
663        assert_eq!(min_y, 0.0);
664        assert_eq!(max_x, 2.0);
665        assert_eq!(max_y, 1.0);
666    }
667
668    #[test]
669    fn test_linestring_invalid() {
670        let coords = vec![Coordinate::new_2d(0.0, 0.0)];
671        let result = LineString::new(coords);
672        assert!(result.is_err());
673    }
674
675    #[test]
676    fn test_polygon() {
677        let exterior_coords = vec![
678            Coordinate::new_2d(0.0, 0.0),
679            Coordinate::new_2d(1.0, 0.0),
680            Coordinate::new_2d(1.0, 1.0),
681            Coordinate::new_2d(0.0, 1.0),
682            Coordinate::new_2d(0.0, 0.0),
683        ];
684        let exterior = LineString::new(exterior_coords).ok();
685        assert!(exterior.is_some());
686        let exterior = exterior.expect("linestring creation failed");
687
688        let poly = Polygon::new(exterior, vec![]);
689        assert!(poly.is_ok());
690    }
691
692    #[test]
693    fn test_polygon_not_closed() {
694        let exterior_coords = vec![
695            Coordinate::new_2d(0.0, 0.0),
696            Coordinate::new_2d(1.0, 0.0),
697            Coordinate::new_2d(1.0, 1.0),
698            Coordinate::new_2d(0.0, 1.0),
699        ];
700        let exterior = LineString::new(exterior_coords).ok();
701        assert!(exterior.is_some());
702        let exterior = exterior.expect("linestring creation failed");
703
704        let result = Polygon::new(exterior, vec![]);
705        assert!(result.is_err());
706    }
707}