zerometry/
zulti_points.rs

1use core::fmt;
2use std::io::{self, Write};
3
4use geo_types::MultiPoint;
5
6use crate::{
7    BoundingBox, COORD_SIZE_IN_BYTES, Coords, InputRelation, OutputRelation, RelationBetweenShapes,
8    Zerometry, Zoint, Zollection, Zolygon, ZultiPolygons, zine::Zine, zulti_lines::ZultiLines,
9};
10
11/// Equivalent of a [`geo_types::MultiPoint`].
12#[derive(Clone, Copy)]
13pub struct ZultiPoints<'a> {
14    bounding_box: &'a BoundingBox,
15    coords: &'a Coords,
16}
17
18impl<'a> ZultiPoints<'a> {
19    /// Create a [`ZultiPoints`] from its bounding box and coords.
20    pub fn new(bounding_box: &'a BoundingBox, coords: &'a Coords) -> Self {
21        Self {
22            bounding_box,
23            coords,
24        }
25    }
26
27    /// # Safety
28    /// The data must be generated from the [`Self::write_from_geometry`] method and be aligned on 64 bits
29    #[inline]
30    pub unsafe fn from_bytes(data: &'a [u8]) -> Self {
31        let bounding_box = unsafe { BoundingBox::from_bytes(&data[0..COORD_SIZE_IN_BYTES * 2]) };
32        let coords = unsafe { Coords::from_bytes(&data[COORD_SIZE_IN_BYTES * 2..]) };
33        Self::new(bounding_box, coords)
34    }
35
36    /// Convert the specified [`geo_types::MultiPoint`] to a valid [`ZultiPoints`] slice of bytes in the input buffer.
37    pub fn write_from_geometry(
38        writer: &mut impl Write,
39        geometry: &MultiPoint<f64>,
40    ) -> Result<(), io::Error> {
41        BoundingBox::write_from_geometry(writer, geometry.iter().copied())?;
42        for point in geometry.iter() {
43            writer.write_all(&point.x().to_ne_bytes())?;
44            writer.write_all(&point.y().to_ne_bytes())?;
45        }
46        Ok(())
47    }
48
49    /// Return the bounding box containing all polygons
50    #[inline]
51    pub fn bounding_box(&self) -> &'a BoundingBox {
52        self.bounding_box
53    }
54
55    /// Return the number of point contained in the multi-point
56    #[inline]
57    pub fn len(&self) -> usize {
58        self.coords.len()
59    }
60
61    /// Return `true` if the multi points doesn't contain any point
62    #[inline]
63    pub fn is_empty(&self) -> bool {
64        self.len() == 0
65    }
66
67    #[inline]
68    pub(crate) fn coords(&self) -> &'a Coords {
69        self.coords
70    }
71
72    /// Return a zoint by index, if the index doesn't exists, returns None
73    #[inline]
74    pub fn get(&self, index: usize) -> Option<Zoint<'a>> {
75        if index > self.coords.len() {
76            None
77        } else {
78            let coord = &self.coords()[index];
79            Some(Zoint::new(coord))
80        }
81    }
82
83    /// Returns the individual [`Zoint`]s that compose the [`ZultiPoints`]
84    #[inline]
85    pub fn points(&'a self) -> impl Iterator<Item = Zoint<'a>> {
86        (0..self.len()).map(move |index| self.get(index).unwrap())
87    }
88
89    /// Convert the [`ZultiPoints`] back to a [`geo_types::MultiPoint`].
90    pub fn to_geo(&self) -> geo_types::MultiPoint<f64> {
91        geo_types::MultiPoint::new(
92            self.coords
93                .iter()
94                .map(|coord| geo_types::Point::new(coord.lng(), coord.lat()))
95                .collect(),
96        )
97    }
98}
99
100impl<'a> fmt::Debug for ZultiPoints<'a> {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        f.debug_struct("ZultiPoints")
103            .field("bounding_box", &self.bounding_box)
104            .field(
105                "points",
106                &self.coords.iter().map(Zoint::new).collect::<Vec<_>>(),
107            )
108            .finish()
109    }
110}
111
112// A point cannot contains or intersect with anything
113impl<'a> RelationBetweenShapes<ZultiPoints<'a>> for ZultiPoints<'a> {
114    fn relation(&self, _other: &ZultiPoints<'a>, relation: InputRelation) -> OutputRelation {
115        relation.to_false().make_disjoint_if_set()
116    }
117}
118
119// A point cannot contains or intersect with anything
120impl<'a> RelationBetweenShapes<Zoint<'a>> for ZultiPoints<'a> {
121    fn relation(&self, _other: &Zoint<'a>, relation: InputRelation) -> OutputRelation {
122        relation.to_false().make_disjoint_if_set()
123    }
124}
125
126// A point cannot contains or intersect with anything
127impl<'a> RelationBetweenShapes<Zine<'a>> for ZultiPoints<'a> {
128    fn relation(&self, _other: &Zine<'a>, relation: InputRelation) -> OutputRelation {
129        relation.to_false().make_disjoint_if_set()
130    }
131}
132
133// A point cannot contains or intersect with anything
134impl<'a> RelationBetweenShapes<ZultiLines<'a>> for ZultiPoints<'a> {
135    fn relation(&self, _other: &ZultiLines<'a>, relation: InputRelation) -> OutputRelation {
136        relation.to_false().make_disjoint_if_set()
137    }
138}
139
140impl<'a> RelationBetweenShapes<Zolygon<'a>> for ZultiPoints<'a> {
141    fn relation(&self, other: &Zolygon<'a>, relation: InputRelation) -> OutputRelation {
142        other
143            .relation(self, relation.swap_contains_relation())
144            .swap_contains_relation()
145    }
146}
147
148impl<'a> RelationBetweenShapes<ZultiPolygons<'a>> for ZultiPoints<'a> {
149    fn relation(&self, other: &ZultiPolygons<'a>, relation: InputRelation) -> OutputRelation {
150        other
151            .relation(self, relation.swap_contains_relation())
152            .swap_contains_relation()
153    }
154}
155
156impl<'a> RelationBetweenShapes<Zollection<'a>> for ZultiPoints<'a> {
157    fn relation(&self, other: &Zollection<'a>, relation: InputRelation) -> OutputRelation {
158        other
159            .relation(self, relation.swap_contains_relation())
160            .swap_contains_relation()
161    }
162}
163
164impl<'a> RelationBetweenShapes<Zerometry<'a>> for ZultiPoints<'a> {
165    fn relation(&self, other: &Zerometry<'a>, relation: InputRelation) -> OutputRelation {
166        other
167            .relation(self, relation.swap_contains_relation())
168            .swap_contains_relation()
169    }
170}
171
172impl<'a> PartialEq<MultiPoint<f64>> for ZultiPoints<'a> {
173    fn eq(&self, other: &MultiPoint<f64>) -> bool {
174        self.coords
175            .iter()
176            .zip(other.iter())
177            .all(|(a, b)| a.lng() == b.x() && a.lat() == b.y())
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use bytemuck::cast_slice;
184    use geo_types::Point;
185    use insta::assert_compact_debug_snapshot;
186
187    use super::*;
188
189    #[test]
190    fn test_zulti_points_binary_format() {
191        let mut buffer = Vec::new();
192        ZultiPoints::write_from_geometry(
193            &mut buffer,
194            &MultiPoint::from(vec![Point::new(1.0, 2.0), Point::new(3.0, 4.0)]),
195        )
196        .unwrap();
197        let input: &[f64] = cast_slice(&buffer);
198        assert_compact_debug_snapshot!(input, @"[1.0, 2.0, 3.0, 4.0, 1.0, 2.0, 3.0, 4.0]");
199        let zulti_points = unsafe { ZultiPoints::from_bytes(&buffer) };
200        assert_compact_debug_snapshot!(zulti_points.bounding_box(), @"BoundingBox { bottom_left: Coord { x: 1.0, y: 2.0 }, top_right: Coord { x: 3.0, y: 4.0 } }");
201        assert_compact_debug_snapshot!(zulti_points.coords(), @"[Coord { x: 1.0, y: 2.0 }, Coord { x: 3.0, y: 4.0 }]");
202    }
203
204    // Prop test ensuring we can round trip from a multi-point to a zulti-points and back to a multi-point
205    proptest::proptest! {
206        #[test]
207        fn test_zulti_points_round_trip(points: Vec<(f64, f64)>) {
208            let multi_point = MultiPoint::from(points);
209            let mut buffer = Vec::new();
210            ZultiPoints::write_from_geometry(&mut buffer, &multi_point).unwrap();
211            let zulti_points = unsafe { ZultiPoints::from_bytes(&buffer) };
212            assert_eq!(zulti_points, multi_point);
213        }
214    }
215}