Skip to main content

nodedb_spatial/predicates/
edge.rs

1//! Low-level segment and edge geometry primitives.
2//!
3//! Used by ST_Contains, ST_Intersects, and ST_Distance. All operations
4//! are planar (coordinate-space), not spherical. This is correct for the
5//! small-area geometries typical of spatial predicates (city blocks, not
6//! hemispheres). For large-area work, haversine handles the globe.
7
8/// Orientation of three points (collinear, clockwise, counter-clockwise).
9/// Uses the cross product of vectors (p→q) and (p→r).
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum Orientation {
12    Collinear,
13    Clockwise,
14    CounterClockwise,
15}
16
17/// Compute orientation of ordered triplet (p, q, r).
18pub fn orientation(p: [f64; 2], q: [f64; 2], r: [f64; 2]) -> Orientation {
19    let val = (q[1] - p[1]) * (r[0] - q[0]) - (q[0] - p[0]) * (r[1] - q[1]);
20    // Use epsilon for floating-point tolerance.
21    if val.abs() < 1e-12 {
22        Orientation::Collinear
23    } else if val > 0.0 {
24        Orientation::Clockwise
25    } else {
26        Orientation::CounterClockwise
27    }
28}
29
30/// Whether point q lies on segment p-r (given that p, q, r are collinear).
31pub fn on_segment(p: [f64; 2], q: [f64; 2], r: [f64; 2]) -> bool {
32    q[0] <= p[0].max(r[0])
33        && q[0] >= p[0].min(r[0])
34        && q[1] <= p[1].max(r[1])
35        && q[1] >= p[1].min(r[1])
36}
37
38/// Whether two segments (p1-q1) and (p2-q2) intersect.
39///
40/// Uses the standard orientation-based algorithm. Handles collinear
41/// overlapping segments correctly.
42pub fn segments_intersect(p1: [f64; 2], q1: [f64; 2], p2: [f64; 2], q2: [f64; 2]) -> bool {
43    let o1 = orientation(p1, q1, p2);
44    let o2 = orientation(p1, q1, q2);
45    let o3 = orientation(p2, q2, p1);
46    let o4 = orientation(p2, q2, q1);
47
48    // General case: different orientations mean crossing.
49    if o1 != o2 && o3 != o4 {
50        return true;
51    }
52
53    // Collinear special cases: check if endpoints lie on the other segment.
54    if o1 == Orientation::Collinear && on_segment(p1, p2, q1) {
55        return true;
56    }
57    if o2 == Orientation::Collinear && on_segment(p1, q2, q1) {
58        return true;
59    }
60    if o3 == Orientation::Collinear && on_segment(p2, p1, q2) {
61        return true;
62    }
63    if o4 == Orientation::Collinear && on_segment(p2, q1, q2) {
64        return true;
65    }
66
67    false
68}
69
70/// Whether a point lies exactly on a line segment (within epsilon tolerance).
71pub fn point_on_segment(pt: [f64; 2], seg_a: [f64; 2], seg_b: [f64; 2]) -> bool {
72    // Check collinearity via cross product.
73    let cross =
74        (pt[0] - seg_a[0]) * (seg_b[1] - seg_a[1]) - (pt[1] - seg_a[1]) * (seg_b[0] - seg_a[0]);
75    if cross.abs() > 1e-10 {
76        return false;
77    }
78    // Check that pt is within the segment's bounding box.
79    pt[0] >= seg_a[0].min(seg_b[0]) - 1e-10
80        && pt[0] <= seg_a[0].max(seg_b[0]) + 1e-10
81        && pt[1] >= seg_a[1].min(seg_b[1]) - 1e-10
82        && pt[1] <= seg_a[1].max(seg_b[1]) + 1e-10
83}
84
85/// Whether a point lies on any edge of a polygon ring.
86pub fn point_on_ring_boundary(pt: [f64; 2], ring: &[[f64; 2]]) -> bool {
87    if ring.len() < 2 {
88        return false;
89    }
90    for i in 0..ring.len() - 1 {
91        if point_on_segment(pt, ring[i], ring[i + 1]) {
92            return true;
93        }
94    }
95    // Check closing segment if ring isn't explicitly closed.
96    if ring.first() != ring.last()
97        && let (Some(&first), Some(&last)) = (ring.first(), ring.last())
98        && point_on_segment(pt, last, first)
99    {
100        return true;
101    }
102    false
103}
104
105/// Minimum squared distance from a point to a line segment.
106///
107/// Returns the squared distance (avoid sqrt for comparison purposes).
108pub fn point_to_segment_dist_sq(pt: [f64; 2], seg_a: [f64; 2], seg_b: [f64; 2]) -> f64 {
109    let dx = seg_b[0] - seg_a[0];
110    let dy = seg_b[1] - seg_a[1];
111    let len_sq = dx * dx + dy * dy;
112
113    if len_sq < 1e-20 {
114        // Degenerate segment (zero length) — distance to point.
115        let ex = pt[0] - seg_a[0];
116        let ey = pt[1] - seg_a[1];
117        return ex * ex + ey * ey;
118    }
119
120    // Project pt onto the line, clamped to [0, 1].
121    let t = ((pt[0] - seg_a[0]) * dx + (pt[1] - seg_a[1]) * dy) / len_sq;
122    let t = t.clamp(0.0, 1.0);
123
124    let proj_x = seg_a[0] + t * dx;
125    let proj_y = seg_a[1] + t * dy;
126
127    let ex = pt[0] - proj_x;
128    let ey = pt[1] - proj_y;
129    ex * ex + ey * ey
130}
131
132/// Minimum squared distance between two line segments.
133pub fn segment_to_segment_dist_sq(a1: [f64; 2], a2: [f64; 2], b1: [f64; 2], b2: [f64; 2]) -> f64 {
134    if segments_intersect(a1, a2, b1, b2) {
135        return 0.0;
136    }
137    // Min of all endpoint-to-segment distances.
138    let d1 = point_to_segment_dist_sq(a1, b1, b2);
139    let d2 = point_to_segment_dist_sq(a2, b1, b2);
140    let d3 = point_to_segment_dist_sq(b1, a1, a2);
141    let d4 = point_to_segment_dist_sq(b2, a1, a2);
142    d1.min(d2).min(d3).min(d4)
143}
144
145/// Extract all edges from a polygon ring as segment pairs.
146pub fn ring_edges(ring: &[[f64; 2]]) -> Vec<([f64; 2], [f64; 2])> {
147    if ring.len() < 2 {
148        return Vec::new();
149    }
150    let mut edges = Vec::with_capacity(ring.len());
151    for i in 0..ring.len() - 1 {
152        edges.push((ring[i], ring[i + 1]));
153    }
154    // Close the ring if not explicitly closed.
155    if ring.first() != ring.last()
156        && let (Some(&first), Some(&last)) = (ring.first(), ring.last())
157    {
158        edges.push((last, first));
159    }
160    edges
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn segments_cross() {
169        assert!(segments_intersect(
170            [0.0, 0.0],
171            [10.0, 10.0],
172            [0.0, 10.0],
173            [10.0, 0.0]
174        ));
175    }
176
177    #[test]
178    fn segments_parallel_no_cross() {
179        assert!(!segments_intersect(
180            [0.0, 0.0],
181            [10.0, 0.0],
182            [0.0, 1.0],
183            [10.0, 1.0]
184        ));
185    }
186
187    #[test]
188    fn segments_collinear_overlap() {
189        assert!(segments_intersect(
190            [0.0, 0.0],
191            [5.0, 0.0],
192            [3.0, 0.0],
193            [8.0, 0.0]
194        ));
195    }
196
197    #[test]
198    fn segments_collinear_no_overlap() {
199        assert!(!segments_intersect(
200            [0.0, 0.0],
201            [2.0, 0.0],
202            [3.0, 0.0],
203            [5.0, 0.0]
204        ));
205    }
206
207    #[test]
208    fn segments_endpoint_touch() {
209        assert!(segments_intersect(
210            [0.0, 0.0],
211            [5.0, 5.0],
212            [5.0, 5.0],
213            [10.0, 0.0]
214        ));
215    }
216
217    #[test]
218    fn point_on_segment_middle() {
219        assert!(point_on_segment([5.0, 5.0], [0.0, 0.0], [10.0, 10.0]));
220    }
221
222    #[test]
223    fn point_on_segment_endpoint() {
224        assert!(point_on_segment([0.0, 0.0], [0.0, 0.0], [10.0, 10.0]));
225    }
226
227    #[test]
228    fn point_off_segment() {
229        assert!(!point_on_segment([5.0, 6.0], [0.0, 0.0], [10.0, 10.0]));
230    }
231
232    #[test]
233    fn point_on_ring_edge() {
234        let ring = vec![
235            [0.0, 0.0],
236            [10.0, 0.0],
237            [10.0, 10.0],
238            [0.0, 10.0],
239            [0.0, 0.0],
240        ];
241        assert!(point_on_ring_boundary([5.0, 0.0], &ring)); // bottom edge
242        assert!(point_on_ring_boundary([0.0, 5.0], &ring)); // left edge
243        assert!(!point_on_ring_boundary([5.0, 5.0], &ring)); // interior
244    }
245
246    #[test]
247    fn point_on_ring_vertex() {
248        let ring = vec![
249            [0.0, 0.0],
250            [10.0, 0.0],
251            [10.0, 10.0],
252            [0.0, 10.0],
253            [0.0, 0.0],
254        ];
255        assert!(point_on_ring_boundary([0.0, 0.0], &ring));
256        assert!(point_on_ring_boundary([10.0, 10.0], &ring));
257    }
258
259    #[test]
260    fn point_to_segment_perpendicular() {
261        let d = point_to_segment_dist_sq([5.0, 1.0], [0.0, 0.0], [10.0, 0.0]);
262        assert!((d - 1.0).abs() < 1e-10); // 1 unit away
263    }
264
265    #[test]
266    fn point_to_segment_endpoint() {
267        let d = point_to_segment_dist_sq([12.0, 0.0], [0.0, 0.0], [10.0, 0.0]);
268        assert!((d - 4.0).abs() < 1e-10); // 2 units past endpoint
269    }
270
271    #[test]
272    fn segment_to_segment_parallel() {
273        let d = segment_to_segment_dist_sq([0.0, 0.0], [10.0, 0.0], [0.0, 3.0], [10.0, 3.0]);
274        assert!((d - 9.0).abs() < 1e-10);
275    }
276
277    #[test]
278    fn segment_to_segment_crossing() {
279        let d = segment_to_segment_dist_sq([0.0, 0.0], [10.0, 10.0], [0.0, 10.0], [10.0, 0.0]);
280        assert!(d < 1e-10);
281    }
282
283    #[test]
284    fn ring_edges_closed() {
285        let ring = vec![
286            [0.0, 0.0],
287            [10.0, 0.0],
288            [10.0, 10.0],
289            [0.0, 10.0],
290            [0.0, 0.0],
291        ];
292        let edges = ring_edges(&ring);
293        assert_eq!(edges.len(), 4);
294    }
295}