toolbox_rs/
bounding_box.rs

1use crate::geometry::FPCoordinate;
2
3#[derive(Clone, Copy, Debug, Eq, PartialEq)]
4pub struct BoundingBox {
5    min: FPCoordinate,
6    max: FPCoordinate,
7}
8
9impl BoundingBox {
10    pub fn from_coordinates(coordinates: &[FPCoordinate]) -> BoundingBox {
11        debug_assert!(!coordinates.is_empty());
12        let mut min_coordinate = FPCoordinate::max();
13        let mut max_coordinate = FPCoordinate::min();
14
15        coordinates.iter().for_each(|coordinate| {
16            min_coordinate.lat = min_coordinate.lat.min(coordinate.lat);
17            min_coordinate.lon = min_coordinate.lon.min(coordinate.lon);
18            max_coordinate.lat = max_coordinate.lat.max(coordinate.lat);
19            max_coordinate.lon = max_coordinate.lon.max(coordinate.lon);
20        });
21
22        BoundingBox {
23            min: min_coordinate,
24            max: max_coordinate,
25        }
26    }
27
28    pub fn invalid() -> BoundingBox {
29        BoundingBox {
30            min: FPCoordinate::max(),
31            max: FPCoordinate::min(),
32        }
33    }
34
35    pub fn extend_with(&mut self, other: &BoundingBox) {
36        self.min.lat = self.min.lat.min(other.min.lat);
37        self.min.lon = self.min.lon.min(other.min.lon);
38
39        self.max.lat = self.max.lat.max(other.max.lat);
40        self.max.lon = self.max.lon.max(other.max.lon);
41    }
42
43    pub fn center(&self) -> FPCoordinate {
44        debug_assert!(self.min.lat <= self.max.lat);
45        debug_assert!(self.min.lon <= self.max.lon);
46
47        let lat_diff = self.max.lat - self.min.lat;
48        let lon_diff = self.max.lon - self.min.lon;
49
50        FPCoordinate {
51            lat: self.min.lat + lat_diff / 2,
52            lon: self.min.lon + lon_diff / 2,
53        }
54    }
55
56    /// Tests if a coordinate lies within the bounding box
57    ///
58    /// A coordinate is considered inside if it lies within or on the boundaries
59    /// of the bounding box.
60    ///
61    /// # Arguments
62    /// * `coordinate` - The coordinate to test
63    ///
64    /// # Returns
65    /// `true` if the coordinate is inside or on the boundary, `false` otherwise
66    ///
67    /// # Examples
68    /// ```rust
69    /// use toolbox_rs::geometry::FPCoordinate;
70    /// use toolbox_rs::bounding_box::BoundingBox;
71    ///
72    /// let bbox = BoundingBox::from_coordinates(&[
73    ///     FPCoordinate::new(10, 10),
74    ///     FPCoordinate::new(20, 20),
75    /// ]);
76    ///
77    /// assert!(bbox.contains(&FPCoordinate::new(15, 15))); // inside
78    /// assert!(bbox.contains(&FPCoordinate::new(10, 10))); // on boundary
79    /// assert!(!bbox.contains(&FPCoordinate::new(5, 15))); // outside
80    /// ```
81    pub fn contains(&self, coordinate: &FPCoordinate) -> bool {
82        coordinate.lat >= self.min.lat
83            && coordinate.lat <= self.max.lat
84            && coordinate.lon >= self.min.lon
85            && coordinate.lon <= self.max.lon
86    }
87
88    /// Calculates the minimum distance from a coordinate to the bounding box
89    ///
90    /// If the coordinate lies inside the bounding box, the distance is 0.
91    /// Otherwise, it returns the shortest distance to any part of the box's boundary.
92    ///
93    /// # Arguments
94    /// * `coordinate` - The coordinate to measure distance to
95    ///
96    /// # Returns
97    /// The minimum distance in kilometers
98    ///
99    /// # Examples
100    /// ```rust
101    /// use toolbox_rs::geometry::FPCoordinate;
102    /// use toolbox_rs::bounding_box::BoundingBox;
103    ///
104    /// let bbox = BoundingBox::from_coordinates(&[
105    ///     FPCoordinate::new_from_lat_lon(50.0, 10.0),
106    ///     FPCoordinate::new_from_lat_lon(51.0, 11.0),
107    /// ]);
108    ///
109    /// // Point inside -> distance is 0
110    /// let inside = FPCoordinate::new_from_lat_lon(50.5, 10.5);
111    /// assert_eq!(bbox.min_distance(&inside), 0.0);
112    ///
113    /// // Point outside -> positive distance
114    /// let outside = FPCoordinate::new_from_lat_lon(49.0, 10.5);
115    /// assert!(bbox.min_distance(&outside) > 0.0);
116    /// ```
117    pub fn min_distance(&self, coordinate: &FPCoordinate) -> f64 {
118        if self.contains(coordinate) {
119            return 0.;
120        }
121
122        let c1 = self.max;
123        let c2 = self.min;
124        let c3 = FPCoordinate::new(c1.lat, c2.lon);
125        let c4 = FPCoordinate::new(c2.lat, c1.lon);
126
127        c1.distance_to(coordinate)
128            .min(c2.distance_to(coordinate))
129            .min(c3.distance_to(coordinate))
130            .min(c4.distance_to(coordinate))
131    }
132
133    pub fn is_valid(&self) -> bool {
134        self.min.lat <= self.max.lat && self.min.lon <= self.max.lon
135    }
136
137    pub fn from_coordinate(coordinate: &FPCoordinate) -> BoundingBox {
138        BoundingBox {
139            min: *coordinate,
140            max: *coordinate,
141        }
142    }
143}
144
145impl From<&BoundingBox> for geojson::Bbox {
146    fn from(bbox: &BoundingBox) -> geojson::Bbox {
147        let result = vec![
148            bbox.min.lon as f64 / 1000000.,
149            bbox.min.lat as f64 / 1000000.,
150            bbox.max.lon as f64 / 1000000.,
151            bbox.max.lat as f64 / 1000000.,
152        ];
153        result
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use crate::{bounding_box::BoundingBox, geometry::FPCoordinate};
160
161    #[test]
162    fn grid() {
163        let mut coordinates: Vec<FPCoordinate> = Vec::new();
164        for i in 0..100 {
165            coordinates.push(FPCoordinate::new(i / 10, i % 10));
166        }
167
168        let expected = BoundingBox {
169            min: FPCoordinate::new(0, 0),
170            max: FPCoordinate::new(9, 9),
171        };
172        assert!(expected.is_valid());
173        let result = BoundingBox::from_coordinates(&coordinates);
174        assert_eq!(expected, result);
175    }
176
177    #[test]
178    fn center() {
179        let bbox = BoundingBox {
180            min: FPCoordinate::new_from_lat_lon(33.406637, -115.000801),
181            max: FPCoordinate::new_from_lat_lon(33.424732, -114.905286),
182        };
183        assert!(bbox.is_valid());
184        let center = bbox.center();
185        assert_eq!(center, FPCoordinate::new(33415684, -114953044));
186    }
187
188    #[test]
189    fn center_with_rounding() {
190        let bbox = BoundingBox {
191            min: FPCoordinate::new(0, 0),
192            max: FPCoordinate::new(9, 9),
193        };
194        assert!(bbox.is_valid());
195        let center = bbox.center();
196        assert_eq!(center, FPCoordinate::new(4, 4));
197    }
198
199    #[test]
200    fn center_without_rounding() {
201        let bbox = BoundingBox {
202            min: FPCoordinate::new(0, 0),
203            max: FPCoordinate::new(100, 100),
204        };
205        assert!(bbox.is_valid());
206        let center = bbox.center();
207        assert_eq!(center, FPCoordinate::new(50, 50));
208    }
209
210    #[test]
211    fn invalid() {
212        let bbox = BoundingBox::invalid();
213        assert!(bbox.min.lat > bbox.max.lat);
214        assert!(bbox.min.lon > bbox.max.lon);
215    }
216
217    #[test]
218    fn extend_with_extend_invalid() {
219        let mut c1 = BoundingBox::invalid();
220        let c2 =
221            BoundingBox::from_coordinates(&[FPCoordinate::new(11, 50), FPCoordinate::new(50, 37)]);
222        c1.extend_with(&c2);
223        assert!(c1.is_valid());
224
225        assert_eq!(c2.min, FPCoordinate::new(11, 37));
226        assert_eq!(c2.max, FPCoordinate::new(50, 50));
227    }
228
229    #[test]
230    fn extend_with_merge_two_valid() {
231        let mut b1 =
232            BoundingBox::from_coordinates(&[FPCoordinate::new(10, 10), FPCoordinate::new(20, 20)]);
233
234        let b2 =
235            BoundingBox::from_coordinates(&[FPCoordinate::new(15, 15), FPCoordinate::new(25, 25)]);
236
237        b1.extend_with(&b2);
238
239        assert_eq!(b1.min, FPCoordinate::new(10, 10));
240        assert_eq!(b1.max, FPCoordinate::new(25, 25));
241
242        println!("{:?}", b1);
243
244        assert!(b1.is_valid());
245    }
246
247    #[test]
248    fn geojson_conversion() {
249        let b1 =
250            BoundingBox::from_coordinates(&[FPCoordinate::new(11, 50), FPCoordinate::new(50, 37)]);
251        let g1 = geojson::Bbox::from(&b1);
252        assert_eq!(4, g1.len());
253
254        assert_eq!(b1.min.lon as f64 / 1000000., g1[0]);
255        assert_eq!(b1.min.lat as f64 / 1000000., g1[1]);
256        assert_eq!(b1.max.lon as f64 / 1000000., g1[2]);
257        assert_eq!(b1.max.lat as f64 / 1000000., g1[3]);
258    }
259
260    #[test]
261    fn extend_with_longitude_extension() {
262        let mut b1 = BoundingBox::from_coordinates(&[
263            FPCoordinate::new(10, -20), // lat=10, lon=-20
264            FPCoordinate::new(15, -10), // lat=15, lon=-10
265        ]);
266
267        let b2 = BoundingBox::from_coordinates(&[
268            FPCoordinate::new(12, 0),  // lat=12, lon=0
269            FPCoordinate::new(14, 10), // lat=14, lon=10
270        ]);
271
272        // Initial checks
273        assert_eq!(b1.max.lon, -10);
274
275        // Extend b1 with b2
276        b1.extend_with(&b2);
277
278        // Verify longitude extension
279        assert_eq!(b1.min.lon, -20); // Should keep original western boundary
280        assert_eq!(b1.max.lon, 10); // Should extend eastern boundary
281
282        assert!(b1.is_valid());
283    }
284
285    #[test]
286    fn test_contains() {
287        let bbox =
288            BoundingBox::from_coordinates(&[FPCoordinate::new(10, 10), FPCoordinate::new(20, 20)]);
289
290        // Test points inside the bounding box
291        assert!(bbox.contains(&FPCoordinate::new(15, 15)));
292        assert!(bbox.contains(&FPCoordinate::new(10, 10))); // boundary
293        assert!(bbox.contains(&FPCoordinate::new(20, 20))); // boundary
294
295        // Test points outside the bounding box
296        assert!(!bbox.contains(&FPCoordinate::new(9, 15))); // west
297        assert!(!bbox.contains(&FPCoordinate::new(21, 15))); // east
298        assert!(!bbox.contains(&FPCoordinate::new(15, 9))); // south
299        assert!(!bbox.contains(&FPCoordinate::new(15, 21))); // north
300    }
301
302    #[test]
303    fn test_min_distance() {
304        let bbox =
305            BoundingBox::from_coordinates(&[FPCoordinate::new(10, 10), FPCoordinate::new(20, 20)]);
306
307        // Test point inside -> distance should be 0
308        assert_eq!(bbox.min_distance(&FPCoordinate::new(15, 15)), 0.0);
309
310        // Test points outside
311        let corner_point = FPCoordinate::new(10, 10);
312        let distance_to_corner = bbox.min_distance(&FPCoordinate::new(5, 5));
313        assert!(distance_to_corner > 0.0);
314        assert_eq!(
315            distance_to_corner,
316            corner_point.distance_to(&FPCoordinate::new(5, 5))
317        );
318
319        // Test point directly east of box
320        let east_point = FPCoordinate::new(15, 25);
321        let distance_east = bbox.min_distance(&east_point);
322        assert!(distance_east > 0.0);
323        assert_eq!(
324            distance_east,
325            FPCoordinate::new(20, 20).distance_to(&east_point)
326        );
327    }
328
329    #[test]
330    fn test_from_coordinate() {
331        let coord = FPCoordinate::new(15, 25);
332        let bbox = BoundingBox::from_coordinate(&coord);
333
334        assert_eq!(bbox.min, coord);
335        assert_eq!(bbox.max, coord);
336        assert!(bbox.is_valid());
337        assert!(bbox.contains(&coord));
338        assert_eq!(bbox.min_distance(&coord), 0.0);
339    }
340}