toolbox_rs/
geometry.rs

1pub mod primitives {
2    use std::fmt::Display;
3
4    use serde::{Deserialize, Serialize};
5
6    use crate::great_circle::distance::haversine;
7
8    #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
9    pub struct FPCoordinate {
10        pub lat: i32,
11        pub lon: i32,
12    }
13
14    impl FPCoordinate {
15        pub const fn new(lat: i32, lon: i32) -> Self {
16            Self { lat, lon }
17        }
18
19        pub const fn min() -> Self {
20            Self {
21                lat: i32::MIN,
22                lon: i32::MIN,
23            }
24        }
25
26        pub const fn max() -> Self {
27            Self {
28                lat: i32::MAX,
29                lon: i32::MAX,
30            }
31        }
32
33        pub fn new_from_lat_lon(lat: f64, lon: f64) -> Self {
34            let lat = (lat * 1000000.) as i32;
35            let lon = (lon * 1000000.) as i32;
36
37            Self { lat, lon }
38        }
39
40        pub fn to_lon_lat_pair(&self) -> (f64, f64) {
41            (self.lon as f64 / 1000000., self.lat as f64 / 1000000.)
42        }
43
44        pub fn to_lon_lat_vec(&self) -> Vec<f64> {
45            let (lon, lat) = self.to_lon_lat_pair();
46            vec![lon, lat]
47        }
48    }
49
50    impl Display for FPCoordinate {
51        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52            write!(
53                f,
54                "{:.6}, {:.6}",
55                self.lat as f64 / 1000000.,
56                self.lon as f64 / 1000000.
57            )
58        }
59    }
60
61    pub const fn cross_product(o: &FPCoordinate, a: &FPCoordinate, b: &FPCoordinate) -> i64 {
62        // upcasting to i64 to avoid integer overflow
63        let first = (a.lon as i64 - o.lon as i64) * (b.lat as i64 - o.lat as i64);
64        let second = (a.lat as i64 - o.lat as i64) * (b.lon as i64 - o.lon as i64);
65        first - second
66    }
67
68    pub const fn is_clock_wise_turn(o: &FPCoordinate, a: &FPCoordinate, b: &FPCoordinate) -> bool {
69        // upcasting to i64 to avoid integer overflow
70        let first = (a.lon as i64 - o.lon as i64) * (b.lat as i64 - o.lat as i64);
71        let second = (a.lat as i64 - o.lat as i64) * (b.lon as i64 - o.lon as i64);
72        first > second
73    }
74
75    pub fn distance(first: &FPCoordinate, b: &FPCoordinate) -> f64 {
76        let (lona, lata) = first.to_lon_lat_pair();
77        let (lonb, latb) = b.to_lon_lat_pair();
78        haversine(lata, lona, latb, lonb)
79    }
80
81    #[derive(Clone, Copy, Debug, PartialEq)]
82    pub struct Point {
83        pub x: f64,
84        pub y: f64,
85    }
86
87    impl Point {
88        pub fn new() -> Self {
89            Point { x: 0., y: 0. }
90        }
91    }
92
93    impl Default for Point {
94        fn default() -> Self {
95            Self::new()
96        }
97    }
98
99    pub struct Segment(pub Point, pub Point);
100
101    pub fn distance_to_segment(point: Point, segment: Segment) -> (f64, Point) {
102        let mut dx = segment.1.x - segment.0.x;
103        let mut dy = segment.1.y - segment.0.y;
104
105        if (dx == 0.) && (dy == 0.) {
106            // It's a point not a line segment.
107            dx = point.x - segment.0.x;
108            dy = point.y - segment.0.y;
109            return ((dx * dx + dy * dy).sqrt(), segment.0);
110        }
111
112        // Calculate the t that minimizes the distance.
113        let t = ((point.x - segment.0.x) * dx + (point.y - segment.0.y) * dy) / (dx * dx + dy * dy);
114
115        // See if this represents one of the segment's
116        // end points or a point in the middle.
117        let closest;
118        if t < 0. {
119            closest = segment.0;
120            dx = point.x - segment.0.x;
121            dy = point.y - segment.0.y;
122        } else if t > 1. {
123            closest = Point {
124                x: segment.1.x,
125                y: segment.1.y,
126            };
127            dx = point.x - segment.1.x;
128            dy = point.y - segment.1.y;
129        } else {
130            closest = Point {
131                x: segment.0.x + t * dx,
132                y: segment.0.y + t * dy,
133            };
134            dx = point.x - closest.x;
135            dy = point.y - closest.y;
136        }
137
138        ((dx * dx + dy * dy).sqrt(), closest)
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    macro_rules! assert_delta {
145        ($x:expr, $y:expr, $d:expr) => {
146            if !($x - $y < $d || $y - $x < $d) {
147                panic!();
148            }
149        };
150    }
151
152    use super::primitives::{cross_product, distance, FPCoordinate};
153    use crate::geometry::primitives::{distance_to_segment, is_clock_wise_turn, Point, Segment};
154
155    #[test]
156    fn distance_one() {
157        let p = Point { x: 1., y: 2. };
158        let s = Segment(Point { x: 0., y: 0. }, Point { x: 0., y: 10. });
159        let (distance, location) = distance_to_segment(p, s);
160        assert_eq!(distance, 1.);
161        assert_eq!(location, Point { x: 0., y: 2. });
162    }
163
164    #[test]
165    fn distance_on_line() {
166        let p = Point { x: 1., y: 2. };
167        let s = Segment(Point { x: 0., y: 0. }, Point { x: 0., y: 0. });
168        let (distance, location) = distance_to_segment(p, s);
169        assert_eq!(distance, 2.23606797749979);
170        assert_eq!(location, Point { x: 0., y: 0. });
171    }
172
173    #[test]
174    fn cross_product_ex1() {
175        let o = FPCoordinate::new(1, 1);
176        let a = FPCoordinate::new(4, 5);
177        let b = FPCoordinate::new(5, 4);
178        assert_eq!(7, cross_product(&o, &a, &b))
179    }
180
181    #[test]
182    fn cross_product_ex2() {
183        let o = FPCoordinate::new(0, 0);
184        let a = FPCoordinate::new(7, 5);
185        let b = FPCoordinate::new(17, 13);
186        assert_eq!(-6, cross_product(&o, &a, &b))
187    }
188
189    #[test]
190    fn cross_product_ex3() {
191        let o = FPCoordinate::new(0, 0);
192        let a = FPCoordinate::new(2, 2);
193        let b = FPCoordinate::new(0, -3);
194        assert_eq!(6, cross_product(&o, &a, &b))
195    }
196
197    #[test]
198    fn clock_wise_turn() {
199        let o = FPCoordinate::new_from_lat_lon(33.376756, -114.990162);
200        let a = FPCoordinate::new_from_lat_lon(33.359699, -114.945064);
201        let b = FPCoordinate::new_from_lat_lon(33.412820, -114.943641);
202
203        let cp = cross_product(&o, &a, &b);
204        assert!(cp > 0);
205        assert!(is_clock_wise_turn(&o, &a, &b));
206    }
207
208    #[test]
209    fn println() {
210        let input = FPCoordinate::new_from_lat_lon(33.359699, -114.945064);
211        let output = format!("{input}");
212        assert_eq!(output, "33.359699, -114.945064");
213    }
214
215    #[test]
216    fn println_truncated() {
217        let input = FPCoordinate::new_from_lat_lon(33.359699123456789, -114.945064127454);
218        let output = format!("{input}");
219        assert_eq!(output, "33.359699, -114.945064");
220    }
221
222    #[test]
223    fn to_lon_lat_pair_vec_equivalent() {
224        let input = FPCoordinate::new_from_lat_lon(33.359699123456789, -114.945064127454);
225        let output1 = input.to_lon_lat_pair();
226        let output2 = input.to_lon_lat_vec();
227
228        assert_eq!(output1.0, output2[0]);
229        assert_eq!(output1.1, output2[1]);
230    }
231
232    #[test]
233    fn trivial_distance_equivalent() {
234        let ny = FPCoordinate::new_from_lat_lon(40.730610, -73.935242);
235        let sf = FPCoordinate::new_from_lat_lon(37.773972, -122.431297);
236        let distance = distance(&ny, &sf);
237
238        assert_delta!(distance, 4140.175105689902, 0.0000001);
239    }
240
241    #[test]
242    fn point_self_new_equivalent() {
243        let p1 = Point::default();
244        let p2 = Point::new();
245        assert_eq!(p1, p2);
246    }
247}