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