toolbox_rs/
geometry.rs

1use std::fmt::Display;
2
3/// A fixed-point coordinate representation for geographical locations.
4///
5/// This struct stores latitude and longitude as fixed-point numbers
6/// (integers multiplied by 1,000,000) to avoid floating-point precision issues
7/// while maintaining 6 decimal places of precision.
8///
9/// # Representation
10/// - `lat`: Latitude in millionths of a degree (-90.000000 to +90.000000)
11/// - `lon`: Longitude in millionths of a degree (-180.000000 to +180.000000)
12///
13/// # Examples
14///
15/// ```
16/// use toolbox_rs::geometry::FPCoordinate;
17///
18/// // Create from raw fixed-point values
19/// let coord = FPCoordinate::new(40730610, -73935242);
20///
21/// // Create from floating-point degrees
22/// let ny = FPCoordinate::new_from_lat_lon(40.730610, -73.935242);
23///
24/// // Convert back to floating-point
25/// let (lon, lat) = ny.to_lon_lat_pair();
26/// ```
27#[derive(Clone, Copy, Debug, Eq, PartialEq, bincode::Decode, bincode::Encode)]
28pub struct FPCoordinate {
29    pub lat: i32,
30    pub lon: i32,
31}
32
33impl FPCoordinate {
34    pub const fn new(lat: i32, lon: i32) -> Self {
35        Self { lat, lon }
36    }
37
38    pub const fn min() -> Self {
39        Self {
40            lat: i32::MIN,
41            lon: i32::MIN,
42        }
43    }
44
45    pub const fn max() -> Self {
46        Self {
47            lat: i32::MAX,
48            lon: i32::MAX,
49        }
50    }
51
52    pub fn new_from_lat_lon(lat: f64, lon: f64) -> Self {
53        let lat = (lat * 1000000.) as i32;
54        let lon = (lon * 1000000.) as i32;
55
56        Self { lat, lon }
57    }
58
59    pub fn to_lon_lat_pair(&self) -> (f64, f64) {
60        (self.lon as f64 / 1000000., self.lat as f64 / 1000000.)
61    }
62
63    pub fn distance(first: &FPCoordinate, second: &FPCoordinate) -> f64 {
64        let (lona, lata) = first.to_lon_lat_pair();
65        let (lonb, latb) = second.to_lon_lat_pair();
66        crate::great_circle::haversine(lata, lona, latb, lonb)
67    }
68    pub fn to_lon_lat_vec(&self) -> Vec<f64> {
69        let (lon, lat) = self.to_lon_lat_pair();
70        vec![lon, lat]
71    }
72
73    /// Calculates the great circle distance between two coordinates using the Haversine formula
74    ///
75    /// The distance is returned in kilometers.
76    ///
77    /// # Examples
78    ///
79    /// ```
80    /// use toolbox_rs::geometry::FPCoordinate;
81    ///
82    /// // Distance between New York and San Francisco
83    /// let ny = FPCoordinate::new_from_lat_lon(40.730610, -73.935242);
84    /// let sf = FPCoordinate::new_from_lat_lon(37.773972, -122.431297);
85    ///
86    /// let distance = ny.distance_to(&sf);
87    /// assert!((distance - 4140.175).abs() < 0.001); // ~4140 km
88    ///
89    /// // Distance to self is always 0
90    /// assert_eq!(ny.distance_to(&ny), 0.0);
91    /// ```
92    pub fn distance_to(&self, other: &FPCoordinate) -> f64 {
93        distance(self, other)
94    }
95}
96
97#[derive(Clone, Copy, Debug, PartialEq)]
98pub struct Point2D {
99    pub x: f64,
100    pub y: f64,
101}
102
103impl Point2D {
104    pub fn new() -> Self {
105        Point2D { x: 0., y: 0. }
106    }
107}
108
109impl Default for Point2D {
110    fn default() -> Self {
111        Self::new()
112    }
113}
114
115pub struct Segment(pub Point2D, pub Point2D);
116
117pub fn distance_to_segment_2d(point: &Point2D, segment: &Segment) -> (f64, Point2D) {
118    let mut dx = segment.1.x - segment.0.x;
119    let mut dy = segment.1.y - segment.0.y;
120
121    if (dx == 0.) && (dy == 0.) {
122        // It's a point not a line segment.
123        dx = point.x - segment.0.x;
124        dy = point.y - segment.0.y;
125        return ((dx * dx + dy * dy).sqrt(), segment.0);
126    }
127
128    // Calculate the t that minimizes the distance.
129    let t = ((point.x - segment.0.x) * dx + (point.y - segment.0.y) * dy) / (dx * dx + dy * dy);
130
131    // See if this represents one of the segment's
132    // end points or a point in the middle.
133    let closest;
134    if t < 0. {
135        closest = segment.0;
136        dx = point.x - segment.0.x;
137        dy = point.y - segment.0.y;
138    } else if t > 1. {
139        closest = Point2D {
140            x: segment.1.x,
141            y: segment.1.y,
142        };
143        dx = point.x - segment.1.x;
144        dy = point.y - segment.1.y;
145    } else {
146        closest = Point2D {
147            x: segment.0.x + t * dx,
148            y: segment.0.y + t * dy,
149        };
150        dx = point.x - closest.x;
151        dy = point.y - closest.y;
152    }
153
154    ((dx * dx + dy * dy).sqrt(), closest)
155}
156
157impl Display for FPCoordinate {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        write!(
160            f,
161            "{:.6}, {:.6}",
162            self.lat as f64 / 1000000.,
163            self.lon as f64 / 1000000.
164        )
165    }
166}
167
168pub const fn cross_product(o: &FPCoordinate, a: &FPCoordinate, b: &FPCoordinate) -> i64 {
169    // upcasting to i64 to avoid integer overflow
170    let first = (a.lon as i64 - o.lon as i64) * (b.lat as i64 - o.lat as i64);
171    let second = (a.lat as i64 - o.lat as i64) * (b.lon as i64 - o.lon as i64);
172    first - second
173}
174
175pub const fn is_clock_wise_turn(o: &FPCoordinate, a: &FPCoordinate, b: &FPCoordinate) -> bool {
176    // upcasting to i64 to avoid integer overflow
177    let first = (a.lon as i64 - o.lon as i64) * (b.lat as i64 - o.lat as i64);
178    let second = (a.lat as i64 - o.lat as i64) * (b.lon as i64 - o.lon as i64);
179    first > second
180}
181
182pub fn distance(first: &FPCoordinate, b: &FPCoordinate) -> f64 {
183    let (lona, lata) = first.to_lon_lat_pair();
184    let (lonb, latb) = b.to_lon_lat_pair();
185    crate::great_circle::haversine(lata, lona, latb, lonb)
186}
187
188/// A 2D point with integer coordinates, primarily used for TSP problems
189/// where distances need to be rounded to the nearest integer.
190#[derive(Clone, Copy, Debug, Eq, PartialEq)]
191pub struct IPoint2D {
192    pub x: i32,
193    pub y: i32,
194}
195
196impl IPoint2D {
197    pub fn new(x: i32, y: i32) -> Self {
198        Self { x, y }
199    }
200
201    /// Computes the Euclidean distance to another point, rounded to the nearest integer.
202    ///
203    /// This follows the TSPLIB standard for EUC_2D edge weight type:
204    /// dij = nint(sqrt(xd*xd + yd*yd)) where xd = xi-xj and yd = yi-yj
205    pub fn distance_to(&self, other: &IPoint2D) -> i32 {
206        let xd = self.x - other.x;
207        let yd = self.y - other.y;
208        let d = ((xd * xd + yd * yd) as f64).sqrt();
209        d.round() as i32
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    macro_rules! assert_delta {
216        ($x:expr, $y:expr, $d:expr) => {
217            if !($x - $y < $d || $y - $x < $d) {
218                panic!();
219            }
220        };
221    }
222
223    use super::{FPCoordinate, cross_product, distance};
224    use crate::geometry::{IPoint2D, Point2D, Segment, distance_to_segment_2d, is_clock_wise_turn};
225
226    #[test]
227    fn distance_one() {
228        let p = Point2D { x: 1., y: 2. };
229        let s = Segment(Point2D { x: 0., y: 0. }, Point2D { x: 0., y: 10. });
230        let (distance, location) = distance_to_segment_2d(&p, &s);
231        assert_eq!(distance, 1.);
232        assert_eq!(location, Point2D { x: 0., y: 2. });
233    }
234
235    #[test]
236    fn distance_on_line() {
237        let p = Point2D { x: 1., y: 2. };
238        let s = Segment(Point2D { x: 0., y: 0. }, Point2D { x: 0., y: 0. });
239        let (distance, location) = distance_to_segment_2d(&p, &s);
240        assert_eq!(distance, 2.23606797749979);
241        assert_eq!(location, Point2D { x: 0., y: 0. });
242    }
243
244    #[test]
245    fn distance_to_zero_length_segment() {
246        let point = Point2D { x: 3.0, y: 4.0 };
247        let segment_point = Point2D { x: 0.0, y: 0.0 };
248        let segment = Segment(segment_point, segment_point); // Zero-length segment (point)
249
250        let (distance, closest) = distance_to_segment_2d(&point, &segment);
251
252        // Expected distance is 5.0 (using Pythagorean theorem: sqrt(3² + 4²))
253        assert_eq!(distance, 5.0);
254        // Closest point should be the segment point
255        assert_eq!(closest, segment_point);
256    }
257
258    #[test]
259    fn distance_beyond_segment_end() {
260        let point = Point2D { x: 0.0, y: 3.0 };
261        let segment = Segment(Point2D { x: 0.0, y: 0.0 }, Point2D { x: 0.0, y: 2.0 });
262
263        let (distance, closest) = distance_to_segment_2d(&point, &segment);
264
265        // Point is 1 unit beyond the end of the segment
266        assert_eq!(distance, 1.0);
267        // Closest point should be the segment endpoint
268        assert_eq!(closest, segment.1);
269    }
270
271    #[test]
272    fn distance_beyond_diagonal_segment_end() {
273        let point = Point2D { x: 3.0, y: 3.0 };
274        let segment = Segment(Point2D { x: 0.0, y: 0.0 }, Point2D { x: 2.0, y: 2.0 });
275
276        let (distance, closest) = distance_to_segment_2d(&point, &segment);
277
278        // Point is (1,1) away from segment end at (2,2)
279        assert_delta!(distance, 2_f64.sqrt(), 0.000001);
280        // Closest point should be the segment endpoint
281        assert_eq!(closest, segment.1);
282    }
283
284    #[test]
285    fn distance_before_segment_start() {
286        let point = Point2D { x: -1.0, y: -1.0 };
287        let segment = Segment(Point2D { x: 0.0, y: 0.0 }, Point2D { x: 2.0, y: 2.0 });
288
289        let (distance, closest) = distance_to_segment_2d(&point, &segment);
290
291        // Point is (-1,-1) away from segment start at (0,0)
292        assert_delta!(distance, 2_f64.sqrt(), 0.000001);
293        // Closest point should be the segment start point
294        assert_eq!(closest, segment.0);
295    }
296
297    #[test]
298    fn cross_product_ex1() {
299        let o = FPCoordinate::new(1, 1);
300        let a = FPCoordinate::new(4, 5);
301        let b = FPCoordinate::new(5, 4);
302        assert_eq!(7, cross_product(&o, &a, &b))
303    }
304
305    #[test]
306    fn cross_product_ex2() {
307        let o = FPCoordinate::new(0, 0);
308        let a = FPCoordinate::new(7, 5);
309        let b = FPCoordinate::new(17, 13);
310        assert_eq!(-6, cross_product(&o, &a, &b))
311    }
312
313    #[test]
314    fn cross_product_ex3() {
315        let o = FPCoordinate::new(0, 0);
316        let a = FPCoordinate::new(2, 2);
317        let b = FPCoordinate::new(0, -3);
318        assert_eq!(6, cross_product(&o, &a, &b))
319    }
320
321    #[test]
322    fn clock_wise_turn() {
323        let o = FPCoordinate::new_from_lat_lon(33.376756, -114.990162);
324        let a = FPCoordinate::new_from_lat_lon(33.359699, -114.945064);
325        let b = FPCoordinate::new_from_lat_lon(33.412820, -114.943641);
326
327        let cp = cross_product(&o, &a, &b);
328        assert!(cp > 0);
329        assert!(is_clock_wise_turn(&o, &a, &b));
330    }
331
332    #[test]
333    fn println() {
334        let input = FPCoordinate::new_from_lat_lon(33.359699, -114.945064);
335        let output = format!("{input}");
336        assert_eq!(output, "33.359699, -114.945064");
337    }
338
339    #[test]
340    fn println_truncated() {
341        let input = FPCoordinate::new_from_lat_lon(33.359_699_123_456_79, -114.945064127454);
342        let output = format!("{input}");
343        assert_eq!(output, "33.359699, -114.945064");
344    }
345
346    #[test]
347    fn to_lon_lat_pair_vec_equivalent() {
348        let input = FPCoordinate::new_from_lat_lon(33.359_699_123_456_79, -114.945064127454);
349        let output1 = input.to_lon_lat_pair();
350        let output2 = input.to_lon_lat_vec();
351
352        assert_eq!(output1.0, output2[0]);
353        assert_eq!(output1.1, output2[1]);
354    }
355
356    #[test]
357    fn trivial_distance_equivalent() {
358        let ny = FPCoordinate::new_from_lat_lon(40.730610, -73.935242);
359        let sf = FPCoordinate::new_from_lat_lon(37.773972, -122.431297);
360        let distance = distance(&ny, &sf);
361
362        assert_delta!(distance, 4140.175105689902, 0.0000001);
363    }
364
365    #[test]
366    fn point_self_new_equivalent() {
367        let p1 = Point2D::default();
368        let p2 = Point2D::new();
369        assert_eq!(p1, p2);
370    }
371
372    #[test]
373    fn test_distance_to() {
374        let ny = FPCoordinate::new_from_lat_lon(40.730610, -73.935242); // New York
375        let sf = FPCoordinate::new_from_lat_lon(37.773972, -122.431297); // San Francisco
376
377        // Test symmetry
378        let d1 = ny.distance_to(&sf);
379        let d2 = sf.distance_to(&ny);
380        assert_eq!(d1, d2);
381
382        // Test known distance (approximately 4140km)
383        assert_delta!(d1, 4140.175105689902, 0.0000001);
384
385        // Test zero distance to self
386        assert_eq!(ny.distance_to(&ny), 0.0);
387    }
388
389    #[test]
390    fn test_coordinate_encoding() {
391        let original = FPCoordinate::new_from_lat_lon(40.730610, -73.935242);
392
393        // Encode to bytes
394        let encoded: Vec<u8> =
395            bincode::encode_to_vec(original, bincode::config::standard()).unwrap();
396
397        // Decode from bytes
398        let decoded: FPCoordinate =
399            bincode::decode_from_slice(&encoded, bincode::config::standard())
400                .unwrap()
401                .0;
402
403        // Verify the roundtrip
404        assert_eq!(original, decoded);
405        assert_eq!(original.lat, decoded.lat);
406        assert_eq!(original.lon, decoded.lon);
407
408        // Verify actual values preserved
409        let (lon, lat) = decoded.to_lon_lat_pair();
410        assert_delta!(lat, 40.730610, 0.000001);
411        assert_delta!(lon, -73.935242, 0.000001);
412    }
413
414    #[test]
415    fn test_ipoint2d_distance() {
416        // Test cases from TSPLIB documentation examples
417        let cases = [
418            ((0, 0), (3, 0), 3), // Horizontal line
419            ((0, 0), (0, 4), 4), // Vertical line
420            ((0, 0), (3, 4), 5), // 3-4-5 triangle
421            ((1, 1), (4, 5), 5), // Diagonal with rounding
422            ((2, 2), (5, 6), 5), // Another diagonal
423        ];
424
425        for ((x1, y1), (x2, y2), expected) in cases {
426            let p1 = IPoint2D::new(x1, y1);
427            let p2 = IPoint2D::new(x2, y2);
428            assert_eq!(p1.distance_to(&p2), expected);
429            // Test symmetry
430            assert_eq!(p2.distance_to(&p1), expected);
431        }
432    }
433}