zerometry/
zine.rs

1use core::fmt;
2use std::io::{self, Write};
3
4use geo::{LineString, Point};
5
6use crate::{
7    BoundingBox, COORD_SIZE_IN_BYTES, Coords, InputRelation, OutputRelation, RelationBetweenShapes,
8    Segment, Zerometry, Zoint, Zollection, Zolygon, ZultiPoints, ZultiPolygons,
9    zulti_lines::ZultiLines,
10};
11
12/// Equivalent of a [`geo_types::LineString`].
13#[derive(Clone, Copy)]
14pub struct Zine<'a> {
15    bounding_box: &'a BoundingBox,
16    coords: &'a Coords,
17}
18
19impl<'a> Zine<'a> {
20    /// Create a [`Zine`] from its bounding box and coords.
21    /// If the bounding box doesn't properly bound the polygon all the operation will breaks.
22    pub fn new(bounding_box: &'a BoundingBox, coords: &'a Coords) -> Self {
23        Self {
24            bounding_box,
25            coords,
26        }
27    }
28
29    /// # Safety
30    /// The data must be generated from the [`Self::write_from_geometry`] method and be aligned on 64 bits
31    #[inline]
32    pub unsafe fn from_bytes(data: &'a [u8]) -> Self {
33        let bounding_box = unsafe { BoundingBox::from_bytes(&data[0..COORD_SIZE_IN_BYTES * 2]) };
34        let coords = unsafe { Coords::from_bytes(&data[COORD_SIZE_IN_BYTES * 2..]) };
35        Self::new(bounding_box, coords)
36    }
37
38    /// Convert the specified [`geo_types::LineString`] to a valid [`Zine`] slice of bytes in the input buffer.
39    pub fn write_from_geometry(
40        writer: &mut impl Write,
41        geometry: &LineString<f64>,
42    ) -> Result<(), io::Error> {
43        BoundingBox::write_from_geometry(
44            writer,
45            geometry.0.iter().map(|coord| Point::new(coord.x, coord.y)),
46        )?;
47        for point in geometry.0.iter() {
48            writer.write_all(&point.x.to_ne_bytes())?;
49            writer.write_all(&point.y.to_ne_bytes())?;
50        }
51        Ok(())
52    }
53
54    /// Return the internal bounding box
55    #[inline]
56    pub fn bounding_box(&self) -> &'a BoundingBox {
57        self.bounding_box
58    }
59
60    /// Return the number of points composing the line
61    #[inline]
62    pub fn len(&self) -> usize {
63        self.coords.len()
64    }
65
66    /// Return true if the line don't contain any point
67    #[inline]
68    pub fn is_empty(&self) -> bool {
69        self.len() == 0
70    }
71
72    /// Return the internal coords
73    #[inline]
74    pub fn coords(&self) -> &'a Coords {
75        self.coords
76    }
77
78    /// Return the segments that composes the line
79    #[inline]
80    pub fn segments(&self) -> impl Iterator<Item = Segment<'a>> {
81        self.coords
82            .consecutive_pairs()
83            // SAFETY: The pairs returned by coords are aligned and contains only two points
84            .map(|coords| unsafe { Segment::from_slice(coords) })
85    }
86
87    /// Convert the [`Zine`] back to a [`geo_types::LineString`].
88    pub fn to_geo(self) -> geo_types::LineString<f64> {
89        geo_types::LineString::new(
90            self.coords
91                .iter()
92                .map(|coord| geo_types::Coord {
93                    x: coord.lng(),
94                    y: coord.lat(),
95                })
96                .collect(),
97        )
98    }
99}
100
101impl<'a> fmt::Debug for Zine<'a> {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        f.debug_struct("Zine")
104            .field("bounding_box", &self.bounding_box)
105            .field(
106                "points",
107                &self.coords.iter().map(Zoint::new).collect::<Vec<_>>(),
108            )
109            .finish()
110    }
111}
112
113// A point cannot contains or intersect with anything
114impl<'a> RelationBetweenShapes<Zoint<'a>> for Zine<'a> {
115    fn relation(&self, _other: &Zoint<'a>, relation: InputRelation) -> OutputRelation {
116        relation.to_false().make_disjoint_if_set()
117    }
118}
119
120// A point cannot contains or intersect with anything
121impl<'a> RelationBetweenShapes<ZultiPoints<'a>> for Zine<'a> {
122    fn relation(&self, _other: &ZultiPoints<'a>, relation: InputRelation) -> OutputRelation {
123        relation.to_false().make_disjoint_if_set()
124    }
125}
126
127impl<'a> RelationBetweenShapes<Zine<'a>> for Zine<'a> {
128    fn relation(&self, other: &Zine<'a>, relation: InputRelation) -> OutputRelation {
129        let relation = relation.to_false();
130        if self.is_empty() || other.is_empty() || self.bounding_box().disjoint(other.bounding_box())
131        {
132            return relation.make_disjoint_if_set();
133        }
134
135        for lhs in self.segments() {
136            for rhs in other.segments() {
137                if lhs.intersects(&rhs) {
138                    return relation.make_intersect_if_set();
139                }
140            }
141        }
142
143        relation.make_disjoint_if_set()
144    }
145}
146
147impl<'a> RelationBetweenShapes<ZultiLines<'a>> for Zine<'a> {
148    fn relation(&self, other: &ZultiLines<'a>, relation: InputRelation) -> OutputRelation {
149        // no need to revert the contains/contained as this cannot happens with lines
150        other.relation(self, relation)
151    }
152}
153
154impl<'a> RelationBetweenShapes<Zolygon<'a>> for Zine<'a> {
155    fn relation(&self, other: &Zolygon<'a>, relation: InputRelation) -> OutputRelation {
156        if self.is_empty() || other.is_empty() || self.bounding_box().disjoint(other.bounding_box())
157        {
158            return relation.to_false().make_disjoint_if_set();
159        }
160
161        // To know if a line and a polygon intersect we check if any of our segments intersect with the polygon.
162        // That's O(n^2) but if you know a better algorithm please let me know.
163        for segment in self.segments() {
164            for other_segment in other.segments() {
165                if segment.intersects(&other_segment) {
166                    return relation.to_false().make_intersect_if_set();
167                }
168            }
169        }
170
171        // If we reached this point, the line and polygon don't intersect. To know if the line
172        // is contained in the polygon we check if any of its points is contained in the polygon.
173        // safe to unwrap because we checked that the polygon and line are not empty
174        let any = self.coords().iter().next().unwrap();
175        if other.contains(any) {
176            return relation.to_false().make_strict_contained_if_set();
177        }
178
179        relation.to_false().make_disjoint_if_set()
180    }
181}
182
183impl<'a> RelationBetweenShapes<ZultiPolygons<'a>> for Zine<'a> {
184    fn relation(&self, other: &ZultiPolygons<'a>, relation: InputRelation) -> OutputRelation {
185        let mut output = relation.to_false();
186        if self.is_empty() || other.is_empty() || self.bounding_box().disjoint(other.bounding_box())
187        {
188            return output.make_disjoint_if_set();
189        }
190
191        for polygon in other.polygons() {
192            output |= self.relation(&polygon, relation.strip_disjoint());
193
194            if output.any_relation() && relation.early_exit {
195                return output;
196            }
197        }
198
199        if output.any_relation() {
200            output
201        } else {
202            output.make_disjoint_if_set()
203        }
204    }
205}
206
207impl<'a> RelationBetweenShapes<Zollection<'a>> for Zine<'a> {
208    fn relation(&self, other: &Zollection<'a>, relation: InputRelation) -> OutputRelation {
209        other
210            .relation(self, relation.swap_contains_relation())
211            .swap_contains_relation()
212    }
213}
214
215impl<'a> RelationBetweenShapes<Zerometry<'a>> for Zine<'a> {
216    fn relation(&self, other: &Zerometry<'a>, relation: InputRelation) -> OutputRelation {
217        match other {
218            Zerometry::Point(zoint) => self.relation(zoint, relation),
219            Zerometry::MultiPoints(zulti_points) => self.relation(zulti_points, relation),
220            Zerometry::Line(zine) => self.relation(zine, relation),
221            Zerometry::MultiLines(zulti_lines) => self.relation(zulti_lines, relation),
222            Zerometry::Polygon(zolygon) => self.relation(zolygon, relation),
223            Zerometry::MultiPolygon(zulti_polygon) => self.relation(zulti_polygon, relation),
224            Zerometry::Collection(zollection) => self.relation(zollection, relation),
225        }
226    }
227}
228
229impl<'a> PartialEq<LineString<f64>> for Zine<'a> {
230    fn eq(&self, other: &LineString<f64>) -> bool {
231        self.coords
232            .iter()
233            .zip(other.0.iter())
234            .all(|(a, b)| a.lng() == b.x && a.lat() == b.y)
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use bytemuck::cast_slice;
241    use geo::{MultiPolygon, coord, polygon};
242    use geo_types::Point;
243    use insta::assert_compact_debug_snapshot;
244
245    use super::*;
246
247    #[test]
248    fn test_zine_binary_format() {
249        let mut buffer = Vec::new();
250        Zine::write_from_geometry(
251            &mut buffer,
252            &LineString::from(vec![Point::new(1.0, 2.0), Point::new(3.0, 4.0)]),
253        )
254        .unwrap();
255        let input: &[f64] = cast_slice(&buffer);
256        assert_compact_debug_snapshot!(input, @"[1.0, 2.0, 3.0, 4.0, 1.0, 2.0, 3.0, 4.0]");
257        let zulti_points = unsafe { Zine::from_bytes(&buffer) };
258        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 } }");
259        assert_compact_debug_snapshot!(zulti_points.coords(), @"[Coord { x: 1.0, y: 2.0 }, Coord { x: 3.0, y: 4.0 }]");
260    }
261
262    #[test]
263    fn test_zine_in_polygon() {
264        let line = LineString::new(vec![
265            coord! { x: 0.4, y: 0.4},
266            coord! { x: 0.6, y: 0.4},
267            coord! { x: 0.6, y: 0.6},
268            coord! { x: 0.4, y: 0.6},
269        ]);
270        let polygon = polygon![
271             (x: 0., y: 0.),
272             (x: 1., y: 0.),
273             (x: 1., y: 1.),
274             (x: 0., y: 1.),
275        ];
276
277        let mut buf = Vec::new();
278        Zine::write_from_geometry(&mut buf, &line).unwrap();
279        let zine = unsafe { Zine::from_bytes(&buf) };
280
281        let mut buf = Vec::new();
282        Zolygon::write_from_geometry(&mut buf, &polygon).unwrap();
283        let zolygon = unsafe { Zolygon::from_bytes(&buf) };
284
285        assert_compact_debug_snapshot!(zine.all_relation(&zolygon), @"OutputRelation { contains: Some(false), strict_contains: Some(false), contained: Some(true), strict_contained: Some(true), intersect: Some(false), disjoint: Some(false) }");
286    }
287
288    #[test]
289    fn test_zine_and_multipolygon() {
290        let line = LineString::new(vec![
291            coord! { x: 0.4, y: 0.4},
292            coord! { x: 0.6, y: 0.4},
293            coord! { x: 0.6, y: 0.6},
294            coord! { x: 0.4, y: 0.6},
295        ]);
296        let inside = polygon![
297             (x: 0., y: 0.),
298             (x: 1., y: 0.),
299             (x: 1., y: 1.),
300             (x: 0., y: 1.),
301        ];
302        let outside = polygon![
303             (x: 5., y: 5.),
304             (x: 6., y: 5.),
305             (x: 6., y: 6.),
306             (x: 5., y: 6.),
307        ];
308        let intersect = polygon![
309             (x: 0.5, y: 0.5),
310             (x: 0.6, y: 0.5),
311             (x: 0.6, y: 0.6),
312             (x: 0.5, y: 0.6),
313        ];
314        let multi_polygons_inside = MultiPolygon::new(vec![inside.clone()]);
315        let multi_polygons_outside = MultiPolygon::new(vec![outside.clone()]);
316        let multi_polygons_intersect = MultiPolygon::new(vec![intersect.clone()]);
317        let multi_polygons_in_and_out = MultiPolygon::new(vec![inside.clone(), outside.clone()]);
318        let multi_polygons_all =
319            MultiPolygon::new(vec![inside.clone(), outside.clone(), intersect.clone()]);
320
321        let mut buf = Vec::new();
322        Zine::write_from_geometry(&mut buf, &line).unwrap();
323        let zine = unsafe { Zine::from_bytes(&buf) };
324
325        let mut buf = Vec::new();
326        ZultiPolygons::write_from_geometry(&mut buf, &multi_polygons_inside).unwrap();
327        let inside = unsafe { ZultiPolygons::from_bytes(&buf) };
328        assert_compact_debug_snapshot!(zine.all_relation(&inside ), @"OutputRelation { contains: Some(false), strict_contains: Some(false), contained: Some(true), strict_contained: Some(true), intersect: Some(false), disjoint: Some(false) }");
329
330        let mut buf = Vec::new();
331        ZultiPolygons::write_from_geometry(&mut buf, &multi_polygons_outside).unwrap();
332        let multi_polygons_outside = unsafe { ZultiPolygons::from_bytes(&buf) };
333        assert_compact_debug_snapshot!(zine.all_relation(&multi_polygons_outside), @"OutputRelation { contains: Some(false), strict_contains: Some(false), contained: Some(false), strict_contained: Some(false), intersect: Some(false), disjoint: Some(true) }");
334
335        let mut buf = Vec::new();
336        ZultiPolygons::write_from_geometry(&mut buf, &multi_polygons_intersect).unwrap();
337        let multi_polygons_intersect = unsafe { ZultiPolygons::from_bytes(&buf) };
338        assert_compact_debug_snapshot!(zine.all_relation(&multi_polygons_intersect), @"OutputRelation { contains: Some(false), strict_contains: Some(false), contained: Some(false), strict_contained: Some(false), intersect: Some(true), disjoint: Some(false) }");
339
340        let mut buf = Vec::new();
341        ZultiPolygons::write_from_geometry(&mut buf, &multi_polygons_in_and_out).unwrap();
342        let multi_polygons_in_and_out = unsafe { ZultiPolygons::from_bytes(&buf) };
343        assert_compact_debug_snapshot!(zine.all_relation(&multi_polygons_in_and_out), @"OutputRelation { contains: Some(false), strict_contains: Some(false), contained: Some(true), strict_contained: Some(true), intersect: Some(false), disjoint: Some(false) }");
344
345        let mut buf = Vec::new();
346        ZultiPolygons::write_from_geometry(&mut buf, &multi_polygons_all).unwrap();
347        let multi_polygons_all = unsafe { ZultiPolygons::from_bytes(&buf) };
348        assert_compact_debug_snapshot!(zine.all_relation(&multi_polygons_all), @"OutputRelation { contains: Some(false), strict_contains: Some(false), contained: Some(true), strict_contained: Some(true), intersect: Some(true), disjoint: Some(false) }");
349        assert_compact_debug_snapshot!(zine.any_relation(&multi_polygons_all), @"OutputRelation { contains: Some(false), strict_contains: Some(false), contained: Some(true), strict_contained: Some(true), intersect: Some(false), disjoint: Some(false) }");
350    }
351
352    // Prop test ensuring we can round trip from a multi-point to a zulti-points and back to a multi-point
353    proptest::proptest! {
354        #[test]
355        fn test_zine_points_round_trip(points: Vec<(f64, f64)>) {
356            let multi_point = LineString::from(points);
357            let mut buffer = Vec::new();
358            Zine::write_from_geometry(&mut buffer, &multi_point).unwrap();
359            let zulti_points = unsafe { Zine::from_bytes(&buffer) };
360            assert_eq!(zulti_points, multi_point);
361        }
362    }
363}