Skip to main content

geo/algorithm/
coordinate_position.rs

1use std::cmp::Ordering;
2
3use crate::geometry::*;
4use crate::intersects::{point_in_rect, value_in_between};
5use crate::kernels::*;
6use crate::{BoundingRect, HasDimensions, Intersects};
7use crate::{GeoNum, GeometryCow};
8
9/// The position of a `Coord` relative to a `Geometry`
10#[derive(PartialEq, Eq, Clone, Copy, Debug)]
11pub enum CoordPos {
12    OnBoundary,
13    Inside,
14    Outside,
15}
16
17/// Determine whether a `Coord` lies inside, outside, or on the boundary of a geometry.
18///
19/// # Examples
20///
21/// ```rust
22/// use geo::{polygon, coord};
23/// use geo::coordinate_position::{CoordinatePosition, CoordPos};
24///
25/// let square_poly = polygon![(x: 0.0, y: 0.0), (x: 2.0, y: 0.0), (x: 2.0, y: 2.0), (x: 0.0, y: 2.0), (x: 0.0, y: 0.0)];
26///
27/// let inside_coord = coord! { x: 1.0, y: 1.0 };
28/// assert_eq!(square_poly.coordinate_position(&inside_coord), CoordPos::Inside);
29///
30/// let boundary_coord = coord! { x: 0.0, y: 1.0 };
31/// assert_eq!(square_poly.coordinate_position(&boundary_coord), CoordPos::OnBoundary);
32///
33/// let outside_coord = coord! { x: 5.0, y: 5.0 };
34/// assert_eq!(square_poly.coordinate_position(&outside_coord), CoordPos::Outside);
35/// ```
36pub trait CoordinatePosition {
37    type Scalar: GeoNum;
38    fn coordinate_position(&self, coord: &Coord<Self::Scalar>) -> CoordPos {
39        let mut is_inside = false;
40        let mut boundary_count = 0;
41
42        self.calculate_coordinate_position(coord, &mut is_inside, &mut boundary_count);
43
44        // “The boundary of an arbitrary collection of geometries whose interiors are disjoint
45        // consists of geometries drawn from the boundaries of the element geometries by
46        // application of the ‘mod 2’ union rule”
47        //
48        // ― OpenGIS Simple Feature Access § 6.1.15.1
49        if boundary_count % 2 == 1 {
50            CoordPos::OnBoundary
51        } else if is_inside {
52            CoordPos::Inside
53        } else {
54            CoordPos::Outside
55        }
56    }
57
58    // impls of this trait must:
59    //  1. set `is_inside = true` if `coord` is contained within the Interior of any component.
60    //  2. increment `boundary_count` for each component whose Boundary contains `coord`.
61    fn calculate_coordinate_position(
62        &self,
63        coord: &Coord<Self::Scalar>,
64        is_inside: &mut bool,
65        boundary_count: &mut usize,
66    );
67}
68
69impl<T> CoordinatePosition for Coord<T>
70where
71    T: GeoNum,
72{
73    type Scalar = T;
74    fn calculate_coordinate_position(
75        &self,
76        coord: &Coord<T>,
77        is_inside: &mut bool,
78        _boundary_count: &mut usize,
79    ) {
80        if self == coord {
81            *is_inside = true;
82        }
83    }
84}
85
86impl<T> CoordinatePosition for Point<T>
87where
88    T: GeoNum,
89{
90    type Scalar = T;
91    fn calculate_coordinate_position(
92        &self,
93        coord: &Coord<T>,
94        is_inside: &mut bool,
95        _boundary_count: &mut usize,
96    ) {
97        if &self.0 == coord {
98            *is_inside = true;
99        }
100    }
101}
102
103impl<T> CoordinatePosition for Line<T>
104where
105    T: GeoNum,
106{
107    type Scalar = T;
108    fn calculate_coordinate_position(
109        &self,
110        coord: &Coord<T>,
111        is_inside: &mut bool,
112        boundary_count: &mut usize,
113    ) {
114        // degenerate line is a point
115        if self.start == self.end {
116            self.start
117                .calculate_coordinate_position(coord, is_inside, boundary_count);
118            return;
119        }
120
121        if coord == &self.start || coord == &self.end {
122            *boundary_count += 1;
123        } else if self.intersects(coord) {
124            *is_inside = true;
125        }
126    }
127}
128
129impl<T> CoordinatePosition for LineString<T>
130where
131    T: GeoNum,
132{
133    type Scalar = T;
134    fn calculate_coordinate_position(
135        &self,
136        coord: &Coord<T>,
137        is_inside: &mut bool,
138        boundary_count: &mut usize,
139    ) {
140        match self.0.len() {
141            0 => {
142                warn!("invalid line string with 0 coords");
143                return;
144            }
145            1 => {
146                warn!("invalid line string with only 1 coord");
147                // degenerate LineString handled like a Point
148                return self[0].calculate_coordinate_position(coord, is_inside, boundary_count);
149            }
150            2 => {
151                // LineString with two coords is just a Line
152                return Line::new(self.0[0], self.0[1]).calculate_coordinate_position(
153                    coord,
154                    is_inside,
155                    boundary_count,
156                );
157            }
158            _ => {}
159        }
160
161        // optimization: return early if there's no chance of an intersection
162        // since self.0 is non-empty, safe to `unwrap`
163        if !self.bounding_rect().unwrap().intersects(coord) {
164            return;
165        }
166
167        // A closed linestring has no boundary, per SFS
168        if !self.is_closed() {
169            // since self.0 is non-empty, safe to `unwrap`
170            if coord == self.0.first().unwrap() || coord == self.0.last().unwrap() {
171                *boundary_count += 1;
172                return;
173            }
174        }
175
176        if self.intersects(coord) {
177            // We've already checked for "Boundary" condition, so if there's an intersection at
178            // this point, coord must be on the interior
179            *is_inside = true
180        }
181    }
182}
183
184impl<T> CoordinatePosition for Triangle<T>
185where
186    T: GeoNum,
187{
188    type Scalar = T;
189    fn calculate_coordinate_position(
190        &self,
191        coord: &Coord<T>,
192        is_inside: &mut bool,
193        boundary_count: &mut usize,
194    ) {
195        *is_inside = self
196            .to_lines()
197            .map(|l| {
198                let orientation = T::Ker::orient2d(l.start, l.end, *coord);
199                if orientation == Orientation::Collinear
200                    && point_in_rect(*coord, l.start, l.end)
201                    // If a coordinate falls *on* one of the triangle's vertices,
202                    // it will appear both as the `end` of one segment and the `start` of the next.
203                    // We only want to count one boundary crossing in that case,
204                    // so we ignore it if it falls on `l.end`,
205                    // knowing that we'll count it on some other segments `l.start`
206                    && *coord != l.end
207                {
208                    *boundary_count += 1;
209                }
210                orientation
211            })
212            .windows(2)
213            .all(|win| win[0] == win[1] && win[0] != Orientation::Collinear);
214    }
215}
216
217impl<T> CoordinatePosition for Rect<T>
218where
219    T: GeoNum,
220{
221    type Scalar = T;
222    fn calculate_coordinate_position(
223        &self,
224        coord: &Coord<T>,
225        is_inside: &mut bool,
226        boundary_count: &mut usize,
227    ) {
228        let mut boundary = false;
229
230        let min = self.min();
231
232        match coord.x.partial_cmp(&min.x).unwrap() {
233            Ordering::Less => return,
234            Ordering::Equal => boundary = true,
235            Ordering::Greater => {}
236        }
237        match coord.y.partial_cmp(&min.y).unwrap() {
238            Ordering::Less => return,
239            Ordering::Equal => boundary = true,
240            Ordering::Greater => {}
241        }
242
243        let max = self.max();
244
245        match max.x.partial_cmp(&coord.x).unwrap() {
246            Ordering::Less => return,
247            Ordering::Equal => boundary = true,
248            Ordering::Greater => {}
249        }
250        match max.y.partial_cmp(&coord.y).unwrap() {
251            Ordering::Less => return,
252            Ordering::Equal => boundary = true,
253            Ordering::Greater => {}
254        }
255
256        if boundary {
257            *boundary_count += 1;
258        } else {
259            *is_inside = true;
260        }
261    }
262}
263
264impl<T> CoordinatePosition for MultiPoint<T>
265where
266    T: GeoNum,
267{
268    type Scalar = T;
269    fn calculate_coordinate_position(
270        &self,
271        coord: &Coord<T>,
272        is_inside: &mut bool,
273        _boundary_count: &mut usize,
274    ) {
275        if self.0.iter().any(|p| &p.0 == coord) {
276            *is_inside = true;
277        }
278    }
279}
280
281impl<T> CoordinatePosition for Polygon<T>
282where
283    T: GeoNum,
284{
285    type Scalar = T;
286    fn calculate_coordinate_position(
287        &self,
288        coord: &Coord<T>,
289        is_inside: &mut bool,
290        boundary_count: &mut usize,
291    ) {
292        if self.is_empty() {
293            return;
294        }
295
296        match coord_pos_relative_to_ring(*coord, self.exterior()) {
297            CoordPos::Outside => {}
298            CoordPos::OnBoundary => {
299                *boundary_count += 1;
300            }
301            CoordPos::Inside => {
302                for hole in self.interiors() {
303                    match coord_pos_relative_to_ring(*coord, hole) {
304                        CoordPos::Outside => {}
305                        CoordPos::OnBoundary => {
306                            *boundary_count += 1;
307                            return;
308                        }
309                        CoordPos::Inside => {
310                            return;
311                        }
312                    }
313                }
314                // the coord is *outside* the interior holes, so it's *inside* the polygon
315                *is_inside = true;
316            }
317        }
318    }
319}
320
321impl<T> CoordinatePosition for MultiLineString<T>
322where
323    T: GeoNum,
324{
325    type Scalar = T;
326    fn calculate_coordinate_position(
327        &self,
328        coord: &Coord<T>,
329        is_inside: &mut bool,
330        boundary_count: &mut usize,
331    ) {
332        for line_string in &self.0 {
333            line_string.calculate_coordinate_position(coord, is_inside, boundary_count);
334        }
335    }
336}
337
338impl<T> CoordinatePosition for MultiPolygon<T>
339where
340    T: GeoNum,
341{
342    type Scalar = T;
343    fn calculate_coordinate_position(
344        &self,
345        coord: &Coord<T>,
346        is_inside: &mut bool,
347        boundary_count: &mut usize,
348    ) {
349        for polygon in &self.0 {
350            polygon.calculate_coordinate_position(coord, is_inside, boundary_count);
351        }
352    }
353}
354
355impl<T> CoordinatePosition for GeometryCollection<T>
356where
357    T: GeoNum,
358{
359    type Scalar = T;
360    fn calculate_coordinate_position(
361        &self,
362        coord: &Coord<T>,
363        is_inside: &mut bool,
364        boundary_count: &mut usize,
365    ) {
366        for geometry in self {
367            geometry.calculate_coordinate_position(coord, is_inside, boundary_count);
368        }
369    }
370}
371
372impl<T> CoordinatePosition for Geometry<T>
373where
374    T: GeoNum,
375{
376    type Scalar = T;
377    crate::geometry_delegate_impl! {
378        fn calculate_coordinate_position(
379            &self,
380            coord: &Coord<T>,
381            is_inside: &mut bool,
382            boundary_count: &mut usize) -> ();
383    }
384}
385
386impl<T: GeoNum> CoordinatePosition for GeometryCow<'_, T> {
387    type Scalar = T;
388    crate::geometry_cow_delegate_impl! {
389        fn calculate_coordinate_position(
390            &self,
391            coord: &Coord<T>,
392            is_inside: &mut bool,
393            boundary_count: &mut usize) -> ();
394    }
395}
396
397/// Calculate the position of a `Coord` relative to a
398/// closed `LineString`.
399pub fn coord_pos_relative_to_ring<T>(coord: Coord<T>, linestring: &LineString<T>) -> CoordPos
400where
401    T: GeoNum,
402{
403    debug_assert!(linestring.is_closed());
404
405    // LineString without points
406    if linestring.0.is_empty() {
407        return CoordPos::Outside;
408    }
409    if linestring.0.len() == 1 {
410        // If LineString has one point, it will not generate
411        // any lines.  So, we handle this edge case separately.
412        return if coord == linestring.0[0] {
413            CoordPos::OnBoundary
414        } else {
415            CoordPos::Outside
416        };
417    }
418
419    // Use winding number algorithm with on boundary short-cicuit
420    // See: https://en.wikipedia.org/wiki/Point_in_polygon#Winding_number_algorithm
421    let mut winding_number = 0;
422    for line in linestring.lines() {
423        // Edge Crossing Rules:
424        //   1. an upward edge includes its starting endpoint, and excludes its final endpoint;
425        //   2. a downward edge excludes its starting endpoint, and includes its final endpoint;
426        //   3. horizontal edges are excluded
427        //   4. the edge-ray intersection point must be strictly right of the coord.
428        if line.start.y <= coord.y {
429            if line.end.y >= coord.y {
430                let o = T::Ker::orient2d(line.start, line.end, coord);
431                if o == Orientation::CounterClockwise && line.end.y != coord.y {
432                    winding_number += 1
433                } else if o == Orientation::Collinear
434                    && value_in_between(coord.x, line.start.x, line.end.x)
435                {
436                    return CoordPos::OnBoundary;
437                }
438            };
439        } else if line.end.y <= coord.y {
440            let o = T::Ker::orient2d(line.start, line.end, coord);
441            if o == Orientation::Clockwise {
442                winding_number -= 1
443            } else if o == Orientation::Collinear
444                && value_in_between(coord.x, line.start.x, line.end.x)
445            {
446                return CoordPos::OnBoundary;
447            }
448        }
449    }
450    if winding_number == 0 {
451        CoordPos::Outside
452    } else {
453        CoordPos::Inside
454    }
455}
456
457#[cfg(test)]
458mod test {
459    use super::*;
460    use crate::algorithm::Convert;
461    use crate::{coord, line_string, point, polygon, wkt};
462
463    #[test]
464    fn test_empty_poly() {
465        let square_poly: Polygon<f64> = Polygon::empty();
466        assert_eq!(
467            square_poly.coordinate_position(&Coord::zero()),
468            CoordPos::Outside
469        );
470    }
471
472    #[test]
473    fn test_simple_poly() {
474        let square_poly = polygon![(x: 0.0, y: 0.0), (x: 2.0, y: 0.0), (x: 2.0, y: 2.0), (x: 0.0, y: 2.0), (x: 0.0, y: 0.0)];
475
476        let inside_coord = coord! { x: 1.0, y: 1.0 };
477        assert_eq!(
478            square_poly.coordinate_position(&inside_coord),
479            CoordPos::Inside
480        );
481
482        let vertex_coord = coord! { x: 0.0, y: 0.0 };
483        assert_eq!(
484            square_poly.coordinate_position(&vertex_coord),
485            CoordPos::OnBoundary
486        );
487
488        let boundary_coord = coord! { x: 0.0, y: 1.0 };
489        assert_eq!(
490            square_poly.coordinate_position(&boundary_coord),
491            CoordPos::OnBoundary
492        );
493
494        let outside_coord = coord! { x: 5.0, y: 5.0 };
495        assert_eq!(
496            square_poly.coordinate_position(&outside_coord),
497            CoordPos::Outside
498        );
499    }
500
501    #[test]
502    fn test_poly_interior() {
503        let poly = polygon![
504            exterior: [
505                (x: 11., y: 11.),
506                (x: 20., y: 11.),
507                (x: 20., y: 20.),
508                (x: 11., y: 20.),
509                (x: 11., y: 11.),
510            ],
511            interiors: [
512                [
513                    (x: 13., y: 13.),
514                    (x: 13., y: 17.),
515                    (x: 17., y: 17.),
516                    (x: 17., y: 13.),
517                    (x: 13., y: 13.),
518                ]
519            ],
520        ];
521
522        let inside_hole = coord! { x: 14.0, y: 14.0 };
523        assert_eq!(poly.coordinate_position(&inside_hole), CoordPos::Outside);
524
525        let outside_poly = coord! { x: 30.0, y: 30.0 };
526        assert_eq!(poly.coordinate_position(&outside_poly), CoordPos::Outside);
527
528        let on_outside_border = coord! { x: 20.0, y: 15.0 };
529        assert_eq!(
530            poly.coordinate_position(&on_outside_border),
531            CoordPos::OnBoundary
532        );
533
534        let on_inside_border = coord! { x: 13.0, y: 15.0 };
535        assert_eq!(
536            poly.coordinate_position(&on_inside_border),
537            CoordPos::OnBoundary
538        );
539
540        let inside_coord = coord! { x: 12.0, y: 12.0 };
541        assert_eq!(poly.coordinate_position(&inside_coord), CoordPos::Inside);
542    }
543
544    #[test]
545    fn test_simple_line() {
546        use crate::point;
547        let line = Line::new(point![x: 0.0, y: 0.0], point![x: 10.0, y: 10.0]);
548
549        let start = coord! { x: 0.0, y: 0.0 };
550        assert_eq!(line.coordinate_position(&start), CoordPos::OnBoundary);
551
552        let end = coord! { x: 10.0, y: 10.0 };
553        assert_eq!(line.coordinate_position(&end), CoordPos::OnBoundary);
554
555        let interior = coord! { x: 5.0, y: 5.0 };
556        assert_eq!(line.coordinate_position(&interior), CoordPos::Inside);
557
558        let outside = coord! { x: 6.0, y: 5.0 };
559        assert_eq!(line.coordinate_position(&outside), CoordPos::Outside);
560    }
561
562    #[test]
563    fn test_degenerate_line() {
564        let line = Line::new(point![x: 0.0, y: 0.0], point![x: 0.0, y: 0.0]);
565
566        let start = coord! { x: 0.0, y: 0.0 };
567        assert_eq!(line.coordinate_position(&start), CoordPos::Inside);
568
569        let outside = coord! { x: 10.0, y: 10.0 };
570        assert_eq!(line.coordinate_position(&outside), CoordPos::Outside);
571    }
572
573    #[test]
574    fn test_point() {
575        let p1 = point![x: 2.0, y: 0.0];
576
577        let c1 = coord! { x: 2.0, y: 0.0 };
578        let c2 = coord! { x: 3.0, y: 3.0 };
579
580        assert_eq!(p1.coordinate_position(&c1), CoordPos::Inside);
581        assert_eq!(p1.coordinate_position(&c2), CoordPos::Outside);
582
583        assert_eq!(c1.coordinate_position(&c1), CoordPos::Inside);
584        assert_eq!(c1.coordinate_position(&c2), CoordPos::Outside);
585    }
586
587    mod line_string {
588        use super::*;
589        #[test]
590        fn test_simple_line_string() {
591            let line_string: LineString = wkt!(LINESTRING(0 0,1 1,2 0,3 0)).convert();
592
593            let start = Coord::zero();
594            assert_eq!(
595                line_string.coordinate_position(&start),
596                CoordPos::OnBoundary
597            );
598
599            let midpoint = coord! { x: 0.5, y: 0.5 };
600            assert_eq!(line_string.coordinate_position(&midpoint), CoordPos::Inside);
601
602            let vertex = coord! { x: 2.0, y: 0.0 };
603            assert_eq!(line_string.coordinate_position(&vertex), CoordPos::Inside);
604
605            let end = coord! { x: 3.0, y: 0.0 };
606            assert_eq!(line_string.coordinate_position(&end), CoordPos::OnBoundary);
607
608            let outside = coord! { x: 3.0, y: 1.0 };
609            assert_eq!(line_string.coordinate_position(&outside), CoordPos::Outside);
610        }
611
612        #[test]
613        fn test_degenerate_line_strings() {
614            let origin = Coord::zero();
615
616            assert_eq!(
617                wkt!(LINESTRING EMPTY).coordinate_position(&origin),
618                CoordPos::Outside
619            );
620
621            // Handled as a Point
622            assert_eq!(
623                wkt!(LINESTRING(0. 0.)).coordinate_position(&origin),
624                CoordPos::Inside
625            );
626
627            // Handled as a Point
628            assert_eq!(
629                wkt!(LINESTRING(0. 0.,0. 0.)).coordinate_position(&origin),
630                CoordPos::Inside
631            );
632
633            // Not actually degenerate, but handled specially as a LINE
634            assert_eq!(
635                wkt!(LINESTRING(0. 0.,2. 0.)).coordinate_position(&origin),
636                CoordPos::OnBoundary
637            );
638        }
639
640        #[test]
641        fn test_closed_line_string() {
642            let line_string: LineString = wkt!(LINESTRING(0 0,1 1,2 0,3 2,0 2,0 0)).convert();
643
644            // sanity check
645            assert!(line_string.is_closed());
646
647            // closed line strings have no boundary
648            let start = Coord::zero();
649            assert_eq!(line_string.coordinate_position(&start), CoordPos::Inside);
650
651            let midpoint = coord! { x: 0.5, y: 0.5 };
652            assert_eq!(line_string.coordinate_position(&midpoint), CoordPos::Inside);
653
654            let outside = coord! { x: 3.0, y: 1.0 };
655            assert_eq!(line_string.coordinate_position(&outside), CoordPos::Outside);
656        }
657    }
658
659    #[test]
660    fn test_boundary_rule() {
661        let multi_line_string = MultiLineString::new(vec![
662            // first two lines have same start point but different end point
663            line_string![(x: 0.0, y: 0.0), (x: 1.0, y: 1.0)],
664            line_string![(x: 0.0, y: 0.0), (x: -1.0, y: -1.0)],
665            // third line has its own start point, but it's end touches the middle of first line
666            line_string![(x: 0.0, y: 1.0), (x: 0.5, y: 0.5)],
667            // fourth and fifth have independent start points, but both end at the middle of the
668            // second line
669            line_string![(x: 0.0, y: -1.0), (x: -0.5, y: -0.5)],
670            line_string![(x: 0.0, y: -2.0), (x: -0.5, y: -0.5)],
671        ]);
672
673        let outside_of_all = coord! { x: 123.0, y: 123.0 };
674        assert_eq!(
675            multi_line_string.coordinate_position(&outside_of_all),
676            CoordPos::Outside
677        );
678
679        let end_of_one_line = coord! { x: -1.0, y: -1.0 };
680        assert_eq!(
681            multi_line_string.coordinate_position(&end_of_one_line),
682            CoordPos::OnBoundary
683        );
684
685        // in boundary of first and second, so considered *not* in the boundary by mod 2 rule
686        let shared_start = Coord::zero();
687        assert_eq!(
688            multi_line_string.coordinate_position(&shared_start),
689            CoordPos::Outside
690        );
691
692        // *in* the first line, on the boundary of the third line
693        let one_end_plus_midpoint = coord! { x: 0.5, y: 0.5 };
694        assert_eq!(
695            multi_line_string.coordinate_position(&one_end_plus_midpoint),
696            CoordPos::OnBoundary
697        );
698
699        // *in* the first line, on the *boundary* of the fourth and fifth line
700        let two_ends_plus_midpoint = coord! { x: -0.5, y: -0.5 };
701        assert_eq!(
702            multi_line_string.coordinate_position(&two_ends_plus_midpoint),
703            CoordPos::Inside
704        );
705    }
706
707    #[test]
708    fn test_rect() {
709        let rect = Rect::new((0.0, 0.0), (10.0, 10.0));
710        assert_eq!(
711            rect.coordinate_position(&coord! { x: 5.0, y: 5.0 }),
712            CoordPos::Inside
713        );
714        assert_eq!(
715            rect.coordinate_position(&coord! { x: 0.0, y: 5.0 }),
716            CoordPos::OnBoundary
717        );
718        assert_eq!(
719            rect.coordinate_position(&coord! { x: 15.0, y: 15.0 }),
720            CoordPos::Outside
721        );
722    }
723
724    #[test]
725    fn test_triangle() {
726        let triangle = Triangle::new((0.0, 0.0).into(), (5.0, 10.0).into(), (10.0, 0.0).into());
727        assert_eq!(
728            triangle.coordinate_position(&coord! { x: 5.0, y: 5.0 }),
729            CoordPos::Inside
730        );
731        assert_eq!(
732            triangle.coordinate_position(&coord! { x: 2.5, y: 5.0 }),
733            CoordPos::OnBoundary
734        );
735        assert_eq!(
736            triangle.coordinate_position(&coord! { x: 5.0, y: 10.0 }),
737            CoordPos::OnBoundary
738        );
739        assert_eq!(
740            triangle.coordinate_position(&coord! { x: 2.49, y: 5.0 }),
741            CoordPos::Outside
742        );
743    }
744
745    #[test]
746    fn test_right_triangle() {
747        let triangle: Triangle = wkt!(TRIANGLE(0. 0.,10. 0.,10. 10.));
748        assert_eq!(
749            triangle.coordinate_position(&coord!(x: 0., y: 0.)),
750            CoordPos::OnBoundary
751        );
752        assert_eq!(
753            triangle.coordinate_position(&coord!(x: 10., y: 5.)),
754            CoordPos::OnBoundary
755        );
756    }
757
758    #[test]
759    fn test_collection() {
760        let triangle = Triangle::new((0.0, 0.0).into(), (5.0, 10.0).into(), (10.0, 0.0).into());
761        let rect = Rect::new((0.0, 0.0), (10.0, 10.0));
762        let collection = GeometryCollection::new_from(vec![triangle.into(), rect.into()]);
763
764        //  outside of both
765        assert_eq!(
766            collection.coordinate_position(&coord! { x: 15.0, y: 15.0 }),
767            CoordPos::Outside
768        );
769
770        // inside both
771        assert_eq!(
772            collection.coordinate_position(&coord! { x: 5.0, y: 5.0 }),
773            CoordPos::Inside
774        );
775
776        // inside one, boundary of other
777        assert_eq!(
778            collection.coordinate_position(&coord! { x: 2.5, y: 5.0 }),
779            CoordPos::OnBoundary
780        );
781
782        //  boundary of both
783        assert_eq!(
784            collection.coordinate_position(&coord! { x: 5.0, y: 10.0 }),
785            CoordPos::Outside
786        );
787    }
788}