Skip to main content

oxigdal_algorithms/vector/
de9im.rs

1//! DE-9IM (Dimensionally Extended 9-Intersection Model) spatial relationships
2//!
3//! Implements the OGC DE-9IM model for computing topological relationships
4//! between geometry pairs. The DE-9IM matrix encodes relationships between
5//! the Interior (I), Boundary (B), and Exterior (E) of two geometries.
6//!
7//! # Matrix Layout
8//!
9//! ```text
10//!        Interior(b)  Boundary(b)  Exterior(b)
11//! I(a)   [0]=II       [1]=IB       [2]=IE
12//! B(a)   [3]=BI       [4]=BB       [5]=BE
13//! E(a)   [6]=EI       [7]=EB       [8]=EE
14//! ```
15//!
16//! # Named Predicates (OGC SF)
17//!
18//! - **Equals**: `T*F**FFF*`
19//! - **Disjoint**: `FF*FF****`
20//! - **Intersects**: NOT Disjoint
21//! - **Touches**: `FT*******` OR `F**T*****` OR `F***T****`
22//! - **Crosses**: dimension-dependent patterns
23//! - **Within**: `T*F**F***`
24//! - **Contains**: `T*****FF*`
25//! - **Overlaps**: dimension-dependent patterns
26//! - **Covers**: `T*****FF*` OR `*T****FF*` OR `***T**FF*` OR `****T*FF*`
27//! - **CoveredBy**: `T*F**F***` OR `*TF**F***` OR `**FT*F***` OR `**F*TF***`
28//!
29//! # Examples
30//!
31//! ```
32//! use oxigdal_algorithms::vector::de9im::{De9im, Dimension};
33//!
34//! // Build a matrix for overlapping polygons: "212101212"
35//! let matrix = De9im::new([
36//!     Dimension::Area,   // II
37//!     Dimension::Line,   // IB
38//!     Dimension::Area,   // IE
39//!     Dimension::Line,   // BI
40//!     Dimension::Point,  // BB
41//!     Dimension::Line,   // BE
42//!     Dimension::Area,   // EI
43//!     Dimension::Line,   // EB
44//!     Dimension::Area,   // EE
45//! ]);
46//! assert!(matrix.is_overlaps(2, 2));
47//! assert!(!matrix.is_disjoint());
48//! assert!(matrix.matches("2*2***2*2"));
49//! ```
50
51use crate::error::{AlgorithmError, Result};
52use crate::vector::contains::{
53    point_in_polygon_or_boundary, point_on_polygon_boundary, point_strictly_inside_polygon,
54};
55use crate::vector::intersection::SegmentIntersection;
56use crate::vector::intersection::intersect_segment_segment;
57use oxigdal_core::vector::{Coordinate, LineString, Point, Polygon};
58
59use core::fmt;
60
61// ---------------------------------------------------------------------------
62// Dimension enum
63// ---------------------------------------------------------------------------
64
65/// Dimension of a geometric intersection component in the DE-9IM model.
66///
67/// Each cell in the 3x3 matrix records the maximum dimension of the
68/// intersection between parts (Interior, Boundary, Exterior) of two geometries.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
70pub enum Dimension {
71    /// The intersection is empty (F in the matrix string, value -1)
72    Empty,
73    /// The intersection is a point or set of points (0 in the matrix string)
74    Point,
75    /// The intersection is a line or set of lines (1 in the matrix string)
76    Line,
77    /// The intersection is an area / surface (2 in the matrix string)
78    Area,
79    /// Wildcard used for pattern matching only -- never stored in a real matrix
80    DontCare,
81}
82
83impl Dimension {
84    /// Returns the character used in the standard DE-9IM string representation.
85    #[must_use]
86    pub fn to_char(self) -> char {
87        match self {
88            Self::Empty => 'F',
89            Self::Point => '0',
90            Self::Line => '1',
91            Self::Area => '2',
92            Self::DontCare => '*',
93        }
94    }
95
96    /// Parse a single character from a DE-9IM string.
97    pub fn from_char(c: char) -> Result<Self> {
98        match c {
99            'F' | 'f' => Ok(Self::Empty),
100            '0' => Ok(Self::Point),
101            '1' => Ok(Self::Line),
102            '2' => Ok(Self::Area),
103            '*' => Ok(Self::DontCare),
104            'T' | 't' => Ok(Self::DontCare), // 'T' is a pattern char meaning "non-Empty"
105            _ => Err(AlgorithmError::InvalidInput(format!(
106                "invalid DE-9IM character: '{c}'"
107            ))),
108        }
109    }
110
111    /// Returns `true` when `self` matches pattern dimension `pat`.
112    ///
113    /// | Pattern | Matches |
114    /// |---------|---------|
115    /// | `*`     | any     |
116    /// | `T`     | Point, Line, Area (non-Empty) |
117    /// | `F`     | Empty   |
118    /// | `0`     | Point   |
119    /// | `1`     | Line    |
120    /// | `2`     | Area    |
121    fn matches_pattern(self, pat: char) -> bool {
122        match pat {
123            '*' => true,
124            'T' | 't' => self != Self::Empty,
125            'F' | 'f' => self == Self::Empty,
126            '0' => self == Self::Point,
127            '1' => self == Self::Line,
128            '2' => self == Self::Area,
129            _ => false,
130        }
131    }
132}
133
134impl fmt::Display for Dimension {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        write!(f, "{}", self.to_char())
137    }
138}
139
140// ---------------------------------------------------------------------------
141// De9im struct
142// ---------------------------------------------------------------------------
143
144/// A DE-9IM (Dimensionally Extended 9-Intersection Model) matrix.
145///
146/// The nine cells are stored in row-major order:
147///
148/// ```text
149/// [II, IB, IE,  BI, BB, BE,  EI, EB, EE]
150/// ```
151///
152/// where `I` = Interior, `B` = Boundary, `E` = Exterior,
153/// and the first letter refers to geometry **a**, the second to geometry **b**.
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
155pub struct De9im {
156    cells: [Dimension; 9],
157}
158
159impl De9im {
160    // -- cell index constants --
161    /// Interior-Interior
162    pub const II: usize = 0;
163    /// Interior-Boundary
164    pub const IB: usize = 1;
165    /// Interior-Exterior
166    pub const IE: usize = 2;
167    /// Boundary-Interior
168    pub const BI: usize = 3;
169    /// Boundary-Boundary
170    pub const BB: usize = 4;
171    /// Boundary-Exterior
172    pub const BE: usize = 5;
173    /// Exterior-Interior
174    pub const EI: usize = 6;
175    /// Exterior-Boundary
176    pub const EB: usize = 7;
177    /// Exterior-Exterior
178    pub const EE: usize = 8;
179
180    /// Create a matrix from an explicit 9-element array.
181    #[must_use]
182    pub const fn new(cells: [Dimension; 9]) -> Self {
183        Self { cells }
184    }
185
186    /// Parse a 9-character DE-9IM string such as `"212101212"`.
187    pub fn from_str(s: &str) -> Result<Self> {
188        let chars: Vec<char> = s.chars().collect();
189        if chars.len() != 9 {
190            return Err(AlgorithmError::InvalidInput(format!(
191                "DE-9IM string must be exactly 9 characters, got {}",
192                chars.len()
193            )));
194        }
195        let mut cells = [Dimension::Empty; 9];
196        for (i, &ch) in chars.iter().enumerate() {
197            cells[i] = Dimension::from_char(ch)?;
198        }
199        Ok(Self { cells })
200    }
201
202    /// Access a single cell by index (0..9).
203    #[must_use]
204    pub fn get(&self, index: usize) -> Dimension {
205        if index < 9 {
206            self.cells[index]
207        } else {
208            Dimension::Empty
209        }
210    }
211
212    /// Set a single cell by index (0..9).
213    pub fn set(&mut self, index: usize, dim: Dimension) {
214        if index < 9 {
215            self.cells[index] = dim;
216        }
217    }
218
219    /// Return the 9-character string form (e.g. `"212101212"`).
220    #[must_use]
221    pub fn to_string_repr(&self) -> String {
222        self.cells.iter().map(|d| d.to_char()).collect()
223    }
224
225    /// Return the transposed matrix (swap a <-> b).
226    #[must_use]
227    pub fn transpose(&self) -> Self {
228        Self::new([
229            self.cells[Self::II],
230            self.cells[Self::BI],
231            self.cells[Self::EI],
232            self.cells[Self::IB],
233            self.cells[Self::BB],
234            self.cells[Self::EB],
235            self.cells[Self::IE],
236            self.cells[Self::BE],
237            self.cells[Self::EE],
238        ])
239    }
240
241    // -----------------------------------------------------------------------
242    // Pattern matching
243    // -----------------------------------------------------------------------
244
245    /// Test whether this matrix matches a 9-character pattern string.
246    ///
247    /// Pattern characters: `T` (non-empty), `F` (empty), `*` (any),
248    /// `0` (Point), `1` (Line), `2` (Area).
249    #[must_use]
250    pub fn matches(&self, pattern: &str) -> bool {
251        let chars: Vec<char> = pattern.chars().collect();
252        if chars.len() != 9 {
253            return false;
254        }
255        self.cells
256            .iter()
257            .zip(chars.iter())
258            .all(|(dim, &pat)| dim.matches_pattern(pat))
259    }
260
261    // -----------------------------------------------------------------------
262    // Named OGC predicates
263    // -----------------------------------------------------------------------
264
265    /// OGC **Equals**: `T*F**FFF*`
266    ///
267    /// Two geometries are topologically equal when their interiors intersect
268    /// and no part of either geometry lies in the exterior of the other.
269    #[must_use]
270    pub fn is_equals(&self) -> bool {
271        self.matches("T*F**FFF*")
272    }
273
274    /// OGC **Disjoint**: `FF*FF****`
275    ///
276    /// Two geometries are disjoint when they share no points in common.
277    #[must_use]
278    pub fn is_disjoint(&self) -> bool {
279        self.matches("FF*FF****")
280    }
281
282    /// OGC **Intersects**: NOT Disjoint.
283    #[must_use]
284    pub fn is_intersects(&self) -> bool {
285        !self.is_disjoint()
286    }
287
288    /// OGC **Touches**: the geometries share boundary but not interior.
289    ///
290    /// Matches `FT*******` OR `F**T*****` OR `F***T****`.
291    #[must_use]
292    pub fn is_touches(&self) -> bool {
293        self.matches("FT*******") || self.matches("F**T*****") || self.matches("F***T****")
294    }
295
296    /// OGC **Crosses** (requires the topological dimensions of the two
297    /// geometries).
298    ///
299    /// - Point/Line, Point/Area, Line/Area (dim_a < dim_b): pattern `T*T******`
300    /// - Line/Line (dim_a == dim_b == 1): pattern `0********`
301    /// - Polygon/Polygon (dim_a == dim_b == 2): always `false` (undefined)
302    #[must_use]
303    pub fn is_crosses(&self, dim_a: u8, dim_b: u8) -> bool {
304        if dim_a < dim_b {
305            // lower-dim crosses higher-dim
306            self.matches("T*T******")
307        } else if dim_a > dim_b {
308            // reverse: same test on transposed matrix
309            self.transpose().matches("T*T******")
310        } else if dim_a == 1 && dim_b == 1 {
311            // Line/Line
312            self.matches("0********")
313        } else {
314            // Polygon/Polygon (same dim 2) -- crosses is undefined, return false
315            false
316        }
317    }
318
319    /// OGC **Within**: `T*F**F***`
320    ///
321    /// Geometry a is within geometry b when every point of a lies inside
322    /// (interior or boundary of) b and the interiors intersect.
323    #[must_use]
324    pub fn is_within(&self) -> bool {
325        self.matches("T*F**F***")
326    }
327
328    /// OGC **Contains**: `T*****FF*`
329    ///
330    /// Geometry a contains geometry b when within(b, a) is true.
331    #[must_use]
332    pub fn is_contains(&self) -> bool {
333        self.matches("T*****FF*")
334    }
335
336    /// OGC **Overlaps** (requires the topological dimensions of the two
337    /// geometries).
338    ///
339    /// - Point/Point or Area/Area (dim_a == dim_b, both != 1): `T*T***T**`
340    /// - Line/Line (dim_a == dim_b == 1): `1*T***T**`
341    /// - Different dimensions: always `false`
342    #[must_use]
343    pub fn is_overlaps(&self, dim_a: u8, dim_b: u8) -> bool {
344        if dim_a != dim_b {
345            return false;
346        }
347        if dim_a == 1 {
348            self.matches("1*T***T**")
349        } else {
350            self.matches("T*T***T**")
351        }
352    }
353
354    /// OGC **Covers**: `T*****FF*` OR `*T****FF*` OR `***T**FF*` OR `****T*FF*`
355    ///
356    /// Geometry a covers geometry b when every point of b is a point of a.
357    #[must_use]
358    pub fn is_covers(&self) -> bool {
359        self.matches("T*****FF*")
360            || self.matches("*T****FF*")
361            || self.matches("***T**FF*")
362            || self.matches("****T*FF*")
363    }
364
365    /// OGC **CoveredBy**: `T*F**F***` OR `*TF**F***` OR `**FT*F***` OR `**F*TF***`
366    ///
367    /// Geometry a is covered by geometry b when every point of a is a point of b.
368    #[must_use]
369    pub fn is_covered_by(&self) -> bool {
370        self.matches("T*F**F***")
371            || self.matches("*TF**F***")
372            || self.matches("**FT*F***")
373            || self.matches("**F*TF***")
374    }
375}
376
377impl fmt::Display for De9im {
378    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
379        write!(f, "{}", self.to_string_repr())
380    }
381}
382
383// ---------------------------------------------------------------------------
384// Relate: compute the DE-9IM matrix for geometry pairs
385// ---------------------------------------------------------------------------
386
387/// Compute the DE-9IM matrix for two polygons.
388///
389/// Classifies every topological pair (Interior, Boundary, Exterior) of a and b
390/// and records the maximum dimension of each intersection component.
391///
392/// # Errors
393///
394/// Returns an error if either polygon has fewer than 4 exterior coordinates.
395pub fn relate_polygons(a: &Polygon, b: &Polygon) -> Result<De9im> {
396    validate_polygon(a, "relate_polygons", "polygon a")?;
397    validate_polygon(b, "relate_polygons", "polygon b")?;
398
399    let mut matrix = [Dimension::Empty; 9];
400
401    // -- EE is always Area for polygons (infinite exterior meets infinite exterior)
402    matrix[De9im::EE] = Dimension::Area;
403
404    // -- Phase 1: Classify boundary vertices and edge midpoints --
405    let mut a_bdry_in_b_interior = false;
406    let mut a_bdry_on_b_boundary = false;
407    let mut a_bdry_in_b_exterior = false;
408
409    classify_boundary_against_polygon(
410        a,
411        b,
412        &mut a_bdry_in_b_interior,
413        &mut a_bdry_on_b_boundary,
414        &mut a_bdry_in_b_exterior,
415    );
416
417    let mut b_bdry_in_a_interior = false;
418    let mut b_bdry_on_a_boundary = false;
419    let mut b_bdry_in_a_exterior = false;
420
421    classify_boundary_against_polygon(
422        b,
423        a,
424        &mut b_bdry_in_a_interior,
425        &mut b_bdry_on_a_boundary,
426        &mut b_bdry_in_a_exterior,
427    );
428
429    // -- Phase 2: Interior classification --
430    // Sample points strictly inside each polygon and classify against the other.
431    // Also generate intersection-region sample points for robustness.
432    let mut a_int_in_b_interior = false;
433    let mut a_int_on_b_boundary = false;
434    let mut a_int_in_b_exterior = false;
435    let mut b_int_in_a_interior = false;
436    let mut b_int_on_a_boundary = false;
437    let mut b_int_in_a_exterior = false;
438
439    // Sample interior of A against B
440    let a_samples = interior_sample_points(a);
441    for pt in &a_samples {
442        classify_point_against_polygon(
443            pt,
444            b,
445            &mut a_int_in_b_interior,
446            &mut a_int_on_b_boundary,
447            &mut a_int_in_b_exterior,
448        );
449    }
450
451    // Sample interior of B against A
452    let b_samples = interior_sample_points(b);
453    for pt in &b_samples {
454        classify_point_against_polygon(
455            pt,
456            a,
457            &mut b_int_in_a_interior,
458            &mut b_int_on_a_boundary,
459            &mut b_int_in_a_exterior,
460        );
461    }
462
463    // Phase 2b: Generate additional samples near boundary intersection points.
464    // When boundary edges cross, sample points slightly offset from the crossing
465    // to detect the interior-interior overlap that vertex sampling might miss.
466    let cross_samples = generate_crossing_interior_samples(a, b);
467    for pt in &cross_samples {
468        // These points are designed to be strictly inside both polygons
469        if point_strictly_inside_polygon(pt, a) && point_strictly_inside_polygon(pt, b) {
470            a_int_in_b_interior = true;
471            b_int_in_a_interior = true;
472        }
473    }
474
475    // -- Phase 3: Boundary-Boundary intersection dimension --
476    let bb_dim = compute_boundary_boundary_dim(a, b);
477
478    // -- Phase 4: Fill matrix from classification flags --
479
480    // II: Interior(a) intersects Interior(b)
481    if a_int_in_b_interior || b_int_in_a_interior {
482        matrix[De9im::II] = Dimension::Area;
483    }
484
485    // IB: Interior(a) intersects Boundary(b)
486    // If boundary of B passes through interior of A, this is at least Line.
487    if b_bdry_in_a_interior || a_int_on_b_boundary {
488        matrix[De9im::IB] = Dimension::Line;
489    }
490
491    // IE: Interior(a) intersects Exterior(b)
492    if a_int_in_b_exterior {
493        matrix[De9im::IE] = Dimension::Area;
494    }
495
496    // BI: Boundary(a) intersects Interior(b)
497    if a_bdry_in_b_interior {
498        matrix[De9im::BI] = Dimension::Line;
499    }
500
501    // BB: Boundary(a) intersects Boundary(b)
502    if a_bdry_on_b_boundary || b_bdry_on_a_boundary || bb_dim != Dimension::Empty {
503        matrix[De9im::BB] = bb_dim;
504    }
505
506    // BE: Boundary(a) intersects Exterior(b)
507    if a_bdry_in_b_exterior {
508        matrix[De9im::BE] = Dimension::Line;
509    }
510
511    // EI: Exterior(a) intersects Interior(b)
512    if b_int_in_a_exterior {
513        matrix[De9im::EI] = Dimension::Area;
514    }
515
516    // EB: Exterior(a) intersects Boundary(b)
517    if b_bdry_in_a_exterior {
518        matrix[De9im::EB] = Dimension::Line;
519    }
520
521    Ok(De9im::new(matrix))
522}
523
524/// Compute the DE-9IM matrix for a point and a polygon.
525///
526/// # Errors
527///
528/// Returns an error if the polygon has fewer than 4 exterior coordinates.
529pub fn relate_point_polygon(pt: &Point, poly: &Polygon) -> Result<De9im> {
530    validate_polygon(poly, "relate_point_polygon", "polygon")?;
531
532    let coord = &pt.coord;
533    let mut matrix = [Dimension::Empty; 9];
534
535    // EE is always Area (exterior is 2D plane minus bounded geometry)
536    matrix[De9im::EE] = Dimension::Area;
537    // IE: Point interior is 0-dim; it is always in exterior of polygon or not
538    // EI: polygon interior is 2-dim area; exterior of point is everything else
539
540    // The point has no boundary (dimension 0 -> boundary is empty)
541    // so IB, BI, BB, BE, EB all depend only on whether the point falls in
542    // the polygon's interior, boundary, or exterior.
543
544    if point_on_polygon_boundary(coord, poly) {
545        // Point is on the polygon boundary
546        matrix[De9im::II] = Dimension::Empty; // point interior does not meet poly interior
547        matrix[De9im::IB] = Dimension::Point; // point interior meets poly boundary
548        matrix[De9im::IE] = Dimension::Empty; // point interior does not meet poly exterior
549        // Boundary of point is empty (dim 0 has no boundary), so BI=BB=BE=F
550        matrix[De9im::EI] = Dimension::Area; // exterior of point covers poly interior
551        matrix[De9im::EB] = Dimension::Line; // exterior of point covers rest of poly boundary
552    } else if point_strictly_inside_polygon(coord, poly) {
553        // Point is strictly inside the polygon
554        matrix[De9im::II] = Dimension::Point; // point interior meets poly interior
555        matrix[De9im::IB] = Dimension::Empty;
556        matrix[De9im::IE] = Dimension::Empty;
557        matrix[De9im::EI] = Dimension::Area;
558        matrix[De9im::EB] = Dimension::Line;
559    } else {
560        // Point is exterior to the polygon
561        matrix[De9im::II] = Dimension::Empty;
562        matrix[De9im::IB] = Dimension::Empty;
563        matrix[De9im::IE] = Dimension::Point;
564        matrix[De9im::EI] = Dimension::Area;
565        matrix[De9im::EB] = Dimension::Line;
566    }
567
568    Ok(De9im::new(matrix))
569}
570
571/// Compute the DE-9IM matrix for a line string and a polygon.
572///
573/// Walks each segment of the line and classifies it against the polygon's
574/// interior, boundary, and exterior.
575///
576/// # Errors
577///
578/// Returns an error if the polygon has fewer than 4 exterior coordinates or
579/// the line has fewer than 2 coordinates.
580pub fn relate_line_polygon(line: &LineString, poly: &Polygon) -> Result<De9im> {
581    validate_polygon(poly, "relate_line_polygon", "polygon")?;
582    if line.coords.len() < 2 {
583        return Err(AlgorithmError::InsufficientData {
584            operation: "relate_line_polygon",
585            message: "line must have at least 2 coordinates".to_string(),
586        });
587    }
588
589    let mut matrix = [Dimension::Empty; 9];
590    matrix[De9im::EE] = Dimension::Area;
591
592    // Classify each vertex and segment midpoint of the line against the polygon
593    let mut line_has_interior_in_poly_interior = false;
594    let mut line_has_interior_on_poly_boundary = false;
595    let mut line_has_interior_in_poly_exterior = false;
596
597    // Classify line vertices
598    for coord in &line.coords {
599        if point_on_polygon_boundary(coord, poly) {
600            line_has_interior_on_poly_boundary = true;
601        } else if point_strictly_inside_polygon(coord, poly) {
602            line_has_interior_in_poly_interior = true;
603        } else {
604            line_has_interior_in_poly_exterior = true;
605        }
606    }
607
608    // Classify segment midpoints for finer resolution
609    for i in 0..line.coords.len().saturating_sub(1) {
610        let mid = midpoint(&line.coords[i], &line.coords[i + 1]);
611        if point_on_polygon_boundary(&mid, poly) {
612            line_has_interior_on_poly_boundary = true;
613        } else if point_strictly_inside_polygon(&mid, poly) {
614            line_has_interior_in_poly_interior = true;
615        } else {
616            line_has_interior_in_poly_exterior = true;
617        }
618    }
619
620    // Line endpoints are the boundary of a LineString (if not closed)
621    let line_is_closed = line.coords.len() >= 3
622        && coords_equal(&line.coords[0], &line.coords[line.coords.len() - 1]);
623
624    let mut line_boundary_in_poly_interior = false;
625    let mut line_boundary_on_poly_boundary = false;
626    let mut line_boundary_in_poly_exterior = false;
627
628    if !line_is_closed && line.coords.len() >= 2 {
629        let endpoints = [&line.coords[0], &line.coords[line.coords.len() - 1]];
630        for ep in &endpoints {
631            if point_on_polygon_boundary(ep, poly) {
632                line_boundary_on_poly_boundary = true;
633            } else if point_strictly_inside_polygon(ep, poly) {
634                line_boundary_in_poly_interior = true;
635            } else {
636                line_boundary_in_poly_exterior = true;
637            }
638        }
639    }
640
641    // Check if the polygon boundary intersects the line
642    let boundary_intersections = count_boundary_line_intersections(line, poly);
643
644    // II: line interior meets polygon interior
645    if line_has_interior_in_poly_interior {
646        matrix[De9im::II] = Dimension::Line;
647    }
648
649    // IB: line interior meets polygon boundary
650    if line_has_interior_on_poly_boundary || boundary_intersections > 0 {
651        if has_segment_on_polygon_boundary(line, poly) {
652            matrix[De9im::IB] = Dimension::Line;
653        } else {
654            matrix[De9im::IB] = Dimension::Point;
655        }
656    }
657
658    // IE: line interior in polygon exterior
659    if line_has_interior_in_poly_exterior {
660        matrix[De9im::IE] = Dimension::Line;
661    }
662
663    // BI: line boundary (endpoints) meets polygon interior
664    if line_boundary_in_poly_interior {
665        matrix[De9im::BI] = Dimension::Point;
666    }
667
668    // BB: line boundary meets polygon boundary
669    if line_boundary_on_poly_boundary {
670        matrix[De9im::BB] = Dimension::Point;
671    }
672
673    // BE: line boundary in polygon exterior
674    if line_boundary_in_poly_exterior {
675        matrix[De9im::BE] = Dimension::Point;
676    }
677
678    // EI: polygon interior meets line exterior -- always Area if polygon exists
679    matrix[De9im::EI] = Dimension::Area;
680
681    // EB: polygon boundary meets line exterior -- always Line
682    matrix[De9im::EB] = Dimension::Line;
683
684    Ok(De9im::new(matrix))
685}
686
687/// High-level relate dispatcher for Polygon-Polygon, Point-Polygon, Line-Polygon.
688///
689/// # Errors
690///
691/// Returns an error if geometries are invalid or the combination is unsupported.
692pub fn relate(a: &Polygon, b: &Polygon) -> Result<De9im> {
693    relate_polygons(a, b)
694}
695
696// ---------------------------------------------------------------------------
697// New predicate traits (Equals, Covers, CoveredBy)
698// ---------------------------------------------------------------------------
699
700/// Trait for geometries that support the **Equals** predicate.
701pub trait EqualsPredicate {
702    /// Tests if this geometry is topologically equal to another.
703    fn equals_topo(&self, other: &Self) -> Result<bool>;
704}
705
706/// Trait for geometries that support the **Covers** predicate.
707pub trait CoversPredicate {
708    /// Tests if this geometry covers another (every point of other is a point of self).
709    fn covers(&self, other: &Self) -> Result<bool>;
710}
711
712/// Trait for geometries that support the **CoveredBy** predicate.
713pub trait CoveredByPredicate {
714    /// Tests if this geometry is covered by another.
715    fn covered_by(&self, other: &Self) -> Result<bool>;
716}
717
718// Polygon implementations delegating to DE-9IM
719
720impl EqualsPredicate for Polygon {
721    fn equals_topo(&self, other: &Self) -> Result<bool> {
722        let m = relate_polygons(self, other)?;
723        Ok(m.is_equals())
724    }
725}
726
727impl CoversPredicate for Polygon {
728    fn covers(&self, other: &Self) -> Result<bool> {
729        let m = relate_polygons(self, other)?;
730        Ok(m.is_covers())
731    }
732}
733
734impl CoveredByPredicate for Polygon {
735    fn covered_by(&self, other: &Self) -> Result<bool> {
736        let m = relate_polygons(self, other)?;
737        Ok(m.is_covered_by())
738    }
739}
740
741// Point implementations
742
743impl EqualsPredicate for Point {
744    fn equals_topo(&self, other: &Self) -> Result<bool> {
745        Ok(coords_equal(&self.coord, &other.coord))
746    }
747}
748
749impl CoversPredicate for Point {
750    fn covers(&self, other: &Self) -> Result<bool> {
751        // A point covers another point iff they are equal
752        Ok(coords_equal(&self.coord, &other.coord))
753    }
754}
755
756impl CoveredByPredicate for Point {
757    fn covered_by(&self, other: &Self) -> Result<bool> {
758        Ok(coords_equal(&self.coord, &other.coord))
759    }
760}
761
762// ---------------------------------------------------------------------------
763// Helper functions
764// ---------------------------------------------------------------------------
765
766/// Validate a polygon has sufficient coordinates.
767fn validate_polygon(poly: &Polygon, operation: &'static str, name: &str) -> Result<()> {
768    if poly.exterior.coords.len() < 4 {
769        return Err(AlgorithmError::InsufficientData {
770            operation,
771            message: format!("{name} exterior must have at least 4 coordinates"),
772        });
773    }
774    Ok(())
775}
776
777/// Test coordinate equality within epsilon.
778fn coords_equal(a: &Coordinate, b: &Coordinate) -> bool {
779    (a.x - b.x).abs() < f64::EPSILON && (a.y - b.y).abs() < f64::EPSILON
780}
781
782/// Compute the midpoint of two coordinates.
783fn midpoint(a: &Coordinate, b: &Coordinate) -> Coordinate {
784    Coordinate::new_2d((a.x + b.x) * 0.5, (a.y + b.y) * 0.5)
785}
786
787/// Classify a point against a polygon as interior/boundary/exterior and set flags.
788fn classify_point_against_polygon(
789    pt: &Coordinate,
790    poly: &Polygon,
791    in_interior: &mut bool,
792    on_boundary: &mut bool,
793    in_exterior: &mut bool,
794) {
795    if point_on_polygon_boundary(pt, poly) {
796        *on_boundary = true;
797    } else if point_strictly_inside_polygon(pt, poly) {
798        *in_interior = true;
799    } else {
800        *in_exterior = true;
801    }
802}
803
804/// Classify boundary vertices and edge midpoints of `source` against `target`.
805fn classify_boundary_against_polygon(
806    source: &Polygon,
807    target: &Polygon,
808    in_interior: &mut bool,
809    on_boundary: &mut bool,
810    in_exterior: &mut bool,
811) {
812    // Classify each vertex
813    for coord in &source.exterior.coords {
814        classify_point_against_polygon(coord, target, in_interior, on_boundary, in_exterior);
815    }
816    // Classify edge midpoints for finer resolution
817    for i in 0..source.exterior.coords.len().saturating_sub(1) {
818        let mid = midpoint(&source.exterior.coords[i], &source.exterior.coords[i + 1]);
819        classify_point_against_polygon(&mid, target, in_interior, on_boundary, in_exterior);
820    }
821}
822
823/// Generate sample points near boundary crossings that are likely strictly
824/// inside both polygons.
825///
826/// When two polygon boundaries cross at a point, the four quadrants around
827/// that crossing alternate between "inside both", "inside A only",
828/// "inside B only", and "outside both". We nudge slightly along the bisector
829/// of the crossing edge normals to find the "inside both" quadrant.
830fn generate_crossing_interior_samples(a: &Polygon, b: &Polygon) -> Vec<Coordinate> {
831    let mut samples = Vec::new();
832    let a_coords = &a.exterior.coords;
833    let b_coords = &b.exterior.coords;
834    let eps = 1e-6;
835
836    for i in 0..a_coords.len().saturating_sub(1) {
837        for j in 0..b_coords.len().saturating_sub(1) {
838            if let SegmentIntersection::Point(pt) = intersect_segment_segment(
839                &a_coords[i],
840                &a_coords[i + 1],
841                &b_coords[j],
842                &b_coords[j + 1],
843            ) {
844                // Compute inward-pointing normals of each edge
845                let a_dx = a_coords[i + 1].x - a_coords[i].x;
846                let a_dy = a_coords[i + 1].y - a_coords[i].y;
847                let b_dx = b_coords[j + 1].x - b_coords[j].x;
848                let b_dy = b_coords[j + 1].y - b_coords[j].y;
849
850                // Inward normal candidates (both orientations)
851                let normals = [(a_dy, -a_dx), (-a_dy, a_dx), (b_dy, -b_dx), (-b_dy, b_dx)];
852
853                // Try combinations of normal directions to find inside-both
854                for &(nx_a, ny_a) in &normals[..2] {
855                    for &(nx_b, ny_b) in &normals[2..] {
856                        let nx = nx_a + nx_b;
857                        let ny = ny_a + ny_b;
858                        let len = (nx * nx + ny * ny).sqrt();
859                        if len < f64::EPSILON {
860                            continue;
861                        }
862                        let candidate =
863                            Coordinate::new_2d(pt.x + eps * nx / len, pt.y + eps * ny / len);
864                        if point_strictly_inside_polygon(&candidate, a)
865                            && point_strictly_inside_polygon(&candidate, b)
866                        {
867                            samples.push(candidate);
868                        }
869                    }
870                }
871            }
872        }
873        if samples.len() >= 4 {
874            break;
875        }
876    }
877    samples
878}
879
880/// Generate sample points known to be in the interior of a polygon.
881///
882/// Uses edge midpoints projected slightly inward, and the centroid as a
883/// fallback. This is not bulletproof for extremely non-convex shapes but
884/// covers the vast majority of practical geometries.
885fn interior_sample_points(poly: &Polygon) -> Vec<Coordinate> {
886    let mut samples = Vec::new();
887    let n = poly.exterior.coords.len();
888    if n < 4 {
889        return samples;
890    }
891
892    // Centroid of the exterior ring (arithmetic mean of vertices)
893    let (mut cx, mut cy) = (0.0_f64, 0.0_f64);
894    // Exclude the closing vertex (last == first)
895    let vertex_count = n - 1;
896    for coord in &poly.exterior.coords[..vertex_count] {
897        cx += coord.x;
898        cy += coord.y;
899    }
900    if vertex_count > 0 {
901        cx /= vertex_count as f64;
902        cy /= vertex_count as f64;
903    }
904    let centroid = Coordinate::new_2d(cx, cy);
905
906    if point_strictly_inside_polygon(&centroid, poly) {
907        samples.push(centroid);
908    }
909
910    // Edge midpoints pulled slightly toward centroid
911    for i in 0..vertex_count {
912        let mid = midpoint(&poly.exterior.coords[i], &poly.exterior.coords[i + 1]);
913        // Pull 10% toward centroid
914        let pulled = Coordinate::new_2d(mid.x + (cx - mid.x) * 0.1, mid.y + (cy - mid.y) * 0.1);
915        if point_strictly_inside_polygon(&pulled, poly) {
916            samples.push(pulled);
917            if samples.len() >= 5 {
918                break;
919            }
920        }
921    }
922
923    // Fallback: if no sample found, try a point grid inside the bounding box
924    if samples.is_empty() {
925        if let Some((min_x, min_y, max_x, max_y)) = poly.bounds() {
926            let step_x = (max_x - min_x) / 5.0;
927            let step_y = (max_y - min_y) / 5.0;
928            'outer: for ix in 1..5 {
929                for iy in 1..5 {
930                    let pt = Coordinate::new_2d(
931                        min_x + step_x * (ix as f64),
932                        min_y + step_y * (iy as f64),
933                    );
934                    if point_strictly_inside_polygon(&pt, poly) {
935                        samples.push(pt);
936                        if samples.len() >= 3 {
937                            break 'outer;
938                        }
939                    }
940                }
941            }
942        }
943    }
944
945    samples
946}
947
948/// Compute the dimension of the Boundary-Boundary intersection between two polygons.
949///
950/// - No intersection -> Empty
951/// - Finite intersection points only -> Point
952/// - Collinear edge overlaps -> Line
953fn compute_boundary_boundary_dim(a: &Polygon, b: &Polygon) -> Dimension {
954    let a_coords = &a.exterior.coords;
955    let b_coords = &b.exterior.coords;
956
957    let mut has_point_intersection = false;
958    let mut has_line_intersection = false;
959
960    for i in 0..a_coords.len().saturating_sub(1) {
961        for j in 0..b_coords.len().saturating_sub(1) {
962            match intersect_segment_segment(
963                &a_coords[i],
964                &a_coords[i + 1],
965                &b_coords[j],
966                &b_coords[j + 1],
967            ) {
968                SegmentIntersection::Point(_) => {
969                    has_point_intersection = true;
970                }
971                SegmentIntersection::Overlap(_, _) => {
972                    has_line_intersection = true;
973                }
974                SegmentIntersection::None => {}
975            }
976        }
977    }
978
979    if has_line_intersection {
980        Dimension::Line
981    } else if has_point_intersection {
982        Dimension::Point
983    } else {
984        Dimension::Empty
985    }
986}
987
988/// Count the number of intersection points between a line and the polygon boundary.
989fn count_boundary_line_intersections(line: &LineString, poly: &Polygon) -> usize {
990    let mut count = 0;
991    let ring = &poly.exterior.coords;
992    for i in 0..line.coords.len().saturating_sub(1) {
993        for j in 0..ring.len().saturating_sub(1) {
994            match intersect_segment_segment(
995                &line.coords[i],
996                &line.coords[i + 1],
997                &ring[j],
998                &ring[j + 1],
999            ) {
1000                SegmentIntersection::Point(_) | SegmentIntersection::Overlap(_, _) => {
1001                    count += 1;
1002                }
1003                SegmentIntersection::None => {}
1004            }
1005        }
1006    }
1007    count
1008}
1009
1010/// Check if any segment of the line lies entirely on the polygon boundary.
1011fn has_segment_on_polygon_boundary(line: &LineString, poly: &Polygon) -> bool {
1012    let ring = &poly.exterior.coords;
1013    for i in 0..line.coords.len().saturating_sub(1) {
1014        for j in 0..ring.len().saturating_sub(1) {
1015            if let SegmentIntersection::Overlap(_, _) = intersect_segment_segment(
1016                &line.coords[i],
1017                &line.coords[i + 1],
1018                &ring[j],
1019                &ring[j + 1],
1020            ) {
1021                return true;
1022            }
1023        }
1024    }
1025    false
1026}
1027
1028// ---------------------------------------------------------------------------
1029// Tests
1030// ---------------------------------------------------------------------------
1031
1032#[cfg(test)]
1033mod tests {
1034    use super::*;
1035    use crate::error::AlgorithmError;
1036
1037    type TestResult = core::result::Result<(), Box<dyn std::error::Error>>;
1038
1039    /// Helper: create a square polygon from (x0,y0) to (x1,y1).
1040    fn make_rect(x0: f64, y0: f64, x1: f64, y1: f64) -> Result<Polygon> {
1041        let coords = vec![
1042            Coordinate::new_2d(x0, y0),
1043            Coordinate::new_2d(x1, y0),
1044            Coordinate::new_2d(x1, y1),
1045            Coordinate::new_2d(x0, y1),
1046            Coordinate::new_2d(x0, y0),
1047        ];
1048        let ext = LineString::new(coords).map_err(AlgorithmError::Core)?;
1049        Polygon::new(ext, vec![]).map_err(AlgorithmError::Core)
1050    }
1051
1052    // =======================================================================
1053    // Dimension / De9im unit tests
1054    // =======================================================================
1055
1056    #[test]
1057    fn test_dimension_to_char() {
1058        assert_eq!(Dimension::Empty.to_char(), 'F');
1059        assert_eq!(Dimension::Point.to_char(), '0');
1060        assert_eq!(Dimension::Line.to_char(), '1');
1061        assert_eq!(Dimension::Area.to_char(), '2');
1062        assert_eq!(Dimension::DontCare.to_char(), '*');
1063    }
1064
1065    #[test]
1066    fn test_dimension_from_char() -> TestResult {
1067        assert_eq!(Dimension::from_char('F')?, Dimension::Empty);
1068        assert_eq!(Dimension::from_char('0')?, Dimension::Point);
1069        assert_eq!(Dimension::from_char('1')?, Dimension::Line);
1070        assert_eq!(Dimension::from_char('2')?, Dimension::Area);
1071        assert_eq!(Dimension::from_char('*')?, Dimension::DontCare);
1072        assert!(Dimension::from_char('X').is_err());
1073        Ok(())
1074    }
1075
1076    #[test]
1077    fn test_de9im_from_str() -> TestResult {
1078        let m = De9im::from_str("212101212")?;
1079        assert_eq!(m.get(De9im::II), Dimension::Area);
1080        assert_eq!(m.get(De9im::IB), Dimension::Line);
1081        assert_eq!(m.get(De9im::IE), Dimension::Area);
1082        assert_eq!(m.get(De9im::BI), Dimension::Line);
1083        assert_eq!(m.get(De9im::BB), Dimension::Point);
1084        assert_eq!(m.get(De9im::BE), Dimension::Line);
1085        assert_eq!(m.get(De9im::EI), Dimension::Area);
1086        assert_eq!(m.get(De9im::EB), Dimension::Line);
1087        assert_eq!(m.get(De9im::EE), Dimension::Area);
1088        Ok(())
1089    }
1090
1091    #[test]
1092    fn test_de9im_display() -> TestResult {
1093        let m = De9im::from_str("212101212")?;
1094        assert_eq!(format!("{m}"), "212101212");
1095        Ok(())
1096    }
1097
1098    #[test]
1099    fn test_de9im_matches_basic() -> TestResult {
1100        let m = De9im::from_str("212101212")?;
1101        assert!(m.matches("2*2***2*2")); // wildcard
1102        assert!(m.matches("T*T***T**")); // T matches non-empty
1103        assert!(!m.matches("FF*FF****")); // disjoint pattern - should not match
1104        Ok(())
1105    }
1106
1107    #[test]
1108    fn test_de9im_transpose() -> TestResult {
1109        let m = De9im::from_str("212101212")?;
1110        let t = m.transpose();
1111        // Transpose swaps rows and columns:
1112        // II stays, IB<->BI, IE<->EI, BB stays, BE<->EB, EE stays
1113        assert_eq!(t.get(De9im::II), Dimension::Area); // same
1114        assert_eq!(t.get(De9im::IB), Dimension::Line); // was BI=1
1115        assert_eq!(t.get(De9im::IE), Dimension::Area); // was EI=2
1116        assert_eq!(t.get(De9im::BI), Dimension::Line); // was IB=1
1117        assert_eq!(t.get(De9im::BB), Dimension::Point); // same
1118        assert_eq!(t.get(De9im::BE), Dimension::Line); // was EB=1
1119        assert_eq!(t.get(De9im::EI), Dimension::Area); // was IE=2
1120        assert_eq!(t.get(De9im::EB), Dimension::Line); // was BE=1
1121        assert_eq!(t.get(De9im::EE), Dimension::Area); // same
1122        Ok(())
1123    }
1124
1125    #[test]
1126    fn test_de9im_from_str_invalid_length() {
1127        assert!(De9im::from_str("212").is_err());
1128        assert!(De9im::from_str("2121012121").is_err());
1129    }
1130
1131    #[test]
1132    fn test_de9im_get_out_of_bounds() {
1133        let m = De9im::new([Dimension::Empty; 9]);
1134        assert_eq!(m.get(99), Dimension::Empty);
1135    }
1136
1137    // =======================================================================
1138    // Named predicate tests on synthetic matrices
1139    // =======================================================================
1140
1141    #[test]
1142    fn test_is_equals_synthetic() -> TestResult {
1143        // T*F**FFF* -> equals
1144        let m = De9im::from_str("2FFF1FFF2")?;
1145        assert!(m.is_equals());
1146        // Overlapping polygons should not be equals
1147        let m2 = De9im::from_str("212101212")?;
1148        assert!(!m2.is_equals());
1149        Ok(())
1150    }
1151
1152    #[test]
1153    fn test_is_disjoint_synthetic() -> TestResult {
1154        let m = De9im::from_str("FF2FF1212")?;
1155        assert!(m.is_disjoint());
1156        assert!(!m.is_intersects());
1157
1158        let m2 = De9im::from_str("212101212")?;
1159        assert!(!m2.is_disjoint());
1160        assert!(m2.is_intersects());
1161        Ok(())
1162    }
1163
1164    #[test]
1165    fn test_is_touches_synthetic() -> TestResult {
1166        // FT******* pattern (boundary contact, no interior contact)
1167        let m = De9im::from_str("F11FF0212")?;
1168        assert!(m.is_touches());
1169
1170        let m2 = De9im::from_str("212101212")?;
1171        assert!(!m2.is_touches());
1172        Ok(())
1173    }
1174
1175    #[test]
1176    fn test_is_crosses_synthetic() -> TestResult {
1177        // Line/Polygon crossing: T*T****** with dim_a=1, dim_b=2
1178        let m = De9im::from_str("1020F1102")?;
1179        assert!(m.is_crosses(1, 2));
1180        // Polygon/Polygon: always false
1181        assert!(!m.is_crosses(2, 2));
1182
1183        // Line/Line crossing: 0********
1184        let m2 = De9im::from_str("0FFFFFFFF")?;
1185        assert!(m2.is_crosses(1, 1));
1186        Ok(())
1187    }
1188
1189    #[test]
1190    fn test_is_within_synthetic() -> TestResult {
1191        // T*F**F***
1192        let m = De9im::from_str("2FF1FF212")?;
1193        assert!(m.is_within());
1194        assert!(!m.is_contains()); // within != contains
1195        Ok(())
1196    }
1197
1198    #[test]
1199    fn test_is_contains_synthetic() -> TestResult {
1200        // T*****FF*
1201        let m = De9im::from_str("212101FF2")?;
1202        assert!(m.is_contains());
1203        Ok(())
1204    }
1205
1206    #[test]
1207    fn test_is_overlaps_synthetic() -> TestResult {
1208        // Area/Area: T*T***T**
1209        let m = De9im::from_str("212101212")?;
1210        assert!(m.is_overlaps(2, 2));
1211        // Different dimensions: false
1212        assert!(!m.is_overlaps(1, 2));
1213
1214        // Line/Line: 1*T***T**
1215        let m2 = De9im::from_str("1FT1FFT1F")?;
1216        assert!(m2.is_overlaps(1, 1));
1217        Ok(())
1218    }
1219
1220    #[test]
1221    fn test_is_covers_synthetic() -> TestResult {
1222        // T*****FF*
1223        let m = De9im::from_str("2FF1FFFF2")?;
1224        assert!(m.is_covers());
1225        Ok(())
1226    }
1227
1228    #[test]
1229    fn test_is_covered_by_synthetic() -> TestResult {
1230        // T*F**F***
1231        let m = De9im::from_str("2FF0FF212")?;
1232        assert!(m.is_covered_by());
1233        Ok(())
1234    }
1235
1236    // =======================================================================
1237    // relate_polygons: geometric tests
1238    // =======================================================================
1239
1240    #[test]
1241    fn test_relate_disjoint_squares() -> TestResult {
1242        let a = make_rect(0.0, 0.0, 4.0, 4.0)?;
1243        let b = make_rect(10.0, 10.0, 14.0, 14.0)?;
1244        let m = relate_polygons(&a, &b)?;
1245        assert!(m.is_disjoint(), "disjoint squares: matrix = {m}");
1246        assert!(!m.is_intersects());
1247        // Exact matrix should be FF2FF1212
1248        assert_eq!(m.get(De9im::II), Dimension::Empty);
1249        assert_eq!(m.get(De9im::EE), Dimension::Area);
1250        Ok(())
1251    }
1252
1253    #[test]
1254    fn test_relate_overlapping_squares() -> TestResult {
1255        let a = make_rect(0.0, 0.0, 4.0, 4.0)?;
1256        let b = make_rect(2.0, 2.0, 6.0, 6.0)?;
1257        let m = relate_polygons(&a, &b)?;
1258        assert!(m.is_intersects(), "overlapping squares: matrix = {m}");
1259        assert!(!m.is_disjoint());
1260        assert!(m.is_overlaps(2, 2), "overlapping squares: matrix = {m}");
1261        assert!(!m.is_contains());
1262        assert!(!m.is_within());
1263        // II should be Area (shared interior region)
1264        assert_eq!(
1265            m.get(De9im::II),
1266            Dimension::Area,
1267            "II = {}",
1268            m.get(De9im::II)
1269        );
1270        // IE should be Area (part of a's interior outside b)
1271        assert_eq!(
1272            m.get(De9im::IE),
1273            Dimension::Area,
1274            "IE = {}",
1275            m.get(De9im::IE)
1276        );
1277        // EI should be Area (part of b's interior outside a)
1278        assert_eq!(
1279            m.get(De9im::EI),
1280            Dimension::Area,
1281            "EI = {}",
1282            m.get(De9im::EI)
1283        );
1284        Ok(())
1285    }
1286
1287    #[test]
1288    fn test_relate_contained_square() -> TestResult {
1289        // b is strictly inside a
1290        let a = make_rect(0.0, 0.0, 10.0, 10.0)?;
1291        let b = make_rect(2.0, 2.0, 8.0, 8.0)?;
1292        let m = relate_polygons(&a, &b)?;
1293        assert!(m.is_contains(), "a contains b: matrix = {m}");
1294        // Transpose should give within
1295        let mt = m.transpose();
1296        assert!(mt.is_within(), "b within a: transposed matrix = {mt}");
1297        Ok(())
1298    }
1299
1300    #[test]
1301    fn test_relate_touching_squares() -> TestResult {
1302        // Two squares sharing an edge at x=4
1303        let a = make_rect(0.0, 0.0, 4.0, 4.0)?;
1304        let b = make_rect(4.0, 0.0, 8.0, 4.0)?;
1305        let m = relate_polygons(&a, &b)?;
1306        assert!(m.is_touches(), "touching squares: matrix = {m}");
1307        assert!(!m.is_disjoint());
1308        assert!(!m.is_overlaps(2, 2));
1309        Ok(())
1310    }
1311
1312    #[test]
1313    fn test_relate_identical_polygons() -> TestResult {
1314        let a = make_rect(0.0, 0.0, 4.0, 4.0)?;
1315        let b = make_rect(0.0, 0.0, 4.0, 4.0)?;
1316        let m = relate_polygons(&a, &b)?;
1317        assert!(m.is_equals(), "identical polygons: matrix = {m}");
1318        assert!(!m.is_disjoint());
1319        Ok(())
1320    }
1321
1322    // =======================================================================
1323    // relate_point_polygon tests
1324    // =======================================================================
1325
1326    #[test]
1327    fn test_relate_point_inside_polygon() -> TestResult {
1328        let poly = make_rect(0.0, 0.0, 10.0, 10.0)?;
1329        let pt = Point::new(5.0, 5.0);
1330        let m = relate_point_polygon(&pt, &poly)?;
1331        assert!(m.is_within(), "point inside polygon: matrix = {m}");
1332        Ok(())
1333    }
1334
1335    #[test]
1336    fn test_relate_point_on_boundary() -> TestResult {
1337        let poly = make_rect(0.0, 0.0, 10.0, 10.0)?;
1338        let pt = Point::new(0.0, 5.0);
1339        let m = relate_point_polygon(&pt, &poly)?;
1340        // Point on boundary should give touches: F0FFFF102 or similar
1341        assert!(m.is_touches(), "point on boundary: matrix = {m}");
1342        Ok(())
1343    }
1344
1345    #[test]
1346    fn test_relate_point_outside_polygon() -> TestResult {
1347        let poly = make_rect(0.0, 0.0, 10.0, 10.0)?;
1348        let pt = Point::new(20.0, 20.0);
1349        let m = relate_point_polygon(&pt, &poly)?;
1350        assert!(m.is_disjoint(), "point outside polygon: matrix = {m}");
1351        Ok(())
1352    }
1353
1354    // =======================================================================
1355    // relate_line_polygon tests
1356    // =======================================================================
1357
1358    #[test]
1359    fn test_relate_line_crossing_polygon() -> TestResult {
1360        // Line passes through polygon interior
1361        let poly = make_rect(0.0, 0.0, 10.0, 10.0)?;
1362        let line_coords = vec![Coordinate::new_2d(-5.0, 5.0), Coordinate::new_2d(15.0, 5.0)];
1363        let line = LineString::new(line_coords).map_err(AlgorithmError::Core)?;
1364        let m = relate_line_polygon(&line, &poly)?;
1365        // Line crosses polygon: dim_a=1 (line), dim_b=2 (polygon)
1366        assert!(m.is_crosses(1, 2), "line crossing polygon: matrix = {m}");
1367        Ok(())
1368    }
1369
1370    #[test]
1371    fn test_relate_line_inside_polygon() -> TestResult {
1372        let poly = make_rect(0.0, 0.0, 10.0, 10.0)?;
1373        let line_coords = vec![Coordinate::new_2d(2.0, 5.0), Coordinate::new_2d(8.0, 5.0)];
1374        let line = LineString::new(line_coords).map_err(AlgorithmError::Core)?;
1375        let m = relate_line_polygon(&line, &poly)?;
1376        assert!(m.is_within(), "line inside polygon: matrix = {m}");
1377        Ok(())
1378    }
1379
1380    #[test]
1381    fn test_relate_line_outside_polygon() -> TestResult {
1382        let poly = make_rect(0.0, 0.0, 10.0, 10.0)?;
1383        let line_coords = vec![
1384            Coordinate::new_2d(20.0, 20.0),
1385            Coordinate::new_2d(30.0, 30.0),
1386        ];
1387        let line = LineString::new(line_coords).map_err(AlgorithmError::Core)?;
1388        let m = relate_line_polygon(&line, &poly)?;
1389        assert!(m.is_disjoint(), "line outside polygon: matrix = {m}");
1390        Ok(())
1391    }
1392
1393    // =======================================================================
1394    // Predicate trait tests
1395    // =======================================================================
1396
1397    #[test]
1398    fn test_equals_predicate_polygon() -> TestResult {
1399        let a = make_rect(0.0, 0.0, 4.0, 4.0)?;
1400        let b = make_rect(0.0, 0.0, 4.0, 4.0)?;
1401        assert!(a.equals_topo(&b)?);
1402        let c = make_rect(1.0, 1.0, 5.0, 5.0)?;
1403        assert!(!a.equals_topo(&c)?);
1404        Ok(())
1405    }
1406
1407    #[test]
1408    fn test_covers_predicate_polygon() -> TestResult {
1409        let a = make_rect(0.0, 0.0, 10.0, 10.0)?;
1410        let b = make_rect(2.0, 2.0, 8.0, 8.0)?;
1411        assert!(a.covers(&b)?);
1412        assert!(!b.covers(&a)?);
1413        Ok(())
1414    }
1415
1416    #[test]
1417    fn test_covered_by_predicate_polygon() -> TestResult {
1418        let a = make_rect(2.0, 2.0, 8.0, 8.0)?;
1419        let b = make_rect(0.0, 0.0, 10.0, 10.0)?;
1420        assert!(a.covered_by(&b)?);
1421        assert!(!b.covered_by(&a)?);
1422        Ok(())
1423    }
1424
1425    #[test]
1426    fn test_equals_predicate_point() -> TestResult {
1427        let a = Point::new(1.0, 2.0);
1428        let b = Point::new(1.0, 2.0);
1429        let c = Point::new(3.0, 4.0);
1430        assert!(a.equals_topo(&b)?);
1431        assert!(!a.equals_topo(&c)?);
1432        Ok(())
1433    }
1434
1435    // =======================================================================
1436    // Pattern matching round-trip
1437    // =======================================================================
1438
1439    #[test]
1440    fn test_pattern_matching_roundtrip() -> TestResult {
1441        let patterns = [
1442            "T*F**FFF*", // equals
1443            "FF*FF****", // disjoint
1444            "FT*******", // touches variant
1445            "T*T******", // crosses (line/polygon)
1446            "T*F**F***", // within
1447            "T*****FF*", // contains
1448            "T*T***T**", // overlaps (area/area)
1449        ];
1450        for pat in &patterns {
1451            let m = De9im::from_str(pat)?;
1452            assert!(
1453                m.matches(pat),
1454                "pattern {pat} should match itself, matrix = {m}"
1455            );
1456        }
1457        Ok(())
1458    }
1459
1460    // =======================================================================
1461    // Crosses fix: Polygon/Polygon
1462    // =======================================================================
1463
1464    #[test]
1465    fn test_polygon_polygon_crosses_always_false_via_de9im() -> TestResult {
1466        // Two overlapping polygons -- crosses is undefined for same-dim(2) geometries
1467        let a = make_rect(0.0, 0.0, 4.0, 4.0)?;
1468        let b = make_rect(2.0, 2.0, 6.0, 6.0)?;
1469        let m = relate_polygons(&a, &b)?;
1470        assert!(
1471            !m.is_crosses(2, 2),
1472            "polygon/polygon crosses must always be false per OGC"
1473        );
1474        Ok(())
1475    }
1476
1477    // =======================================================================
1478    // Edge cases
1479    // =======================================================================
1480
1481    #[test]
1482    fn test_relate_invalid_polygon() {
1483        // Polygon with too few coordinates
1484        let coords = vec![
1485            Coordinate::new_2d(0.0, 0.0),
1486            Coordinate::new_2d(1.0, 0.0),
1487            Coordinate::new_2d(0.0, 0.0),
1488        ];
1489        let ext = LineString::new(coords);
1490        // LineString::new may succeed with 3 coords, but Polygon::new should fail
1491        if let Ok(e) = ext {
1492            if let Ok(bad_poly) = Polygon::new(e, vec![]) {
1493                let good = make_rect(0.0, 0.0, 10.0, 10.0);
1494                if let Ok(g) = good {
1495                    let result = relate_polygons(&bad_poly, &g);
1496                    assert!(result.is_err());
1497                }
1498            }
1499        }
1500    }
1501
1502    #[test]
1503    fn test_relate_line_polygon_invalid_line() {
1504        let poly_result = make_rect(0.0, 0.0, 10.0, 10.0);
1505        if let Ok(poly) = poly_result {
1506            // Cannot create a LineString with 1 coord, so we test with the error path
1507            let coords = vec![Coordinate::new_2d(5.0, 5.0)];
1508            let line_result = LineString::new(coords);
1509            // LineString::new should fail with < 2 coords
1510            assert!(line_result.is_err());
1511        }
1512    }
1513
1514    #[test]
1515    fn test_dimension_display() {
1516        assert_eq!(format!("{}", Dimension::Empty), "F");
1517        assert_eq!(format!("{}", Dimension::Point), "0");
1518        assert_eq!(format!("{}", Dimension::Line), "1");
1519        assert_eq!(format!("{}", Dimension::Area), "2");
1520    }
1521}